@sentropic/design-system-svelte 0.34.22 → 0.34.24

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.
@@ -53,6 +53,13 @@
53
53
 
54
54
  <script lang="ts">
55
55
  import ChartDataList from "./ChartDataList.svelte";
56
+ import {
57
+ resolveAnnotations,
58
+ annotationDataListItems,
59
+ polygonPoints,
60
+ type ChartAnnotation
61
+ } from "./chartAnnotations.js";
62
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
56
63
 
57
64
  type LineChartProps = {
58
65
  data: LineChartDatum[];
@@ -70,6 +77,21 @@
70
77
  goalLine?: ChartGoalLine;
71
78
  /** Least-squares trend line over the data points. */
72
79
  trend?: boolean;
80
+ /**
81
+ * Annotation overlay in DATA space (points, labels, axis lines, regions,
82
+ * polygons). Resolved to pixels via the chart's scales and drawn in a
83
+ * dedicated `<g class="st-lineChart__annotations">` — regions behind the
84
+ * series, every other kind above it. Additive: absent ⇒ unchanged.
85
+ */
86
+ annotations?: ChartAnnotation[];
87
+ /**
88
+ * Per-point value labels. `false`/absent (default) → none. `true` → each
89
+ * point's value with the chart's numeric formatter. Object → `format(value)`
90
+ * and/or a `position` override. Default position is `top` (above the point).
91
+ * Labels are `aria-hidden` — the values already live in the accessible
92
+ * ChartDataList.
93
+ */
94
+ dataLabels?: DataLabelsProp;
73
95
  /**
74
96
  * Fixed value-axis (y) domain `[min, max]`. When provided (and finite,
75
97
  * min<max) the y scale uses it instead of the data-derived range — letting
@@ -104,6 +126,8 @@
104
126
  bands,
105
127
  goalLine,
106
128
  trend = false,
129
+ annotations,
130
+ dataLabels,
107
131
  domain,
108
132
  scale = "linear",
109
133
  invertAxis = false,
@@ -441,6 +465,53 @@
441
465
  };
442
466
  });
443
467
 
468
+ // --- Annotation overlay ---------------------------------------------------
469
+ // Data-space annotations resolved to absolute pixels via the chart scales.
470
+ // `xScale` honours the ordinal/numeric x domain (a category matches by
471
+ // equality, a numeric value must sit inside the domain); `yScale` reuses the
472
+ // value fraction. Out-of-domain coordinates yield `null` → the resolver drops
473
+ // them, so an annotation never escapes the plot.
474
+ const annotationXScale = $derived((v: number | string): number | null => {
475
+ if (xDomain.kind === "numeric") {
476
+ if (typeof v !== "number" || !Number.isFinite(v)) return null;
477
+ if (v < xDomain.min || v > xDomain.max) return null;
478
+ return scaleLinear(v, xDomain.min, xDomain.max, 0, plotWidth);
479
+ }
480
+ const i = data.findIndex((d) => d.x === v);
481
+ if (i < 0) return null;
482
+ const denom = Math.max(data.length - 1, 1);
483
+ return data.length === 1 ? plotWidth / 2 : (i / denom) * plotWidth;
484
+ });
485
+ const annotationYScale = $derived((v: number): number | null => {
486
+ if (!Number.isFinite(v)) return null;
487
+ return plotHeight * (1 - valueFraction(v));
488
+ });
489
+ const resolvedAnnotations = $derived(
490
+ resolveAnnotations(annotations, {
491
+ xScale: annotationXScale,
492
+ yScale: annotationYScale,
493
+ plotLeft: MARGIN.left,
494
+ plotTop: MARGIN.top,
495
+ plotWidth,
496
+ plotHeight
497
+ })
498
+ );
499
+ const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
500
+ const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
501
+
502
+ // --- Data labels ----------------------------------------------------------
503
+ // One value label per point. Default `top`: just above the dot. `center` sits
504
+ // on the dot. aria-hidden (values are in the ChartDataList already).
505
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
506
+ const dataLabelItems = $derived.by(() => {
507
+ if (!dataLabelOpts.enabled) return [] as { key: number; x: number; y: number; text: string; baseline: string }[];
508
+ return points.map((p) => {
509
+ const text = formatDataLabel(p.datum.y, dataLabelOpts, formatTick);
510
+ const center = dataLabelOpts.position === "center" || dataLabelOpts.position === "inside";
511
+ return { key: p.index, x: p.x, y: center ? p.y : p.y - 8, text, baseline: center ? "middle" : "auto" };
512
+ });
513
+ });
514
+
444
515
  // --- Forecast segments ------------------------------------------------------
445
516
  // A datum with `forecast: true` renders as a forecast point: its dot takes
446
517
  // the forecast tone and every segment touching a forecast point is dashed,
@@ -466,7 +537,8 @@
466
537
 
467
538
  const dataValueItems = $derived([
468
539
  ...data.map((d, i) => (forecastFlags[i] ? `${d.x}: ${d.y} (prévision)` : `${d.x}: ${d.y}`)),
469
- ...overlayDataListItems(referenceLines, bands, goal, trendModel)
540
+ ...overlayDataListItems(referenceLines, bands, goal, trendModel),
541
+ ...annotationDataListItems(annotations)
470
542
  ]);
471
543
 
472
544
  function buildLinearPath(pts: { x: number; y: number }[]): string {
@@ -651,6 +723,20 @@
651
723
  <line class="st-lineChart__trend" x1={trendLine.x1} y1={trendLine.y1} x2={trendLine.x2} y2={trendLine.y2} />
652
724
  {/if}
653
725
 
726
+ <!-- Annotation regions sit BEHIND the series (filled bands). -->
727
+ {#if annotationRegions.length > 0}
728
+ <g class="st-lineChart__annotations st-lineChart__annotations--behind">
729
+ {#each annotationRegions as a (a.key)}
730
+ {#if a.kind === "region"}
731
+ <rect class="st-lineChart__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
732
+ {#if a.label}
733
+ <text class="st-lineChart__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
734
+ {/if}
735
+ {/if}
736
+ {/each}
737
+ </g>
738
+ {/if}
739
+
654
740
  {#if area && areaPath}
655
741
  <path class="st-lineChart__area" d={areaPath} />
656
742
  {/if}
@@ -694,6 +780,46 @@
694
780
  {goalGeom.label ?? `Objectif ${goalGeom.value}`}
695
781
  </text>
696
782
  {/if}
783
+
784
+ <!-- Annotations ABOVE the series: lines, shapes, points, labels. -->
785
+ {#if annotationAbove.length > 0}
786
+ <g class="st-lineChart__annotations st-lineChart__annotations--above">
787
+ {#each annotationAbove as a (a.key)}
788
+ {#if a.kind === "line"}
789
+ <line class="st-lineChart__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
790
+ {#if a.label}
791
+ <text
792
+ class="st-lineChart__annotationLabel"
793
+ x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + plotWidth - 4}
794
+ y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
795
+ text-anchor={a.axis === "x" ? "start" : "end"}
796
+ >{a.label}</text>
797
+ {/if}
798
+ {:else if a.kind === "shape"}
799
+ <polygon class="st-lineChart__annotationShape" points={polygonPoints(a.points)} />
800
+ {#if a.label}
801
+ <text class="st-lineChart__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
802
+ {/if}
803
+ {:else if a.kind === "point"}
804
+ <circle class="st-lineChart__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
805
+ {#if a.label}
806
+ <text class="st-lineChart__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
807
+ {/if}
808
+ {:else}
809
+ <text class="st-lineChart__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
810
+ {/if}
811
+ {/each}
812
+ </g>
813
+ {/if}
814
+
815
+ <!-- Data labels — one value per point, drawn on top. aria-hidden. -->
816
+ {#if dataLabelItems.length > 0}
817
+ <g class="st-lineChart__dataLabels" aria-hidden="true">
818
+ {#each dataLabelItems as d (d.key)}
819
+ <text class="st-lineChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline={d.baseline}>{d.text}</text>
820
+ {/each}
821
+ </g>
822
+ {/if}
697
823
  </svg>
698
824
  </div>
699
825
 
@@ -867,4 +993,39 @@
867
993
  font-size: 0.6875rem;
868
994
  font-weight: 600;
869
995
  }
996
+
997
+ /* --- Annotation layer ----------------------------------------------------
998
+ Regions render BEHIND the series; lines/shapes/points/labels render ABOVE. */
999
+ .st-lineChart__annotationRegion {
1000
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent);
1001
+ stroke: none;
1002
+ }
1003
+ .st-lineChart__annotationLine {
1004
+ stroke: var(--st-semantic-feedback-info);
1005
+ stroke-width: 1.5;
1006
+ stroke-dasharray: 4 3;
1007
+ }
1008
+ .st-lineChart__annotationShape {
1009
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent);
1010
+ stroke: var(--st-semantic-feedback-info);
1011
+ stroke-width: 1.5;
1012
+ }
1013
+ .st-lineChart__annotationPoint {
1014
+ fill: var(--st-semantic-feedback-info);
1015
+ stroke: var(--st-semantic-surface-default);
1016
+ stroke-width: 1.5;
1017
+ }
1018
+ .st-lineChart__annotationLabel,
1019
+ .st-lineChart__annotationText {
1020
+ fill: var(--st-semantic-text-primary);
1021
+ font-size: 0.625rem;
1022
+ font-weight: 600;
1023
+ }
1024
+
1025
+ /* Data labels — per-point value, drawn on top. Token-only colour. */
1026
+ .st-lineChart__dataLabel {
1027
+ fill: var(--st-semantic-text-primary);
1028
+ font-size: 0.6875rem;
1029
+ font-weight: 600;
1030
+ }
870
1031
  </style>
