@sentropic/design-system-svelte 0.19.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/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/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/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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,319 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* MarimekkoChart - rectangles largeur (part catégorie) × hauteur (segments %).
|
|
4
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner)
|
|
5
|
+
*
|
|
6
|
+
* Props obligatoires :
|
|
7
|
+
* data MarimekkoChartDatum[] - tableau {label, width, segments[]}
|
|
8
|
+
* label string
|
|
9
|
+
*
|
|
10
|
+
* Props optionnelles :
|
|
11
|
+
* width number (défaut 480)
|
|
12
|
+
* height number (défaut 300)
|
|
13
|
+
* class string
|
|
14
|
+
*/
|
|
15
|
+
export type MarimekkoChartTone =
|
|
16
|
+
| "category1"
|
|
17
|
+
| "category2"
|
|
18
|
+
| "category3"
|
|
19
|
+
| "category4"
|
|
20
|
+
| "category5"
|
|
21
|
+
| "category6"
|
|
22
|
+
| "category7"
|
|
23
|
+
| "category8";
|
|
24
|
+
|
|
25
|
+
export type MarimekkoChartSegment = {
|
|
26
|
+
label: string;
|
|
27
|
+
value: number;
|
|
28
|
+
tone?: MarimekkoChartTone;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type MarimekkoChartDatum = {
|
|
32
|
+
label: string;
|
|
33
|
+
width: number;
|
|
34
|
+
segments: MarimekkoChartSegment[];
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
40
|
+
import { contrastTextForTone } from "./chartContrast";
|
|
41
|
+
|
|
42
|
+
const TONES: MarimekkoChartTone[] = [
|
|
43
|
+
"category1","category2","category3","category4",
|
|
44
|
+
"category5","category6","category7","category8"
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
type MarimekkoChartProps = {
|
|
48
|
+
data: MarimekkoChartDatum[];
|
|
49
|
+
label: string;
|
|
50
|
+
width?: number;
|
|
51
|
+
height?: number;
|
|
52
|
+
class?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
data = [],
|
|
57
|
+
label,
|
|
58
|
+
width = 480,
|
|
59
|
+
height = 300,
|
|
60
|
+
class: className
|
|
61
|
+
}: MarimekkoChartProps = $props();
|
|
62
|
+
|
|
63
|
+
const MARGIN = { top: 12, right: 16, bottom: 32, left: 8 };
|
|
64
|
+
|
|
65
|
+
let hoveredKey: string | null = $state(null);
|
|
66
|
+
|
|
67
|
+
const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
|
|
68
|
+
const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
|
|
69
|
+
|
|
70
|
+
// FIX #4 : ignorer (skip) largeurs non-finies ou <=0, PAS de Math.abs
|
|
71
|
+
const totalWidth = $derived.by(() => {
|
|
72
|
+
const sum = data.reduce((acc, d) => {
|
|
73
|
+
const w = d.width;
|
|
74
|
+
return acc + (Number.isFinite(w) && w > 0 ? w : 0);
|
|
75
|
+
}, 0);
|
|
76
|
+
return sum > 0 ? sum : 1;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const cells = $derived.by(() => {
|
|
80
|
+
let xCursor = MARGIN.left;
|
|
81
|
+
const result: {
|
|
82
|
+
key: string;
|
|
83
|
+
catLabel: string;
|
|
84
|
+
segLabel: string;
|
|
85
|
+
tone: MarimekkoChartTone;
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
w: number;
|
|
89
|
+
h: number;
|
|
90
|
+
cx: number;
|
|
91
|
+
cy: number;
|
|
92
|
+
pct: number;
|
|
93
|
+
colPct: number;
|
|
94
|
+
}[] = [];
|
|
95
|
+
|
|
96
|
+
for (const datum of data) {
|
|
97
|
+
const safeW = (Number.isFinite(datum.width) && datum.width > 0) ? datum.width : 0;
|
|
98
|
+
// FIX #4 : skip colonnes invalides (largeur 0 ou non-finie)
|
|
99
|
+
if (safeW <= 0) continue;
|
|
100
|
+
const colW = (safeW / totalWidth) * plotWidth;
|
|
101
|
+
const colPct = safeW / totalWidth;
|
|
102
|
+
|
|
103
|
+
// FIX #4 : ignorer segments non-finis ou <=0 (PAS de Math.abs, PAS de plancher 0.5px)
|
|
104
|
+
const validSegs = datum.segments.filter((s) => Number.isFinite(s.value) && s.value > 0);
|
|
105
|
+
const segSum = validSegs.reduce((acc, s) => acc + s.value, 0);
|
|
106
|
+
const safeSum = segSum > 0 ? segSum : 1;
|
|
107
|
+
|
|
108
|
+
let yCursor = MARGIN.top;
|
|
109
|
+
for (let si = 0; si < validSegs.length; si++) {
|
|
110
|
+
const seg = validSegs[si];
|
|
111
|
+
const pct = seg.value / safeSum;
|
|
112
|
+
const segH = pct * plotHeight;
|
|
113
|
+
const tone = seg.tone ?? TONES[si % TONES.length];
|
|
114
|
+
result.push({
|
|
115
|
+
key: `${datum.label}-${seg.label}`,
|
|
116
|
+
catLabel: datum.label,
|
|
117
|
+
segLabel: seg.label,
|
|
118
|
+
tone,
|
|
119
|
+
x: xCursor,
|
|
120
|
+
y: yCursor,
|
|
121
|
+
// FIX #4 : pas de plancher min 0.5px pour les zéros (ils sont filtrés)
|
|
122
|
+
w: Math.max(colW - 1, 1),
|
|
123
|
+
h: Math.max(segH, 1),
|
|
124
|
+
cx: xCursor + colW / 2,
|
|
125
|
+
cy: yCursor + segH / 2,
|
|
126
|
+
pct,
|
|
127
|
+
colPct
|
|
128
|
+
});
|
|
129
|
+
yCursor += segH;
|
|
130
|
+
}
|
|
131
|
+
xCursor += colW;
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// FIX #4 a11y : SR inclut la part de LARGEUR (colPct) en plus du % de segment
|
|
137
|
+
const dataValueItems = $derived(
|
|
138
|
+
cells.map((c) => `${c.catLabel}, ${c.segLabel}: ${Math.round(c.pct * 100)}% (colonne ${Math.round(c.colPct * 100)}%)`)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
function handlePointerMove(event: PointerEvent) {
|
|
142
|
+
const target = event.target;
|
|
143
|
+
if (!(target instanceof Element)) { hoveredKey = null; return; }
|
|
144
|
+
hoveredKey = target.getAttribute("data-chart-key") ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const hoveredCell = $derived(hoveredKey !== null ? cells.find((c) => c.key === hoveredKey) ?? null : null);
|
|
148
|
+
|
|
149
|
+
const classes = () => ["st-marimekkoChart", className].filter(Boolean).join(" ");
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<div class={classes()}>
|
|
153
|
+
<div
|
|
154
|
+
class="st-marimekkoChart__visual"
|
|
155
|
+
role="img"
|
|
156
|
+
aria-label={label}
|
|
157
|
+
onpointermove={handlePointerMove}
|
|
158
|
+
onpointerleave={() => (hoveredKey = null)}
|
|
159
|
+
>
|
|
160
|
+
<svg
|
|
161
|
+
viewBox="0 0 {width} {height}"
|
|
162
|
+
preserveAspectRatio="xMidYMid meet"
|
|
163
|
+
width="100%"
|
|
164
|
+
height="100%"
|
|
165
|
+
focusable="false"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
>
|
|
168
|
+
<!-- axis -->
|
|
169
|
+
<line
|
|
170
|
+
class="st-marimekkoChart__axis"
|
|
171
|
+
x1={MARGIN.left}
|
|
172
|
+
x2={width - MARGIN.right}
|
|
173
|
+
y1={height - MARGIN.bottom}
|
|
174
|
+
y2={height - MARGIN.bottom}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
<!-- cells -->
|
|
178
|
+
{#each cells as cell (cell.key)}
|
|
179
|
+
<rect
|
|
180
|
+
class="st-marimekkoChart__cell st-marimekkoChart__cell--{cell.tone}"
|
|
181
|
+
class:st-marimekkoChart__cell--dim={hoveredKey !== null && hoveredKey !== cell.key}
|
|
182
|
+
x={cell.x}
|
|
183
|
+
y={cell.y}
|
|
184
|
+
width={cell.w}
|
|
185
|
+
height={cell.h}
|
|
186
|
+
data-chart-key={cell.key}
|
|
187
|
+
/>
|
|
188
|
+
{#if cell.w > 28 && cell.h > 14}
|
|
189
|
+
<text
|
|
190
|
+
class="st-marimekkoChart__cellLabel"
|
|
191
|
+
x={cell.cx}
|
|
192
|
+
y={cell.cy}
|
|
193
|
+
text-anchor="middle"
|
|
194
|
+
dominant-baseline="middle"
|
|
195
|
+
style="fill: {contrastTextForTone(cell.tone)}"
|
|
196
|
+
pointer-events="none"
|
|
197
|
+
>
|
|
198
|
+
{Math.round(cell.pct * 100)}%
|
|
199
|
+
</text>
|
|
200
|
+
{/if}
|
|
201
|
+
{/each}
|
|
202
|
+
|
|
203
|
+
<!-- category labels below axis -->
|
|
204
|
+
{#each data as datum, i (datum.label)}
|
|
205
|
+
{#if Number.isFinite(datum.width) && datum.width > 0}
|
|
206
|
+
{@const safeW = datum.width}
|
|
207
|
+
{@const colW = (safeW / totalWidth) * plotWidth}
|
|
208
|
+
{@const startX = cells.find(c => c.catLabel === datum.label)?.x ?? MARGIN.left}
|
|
209
|
+
<text
|
|
210
|
+
class="st-marimekkoChart__catLabel"
|
|
211
|
+
x={startX + colW / 2}
|
|
212
|
+
y={height - MARGIN.bottom + 16}
|
|
213
|
+
text-anchor="middle"
|
|
214
|
+
>
|
|
215
|
+
{datum.label}
|
|
216
|
+
</text>
|
|
217
|
+
{/if}
|
|
218
|
+
{/each}
|
|
219
|
+
</svg>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<ChartDataList {label} items={dataValueItems} />
|
|
223
|
+
|
|
224
|
+
{#if hoveredCell !== null}
|
|
225
|
+
<div
|
|
226
|
+
class="st-marimekkoChart__tooltip"
|
|
227
|
+
role="presentation"
|
|
228
|
+
style="left: {(hoveredCell.cx / width) * 100}%; top: {(hoveredCell.cy / height) * 100}%"
|
|
229
|
+
>
|
|
230
|
+
<span class="st-marimekkoChart__tooltipLabel">{hoveredCell.catLabel} / {hoveredCell.segLabel}</span>
|
|
231
|
+
<span class="st-marimekkoChart__tooltipValue">{Math.round(hoveredCell.pct * 100)}%</span>
|
|
232
|
+
</div>
|
|
233
|
+
{/if}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<style>
|
|
237
|
+
.st-marimekkoChart {
|
|
238
|
+
color: var(--st-semantic-text-secondary);
|
|
239
|
+
display: block;
|
|
240
|
+
font-family: inherit;
|
|
241
|
+
position: relative;
|
|
242
|
+
width: 100%;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.st-marimekkoChart svg {
|
|
246
|
+
display: block;
|
|
247
|
+
overflow: visible;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.st-marimekkoChart__visual {
|
|
251
|
+
display: block;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.st-marimekkoChart__axis {
|
|
255
|
+
stroke: var(--st-semantic-border-subtle);
|
|
256
|
+
stroke-width: 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.st-marimekkoChart__cell {
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
stroke: var(--st-semantic-surface-default, Canvas);
|
|
262
|
+
stroke-width: 1;
|
|
263
|
+
transition: opacity 120ms ease;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.st-marimekkoChart__cell--dim {
|
|
267
|
+
opacity: 0.4;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (prefers-reduced-motion: reduce) {
|
|
271
|
+
.st-marimekkoChart__cell {
|
|
272
|
+
transition: none;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.st-marimekkoChart__cell--category1 { fill: var(--st-semantic-data-category1); }
|
|
277
|
+
.st-marimekkoChart__cell--category2 { fill: var(--st-semantic-data-category2); }
|
|
278
|
+
.st-marimekkoChart__cell--category3 { fill: var(--st-semantic-data-category3); }
|
|
279
|
+
.st-marimekkoChart__cell--category4 { fill: var(--st-semantic-data-category4); }
|
|
280
|
+
.st-marimekkoChart__cell--category5 { fill: var(--st-semantic-data-category5); }
|
|
281
|
+
.st-marimekkoChart__cell--category6 { fill: var(--st-semantic-data-category6); }
|
|
282
|
+
.st-marimekkoChart__cell--category7 { fill: var(--st-semantic-data-category7); }
|
|
283
|
+
.st-marimekkoChart__cell--category8 { fill: var(--st-semantic-data-category8); }
|
|
284
|
+
|
|
285
|
+
.st-marimekkoChart__cellLabel {
|
|
286
|
+
font-size: 0.625rem;
|
|
287
|
+
pointer-events: none;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.st-marimekkoChart__catLabel {
|
|
291
|
+
fill: var(--st-semantic-text-secondary);
|
|
292
|
+
font-size: 0.6875rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.st-marimekkoChart__tooltip {
|
|
296
|
+
background: var(--st-semantic-surface-inverse);
|
|
297
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
298
|
+
color: var(--st-semantic-text-inverse);
|
|
299
|
+
display: inline-flex;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
font-size: 0.75rem;
|
|
302
|
+
gap: 0.125rem;
|
|
303
|
+
line-height: 1.2;
|
|
304
|
+
padding: 0.375rem 0.5rem;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
position: absolute;
|
|
307
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
308
|
+
white-space: nowrap;
|
|
309
|
+
z-index: 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.st-marimekkoChart__tooltipLabel {
|
|
313
|
+
font-weight: 600;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.st-marimekkoChart__tooltipValue {
|
|
317
|
+
opacity: 0.85;
|
|
318
|
+
}
|
|
319
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarimekkoChart - rectangles largeur (part catégorie) × hauteur (segments %).
|
|
3
|
+
* API canonique (référence Svelte, React/Vue doivent s'aligner)
|
|
4
|
+
*
|
|
5
|
+
* Props obligatoires :
|
|
6
|
+
* data MarimekkoChartDatum[] - tableau {label, width, segments[]}
|
|
7
|
+
* label string
|
|
8
|
+
*
|
|
9
|
+
* Props optionnelles :
|
|
10
|
+
* width number (défaut 480)
|
|
11
|
+
* height number (défaut 300)
|
|
12
|
+
* class string
|
|
13
|
+
*/
|
|
14
|
+
export type MarimekkoChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
|
|
15
|
+
export type MarimekkoChartSegment = {
|
|
16
|
+
label: string;
|
|
17
|
+
value: number;
|
|
18
|
+
tone?: MarimekkoChartTone;
|
|
19
|
+
};
|
|
20
|
+
export type MarimekkoChartDatum = {
|
|
21
|
+
label: string;
|
|
22
|
+
width: number;
|
|
23
|
+
segments: MarimekkoChartSegment[];
|
|
24
|
+
};
|
|
25
|
+
type MarimekkoChartProps = {
|
|
26
|
+
data: MarimekkoChartDatum[];
|
|
27
|
+
label: string;
|
|
28
|
+
width?: number;
|
|
29
|
+
height?: number;
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
declare const MarimekkoChart: import("svelte").Component<MarimekkoChartProps, {}, "">;
|
|
33
|
+
type MarimekkoChart = ReturnType<typeof MarimekkoChart>;
|
|
34
|
+
export default MarimekkoChart;
|
|
35
|
+
//# sourceMappingURL=MarimekkoChart.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarimekkoChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/MarimekkoChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,kBAAkB,GAC1B,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,qBAAqB,EAAE,CAAC;CACnC,CAAC;AAOF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA0JJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -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>
|