@sentropic/design-system-svelte 0.28.0 → 0.30.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.
@@ -13,6 +13,35 @@
13
13
  label: string;
14
14
  value: number;
15
15
  tone?: BarChartTone;
16
+ /** Lower error-bar extent (value-axis units). Drawn only when finite. */
17
+ errorLow?: number;
18
+ /** Upper error-bar extent (value-axis units). Drawn only when finite. */
19
+ errorHigh?: number;
20
+ };
21
+
22
+ /**
23
+ * Semantic tone for an analytical overlay (reference line / band / goal).
24
+ * Maps to the feedback token family — markers, not categorical series.
25
+ */
26
+ export type ChartOverlayTone = "neutral" | "success" | "warning" | "error" | "info";
27
+
28
+ export type ChartReferenceLine = {
29
+ value: number;
30
+ label?: string;
31
+ tone?: ChartOverlayTone;
32
+ axis?: "x" | "y";
33
+ };
34
+
35
+ export type ChartBand = {
36
+ from: number;
37
+ to: number;
38
+ label?: string;
39
+ tone?: ChartOverlayTone;
40
+ };
41
+
42
+ export type ChartGoalLine = {
43
+ value: number;
44
+ label?: string;
16
45
  };
17
46
  </script>
18
47
 
@@ -48,6 +77,12 @@
48
77
  * purely presentational (no interactivity, unchanged).
49
78
  */
50
79
  onSelect?: (key: string) => void;
80
+ /** Reference lines on the value axis (default `axis: "y"`). */
81
+ referenceLines?: ChartReferenceLine[];
82
+ /** Shaded value-axis bands between `from`..`to`. */
83
+ bands?: ChartBand[];
84
+ /** A single goal line, emphasised above the bars. */
85
+ goalLine?: ChartGoalLine;
51
86
  class?: string;
52
87
  };
53
88
 
@@ -60,6 +95,9 @@
60
95
  domain,
61
96
  selectedKeys = [],
62
97
  onSelect,
98
+ referenceLines,
99
+ bands,
100
+ goalLine,
63
101
  class: className
64
102
  }: BarChartProps = $props();
65
103
 
@@ -99,6 +137,58 @@
99
137
  return v.toFixed(1);
100
138
  }
101
139
 
140
+ // --- Analytical overlay helpers (inline; parity with chartScale) ----------
141
+ function overlayToneClass(prefix: string, t: ChartOverlayTone | undefined): string {
142
+ return `${prefix}--${t ?? "neutral"}`;
143
+ }
144
+
145
+ function extendValueDomain(
146
+ minV: number,
147
+ maxV: number,
148
+ refs: ChartReferenceLine[] | undefined,
149
+ bnds: ChartBand[] | undefined,
150
+ goal: ChartGoalLine | null,
151
+ extras: number[]
152
+ ): [number, number] {
153
+ let lo = minV;
154
+ let hi = maxV;
155
+ const fold = (v: number | undefined) => {
156
+ if (v === undefined || !Number.isFinite(v)) return;
157
+ if (v < lo) lo = v;
158
+ if (v > hi) hi = v;
159
+ };
160
+ for (const r of refs ?? []) if ((r.axis ?? "y") === "y") fold(r.value);
161
+ for (const b of bnds ?? []) {
162
+ fold(b.from);
163
+ fold(b.to);
164
+ }
165
+ if (goal) fold(goal.value);
166
+ for (const v of extras) fold(v);
167
+ return [lo, hi];
168
+ }
169
+
170
+ function overlayDataListItems(
171
+ refs: ChartReferenceLine[] | undefined,
172
+ bnds: ChartBand[] | undefined,
173
+ goal: ChartGoalLine | null
174
+ ): string[] {
175
+ const items: string[] = [];
176
+ for (const r of refs ?? []) {
177
+ if (!Number.isFinite(r.value)) continue;
178
+ items.push(r.label ? `Référence: ${r.label} = ${r.value}` : `Référence: ${r.value}`);
179
+ }
180
+ for (const b of bnds ?? []) {
181
+ if (!Number.isFinite(b.from) || !Number.isFinite(b.to)) continue;
182
+ const lo = Math.min(b.from, b.to);
183
+ const hi = Math.max(b.from, b.to);
184
+ items.push(b.label ? `Bande: ${b.label} (${lo}–${hi})` : `Bande: ${lo}–${hi}`);
185
+ }
186
+ if (goal && Number.isFinite(goal.value)) {
187
+ items.push(goal.label ? `Objectif: ${goal.label} = ${goal.value}` : `Objectif: ${goal.value}`);
188
+ }
189
+ return items;
190
+ }
191
+
102
192
  let hoveredIndex: number | null = $state(null);
