diff --git a/apps/showcase/src/app/pages/docs/typography/demos/headings-typography-demo-container.ts b/apps/showcase/src/app/pages/docs/typography/demos/headings-typography-demo-container.ts
index b681d6276..11191d84b 100644
--- a/apps/showcase/src/app/pages/docs/typography/demos/headings-typography-demo-container.ts
+++ b/apps/showcase/src/app/pages/docs/typography/demos/headings-typography-demo-container.ts
@@ -44,5 +44,5 @@ import { ScHeading } from '@semantic-components/ui';
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class HeadingsTypographyDemo {};`;
+export class HeadingsTypographyDemo {}`;
}
diff --git a/apps/showcase/src/app/pages/docs/typography/demos/underline-typography-demo-container.ts b/apps/showcase/src/app/pages/docs/typography/demos/underline-typography-demo-container.ts
index e64f7df87..3cda98290 100644
--- a/apps/showcase/src/app/pages/docs/typography/demos/underline-typography-demo-container.ts
+++ b/apps/showcase/src/app/pages/docs/typography/demos/underline-typography-demo-container.ts
@@ -44,5 +44,5 @@ import { ScHeading } from '@semantic-components/ui';
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class UnderlineTypographyDemo {};`;
+export class UnderlineTypographyDemo {}`;
}
diff --git a/libs/ui-lab/src/lib/components/chart/bar-chart.ts b/libs/ui-lab/src/lib/components/chart/bar-chart.ts
index d33fc6f96..e342b3778 100644
--- a/libs/ui-lab/src/lib/components/chart/bar-chart.ts
+++ b/libs/ui-lab/src/lib/components/chart/bar-chart.ts
@@ -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';
@@ -126,32 +127,36 @@ export class ScBarChart {
return Math.max(...values, 0) * 1.1; // Add 10% padding
});
+ private readonly xScale = computed(() =>
+ scaleBand
()
+ .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) ||
@@ -159,10 +164,10 @@ export class ScBarChart {
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,
};
});
diff --git a/libs/ui-lab/src/lib/components/chart/line-chart.ts b/libs/ui-lab/src/lib/components/chart/line-chart.ts
index dd77534e5..93f5d3b51 100644
--- a/libs/ui-lab/src/lib/components/chart/line-chart.ts
+++ b/libs/ui-lab/src/lib/components/chart/line-chart.ts
@@ -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';
@@ -151,55 +153,68 @@ export class ScLineChart {
return Math.max(...values, 0) * 1.1;
});
+ private readonly xScale = computed(() =>
+ scalePoint()
+ .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()
+ .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()
+ .x((d) => x(d.label)!)
+ .y0(baseline)
+ .y1((d) => y(d.value));
+
+ return generator(data) ?? '';
});
onPointHover(event: MouseEvent, point: ChartDataPoint): void {
diff --git a/libs/ui-lab/src/lib/components/chart/pie-chart.ts b/libs/ui-lab/src/lib/components/chart/pie-chart.ts
index 942bf0497..7b6a05448 100644
--- a/libs/ui-lab/src/lib/components/chart/pie-chart.ts
+++ b/libs/ui-lab/src/lib/components/chart/pie-chart.ts
@@ -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';
@@ -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) {
-
- }
-
- @if (showLabels()) {
- @for (slice of slices(); track slice.label) {
-
- {{ slice.percentage }}%
-
+
+ @for (slice of slices(); track slice.label; let i = $index) {
+
}
- }
+
+ @if (showLabels()) {
+ @for (slice of slices(); track slice.label) {
+
+ {{ slice.percentage }}%
+
+ }
+ }
+
@if (hoveredSlice()) {
@@ -92,6 +95,10 @@ 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),
);
@@ -99,58 +106,39 @@ export class ScPieChart {
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()
+ .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,
@@ -158,12 +146,8 @@ export class ScPieChart {
color,
labelX,
labelY,
- });
-
- startAngle = endAngle;
- }
-
- return slices;
+ };
+ });
});
onSliceHover(
diff --git a/package-lock.json b/package-lock.json
index 90a46d78e..00cb72dbe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,8 @@
"@tiptap/starter-kit": "^3.18.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.2.0",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"express": "^5.1.0",
@@ -57,6 +59,8 @@
"@swc/core": "1.15.8",
"@swc/helpers": "0.5.18",
"@tailwindcss/postcss": "^4.2.0",
+ "@types/d3-scale": "^4.0.9",
+ "@types/d3-shape": "^3.1.8",
"@types/express": "^5.0.1",
"@types/node": "^20.17.19",
"@typescript-eslint/utils": "^8.40.0",
@@ -871,24 +875,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/@angular/build/node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
"node_modules/@angular/cdk": {
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.5.tgz",
@@ -11374,6 +11360,40 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -15425,6 +15445,109 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -18628,6 +18751,15 @@
"tslib": "^2.0.0"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
@@ -19216,21 +19348,6 @@
}
}
},
- "node_modules/jsdom/node_modules/@noble/hashes": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
- "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">= 20.19.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
"node_modules/jsdom/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -28752,24 +28869,6 @@
}
}
},
- "node_modules/vitest/node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
diff --git a/package.json b/package.json
index 4200afdfa..fe233aad5 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,8 @@
"@tiptap/starter-kit": "^3.18.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.2.0",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"express": "^5.1.0",
@@ -58,6 +60,8 @@
"@swc/core": "1.15.8",
"@swc/helpers": "0.5.18",
"@tailwindcss/postcss": "^4.2.0",
+ "@types/d3-scale": "^4.0.9",
+ "@types/d3-shape": "^3.1.8",
"@types/express": "^5.0.1",
"@types/node": "^20.17.19",
"@typescript-eslint/utils": "^8.40.0",