@fundar/data-chart-telling 0.0.26 → 0.0.27

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 (58) hide show
  1. package/dist/charts/BaseChart.svelte +18 -10
  2. package/dist/charts/BaseChart.svelte.d.ts +4 -2
  3. package/dist/charts/heatmap/Chart.svelte +2 -2
  4. package/dist/charts/pyramid/Chart.svelte +2 -2
  5. package/dist/configuration/config.svelte.js +26 -0
  6. package/dist/configuration/themes/dark.js +4 -0
  7. package/dist/configuration/themes/index.d.ts +64 -0
  8. package/dist/index.d.ts +6 -2
  9. package/dist/index.js +2 -0
  10. package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -3
  11. package/dist/layout/facet/FacetIndicator.svelte +31 -0
  12. package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
  13. package/dist/layout/facet/FacetLayout.svelte +249 -24
  14. package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
  15. package/dist/layout/legend/ContinuousSection.svelte +1 -1
  16. package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
  17. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  18. package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
  19. package/dist/layout/legend/LegendLayout.svelte +1 -1
  20. package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
  21. package/dist/layout/legend/interaction.svelte.d.ts +9 -1
  22. package/dist/layout/legend/interaction.svelte.js +9 -1
  23. package/dist/layout/timeline/Timeline.svelte +1 -1
  24. package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
  25. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  26. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  27. package/dist/layout/tooltip/controller.svelte.js +16 -2
  28. package/dist/markers/HoverMarker.svelte +2 -2
  29. package/dist/plots/bar/Plot.svelte +43 -4
  30. package/dist/plots/bar/ValueLabels.svelte +69 -23
  31. package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
  32. package/dist/plots/heatmap/Plot.svelte +4 -4
  33. package/dist/plots/heatmap/ValueLabels.svelte +3 -3
  34. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
  35. package/dist/plots/line/Plot.svelte +34 -29
  36. package/dist/plots/line/ValueLabels.svelte +73 -51
  37. package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
  38. package/dist/plots/pyramid/Plot.svelte +41 -6
  39. package/dist/plots/pyramid/ValueLabels.svelte +70 -24
  40. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
  41. package/dist/plots/scatter/Plot.svelte +40 -4
  42. package/dist/plots/scatter/ValueLabels.svelte +90 -47
  43. package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
  44. package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
  45. package/dist/plots/utils/labelOverflow.svelte.js +116 -0
  46. package/dist/plots/utils/legendDisabled.d.ts +1 -1
  47. package/dist/types/charts/common.d.ts +3 -50
  48. package/dist/types/charts/props.d.ts +3 -2
  49. package/dist/types/configuration/styling.d.ts +19 -0
  50. package/dist/types/layout/facet.d.ts +41 -0
  51. package/dist/types/layout/legend.d.ts +65 -1
  52. package/dist/types/layout/styles.d.ts +1 -12
  53. package/dist/types/layout/timeline.d.ts +32 -0
  54. package/dist/types/layout/timeline.js +1 -0
  55. package/dist/utils/interpolate.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/dist/types/charts/legend.d.ts +0 -60
  58. /package/dist/types/{charts/legend.js → layout/facet.js} +0 -0
@@ -1,4 +1,4 @@
1
- import type { FacetColumns, FacetCellSnippet } from '../../types/charts/common';
1
+ import type { FacetColumns, FacetMode, Responsive, FacetCellSnippet, FacetIndicatorSnippet, FacetNavSnippet } from '../../types/layout/facet';
2
2
  import type { Accessor } from '../../types/plots/data/common';