103
193
 
104
194
  // Selection (controlled): fast lookup + "is any bar selected" flag. Only when
@@ -116,10 +206,21 @@
116
206
  return [d0, d1];
117
207
  });
118
208
 
209
+ // A finite goal value is required; otherwise the goal line is ignored.
210
+ const goal = $derived(goalLine && Number.isFinite(goalLine.value) ? goalLine : null);
211
+
119
212
  const scales = $derived.by(() => {
120
213
  const values = data.map((d) => d.value);
121
- const minRaw = validDomain ? validDomain[0] : Math.min(0, ...values);
122
- const maxRaw = validDomain ? validDomain[1] : Math.max(0, ...values);
214
+ let minRaw = validDomain ? validDomain[0] : Math.min(0, ...values);
215
+ let maxRaw = validDomain ? validDomain[1] : Math.max(0, ...values);
216
+ // A pinned domain is authoritative (small-multiples); only the auto domain
217
+ // is widened to keep finite overlays + error bars on-plot.
218
+ if (!validDomain) {
219
+ const errorExtents = data.flatMap((d) =>
220
+ [d.errorLow, d.errorHigh].filter((v): v is number => v !== undefined && Number.isFinite(v))
221
+ );
222
+ [minRaw, maxRaw] = extendValueDomain(minRaw, maxRaw, referenceLines, bands, goal, errorExtents);
223
+ }
123
224
  const ticks = niceTicks(minRaw, maxRaw, 5);
124
225
  const domainMin = ticks[0];
125
226
  const domainMax = ticks[ticks.length - 1];
@@ -174,7 +275,83 @@
174
275
  });
175
276
  });
176
277
 
