@sentropic/design-system-svelte 0.34.33 → 0.34.35

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,451 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * StateTimelineChart - bandes d'états DISCRETS dans le temps (façon Grafana
4
+ * « state timeline » / Highcharts xrange). Une ligne (series) = une séquence
5
+ * de segments contigus qui pavent l'axe temporel X ; la couleur encode l'état.
6
+ * API canonique (référence Svelte, React/Vue doivent s'aligner).
7
+ *
8
+ * Modèle : une lane par series, empilées verticalement. Chaque segment a un
9
+ * OFFSET de départ (x0 = start) et une largeur (end - start) sur un axe X
10
+ * commun gradué (niceTicks). La couleur suit `tone` si fourni, sinon dérive
11
+ * une teinte stable par `state` (cycle sur category1..8). Étiquette de series à
12
+ * gauche (ellipsis). Légende des états sous le graphe.
13
+ *
14
+ * Props obligatoires :
15
+ * data StateTimelineSeries[] - tableau {series, segments[]}
16
+ *
17
+ * Props optionnelles :
18
+ * label string
19
+ * width number (défaut 640)
20
+ * height number (défaut 320)
21
+ * class string
22
+ */
23
+ export type StateTimelineTone =
24
+ | "neutral" | "info" | "success" | "warning" | "error"
25
+ | "category1" | "category2" | "category3" | "category4"
26
+ | "category5" | "category6" | "category7" | "category8";
27
+
28
+ export type StateTimelineSegment = {
29
+ start: number;
30
+ end: number;
31
+ state: string | number;
32
+ tone?: StateTimelineTone;
33
+ };
34
+
35
+ export type StateTimelineSeries = {
36
+ series: string;
37
+ segments: StateTimelineSegment[];
38
+ };
39
+ </script>
40
+
41
+ <script lang="ts">
42
+ import ChartDataList from "./ChartDataList.svelte";
43
+
44
+ type StateTimelineChartProps = {
45
+ data: StateTimelineSeries[];
46
+ label?: string;
47
+ width?: number;
48
+ height?: number;
49
+ class?: string;
50
+ };
51
+
52
+ let {
53
+ data = [],
54
+ label,
55
+ width = 640,
56
+ height = 320,
57
+ class: className
58
+ }: StateTimelineChartProps = $props();
59
+
60
+ const MARGIN = { top: 16, right: 16, bottom: 32, left: 132 };
61
+
62
+ function scaleLinear(v: number, d0: number, d1: number, r0: number, r1: number) {
63
+ if (d1 === d0) return r0;
64
+ return r0 + ((v - d0) * (r1 - r0)) / (d1 - d0);
65
+ }
66
+
67
+ function niceTicks(min: number, max: number, target = 5): number[] {
68
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {
69
+ const base = Number.isFinite(max) ? max : 0;
70
+ return [base];
71
+ }
72
+ const range = max - min;
73
+ const rough = range / Math.max(target - 1, 1);
74
+ const pow = Math.pow(10, Math.floor(Math.log10(rough)));
75
+ const norm = rough / pow;
76
+ let step: number;
77
+ if (norm < 1.5) step = 1 * pow;
78
+ else if (norm < 3) step = 2 * pow;
79
+ else if (norm < 7) step = 5 * pow;
80
+ else step = 10 * pow;
81
+ const start = Math.floor(min / step) * step;
82
+ const end = Math.ceil(max / step) * step;
83
+ const ticks: number[] = [];
84
+ for (let v = start; v <= end + step / 2; v += step) {
85
+ ticks.push(Number(v.toFixed(10)));
86
+ }
87
+ return ticks;
88
+ }
89
+
90
+ function formatTick(v: number): string {
91
+ if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
92
+ if (Number.isInteger(v)) return String(v);
93
+ return v.toFixed(1);
94
+ }
95
+
96
+ // Tronque une étiquette à la largeur de la marge gauche (approx. par char).
97
+ function ellipsize(text: string, maxChars: number): string {
98
+ if (text.length <= maxChars) return text;
99
+ if (maxChars <= 1) return "…";
100
+ return `${text.slice(0, maxChars - 1)}…`;
101
+ }
102
+
103
+ let hoveredKey: string | null = $state(null);
104
+
105
+ const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
106
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
107
+
108
+ // Normalise start ≤ end, filtre les segments non finis et les lanes sans
109
+ // libellé. Une lane sans segment valide est conservée (ligne vide).
110
+ const validData = $derived(
111
+ data
112
+ .filter((d) => typeof d.series === "string" && d.series.length > 0)
113
+ .map((d) => ({
114
+ series: d.series,
115
+ segments: (d.segments ?? [])
116
+ .filter((s) => Number.isFinite(s.start) && Number.isFinite(s.end))
117
+ .map((s) => ({
118
+ start: Math.min(s.start, s.end),
119
+ end: Math.max(s.start, s.end),
120
+ state: s.state,
121
+ tone: s.tone
122
+ }))
123
+ }))
124
+ );
125
+
126
+ // États distincts (ordre d'apparition) → index categoryN (1..8, cyclé) si
127
+ // aucun `tone` explicite. Un `tone` sur un segment l'emporte sur la dérivation.
128
+ const stateOrder = $derived.by(() => {
129
+ const seen: string[] = [];
130
+ for (const d of validData) {
131
+ for (const s of d.segments) {
132
+ const key = String(s.state);
133
+ if (!seen.includes(key)) seen.push(key);
134
+ }
135
+ }
136
+ return seen;
137
+ });
138
+ const explicitToneByState = $derived.by(() => {
139
+ const map = new Map<string, StateTimelineTone>();
140
+ for (const d of validData) {
141
+ for (const s of d.segments) {
142
+ if (s.tone) map.set(String(s.state), s.tone);
143
+ }
144
+ }
145
+ return map;
146
+ });
147
+ const toneOf = (segment: { state: string | number; tone?: StateTimelineTone }): StateTimelineTone => {
148
+ if (segment.tone) return segment.tone;
149
+ const key = String(segment.state);
150
+ const explicit = explicitToneByState.get(key);
151
+ if (explicit) return explicit;
152
+ const idx = stateOrder.indexOf(key);
153
+ return `category${((idx < 0 ? 0 : idx) % 8) + 1}` as StateTimelineTone;
154
+ };
155
+ const legendItems = $derived(
156
+ stateOrder.map((state) => ({ state, tone: toneOf({ state }) }))
157
+ );
158
+ const hasLegend = $derived(stateOrder.length > 0);
159
+
160
+ const domainBounds = $derived.by(() => {
161
+ const vals: number[] = [];
162
+ for (const d of validData) {
163
+ for (const s of d.segments) vals.push(s.start, s.end);
164
+ }
165
+ if (vals.length === 0) return { rawMin: 0, rawMax: 1 };
166
+ const rawMin = Math.min(...vals);
167
+ const rawMax = Math.max(...vals);
168
+ return { rawMin, rawMax: rawMax === rawMin ? rawMin + 1 : rawMax };
169
+ });
170
+
171
+ const ticks = $derived(niceTicks(domainBounds.rawMin, domainBounds.rawMax, 5));
172
+ const domainMin = $derived(ticks[0]);
173
+ const domainMax = $derived(ticks[ticks.length - 1]);
174
+
175
+ const xOf = $derived(
176
+ (v: number) => MARGIN.left + scaleLinear(v, domainMin, domainMax, 0, plotWidth)
177
+ );
178
+
179
+ const lanes = $derived.by(() => {
180
+ if (validData.length === 0) return [];
181
+ const band = plotHeight / validData.length;
182
+ const laneHeight = Math.min(band * 0.62, 28);
183
+ return validData.map((d, i) => {
184
+ const y = MARGIN.top + band * i + (band - laneHeight) / 2;
185
+ const segments = d.segments.map((s, j) => {
186
+ const x0 = xOf(s.start);
187
+ const x1 = xOf(s.end);
188
+ const x = Math.min(x0, x1);
189
+ const w = Math.max(Math.abs(x1 - x0), 1);
190
+ return {
191
+ key: `${i}-${j}`,
192
+ datum: s,
193
+ x,
194
+ width: w,
195
+ cx: x + w / 2,
196
+ tone: toneOf(s)
197
+ };
198
+ });
199
+ return {
200
+ datum: d,
201
+ index: i,
202
+ y,
203
+ height: laneHeight,
204
+ rowCenterY: MARGIN.top + band * (i + 0.5),
205
+ segments
206
+ };
207
+ });
208
+ });
209
+
210
+ const dataValueItems = $derived(
211
+ validData.map(
212
+ (d) =>
213
+ `${d.series}: ${d.segments.map((s) => `${s.state} [${s.start} → ${s.end}]`).join(", ")}`
214
+ )
215
+ );
216
+
217
+ function handlePointerMove(event: PointerEvent) {
218
+ const target = event.target;
219
+ if (!(target instanceof Element)) {
220
+ hoveredKey = null;
221
+ return;
222
+ }
223
+ const key = target.getAttribute("data-chart-key");
224
+ hoveredKey = key ?? null;
225
+ }
226
+
227
+ const hoveredSegment = $derived.by(() => {
228
+ if (hoveredKey === null) return null;
229
+ for (const lane of lanes) {
230
+ for (const seg of lane.segments) {
231
+ if (seg.key === hoveredKey) return { lane, seg };
232
+ }
233
+ }
234
+ return null;
235
+ });
236
+
237
+ const classes = () => ["st-stateTimelineChart", className].filter(Boolean).join(" ");
238
+ </script>
239
+
240
+ <div class={classes()}>
241
+ <div
242
+ class="st-stateTimelineChart__visual"
243
+ role="img"
244
+ aria-label={label}
245
+ onpointermove={handlePointerMove}
246
+ onpointerleave={() => (hoveredKey = null)}
247
+ >
248
+ <svg
249
+ viewBox="0 0 {width} {height}"
250
+ preserveAspectRatio="xMidYMid meet"
251
+ width="100%"
252
+ height="100%"
253
+ focusable="false"
254
+ aria-hidden="true"
255
+ >
256
+ <!-- gridlines + tick labels (axe X temporel) -->
257
+ {#each ticks as tick (tick)}
258
+ {@const tx = xOf(tick)}
259
+ <line class="st-stateTimelineChart__grid" x1={tx} x2={tx} y1={MARGIN.top} y2={height - MARGIN.bottom} />
260
+ <text class="st-stateTimelineChart__tickLabel" x={tx} y={height - MARGIN.bottom + 16} text-anchor="middle">
261
+ {formatTick(tick)}
262
+ </text>
263
+ {/each}
264
+
265
+ <!-- axes -->
266
+ <line class="st-stateTimelineChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
267
+ <line class="st-stateTimelineChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
268
+
269
+ <!-- une lane par series : étiquette à gauche + segments d'états contigus -->
270
+ {#each lanes as lane (`${lane.index}-${lane.datum.series}`)}
271
+ <text
272
+ class="st-stateTimelineChart__seriesLabel"
273
+ x={MARGIN.left - 8}
274
+ y={lane.rowCenterY}
275
+ text-anchor="end"
276
+ dominant-baseline="middle"
277
+ >
278
+ {ellipsize(lane.datum.series, 18)}
279
+ </text>
280
+ {#each lane.segments as seg (seg.key)}
281
+ <rect
282
+ class="st-stateTimelineChart__segment st-stateTimelineChart__segment--{seg.tone}"
283
+ class:st-stateTimelineChart__segment--dim={hoveredKey !== null && hoveredKey !== seg.key}
284
+ x={seg.x}
285
+ y={lane.y}
286
+ width={seg.width}
287
+ height={lane.height}
288
+ rx="2"
289
+ data-chart-key={seg.key}
290
+ />
291
+ {/each}
292
+ {/each}
293
+ </svg>
294
+ </div>
295
+
296
+ {#if hasLegend}
297
+ <ul class="st-stateTimelineChart__legend" aria-label={`États de ${label ?? "timeline"}`}>
298
+ {#each legendItems as item (item.state)}
299
+ <li class="st-stateTimelineChart__legendItem">
300
+ <span class="st-stateTimelineChart__legendSwatch st-stateTimelineChart__legendSwatch--{item.tone}" aria-hidden="true"></span>
301
+ {item.state}
302
+ </li>
303
+ {/each}
304
+ </ul>
305
+ {/if}
306
+
307
+ <ChartDataList label={label ?? "state timeline"} items={dataValueItems} />
308
+
309
+ {#if hoveredSegment}
310
+ {@const seg = hoveredSegment.seg}
311
+ {@const lane = hoveredSegment.lane}
312
+ <div
313
+ class="st-stateTimelineChart__tooltip"
314
+ role="presentation"
315
+ style="left: {(seg.cx / width) * 100}%; top: {(lane.rowCenterY / height) * 100}%"
316
+ >
317
+ <span class="st-stateTimelineChart__tooltipLabel">{lane.datum.series} · {seg.datum.state}</span>
318
+ <span class="st-stateTimelineChart__tooltipValue">{seg.datum.start} → {seg.datum.end}</span>
319
+ </div>
320
+ {/if}
321
+ </div>
322
+
323
+ <style>
324
+ .st-stateTimelineChart {
325
+ color: var(--st-semantic-text-secondary);
326
+ display: block;
327
+ font-family: inherit;
328
+ position: relative;
329
+ width: 100%;
330
+ }
331
+
332
+ .st-stateTimelineChart svg {
333
+ display: block;
334
+ overflow: visible;
335
+ }
336
+
337
+ .st-stateTimelineChart__visual {
338
+ display: block;
339
+ }
340
+
341
+ .st-stateTimelineChart__axis {
342
+ stroke: var(--st-semantic-border-subtle);
343
+ stroke-width: 1;
344
+ }
345
+
346
+ .st-stateTimelineChart__grid {
347
+ stroke: var(--st-semantic-border-subtle);
348
+ stroke-dasharray: 2 3;
349
+ stroke-width: 1;
350
+ opacity: 0.7;
351
+ }
352
+
353
+ .st-stateTimelineChart__tickLabel,
354
+ .st-stateTimelineChart__seriesLabel {
355
+ fill: var(--st-semantic-text-secondary);
356
+ font-size: 0.6875rem;
357
+ }
358
+
359
+ .st-stateTimelineChart__segment {
360
+ cursor: pointer;
361
+ transition: opacity 120ms ease;
362
+ }
363
+
364
+ .st-stateTimelineChart__segment--dim {
365
+ opacity: 0.4;
366
+ }
367
+
368
+ .st-stateTimelineChart__segment--neutral { fill: var(--st-semantic-border-strong, var(--st-semantic-surface-subtle)); }
369
+ .st-stateTimelineChart__segment--info { fill: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
370
+ .st-stateTimelineChart__segment--success { fill: var(--st-semantic-feedback-success); }
371
+ .st-stateTimelineChart__segment--warning { fill: var(--st-semantic-feedback-warning); }
372
+ .st-stateTimelineChart__segment--error { fill: var(--st-semantic-feedback-error); }
373
+
374
+ .st-stateTimelineChart__segment--category1 { fill: var(--st-semantic-data-category1); }
375
+ .st-stateTimelineChart__segment--category2 { fill: var(--st-semantic-data-category2); }
376
+ .st-stateTimelineChart__segment--category3 { fill: var(--st-semantic-data-category3); }
377
+ .st-stateTimelineChart__segment--category4 { fill: var(--st-semantic-data-category4); }
378
+ .st-stateTimelineChart__segment--category5 { fill: var(--st-semantic-data-category5); }
379
+ .st-stateTimelineChart__segment--category6 { fill: var(--st-semantic-data-category6); }
380
+ .st-stateTimelineChart__segment--category7 { fill: var(--st-semantic-data-category7); }
381
+ .st-stateTimelineChart__segment--category8 { fill: var(--st-semantic-data-category8); }
382
+
383
+ .st-stateTimelineChart__legend {
384
+ display: flex;
385
+ flex-wrap: wrap;
386
+ gap: var(--st-spacing-3, 0.75rem);
387
+ list-style: none;
388
+ margin: var(--st-spacing-2, 0.5rem) 0 0 0;
389
+ padding: 0;
390
+ }
391
+
392
+ .st-stateTimelineChart__legendItem {
393
+ align-items: center;
394
+ color: var(--st-semantic-text-secondary);
395
+ display: inline-flex;
396
+ font-size: 0.75rem;
397
+ gap: var(--st-spacing-1, 0.25rem);
398
+ line-height: 1;
399
+ }
400
+
401
+ .st-stateTimelineChart__legendSwatch {
402
+ border-radius: var(--st-radius-sm, 0.25rem);
403
+ display: inline-block;
404
+ height: 0.625rem;
405
+ width: 0.625rem;
406
+ }
407
+ .st-stateTimelineChart__legendSwatch--neutral { background: var(--st-semantic-border-strong, var(--st-semantic-surface-subtle)); }
408
+ .st-stateTimelineChart__legendSwatch--info { background: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
409
+ .st-stateTimelineChart__legendSwatch--success { background: var(--st-semantic-feedback-success); }
410
+ .st-stateTimelineChart__legendSwatch--warning { background: var(--st-semantic-feedback-warning); }
411
+ .st-stateTimelineChart__legendSwatch--error { background: var(--st-semantic-feedback-error); }
412
+ .st-stateTimelineChart__legendSwatch--category1 { background: var(--st-semantic-data-category1); }
413
+ .st-stateTimelineChart__legendSwatch--category2 { background: var(--st-semantic-data-category2); }
414
+ .st-stateTimelineChart__legendSwatch--category3 { background: var(--st-semantic-data-category3); }
415
+ .st-stateTimelineChart__legendSwatch--category4 { background: var(--st-semantic-data-category4); }
416
+ .st-stateTimelineChart__legendSwatch--category5 { background: var(--st-semantic-data-category5); }
417
+ .st-stateTimelineChart__legendSwatch--category6 { background: var(--st-semantic-data-category6); }
418
+ .st-stateTimelineChart__legendSwatch--category7 { background: var(--st-semantic-data-category7); }
419
+ .st-stateTimelineChart__legendSwatch--category8 { background: var(--st-semantic-data-category8); }
420
+
421
+ .st-stateTimelineChart__tooltip {
422
+ background: var(--st-semantic-surface-inverse);
423
+ border-radius: var(--st-radius-sm, 0.25rem);
424
+ color: var(--st-semantic-text-inverse);
425
+ display: inline-flex;
426
+ flex-direction: column;
427
+ font-size: 0.75rem;
428
+ gap: 0.125rem;
429
+ line-height: 1.2;
430
+ padding: 0.375rem 0.5rem;
431
+ pointer-events: none;
432
+ position: absolute;
433
+ transform: translate(-50%, calc(-100% - 8px));
434
+ white-space: nowrap;
435
+ z-index: 1;
436
+ }
437
+
438
+ .st-stateTimelineChart__tooltipLabel {
439
+ font-weight: 600;
440
+ }
441
+
442
+ .st-stateTimelineChart__tooltipValue {
443
+ opacity: 0.85;
444
+ }
445
+
446
+ @media (prefers-reduced-motion: reduce) {
447
+ .st-stateTimelineChart__segment {
448
+ transition: none;
449
+ }
450
+ }
451
+ </style>
@@ -0,0 +1,43 @@
1
+ /**
2
+ * StateTimelineChart - bandes d'états DISCRETS dans le temps (façon Grafana
3
+ * « state timeline » / Highcharts xrange). Une ligne (series) = une séquence
4
+ * de segments contigus qui pavent l'axe temporel X ; la couleur encode l'état.
5
+ * API canonique (référence Svelte, React/Vue doivent s'aligner).
6
+ *
7
+ * Modèle : une lane par series, empilées verticalement. Chaque segment a un
8
+ * OFFSET de départ (x0 = start) et une largeur (end - start) sur un axe X
9
+ * commun gradué (niceTicks). La couleur suit `tone` si fourni, sinon dérive
10
+ * une teinte stable par `state` (cycle sur category1..8). Étiquette de series à
11
+ * gauche (ellipsis). Légende des états sous le graphe.
12
+ *
13
+ * Props obligatoires :
14
+ * data StateTimelineSeries[] - tableau {series, segments[]}
15
+ *
16
+ * Props optionnelles :
17
+ * label string
18
+ * width number (défaut 640)
19
+ * height number (défaut 320)
20
+ * class string
21
+ */
22
+ export type StateTimelineTone = "neutral" | "info" | "success" | "warning" | "error" | "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
23
+ export type StateTimelineSegment = {
24
+ start: number;
25
+ end: number;
26
+ state: string | number;
27
+ tone?: StateTimelineTone;
28
+ };
29
+ export type StateTimelineSeries = {
30
+ series: string;
31
+ segments: StateTimelineSegment[];
32
+ };
33
+ type StateTimelineChartProps = {
34
+ data: StateTimelineSeries[];
35
+ label?: string;
36
+ width?: number;
37
+ height?: number;
38
+ class?: string;
39
+ };
40
+ declare const StateTimelineChart: import("svelte").Component<StateTimelineChartProps, {}, "">;
41
+ type StateTimelineChart = ReturnType<typeof StateTimelineChart>;
42
+ export default StateTimelineChart;
43
+ //# sourceMappingURL=StateTimelineChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StateTimelineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StateTimelineChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GACpD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC,CAAC;AAMF,KAAK,uBAAuB,GAAG;IAC7B,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA0PJ,QAAA,MAAM,kBAAkB,6DAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}