Skip to content
Closed

chart #1397

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class ScNavigationMenuDemoContainer {
import {
ScNavigationMenu,
ScNavigationMenuContent,
ScNavigationMenuGrid,
ScNavigationMenuItem,
ScNavigationMenuLink,
ScNavigationMenuList,
Expand All @@ -40,6 +41,7 @@ import { SiSparklesIcon } from '@semantic-icons/lucide-icons';
imports: [
ScNavigationMenu,
ScNavigationMenuContent,
ScNavigationMenuGrid,
ScNavigationMenuItem,
ScNavigationMenuLink,
ScNavigationMenuList,
Expand Down Expand Up @@ -119,9 +121,7 @@ import { SiSparklesIcon } from '@semantic-icons/lucide-icons';
<button scNavigationMenuTrigger>Components</button>
<ng-template scNavigationMenuPortal>
<div scNavigationMenuContent>
<ul
class="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]"
>
<ul scNavigationMenuGrid>
<li>
<a scNavigationMenuLink href="#">
<div class="text-sm font-medium leading-none">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ import { ScHeading } from '@semantic-components/ui';
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeadingsTypographyDemo {};`;
export class HeadingsTypographyDemo {}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ import { ScHeading } from '@semantic-components/ui';
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UnderlineTypographyDemo {};`;
export class UnderlineTypographyDemo {}`;
}
49 changes: 27 additions & 22 deletions libs/ui-lab/src/lib/components/chart/bar-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ViewEncapsulation,
} from '@angular/core';
import { cn } from '@semantic-components/ui';
import { scaleBand, scaleLinear } from 'd3-scale';
import { CHART_COLORS, ChartDataPoint } from './chart-types';
import { SC_CHART } from './chart-container';

Expand Down Expand Up @@ -126,43 +127,47 @@ export class ScBarChart {
return Math.max(...values, 0) * 1.1; // Add 10% padding
});

private readonly xScale = computed(() =>
scaleBand<string>()
.domain(this.data().map((d) => d.label))
.range([this.padding().left, this.padding().left + this.chartWidth()])
.padding(this.barGap() / (this.chartWidth() / this.data().length || 1)),
);

private readonly yScale = computed(() =>
scaleLinear()
.domain([0, this.maxValue()])
.range([this.padding().top + this.chartHeight(), this.padding().top]),
);

protected readonly gridLines = computed(() => {
const max = this.maxValue();
const lines: { y: number; label: string }[] = [];
const steps = 5;

for (let i = 0; i <= steps; i++) {
const value = (max / steps) * i;
const y =
this.padding().top +
this.chartHeight() -
(value / max) * this.chartHeight();
lines.push({ y, label: Math.round(value).toString() });
}
const y = this.yScale();
const ticks = y.ticks(5);

return lines;
return ticks.map((value) => ({
y: y(value),
label: Math.round(value).toString(),
}));
});

protected readonly bars = computed(() => {
const data = this.data();
const barCount = data.length;
const totalGaps = (barCount - 1) * this.barGap();
const barWidth = (this.chartWidth() - totalGaps) / barCount;
const max = this.maxValue();
const x = this.xScale();
const y = this.yScale();
const baseline = y(0);

return data.map((d, i) => {
const barHeight = (d.value / max) * this.chartHeight();
const color =
d.color ||
this.container?.getColor(d.label, i) ||
CHART_COLORS[i % CHART_COLORS.length];

return {
...d,
x: this.padding().left + i * (barWidth + this.barGap()),
y: this.padding().top + this.chartHeight() - barHeight,
width: barWidth,
height: barHeight,
x: x(d.label)!,
y: y(d.value),
width: x.bandwidth(),
height: baseline - y(d.value),
color,
};
});
Expand Down
75 changes: 45 additions & 30 deletions libs/ui-lab/src/lib/components/chart/line-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ViewEncapsulation,
} from '@angular/core';
import { cn } from '@semantic-components/ui';
import { scaleLinear, scalePoint } from 'd3-scale';
import { area, line } from 'd3-shape';
import { CHART_COLORS, ChartDataPoint } from './chart-types';
import { SC_CHART } from './chart-container';

Expand Down Expand Up @@ -151,55 +153,68 @@ export class ScLineChart {
return Math.max(...values, 0) * 1.1;
});

private readonly xScale = computed(() =>
scalePoint<string>()
.domain(this.data().map((d) => d.label))
.range([this.padding().left, this.padding().left + this.chartWidth()]),
);

private readonly yScale = computed(() =>
scaleLinear()
.domain([0, this.maxValue()])
.range([this.padding().top + this.chartHeight(), this.padding().top]),
);

protected readonly gridLines = computed(() => {
const max = this.maxValue();
const lines: { y: number; label: string }[] = [];
const steps = 5;

for (let i = 0; i <= steps; i++) {
const value = (max / steps) * i;
const y =
this.padding().top +
this.chartHeight() -
(value / max) * this.chartHeight();
lines.push({ y, label: Math.round(value).toString() });
}
const y = this.yScale();
const ticks = y.ticks(5);

return lines;
return ticks.map((value) => ({
y: y(value),
label: Math.round(value).toString(),
}));
});

protected readonly points = computed(() => {
const data = this.data();
const max = this.maxValue();
const stepX = this.chartWidth() / Math.max(data.length - 1, 1);
const x = this.xScale();
const y = this.yScale();

return data.map((d, i) => ({
return data.map((d) => ({
...d,
x: this.padding().left + i * stepX,
y:
this.padding().top +
this.chartHeight() -
(d.value / max) * this.chartHeight(),
x: x(d.label)!,
y: y(d.value),
}));
});

protected readonly linePath = computed(() => {
const pts = this.points();
if (pts.length === 0) return '';
const data = this.data();
if (data.length === 0) return '';

const x = this.xScale();
const y = this.yScale();

const generator = line<ChartDataPoint>()
.x((d) => x(d.label)!)
.y((d) => y(d.value));

return pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
return generator(data) ?? '';
});

protected readonly areaPath = computed(() => {
const pts = this.points();
if (pts.length === 0) return '';
const data = this.data();
if (data.length === 0) return '';

const x = this.xScale();
const y = this.yScale();
const baseline = this.padding().top + this.chartHeight();
const linePart = pts
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
.join(' ');

return `${linePart} L ${pts[pts.length - 1].x} ${baseline} L ${pts[0].x} ${baseline} Z`;
const generator = area<ChartDataPoint>()
.x((d) => x(d.label)!)
.y0(baseline)
.y1((d) => y(d.value));

return generator(data) ?? '';
});

onPointHover(event: MouseEvent, point: ChartDataPoint): void {
Expand Down
122 changes: 53 additions & 69 deletions libs/ui-lab/src/lib/components/chart/pie-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ViewEncapsulation,
} from '@angular/core';
import { cn } from '@semantic-components/ui';
import { arc, pie } from 'd3-shape';
import { CHART_COLORS, ChartDataPoint } from './chart-types';
import { SC_CHART } from './chart-container';

Expand All @@ -20,28 +21,30 @@ import { SC_CHART } from './chart-container';
[style.height.px]="size()"
preserveAspectRatio="xMidYMid meet"
>
@for (slice of slices(); track slice.label; let i = $index) {
<path
[attr.d]="slice.path"
[attr.fill]="slice.color"
class="cursor-pointer transition-opacity hover:opacity-80"
(mouseenter)="onSliceHover($event, slice)"
(mouseleave)="onSliceLeave()"
/>
}

@if (showLabels()) {
@for (slice of slices(); track slice.label) {
<text
[attr.x]="slice.labelX"
[attr.y]="slice.labelY"
text-anchor="middle"
class="pointer-events-none fill-background text-xs font-medium"
>
{{ slice.percentage }}%
</text>
<g [attr.transform]="centerTransform()">
@for (slice of slices(); track slice.label; let i = $index) {
<path
[attr.d]="slice.path"
[attr.fill]="slice.color"
class="cursor-pointer transition-opacity hover:opacity-80"
(mouseenter)="onSliceHover($event, slice)"
(mouseleave)="onSliceLeave()"
/>
}
}

@if (showLabels()) {
@for (slice of slices(); track slice.label) {
<text
[attr.x]="slice.labelX"
[attr.y]="slice.labelY"
text-anchor="middle"
class="pointer-events-none fill-background text-xs font-medium"
>
{{ slice.percentage }}%
</text>
}
}
</g>
</svg>

@if (hoveredSlice()) {
Expand Down Expand Up @@ -92,78 +95,59 @@ export class ScPieChart {
);
protected readonly class = computed(() => cn('', this.classInput()));

protected readonly centerTransform = computed(
() => `translate(${this.size() / 2},${this.size() / 2})`,
);

protected readonly total = computed(() =>
this.data().reduce((sum, d) => sum + d.value, 0),
);

protected readonly slices = computed(() => {
const data = this.data();
const total = this.total();
const centerX = this.size() / 2;
const centerY = this.size() / 2;
const outerRadius = this.size() / 2 - 10;
const innerR = this.innerRadius();

let startAngle = -Math.PI / 2;
const slices: {
label: string;
value: number;
percentage: number;
path: string;
color: string;
labelX: number;
labelY: number;
}[] = [];

for (let i = 0; i < data.length; i++) {
const d = data[i];
const percentage = Math.round((d.value / total) * 100);
const angle = (d.value / total) * 2 * Math.PI;
const endAngle = startAngle + angle;
const pieGenerator = pie<ChartDataPoint>()
.value((d) => d.value)
.sortValues(null)
.startAngle(-Math.PI / 2)
.endAngle(-Math.PI / 2 + 2 * Math.PI);

const arcGenerator = arc<{ startAngle: number; endAngle: number }>()
.innerRadius(innerR)
.outerRadius(outerRadius);

const x1 = centerX + outerRadius * Math.cos(startAngle);
const y1 = centerY + outerRadius * Math.sin(startAngle);
const x2 = centerX + outerRadius * Math.cos(endAngle);
const y2 = centerY + outerRadius * Math.sin(endAngle);
const arcs = pieGenerator(data);

const largeArc = angle > Math.PI ? 1 : 0;
return arcs.map((a, i) => {
const d = a.data;
const percentage = Math.round((d.value / total) * 100);
const color =
d.color ||
this.container?.getColor(d.label, i) ||
CHART_COLORS[i % CHART_COLORS.length];

let path: string;
if (innerR > 0) {
const ix1 = centerX + innerR * Math.cos(startAngle);
const iy1 = centerY + innerR * Math.sin(startAngle);
const ix2 = centerX + innerR * Math.cos(endAngle);
const iy2 = centerY + innerR * Math.sin(endAngle);

path = `M ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2} ${y2} L ${ix2} ${iy2} A ${innerR} ${innerR} 0 ${largeArc} 0 ${ix1} ${iy1} Z`;
} else {
path = `M ${centerX} ${centerY} L ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
}

const labelAngle = startAngle + angle / 2;
const labelRadius =
innerR > 0 ? (outerRadius + innerR) / 2 : outerRadius * 0.6;
const labelX = centerX + labelRadius * Math.cos(labelAngle);
const labelY = centerY + labelRadius * Math.sin(labelAngle);

slices.push({
const path = arcGenerator({
startAngle: a.startAngle,
endAngle: a.endAngle,
})!;
const [labelX, labelY] = arcGenerator.centroid({
startAngle: a.startAngle,
endAngle: a.endAngle,
});

return {
label: d.label,
value: d.value,
percentage,
path,
color,
labelX,
labelY,
});

startAngle = endAngle;
}

return slices;
};
});
});

onSliceHover(
Expand Down
Loading