3
3
  declare function $$render<TRow extends Record<string, unknown>>(): {
4
4
  props: {
@@ -7,6 +7,12 @@ declare function $$render<TRow extends Record<string, unknown>>(): {
7
7
  columns?: FacetColumns;
8
8
  values?: string[];
9
9
  gap?: number;
10
+ mode?: Responsive<FacetMode>;
11
+ visible?: number;
12
+ pageSize?: number;
13
+ formatIndicator?: (current: number, total: number) => string;
14
+ indicator?: FacetIndicatorSnippet;
15
+ navButton?: FacetNavSnippet;
10
16
  width: number;
11
17
  height: number;
12
18
  cell: FacetCellSnippet<TRow>;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { untrack } from 'svelte';
3
- import type { ContinuousLegendSection } from '../../types/charts/legend';
3
+ import type { ContinuousLegendSection } from '../../types/layout/legend';
4
4
  import { makeColorScale } from '../../utils/color';
5
5
  import { getConfiguration } from '../../configuration/config.svelte';
6
6
  import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
@@ -1,4 +1,4 @@
1
- import type { ContinuousLegendSection } from '../../types/charts/legend';
1
+ import type { ContinuousLegendSection } from '../../types/layout/legend';
2
2
  type $$ComponentProps = {
3
3
  section: ContinuousLegendSection;
4
4
  };
@@ -3,7 +3,7 @@
3
3
  import { getConfiguration } from '../../configuration/config.svelte';
4
4
  import { paletteColor } from '../../utils/color';
5
5
  import { getLegendInteraction, createLegendInteraction } from './interaction.svelte';
6
- import type { DiscreteLegendSection, LegendItem } from '../../types/charts/legend';
6
+ import type { DiscreteLegendSection, LegendItem } from '../../types/layout/legend';
7
7
  import type { LineStyle } from '../../types/plots/constants';
8
8
 
9
9
  let { section }: { section: DiscreteLegendSection } = $props();
@@ -1,4 +1,4 @@
1
- import type { DiscreteLegendSection } from '../../types/charts/legend';
1
+ import type { DiscreteLegendSection } from '../../types/layout/legend';
2
2
  type $$ComponentProps = {
3
3
  section: DiscreteLegendSection;
4
4
  };
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { LegendSection } from '../../types/charts/legend';
2
+ import type { LegendSection } from '../../types/layout/legend';
3
3
  import DiscreteSection from './DiscreteSection.svelte';
4
4
  import ContinuousSection from './ContinuousSection.svelte';
5
5
 
@@ -1,4 +1,4 @@
1
- import type { LegendSection } from '../../types/charts/legend';
1
+ import type { LegendSection } from '../../types/layout/legend';
2
2
  type $$ComponentProps = {
3
3
  sections: LegendSection[];
4
4
  };
@@ -1,7 +1,15 @@
1
1
  import type { LegendInteractionStore } from '../../types/layout/legend';
2
2
  /** Create + register the shared legend-interaction store. Call during component init. */
3
3
  export declare function provideLegendInteraction(): LegendInteractionStore;
4
- /** Read the shared legend-interaction store, or `null` when there is no provider. */
4
+ /**
5
+ * Read the shared legend-interaction store, or `null` when there is no provider.
6
+ *
7
+ * Only resolves correctly when called during a real descendant component's
8
+ * own `<script>` instantiation, not from inside a `{@render}`ed snippet body
9
+ * — snippets run in their author's scope rather than as a fresh component
10
+ * instance. `BaseChart`'s `plot` snippet gets the same data via its `legend`
11
+ * context field instead, which works from either scope.
12
+ */
5
13
  export declare function getLegendInteraction(): LegendInteractionStore | null;
6
14
  /** A private legend-interaction store for a legend/plot used outside of a {@link BaseChart}. */
7
15
  export declare function createLegendInteraction(): LegendInteractionStore;
@@ -13,7 +13,15 @@ export function provideLegendInteraction() {
13
13
  setContext(LEGEND_INTERACTION_KEY, store);
14
14
  return store;
15
15
  }
16
- /** Read the shared legend-interaction store, or `null` when there is no provider. */
16
+ /**
17
+ * Read the shared legend-interaction store, or `null` when there is no provider.
18
+ *
19
+ * Only resolves correctly when called during a real descendant component's
20
+ * own `<script>` instantiation, not from inside a `{@render}`ed snippet body
21
+ * — snippets run in their author's scope rather than as a fresh component
22
+ * instance. `BaseChart`'s `plot` snippet gets the same data via its `legend`
23
+ * context field instead, which works from either scope.
24
+ */
17
25
  export function getLegendInteraction() {
18
26
  return getContext(LEGEND_INTERACTION_KEY) ?? null;
19
27
  }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Plot, RuleX, RuleY, Text } from 'svelteplot';
3
- import type { Orientation, TimeValue } from '../../types/charts/common';
3
+ import type { Orientation, TimeValue } from '../../types/layout/timeline';
4
4
 
5
5
  /**
6
6
  * Purely visual time axis: a baseline rule plus the ordered step labels with
@@ -1,4 +1,4 @@
1
- import type { Orientation, TimeValue } from '../../types/charts/common';
1
+ import type { Orientation, TimeValue } from '../../types/layout/timeline';
2
2
  type $$ComponentProps = {
3
3
  currentValue: TimeValue;
4
4
  values: TimeValue[];
@@ -7,7 +7,7 @@
7
7
  TimeValue,
8
8
  TimelineChildSnippet,
9
9
  TimelineInterpolation,
10
- } from '../../types/charts/common';
10
+ } from '../../types/layout/timeline';
11
11
  import type { Accessor } from '../../types/plots/data/common';
12
12
 
13
13
  /**
@@ -1,4 +1,4 @@
1
- import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/charts/common';
1
+ import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/layout/timeline';
2
2
  import type { Accessor } from '../../types/plots/data/common';
3
3
  declare function $$render<TRow extends Record<string, unknown>>(): {
4
4
  props: {
@@ -47,12 +47,26 @@ export function createHoverController(args) {
47
47
  * point at the row under the cursor rather than an arbitrary matched
48
48
  * sibling. `BasePlotLayout` gates actual display with
49
49
  * `impliesCrosshairX`/`Y` + the marker's own `ruleX`/`ruleY` props.
50
+ *
51
+ * `activeSeries` alone can't disambiguate two matched rows that share the
52
+ * same (or absent) series tag — e.g. an ungrouped bar/line chart where two
53
+ * categories tie on the matched axis value. `onPointer` sets `activeX`/
54
+ * `activeY` straight from the hit row, so when more than one series-tied
55
+ * candidate remains, narrowing to the one whose own x/y equals the hit
56
+ * row's picks the actual row under the cursor instead of an arbitrary tie.
50
57
  */
51
58
  const activePoint = $derived.by(() => {
52
59
  if (matched.length === 0)
53
60
  return null;
54
- const series = args.hover().activeSeries;
55
- return matched.find((p) => p.series === series) ?? matched[0];
61
+ const h = args.hover();
62
+ const bySeries = matched.filter((p) => p.series === h.activeSeries);
63
+ const candidates = bySeries.length > 0 ? bySeries : matched;
64
+ if (candidates.length === 1)
65
+ return candidates[0];
66
+ const { activeX, activeY } = h;
67
+ if (activeX == null || activeY == null)
68
+ return candidates[0];
69
+ return candidates.find((p) => eq(p.x, activeX) && eq(p.y, activeY)) ?? candidates[0];
56
70
  });
57
71
  // Whether the current strategy implies a crosshair rule on each axis by default.
58
72
  const _impliesCrosshairX = $derived(impliesCrosshairX(args.hover().strategy));
@@ -102,7 +102,7 @@
102
102
  {/if}
103
103
 
104
104
  {#if data.length > 0}
105
- {#each data as point (point.series ?? `${point.x}_${point.y}`)}
105
+ {#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
106
106
  <!-- Pixel nudge for plot kinds whose series position isn't fully captured
107
107
  by x/y alone (e.g. BarPlot's 'grouped' layout) — see HoverDisplayPoint.dx/dy. -->
108
108
  <g transform="translate({point.dx ?? 0}, {point.dy ?? 0})">
@@ -120,7 +120,7 @@
120
120
  </g>
121
121
  {/each}
122
122
  {#if showLabels}
123
- {#each data as point (point.series ?? `${point.x}_${point.y}`)}
123
+ {#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
124
124
  <g transform="translate({point.dx ?? 0}, {point.dy ?? 0})">
125
125
  <TextMarker
126
126
  data={[point]}
@@ -10,6 +10,7 @@
10
10
  import { getConfiguration } from '../../configuration/config.svelte';
11
11
  import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
12
12
  import { createBarLayout } from './layout.svelte';
13
+ import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
13
14
  import { buildHoverPoints } from './hoverPoints';
14
15
  import { buildBarMarkers } from './buildBarMarkers';
15
16
  import ValueLabels from './ValueLabels.svelte';
@@ -22,6 +23,7 @@
22
23
  import type { BasePlotStylesConfig } from '../../types/layout/styles';
23
24
  import type { ValueAnchor } from '../../types/plots/constants';
24
25
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
26
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
25
27
 
26
28
  /**
27
29
  * A bar chart, vertical by default. Accepts either a pre-built `series`
@@ -63,7 +65,7 @@
63
65
  buildGroupedSeries<Series<TData>, BarSegmentStyle>(resolvedSeries, segments, false),
64
66
  );
65
67
  const showVals = $derived(styles.values?.show ?? false);
66
- const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
68
+ const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
67
69
  const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
68
70
 
69
71
  // `xAcc`/`yAcc` are always data-semantic (category/value) — independent of
@@ -153,6 +155,23 @@
153
155
  // `row` (see BasePlotLayout/HoverPoint) is what lets the template recover the
154
156
  // semantic category regardless of which axis it landed on.
155
157
  const legendInteraction = $derived(getLegendInteraction());
158
+ const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
159
+
160
+ // A value label hovered while `styles.values.hoverIsolate` is on dims
161
+ // every other series exactly like a legend toggle would — local to this
162
+ // one `<Plot>` instance (not the shared HoverStore) so the gesture can't
163
+ // leak into unrelated tooltip/crosshair behavior.
164
+ let hoverIsolatedSeries = $state<string | null>(null);
165
+
166
+ function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
167
+ return (
168
+ legendInteraction?.disabledSeries.get(name) ??
169
+ (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
170
+ ? { opacity: cfg.legend.disabledOpacity }
171
+ : undefined)
172
+ );
173
+ }
174
+
156
175
  // A legend-disabled series is dimmed on the bar itself, but never surfaces
157
176
  // in hover/tooltip — its points are simply absent from the candidate list.
158
177
  const hoverPoints = $derived(
@@ -178,6 +197,13 @@
178
197
  // comment.
179
198
  pointMatching: 'contains' as const,
180
199
  });
200
+
201
+ // A bar's value label can grow past the plot's own content box (e.g. a
202
+ // tall bar's `anchor: 'outside'` label pushing past the top margin) —
203
+ // reserving real space there is what each label's own overflow
204
+ // measurement feeds into here, via a tracker local to this one `<Plot>`
205
+ // instance. See `labelOverflow.svelte.ts`.
206
+ const labelMargin = createLabelMarginTracker(() => margins);
181
207
  </script>
182
208
 
183
209
  <BasePlotLayout
@@ -189,7 +215,7 @@
189
215
  getY={plotGetY}
190
216
  {hover}
191
217
  scales={resolvedScales}
192
- {margins}
218
+ margins={labelMargin.resolvedMargins}
193
219
  xTickRotate={resolvedScales?.x?.tickRotate}
194
220
  markers={({ setHoverMatch, clearHoverMatch }) => [
195
221
  ...buildBarMarkers({
@@ -198,7 +224,7 @@
198
224
  isHorizontal,
199
225
  hoverPoints,
200
226
  palette: cfg.palette,
201
- disabledFillFor: (name) => disabledFillOverride(legendInteraction?.disabledSeries.get(name)),
227
+ disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
202
228
  active: hover.active,
203
229
  setHoverMatch,
204
230
  clearHoverMatch,
@@ -210,7 +236,20 @@
210
236
  >
211
237
  {#if showVals}
212
238
  {#each groupedSeries as group, seriesIndex (group.series.name)}
213
- <ValueLabels {group} {seriesIndex} {barLayout} {isHorizontal} {valAnchor} {fmtVal} values={styles.values} axisColor={cfg.axis.color} />
239
+ <ValueLabels
240
+ {group}
241
+ {seriesIndex}
242
+ {barLayout}
243
+ {isHorizontal}
244
+ {valAnchor}
245
+ {formatValue}
246
+ values={styles.values}
247
+ axisColor={cfg.axis.color}
248
+ disabled={effectiveDisabledFor(group.series.name)}
249
+ onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
250
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
251
+ onOverflow={labelMargin.report}
252
+ />
214
253
  {/each}
215
254
  {/if}
216
255
  </BasePlotLayout>
@@ -1,11 +1,15 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { Text } from 'svelteplot';
2
+ import { Text, usePlot } from 'svelteplot';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
+ import { disabledFillOverride } from '../utils/legendDisabled';
5
+ import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
4
6
  import type { Series } from '../../types/plots/data/common';
5
7
  import type { ValuesStyle } from '../../types/layout/styles';
6
8
  import type { ValueAnchor } from '../../types/plots/constants';
7
9
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
8
10
  import type { VisualGroup } from '../../types/plots/segments/config';
11
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
12
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
9
13
  import type { BarLayout } from './layout.svelte';
10
14
 
11
15
  /**
@@ -21,20 +25,56 @@
21
25
  barLayout,
22
26
  isHorizontal,
23
27
  valAnchor,
24
- fmtVal,
28
+ formatValue,
25
29
  values,
26
30
  axisColor,
31
+ disabled,
32
+ onHoverSeries,
33
+ onHoverEnd,
34
+ onOverflow,
27
35
  }: {
28
36
  group: VisualGroup<Series<TData>, BarSegmentStyle>;
29
37
  seriesIndex: number;
30
38
  barLayout: BarLayout<TData>;
31
39
  isHorizontal: boolean;
32
40
  valAnchor: ValueAnchor;
33
- fmtVal: (value: number, seriesName: string) => string;
41
+ formatValue: (value: number, seriesName: string) => string;
34
42
  values: ValuesStyle | undefined;
35
43
  axisColor: string;
44
+ /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
45
+ disabled?: LegendDisabledStyle;
46
+ /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
47
+ onHoverSeries?: () => void;
48
+ onHoverEnd?: () => void;
49
+ /** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
50
+ onOverflow?: (overflow: MarginOverflow) => void;
36
51
  } = $props();
37
52
 
53
+ const fillOverride = $derived(disabledFillOverride(disabled));
54
+ const plot = usePlot();
55
+ let groupEl: SVGGElement | undefined = $state();
56
+
57
+ watchLabelOverflow({
58
+ el: () => groupEl,
59
+ bounds: () => {
60
+ // getBBox() inside isn't itself tracked — depend on whatever actually
61
+ // changes the rendered label layout so this re-measures.
62
+ void group;
63
+ void values;
64
+ void valAnchor;
65
+ const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
66
+ const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
67
+ if (!xRange || !yRange) return undefined;
68
+ return {
69
+ left: Math.min(xRange[0], xRange[1]),
70
+ right: Math.max(xRange[0], xRange[1]),
71
+ top: Math.min(yRange[0], yRange[1]),
72
+ bottom: Math.max(yRange[0], yRange[1]),
73
+ };
74
+ },
75
+ onOverflow: (overflow) => onOverflow?.(overflow),
76
+ });
77
+
38
78
  const xFn = $derived(resolveAccessor(group.series.x));
39
79
  const yFn = $derived(resolveAccessor(group.series.y));
40
80
 
@@ -68,23 +108,29 @@
68
108
  const resolvedTextAnchor = $derived(font?.textAnchor === 'outside' ? undefined : font?.textAnchor);
69
109
  </script>
70
110
 
71
- <Text
72
- data={group.series.data}
73
- {...isHorizontal ? { y: xFn, x: valueTextFn } : { x: xFn, y: valueTextFn }}
74
- text={(d: TData) => fmtVal(Number(yFn(d)), group.series.name)}
75
- dx={font?.dx ?? (isHorizontal ? defaultOffset : groupOffset.dx)}
76
- dy={font?.dy ?? (isHorizontal ? groupOffset.dy : defaultOffset)}
77
- textAnchor={resolvedTextAnchor ?? (isHorizontal ? (valAnchor === 'outside' || valAnchor === 'end' ? 'start' : valAnchor === 'start' ? 'end' : 'middle') : 'middle')}
78
- lineAnchor={font?.lineAnchor ?? (isHorizontal ? 'middle' : defaultLA)}
79
- lineHeight={font?.lineHeight}
80
- rotate={font?.rotate}
81
- class={font?.class}
82
- textClass={font?.textClass}
83
- fill={font?.fill ?? axisColor}
84
- fontSize={font?.fontSize ?? 12}
85
- fontWeight={font?.fontWeight}
86
- fontStyle={font?.fontStyle}
87
- stroke={font?.stroke}
88
- strokeWidth={font?.strokeWidth}
89
- paintOrder={font?.paintOrder}
90
- />
111
+ <!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
112
+ despite declaring them in its types — a wrapping <g> catches the
113
+ boundary crossing instead (pointerenter/leave fire on every ancestor). -->
114
+ <g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
115
+ <Text
116
+ data={group.series.data}
117
+ {...isHorizontal ? { y: xFn, x: valueTextFn } : { x: xFn, y: valueTextFn }}
118
+ text={(d: TData) => formatValue(Number(yFn(d)), group.series.name)}
119
+ dx={font?.dx ?? (isHorizontal ? defaultOffset : groupOffset.dx)}
120
+ dy={font?.dy ?? (isHorizontal ? groupOffset.dy : defaultOffset)}
121
+ textAnchor={resolvedTextAnchor ?? (isHorizontal ? (valAnchor === 'outside' || valAnchor === 'end' ? 'start' : valAnchor === 'start' ? 'end' : 'middle') : 'middle')}
122
+ lineAnchor={font?.lineAnchor ?? (isHorizontal ? 'middle' : defaultLA)}
123
+ lineHeight={font?.lineHeight}
124
+ rotate={font?.rotate}
125
+ class={font?.class}
126
+ textClass={font?.textClass}
127
+ fill={font?.fill ?? axisColor}
128
+ fillOpacity={fillOverride?.fillOpacity}
129
+ fontSize={font?.fontSize ?? 12}
130
+ fontWeight={font?.fontWeight}
131
+ fontStyle={font?.fontStyle}
132
+ stroke={font?.stroke}
133
+ strokeWidth={font?.strokeWidth}
134
+ paintOrder={font?.paintOrder}
135
+ />
136
+ </g>
@@ -3,6 +3,8 @@ import type { ValuesStyle } from '../../types/layout/styles';
3
3
  import type { ValueAnchor } from '../../types/plots/constants';
4
4
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
5
5
  import type { VisualGroup } from '../../types/plots/segments/config';
6
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
7
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
6
8
  import type { BarLayout } from './layout.svelte';
7
9
  declare function $$render<TData extends Record<string, unknown>>(): {
8
10
  props: {
@@ -11,9 +13,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
11
13
  barLayout: BarLayout<TData>;
12
14
  isHorizontal: boolean;
13
15
  valAnchor: ValueAnchor;
14
- fmtVal: (value: number, seriesName: string) => string;
16
+ formatValue: (value: number, seriesName: string) => string;
15
17
  values: ValuesStyle | undefined;
16
18
  axisColor: string;
19
+ /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
20
+ disabled?: LegendDisabledStyle;
21
+ /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
22
+ onHoverSeries?: () => void;
23
+ onHoverEnd?: () => void;
24
+ /** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
25
+ onOverflow?: (overflow: MarginOverflow) => void;
17
26
  };
18
27
  exports: {};
19
28
  bindings: "";
@@ -73,7 +73,7 @@
73
73
  const minColor = $derived(styles.colors?.min ?? cfg.continuous.min);
74
74
  const maxColor = $derived(styles.colors?.max ?? cfg.continuous.max);
75
75
  const showVals = $derived(styles.values?.show ?? true);
76
- const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
76
+ const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
77
77
 
78
78
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, undefined));
79
79
  const flat = $derived(resolvedSeries.flatMap((s) => s.data));
@@ -127,7 +127,7 @@
127
127
  x: getX(d) as AxisValue,
128
128
  y: getY(d) as AxisValue,
129
129
  row: d,
130
- label: fmtVal(Number(getZ(d)), ''),
130
+ label: formatValue(Number(getZ(d)), ''),
131
131
  })),
132
132
  );
133
133
 
@@ -216,7 +216,7 @@
216
216
  const hover = $derived({
217
217
  active: tooltip != null || hasHoverMarker(markers),
218
218
  points: hoverPoints,
219
- label: (d: TData) => fmtVal(Number(getZ(d)), ''),
219
+ label: (d: TData) => formatValue(Number(getZ(d)), ''),
220
220
  strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'punctual'),
221
221
  sync: resolveHoverSync(tooltip?.sync, markers),
222
222
  tooltip,
@@ -244,7 +244,7 @@
244
244
  {getX}
245
245
  {getY}
246
246
  {getZ}
247
- {fmtVal}
247
+ {formatValue}
248
248
  values={styles.values}
249
249
  defaultColor={cfg.continuous.labelColor}
250
250
  />
@@ -14,7 +14,7 @@
14
14
  getX,
15
15
  getY,
16
16
  getZ,
17
- fmtVal,
17
+ formatValue,
18
18
  values,
19
19
  defaultColor,
20
20
  }: {
@@ -22,7 +22,7 @@
22
22
  getX: (d: TData) => AxisValue;
23
23
  getY: (d: TData) => AxisValue;
24
24
  getZ: (d: TData) => unknown;
25
- fmtVal: (value: number, seriesName: string) => string;
25
+ formatValue: (value: number, seriesName: string) => string;
26
26
  values: ValuesStyle | undefined;
27
27
  defaultColor: string;
28
28
  } = $props();
@@ -37,7 +37,7 @@
37
37
  {data}
38
38
  x={getX}
39
39
  y={getY}
40
- text={(d: TData) => fmtVal(Number(getZ(d)), '')}
40
+ text={(d: TData) => formatValue(Number(getZ(d)), '')}
41
41
  fill={font?.fill ?? defaultColor}
42
42
  fontSize={font?.fontSize ?? 12}
43
43
  fontWeight={font?.fontWeight ?? 600}
@@ -6,7 +6,7 @@ declare function $$render<TData extends Record<string, unknown>>(): {
6
6
  getX: (d: TData) => AxisValue;
7
7
  getY: (d: TData) => AxisValue;
8
8
  getZ: (d: TData) => unknown;
9
- fmtVal: (value: number, seriesName: string) => string;
9
+ formatValue: (value: number, seriesName: string) => string;
10
10
  values: ValuesStyle | undefined;
11
11
  defaultColor: string;
12
12
  };
@@ -9,6 +9,7 @@
9
9
  import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
10
10
  import { disabledStrokeOverride, disabledDotsOverride } from '../utils/legendDisabled';
11
11
  import { buildLineMarkers } from './buildLineMarkers';
12
+ import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
12
13
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
13
14
  import ValueLabels from './ValueLabels.svelte';
14
15
  import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
@@ -19,6 +20,7 @@
19
20
  import type { BasePlotStylesConfig } from '../../types/layout/styles';
20
21
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
21
22
  import type { LineMarkersConfig } from '../../types/markers/line';
23
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
22
24
 
23
25
  type Props = BasePlotProps<
24
26
  SeriesProps<TData> | DataProps<TData>,
@@ -56,6 +58,22 @@
56
58
 
57
59
  const cfg = $derived(getConfiguration());
58
60
  const showVals = $derived(styles.values?.show ?? false);
61
+ const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
62
+
63
+ // A value label hovered while `styles.values.hoverIsolate` is on dims
64
+ // every other series exactly like a legend toggle would — local to this
65
+ // one `<Plot>` instance (not the shared HoverStore) so the gesture can't
66
+ // leak into unrelated tooltip/crosshair behavior.
67
+ let hoverIsolatedSeries = $state<string | null>(null);
68
+
69
+ function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
70
+ return (
71
+ legendInteraction?.disabledSeries.get(name) ??
72
+ (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
73
+ ? { opacity: cfg.legend.disabledOpacity }
74
+ : undefined)
75
+ );
76
+ }
59
77
 
60
78
  const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
61
79
  const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
@@ -97,8 +115,8 @@
97
115
  const lineMarkers = $derived(
98
116
  buildLineMarkers({
99
117
  groupedSeries,
100
- disabledStrokeFor: (name) => disabledStrokeOverride(legendInteraction?.disabledSeries.get(name)),
101
- disabledDotsFor: (name) => disabledDotsOverride(legendInteraction?.disabledSeries.get(name)),
118
+ disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
119
+ disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name)),
102
120
  }),
103
121
  );
104
122
 
@@ -114,31 +132,11 @@
114
132
  // End-of-line labels can grow past the plot's own content box, into its
115
133
  // margin — reserving real space there (rather than padding the domain,
116
134
  // which would falsely imply data exists past the last real point) is
117
- // what ValueLabels's own overflow measurement feeds into here. Grown
118
- // monotonically (never shrunk) from that real `getBBox()` measurement:
119
- // once the real overflow reaches ~0 (the margin now fits it), reporting
120
- // that same value again is a no-op (`Math.max` of an unchanged value),
121
- // which is what keeps this converging instead of oscillating between
122
- // "shrink margin → overflow reappears → grow margin → …". Local to this
123
- // one `<Plot>` instance — see ValueLabels.svelte's own doc comment for
124
- // why that locality (vs. svelteplot's shared margin context) matters.
125
- let measuredLabelMargin = $state({ left: 0, right: 0 });
126
-
127
- function handleLabelOverflow(overflow: { left: number; right: number }) {
128
- const left = Math.max(measuredLabelMargin.left, overflow.left);
129
- const right = Math.max(measuredLabelMargin.right, overflow.right);
130
- if (left !== measuredLabelMargin.left || right !== measuredLabelMargin.right) {
131
- measuredLabelMargin = { left, right };
132
- }
133
- }
134
-
135
- const resolvedMargins = $derived.by(() => {
136
- if (measuredLabelMargin.left <= 0 && measuredLabelMargin.right <= 0) return margins;
137
- const out = { ...margins };
138
- if (measuredLabelMargin.right > 0 && margins?.right === undefined) out.right = measuredLabelMargin.right;
139
- if (measuredLabelMargin.left > 0 && margins?.left === undefined) out.left = measuredLabelMargin.left;
140
- return out;
141
- });
135
+ // what ValueLabels's own overflow measurement feeds into here, via a
136
+ // tracker local to this one `<Plot>` instance — see ValueLabels.svelte's
137
+ // own doc comment for why that locality (vs. svelteplot's shared margin
138
+ // context) matters.
139
+ const labelMargin = createLabelMarginTracker(() => margins);
142
140
  </script>
143
141
 
144
142
  <BasePlotLayout
@@ -150,13 +148,20 @@
150
148
  getY={(d) => Number(yAcc(d))}
151
149
  {hover}
152
150
  {scales}
153
- margins={resolvedMargins}
151
+ margins={labelMargin.resolvedMargins}
154
152
  xTickRotate={scales?.x?.tickRotate}
155
153
  markers={[...lineMarkers, ...markers]}
156
154
  seriesData={resolvedSeries}
157
155
  {scopedGroups}
158
156
  >
159
157
  {#if showVals}
160
- <ValueLabels {lastPoints} values={styles.values} onOverflow={handleLabelOverflow} />
158
+ <ValueLabels
159
+ {lastPoints}
160
+ values={styles.values}
161
+ onOverflow={labelMargin.report}
162
+ disabledFor={effectiveDisabledFor}
163
+ onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
164
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
165
+ />
161
166
  {/if}
162
167
  </BasePlotLayout>