@@ -34,6 +34,8 @@ export type ChartGoalLine = {
34
34
  };
35
35
  /** Value-axis scale type. `log` requires strictly positive values. */
36
36
  export type ChartScale = "linear" | "log";
37
+ import { type ChartAnnotation } from "./chartAnnotations.js";
38
+ import { type DataLabelsProp } from "./chartDataLabels.js";
37
39
  type LineChartProps = {
38
40
  data: LineChartDatum[];
39
41
  width?: number;
@@ -50,6 +52,21 @@ type LineChartProps = {
50
52
  goalLine?: ChartGoalLine;
51
53
  /** Least-squares trend line over the data points. */
52
54
  trend?: boolean;
55
+ /**
56
+ * Annotation overlay in DATA space (points, labels, axis lines, regions,
57
+ * polygons). Resolved to pixels via the chart's scales and drawn in a
58
+ * dedicated `<g class="st-lineChart__annotations">` — regions behind the
59
+ * series, every other kind above it. Additive: absent ⇒ unchanged.
60
+ */
61
+ annotations?: ChartAnnotation[];
62
+ /**
63
+ * Per-point value labels. `false`/absent (default) → none. `true` → each
64
+ * point's value with the chart's numeric formatter. Object → `format(value)`
65
+ * and/or a `position` override. Default position is `top` (above the point).
66
+ * Labels are `aria-hidden` — the values already live in the accessible
67
+ * ChartDataList.
68
+ */
69
+ dataLabels?: DataLabelsProp;
53
70
  /**
54
71
  * Fixed value-axis (y) domain `[min, max]`. When provided (and finite,
55
72
  * min<max) the y scale uses it instead of the data-derived range — letting
@@ -1 +1 @@
1
- {"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,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;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAM1C,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgjBJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,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;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAI5C,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqpBJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -17,6 +17,7 @@
17
17
 
18
18
  <script lang="ts">
19
19
  import ChartDataList from "./ChartDataList.svelte";
20
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
20
21
 
21
22
  type StackedBarChartProps = {
22
23
  data: StackedBarDatum[];
@@ -24,6 +25,14 @@
24
25
  height?: number;
25
26
  label: string;
26
27
  showLegend?: boolean;
28
+ /**
29
+ * Per-segment value labels. `false`/absent (default) → none. `true` → each
30
+ * segment's value with the chart's numeric formatter. Object → `format(value)`
31
+ * and/or a `position` override (default `center` of the segment). Segments too
32
+ * short to host a legible label are skipped. Labels are `aria-hidden` — the
33
+ * values already live in the accessible ChartDataList.
34
+ */
35
+ dataLabels?: DataLabelsProp;
27
36
  class?: string;
28
37
  };
29
38
 
@@ -33,12 +42,16 @@
33
42
  height = 260,
34
43
  label,
35
44
  showLegend = true,
45
+ dataLabels,
36
46
  class: className
37
47
  }: StackedBarChartProps = $props();