177
- const dataValueItems = $derived(data.map((d) => `${d.label}: ${d.value}`));
278
+ // --- Analytical overlays + error bars ------------------------------------
279
+ const isVertical = $derived(orientation === "vertical");
280
+ // Map a value to its pixel on the value axis (y for vertical, x for horizontal).
281
+ const valuePos = $derived((v: number) => {
282
+ const { domainMin, domainMax, plotWidth, plotHeight } = scales;
283
+ return isVertical
284
+ ? MARGIN.top + scaleLinear(v, domainMin, domainMax, plotHeight, 0)
285
+ : MARGIN.left + scaleLinear(v, domainMin, domainMax, 0, plotWidth);
286
+ });
287
+
288
+ const bandRects = $derived(
289
+ (bands ?? [])
290
+ .filter((b) => Number.isFinite(b.from) && Number.isFinite(b.to))
291
+ .map((b, i) => {
292
+ const p1 = valuePos(b.from);
293
+ const p2 = valuePos(b.to);
294
+ return isVertical
295
+ ? { key: i, x: MARGIN.left, y: Math.min(p1, p2), width: scales.plotWidth, height: Math.max(Math.abs(p2 - p1), 0.5), label: b.label, tone: b.tone }
296
+ : { key: i, x: Math.min(p1, p2), y: MARGIN.top, width: Math.max(Math.abs(p2 - p1), 0.5), height: scales.plotHeight, label: b.label, tone: b.tone };
297
+ })
298
+ );
299
+
300
+ const refLines = $derived(
301
+ (referenceLines ?? [])
302
+ .filter((r) => Number.isFinite(r.value))
303
+ .map((r, i) => {
304
+ const p = valuePos(r.value);
305
+ return isVertical
306
+ ? { key: i, x1: MARGIN.left, x2: MARGIN.left + scales.plotWidth, y1: p, y2: p, label: r.label, tone: r.tone }
307
+ : { key: i, x1: p, x2: p, y1: MARGIN.top, y2: MARGIN.top + scales.plotHeight, label: r.label, tone: r.tone };
308
+ })
309
+ );
310
+
311
+ const goalGeom = $derived(goal ? { p: valuePos(goal.value), label: goal.label, value: goal.value } : null);
312
+
313
+ const errorBarGeom = $derived.by(() => {
314
+ const out: {
315
+ key: string;
316
+ stem: { x1: number; y1: number; x2: number; y2: number };
317
+ capLow: { x1: number; y1: number; x2: number; y2: number };
318
+ capHigh: { x1: number; y1: number; x2: number; y2: number };
319
+ }[] = [];
320
+ for (const bar of bars) {
321
+ const { errorLow, errorHigh } = bar.datum;
322
+ const hasLow = errorLow !== undefined && Number.isFinite(errorLow);
323
+ const hasHigh = errorHigh !== undefined && Number.isFinite(errorHigh);
324
+ if (!hasLow && !hasHigh) continue;
325
+ const lowV = hasLow ? (errorLow as number) : bar.datum.value;
326
+ const highV = hasHigh ? (errorHigh as number) : bar.datum.value;
327
+ const lowP = valuePos(lowV);
328
+ const highP = valuePos(highV);
329
+ const cap = 4;
330
+ if (isVertical) {
331
+ const cx = bar.x + bar.width / 2;
332
+ out.push({
333
+ key: bar.datum.label,
334
+ stem: { x1: cx, y1: lowP, x2: cx, y2: highP },
335
+ capLow: { x1: cx - cap, y1: lowP, x2: cx + cap, y2: lowP },
336
+ capHigh: { x1: cx - cap, y1: highP, x2: cx + cap, y2: highP }
337
+ });
338
+ } else {
339
+ const cy = bar.y + bar.height / 2;
340
+ out.push({
341
+ key: bar.datum.label,
342
+ stem: { x1: lowP, y1: cy, x2: highP, y2: cy },
343
+ capLow: { x1: lowP, y1: cy - cap, x2: lowP, y2: cy + cap },
344
+ capHigh: { x1: highP, y1: cy - cap, x2: highP, y2: cy + cap }
345
+ });
346
+ }
347
+ }
348
+ return out;
349
+ });
350
+
351
+ const dataValueItems = $derived([
352
+ ...data.map((d) => `${d.label}: ${d.value}`),
353
+ ...overlayDataListItems(referenceLines, bands, goal)
354
+ ]);
178
355
 
