@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.
- package/dist/BoxPlotChart.svelte +302 -0
- package/dist/BoxPlotChart.svelte.d.ts +40 -0
- package/dist/BoxPlotChart.svelte.d.ts.map +1 -0
- package/dist/BulletChart.svelte +479 -0
- package/dist/BulletChart.svelte.d.ts +32 -0
- package/dist/BulletChart.svelte.d.ts.map +1 -0
- package/dist/BumpChart.svelte +387 -0
- package/dist/BumpChart.svelte.d.ts +35 -0
- package/dist/BumpChart.svelte.d.ts.map +1 -0
- package/dist/CalendarHeatmapChart.svelte +345 -0
- package/dist/CalendarHeatmapChart.svelte.d.ts +28 -0
- package/dist/CalendarHeatmapChart.svelte.d.ts.map +1 -0
- package/dist/CandlestickChart.svelte +333 -0
- package/dist/CandlestickChart.svelte.d.ts +31 -0
- package/dist/CandlestickChart.svelte.d.ts.map +1 -0
- package/dist/HeatmapChart.svelte +337 -0
- package/dist/HeatmapChart.svelte.d.ts +35 -0
- package/dist/HeatmapChart.svelte.d.ts.map +1 -0
- package/dist/HistogramChart.svelte +294 -0
- package/dist/HistogramChart.svelte.d.ts +38 -0
- package/dist/HistogramChart.svelte.d.ts.map +1 -0
- package/dist/MarimekkoChart.svelte +319 -0
- package/dist/MarimekkoChart.svelte.d.ts +35 -0
- package/dist/MarimekkoChart.svelte.d.ts.map +1 -0
- package/dist/ParallelCoordinatesChart.svelte +315 -0
- package/dist/ParallelCoordinatesChart.svelte.d.ts +35 -0
- package/dist/ParallelCoordinatesChart.svelte.d.ts.map +1 -0
- package/dist/RadarChart.svelte +340 -0
- package/dist/RadarChart.svelte.d.ts +43 -0
- package/dist/RadarChart.svelte.d.ts.map +1 -0
- package/dist/SankeyChart.svelte +364 -0
- package/dist/SankeyChart.svelte.d.ts +45 -0
- package/dist/SankeyChart.svelte.d.ts.map +1 -0
- package/dist/SunburstChart.svelte +388 -0
- package/dist/SunburstChart.svelte.d.ts +39 -0
- package/dist/SunburstChart.svelte.d.ts.map +1 -0
- package/dist/chartContrast.d.ts +0 -4
- package/dist/chartContrast.d.ts.map +1 -1
- package/dist/chartContrast.js +4 -56
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CandlestickChart - OHLC (open/high/low/close), bougies vertes/rouges.
|
|
3
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner)
|
|
4
|
+
*
|
|
5
|
+
* Props obligatoires :
|
|
6
|
+
* data CandlestickChartDatum[] - tableau {label, open, high, low, close}
|
|
7
|
+
* label string
|
|
8
|
+
*
|
|
9
|
+
* Props optionnelles :
|
|
10
|
+
* width number (défaut 480)
|
|
11
|
+
* height number (défaut 240)
|
|
12
|
+
* class string
|
|
13
|
+
*/
|
|
14
|
+
export type CandlestickChartDatum = {
|
|
15
|
+
label: string;
|
|
16
|
+
open: number;
|
|
17
|
+
high: number;
|
|
18
|
+
low: number;
|
|
19
|
+
close: number;
|
|
20
|
+
};
|
|
21
|
+
type CandlestickChartProps = {
|
|
22
|
+
data: CandlestickChartDatum[];
|
|
23
|
+
label: string;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
class?: string;
|
|
27
|
+
};
|
|
28
|
+
declare const CandlestickChart: import("svelte").Component<CandlestickChartProps, {}, "">;
|
|
29
|
+
type CandlestickChart = ReturnType<typeof CandlestickChart>;
|
|
30
|
+
export default CandlestickChart;
|
|
31
|
+
//# sourceMappingURL=CandlestickChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CandlestickChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CandlestickChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,qBAAqB,EAAE,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgLJ,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,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>
|
|
@@ -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>
|