@sentropic/design-system-svelte 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/BoxPlotChart.svelte +302 -0
  2. package/dist/BoxPlotChart.svelte.d.ts +40 -0
  3. package/dist/BoxPlotChart.svelte.d.ts.map +1 -0
  4. package/dist/Calendar.svelte +237 -42
  5. package/dist/Calendar.svelte.d.ts.map +1 -1
  6. package/dist/HeatmapChart.svelte +337 -0
  7. package/dist/HeatmapChart.svelte.d.ts +35 -0
  8. package/dist/HeatmapChart.svelte.d.ts.map +1 -0
  9. package/dist/HistogramChart.svelte +294 -0
  10. package/dist/HistogramChart.svelte.d.ts +38 -0
  11. package/dist/HistogramChart.svelte.d.ts.map +1 -0
  12. package/dist/Popper.svelte +157 -0
  13. package/dist/Popper.svelte.d.ts +17 -0
  14. package/dist/Popper.svelte.d.ts.map +1 -1
  15. package/dist/RadarChart.svelte +340 -0
  16. package/dist/RadarChart.svelte.d.ts +43 -0
  17. package/dist/RadarChart.svelte.d.ts.map +1 -0
  18. package/dist/Rating.svelte +130 -35
  19. package/dist/Rating.svelte.d.ts.map +1 -1
  20. package/dist/SankeyChart.svelte +364 -0
  21. package/dist/SankeyChart.svelte.d.ts +45 -0
  22. package/dist/SankeyChart.svelte.d.ts.map +1 -0
  23. package/dist/SelectableList.svelte +60 -12
  24. package/dist/SelectableList.svelte.d.ts.map +1 -1
  25. package/dist/SelectableRow.svelte +23 -8
  26. package/dist/SelectableRow.svelte.d.ts +5 -4
  27. package/dist/SelectableRow.svelte.d.ts.map +1 -1
  28. package/dist/SlideIndicator.svelte +17 -3
  29. package/dist/SlideIndicator.svelte.d.ts.map +1 -1
  30. package/dist/SunburstChart.svelte +388 -0
  31. package/dist/SunburstChart.svelte.d.ts +39 -0
  32. package/dist/SunburstChart.svelte.d.ts.map +1 -0
  33. package/dist/TimePicker.svelte +176 -13
  34. package/dist/TimePicker.svelte.d.ts.map +1 -1
  35. package/dist/chartContrast.d.ts +0 -4
  36. package/dist/chartContrast.d.ts.map +1 -1
  37. package/dist/chartContrast.js +4 -56
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +6 -0
  41. package/package.json +1 -1
@@ -49,8 +49,21 @@
49
49
  const stars = $derived(Array.from({ length: max }, (_, i) => i + 1));
50
50
 
51
51
  // L'étoile « focusable » (tabindex 0) suit la valeur ; à 0 c'est la première.
52
+ // En mode allowHalf, la valeur peut être un demi-entier : on focus l'étoile plafond.
52
53
  const focusedStar = $derived(value > 0 ? Math.ceil(value) : 1);
53
54
 
