@sentropic/design-system-svelte 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/BoxPlotChart.svelte +302 -0
  2. package/dist/BoxPlotChart.svelte.d.ts +40 -0
  3. package/dist/BoxPlotChart.svelte.d.ts.map +1 -0
  4. package/dist/BulletChart.svelte +479 -0
  5. package/dist/BulletChart.svelte.d.ts +32 -0
  6. package/dist/BulletChart.svelte.d.ts.map +1 -0
  7. package/dist/BumpChart.svelte +387 -0
  8. package/dist/BumpChart.svelte.d.ts +35 -0
  9. package/dist/BumpChart.svelte.d.ts.map +1 -0
  10. package/dist/CalendarHeatmapChart.svelte +345 -0
  11. package/dist/CalendarHeatmapChart.svelte.d.ts +28 -0
  12. package/dist/CalendarHeatmapChart.svelte.d.ts.map +1 -0
  13. package/dist/CandlestickChart.svelte +333 -0
  14. package/dist/CandlestickChart.svelte.d.ts +31 -0
  15. package/dist/CandlestickChart.svelte.d.ts.map +1 -0
  16. package/dist/HeatmapChart.svelte +337 -0
  17. package/dist/HeatmapChart.svelte.d.ts +35 -0
  18. package/dist/HeatmapChart.svelte.d.ts.map +1 -0
  19. package/dist/HistogramChart.svelte +294 -0
  20. package/dist/HistogramChart.svelte.d.ts +38 -0
  21. package/dist/HistogramChart.svelte.d.ts.map +1 -0
  22. package/dist/MarimekkoChart.svelte +319 -0
  23. package/dist/MarimekkoChart.svelte.d.ts +35 -0
  24. package/dist/MarimekkoChart.svelte.d.ts.map +1 -0
  25. package/dist/ParallelCoordinatesChart.svelte +315 -0
  26. package/dist/ParallelCoordinatesChart.svelte.d.ts +35 -0
  27. package/dist/ParallelCoordinatesChart.svelte.d.ts.map +1 -0
  28. package/dist/RadarChart.svelte +340 -0
  29. package/dist/RadarChart.svelte.d.ts +43 -0
  30. package/dist/RadarChart.svelte.d.ts.map +1 -0
  31. package/dist/SankeyChart.svelte +364 -0
  32. package/dist/SankeyChart.svelte.d.ts +45 -0
  33. package/dist/SankeyChart.svelte.d.ts.map +1 -0
  34. package/dist/SunburstChart.svelte +388 -0
  35. package/dist/SunburstChart.svelte.d.ts +39 -0
  36. package/dist/SunburstChart.svelte.d.ts.map +1 -0
  37. package/dist/chartContrast.d.ts +0 -4
  38. package/dist/chartContrast.d.ts.map +1 -1
  39. package/dist/chartContrast.js +4 -56
  40. package/dist/index.d.ts +24 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +12 -0
  43. package/package.json +1 -1
