@sentropic/design-system-svelte 0.18.0 → 0.19.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.
@@ -0,0 +1,302 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * BoxPlotChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * data BoxPlotChartDatum[] - tableau {label, min, q1, median, q3, max, outliers?, tone?}
7
+ * label string - aria-label du graphique
8
+ *
9
+ * Props optionnelles :
10
+ * width number (défaut 480) - largeur du viewBox en px
11
+ * height number (défaut 260) - hauteur du viewBox en px
12
+ * class string - classe CSS supplémentaire
13
+ *
14
+ * NaN/vide : les valeurs non-finies sont exclues du domaine (filter Number.isFinite).
15
+ * Tableau vide → rendu vide sans crash.
16
+ * Note : l'ordre min≤q1≤median≤q3≤max n'est pas imposé par clamp mais rendu
17
+ * tel quel (abs() sur la hauteur de boîte) ; les données incohérentes produisent
18
+ * un rendu plausible mais peuvent induire en erreur.
19
+ */
20
+ export type BoxPlotChartTone =
21
+ | "category1"
22
+ | "category2"
23
+ | "category3"
24
+ | "category4"
25
+ | "category5"
26
+ | "category6"
27
+ | "category7"
28
+ | "category8";
29
+
30
+ export type BoxPlotChartDatum = {
31
+ label: string;
32
+ min: number;
33
+ q1: number;
34
+ median: number;
35
+ q3: number;
36
+ max: number;
37
+ outliers?: number[];
38
+ tone?: BoxPlotChartTone;
39
+ };
40
+ </script>
41
+
42
+ <script lang="ts">
43
+ import ChartDataList from "./ChartDataList.svelte";
44
+
45
+ type BoxPlotChartProps = {
46
+ data: BoxPlotChartDatum[];
47
+ label: string;
48
+ width?: number;
49
+ height?: number;
50
+ class?: string;
51
+ };
52
+
53
+ let {
54
+ data,
55
+ label,
56
+ width = 480,
57
+ height = 260,
58
+ class: className
59
+ }: BoxPlotChartProps = $props();
60
+
61
+ const MARGIN = { top: 16, right: 20, bottom: 38, left: 48 };
62
+ const TONES = [
63
+ "category1",
64
+ "category2",
65
+ "category3",
66
+ "category4",
67
+ "category5",
68
+ "category6",
69
+ "category7",
70
+ "category8"
71
+ ] as const;
72
+
73
+ function formatNumber(value: number): string {
74
+ if (!Number.isFinite(value)) return "0";
75
+ if (Number.isInteger(value)) return String(value);
76
+ return value.toFixed(2).replace(/\.?0+$/, "");
77
+ }
78
+
79
+ function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
80
+ if (d1 === d0) return r0;
81
+ return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
82
+ }
83
+
84
+ let hoveredIndex: number | null = $state(null);
85
+
86
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
87
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
88
+
89
+ const domain = $derived.by(() => {
90
+ const values = data.flatMap((datum) => [
91
+ datum.min,
92
+ datum.q1,
93
+ datum.median,
94
+ datum.q3,
95
+ datum.max,
96
+ ...(datum.outliers ?? [])
97
+ ]).filter(Number.isFinite);
98
+ if (values.length === 0) return { min: 0, max: 1 };
99
+ const min = Math.min(...values);
100
+ const max = Math.max(...values);
101
+ const pad = (max - min) * 0.08 || Math.max(Math.abs(max), 1) * 0.1;
102
+ return { min: min - pad, max: max + pad };
103
+ });
104
+
105
+ const plots = $derived.by(() => {
106
+ const band = data.length > 0 ? plotWidth / data.length : plotWidth;
107
+ const boxWidth = Math.min(54, Math.max(18, band * 0.44));
108
+
109
+ return data.map((datum, index) => {
110
+ const cx = MARGIN.left + band * (index + 0.5);
111
+ const y = (value: number) => MARGIN.top + scaleLinear(value, domain.min, domain.max, plotHeight, 0);
112
+ const q1Y = y(datum.q1);
113
+ const q3Y = y(datum.q3);
114
+ const minY = y(datum.min);
115
+ const maxY = y(datum.max);
116
+ return {
117
+ datum,
118
+ tone: datum.tone ?? TONES[index % TONES.length],
119
+ cx,
120
+ boxX: cx - boxWidth / 2,
121
+ boxY: Math.min(q1Y, q3Y),
122
+ boxWidth,
123
+ boxHeight: Math.max(Math.abs(q1Y - q3Y), 1),
124
+ medianY: y(datum.median),
125
+ minY,
126
+ maxY,
127
+ capWidth: boxWidth * 0.72,
128
+ outliers: (datum.outliers ?? []).filter(Number.isFinite).map((value) => ({ value, y: y(value) }))
129
+ };
130
+ });
131
+ });
132
+
133
+ const dataValueItems = $derived(
134
+ data.map((datum) => {
135
+ const summary = `${datum.label}: min ${formatNumber(datum.min)}, q1 ${formatNumber(datum.q1)}, median ${formatNumber(datum.median)}, q3 ${formatNumber(datum.q3)}, max ${formatNumber(datum.max)}`;
136
+ const outliers = datum.outliers?.length ? `, outliers ${datum.outliers.map(formatNumber).join(", ")}` : "";
137
+ return `${summary}${outliers}`;
138
+ })
139
+ );
140
+
141
+ function handleVisualPointerMove(event: PointerEvent) {
142
+ const target = event.target;
143
+ if (!(target instanceof Element)) {
144
+ hoveredIndex = null;
145
+ return;
146
+ }
147
+ const index = Number(target.getAttribute("data-chart-index"));
148
+ hoveredIndex = Number.isInteger(index) ? index : null;
149
+ }
150
+
151
+ const classes = () => ["st-boxPlotChart", className].filter(Boolean).join(" ");
152
+ </script>
153
+
154
+ <div class={classes()}>
155
+ <div
156
+ class="st-boxPlotChart__visual"
157
+ role="img"
158
+ aria-label={label}
159
+ onpointermove={handleVisualPointerMove}
160
+ onpointerleave={() => (hoveredIndex = null)}
161
+ >
162
+ <svg
163
+ viewBox="0 0 {width} {height}"
164
+ preserveAspectRatio="xMidYMid meet"
165
+ width="100%"
166
+ height="100%"
167
+ focusable="false"
168
+ aria-hidden="true"
169
+ >
170
+ <line class="st-boxPlotChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
171
+ <line class="st-boxPlotChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
172
+
173
+ {#each plots as plot, i (plot.datum.label)}
174
+ <line class="st-boxPlotChart__whisker" x1={plot.cx} x2={plot.cx} y1={plot.minY} y2={plot.maxY} data-chart-index={i} />
175
+ <line class="st-boxPlotChart__whiskerCap" x1={plot.cx - plot.capWidth / 2} x2={plot.cx + plot.capWidth / 2} y1={plot.minY} y2={plot.minY} data-chart-index={i} />
176
+ <line class="st-boxPlotChart__whiskerCap" x1={plot.cx - plot.capWidth / 2} x2={plot.cx + plot.capWidth / 2} y1={plot.maxY} y2={plot.maxY} data-chart-index={i} />
177
+ <rect
178
+ class="st-boxPlotChart__box st-boxPlotChart__box--{plot.tone}"
179
+ class:st-boxPlotChart__box--dim={hoveredIndex !== null && hoveredIndex !== i}
180
+ x={plot.boxX}
181
+ y={plot.boxY}
182
+ width={plot.boxWidth}
183
+ height={plot.boxHeight}
184
+ data-chart-index={i}
185
+ />
186
+ <line class="st-boxPlotChart__median" x1={plot.boxX} x2={plot.boxX + plot.boxWidth} y1={plot.medianY} y2={plot.medianY} data-chart-index={i} />
187
+ {#each plot.outliers as outlier (`${plot.datum.label}-${outlier.value}`)}
188
+ <circle class="st-boxPlotChart__outlier" cx={plot.cx} cy={outlier.y} r="3" data-chart-index={i} />
189
+ {/each}
190
+ <text class="st-boxPlotChart__label" x={plot.cx} y={height - MARGIN.bottom + 16} text-anchor="middle">
191
+ {plot.datum.label}
192
+ </text>
193
+ {/each}
194
+ </svg>
195
+ </div>
196
+
197
+ <ChartDataList {label} items={dataValueItems} />
198
+
199
+ {#if hoveredIndex !== null && plots[hoveredIndex]}
200
+ {@const plot = plots[hoveredIndex]}
201
+ <div
202
+ class="st-boxPlotChart__tooltip"
203
+ role="presentation"
204
+ style="left: {(plot.cx / width) * 100}%; top: {(plot.medianY / height) * 100}%"
205
+ >
206
+ <span class="st-boxPlotChart__tooltipLabel">{plot.datum.label}</span>
207
+ <span class="st-boxPlotChart__tooltipValue">Median {formatNumber(plot.datum.median)}</span>
208
+ </div>
209
+ {/if}
210
+ </div>
211
+
212
+ <style>
213
+ .st-boxPlotChart {
214
+ color: var(--st-semantic-text-secondary);
215
+ display: block;
216
+ font-family: inherit;
217
+ max-width: 100%;
218
+ position: relative;
219
+ width: 100%;
220
+ }
221
+
222
+ .st-boxPlotChart svg,
223
+ .st-boxPlotChart__visual {
224
+ display: block;
225
+ overflow: visible;
226
+ }
227
+
228
+ .st-boxPlotChart__axis,
229
+ .st-boxPlotChart__whisker,
230
+ .st-boxPlotChart__whiskerCap {
231
+ stroke: var(--st-semantic-border-subtle);
232
+ stroke-width: 1;
233
+ }
234
+
235
+ .st-boxPlotChart__median {
236
+ stroke: var(--st-semantic-text-primary);
237
+ stroke-width: 2;
238
+ }
239
+
240
+ .st-boxPlotChart__box {
241
+ cursor: pointer;
242
+ fill-opacity: 0.72;
243
+ stroke: var(--st-semantic-surface-default, Canvas);
244
+ stroke-width: 1;
245
+ transition: opacity 120ms ease;
246
+ }
247
+
248
+ .st-boxPlotChart__box--dim {
249
+ opacity: 0.45;
250
+ }
251
+
252
+ @media (prefers-reduced-motion: reduce) {
253
+ .st-boxPlotChart__box {
254
+ transition: none;
255
+ }
256
+ }
257
+
258
+ .st-boxPlotChart__box--category1 { fill: var(--st-semantic-data-category1); }
259
+ .st-boxPlotChart__box--category2 { fill: var(--st-semantic-data-category2); }
260
+ .st-boxPlotChart__box--category3 { fill: var(--st-semantic-data-category3); }
261
+ .st-boxPlotChart__box--category4 { fill: var(--st-semantic-data-category4); }
262
+ .st-boxPlotChart__box--category5 { fill: var(--st-semantic-data-category5); }
263
+ .st-boxPlotChart__box--category6 { fill: var(--st-semantic-data-category6); }
264
+ .st-boxPlotChart__box--category7 { fill: var(--st-semantic-data-category7); }
265
+ .st-boxPlotChart__box--category8 { fill: var(--st-semantic-data-category8); }
266
+
267
+ .st-boxPlotChart__outlier {
268
+ fill: var(--st-semantic-surface-default, Canvas);
269
+ stroke: var(--st-semantic-text-secondary);
270
+ stroke-width: 1.5;
271
+ }
272
+
273
+ .st-boxPlotChart__label {
274
+ fill: var(--st-semantic-text-secondary);
275
+ font-size: 0.75rem;
276
+ }
277
+
278
+ .st-boxPlotChart__tooltip {
279
+ background: var(--st-semantic-surface-inverse);
280
+ border-radius: var(--st-radius-sm, 0.25rem);
281
+ color: var(--st-semantic-text-inverse);
282
+ display: inline-flex;
283
+ flex-direction: column;
284
+ font-size: 0.75rem;
285
+ gap: 0.125rem;
286
+ line-height: 1.2;
287
+ padding: 0.375rem 0.5rem;
288
+ pointer-events: none;
289
+ position: absolute;
290
+ transform: translate(-50%, -115%);
291
+ white-space: nowrap;
292
+ z-index: 1;
293
+ }
294
+
295
+ .st-boxPlotChart__tooltipLabel {
296
+ font-weight: 600;
297
+ }
298
+
299
+ .st-boxPlotChart__tooltipValue {
300
+ opacity: 0.85;
301
+ }
302
+ </style>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * BoxPlotChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
3
+ *
4
+ * Props obligatoires :
5
+ * data BoxPlotChartDatum[] - tableau {label, min, q1, median, q3, max, outliers?, tone?}
6
+ * label string - aria-label du graphique
7
+ *
8
+ * Props optionnelles :
9
+ * width number (défaut 480) - largeur du viewBox en px
10
+ * height number (défaut 260) - hauteur du viewBox en px
11
+ * class string - classe CSS supplémentaire
12
+ *
13
+ * NaN/vide : les valeurs non-finies sont exclues du domaine (filter Number.isFinite).
14
+ * Tableau vide → rendu vide sans crash.
15
+ * Note : l'ordre min≤q1≤median≤q3≤max n'est pas imposé par clamp mais rendu
16
+ * tel quel (abs() sur la hauteur de boîte) ; les données incohérentes produisent
17
+ * un rendu plausible mais peuvent induire en erreur.
18
+ */
19
+ export type BoxPlotChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
20
+ export type BoxPlotChartDatum = {
21
+ label: string;
22
+ min: number;
23
+ q1: number;
24
+ median: number;
25
+ q3: number;
26
+ max: number;
27
+ outliers?: number[];
28
+ tone?: BoxPlotChartTone;
29
+ };
30
+ type BoxPlotChartProps = {
31
+ data: BoxPlotChartDatum[];
32
+ label: string;
33
+ width?: number;
34
+ height?: number;
35
+ class?: string;
36
+ };
37
+ declare const BoxPlotChart: import("svelte").Component<BoxPlotChartProps, {}, "">;
38
+ type BoxPlotChart = ReturnType<typeof BoxPlotChart>;
39
+ export default BoxPlotChart;
40
+ //# sourceMappingURL=BoxPlotChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BoxPlotChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BoxPlotChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GACxB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAMF,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiJJ,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,337 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * HeatmapChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * data HeatmapChartDatum[] - tableau {x, y, value, tone?}
7
+ * label string - aria-label du graphique
8
+ *
9
+ * Props optionnelles :
10
+ * legend boolean (défaut false) - affiche le gradient Low→High
11
+ * width number (défaut 480) - largeur du viewBox en px
12
+ * height number (défaut 300) - hauteur du viewBox en px
13
+ * class string - classe CSS supplémentaire
14
+ *
15
+ * NaN/vide : les valeurs non-finies sont exclues du calcul min/max (gardé par
16
+ * Number.isFinite). Tableau vide ou tout-NaN → rendu vide sans crash.
17
+ */
18
+ export type HeatmapChartTone =
19
+ | "category1"
20
+ | "category2"
21
+ | "category3"
22
+ | "category4"
23
+ | "category5"
24
+ | "category6"
25
+ | "category7"
26
+ | "category8";
27
+
28
+ export type HeatmapChartDatum = {
29
+ x: string;
30
+ y: string;
31
+ value: number;
32
+ tone?: HeatmapChartTone;
33
+ };
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ import ChartDataList from "./ChartDataList.svelte";
38
+
39
+ type HeatmapChartProps = {
40
+ data: HeatmapChartDatum[];
41
+ label: string;
42
+ legend?: boolean;
43
+ width?: number;
44
+ height?: number;
45
+ class?: string;
46
+ };
47
+
48
+ let {
49
+ data,
50
+ label,
51
+ legend = false,
52
+ width = 480,
53
+ height = 300,
54
+ class: className
55
+ }: HeatmapChartProps = $props();
56
+
57
+ const MARGIN = { top: 28, right: 18, bottom: 44, left: 64 };
58
+ const TONES = [
59
+ "category1",
60
+ "category2",
61
+ "category3",
62
+ "category4",
63
+ "category5",
64
+ "category6",
65
+ "category7",
66
+ "category8"
67
+ ] as const;
68
+
69
+ function uniqueInOrder(values: string[]): string[] {
70
+ const seen = new Set<string>();
71
+ const out: string[] = [];
72
+ for (const value of values) {
73
+ if (!seen.has(value)) {
74
+ seen.add(value);
75
+ out.push(value);
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function toneForValue(value: number, min: number, max: number): HeatmapChartTone {
82
+ if (!Number.isFinite(value) || max <= min) return "category1";
83
+ const index = Math.max(0, Math.min(TONES.length - 1, Math.floor(((value - min) / (max - min)) * TONES.length)));
84
+ return TONES[index];
85
+ }
86
+
87
+ let hoveredIndex: number | null = $state(null);
88
+
89
+ const xLabels = $derived(uniqueInOrder(data.map((d) => d.x)));
90
+ const yLabels = $derived(uniqueInOrder(data.map((d) => d.y)));
91
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
92
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
93
+
94
+ const cells = $derived.by(() => {
95
+ const values = data.map((d) => d.value).filter(Number.isFinite);
96
+ const min = values.length > 0 ? Math.min(...values) : 0;
97
+ const max = values.length > 0 ? Math.max(...values) : 1;
98
+ const cellWidth = xLabels.length > 0 ? plotWidth / xLabels.length : plotWidth;
99
+ const cellHeight = yLabels.length > 0 ? plotHeight / yLabels.length : plotHeight;
100
+
101
+ return data.map((datum, index) => {
102
+ const xIndex = Math.max(0, xLabels.indexOf(datum.x));
103
+ const yIndex = Math.max(0, yLabels.indexOf(datum.y));
104
+ return {
105
+ datum,
106
+ index,
107
+ tone: datum.tone ?? toneForValue(datum.value, min, max),
108
+ x: MARGIN.left + xIndex * cellWidth,
109
+ y: MARGIN.top + yIndex * cellHeight,
110
+ width: Math.max(cellWidth - 2, 1),
111
+ height: Math.max(cellHeight - 2, 1),
112
+ cx: MARGIN.left + xIndex * cellWidth + cellWidth / 2,
113
+ cy: MARGIN.top + yIndex * cellHeight + cellHeight / 2
114
+ };
115
+ });
116
+ });
117
+
118
+ const dataValueItems = $derived(data.map((d) => `${d.y}, ${d.x}: ${d.value}`));
119
+
120
+ const legendItems = $derived(
121
+ TONES.map((tone, index) => ({
122
+ tone,
123
+ label: index === 0 ? "Low" : index === TONES.length - 1 ? "High" : ""
124
+ }))
125
+ );
126
+
127
+ function handleVisualPointerMove(event: PointerEvent) {
128
+ const target = event.target;
129
+ if (!(target instanceof Element)) {
130
+ hoveredIndex = null;
131
+ return;
132
+ }
133
+ const index = Number(target.getAttribute("data-chart-index"));
134
+ hoveredIndex = Number.isInteger(index) ? index : null;
135
+ }
136
+
137
+ const classes = () => ["st-heatmapChart", className].filter(Boolean).join(" ");
138
+ </script>
139
+
140
+ <div class={classes()}>
141
+ <div
142
+ class="st-heatmapChart__visual"
143
+ role="img"
144
+ aria-label={label}
145
+ onpointermove={handleVisualPointerMove}
146
+ onpointerleave={() => (hoveredIndex = null)}
147
+ >
148
+ <svg
149
+ viewBox="0 0 {width} {height}"
150
+ preserveAspectRatio="xMidYMid meet"
151
+ width="100%"
152
+ height="100%"
153
+ focusable="false"
154
+ aria-hidden="true"
155
+ >
156
+ {#each yLabels as yLabel, row (yLabel)}
157
+ <text
158
+ class="st-heatmapChart__axisLabel st-heatmapChart__axisLabel--y"
159
+ x={MARGIN.left - 8}
160
+ y={MARGIN.top + (row + 0.5) * (plotHeight / Math.max(yLabels.length, 1))}
161
+ text-anchor="end"
162
+ dominant-baseline="middle"
163
+ >
164
+ {yLabel}
165
+ </text>
166
+ {/each}
167
+
168
+ {#each xLabels as xLabel, column (xLabel)}
169
+ <text
170
+ class="st-heatmapChart__axisLabel st-heatmapChart__axisLabel--x"
171
+ x={MARGIN.left + (column + 0.5) * (plotWidth / Math.max(xLabels.length, 1))}
172
+ y={height - MARGIN.bottom + 20}
173
+ text-anchor="middle"
174
+ >
175
+ {xLabel}
176
+ </text>
177
+ {/each}
178
+
179
+ <rect
180
+ class="st-heatmapChart__plot"
181
+ x={MARGIN.left}
182
+ y={MARGIN.top}
183
+ width={plotWidth}
184
+ height={plotHeight}
185
+ />
186
+
187
+ {#each cells as cell, i (`${cell.datum.y}-${cell.datum.x}-${i}`)}
188
+ <rect
189
+ class="st-heatmapChart__cell st-heatmapChart__cell--{cell.tone}"
190
+ class:st-heatmapChart__cell--dim={hoveredIndex !== null && hoveredIndex !== i}
191
+ x={cell.x}
192
+ y={cell.y}
193
+ width={cell.width}
194
+ height={cell.height}
195
+ rx="2"
196
+ data-chart-index={i}
197
+ />
198
+ {/each}
199
+ </svg>
200
+ </div>
201
+
202
+ <ChartDataList {label} items={dataValueItems} />
203
+
204
+ {#if hoveredIndex !== null && cells[hoveredIndex]}
205
+ {@const cell = cells[hoveredIndex]}
206
+ <div
207
+ class="st-heatmapChart__tooltip"
208
+ role="presentation"
209
+ style="left: {(cell.cx / width) * 100}%; top: {(cell.cy / height) * 100}%"
210
+ >
211
+ <span class="st-heatmapChart__tooltipLabel">{cell.datum.y}, {cell.datum.x}</span>
212
+ <span class="st-heatmapChart__tooltipValue">{cell.datum.value}</span>
213
+ </div>
214
+ {/if}
215
+
216
+ {#if legend}
217
+ <div class="st-heatmapChart__legend" aria-hidden="true">
218
+ <span class="st-heatmapChart__legendText">Low</span>
219
+ <span class="st-heatmapChart__legendRamp">
220
+ {#each legendItems as item (item.tone)}
221
+ <span class="st-heatmapChart__legendSwatch st-heatmapChart__legendSwatch--{item.tone}"></span>
222
+ {/each}
223
+ </span>
224
+ <span class="st-heatmapChart__legendText">High</span>
225
+ </div>
226
+ {/if}
227
+ </div>
228
+
229
+ <style>
230
+ .st-heatmapChart {
231
+ color: var(--st-semantic-text-secondary);
232
+ display: block;
233
+ font-family: inherit;
234
+ max-width: 100%;
235
+ position: relative;
236
+ width: 100%;
237
+ }
238
+
239
+ .st-heatmapChart svg,
240
+ .st-heatmapChart__visual {
241
+ display: block;
242
+ overflow: visible;
243
+ }
244
+
245
+ .st-heatmapChart__plot {
246
+ fill: none;
247
+ stroke: var(--st-semantic-border-subtle);
248
+ stroke-width: 1;
249
+ }
250
+
251
+ .st-heatmapChart__axisLabel {
252
+ fill: var(--st-semantic-text-secondary);
253
+ font-size: 0.75rem;
254
+ }
255
+
256
+ .st-heatmapChart__cell {
257
+ cursor: pointer;
258
+ stroke: var(--st-semantic-surface-default, Canvas);
259
+ stroke-width: 1;
260
+ transition: opacity 120ms ease;
261
+ }
262
+
263
+ .st-heatmapChart__cell--dim {
264
+ opacity: 0.45;
265
+ }
266
+
267
+ @media (prefers-reduced-motion: reduce) {
268
+ .st-heatmapChart__cell {
269
+ transition: none;
270
+ }
271
+ }
272
+
273
+ .st-heatmapChart__cell--category1,
274
+ .st-heatmapChart__legendSwatch--category1 { fill: var(--st-semantic-data-category1); background: var(--st-semantic-data-category1); }
275
+ .st-heatmapChart__cell--category2,
276
+ .st-heatmapChart__legendSwatch--category2 { fill: var(--st-semantic-data-category2); background: var(--st-semantic-data-category2); }
277
+ .st-heatmapChart__cell--category3,
278
+ .st-heatmapChart__legendSwatch--category3 { fill: var(--st-semantic-data-category3); background: var(--st-semantic-data-category3); }
279
+ .st-heatmapChart__cell--category4,
280
+ .st-heatmapChart__legendSwatch--category4 { fill: var(--st-semantic-data-category4); background: var(--st-semantic-data-category4); }
281
+ .st-heatmapChart__cell--category5,
282
+ .st-heatmapChart__legendSwatch--category5 { fill: var(--st-semantic-data-category5); background: var(--st-semantic-data-category5); }
283
+ .st-heatmapChart__cell--category6,
284
+ .st-heatmapChart__legendSwatch--category6 { fill: var(--st-semantic-data-category6); background: var(--st-semantic-data-category6); }
285
+ .st-heatmapChart__cell--category7,
286
+ .st-heatmapChart__legendSwatch--category7 { fill: var(--st-semantic-data-category7); background: var(--st-semantic-data-category7); }
287
+ .st-heatmapChart__cell--category8,
288
+ .st-heatmapChart__legendSwatch--category8 { fill: var(--st-semantic-data-category8); background: var(--st-semantic-data-category8); }
289
+
290
+ .st-heatmapChart__legend {
291
+ align-items: center;
292
+ display: flex;
293
+ gap: var(--st-spacing-2, 0.5rem);
294
+ margin-top: var(--st-spacing-2, 0.5rem);
295
+ }
296
+
297
+ .st-heatmapChart__legendRamp {
298
+ display: inline-grid;
299
+ grid-template-columns: repeat(8, minmax(0.75rem, 1fr));
300
+ min-width: 8rem;
301
+ }
302
+
303
+ .st-heatmapChart__legendSwatch {
304
+ display: block;
305
+ height: 0.5rem;
306
+ }
307
+
308
+ .st-heatmapChart__legendText {
309
+ color: var(--st-semantic-text-secondary);
310
+ font-size: 0.75rem;
311
+ }
312
+
313
+ .st-heatmapChart__tooltip {
314
+ background: var(--st-semantic-surface-inverse);
315
+ border-radius: var(--st-radius-sm, 0.25rem);
316
+ color: var(--st-semantic-text-inverse);
317
+ display: inline-flex;
318
+ flex-direction: column;
319
+ font-size: 0.75rem;
320
+ gap: 0.125rem;
321
+ line-height: 1.2;
322
+ padding: 0.375rem 0.5rem;
323
+ pointer-events: none;
324
+ position: absolute;
325
+ transform: translate(-50%, -115%);
326
+ white-space: nowrap;
327
+ z-index: 1;
328
+ }
329
+
330
+ .st-heatmapChart__tooltipLabel {
331
+ font-weight: 600;
332
+ }
333
+
334
+ .st-heatmapChart__tooltipValue {
335
+ opacity: 0.85;
336
+ }
337
+ </style>