55
+ // Refs des boutons radio pour déplacer le focus programmatiquement.
56
+ let radioRefs = $state<Record<number, HTMLElement | null>>({});
57
+
58
+ // Texte accessible décrivant la valeur courante (utilisé pour aria-valuetext et aria-label readonly).
59
+ const valueText = $derived(
60
+ value === 0
61
+ ? `0 / ${max}`
62
+ : allowHalf && value % 1 !== 0
63
+ ? `${value} / ${max}`
64
+ : `${value} / ${max}`
65
+ );
66
+
54
67
  function fill(star: number): "full" | "half" | "empty" {
55
68
  if (value >= star) return "full";
56
69
  if (allowHalf && value >= star - 0.5) return "half";
@@ -85,64 +98,140 @@
85
98
  if (readonly) return;
86
99
  const step = allowHalf ? 0.5 : 1;
87
100
  let handled = true;
101
+ let next: number | null = null;
88
102
  switch (event.key) {
89
103
  case "ArrowRight":
90
104
  case "ArrowUp":
91
- commit(Math.min(max, value + step));
105
+ next = Math.min(max, value + step);
92
106
  break;
93
107
  case "ArrowLeft":
94
108
  case "ArrowDown":
95
- commit(Math.max(0, value - step));
109
+ // En mode entier, ne pas descendre sous 1 (pas de radio "0").
110
+ next = allowHalf ? Math.max(0, value - step) : Math.max(1, value - step);
96
111
  break;
97
112
  case "Home":
98
- commit(0);
113
+ // Home → première étoile (1), pas 0 (aucun radio "0" n'existe).
114
+ next = allowHalf ? 0 : 1;
99
115
  break;
100
116
  case "End":
101
- commit(max);
117
+ next = max;
102
118
  break;
103
119
  default:
104
120
  handled = false;
105
121
  }
106
- if (handled) event.preventDefault();
122
+ if (handled) {
123
+ event.preventDefault();
124
+ if (next !== null) {
125
+ commit(next);
126
+ // En mode entier, déplacer le focus DOM vers le radio cible.
127
+ if (!allowHalf) {
128
+ const targetStar = next > 0 ? Math.ceil(next) : 1;
129
+ const targetEl = radioRefs[targetStar];
130
+ if (targetEl) targetEl.focus();
131
+ }
132
+ }
133
+ }
107
134
  }
135
+
136
+ // En mode allowHalf, on expose un slider ARIA (valeurs fractionnaires non représentables
137
+ // fidèlement par un radiogroup). En mode entier, on garde radiogroup/radio.
138
+ // Readonly : rendu non interactif avec span + aria-label global pour éviter les boutons disabled
139
+ // qui disparaissent de l'arbre d'accessibilité interactif.
108
140
  </script>
109
141
 
