@sentropic/design-system-svelte 0.34.24 → 0.34.25

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.
@@ -24,6 +24,7 @@
24
24
  type ChartAnnotation
25
25
  } from "./chartAnnotations.js";
26
26
  import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
27
+ import { keyForX, resolveActiveIndex } from "./chartCrosshair.js";
27
28
 
28
29
  type AreaChartProps = {
29
30
  data: (number | AreaChartDatum)[];
@@ -46,6 +47,20 @@
46
47
  * ChartDataList.
47
48
  */
48
49
  dataLabels?: DataLabelsProp;
50
+ /**
51
+ * CONTROLLED synchronised hover key (FR-3). A datum's key is `String(x)`. When
52
+ * provided (string or null), the crosshair + tooltip track this key instead of
53
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
54
+ * share one hover channel across several aligned charts. Absent (`undefined`)
55
+ * keeps the legacy uncontrolled behaviour.
56
+ */
57
+ hoverKey?: string | null;
58
+ /**
59
+ * Emitted when the user hovers a datum (its key) or leaves the plot (`null`).
60
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
61
+ * keep the shared hover channel in sync.
62
+ */
63
+ onHoverKeyChange?: (key: string | null) => void;
49
64
  class?: string;
50
65
  };
51
66
 
@@ -58,6 +73,8 @@
58
73
  label,
59
74
  annotations,
60
75
  dataLabels,
76
+ hoverKey,
77
+ onHoverKeyChange,
61
78
  class: className
62
79
  }: AreaChartProps = $props();
63
80
 
@@ -271,19 +288,34 @@
271
288
  return entries;
272
289
  });
273
290
 
291
+ // Stable key per datum (FR-3): `String(x)` of the normalised datum (a bare
292
+ // number becomes its index). Resolves a controlled `hoverKey` to an index and
293
+ // feeds `onHoverKeyChange` from pointer events.
294
+ const hoverKeys = $derived(normalizedData.map((d) => keyForX(d.x)));
295
+ function emitHoverKey(index: number | null) {
296
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
297
+ }
274
298
  function handleLeave() {
275
299
  hoveredIndex = null;
300
+ emitHoverKey(null);
276
301
  }
277
302
  function handleVisualPointerMove(event: PointerEvent) {
278
303
  const target = event.target;
279
304
  if (!(target instanceof Element)) {
280
305
  hoveredIndex = null;
306
+ emitHoverKey(null);
281
307
  return;
282
308
  }
283
- const index = Number(target.getAttribute("data-chart-index"));
284
- hoveredIndex = Number.isInteger(index) ? index : null;
309
+ const raw = Number(target.getAttribute("data-chart-index"));
310
+ const index = Number.isInteger(raw) ? raw : null;
311
+ hoveredIndex = index;
312
+ emitHoverKey(index);
285
313
  }
286
314
 
315
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
316
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
317
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
318
+
287
319
  // Generates a unique gradient id to avoid conflicts when rendering multiple charts on the same page
