@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.
@@ -0,0 +1,387 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * BumpChart - classements dans le temps (lignes qui montent/descendent).
4
+ * API canonique (référence Svelte, React/Vue doivent s'aligner)
5
+ *
6
+ * Props obligatoires :
7
+ * data BumpChartSeries[] - tableau {label, ranks: number[], tone?}
8
+ * categories string[] - libellés des périodes (axe X)
9
+ * label string - aria-label
10
+ *
11
+ * Props optionnelles :
12
+ * width number (défaut 480)
13
+ * height number (défaut 300)
14
+ * class string
15
+ *
16
+ * Convention : rank 1 = meilleur (affiché en haut).
17
+ * Rang invalide (non-entier, <1, null, undefined, NaN) → GAP (ni point ni segment).
18
+ */
19
+ export type BumpChartTone =
20
+ | "category1"
21
+ | "category2"
22
+ | "category3"
23
+ | "category4"
24
+ | "category5"
25
+ | "category6"
26
+ | "category7"
27
+ | "category8";
28
+
29
+ export type BumpChartSeries = {
30
+ label: string;
31
+ ranks: (number | null | undefined)[];
32
+ tone?: BumpChartTone;
33
+ };
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ import ChartDataList from "./ChartDataList.svelte";
38
+
39
+ const TONES: BumpChartTone[] = [
40
+ "category1","category2","category3","category4",
41
+ "category5","category6","category7","category8"
42
+ ];
43
+
44
+ type BumpChartProps = {
45
+ data: BumpChartSeries[];
46
+ categories: string[];
47
+ label: string;
48
+ width?: number;
49
+ height?: number;
50
+ class?: string;
51
+ };
52
+
53
+ let {
54
+ data = [],
55
+ categories = [],
56
+ label,
57
+ width = 480,
58
+ height = 300,
59
+ class: className
60
+ }: BumpChartProps = $props();
61
+
62
+ const MARGIN = { top: 16, right: 80, bottom: 32, left: 80 };
63
+
64
+ let hoveredIndex: number | null = $state(null);
65
+
66
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
67
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
68
+
69
+ // FIX #6 : valide un rang (entier ≥1 uniquement)
70
+ function isValidRank(r: unknown): r is number {
71
+ return typeof r === "number" && Number.isFinite(r) && Number.isInteger(r) && r >= 1;
72
+ }
73
+
74
+ const maxRank = $derived.by(() => {
75
+ let m = 1;
76
+ for (const s of data) {
77
+ for (const r of s.ranks) {
78
+ if (isValidRank(r) && r > m) m = r;
79
+ }
80
+ }
81
+ return m;
82
+ });
83
+
84
+ // FIX #6 : aligner ranks et categories : utiliser Math.min(ranks.length, categories.length)
85
+ const catCount = $derived(Math.max(categories.length, 2));
86
+
87
+ function rankToY(rank: number): number {
88
+ // rank 1 = top ; rank valide garanti par l'appelant
89
+ return MARGIN.top + ((rank - 1) / Math.max(maxRank - 1, 1)) * plotHeight;
90
+ }
91
+
92
+ function catToX(ci: number): number {
93
+ return MARGIN.left + (ci / Math.max(catCount - 1, 1)) * plotWidth;
94
+ }
95
+
96
+ /**
97
+ * Construit un path SVG avec GAP pour les rangs invalides.
98
+ * Un segment contenant un rang invalide n'est pas tracé.
99
+ */
100
+ function buildBumpPath(points: ({ x: number; y: number } | null)[]): string {
101
+ const parts: string[] = [];
102
+ let segment: { x: number; y: number }[] = [];
103
+
104
+ for (const pt of points) {
105
+ if (pt === null) {
106
+ if (segment.length >= 2) {
107
+ let d = `M${segment[0].x.toFixed(2)},${segment[0].y.toFixed(2)}`;
108
+ for (let i = 0; i < segment.length - 1; i++) {
109
+ const p1 = segment[i];
110
+ const p2 = segment[i + 1];
111
+ const mx = (p1.x + p2.x) / 2;
112
+ d += ` C${mx.toFixed(2)},${p1.y.toFixed(2)} ${mx.toFixed(2)},${p2.y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
113
+ }
114
+ parts.push(d);
115
+ } else if (segment.length === 1) {
116
+ parts.push(`M${segment[0].x.toFixed(2)},${segment[0].y.toFixed(2)}`);
117
+ }
118
+ segment = [];
119
+ } else {
120
+ segment.push(pt);
121
+ }
122
+ }
123
+ // flush dernier segment
124
+ if (segment.length >= 2) {
125
+ let d = `M${segment[0].x.toFixed(2)},${segment[0].y.toFixed(2)}`;
126
+ for (let i = 0; i < segment.length - 1; i++) {
127
+ const p1 = segment[i];
128
+ const p2 = segment[i + 1];
129
+ const mx = (p1.x + p2.x) / 2;
130
+ d += ` C${mx.toFixed(2)},${p1.y.toFixed(2)} ${mx.toFixed(2)},${p2.y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
131
+ }
132
+ parts.push(d);
133
+ } else if (segment.length === 1) {
134
+ parts.push(`M${segment[0].x.toFixed(2)},${segment[0].y.toFixed(2)}`);
135
+ }
136
+ return parts.join(" ");
137
+ }
138
+
139
+ const series = $derived(
140
+ data.map((s, si) => {
141
+ const tone = s.tone ?? TONES[si % TONES.length];
142
+ // FIX #6 : aligner ranks/categories ; rang invalide → null (GAP)
143
+ const alignedLength = Math.min(s.ranks.length, categories.length);
144
+ const points: ({ x: number; y: number } | null)[] = [];
145
+ for (let ci = 0; ci < alignedLength; ci++) {
146
+ const r = s.ranks[ci];
147
+ if (isValidRank(r)) {
148
+ points.push({ x: catToX(ci), y: rankToY(r) });
149
+ } else {
150
+ points.push(null); // GAP
151
+ }
152
+ }
153
+ return {
154
+ label: s.label,
155
+ ranks: s.ranks,
156
+ tone,
157
+ points,
158
+ index: si,
159
+ path: buildBumpPath(points),
160
+ alignedLength
161
+ };
162
+ })
163
+ );
164
+
165
+ const rankTicks = $derived.by(() => {
166
+ const ticks: number[] = [];
167
+ for (let r = 1; r <= maxRank; r++) ticks.push(r);
168
+ return ticks;
169
+ });
170
+
171
+ // FIX #6 : SR n'annonce pas #1 pour un rang absent (utilise "?" à la place)
172
+ const dataValueItems = $derived(
173
+ data.map((s) => {
174
+ const alignedLength = Math.min(s.ranks.length, categories.length);
175
+ return `${s.label}: ` + categories.slice(0, alignedLength).map((cat, ci) => {
176
+ const r = s.ranks[ci];
177
+ return `${cat} ${isValidRank(r) ? `#${r}` : "?"}`;
178
+ }).join(", ");
179
+ })
180
+ );
181
+
182
+ function handlePointerMove(event: PointerEvent) {
183
+ const target = event.target;
184
+ if (!(target instanceof Element)) { hoveredIndex = null; return; }
185
+ const idx = Number(target.getAttribute("data-chart-index"));
186
+ hoveredIndex = Number.isInteger(idx) ? idx : null;
187
+ }
188
+
189
+ const classes = () => ["st-bumpChart", className].filter(Boolean).join(" ");
190
+ </script>
191
+
192
+ <div class={classes()}>
193
+ <div
194
+ class="st-bumpChart__visual"
195
+ role="img"
196
+ aria-label={label}
197
+ onpointermove={handlePointerMove}
198
+ onpointerleave={() => (hoveredIndex = null)}
199
+ >
200
+ <svg
201
+ viewBox="0 0 {width} {height}"
202
+ preserveAspectRatio="xMidYMid meet"
203
+ width="100%"
204
+ height="100%"
205
+ focusable="false"
206
+ aria-hidden="true"
207
+ >
208
+ <!-- horizontal rank grid lines -->
209
+ {#each rankTicks as rank (rank)}
210
+ {@const ty = rankToY(rank)}
211
+ <line
212
+ class="st-bumpChart__grid"
213
+ x1={MARGIN.left}
214
+ x2={width - MARGIN.right}
215
+ y1={ty}
216
+ y2={ty}
217
+ />
218
+ <text
219
+ class="st-bumpChart__rankLabel"
220
+ x={MARGIN.left - 8}
221
+ y={ty}
222
+ text-anchor="end"
223
+ dominant-baseline="middle"
224
+ >
225
+ #{rank}
226
+ </text>
227
+ {/each}
228
+
229
+ <!-- category labels -->
230
+ {#each categories as cat, ci (cat)}
231
+ <text
232
+ class="st-bumpChart__catLabel"
233
+ x={catToX(ci)}
234
+ y={height - MARGIN.bottom + 16}
235
+ text-anchor="middle"
236
+ >
237
+ {cat}
238
+ </text>
239
+ {/each}
240
+
241
+ <!-- FIX #7 : clé composite pour éviter les doublons sur s.label -->
242
+ {#each series as s, si (`${si}-${s.label}`)}
243
+ <path
244
+ class="st-bumpChart__line st-bumpChart__line--{s.tone}"
245
+ class:st-bumpChart__line--dim={hoveredIndex !== null && hoveredIndex !== s.index}
246
+ class:st-bumpChart__line--active={hoveredIndex === s.index}
247
+ d={s.path}
248
+ fill="none"
249
+ data-chart-index={s.index}
250
+ />
251
+ <!-- FIX #6 : dots uniquement pour les rangs valides (non-null) -->
252
+ {#each s.points as pt, ci (`${si}-${s.label}-${ci}`)}
253
+ {#if pt !== null}
254
+ <circle
255
+ class="st-bumpChart__dot st-bumpChart__dot--{s.tone}"
256
+ class:st-bumpChart__dot--dim={hoveredIndex !== null && hoveredIndex !== s.index}
257
+ cx={pt.x}
258
+ cy={pt.y}
259
+ r="4"
260
+ data-chart-index={s.index}
261
+ />
262
+ {/if}
263
+ {/each}
264
+ <!-- end label : dernier point valide -->
265
+ {#if s.points.some(p => p !== null)}
266
+ {@const lastValidPt = [...s.points].reverse().find(p => p !== null)}
267
+ {#if lastValidPt}
268
+ <text
269
+ class="st-bumpChart__seriesLabel"
270
+ x={lastValidPt.x + 8}
271
+ y={lastValidPt.y}
272
+ dominant-baseline="middle"
273
+ >
274
+ {s.label}
275
+ </text>
276
+ {/if}
277
+ <!-- start label : premier point valide (si >1 point valide) -->
278
+ {@const firstValidPt = s.points.find(p => p !== null)}
279
+ {@const validCount = s.points.filter(p => p !== null).length}
280
+ {#if firstValidPt && validCount > 1}
281
+ <text
282
+ class="st-bumpChart__seriesLabel"
283
+ x={firstValidPt.x - 8}
284
+ y={firstValidPt.y}
285
+ text-anchor="end"
286
+ dominant-baseline="middle"
287
+ >
288
+ {s.label}
289
+ </text>
290
+ {/if}
291
+ {/if}
292
+ {/each}
293
+ </svg>
294
+ </div>
295
+
296
+ <ChartDataList {label} items={dataValueItems} />
297
+ </div>
298
+
299
+ <style>
300
+ .st-bumpChart {
301
+ color: var(--st-semantic-text-secondary);
302
+ display: block;
303
+ font-family: inherit;
304
+ position: relative;
305
+ width: 100%;
306
+ }
307
+
308
+ .st-bumpChart svg {
309
+ display: block;
310
+ overflow: visible;
311
+ }
312
+
313
+ .st-bumpChart__visual {
314
+ display: block;
315
+ }
316
+
317
+ .st-bumpChart__grid {
318
+ stroke: var(--st-semantic-border-subtle);
319
+ stroke-dasharray: 2 3;
320
+ stroke-width: 1;
321
+ opacity: 0.5;
322
+ }
323
+
324
+ .st-bumpChart__rankLabel,
325
+ .st-bumpChart__catLabel,
326
+ .st-bumpChart__seriesLabel {
327
+ fill: var(--st-semantic-text-secondary);
328
+ font-size: 0.6875rem;
329
+ }
330
+
331
+ .st-bumpChart__line {
332
+ cursor: pointer;
333
+ stroke-width: 2;
334
+ stroke-opacity: 0.7;
335
+ transition: stroke-opacity 120ms ease, stroke-width 120ms ease;
336
+ }
337
+
338
+ .st-bumpChart__line--dim {
339
+ stroke-opacity: 0.12;
340
+ }
341
+
342
+ .st-bumpChart__line--active {
343
+ stroke-opacity: 1;
344
+ stroke-width: 3;
345
+ }
346
+
347
+ @media (prefers-reduced-motion: reduce) {
348
+ .st-bumpChart__line {
349
+ transition: none;
350
+ }
351
+ }
352
+
353
+ .st-bumpChart__dot {
354
+ cursor: pointer;
355
+ stroke: var(--st-semantic-surface-default, Canvas);
356
+ stroke-width: 1.5;
357
+ transition: r 120ms ease;
358
+ }
359
+
360
+ .st-bumpChart__dot--dim {
361
+ opacity: 0.15;
362
+ }
363
+
364
+ @media (prefers-reduced-motion: reduce) {
365
+ .st-bumpChart__dot {
366
+ transition: none;
367
+ }
368
+ }
369
+
370
+ .st-bumpChart__line--category1 { stroke: var(--st-semantic-data-category1); }
371
+ .st-bumpChart__line--category2 { stroke: var(--st-semantic-data-category2); }
372
+ .st-bumpChart__line--category3 { stroke: var(--st-semantic-data-category3); }
373
+ .st-bumpChart__line--category4 { stroke: var(--st-semantic-data-category4); }
374
+ .st-bumpChart__line--category5 { stroke: var(--st-semantic-data-category5); }
375
+ .st-bumpChart__line--category6 { stroke: var(--st-semantic-data-category6); }
376
+ .st-bumpChart__line--category7 { stroke: var(--st-semantic-data-category7); }
377
+ .st-bumpChart__line--category8 { stroke: var(--st-semantic-data-category8); }
378
+
379
+ .st-bumpChart__dot--category1 { fill: var(--st-semantic-data-category1); }
380
+ .st-bumpChart__dot--category2 { fill: var(--st-semantic-data-category2); }
381
+ .st-bumpChart__dot--category3 { fill: var(--st-semantic-data-category3); }
382
+ .st-bumpChart__dot--category4 { fill: var(--st-semantic-data-category4); }
383
+ .st-bumpChart__dot--category5 { fill: var(--st-semantic-data-category5); }
384
+ .st-bumpChart__dot--category6 { fill: var(--st-semantic-data-category6); }
385
+ .st-bumpChart__dot--category7 { fill: var(--st-semantic-data-category7); }
386
+ .st-bumpChart__dot--category8 { fill: var(--st-semantic-data-category8); }
387
+ </style>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * BumpChart - classements dans le temps (lignes qui montent/descendent).
3
+ * API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * data BumpChartSeries[] - tableau {label, ranks: number[], tone?}
7
+ * categories string[] - libellés des périodes (axe X)
8
+ * label string - aria-label
9
+ *
10
+ * Props optionnelles :
11
+ * width number (défaut 480)
12
+ * height number (défaut 300)
13
+ * class string
14
+ *
15
+ * Convention : rank 1 = meilleur (affiché en haut).
16
+ * Rang invalide (non-entier, <1, null, undefined, NaN) → GAP (ni point ni segment).
17
+ */
18
+ export type BumpChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
19
+ export type BumpChartSeries = {
20
+ label: string;
21
+ ranks: (number | null | undefined)[];
22
+ tone?: BumpChartTone;
23
+ };
24
+ type BumpChartProps = {
25
+ data: BumpChartSeries[];
26
+ categories: string[];
27
+ label: string;
28
+ width?: number;
29
+ height?: number;
30
+ class?: string;
31
+ };
32
+ declare const BumpChart: import("svelte").Component<BumpChartProps, {}, "">;
33
+ type BumpChart = ReturnType<typeof BumpChart>;
34
+ export default BumpChart;
35
+ //# sourceMappingURL=BumpChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BumpChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BumpChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IACrC,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAMF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+MJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}