@sentropic/design-system-svelte 0.17.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.
Files changed (41) 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/Calendar.svelte +237 -42
  5. package/dist/Calendar.svelte.d.ts.map +1 -1
  6. package/dist/HeatmapChart.svelte +337 -0
  7. package/dist/HeatmapChart.svelte.d.ts +35 -0
  8. package/dist/HeatmapChart.svelte.d.ts.map +1 -0
  9. package/dist/HistogramChart.svelte +294 -0
  10. package/dist/HistogramChart.svelte.d.ts +38 -0
  11. package/dist/HistogramChart.svelte.d.ts.map +1 -0
  12. package/dist/Popper.svelte +157 -0
  13. package/dist/Popper.svelte.d.ts +17 -0
  14. package/dist/Popper.svelte.d.ts.map +1 -1
  15. package/dist/RadarChart.svelte +340 -0
  16. package/dist/RadarChart.svelte.d.ts +43 -0
  17. package/dist/RadarChart.svelte.d.ts.map +1 -0
  18. package/dist/Rating.svelte +130 -35
  19. package/dist/Rating.svelte.d.ts.map +1 -1
  20. package/dist/SankeyChart.svelte +364 -0
  21. package/dist/SankeyChart.svelte.d.ts +45 -0
  22. package/dist/SankeyChart.svelte.d.ts.map +1 -0
  23. package/dist/SelectableList.svelte +60 -12
  24. package/dist/SelectableList.svelte.d.ts.map +1 -1
  25. package/dist/SelectableRow.svelte +23 -8
  26. package/dist/SelectableRow.svelte.d.ts +5 -4
  27. package/dist/SelectableRow.svelte.d.ts.map +1 -1
  28. package/dist/SlideIndicator.svelte +17 -3
  29. package/dist/SlideIndicator.svelte.d.ts.map +1 -1
  30. package/dist/SunburstChart.svelte +388 -0
  31. package/dist/SunburstChart.svelte.d.ts +39 -0
  32. package/dist/SunburstChart.svelte.d.ts.map +1 -0
  33. package/dist/TimePicker.svelte +176 -13
  34. package/dist/TimePicker.svelte.d.ts.map +1 -1
  35. package/dist/chartContrast.d.ts +0 -4
  36. package/dist/chartContrast.d.ts.map +1 -1
  37. package/dist/chartContrast.js +4 -56
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +6 -0
  41. package/package.json +1 -1