288
320
  const gradientId = $derived.by(() => {
289
321
  return `st-areachart-gradient-${Math.random().toString(36).substring(2, 9)}`;
@@ -441,13 +473,23 @@
441
473
  {/each}
442
474
  </g>
443
475
  {/if}
476
+
477
+ <!-- Crosshair (FR-3) — a tokenised vertical line + marker at the active key.
478
+ Decorative (aria-hidden); the value is in the tooltip + ChartDataList. -->
479
+ {#if activeIndex >= 0 && points[activeIndex]}
480
+ {@const cp = points[activeIndex]}
481
+ <g class="st-areaChart__crosshair" aria-hidden="true">
482
+ <line class="st-areaChart__crosshairLine" x1={cp.x} x2={cp.x} y1={MARGIN.top} y2={MARGIN.top + plotHeight} />
483
+ <circle class="st-areaChart__crosshairMarker" cx={cp.x} cy={cp.y} r="5" />
484
+ </g>
485
+ {/if}
444
486
  </svg>
445
487
  </div>
446
488
 
447
489
  <ChartDataList {label} items={dataValueItems} />
448
490
 
449
- {#if hoveredIndex !== null && points[hoveredIndex]}
450
- {@const p = points[hoveredIndex]}
491
+ {#if activeIndex >= 0 && points[activeIndex]}
492
+ {@const p = points[activeIndex]}
451
493
  <div
452
494
  class="st-areaChart__tooltip"
453
495
  role="presentation"
@@ -588,4 +630,19 @@
588
630
  font-size: 0.6875rem;
589
631
  font-weight: 600;
590
632
  }
633
+
634
+ /* --- Crosshair layer (FR-3) ----------------------------------------------
635
+ A tokenised dashed vertical line at the active (hovered/controlled) key,
636
+ plus an emphasised marker on the point. Decorative (aria-hidden). */
637
+ .st-areaChart__crosshairLine {
638
+ stroke: var(--st-semantic-border-strong);
639
+ stroke-width: 1;
640
+ stroke-dasharray: 3 3;
641
+ opacity: 0.7;
642
+ }
643
+ .st-areaChart__crosshairMarker {
644
+ fill: currentColor;
645
+ stroke: var(--st-semantic-surface-default);
646
+ stroke-width: 2;
647
+ }
591
648
  </style>
@@ -26,6 +26,20 @@ type AreaChartProps = {
26
26
  * ChartDataList.
27
27
  */
28
28
  dataLabels?: DataLabelsProp;
29
+ /**
30
+ * CONTROLLED synchronised hover key (FR-3). A datum's key is `String(x)`. When
31
+ * provided (string or null), the crosshair + tooltip track this key instead of
32
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
33
+ * share one hover channel across several aligned charts. Absent (`undefined`)
34
+ * keeps the legacy uncontrolled behaviour.
35
+ */
36
+ hoverKey?: string | null;
37
+ /**
38
+ * Emitted when the user hovers a datum (its key) or leaves the plot (`null`).
39
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
40
+ * keep the shared hover channel in sync.
41
+ */
42
+ onHoverKeyChange?: (key: string | null) => void;
29
43
  class?: string;
30
44
  };
31
45
  declare const AreaChart: import("svelte").Component<AreaChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"AreaChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AreaChart.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;CACX,CAAC;AAIJ,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,MAAM,GAAG,cAAc,CAAC,EAAE,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuWJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"AreaChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AreaChart.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;CACX,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,CAAC,MAAM,GAAG,cAAc,CAAC,EAAE,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAkYJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -57,6 +57,7 @@
57
57
  type ChartAnnotation
58
58
  } from "./chartAnnotations.js";
59
59
  import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
60
+ import { resolveActiveIndex } from "./chartCrosshair.js";
60
61
 
61
62
  type BarChartProps = {
62
63
  data: BarChartDatum[];
@@ -124,6 +125,20 @@
124
125
  * cross-chart parity and otherwise ignored.
125
126
  */
126
127
  showLegend?: boolean;
128
+ /**
129
+ * CONTROLLED synchronised hover key (FR-3). A bar's key is its `label`. When
130
+ * provided (string or null), the crosshair + tooltip track this key instead of
131
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
132
+ * share one hover channel across several aligned charts. Absent (`undefined`)
133
+ * keeps the legacy uncontrolled behaviour. Independent of `selectedKeys`.
134
+ */
135
+ hoverKey?: string | null;
136
+ /**
137
+ * Emitted when the user hovers a bar (its `label`) or leaves the plot (`null`).
138
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
139
+ * keep the shared hover channel in sync.
140
+ */
141
+ onHoverKeyChange?: (key: string | null) => void;
127
142
  class?: string;
128
143
  };
129
144
 
@@ -144,6 +159,8 @@
144
159
  scale = "linear",
145
160
  invertAxis = false,
146
161
  showLegend,
162
+ hoverKey,
163
+ onHoverKeyChange,
147
164
  class: className
148
165
  }: BarChartProps = $props();
149
166
 
@@ -599,19 +616,33 @@
599
616
  }));