38
48
 
39
49
  const MARGIN = { top: 14, right: 16, bottom: 34, left: 44 };
40
50
  const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
41
51
 
52
+ // A segment must be at least this tall (px) to host a legible label.
53
+ const DATA_LABEL_MIN_SEG_PX = 14;
54
+
42
55
  function niceTicks(min: number, max: number, target = 5): number[] {
43
56
  if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) return [Number.isFinite(max) ? max : 0];
44
57
  const range = max - min;
@@ -104,6 +117,26 @@
104
117
  data.flatMap((bar) => bar.segments.map((seg) => `${bar.label}, ${seg.label}: ${seg.value}`))
105
118
  );
106
119
 
120
+ // --- Data labels ----------------------------------------------------------
121
+ // One value label centred in each segment (default `center`). Segments shorter
122
+ // than DATA_LABEL_MIN_SEG_PX are skipped so labels stay legible. aria-hidden
123
+ // (values are in the ChartDataList already).
124
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
125
+ const dataLabelItems = $derived(
126
+ dataLabelOpts.enabled
127
+ ? bars.flatMap((bar) =>
128
+ bar.segs
129
+ .filter((s) => s.height >= DATA_LABEL_MIN_SEG_PX)
130
+ .map((s) => ({
131
+ key: `${bar.label}-${s.seg.label}`,
132
+ x: s.cx,
133
+ y: s.cy,
134
+ text: formatDataLabel(s.seg.value, dataLabelOpts, fmt)
135
+ }))
136
+ )
137
+ : []
138
+ );
139
+
107
140
  function handleVisualPointerMove(event: PointerEvent) {
108
141
  const target = event.target;
109
142
  if (!(target instanceof Element)) {
@@ -148,6 +181,15 @@
148
181
  />
149
182
  {/each}
150
183
  {/each}
184
+
185
+ <!-- Data labels — one value per segment, drawn on top. aria-hidden. -->
186
+ {#if dataLabelItems.length > 0}
187
+ <g class="st-stackedBar__dataLabels" aria-hidden="true">
188
+ {#each dataLabelItems as d (d.key)}
189
+ <text class="st-stackedBar__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline="central">{d.text}</text>
190
+ {/each}
191
+ </g>
192
+ {/if}
151
193
  </svg>
152
194
  </div>
153
195
 
@@ -189,6 +231,8 @@
189
231
  .st-stackedBar__seg--category6 { fill: var(--st-semantic-data-category6); }
190
232
  .st-stackedBar__seg--category7 { fill: var(--st-semantic-data-category7); }
191
233
  .st-stackedBar__seg--category8 { fill: var(--st-semantic-data-category8); }
234
+ /* Data labels — per-segment value, centred. Token-only colour. */
235
+ .st-stackedBar__dataLabel { fill: var(--st-semantic-text-inverse); font-size: 0.6875rem; font-weight: 600; }
192
236
  .st-stackedBar__tooltip {
193
237
  background: var(--st-semantic-surface-inverse); border-radius: var(--st-radius-sm, 0.25rem);
194
238
  color: var(--st-semantic-text-inverse); display: inline-flex; flex-direction: column; font-size: 0.75rem;
@@ -8,12 +8,21 @@ export type StackedBarDatum = {
8
8
  label: string;
9
9
  segments: StackedBarSegment[];
10
10
  };
11
+ import { type DataLabelsProp } from "./chartDataLabels.js";
11
12
  type StackedBarChartProps = {
12
13
  data: StackedBarDatum[];
13
14
  width?: number;
14
15
  height?: number;
15
16
  label: string;
16
17
  showLegend?: boolean;
18
+ /**
19
+ * Per-segment value labels. `false`/absent (default) → none. `true` → each
20
+ * segment's value with the chart's numeric formatter. Object → `format(value)`
21
+ * and/or a `position` override (default `center` of the segment). Segments too
22
+ * short to host a legible label are skipped. Labels are `aria-hidden` — the
23
+ * values already live in the accessible ChartDataList.
24
+ */
25
+ dataLabels?: DataLabelsProp;
17
26
  class?: string;
18
27
  };
19
28
  declare const StackedBarChart: import("svelte").Component<StackedBarChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAMF,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiJJ,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAIJ,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAmLJ,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Marker glyph for a `point` annotation. Defaults to `circle`.
3
+ */
4
+ export type ChartAnnotationMarker = "circle" | "square" | "diamond";
5
+ /**
6
+ * A single chart annotation in DATA space. Discriminated on `kind`:
7
+ * - `point` — a marked coordinate `(x, y)` with an optional label.
8
+ * - `label` — free text anchored at `(x, y)`.
9
+ * - `line` — a full-width/height guide on one axis at `value` (vertical when
10
+ * `axis: "x"`, horizontal when `axis: "y"`).
11
+ * - `region` — a filled band on one axis between `from` and `to`.
12
+ * - `shape` — a closed polygon through `points` (data-space coordinates).
13
+ *
14
+ * `x` may be a string for categorical (ordinal) x axes — it then matches a
15
+ * category by equality; `y` and value-axis numbers are always numeric.
16
+ */
17
+ export type ChartAnnotation = {
18
+ kind: "point";
19
+ x: number | string;
20
+ y: number;
21
+ label?: string;
22
+ marker?: ChartAnnotationMarker;
23
+ } | {
24
+ kind: "label";
25
+ x: number | string;
26
+ y: number;
27
+ text: string;
28
+ anchor?: "start" | "middle" | "end";
29
+ } | {
30
+ kind: "line";
31
+ axis: "x" | "y";
32
+ value: number;
33
+ label?: string;
34
+ } | {
35
+ kind: "region";
36
+ axis: "x" | "y";
37
+ from: number;
38
+ to: number;
39
+ label?: string;
40
+ } | {
41
+ kind: "shape";
42
+ points: {
43
+ x: number | string;
44
+ y: number;
45
+ }[];
46
+ label?: string;
47
+ };
48
+ /**
49
+ * Scale context for {@link resolveAnnotations}. `xScale` / `yScale` map a data
50
+ * value to a pixel offset RELATIVE to the plot (0..plotWidth / 0..plotHeight),
51
+ * returning `null` when the value is non-finite, out of domain, or — for a
52
+ * categorical x axis — does not match any category. `plotLeft` / `plotTop` are
53
+ * the plot's pixel origin (chart margins); `plotWidth` / `plotHeight` its size.
54
+ */
55
+ export type AnnotationScaleContext = {
56
+ xScale: (value: number | string) => number | null;
57
+ yScale: (value: number) => number | null;
58
+ plotLeft: number;
59
+ plotTop: number;
60
+ plotWidth: number;
61
+ plotHeight: number;
62
+ };
63
+ /** Resolved annotation primitive in ABSOLUTE pixel space (plot origin folded in). */
64
+ export type ResolvedAnnotation = {
65
+ kind: "region";
66
+ key: number;
67
+ axis: "x" | "y";
68
+ x: number;
69
+ y: number;
70
+ width: number;
71
+ height: number;
72
+ label?: string;
73
+ } | {
74
+ kind: "line";
75
+ key: number;
76
+ axis: "x" | "y";
77
+ x1: number;
78
+ y1: number;
79
+ x2: number;
80
+ y2: number;
81
+ label?: string;
82
+ } | {
83
+ kind: "shape";
84
+ key: number;
85
+ points: {
86
+ x: number;
87
+ y: number;
88
+ }[];
89
+ label?: string;
90
+ labelX: number;
91
+ labelY: number;
92
+ } | {
93
+ kind: "point";
94
+ key: number;
95
+ x: number;
96
+ y: number;
97
+ label?: string;
98
+ marker: ChartAnnotationMarker;
99
+ } | {
100
+ kind: "label";
101
+ key: number;
102
+ x: number;
103
+ y: number;
104
+ text: string;
105
+ anchor: "start" | "middle" | "end";
106
+ };
107
+ /**
108
+ * Resolves DATA-space annotations to ABSOLUTE-pixel primitives. Pure: given the
109
+ * same inputs it always returns the same output, with no DOM/framework access.
110
+ *
111
+ * Region/line on `axis: "x"` are positioned via `xScale` (vertical guide / band
112
+ * spanning the full plot height); on `axis: "y"` via `yScale` (horizontal guide
113
+ * / band spanning the full plot width). Points, labels and shape vertices are
114
+ * positioned via both scales. Any annotation whose required coordinates fall
115
+ * outside the domain (scale returns `null`) or are non-finite is DROPPED, so an
116
+ * annotation never escapes the plot. The original index is preserved as `key`
117
+ * for stable rendering keys + a11y ordering.
118
+ */
119
+ export declare function resolveAnnotations(annotations: ReadonlyArray<ChartAnnotation> | undefined, ctx: AnnotationScaleContext): ResolvedAnnotation[];
120
+ /**
121
+ * Screen-reader descriptions of the annotations, appended after the data values
122
+ * (and any analytical overlays) in the ChartDataList. Each item is
123
+ * `Annotation: <label/text>`; annotations without a label/text are skipped
124
+ * (nothing meaningful to announce). Empty when no annotation is present.
125
+ */
126
+ export declare function annotationDataListItems(annotations: ReadonlyArray<ChartAnnotation> | undefined): string[];
127
+ /** SVG polygon `points` attribute from resolved pixel vertices. */
128
+ export declare function polygonPoints(points: ReadonlyArray<{
129
+ x: number;
130
+ y: number;
131
+ }>): string;
132
+ //# sourceMappingURL=chartAnnotations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chartAnnotations.d.ts","sourceRoot":"","sources":["../src/lib/chartAnnotations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ // --- Chart annotation layer (shared, framework-agnostic) -------------------
2
+ //
3
+ // A declarative overlay of annotations expressed in DATA space (x / y values),
4
+ // resolved here to framework-agnostic PIXEL primitives. The model is the
5
+ // `ChartAnnotation` discriminated union provided by dataviz-core; the resolver
6
+ // is pure and identical across react / vue / svelte so the three renderers stay
7
+ // in strict parity. Purely additive: an absent/empty `annotations` prop yields
8
+ // no primitives and no a11y items.
9
+ const finite = (v) => typeof v === "number" && Number.isFinite(v);
10
+ /**
11
+ * Resolves DATA-space annotations to ABSOLUTE-pixel primitives. Pure: given the
12
+ * same inputs it always returns the same output, with no DOM/framework access.
13
+ *
14
+ * Region/line on `axis: "x"` are positioned via `xScale` (vertical guide / band
15
+ * spanning the full plot height); on `axis: "y"` via `yScale` (horizontal guide
16
+ * / band spanning the full plot width). Points, labels and shape vertices are
17
+ * positioned via both scales. Any annotation whose required coordinates fall
18
+ * outside the domain (scale returns `null`) or are non-finite is DROPPED, so an
19
+ * annotation never escapes the plot. The original index is preserved as `key`
20
+ * for stable rendering keys + a11y ordering.
21
+ */
22
+ export function resolveAnnotations(annotations, ctx) {
23
+ if (!annotations || annotations.length === 0)
24
+ return [];
25
+ const { xScale, yScale, plotLeft, plotTop, plotWidth, plotHeight } = ctx;
26
+ const out = [];
27
+ annotations.forEach((a, key) => {
28
+ switch (a.kind) {
29
+ case "region": {
30
+ if (!finite(a.from) || !finite(a.to))
31
+ return;
32
+ if (a.axis === "x") {
33
+ const p1 = xScale(a.from);
34
+ const p2 = xScale(a.to);
35
+ if (p1 === null || p2 === null)
36
+ return;
37
+ const x = plotLeft + Math.min(p1, p2);
38
+ out.push({ kind: "region", key, axis: "x", x, y: plotTop, width: Math.max(Math.abs(p2 - p1), 0.5), height: plotHeight, label: a.label });
39
+ }
40
+ else {
41
+ const p1 = yScale(a.from);
42
+ const p2 = yScale(a.to);
43
+ if (p1 === null || p2 === null)
44
+ return;
45
+ const y = plotTop + Math.min(p1, p2);
46
+ out.push({ kind: "region", key, axis: "y", x: plotLeft, y, width: plotWidth, height: Math.max(Math.abs(p2 - p1), 0.5), label: a.label });
47
+ }
48
+ return;
49
+ }
50
+ case "line": {
51
+ if (!finite(a.value))
52
+ return;
53
+ if (a.axis === "x") {
54
+ const p = xScale(a.value);
55
+ if (p === null)
56
+ return;
57
+ const x = plotLeft + p;
58
+ out.push({ kind: "line", key, axis: "x", x1: x, y1: plotTop, x2: x, y2: plotTop + plotHeight, label: a.label });
59
+ }
60
+ else {
61
+ const p = yScale(a.value);
62
+ if (p === null)
63
+ return;
64
+ const y = plotTop + p;
65
+ out.push({ kind: "line", key, axis: "y", x1: plotLeft, y1: y, x2: plotLeft + plotWidth, y2: y, label: a.label });
66
+ }
67
+ return;
68
+ }
69
+ case "shape": {
70
+ if (!a.points || a.points.length < 2)
71
+ return;
72
+ const pts = [];
73
+ for (const p of a.points) {
74
+ const px = xScale(p.x);
75
+ const py = yScale(p.y);
76
+ if (px === null || py === null)
77
+ return; // a missing vertex invalidates the polygon
78
+ pts.push({ x: plotLeft + px, y: plotTop + py });
79
+ }
80
+ const labelX = pts.reduce((s, p) => s + p.x, 0) / pts.length;
81
+ const labelY = pts.reduce((s, p) => s + p.y, 0) / pts.length;
82
+ out.push({ kind: "shape", key, points: pts, label: a.label, labelX, labelY });
83
+ return;
84
+ }
85
+ case "point": {
86
+ const px = xScale(a.x);
87
+ const py = yScale(a.y);
88
+ if (px === null || py === null)
89
+ return;
90
+ out.push({ kind: "point", key, x: plotLeft + px, y: plotTop + py, label: a.label, marker: a.marker ?? "circle" });
91
+ return;
92
+ }
93
+ case "label": {
94
+ const px = xScale(a.x);
95
+ const py = yScale(a.y);
96
+ if (px === null || py === null)
97
+ return;
98
+ out.push({ kind: "label", key, x: plotLeft + px, y: plotTop + py, text: a.text, anchor: a.anchor ?? "middle" });
99
+ return;
100
+ }
101
+ }
102
+ });
103
+ return out;
104
+ }
105
+ /**
106
+ * Screen-reader descriptions of the annotations, appended after the data values
107
+ * (and any analytical overlays) in the ChartDataList. Each item is
108
+ * `Annotation: <label/text>`; annotations without a label/text are skipped
109
+ * (nothing meaningful to announce). Empty when no annotation is present.
110
+ */
111
+ export function annotationDataListItems(annotations) {
112
+ if (!annotations || annotations.length === 0)
113
+ return [];
114
+ const items = [];
115
+ for (const a of annotations) {
116
+ const text = a.kind === "label" ? a.text : a.label;
117
+ if (text)
118
+ items.push(`Annotation: ${text}`);
119
+ }
120
+ return items;
121
+ }
122
+ /** SVG polygon `points` attribute from resolved pixel vertices. */
123
+ export function polygonPoints(points) {
124
+ return points.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" ");
125
+ }