@@ -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>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * HeatmapChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
3
+ *
4
+ * Props obligatoires :
5
+ * data HeatmapChartDatum[] - tableau {x, y, value, tone?}
6
+ * label string - aria-label du graphique
7
+ *
8
+ * Props optionnelles :
9
+ * legend boolean (défaut false) - affiche le gradient Low→High
10
+ * width number (défaut 480) - largeur du viewBox en px
11
+ * height number (défaut 300) - hauteur du viewBox en px
12
+ * class string - classe CSS supplémentaire
13
+ *
14
+ * NaN/vide : les valeurs non-finies sont exclues du calcul min/max (gardé par
15
+ * Number.isFinite). Tableau vide ou tout-NaN → rendu vide sans crash.
16
+ */
17
+ export type HeatmapChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
18
+ export type HeatmapChartDatum = {
19
+ x: string;
20
+ y: string;
21
+ value: number;
22
+ tone?: HeatmapChartTone;
23
+ };
24
+ type HeatmapChartProps = {
25
+ data: HeatmapChartDatum[];
26
+ label: string;
27
+ legend?: boolean;
28
+ width?: number;
29
+ height?: number;
30
+ class?: string;
31
+ };
32
+ declare const HeatmapChart: import("svelte").Component<HeatmapChartProps, {}, "">;
33
+ type HeatmapChart = ReturnType<typeof HeatmapChart>;
34
+ export default HeatmapChart;
35
+ //# sourceMappingURL=HeatmapChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HeatmapChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/HeatmapChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;GAeG;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,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAMF,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqJJ,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,294 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * HistogramChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * data HistogramChartDatum[] | number[]
7
+ * - si tableau de nombres : bins numériques calculés automatiquement
8
+ * - si tableau de {label, value, tone?} : bins pré-calculés (passthrough)
9
+ * label string - aria-label du graphique
10
+ *
11
+ * Props optionnelles :
12
+ * bins number (défaut 10) - nombre de bins pour le mode numérique.
13
+ * Défaut fixe = 10 (pas ceil(√n)) pour parité
14
+ * Svelte/React/Vue et prédictibilité.
15
+ * width number (défaut 480) - largeur du viewBox en px
16
+ * height number (défaut 240) - hauteur du viewBox en px
17
+ * class string - classe CSS supplémentaire
18
+ *
19
+ * NaN/vide : les valeurs non-finies sont exclues avant le binning (filter
20
+ * Number.isFinite). Tableau vide → rendu vide sans crash.
21
+ */
22
+ export type HistogramChartTone =
23
+ | "category1"
24
+ | "category2"
25
+ | "category3"
26
+ | "category4"
27
+ | "category5"
28
+ | "category6"
29
+ | "category7"
30
+ | "category8";
31
+
32
+ export type HistogramChartDatum = {
33
+ label: string;
34
+ value: number;
35
+ tone?: HistogramChartTone;
36
+ };
37
+ </script>
38
+
39
+ <script lang="ts">
40
+ import ChartDataList from "./ChartDataList.svelte";
41
+
42
+ type HistogramChartProps = {
43
+ data: HistogramChartDatum[] | number[];
44
+ bins?: number;
45
+ label: string;
46
+ width?: number;
47
+ height?: number;
48
+ class?: string;
49
+ };
50
+
51
+ type HistogramBin = {
52
+ label: string;
53
+ value: number;
54
+ tone: HistogramChartTone;
55
+ };
56
+
57
+ let {
58
+ data,
59
+ bins,
60
+ label,
61
+ width = 480,
62
+ height = 240,
63
+ class: className
64
+ }: HistogramChartProps = $props();
65
+
66
+ const MARGIN = { top: 14, right: 16, bottom: 36, left: 44 };
67
+ const TONES = [
68
+ "category1",
69
+ "category2",
70
+ "category3",
71
+ "category4",
72
+ "category5",
73
+ "category6",
74
+ "category7",
75
+ "category8"
76
+ ] as const;
77
+
78
+ function isNumberArray(values: HistogramChartDatum[] | number[]): values is number[] {
79
+ return values.every((value) => typeof value === "number");
80
+ }
81
+
82
+ function formatNumber(value: number): string {
83
+ if (!Number.isFinite(value)) return "0";
84
+ if (Number.isInteger(value)) return String(value);
85
+ return value.toFixed(2).replace(/\.?0+$/, "");
86
+ }
87
+
88
+ function buildNumericBins(values: number[], count: number): HistogramBin[] {
89
+ const finite = values.filter(Number.isFinite);
90
+ if (finite.length === 0) return [];
91
+ const binCount = Math.max(1, Math.floor(count));
92
+ const min = Math.min(...finite);
93
+ const max = Math.max(...finite);
94
+ const step = max === min ? 1 : (max - min) / binCount;
95
+ const out = Array.from({ length: binCount }, (_, index) => {
96
+ const start = min + step * index;
97
+ const end = index === binCount - 1 ? max : min + step * (index + 1);
98
+ return {
99
+ label: `${formatNumber(start)}-${formatNumber(end)}`,
100
+ value: 0,
101
+ tone: TONES[index % TONES.length]
102
+ };
103
+ });
104
+
105
+ for (const value of finite) {
106
+ const index = value === max ? binCount - 1 : Math.max(0, Math.min(binCount - 1, Math.floor((value - min) / step)));
107
+ out[index].value += 1;
108
+ }
109
+
110
+ return out;
111
+ }
112
+
113
+ function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
114
+ if (d1 === d0) return r0;
115
+ return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
116
+ }
117
+
118
+ let hoveredIndex: number | null = $state(null);
119
+
120
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
121
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
122
+
123
+ const normalizedBins = $derived.by<HistogramBin[]>(() => {
124
+ if (isNumberArray(data)) {
125
+ return buildNumericBins(data, bins ?? 10);
126
+ }
127
+
128
+ return data.map((datum, index) => ({
129
+ label: datum.label,
130
+ value: Number.isFinite(datum.value) ? datum.value : 0,
131
+ tone: datum.tone ?? TONES[index % TONES.length]
132
+ }));
133
+ });
134
+
135
+ const bars = $derived.by(() => {
136
+ const maxValue = Math.max(0, ...normalizedBins.map((bin) => bin.value));
137
+ const safeMax = maxValue > 0 ? maxValue : 1;
138
+ const band = normalizedBins.length > 0 ? plotWidth / normalizedBins.length : plotWidth;
139
+ const barWidth = Math.max(band * 0.68, 1);
140
+
141
+ return normalizedBins.map((bin, index) => {
142
+ const h = scaleLinear(bin.value, 0, safeMax, 0, plotHeight);
143
+ return {
144
+ bin,
145
+ x: MARGIN.left + band * index + (band - barWidth) / 2,
146
+ y: MARGIN.top + plotHeight - h,
147
+ width: barWidth,
148
+ height: Math.max(h, 0.5),
149
+ labelX: MARGIN.left + band * (index + 0.5)
150
+ };
151
+ });
152
+ });
153
+
154
+ const dataValueItems = $derived(normalizedBins.map((bin) => `${bin.label}: ${bin.value}`));
155
+
156
+ function handleVisualPointerMove(event: PointerEvent) {
157
+ const target = event.target;
158
+ if (!(target instanceof Element)) {
159
+ hoveredIndex = null;
160
+ return;
161
+ }
162
+ const index = Number(target.getAttribute("data-chart-index"));
163
+ hoveredIndex = Number.isInteger(index) ? index : null;
164
+ }
165
+
166
+ const classes = () => ["st-histogramChart", className].filter(Boolean).join(" ");
167
+ </script>
168
+
169
+ <div class={classes()}>
170
+ <div
171
+ class="st-histogramChart__visual"
172
+ role="img"
173
+ aria-label={label}
174
+ onpointermove={handleVisualPointerMove}
175
+ onpointerleave={() => (hoveredIndex = null)}
176
+ >
177
+ <svg
178
+ viewBox="0 0 {width} {height}"
179
+ preserveAspectRatio="xMidYMid meet"
180
+ width="100%"
181
+ height="100%"
182
+ focusable="false"
183
+ aria-hidden="true"
184
+ >
185
+ <line class="st-histogramChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
186
+ <line class="st-histogramChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
187
+
188
+ {#each bars as bar, i (bar.bin.label)}
189
+ <rect
190
+ class="st-histogramChart__bar st-histogramChart__bar--{bar.bin.tone}"
191
+ class:st-histogramChart__bar--dim={hoveredIndex !== null && hoveredIndex !== i}
192
+ x={bar.x}
193
+ y={bar.y}
194
+ width={bar.width}
195
+ height={bar.height}
196
+ data-chart-index={i}
197
+ />
198
+ <text class="st-histogramChart__label" x={bar.labelX} y={height - MARGIN.bottom + 16} text-anchor="middle">
199
+ {bar.bin.label}
200
+ </text>
201
+ {/each}
202
+ </svg>
203
+ </div>
204
+
205
+ <ChartDataList {label} items={dataValueItems} />
206
+
207
+ {#if hoveredIndex !== null && bars[hoveredIndex]}
208
+ {@const bar = bars[hoveredIndex]}
209
+ <div
210
+ class="st-histogramChart__tooltip"
211
+ role="presentation"
212
+ style="left: {(bar.labelX / width) * 100}%; top: {(bar.y / height) * 100}%"
213
+ >
214
+ <span class="st-histogramChart__tooltipLabel">{bar.bin.label}</span>
215
+ <span class="st-histogramChart__tooltipValue">{bar.bin.value}</span>
216
+ </div>
217
+ {/if}
218
+ </div>
219
+
220
+ <style>
221
+ .st-histogramChart {
222
+ color: var(--st-semantic-text-secondary);
223
+ display: block;
224
+ font-family: inherit;
225
+ max-width: 100%;
226
+ position: relative;
227
+ width: 100%;
228
+ }
229
+
230
+ .st-histogramChart svg,
231
+ .st-histogramChart__visual {
232
+ display: block;
233
+ overflow: visible;
234
+ }
235
+
236
+ .st-histogramChart__axis {
237
+ stroke: var(--st-semantic-border-subtle);
238
+ stroke-width: 1;
239
+ }
240
+
241
+ .st-histogramChart__label {
242
+ fill: var(--st-semantic-text-secondary);
243
+ font-size: 0.7rem;
244
+ }
245
+
246
+ .st-histogramChart__bar {
247
+ cursor: pointer;
248
+ transition: opacity 120ms ease;
249
+ }
250
+
251
+ .st-histogramChart__bar--dim {
252
+ opacity: 0.45;
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .st-histogramChart__bar {
257
+ transition: none;
258
+ }
259
+ }
260
+
261
+ .st-histogramChart__bar--category1 { fill: var(--st-semantic-data-category1); }
262
+ .st-histogramChart__bar--category2 { fill: var(--st-semantic-data-category2); }
263
+ .st-histogramChart__bar--category3 { fill: var(--st-semantic-data-category3); }
264
+ .st-histogramChart__bar--category4 { fill: var(--st-semantic-data-category4); }
265
+ .st-histogramChart__bar--category5 { fill: var(--st-semantic-data-category5); }
266
+ .st-histogramChart__bar--category6 { fill: var(--st-semantic-data-category6); }
267
+ .st-histogramChart__bar--category7 { fill: var(--st-semantic-data-category7); }
268
+ .st-histogramChart__bar--category8 { fill: var(--st-semantic-data-category8); }
269
+
270
+ .st-histogramChart__tooltip {
271
+ background: var(--st-semantic-surface-inverse);
272
+ border-radius: var(--st-radius-sm, 0.25rem);
273
+ color: var(--st-semantic-text-inverse);
274
+ display: inline-flex;
275
+ flex-direction: column;
276
+ font-size: 0.75rem;
277
+ gap: 0.125rem;
278
+ line-height: 1.2;
279
+ padding: 0.375rem 0.5rem;
280
+ pointer-events: none;
281
+ position: absolute;
282
+ transform: translate(-50%, -115%);
283
+ white-space: nowrap;
284
+ z-index: 1;
285
+ }
286
+
287
+ .st-histogramChart__tooltipLabel {
288
+ font-weight: 600;
289
+ }
290
+
291
+ .st-histogramChart__tooltipValue {
292
+ opacity: 0.85;
293
+ }
294
+ </style>
@@ -0,0 +1,38 @@
1
+ /**
2
+ * HistogramChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
3
+ *
4
+ * Props obligatoires :
5
+ * data HistogramChartDatum[] | number[]
6
+ * - si tableau de nombres : bins numériques calculés automatiquement
7
+ * - si tableau de {label, value, tone?} : bins pré-calculés (passthrough)
8
+ * label string - aria-label du graphique
9
+ *
10
+ * Props optionnelles :
11
+ * bins number (défaut 10) - nombre de bins pour le mode numérique.
12
+ * Défaut fixe = 10 (pas ceil(√n)) pour parité
13
+ * Svelte/React/Vue et prédictibilité.
14
+ * width number (défaut 480) - largeur du viewBox en px
15
+ * height number (défaut 240) - hauteur du viewBox en px
16
+ * class string - classe CSS supplémentaire
17
+ *
18
+ * NaN/vide : les valeurs non-finies sont exclues avant le binning (filter
19
+ * Number.isFinite). Tableau vide → rendu vide sans crash.
20
+ */
21
+ export type HistogramChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
22
+ export type HistogramChartDatum = {
23
+ label: string;
24
+ value: number;
25
+ tone?: HistogramChartTone;
26
+ };
27
+ type HistogramChartProps = {
28
+ data: HistogramChartDatum[] | number[];
29
+ bins?: number;
30
+ label: string;
31
+ width?: number;
32
+ height?: number;
33
+ class?: string;
34
+ };
35
+ declare const HistogramChart: import("svelte").Component<HistogramChartProps, {}, "">;
36
+ type HistogramChart = ReturnType<typeof HistogramChart>;
37
+ export default HistogramChart;
38
+ //# sourceMappingURL=HistogramChart.svelte.d.ts.map