@@ -0,0 +1,315 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * ParallelCoordinatesChart - axes verticaux parallèles, polylignes par enregistrement.
4
+ * API canonique (référence Svelte, React/Vue doivent s'aligner)
5
+ *
6
+ * Props obligatoires :
7
+ * axes ParallelAxis[] - définition des axes {key, label, min?, max?}
8
+ * data Array<Record<string, unknown>> - enregistrements
9
+ * label string - aria-label
10
+ *
11
+ * Props optionnelles :
12
+ * tones tone par série (string[]) - one tone per datum row, cycled
13
+ * width number (défaut 480)
14
+ * height number (défaut 300)
15
+ * class string
16
+ */
17
+ export type ParallelCoordinatesChartTone =
18
+ | "category1"
19
+ | "category2"
20
+ | "category3"
21
+ | "category4"
22
+ | "category5"
23
+ | "category6"
24
+ | "category7"
25
+ | "category8";
26
+
27
+ export type ParallelAxis = {
28
+ key: string;
29
+ label: string;
30
+ min?: number;
31
+ max?: number;
32
+ };
33
+ </script>
34
+
35
+ <script lang="ts">
36
+ import ChartDataList from "./ChartDataList.svelte";
37
+
38
+ const TONES: ParallelCoordinatesChartTone[] = [
39
+ "category1","category2","category3","category4",
40
+ "category5","category6","category7","category8"
41
+ ];
42
+
43
+ type ParallelCoordinatesChartProps = {
44
+ axes: ParallelAxis[];
45
+ data: Record<string, unknown>[];
46
+ label: string;
47
+ // FIX #5 : renommage tone → tones (cohérence avec les autres charts)
48
+ tones?: ParallelCoordinatesChartTone[];
49
+ width?: number;
50
+ height?: number;
51
+ class?: string;
52
+ };
53
+
54
+ let {
55
+ axes = [],
56
+ data = [],
57
+ label,
58
+ tones,
59
+ width = 480,
60
+ height = 300,
61
+ class: className
62
+ }: ParallelCoordinatesChartProps = $props();
63
+
64
+ const MARGIN = { top: 32, right: 24, bottom: 16, left: 24 };
65
+
66
+ let hoveredIndex: number | null = $state(null);
67
+
68
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
69
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
70
+
71
+ // FIX #5 : domaines par axe explicites, sans coercion Number() silencieuse
72
+ function axisDomain(axis: ParallelAxis): { min: number; max: number } {
73
+ // Parse STRICT : seules les valeurs finies comptent
74
+ const vals = data
75
+ .map((d) => {
76
+ const raw = d[axis.key];
77
+ if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
78
+ if (typeof raw === "string" && raw !== "") {
79
+ const n = Number(raw);
80
+ return Number.isFinite(n) ? n : null;
81
+ }
82
+ return null;
83
+ })
84
+ .filter((v): v is number => v !== null);
85
+
86
+ const rawMin = vals.length > 0 ? Math.min(...vals) : 0;
87
+ const rawMax = vals.length > 0 ? Math.max(...vals) : 1;
88
+ const safeMax = rawMax === rawMin ? rawMin + 1 : rawMax;
89
+
90
+ return {
91
+ min: Number.isFinite(axis.min) ? (axis.min as number) : rawMin,
92
+ max: Number.isFinite(axis.max) ? (axis.max as number) : safeMax
93
+ };
94
+ }
95
+
96
+ const axisX = $derived(
97
+ axes.map((_, i) => MARGIN.left + (axes.length <= 1 ? plotWidth / 2 : (i / (axes.length - 1)) * plotWidth))
98
+ );
99
+
100
+ /**
101
+ * FIX #5 : parse STRICT d'une valeur de ligne.
102
+ * Retourne null si la valeur n'est pas finie → crée un GAP dans le path.
103
+ */
104
+ function parseStrictFinite(raw: unknown): number | null {
105
+ if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
106
+ if (typeof raw === "string" && raw !== "") {
107
+ const n = Number(raw);
108
+ return Number.isFinite(n) ? n : null;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Construit un path SVG avec GAP (M...L... / M...) pour les points null.
115
+ * Un segment contenant un point null ne sera pas tracé.
116
+ */
117
+ function buildPathWithGaps(points: ({ x: number; y: number } | null)[]): string {
118
+ const parts: string[] = [];
119
+ let segment: { x: number; y: number }[] = [];
120
+
121
+ for (const pt of points) {
122
+ if (pt === null) {
123
+ if (segment.length > 0) {
124
+ parts.push(segment.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" "));
125
+ segment = [];
126
+ }
127
+ } else {
128
+ segment.push(pt);
129
+ }
130
+ }
131
+ if (segment.length > 0) {
132
+ parts.push(segment.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" "));
133
+ }
134
+ return parts.join(" ");
135
+ }
136
+
137
+ const lines = $derived.by(() => {
138
+ return data.map((row, ri) => {
139
+ const seriesTones = tones ?? [];
140
+ const rowTone = seriesTones[ri] ?? TONES[ri % TONES.length];
141
+ const points: ({ x: number; y: number } | null)[] = axes.map((axis, ai) => {
142
+ const domain = axisDomain(axis);
143
+ // FIX #5 : parse strict → null si invalide
144
+ const val = parseStrictFinite(row[axis.key]);
145
+ if (val === null) return null; // GAP
146
+ // FIX #5 : clamp aux bornes du domaine
147
+ const clamped = Math.min(Math.max(val, domain.min), domain.max);
148
+ const t = domain.max === domain.min ? 0.5 : (clamped - domain.min) / (domain.max - domain.min);
149
+ return {
150
+ x: axisX[ai],
151
+ y: MARGIN.top + (1 - t) * plotHeight
152
+ };
153
+ });
154
+ return { points, tone: rowTone, index: ri, row, path: buildPathWithGaps(points) };
155
+ });
156
+ });
157
+
158
+ const dataValueItems = $derived(
159
+ data.map((row) =>
160
+ axes.map((axis) => `${axis.label}: ${row[axis.key] ?? ""}`).join(", ")
161
+ )
162
+ );
163
+
164
+ function formatTick(v: number): string {
165
+ if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}k`;
166
+ if (Number.isInteger(v)) return String(v);
167
+ return v.toFixed(1);
168
+ }
169
+
170
+ function handlePointerMove(event: PointerEvent) {
171
+ const target = event.target;
172
+ if (!(target instanceof Element)) { hoveredIndex = null; return; }
173
+ const idx = Number(target.getAttribute("data-chart-index"));
174
+ hoveredIndex = Number.isInteger(idx) ? idx : null;
175
+ }
176
+
177
+ const classes = () => ["st-parallelCoordinatesChart", className].filter(Boolean).join(" ");
178
+ </script>
179
+
180
+ <div class={classes()}>
181
+ <div
182
+ class="st-parallelCoordinatesChart__visual"
183
+ role="img"
184
+ aria-label={label}
185
+ onpointermove={handlePointerMove}
186
+ onpointerleave={() => (hoveredIndex = null)}
187
+ >
188
+ <svg
189
+ viewBox="0 0 {width} {height}"
190
+ preserveAspectRatio="xMidYMid meet"
191
+ width="100%"
192
+ height="100%"
193
+ focusable="false"
194
+ aria-hidden="true"
195
+ >
196
+ <!-- polylines (draw non-hovered first, then hovered on top) -->
197
+ {#each lines as line (line.index)}
198
+ <path
199
+ class="st-parallelCoordinatesChart__line st-parallelCoordinatesChart__line--{line.tone}"
200
+ class:st-parallelCoordinatesChart__line--dim={hoveredIndex !== null && hoveredIndex !== line.index}
201
+ class:st-parallelCoordinatesChart__line--active={hoveredIndex === line.index}
202
+ d={line.path}
203
+ fill="none"
204
+ data-chart-index={line.index}
205
+ />
206
+ {/each}
207
+
208
+ <!-- axes and labels -->
209
+ {#each axes as axis, ai (axis.key)}
210
+ {@const domain = axisDomain(axis)}
211
+ {@const ax = axisX[ai]}
212
+ <line
213
+ class="st-parallelCoordinatesChart__axis"
214
+ x1={ax}
215
+ x2={ax}
216
+ y1={MARGIN.top}
217
+ y2={MARGIN.top + plotHeight}
218
+ />
219
+ <text
220
+ class="st-parallelCoordinatesChart__axisLabel"
221
+ x={ax}
222
+ y={MARGIN.top - 10}
223
+ text-anchor="middle"
224
+ >
225
+ {axis.label}
226
+ </text>
227
+ <!-- min/max ticks -->
228
+ <text
229
+ class="st-parallelCoordinatesChart__tickLabel"
230
+ x={ax + 4}
231
+ y={MARGIN.top + plotHeight}
232
+ dominant-baseline="auto"
233
+ >
234
+ {formatTick(domain.min)}
235
+ </text>
236
+ <text
237
+ class="st-parallelCoordinatesChart__tickLabel"
238
+ x={ax + 4}
239
+ y={MARGIN.top}
240
+ dominant-baseline="hanging"
241
+ >
242
+ {formatTick(domain.max)}
243
+ </text>
244
+ {/each}
245
+ </svg>
246
+ </div>
247
+
248
+ <ChartDataList {label} items={dataValueItems} />
249
+ </div>
250
+
251
+ <style>
252
+ .st-parallelCoordinatesChart {
253
+ color: var(--st-semantic-text-secondary);
254
+ display: block;
255
+ font-family: inherit;
256
+ position: relative;
257
+ width: 100%;
258
+ }
259
+
260
+ .st-parallelCoordinatesChart svg {
261
+ display: block;
262
+ overflow: visible;
263
+ }
264
+
265
+ .st-parallelCoordinatesChart__visual {
266
+ display: block;
267
+ }
268
+
269
+ .st-parallelCoordinatesChart__axis {
270
+ stroke: var(--st-semantic-border-subtle);
271
+ stroke-width: 1.5;
272
+ }
273
+
274
+ .st-parallelCoordinatesChart__axisLabel {
275
+ fill: var(--st-semantic-text-secondary);
276
+ font-size: 0.6875rem;
277
+ font-weight: 600;
278
+ }
279
+
280
+ .st-parallelCoordinatesChart__tickLabel {
281
+ fill: var(--st-semantic-text-secondary);
282
+ font-size: 0.5625rem;
283
+ }
284
+
285
+ .st-parallelCoordinatesChart__line {
286
+ cursor: pointer;
287
+ stroke-width: 1.5;
288
+ stroke-opacity: 0.65;
289
+ transition: stroke-opacity 120ms ease, stroke-width 120ms ease;
290
+ }
291
+
292
+ .st-parallelCoordinatesChart__line--dim {
293
+ stroke-opacity: 0.12;
294
+ }
295
+
296
+ .st-parallelCoordinatesChart__line--active {
297
+ stroke-opacity: 1;
298
+ stroke-width: 2.5;
299
+ }
300
+
301
+ @media (prefers-reduced-motion: reduce) {
302
+ .st-parallelCoordinatesChart__line {
303
+ transition: none;
304
+ }
305
+ }
306
+
307
+ .st-parallelCoordinatesChart__line--category1 { stroke: var(--st-semantic-data-category1); }
308
+ .st-parallelCoordinatesChart__line--category2 { stroke: var(--st-semantic-data-category2); }
309
+ .st-parallelCoordinatesChart__line--category3 { stroke: var(--st-semantic-data-category3); }
310
+ .st-parallelCoordinatesChart__line--category4 { stroke: var(--st-semantic-data-category4); }
311
+ .st-parallelCoordinatesChart__line--category5 { stroke: var(--st-semantic-data-category5); }
312
+ .st-parallelCoordinatesChart__line--category6 { stroke: var(--st-semantic-data-category6); }
313
+ .st-parallelCoordinatesChart__line--category7 { stroke: var(--st-semantic-data-category7); }
314
+ .st-parallelCoordinatesChart__line--category8 { stroke: var(--st-semantic-data-category8); }
315
+ </style>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ParallelCoordinatesChart - axes verticaux parallèles, polylignes par enregistrement.
3
+ * API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * axes ParallelAxis[] - définition des axes {key, label, min?, max?}
7
+ * data Array<Record<string, unknown>> - enregistrements
8
+ * label string - aria-label
9
+ *
10
+ * Props optionnelles :
11
+ * tones tone par série (string[]) - one tone per datum row, cycled
12
+ * width number (défaut 480)
13
+ * height number (défaut 300)
14
+ * class string
15
+ */
16
+ export type ParallelCoordinatesChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
17
+ export type ParallelAxis = {
18
+ key: string;
19
+ label: string;
20
+ min?: number;
21
+ max?: number;
22
+ };
23
+ type ParallelCoordinatesChartProps = {
24
+ axes: ParallelAxis[];
25
+ data: Record<string, unknown>[];
26
+ label: string;
27
+ tones?: ParallelCoordinatesChartTone[];
28
+ width?: number;
29
+ height?: number;
30
+ class?: string;
31
+ };
32
+ declare const ParallelCoordinatesChart: import("svelte").Component<ParallelCoordinatesChartProps, {}, "">;
33
+ type ParallelCoordinatesChart = ReturnType<typeof ParallelCoordinatesChart>;
34
+ export default ParallelCoordinatesChart;
35
+ //# sourceMappingURL=ParallelCoordinatesChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelCoordinatesChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ParallelCoordinatesChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,4BAA4B,GACpC,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAMF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IAEd,KAAK,CAAC,EAAE,4BAA4B,EAAE,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4KJ,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,340 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * RadarChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * axes string[] - libellés des axes (N axes = polygone à N côtés)
7
+ * series RadarChartSeries[] - séries {label, values: number[], tone?}
8
+ * label string - aria-label du graphique
9
+ *
10
+ * Props optionnelles :
11
+ * maxValue number (défaut : max des valeurs, min 1) - valeur plafond du
12
+ * domaine. PAS de plancher arbitraire à 100 - l'échelle
13
+ * s'adapte aux données. React/Vue doivent supprimer leur
14
+ * `Math.max(100, …)` pour s'aligner sur ce comportement.
15
+ * levels number (défaut 4) - nombre de cercles / anneaux de grille
16
+ * legend boolean (défaut false) - affiche la légende des séries
17
+ * width number (défaut 360) - largeur du viewBox en px
18
+ * height number (défaut 320) - hauteur du viewBox en px
19
+ * class string - classe CSS supplémentaire
20
+ *
21
+ * NaN/vide : les valeurs non-finies sont exclues du calcul du domaine
22
+ * (filter Number.isFinite). Séries vides → polygone nul sans crash.
23
+ */
24
+ export type RadarChartTone =
25
+ | "category1"
26
+ | "category2"
27
+ | "category3"
28
+ | "category4"
29
+ | "category5"
30
+ | "category6"
31
+ | "category7"
32
+ | "category8";
33
+
34
+ export type RadarChartSeries = {
35
+ label: string;
36
+ values: number[];
37
+ tone?: RadarChartTone;
38
+ };
39
+ </script>
40
+
41
+ <script lang="ts">
42
+ import ChartDataList from "./ChartDataList.svelte";
43
+
44
+ type RadarChartProps = {
45
+ axes: string[];
46
+ series: RadarChartSeries[];
47
+ label: string;
48
+ legend?: boolean;
49
+ maxValue?: number;
50
+ levels?: number;
51
+ width?: number;
52
+ height?: number;
53
+ class?: string;
54
+ };
55
+
56
+ let {
57
+ axes,
58
+ series,
59
+ label,
60
+ legend = false,
61
+ maxValue,
62
+ levels = 4,
63
+ width = 360,
64
+ height = 320,
65
+ class: className
66
+ }: RadarChartProps = $props();
67
+
68
+ const TONES = [
69
+ "category1",
70
+ "category2",
71
+ "category3",
72
+ "category4",
73
+ "category5",
74
+ "category6",
75
+ "category7",
76
+ "category8"
77
+ ] as const;
78
+
79
+ function pointAt(cx: number, cy: number, radius: number, angle: number) {
80
+ return {
81
+ x: cx + radius * Math.cos(angle),
82
+ y: cy + radius * Math.sin(angle)
83
+ };
84
+ }
85
+
86
+ let hoveredIndex: number | null = $state(null);
87
+
88
+ const center = $derived({ x: width / 2, y: height / 2 });
89
+ const radius = $derived(Math.max(Math.min(width, height) / 2 - 42, 1));
90
+ const safeLevelCount = $derived(Math.max(1, Math.floor(levels)));
91
+ const domainMax = $derived.by(() => {
92
+ if (Number.isFinite(maxValue) && (maxValue ?? 0) > 0) return maxValue as number;
93
+ const values = series.flatMap((entry) => entry.values).filter(Number.isFinite);
94
+ return Math.max(1, ...values);
95
+ });
96
+
97
+ const axisEntries = $derived(
98
+ axes.map((axis, index) => {
99
+ const angle = -Math.PI / 2 + (Math.PI * 2 * index) / Math.max(axes.length, 1);
100
+ const end = pointAt(center.x, center.y, radius, angle);
101
+ const labelPoint = pointAt(center.x, center.y, radius + 22, angle);
102
+ return { axis, index, angle, end, labelPoint };
103
+ })
104
+ );
105
+
106
+ const rings = $derived(
107
+ Array.from({ length: safeLevelCount }, (_, index) => {
108
+ const ringRadius = (radius * (index + 1)) / safeLevelCount;
109
+ return axisEntries.map((axis) => pointAt(center.x, center.y, ringRadius, axis.angle)).map((point) => `${point.x},${point.y}`).join(" ");
110
+ })
111
+ );
112
+
113
+ const polygons = $derived(
114
+ series.map((entry, seriesIndex) => {
115
+ const tone = entry.tone ?? TONES[seriesIndex % TONES.length];
116
+ const points = axes.map((_, axisIndex) => {
117
+ const value = Math.max(0, entry.values[axisIndex] ?? 0);
118
+ const scaled = Math.min(value / domainMax, 1) * radius;
119
+ const angle = -Math.PI / 2 + (Math.PI * 2 * axisIndex) / Math.max(axes.length, 1);
120
+ return pointAt(center.x, center.y, scaled, angle);
121
+ });
122
+ return {
123
+ entry,
124
+ tone,
125
+ points,
126
+ pointString: points.map((point) => `${point.x},${point.y}`).join(" ")
127
+ };
128
+ })
129
+ );
130
+
131
+ const legendItems = $derived(series.map((entry, index) => ({ label: entry.label, tone: entry.tone ?? TONES[index % TONES.length] })));
132
+
133
+ const dataValueItems = $derived(
134
+ series.flatMap((entry) => axes.map((axis, axisIndex) => `${entry.label}, ${axis}: ${entry.values[axisIndex] ?? 0}`))
135
+ );
136
+
137
+ function handleVisualPointerMove(event: PointerEvent) {
138
+ const target = event.target;
139
+ if (!(target instanceof Element)) {
140
+ hoveredIndex = null;
141
+ return;
142
+ }
143
+ const index = Number(target.getAttribute("data-chart-index"));
144
+ hoveredIndex = Number.isInteger(index) ? index : null;
145
+ }
146
+
147
+ const classes = () => ["st-radarChart", className].filter(Boolean).join(" ");
148
+ </script>
149
+
150
+ <div class={classes()}>
151
+ <div
152
+ class="st-radarChart__visual"
153
+ role="img"
154
+ aria-label={label}
155
+ onpointermove={handleVisualPointerMove}
156
+ onpointerleave={() => (hoveredIndex = null)}
157
+ >
158
+ <svg
159
+ viewBox="0 0 {width} {height}"
160
+ preserveAspectRatio="xMidYMid meet"
161
+ width="100%"
162
+ height="100%"
163
+ focusable="false"
164
+ aria-hidden="true"
165
+ >
166
+ {#each rings as ring, i (i)}
167
+ <polygon class="st-radarChart__ring" points={ring} />
168
+ {/each}
169
+
170
+ {#each axisEntries as axis (axis.axis)}
171
+ <line class="st-radarChart__axis" x1={center.x} x2={axis.end.x} y1={center.y} y2={axis.end.y} />
172
+ <text
173
+ class="st-radarChart__axisLabel"
174
+ x={axis.labelPoint.x}
175
+ y={axis.labelPoint.y}
176
+ text-anchor="middle"
177
+ dominant-baseline="middle"
178
+ >
179
+ {axis.axis}
180
+ </text>
181
+ {/each}
182
+
183
+ {#each polygons as polygon, i (polygon.entry.label)}
184
+ <polygon
185
+ class="st-radarChart__polygon st-radarChart__polygon--{polygon.tone}"
186
+ class:st-radarChart__polygon--dim={hoveredIndex !== null && hoveredIndex !== i}
187
+ points={polygon.pointString}
188
+ data-chart-index={i}
189
+ />
190
+ {#each polygon.points as point, pointIndex (`${polygon.entry.label}-${pointIndex}`)}
191
+ <circle class="st-radarChart__point st-radarChart__point--{polygon.tone}" cx={point.x} cy={point.y} r="3" data-chart-index={i} />
192
+ {/each}
193
+ {/each}
194
+ </svg>
195
+ </div>
196
+
197
+ <ChartDataList {label} items={dataValueItems} />
198
+
199
+ {#if hoveredIndex !== null && polygons[hoveredIndex]}
200
+ {@const polygon = polygons[hoveredIndex]}
201
+ <div class="st-radarChart__tooltip" role="presentation">
202
+ <span class="st-radarChart__tooltipLabel">{polygon.entry.label}</span>
203
+ </div>
204
+ {/if}
205
+
206
+ {#if legend && legendItems.length > 0}
207
+ <ul class="st-radarChart__legend" aria-hidden="true">
208
+ {#each legendItems as item (item.label)}
209
+ <li class="st-radarChart__legendItem">
210
+ <span class="st-radarChart__legendSwatch st-radarChart__legendSwatch--{item.tone}"></span>
211
+ {item.label}
212
+ </li>
213
+ {/each}
214
+ </ul>
215
+ {/if}
216
+ </div>
217
+
218
+ <style>
219
+ .st-radarChart {
220
+ color: var(--st-semantic-text-secondary);
221
+ display: block;
222
+ font-family: inherit;
223
+ max-width: 100%;
224
+ position: relative;
225
+ width: 100%;
226
+ }
227
+
228
+ .st-radarChart svg,
229
+ .st-radarChart__visual {
230
+ display: block;
231
+ overflow: visible;
232
+ }
233
+
234
+ .st-radarChart__ring {
235
+ fill: none;
236
+ stroke: var(--st-semantic-border-subtle);
237
+ stroke-width: 1;
238
+ }
239
+
240
+ .st-radarChart__axis {
241
+ stroke: var(--st-semantic-border-subtle);
242
+ stroke-width: 1;
243
+ }
244
+
245
+ .st-radarChart__axisLabel {
246
+ fill: var(--st-semantic-text-secondary);
247
+ font-size: 0.72rem;
248
+ }
249
+
250
+ .st-radarChart__polygon {
251
+ cursor: pointer;
252
+ fill-opacity: 0.16;
253
+ stroke-width: 2;
254
+ transition: opacity 120ms ease;
255
+ }
256
+
257
+ .st-radarChart__polygon--dim {
258
+ opacity: 0.35;
259
+ }
260
+
261
+ @media (prefers-reduced-motion: reduce) {
262
+ .st-radarChart__polygon {
263
+ transition: none;
264
+ }
265
+ }
266
+
267
+ .st-radarChart__point {
268
+ stroke: var(--st-semantic-surface-default, Canvas);
269
+ stroke-width: 1;
270
+ }
271
+
272
+ .st-radarChart__polygon--category1,
273
+ .st-radarChart__point--category1,
274
+ .st-radarChart__legendSwatch--category1 { fill: var(--st-semantic-data-category1); stroke: var(--st-semantic-data-category1); background: var(--st-semantic-data-category1); }
275
+ .st-radarChart__polygon--category2,
276
+ .st-radarChart__point--category2,
277
+ .st-radarChart__legendSwatch--category2 { fill: var(--st-semantic-data-category2); stroke: var(--st-semantic-data-category2); background: var(--st-semantic-data-category2); }
278
+ .st-radarChart__polygon--category3,
279
+ .st-radarChart__point--category3,
280
+ .st-radarChart__legendSwatch--category3 { fill: var(--st-semantic-data-category3); stroke: var(--st-semantic-data-category3); background: var(--st-semantic-data-category3); }
281
+ .st-radarChart__polygon--category4,
282
+ .st-radarChart__point--category4,
283
+ .st-radarChart__legendSwatch--category4 { fill: var(--st-semantic-data-category4); stroke: var(--st-semantic-data-category4); background: var(--st-semantic-data-category4); }
284
+ .st-radarChart__polygon--category5,
285
+ .st-radarChart__point--category5,
286
+ .st-radarChart__legendSwatch--category5 { fill: var(--st-semantic-data-category5); stroke: var(--st-semantic-data-category5); background: var(--st-semantic-data-category5); }
287
+ .st-radarChart__polygon--category6,
288
+ .st-radarChart__point--category6,
289
+ .st-radarChart__legendSwatch--category6 { fill: var(--st-semantic-data-category6); stroke: var(--st-semantic-data-category6); background: var(--st-semantic-data-category6); }
290
+ .st-radarChart__polygon--category7,
291
+ .st-radarChart__point--category7,
292
+ .st-radarChart__legendSwatch--category7 { fill: var(--st-semantic-data-category7); stroke: var(--st-semantic-data-category7); background: var(--st-semantic-data-category7); }
293
+ .st-radarChart__polygon--category8,
294
+ .st-radarChart__point--category8,
295
+ .st-radarChart__legendSwatch--category8 { fill: var(--st-semantic-data-category8); stroke: var(--st-semantic-data-category8); background: var(--st-semantic-data-category8); }
296
+
297
+ .st-radarChart__legend {
298
+ display: flex;
299
+ flex-wrap: wrap;
300
+ gap: var(--st-spacing-2, 0.5rem) var(--st-spacing-4, 1rem);
301
+ list-style: none;
302
+ margin: var(--st-spacing-2, 0.5rem) 0 0;
303
+ padding: 0;
304
+ }
305
+
306
+ .st-radarChart__legendItem {
307
+ align-items: center;
308
+ color: var(--st-semantic-text-secondary);
309
+ display: inline-flex;
310
+ font-size: 0.75rem;
311
+ gap: var(--st-spacing-2, 0.5rem);
312
+ }
313
+
314
+ .st-radarChart__legendSwatch {
315
+ display: inline-block;
316
+ height: 0.625rem;
317
+ width: 0.625rem;
318
+ }
319
+
320
+ .st-radarChart__tooltip {
321
+ background: var(--st-semantic-surface-inverse);
322
+ border-radius: var(--st-radius-sm, 0.25rem);
323
+ color: var(--st-semantic-text-inverse);
324
+ display: inline-flex;
325
+ font-size: 0.75rem;
326
+ left: 50%;
327
+ line-height: 1.2;
328
+ padding: 0.375rem 0.5rem;
329
+ pointer-events: none;
330
+ position: absolute;
331
+ top: 50%;
332
+ transform: translate(-50%, -50%);
333
+ white-space: nowrap;
334
+ z-index: 1;
335
+ }
336
+
337
+ .st-radarChart__tooltipLabel {
338
+ font-weight: 600;
339
+ }
340
+ </style>