600
617
  });
601
618
 
619
+ // Stable key per bar (FR-3): its `label`. Resolves a controlled `hoverKey` to
620
+ // an index and feeds `onHoverKeyChange` from pointer events.
621
+ const hoverKeys = $derived(bars.map((b) => b.datum.label));
622
+ function emitHoverKey(index: number | null) {
623
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
624
+ }
602
625
  function handleLeave() {
603
626
  hoveredIndex = null;
627
+ emitHoverKey(null);
604
628
  }
605
629
  function handleVisualPointerMove(event: PointerEvent) {
606
630
  const target = event.target;
607
631
  if (!(target instanceof Element)) {
608
632
  hoveredIndex = null;
633
+ emitHoverKey(null);
609
634
  return;
610
635
  }
611
- const index = Number(target.getAttribute("data-chart-index"));
612
- hoveredIndex = Number.isInteger(index) ? index : null;
636
+ const raw = Number(target.getAttribute("data-chart-index"));
637
+ const index = Number.isInteger(raw) ? raw : null;
638
+ hoveredIndex = index;
639
+ emitHoverKey(index);
613
640
  }
614
641
 
642
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
643
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
644
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
645
+
615
646
  const classes = () => ["st-barChart", className].filter(Boolean).join(" ");
616
647
  </script>
617
648
 
@@ -836,6 +867,20 @@
836
867
  {/each}
837
868
  </g>
838
869
  {/if}