179
356
  const valueAxisTicks = $derived.by(() => {
180
357
  const { ticks, domainMin, domainMax, plotWidth, plotHeight } = scales;
@@ -312,6 +489,26 @@
312
489
  {/if}
313
490
  {/each}
314
491
 
492
+ <!-- Analytical overlays — bands + reference lines BELOW the bars. -->
493
+ {#each bandRects as b (b.key)}
494
+ <rect class={`st-barChart__band ${overlayToneClass("st-barChart__band", b.tone)}`} x={b.x} y={b.y} width={b.width} height={b.height} />
495
+ {#if b.label}
496
+ <text class="st-barChart__overlayLabel" x={b.x + 4} y={b.y + 11}>{b.label}</text>
497
+ {/if}
498
+ {/each}
499
+
500
+ {#each refLines as r (r.key)}
501
+ <line class={`st-barChart__refLine ${overlayToneClass("st-barChart__refLine", r.tone)}`} x1={r.x1} x2={r.x2} y1={r.y1} y2={r.y2} />
502
+ {#if r.label}
503
+ <text
504
+ class="st-barChart__overlayLabel"
505
+ x={isVertical ? MARGIN.left + scales.plotWidth - 4 : r.x1 + 4}
506
+ y={isVertical ? r.y1 - 4 : MARGIN.top + 11}
507
+ text-anchor={isVertical ? "end" : "start"}
508
+ >{r.label}</text>
509
+ {/if}
510
+ {/each}
511
+
315
512
  <!-- bars -->
316
513
  <!-- The bars live inside an aria-hidden SVG, so they are NEVER an accessible
317
514
  surface. When `onSelect` is provided they only carry a mouse click
@@ -339,6 +536,30 @@
339
536
  onclick={interactive ? () => onSelect?.(bar.datum.label) : undefined}
340
537
  />
341
538
  {/each}
539
+
540
+ <!-- Error bars ride on top of their bar (still below the goal line). -->
541
+ {#each errorBarGeom as e (e.key)}
542
+ <g class="st-barChart__errorBar">
543
+ <line class="st-barChart__errorStem" x1={e.stem.x1} y1={e.stem.y1} x2={e.stem.x2} y2={e.stem.y2} />
544
+ <line class="st-barChart__errorCap" x1={e.capLow.x1} y1={e.capLow.y1} x2={e.capLow.x2} y2={e.capLow.y2} />
545
+ <line class="st-barChart__errorCap" x1={e.capHigh.x1} y1={e.capHigh.y1} x2={e.capHigh.x2} y2={e.capHigh.y2} />
546
+ </g>
547
+ {/each}
548
+
549
+ <!-- Goal line — emphasised, ABOVE the bars. -->
550
+ {#if goalGeom}
551
+ {#if isVertical}
552
+ <line class="st-barChart__goalLine" x1={MARGIN.left} x2={MARGIN.left + scales.plotWidth} y1={goalGeom.p} y2={goalGeom.p} />
553
+ {:else}
554
+ <line class="st-barChart__goalLine" x1={goalGeom.p} x2={goalGeom.p} y1={MARGIN.top} y2={MARGIN.top + scales.plotHeight} />
555
+ {/if}
556
+ <text
557
+ class="st-barChart__goalLabel"
558
+ x={isVertical ? MARGIN.left + scales.plotWidth - 4 : goalGeom.p + 4}
559
+ y={isVertical ? goalGeom.p - 4 : MARGIN.top + 11}
560
+ text-anchor={isVertical ? "end" : "start"}
561
+ >{goalGeom.label ?? `Objectif ${goalGeom.value}`}</text>
562
+ {/if}
342
563
  </svg>
343
564
  </div>
344
565
 
@@ -540,6 +761,54 @@
540
761
  opacity: 0.85;
541
762
  }
542
763
 
764
+ /* --- Analytical overlay layer --------------------------------------------
765
+ Bands sit BELOW the bars via render order; fill uses color-mix (a
766
+ semi-transparent tint of the tone token) instead of raw opacity, so the
767
+ bars drawn on top keep full contrast. */
768
+ .st-barChart__band {
769
+ fill: color-mix(in srgb, var(--st-overlay-tone, var(--st-semantic-border-strong)) 12%, transparent);
770
+ stroke: none;
771
+ }
772
+ .st-barChart__band--neutral { --st-overlay-tone: var(--st-semantic-border-strong); }
773
+ .st-barChart__band--success { --st-overlay-tone: var(--st-semantic-feedback-success); }
774
+ .st-barChart__band--warning { --st-overlay-tone: var(--st-semantic-feedback-warning); }
775
+ .st-barChart__band--error { --st-overlay-tone: var(--st-semantic-feedback-error); }
776
+ .st-barChart__band--info { --st-overlay-tone: var(--st-semantic-feedback-info); }
777
+
778
+ .st-barChart__refLine {
779
+ stroke: var(--st-overlay-tone, var(--st-semantic-border-strong));
780
+ stroke-width: 1;
781
+ stroke-dasharray: 4 3;
782
+ }
783
+ .st-barChart__refLine--neutral { --st-overlay-tone: var(--st-semantic-border-strong); }
784
+ .st-barChart__refLine--success { --st-overlay-tone: var(--st-semantic-feedback-success); }
785
+ .st-barChart__refLine--warning { --st-overlay-tone: var(--st-semantic-feedback-warning); }
786
+ .st-barChart__refLine--error { --st-overlay-tone: var(--st-semantic-feedback-error); }
787
+ .st-barChart__refLine--info { --st-overlay-tone: var(--st-semantic-feedback-info); }
788
+
789
+ .st-barChart__overlayLabel {
790
+ fill: var(--st-semantic-text-secondary);
791
+ font-size: 0.625rem;
792
+ }
793
+
794
+ /* Error bars — a value-axis whisker centred on each bar. */
795
+ .st-barChart__errorStem,
796
+ .st-barChart__errorCap {
797
+ stroke: var(--st-semantic-text-primary);
798
+ stroke-width: 1.25;
799
+ }
800
+
801
+ /* Goal line — drawn ABOVE the bars, emphasised (thicker, solid accent). */
802
+ .st-barChart__goalLine {
803
+ stroke: var(--st-semantic-feedback-success);
804
+ stroke-width: 2.5;
805
+ }
806
+ .st-barChart__goalLabel {
807
+ fill: var(--st-semantic-feedback-success);
808
+ font-size: 0.6875rem;
809
+ font-weight: 600;
810
+ }
811
+
543
812
  @media (prefers-reduced-motion: reduce) {
544
813
  .st-barChart__bar,
545
814
  .st-barChart__filterChip { transition: none; }
@@ -3,6 +3,31 @@ export type BarChartDatum = {
3
3
  label: string;
4
4
  value: number;
5
5
  tone?: BarChartTone;
6
+ /** Lower error-bar extent (value-axis units). Drawn only when finite. */
7
+ errorLow?: number;
8
+ /** Upper error-bar extent (value-axis units). Drawn only when finite. */
9
+ errorHigh?: number;
10
+ };
11
+ /**
12
+ * Semantic tone for an analytical overlay (reference line / band / goal).
13
+ * Maps to the feedback token family — markers, not categorical series.
14
+ */
15
+ export type ChartOverlayTone = "neutral" | "success" | "warning" | "error" | "info";
16
+ export type ChartReferenceLine = {
17
+ value: number;
18
+ label?: string;
19
+ tone?: ChartOverlayTone;
20
+ axis?: "x" | "y";
21
+ };
22
+ export type ChartBand = {
23
+ from: number;
24
+ to: number;
25
+ label?: string;
26
+ tone?: ChartOverlayTone;
27
+ };
28
+ export type ChartGoalLine = {
29
+ value: number;
30
+ label?: string;
6
31
  };
7
32
  type BarChartProps = {
8
33
  data: BarChartDatum[];
@@ -33,6 +58,12 @@ type BarChartProps = {
33
58
  * purely presentational (no interactivity, unchanged).
34
59
  */
35
60
  onSelect?: (key: string) => void;
61
+ /** Reference lines on the value axis (default `axis: "y"`). */
62
+ referenceLines?: ChartReferenceLine[];
63
+ /** Shaded value-axis bands between `from`..`to`. */
64
+ bands?: ChartBand[];
65
+ /** A single goal line, emphasised above the bars. */
66
+ goalLine?: ChartGoalLine;
36
67
  class?: string;
37
68
  };
38
69
  declare const BarChart: import("svelte").Component<BarChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAMF,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAyPJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAMF,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAyaJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}