110
- <div
111
- {...rest}
112
- class={classes}
113
- role="radiogroup"
114
- aria-label={label}
115
- aria-readonly={readonly ? "true" : undefined}
116
- >
117
- {#each stars as star (star)}
118
- {@const state = fill(star)}
119
- <button
120
- type="button"
121
- class="st-rating__star"
122
- class:st-rating__star--full={state === "full"}
123
- class:st-rating__star--half={state === "half"}
124
- role="radio"
125
- name={name}
126
- aria-checked={Math.ceil(value) === star ? "true" : "false"}
127
- aria-label={`${star} / ${max}`}
128
- tabindex={!readonly && star === focusedStar ? 0 : -1}
129
- disabled={readonly}
130
- onclick={(event) => onStarClick(event, star)}
131
- onkeydown={onKeyDown}
132
- >
133
- {#if state === "half"}
134
- <StarHalf size={iconSize} strokeWidth={1.75} aria-hidden="true" />
135
- {:else}
142
+ {#if readonly}
143
+ <!-- Readonly : pas d'éléments interactifs disabled — on expose la note via aria-label sur le groupe. -->
144
+ <div
145
+ {...rest}
146
+ class={classes}
147
+ role="img"
148
+ aria-label={label ? `${label} : ${valueText}` : valueText}
149
+ >
150
+ {#each stars as star (star)}
151
+ {@const state = fill(star)}
152
+ <span class="st-rating__star" class:st-rating__star--full={state === "full"} class:st-rating__star--half={state === "half"} aria-hidden="true">
153
+ {#if state === "half"}
154
+ <StarHalf size={iconSize} strokeWidth={1.75} aria-hidden="true" />
155
+ {:else}
156
+ <Star
157
+ size={iconSize}
158
+ strokeWidth={1.75}
159
+ fill={state === "full" ? "currentColor" : "none"}
160
+ aria-hidden="true"
161
+ />
162
+ {/if}
163
+ </span>
164
+ {/each}
165
+ </div>
166
+ {:else if allowHalf}
167
+ <!-- allowHalf : slider ARIA — valeurs fractionnaires (0.5 step), plus fidèle que radiogroup. -->
168
+ <div
169
+ {...rest}
170
+ class={classes}
171
+ role="slider"
172
+ aria-label={label}
173
+ aria-valuemin={0}
174
+ aria-valuemax={max}
175
+ aria-valuenow={value}
176
+ aria-valuetext={valueText}
177
+ tabindex={0}
178
+ onkeydown={onKeyDown}
179
+ >
180
+ {#each stars as star (star)}
181
+ {@const state = fill(star)}
182
+ <span
183
+ class="st-rating__star"
184
+ class:st-rating__star--full={state === "full"}
185
+ class:st-rating__star--half={state === "half"}
186
+ aria-hidden="true"
187
+ onclick={(event) => onStarClick(event, star)}
188
+ >
189
+ {#if state === "half"}
190
+ <StarHalf size={iconSize} strokeWidth={1.75} aria-hidden="true" />
191
+ {:else}
192
+ <Star
193
+ size={iconSize}
194
+ strokeWidth={1.75}
195
+ fill={state === "full" ? "currentColor" : "none"}
196
+ aria-hidden="true"
197
+ />
198
+ {/if}
199
+ </span>
200
+ {/each}
201
+ </div>
202
+ {:else}
203
+ <!-- Mode entier : radiogroup / radio. aria-checked=true uniquement sur l'étoile == value. -->
204
+ <div
205
+ {...rest}
206
+ class={classes}
207
+ role="radiogroup"
208
+ aria-label={label}
209
+ >
210
+ {#each stars as star (star)}
211
+ {@const state = fill(star)}
212
+ <button
213
+ type="button"
214
+ class="st-rating__star"
215
+ class:st-rating__star--full={state === "full"}
216
+ role="radio"
217
+ name={name}
218
+ aria-checked={value === star ? "true" : "false"}
219
+ aria-label={`${star} / ${max}`}
220
+ tabindex={star === focusedStar ? 0 : -1}
221
+ bind:this={radioRefs[star]}
222
+ onclick={(event) => onStarClick(event, star)}
223
+ onkeydown={onKeyDown}
224
+ >
136
225
  <Star
137
226
  size={iconSize}
138
227
  strokeWidth={1.75}
139
228
  fill={state === "full" ? "currentColor" : "none"}
140
229
  aria-hidden="true"
141
230
  />
142
- {/if}
143
- </button>
144
- {/each}
145
- </div>
231
+ </button>
232
+ {/each}
233
+ </div>
234
+ {/if}
146
235
 
147
236
  <style>
148
237
  .st-rating {
@@ -186,4 +275,10 @@
186
275
  .st-rating--readonly .st-rating__star {
187
276
  cursor: default;
188
277
  }
278
+
279
+ /* Mode allowHalf : le slider (conteneur) doit afficher un focus-visible. */
280
+ [role="slider"].st-rating:focus-visible {
281
+ outline: 2px solid var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
282
+ outline-offset: 2px;
283
+ }
189
284
  </style>
@@ -1 +1 @@
1
- {"version":3,"file":"Rating.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Rating.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAI9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAC9E,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA2GJ,QAAA,MAAM,MAAM,iDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Rating.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Rating.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAI9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAC9E,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAsKJ,QAAA,MAAM,MAAM,iDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,364 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * SankeyChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * nodes SankeyChartNode[] - liste des nœuds {id, label, tone?}
7
+ * links SankeyChartLink[] - liste des liens {source, target, value, tone?}
8
+ * source/target = id d'un nœud existant ;
9
+ * les liens orphelins (nœud absent) sont rendus
10
+ * avec un fallback (pas de drop silencieux)
11
+ * label string - aria-label du graphique
12
+ *
13
+ * Props optionnelles :
14
+ * width number (défaut 560) - largeur du viewBox en px
15
+ * height number (défaut 280) - hauteur du viewBox en px
16
+ * class string - classe CSS supplémentaire
17
+ *
18
+ * Layout :
19
+ * Hauteur d'un nœud = max(valeurs entrantes sommées, valeurs sortantes sommées)
20
+ * - conservation de flux : un nœud agrégateur occupe autant que la somme
21
+ * de ses flux, pas juste le max d'un lien individuel.
22
+ */
23
+ export type SankeyChartTone =
24
+ | "category1"
25
+ | "category2"
26
+ | "category3"
27
+ | "category4"
28
+ | "category5"
29
+ | "category6"
30
+ | "category7"
31
+ | "category8";
32
+
33
+ export type SankeyChartNode = {
34
+ id: string;
35
+ label: string;
36
+ tone?: SankeyChartTone;
37
+ };
38
+
39
+ export type SankeyChartLink = {
40
+ source: string;
41
+ target: string;
42
+ value: number;
43
+ tone?: SankeyChartTone;
44
+ };
45
+ </script>
46
+
47
+ <script lang="ts">
48
+ import ChartDataList from "./ChartDataList.svelte";
49
+
50
+ type SankeyChartProps = {
51
+ nodes: SankeyChartNode[];
52
+ links: SankeyChartLink[];
53
+ label: string;
54
+ width?: number;
55
+ height?: number;
56
+ class?: string;
57
+ };
58
+
59
+ let {
60
+ nodes,
61
+ links,
62
+ label,
63
+ width = 560,
64
+ height = 280,
65
+ class: className
66
+ }: SankeyChartProps = $props();
67
+
68
+ const MARGIN = { top: 18, right: 26, bottom: 18, left: 26 };
69
+ const NODE_WIDTH = 14;
70
+ const TONES = [
71
+ "category1",
72
+ "category2",
73
+ "category3",
74
+ "category4",
75
+ "category5",
76
+ "category6",
77
+ "category7",
78
+ "category8"
79
+ ] as const;
80
+
81
+ function magnitude(value: number): number {
82
+ return Number.isFinite(value) && value > 0 ? value : 0;
83
+ }
84
+
85
+ function nodeDepths(): Map<string, number> {
86
+ const depths = new Map(nodes.map((node) => [node.id, 0]));
87
+ for (let pass = 0; pass < nodes.length; pass += 1) {
88
+ let changed = false;
89
+ for (const link of links) {
90
+ const sourceDepth = depths.get(link.source) ?? 0;
91
+ const targetDepth = depths.get(link.target) ?? 0;
92
+ if (sourceDepth + 1 > targetDepth) {
93
+ depths.set(link.target, sourceDepth + 1);
94
+ changed = true;
95
+ }
96
+ }
97
+ if (!changed) break;
98
+ }
99
+ return depths;
100
+ }
101
+
102
+ let hoveredLinkIndex: number | null = $state(null);
103
+
104
+ const nodeById = $derived(new Map(nodes.map((node) => [node.id, node])));
105
+
106
+ // Conservation de flux : hauteur nœud = max(Σ flux sortants, Σ flux entrants)
107
+ const nodeValues = $derived.by(() => {
108
+ const valueOut = new Map<string, number>();
109
+ const valueIn = new Map<string, number>();
110
+ for (const node of nodes) {
111
+ valueOut.set(node.id, 0);
112
+ valueIn.set(node.id, 0);
113
+ }
114
+ for (const link of links) {
115
+ const value = magnitude(link.value);
116
+ valueOut.set(link.source, (valueOut.get(link.source) ?? 0) + value);
117
+ valueIn.set(link.target, (valueIn.get(link.target) ?? 0) + value);
118
+ }
119
+ const values = new Map<string, number>();
120
+ for (const node of nodes) {
121
+ values.set(node.id, Math.max(valueOut.get(node.id) ?? 0, valueIn.get(node.id) ?? 0));
122
+ }
123
+ return values;
124
+ });
125
+
126
+ const layout = $derived.by(() => {
127
+ const depths = nodeDepths();
128
+ const maxDepth = Math.max(0, ...Array.from(depths.values()));
129
+ const plotWidth = Math.max(width - MARGIN.left - MARGIN.right - NODE_WIDTH, 1);
130
+ const plotHeight = Math.max(height - MARGIN.top - MARGIN.bottom, 1);
131
+ const maxNodeValue = Math.max(1, ...Array.from(nodeValues.values()));
132
+ const byDepth = new Map<number, SankeyChartNode[]>();
133
+
134
+ nodes.forEach((node) => {
135
+ const depth = depths.get(node.id) ?? 0;
136
+ const bucket = byDepth.get(depth) ?? [];
137
+ bucket.push(node);
138
+ byDepth.set(depth, bucket);
139
+ });
140
+
141
+ const positionedNodes = nodes.map((node, index) => {
142
+ const depth = depths.get(node.id) ?? 0;
143
+ const bucket = byDepth.get(depth) ?? [node];
144
+ const row = Math.max(0, bucket.findIndex((entry) => entry.id === node.id));
145
+ const slot = plotHeight / Math.max(bucket.length, 1);
146
+ const nodeHeight = Math.max(24, Math.min(slot * 0.72, 18 + ((nodeValues.get(node.id) ?? 0) / maxNodeValue) * 54));
147
+ const x = MARGIN.left + (maxDepth === 0 ? plotWidth / 2 : (plotWidth * depth) / maxDepth);
148
+ const y = MARGIN.top + slot * row + (slot - nodeHeight) / 2;
149
+ const tone = node.tone ?? TONES[index % TONES.length];
150
+ return {
151
+ node,
152
+ tone,
153
+ x,
154
+ y,
155
+ width: NODE_WIDTH,
156
+ height: nodeHeight,
157
+ centerY: y + nodeHeight / 2
158
+ };
159
+ });
160
+
161
+ const positionedById = new Map(positionedNodes.map((node) => [node.node.id, node]));
162
+ const maxLinkValue = Math.max(1, ...links.map((link) => magnitude(link.value)));
163
+
164
+ const positionedLinks = links.map((link, index) => {
165
+ const source = positionedById.get(link.source);
166
+ const target = positionedById.get(link.target);
167
+ const fallbackY = MARGIN.top + plotHeight / 2;
168
+ const x1 = (source?.x ?? MARGIN.left) + NODE_WIDTH;
169
+ const y1 = source?.centerY ?? fallbackY;
170
+ const x2 = target?.x ?? width - MARGIN.right;
171
+ const y2 = target?.centerY ?? fallbackY;
172
+ const c = Math.max(32, Math.abs(x2 - x1) * 0.5);
173
+ return {
174
+ link,
175
+ source,
176
+ target,
177
+ tone: link.tone ?? source?.tone ?? TONES[index % TONES.length],
178
+ width: Math.max(2, (magnitude(link.value) / maxLinkValue) * 18),
179
+ path: `M ${x1} ${y1} C ${x1 + c} ${y1}, ${x2 - c} ${y2}, ${x2} ${y2}`,
180
+ midX: (x1 + x2) / 2,
181
+ midY: (y1 + y2) / 2
182
+ };
183
+ });
184
+
185
+ return { nodes: positionedNodes, links: positionedLinks };
186
+ });
187
+
188
+ const dataValueItems = $derived(
189
+ links.map((link) => {
190
+ const source = nodeById.get(link.source)?.label ?? link.source;
191
+ const target = nodeById.get(link.target)?.label ?? link.target;
192
+ return `${source} -> ${target}: ${link.value}`;
193
+ })
194
+ );
195
+
196
+ function handleVisualPointerMove(event: PointerEvent) {
197
+ const target = event.target;
198
+ if (!(target instanceof Element)) {
199
+ hoveredLinkIndex = null;
200
+ return;
201
+ }
202
+ const index = Number(target.getAttribute("data-link-index"));
203
+ hoveredLinkIndex = Number.isInteger(index) ? index : null;
204
+ }
205
+
206
+ const classes = () => ["st-sankeyChart", className].filter(Boolean).join(" ");
207
+ </script>
208
+
209
+ <div class={classes()}>
210
+ <div
211
+ class="st-sankeyChart__visual"
212
+ role="img"
213
+ aria-label={label}
214
+ onpointermove={handleVisualPointerMove}
215
+ onpointerleave={() => (hoveredLinkIndex = null)}
216
+ >
217
+ <svg
218
+ viewBox="0 0 {width} {height}"
219
+ preserveAspectRatio="xMidYMid meet"
220
+ width="100%"
221
+ height="100%"
222
+ focusable="false"
223
+ aria-hidden="true"
224
+ >
225
+ <g class="st-sankeyChart__links">
226
+ {#each layout.links as flow, i (`${flow.link.source}-${flow.link.target}-${i}`)}
227
+ <path
228
+ class="st-sankeyChart__link st-sankeyChart__link--{flow.tone}"
229
+ class:st-sankeyChart__link--dim={hoveredLinkIndex !== null && hoveredLinkIndex !== i}
230
+ d={flow.path}
231
+ stroke-width={flow.width}
232
+ data-link-index={i}
233
+ />
234
+ {/each}
235
+ </g>
236
+
237
+ <g class="st-sankeyChart__nodes">
238
+ {#each layout.nodes as entry (entry.node.id)}
239
+ <rect
240
+ class="st-sankeyChart__node st-sankeyChart__node--{entry.tone}"
241
+ x={entry.x}
242
+ y={entry.y}
243
+ width={entry.width}
244
+ height={entry.height}
245
+ rx="2"
246
+ />
247
+ <text
248
+ class="st-sankeyChart__nodeLabel"
249
+ x={entry.x + entry.width + 6}
250
+ y={entry.centerY}
251
+ dominant-baseline="middle"
252
+ >
253
+ {entry.node.label}
254
+ </text>
255
+ {/each}
256
+ </g>
257
+ </svg>
258
+ </div>
259
+
260
+ <ChartDataList {label} items={dataValueItems} />
261
+
262
+ {#if hoveredLinkIndex !== null && layout.links[hoveredLinkIndex]}
263
+ {@const flow = layout.links[hoveredLinkIndex]}
264
+ <div
265
+ class="st-sankeyChart__tooltip"
266
+ role="presentation"
267
+ style="left: {(flow.midX / width) * 100}%; top: {(flow.midY / height) * 100}%"
268
+ >
269
+ <span class="st-sankeyChart__tooltipLabel">{flow.source?.node.label ?? flow.link.source} -> {flow.target?.node.label ?? flow.link.target}</span>
270
+ <span class="st-sankeyChart__tooltipValue">{flow.link.value}</span>
271
+ </div>
272
+ {/if}
273
+ </div>
274
+
275
+ <style>
276
+ .st-sankeyChart {
277
+ color: var(--st-semantic-text-secondary);
278
+ display: block;
279
+ font-family: inherit;
280
+ max-width: 100%;
281
+ position: relative;
282
+ width: 100%;
283
+ }
284
+
285
+ .st-sankeyChart svg,
286
+ .st-sankeyChart__visual {
287
+ display: block;
288
+ overflow: visible;
289
+ }
290
+
291
+ .st-sankeyChart__link {
292
+ cursor: pointer;
293
+ fill: none;
294
+ opacity: 0.38;
295
+ stroke-linecap: round;
296
+ transition: opacity 120ms ease;
297
+ }
298
+
299
+ .st-sankeyChart__link:hover {
300
+ opacity: 0.62;
301
+ }
302
+
303
+ .st-sankeyChart__link--dim {
304
+ opacity: 0.16;
305
+ }
306
+
307
+ @media (prefers-reduced-motion: reduce) {
308
+ .st-sankeyChart__link {
309
+ transition: none;
310
+ }
311
+ }
312
+
313
+ .st-sankeyChart__node {
314
+ stroke: var(--st-semantic-surface-default, Canvas);
315
+ stroke-width: 1;
316
+ }
317
+
318
+ .st-sankeyChart__link--category1,
319
+ .st-sankeyChart__node--category1 { stroke: var(--st-semantic-data-category1); fill: var(--st-semantic-data-category1); }
320
+ .st-sankeyChart__link--category2,
321
+ .st-sankeyChart__node--category2 { stroke: var(--st-semantic-data-category2); fill: var(--st-semantic-data-category2); }
322
+ .st-sankeyChart__link--category3,
323
+ .st-sankeyChart__node--category3 { stroke: var(--st-semantic-data-category3); fill: var(--st-semantic-data-category3); }
324
+ .st-sankeyChart__link--category4,
325
+ .st-sankeyChart__node--category4 { stroke: var(--st-semantic-data-category4); fill: var(--st-semantic-data-category4); }
326
+ .st-sankeyChart__link--category5,
327
+ .st-sankeyChart__node--category5 { stroke: var(--st-semantic-data-category5); fill: var(--st-semantic-data-category5); }
328
+ .st-sankeyChart__link--category6,
329
+ .st-sankeyChart__node--category6 { stroke: var(--st-semantic-data-category6); fill: var(--st-semantic-data-category6); }
330
+ .st-sankeyChart__link--category7,
331
+ .st-sankeyChart__node--category7 { stroke: var(--st-semantic-data-category7); fill: var(--st-semantic-data-category7); }
332
+ .st-sankeyChart__link--category8,
333
+ .st-sankeyChart__node--category8 { stroke: var(--st-semantic-data-category8); fill: var(--st-semantic-data-category8); }
334
+
335
+ .st-sankeyChart__nodeLabel {
336
+ fill: var(--st-semantic-text-secondary);
337
+ font-size: 0.75rem;
338
+ }
339
+
340
+ .st-sankeyChart__tooltip {
341
+ background: var(--st-semantic-surface-inverse);
342
+ border-radius: var(--st-radius-sm, 0.25rem);
343
+ color: var(--st-semantic-text-inverse);
344
+ display: inline-flex;
345
+ flex-direction: column;
346
+ font-size: 0.75rem;
347
+ gap: 0.125rem;
348
+ line-height: 1.2;
349
+ padding: 0.375rem 0.5rem;
350
+ pointer-events: none;
351
+ position: absolute;
352
+ transform: translate(-50%, -115%);
353
+ white-space: nowrap;
354
+ z-index: 1;
355
+ }
356
+
357
+ .st-sankeyChart__tooltipLabel {
358
+ font-weight: 600;
359
+ }
360
+
361
+ .st-sankeyChart__tooltipValue {
362
+ opacity: 0.85;
363
+ }
364
+ </style>
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SankeyChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
3
+ *
4
+ * Props obligatoires :
5
+ * nodes SankeyChartNode[] - liste des nœuds {id, label, tone?}
6
+ * links SankeyChartLink[] - liste des liens {source, target, value, tone?}
7
+ * source/target = id d'un nœud existant ;
8
+ * les liens orphelins (nœud absent) sont rendus
9
+ * avec un fallback (pas de drop silencieux)
10
+ * label string - aria-label du graphique
11
+ *
12
+ * Props optionnelles :
13
+ * width number (défaut 560) - largeur du viewBox en px
14
+ * height number (défaut 280) - hauteur du viewBox en px
15
+ * class string - classe CSS supplémentaire
16
+ *
17
+ * Layout :
18
+ * Hauteur d'un nœud = max(valeurs entrantes sommées, valeurs sortantes sommées)
19
+ * - conservation de flux : un nœud agrégateur occupe autant que la somme
20
+ * de ses flux, pas juste le max d'un lien individuel.
21
+ */
22
+ export type SankeyChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
23
+ export type SankeyChartNode = {
24
+ id: string;
25
+ label: string;
26
+ tone?: SankeyChartTone;
27
+ };
28
+ export type SankeyChartLink = {
29
+ source: string;
30
+ target: string;
31
+ value: number;
32
+ tone?: SankeyChartTone;
33
+ };
34
+ type SankeyChartProps = {
35
+ nodes: SankeyChartNode[];
36
+ links: SankeyChartLink[];
37
+ label: string;
38
+ width?: number;
39
+ height?: number;
40
+ class?: string;
41
+ };
42
+ declare const SankeyChart: import("svelte").Component<SankeyChartProps, {}, "">;
43
+ type SankeyChart = ReturnType<typeof SankeyChart>;
44
+ export default SankeyChart;
45
+ //# sourceMappingURL=SankeyChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SankeyChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SankeyChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,eAAe,GACvB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,CAAC;AAMF,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgMJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}