870
+
871
+ <!-- Crosshair (FR-3) — a tokenised dashed line on the CATEGORY axis at the
872
+ active bar: vertical (vertical bars) / horizontal (horizontal bars).
873
+ Decorative (aria-hidden); the value is in the tooltip + ChartDataList. -->
874
+ {#if activeIndex >= 0 && bars[activeIndex]}
875
+ {@const cb = bars[activeIndex]}
876
+ <g class="st-barChart__crosshair" aria-hidden="true">
877
+ {#if isVertical}
878
+ <line class="st-barChart__crosshairLine" x1={cb.cx} x2={cb.cx} y1={MARGIN.top} y2={MARGIN.top + scales.plotHeight} />
879
+ {:else}
880
+ <line class="st-barChart__crosshairLine" x1={MARGIN.left} x2={MARGIN.left + scales.plotWidth} y1={cb.cy} y2={cb.cy} />
881
+ {/if}
882
+ </g>
883
+ {/if}
839
884
  </svg>
840
885
  </div>
841
886
 
@@ -861,8 +906,8 @@
861
906
 
862
907
  <ChartDataList {label} items={dataValueItems} />
863
908
 
864
- {#if hoveredIndex !== null && bars[hoveredIndex]}
865
- {@const bar = bars[hoveredIndex]}
909
+ {#if activeIndex >= 0 && bars[activeIndex]}
910
+ {@const bar = bars[activeIndex]}
866
911
  <div
867
912
  class="st-barChart__tooltip"
868
913
  role="presentation"
@@ -1124,4 +1169,14 @@
1124
1169
  font-size: 0.6875rem;
1125
1170
  font-weight: 600;
1126
1171
  }
1172
+
1173
+ /* --- Crosshair layer (FR-3) ----------------------------------------------
1174
+ A tokenised dashed line on the CATEGORY axis at the active (hovered/
1175
+ controlled) bar. Decorative (aria-hidden). */
1176
+ .st-barChart__crosshairLine {
1177
+ stroke: var(--st-semantic-border-strong);
1178
+ stroke-width: 1;
1179
+ stroke-dasharray: 3 3;
1180
+ opacity: 0.7;
1181
+ }
1127
1182
  </style>
@@ -99,6 +99,20 @@ type BarChartProps = {
99
99
  * cross-chart parity and otherwise ignored.
100
100
  */
101
101
  showLegend?: boolean;
102
+ /**
103
+ * CONTROLLED synchronised hover key (FR-3). A bar's key is its `label`. When
104
+ * provided (string or null), the crosshair + tooltip track this key instead of
105
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
106
+ * share one hover channel across several aligned charts. Absent (`undefined`)
107
+ * keeps the legacy uncontrolled behaviour. Independent of `selectedKeys`.
108
+ */
109
+ hoverKey?: string | null;
110
+ /**
111
+ * Emitted when the user hovers a bar (its `label`) or leaves the plot (`null`).
112
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
113
+ * keep the shared hover channel in sync.
114
+ */
115
+ onHoverKeyChange?: (key: string | null) => void;
102
116
  class?: string;
103
117
  };
104
118
  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;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;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,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;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAipBJ,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;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;AAI/F,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;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA8qBJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -60,6 +60,7 @@
60
60
  type ChartAnnotation
61
61
  } from "./chartAnnotations.js";
62
62
  import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
63
+ import { keyForX, resolveActiveIndex } from "./chartCrosshair.js";
63
64
 
64
65
  type LineChartProps = {
65
66
  data: LineChartDatum[];
@@ -111,6 +112,20 @@
111
112
  * no legend surface, so this prop is accepted for parity and otherwise ignored.
112
113
  */
113
114
  showLegend?: boolean;
115
+ /**
116
+ * CONTROLLED synchronised hover key (FR-3). A datum's key is `String(x)`. When
117
+ * provided (string or null), the crosshair + tooltip track this key instead of
118
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
119
+ * share one hover channel across several aligned charts. Absent (`undefined`)
120
+ * keeps the legacy uncontrolled behaviour.
121
+ */
122
+ hoverKey?: string | null;
123
+ /**
124
+ * Emitted when the user hovers a datum (its key) or leaves the plot (`null`).
125
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
126
+ * keep the shared hover channel in sync.
127
+ */
128
+ onHoverKeyChange?: (key: string | null) => void;
114
129
  class?: string;
115
130
  };
116
131
 
@@ -132,6 +147,8 @@
132
147
  scale = "linear",
133
148
  invertAxis = false,
134
149
  showLegend,
150
+ hoverKey,
151
+ onHoverKeyChange,
135
152
  class: className
136
153
  }: LineChartProps = $props();
137
154
 
@@ -617,19 +634,33 @@
617
634
  return entries;
618
635
  });
619
636
 
637
+ // Stable key per datum (FR-3): `String(x)`. Resolves a controlled `hoverKey`
638
+ // to an index and feeds `onHoverKeyChange` from pointer events.
639
+ const hoverKeys = $derived(data.map((d) => keyForX(d.x)));
640
+ function emitHoverKey(index: number | null) {
641
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
642
+ }
620
643
  function handleLeave() {
621
644
  hoveredIndex = null;
645
+ emitHoverKey(null);
622
646
  }
623
647
  function handleVisualPointerMove(event: PointerEvent) {
624
648
  const target = event.target;
625
649
  if (!(target instanceof Element)) {
626
650
  hoveredIndex = null;
651
+ emitHoverKey(null);
627
652
  return;
628
653
  }
629
- const index = Number(target.getAttribute("data-chart-index"));
630
- hoveredIndex = Number.isInteger(index) ? index : null;
654
+ const raw = Number(target.getAttribute("data-chart-index"));
655
+ const index = Number.isInteger(raw) ? raw : null;
656
+ hoveredIndex = index;
657
+ emitHoverKey(index);
631
658
  }
632
659
 
660
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
661
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
662
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
663
+
633
664
  const classes = () =>
634
665
  ["st-lineChart", `st-lineChart--${tone}`, className].filter(Boolean).join(" ");
635
666
  </script>
@@ -820,13 +851,23 @@
820
851
  {/each}
821
852
  </g>
822
853
  {/if}
854
+
855
+ <!-- Crosshair (FR-3) — a tokenised vertical line + marker at the active key.
856
+ Decorative (aria-hidden); the value is in the tooltip + ChartDataList. -->
857
+ {#if activeIndex >= 0 && points[activeIndex]}
858
+ {@const cp = points[activeIndex]}
859
+ <g class="st-lineChart__crosshair" aria-hidden="true">
860
+ <line class="st-lineChart__crosshairLine" x1={cp.x} x2={cp.x} y1={MARGIN.top} y2={MARGIN.top + plotHeight} />
861
+ <circle class="st-lineChart__crosshairMarker" cx={cp.x} cy={cp.y} r="5" />
862
+ </g>
863
+ {/if}
823
864
  </svg>
824
865
  </div>
825
866
 
826
867
  <ChartDataList {label} items={dataValueItems} />
827
868
 
828
- {#if hoveredIndex !== null && points[hoveredIndex]}
829
- {@const p = points[hoveredIndex]}
869
+ {#if activeIndex >= 0 && points[activeIndex]}
870
+ {@const p = points[activeIndex]}
830
871
  <div
831
872
  class="st-lineChart__tooltip"
832
873
  role="presentation"
@@ -1028,4 +1069,19 @@
1028
1069
  font-size: 0.6875rem;
1029
1070
  font-weight: 600;
1030
1071
  }
1072
+
1073
+ /* --- Crosshair layer (FR-3) ----------------------------------------------
1074
+ A tokenised dashed vertical line at the active (hovered/controlled) key,
1075
+ plus an emphasised marker on the point. Decorative (aria-hidden). */
1076
+ .st-lineChart__crosshairLine {
1077
+ stroke: var(--st-semantic-border-strong);
1078
+ stroke-width: 1;
1079
+ stroke-dasharray: 3 3;
1080
+ opacity: 0.7;
1081
+ }
1082
+ .st-lineChart__crosshairMarker {
1083
+ fill: currentColor;
1084
+ stroke: var(--st-semantic-surface-default);
1085
+ stroke-width: 2;
1086
+ }
1031
1087
  </style>
@@ -86,6 +86,20 @@ type LineChartProps = {
86
86
  * no legend surface, so this prop is accepted for parity and otherwise ignored.
87
87
  */
88
88
  showLegend?: boolean;
89
+ /**
90
+ * CONTROLLED synchronised hover key (FR-3). A datum's key is `String(x)`. When
91
+ * provided (string or null), the crosshair + tooltip track this key instead of
92
+ * the chart's internal pointer hover (null ⇒ nothing shown), letting a parent
93
+ * share one hover channel across several aligned charts. Absent (`undefined`)
94
+ * keeps the legacy uncontrolled behaviour.
95
+ */
96
+ hoverKey?: string | null;
97
+ /**
98
+ * Emitted when the user hovers a datum (its key) or leaves the plot (`null`).
99
+ * Always fired on pointer move/leave — even while CONTROLLED — so dataviz can
100
+ * keep the shared hover channel in sync.
101
+ */
102
+ onHoverKeyChange?: (key: string | null) => void;
89
103
  class?: string;
90
104
  };
91
105
  declare const LineChart: import("svelte").Component<LineChartProps, {}, "">;
@@ -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;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"}
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;AAI/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;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+qBJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,19 @@
1
+ /** Serialises a Line/Area datum's `x` to its stable hover key. */
2
+ export declare function keyForX(x: number | string): string;
3
+ /**
4
+ * Resolves the hover key to a datum index within `keys` (the ordered list of
5
+ * every datum's key). Returns -1 when the key is null/undefined or unmatched.
6
+ */
7
+ export declare function indexForHoverKey(hoverKey: string | null | undefined, keys: string[]): number;
8
+ /**
9
+ * Picks the datum index to DISPLAY the crosshair/tooltip at.
10
+ * - Controlled (`hoverKey !== undefined`): the index of `hoverKey` in `keys`
11
+ * (or -1 when null/unmatched). The internal pointer index is ignored for
12
+ * display.
13
+ * - Uncontrolled (`hoverKey === undefined`): the internal pointer index.
14
+ * Returns -1 when nothing should be shown.
15
+ */
16
+ export declare function resolveActiveIndex(hoverKey: string | null | undefined, internalIndex: number | null, keys: string[]): number;
17
+ /** True when the chart is CONTROLLED (the parent supplied `hoverKey`). */
18
+ export declare function isControlled(hoverKey: string | null | undefined): boolean;
19
+ //# sourceMappingURL=chartCrosshair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chartCrosshair.d.ts","sourceRoot":"","sources":["../src/lib/chartCrosshair.ts"],"names":[],"mappings":"AAsBA,kEAAkE;AAClE,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAG5F;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAGR;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEzE"}
@@ -0,0 +1,51 @@
1
+ // --- Chart crosshair / synchronised hover layer (shared, framework-agnostic) -
2
+ //
3
+ // FR-3: a CONTROLLED crosshair + tooltip whose position is driven by a `hoverKey`
4
+ // string the parent owns, so dataviz can share one hover channel across several
5
+ // aligned panels (a "linked" tooltip). The DS stays presentational: it resolves
6
+ // the key to a datum index, draws a tokenised vertical line at that x (plus a
7
+ // marker on the point for Line/Area), and reuses the existing tooltip surface.
8
+ //
9
+ // The "key" is the stable identifier of a datum on the categorical/x axis:
10
+ // - Bar: the bar's `label`.
11
+ // - Line/Area: the point's `x`, serialised with `String(x)`.
12
+ //
13
+ // Behaviour:
14
+ // - `hoverKey === undefined` → UNCONTROLLED (internal hover, unchanged, fully
15
+ // backward compatible).
16
+ // - `hoverKey` provided (string or null) → CONTROLLED: the displayed
17
+ // crosshair/tooltip tracks `hoverKey` (null = nothing shown), the chart's own
18
+ // pointer hover no longer drives the DISPLAY, but `onHoverKeyChange` is still
19
+ // emitted so the parent can keep the shared channel in sync.
20
+ //
21
+ // Purely additive: a chart that passes neither prop renders exactly as before.
22
+ /** Serialises a Line/Area datum's `x` to its stable hover key. */
23
+ export function keyForX(x) {
24
+ return String(x);
25
+ }
26
+ /**
27
+ * Resolves the hover key to a datum index within `keys` (the ordered list of
28
+ * every datum's key). Returns -1 when the key is null/undefined or unmatched.
29
+ */
30
+ export function indexForHoverKey(hoverKey, keys) {
31
+ if (hoverKey == null)
32
+ return -1;
33
+ return keys.indexOf(hoverKey);
34
+ }
35
+ /**
36
+ * Picks the datum index to DISPLAY the crosshair/tooltip at.
37
+ * - Controlled (`hoverKey !== undefined`): the index of `hoverKey` in `keys`
38
+ * (or -1 when null/unmatched). The internal pointer index is ignored for
39
+ * display.
40
+ * - Uncontrolled (`hoverKey === undefined`): the internal pointer index.
41
+ * Returns -1 when nothing should be shown.
42
+ */
43
+ export function resolveActiveIndex(hoverKey, internalIndex, keys) {
44
+ if (hoverKey !== undefined)
45
+ return indexForHoverKey(hoverKey, keys);
46
+ return internalIndex == null ? -1 : internalIndex;
47
+ }
48
+ /** True when the chart is CONTROLLED (the parent supplied `hoverKey`). */
49
+ export function isControlled(hoverKey) {
50
+ return hoverKey !== undefined;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/design-system-svelte",
3
- "version": "0.34.24",
3
+ "version": "0.34.25",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"