@fundar/data-chart-telling 0.0.29 → 0.0.31

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 (48) hide show
  1. package/dist/charts/BaseChart.svelte +1 -0
  2. package/dist/charts/bar/Chart.svelte +2 -2
  3. package/dist/charts/bar/Chart.svelte.d.ts +2 -2
  4. package/dist/charts/line/Chart.svelte +2 -2
  5. package/dist/charts/line/Chart.svelte.d.ts +2 -2
  6. package/dist/charts/pyramid/Chart.svelte +2 -2
  7. package/dist/charts/pyramid/Chart.svelte.d.ts +3 -2
  8. package/dist/configuration/config.svelte.js +2 -0
  9. package/dist/configuration/themes/index.d.ts +88 -0
  10. package/dist/index.d.ts +4 -3
  11. package/dist/layout/coordinates/CoordinatesLayout.svelte +10 -5
  12. package/dist/layout/coordinates/CoordinatesLayout.svelte.d.ts +2 -0
  13. package/dist/layout/facet/FacetLayout.svelte +77 -20
  14. package/dist/layout/facet/FacetLayout.svelte.d.ts +2 -1
  15. package/dist/layout/plot/BasePlotLayout.svelte +19 -0
  16. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +17 -0
  17. package/dist/plots/bar/Plot.svelte +13 -2
  18. package/dist/plots/bar/Plot.svelte.d.ts +2 -2
  19. package/dist/plots/bar/ValueLabels.svelte +5 -1
  20. package/dist/plots/bar/buildBarMarkers.d.ts +10 -3
  21. package/dist/plots/bar/buildBarMarkers.js +80 -57
  22. package/dist/plots/bar/hoverPoints.js +2 -1
  23. package/dist/plots/bar/layout.svelte.d.ts +2 -0
  24. package/dist/plots/bar/layout.svelte.js +6 -1
  25. package/dist/plots/heatmap/Plot.svelte +2 -2
  26. package/dist/plots/heatmap/Plot.svelte.d.ts +2 -2
  27. package/dist/plots/line/Plot.svelte +9 -9
  28. package/dist/plots/line/Plot.svelte.d.ts +2 -6
  29. package/dist/plots/line/ValueLabels.svelte +17 -2
  30. package/dist/plots/line/buildGapMarkers.d.ts +1 -1
  31. package/dist/plots/pyramid/Plot.svelte +39 -14
  32. package/dist/plots/pyramid/Plot.svelte.d.ts +2 -2
  33. package/dist/plots/pyramid/ValueLabels.svelte +5 -1
  34. package/dist/plots/pyramid/buildPyramidBarMarkers.d.ts +8 -1
  35. package/dist/plots/pyramid/buildPyramidBarMarkers.js +43 -3
  36. package/dist/plots/scatter/Plot.svelte +2 -2
  37. package/dist/plots/scatter/Plot.svelte.d.ts +2 -2
  38. package/dist/plots/utils/gaps.d.ts +20 -0
  39. package/dist/plots/utils/gaps.js +67 -0
  40. package/dist/plots/utils/labelOverflow.svelte.d.ts +9 -0
  41. package/dist/plots/utils/labelOverflow.svelte.js +11 -0
  42. package/dist/types/charts/props.d.ts +11 -2
  43. package/dist/types/configuration/styling.d.ts +11 -1
  44. package/dist/types/layout/facet.d.ts +2 -0
  45. package/dist/types/layout/styles.d.ts +29 -11
  46. package/dist/types/plots/gaps.d.ts +17 -2
  47. package/dist/types/plots/props.d.ts +1 -10
  48. package/package.json +1 -1
@@ -4,11 +4,17 @@ import type { VisualGroup } from '../../types/plots/segments/config';
4
4
  import type { HoverPoint } from '../../types/layout/tooltip';
5
5
  import type { BarMarkerConfig } from '../../types/markers/common';
6
6
  import type { BarLayout } from './layout.svelte';
7
+ import type { DiscreteGapFill } from '../utils/gaps';
7
8
  /**
8
9
  * Every `'bar'` marker for one BarPlot render: one `role: 'segment'` marker
9
- * per series (stacked) or per visual segment (grouped/overlap), plus one
10
- * `role: 'hitArea'` marker per category, so a chart never gets segment
11
- * markers without their hit areas.
10
+ * per series (stacked) or per visual segment (grouped/overlap), one
11
+ * additional `role: 'segment'` marker per series per gap zone kind for any
12
+ * row `styles.gaps` fills (distinctly styled, drawn at its interpolated
13
+ * value — see `resolveDiscreteGapFills`), plus one `role: 'hitArea'` marker
14
+ * per category, so a chart never gets segment markers without their hit
15
+ * areas. A row whose value is missing and *not* filled by a gap zone is
16
+ * skipped entirely — no bar drawn, rather than the broken zero/`NaN`
17
+ * geometry a raw missing value would otherwise produce.
12
18
  *
13
19
  * `'hitArea'` geometry is left unresolved (just `category` + `data`) —
14
20
  * resolving it to pixels needs svelteplot's live scale, only available
@@ -27,4 +33,5 @@ export declare function buildBarMarkers<TData extends Record<string, unknown>>(a
27
33
  active: boolean;
28
34
  setHoverMatch: (rows: TData[]) => void;
29
35
  clearHoverMatch: () => void;
36
+ gapFillMaps: Map<string, Map<TData, DiscreteGapFill<BarSegmentStyle>>>;
30
37
  }): BarMarkerConfig[];
@@ -1,89 +1,112 @@
1
1
  import { resolveAccessor } from '../utils/accessors';
2
2
  import { paletteColor } from '../../utils/color';
3
+ import { getConfiguration } from '../../configuration/config.svelte';
4
+ import { isGapValue, resolveGapAreaStyle } from '../utils/gaps';
5
+ const GAP_KINDS = ['start', 'middle', 'end'];
3
6
  /**
4
7
  * Every `'bar'` marker for one BarPlot render: one `role: 'segment'` marker
5
- * per series (stacked) or per visual segment (grouped/overlap), plus one
6
- * `role: 'hitArea'` marker per category, so a chart never gets segment
7
- * markers without their hit areas.
8
+ * per series (stacked) or per visual segment (grouped/overlap), one
9
+ * additional `role: 'segment'` marker per series per gap zone kind for any
10
+ * row `styles.gaps` fills (distinctly styled, drawn at its interpolated
11
+ * value — see `resolveDiscreteGapFills`), plus one `role: 'hitArea'` marker
12
+ * per category, so a chart never gets segment markers without their hit
13
+ * areas. A row whose value is missing and *not* filled by a gap zone is
14
+ * skipped entirely — no bar drawn, rather than the broken zero/`NaN`
15
+ * geometry a raw missing value would otherwise produce.
8
16
  *
9
17
  * `'hitArea'` geometry is left unresolved (just `category` + `data`) —
10
18
  * resolving it to pixels needs svelteplot's live scale, only available
11
19
  * where the marker actually renders.
12
20
  */
13
21
  export function buildBarMarkers(args) {
14
- const { groupedSeries, barLayout, isHorizontal, hoverPoints, palette, disabledFillFor, active, setHoverMatch, clearHoverMatch } = args;
22
+ const { groupedSeries, barLayout, isHorizontal, hoverPoints, palette, disabledFillFor, active, setHoverMatch, clearHoverMatch, gapFillMaps } = args;
23
+ const gapDefaults = getConfiguration().bar.gap;
15
24
  const segmentMarkers = [];
16
25
  groupedSeries.forEach((group, seriesIndex) => {
17
26
  const xFn = resolveAccessor(group.series.x);
18
27
  const yFn = resolveAccessor(group.series.y);
19
28
  const defaultColor = paletteColor(seriesIndex, palette);
20
29
  const disabledFill = disabledFillFor(group.series.name);
21
- if (barLayout.layout === 'stacked') {
22
- const stackedBaselineFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex);
23
- const stackedTopFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex) + Number(yFn(d));
24
- // Mirrors the grouped/overlap branch below: a series-scoped `segments`
25
- // catch-all style (e.g. an explicit `fill`) must win over the
26
- // index-derived `defaultColor`, same as it already does there. Stacked
27
- // bars were previously drawn straight from `group.series.data`, which
28
- // skipped `group.visualSegments` entirely and silently dropped any such
29
- // override.
30
- for (const seg of group.visualSegments) {
31
- const marker = {
32
- type: 'bar',
33
- role: 'segment',
34
- isHorizontal,
35
- data: seg.data,
36
- style: {
37
- fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
38
- fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
39
- },
40
- };
30
+ // Places one series' bar(s) — real or gap-fill — using whichever
31
+ // layout mode (stacked baseline, or flat baseline + grouped insets) is
32
+ // active, so a gap-fill bar always lands in exactly the same slot a
33
+ // real bar for that row would.
34
+ function makeMarker(data, valueFn, style) {
35
+ if (data.length === 0)
36
+ return null;
37
+ const marker = { type: 'bar', role: 'segment', isHorizontal, data, style };
38
+ if (barLayout.layout === 'stacked') {
39
+ const baselineFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex);
40
+ const topFn = (d) => baselineFn(d) + valueFn(d);
41
41
  if (isHorizontal) {
42
42
  marker.y = xFn;
43
- marker.x1 = stackedBaselineFn;
44
- marker.x2 = stackedTopFn;
43
+ marker.x1 = baselineFn;
44
+ marker.x2 = topFn;
45
45
  }
46
46
  else {
47
47
  marker.x = xFn;
48
- marker.y1 = stackedBaselineFn;
49
- marker.y2 = stackedTopFn;
48
+ marker.y1 = baselineFn;
49
+ marker.y2 = topFn;
50
50
  }
51
- segmentMarkers.push(marker);
52
- }
53
- return;
54
- }
55
- for (const seg of group.visualSegments) {
56
- const marker = {
57
- type: 'bar',
58
- role: 'segment',
59
- isHorizontal,
60
- data: seg.data,
61
- style: {
62
- fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
63
- fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
64
- },
65
- };
66
- if (isHorizontal) {
67
- marker.y = xFn;
68
- marker.x1 = barLayout.valueBaseline;
69
- marker.x2 = yFn;
70
51
  }
71
52
  else {
72
- marker.x = xFn;
73
- marker.y1 = barLayout.valueBaseline;
74
- marker.y2 = yFn;
75
- }
76
- if (barLayout.layout === 'grouped') {
77
53
  if (isHorizontal) {
78
- marker.insetTop = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
79
- marker.insetBottom = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
54
+ marker.y = xFn;
55
+ marker.x1 = barLayout.valueBaseline;
56
+ marker.x2 = valueFn;
80
57
  }
81
58
  else {
82
- marker.insetLeft = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
83
- marker.insetRight = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
59
+ marker.x = xFn;
60
+ marker.y1 = barLayout.valueBaseline;
61
+ marker.y2 = valueFn;
62
+ }
63
+ if (barLayout.layout === 'grouped') {
64
+ if (isHorizontal) {
65
+ marker.insetTop = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
66
+ marker.insetBottom = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
67
+ }
68
+ else {
69
+ marker.insetLeft = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
70
+ marker.insetRight = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
71
+ }
84
72
  }
85
73
  }
86
- segmentMarkers.push(marker);
74
+ return marker;
75
+ }
76
+ // Mirrors the pre-existing behavior for both branches: a series-scoped
77
+ // `segments` catch-all style (e.g. an explicit `fill`) wins over the
78
+ // index-derived `defaultColor`.
79
+ for (const seg of group.visualSegments) {
80
+ const realData = seg.data.filter((d) => !isGapValue(yFn(d)));
81
+ const style = {
82
+ fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
83
+ fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
84
+ };
85
+ const valueFn = barLayout.layout === 'stacked' ? (d) => Number(yFn(d)) : yFn;
86
+ const marker = makeMarker(realData, valueFn, style);
87
+ if (marker)
88
+ segmentMarkers.push(marker);
89
+ }
90
+ const fillMap = gapFillMaps.get(group.series.name) ?? new Map();
91
+ for (const kind of GAP_KINDS) {
92
+ const filledRows = group.series.data.filter((d) => fillMap.get(d)?.kind === kind);
93
+ if (filledRows.length === 0)
94
+ continue;
95
+ const base = resolveGapAreaStyle(fillMap.get(filledRows[0]).style, defaultColor, gapDefaults);
96
+ const gapStyle = {
97
+ ...base,
98
+ fill: disabledFill?.fill ?? base.fill,
99
+ fillOpacity: disabledFill?.fillOpacity ?? base.fillOpacity,
100
+ };
101
+ // svelteplot shallow-clones each row before invoking a mark's own
102
+ // geometry accessors (same reason PyramidPlot's `flat` tags rows
103
+ // instead of relying on identity) — a `fillMap.get(d)` lookup inside
104
+ // `valueFn` would see a clone, not the original key. Tagging the
105
+ // resolved value onto the row as a plain field survives that clone.
106
+ const taggedData = filledRows.map((d) => ({ ...d, __gapValue: fillMap.get(d).value }));
107
+ const marker = makeMarker(taggedData, (d) => d.__gapValue, gapStyle);
108
+ if (marker)
109
+ segmentMarkers.push(marker);
87
110
  }
88
111
  });
89
112
  const byCategory = new Map();
@@ -1,5 +1,6 @@
1
1
  import { resolveAccessor } from '../utils/accessors';
2
2
  import { paletteColor } from '../../utils/color';
3
+ import { isGapValue } from '../utils/gaps';
3
4
  /**
4
5
  * Builds hover-marker candidate points for every row across every series.
5
6
  *
@@ -15,7 +16,7 @@ export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, palette
15
16
  return resolvedSeries.flatMap((s, idx) => {
16
17
  const sx = resolveAccessor(s.x);
17
18
  const sy = resolveAccessor(s.y);
18
- return s.data.map((d) => {
19
+ return s.data.filter((d) => !isGapValue(sy(d))).map((d) => {
19
20
  const category = sx(d);
20
21
  const rawValue = Number(sy(d));
21
22
  const value = geometry.layout === 'stacked' ? geometry.stackedBaseline(category, idx) + rawValue : rawValue;
@@ -3,6 +3,7 @@ import type { Series } from '../../types/plots/data/common';
3
3
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
4
4
  import type { VisualGroup } from '../../types/plots/segments/config';
5
5
  import type { MarginConfig } from '../../types/plots/props';
6
+ import type { DiscreteGapFill } from '../utils/gaps';
6
7
  export type BarSeriesLayout = 'overlap' | 'grouped' | 'stacked';
7
8
  /**
8
9
  * Reactive geometry for BarPlot's series-layout modes — the single source of
@@ -20,6 +21,7 @@ export declare function createBarLayout<TData extends Record<string, unknown>>(a
20
21
  flat: () => TData[];
21
22
  xAcc: () => (d: TData) => AxisValue;
22
23
  groupedSeries: () => VisualGroup<Series<TData>, BarSegmentStyle>[];
24
+ gapFillMaps: () => Map<string, Map<TData, DiscreteGapFill<BarSegmentStyle>>>;
23
25
  }): {
24
26
  readonly layout: BarSeriesLayout;
25
27
  readonly valueBaseline: number;
@@ -2,6 +2,7 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
2
2
  import { getConfiguration } from '../../configuration/config.svelte';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
4
  import { numericMargin } from '../../layout/plot/margins';
5
+ import { isGapValue } from '../utils/gaps';
5
6
  /**
6
7
  * Reactive geometry for BarPlot's series-layout modes — the single source of
7
8
  * truth for "where does series N's bar actually sit," shared by bar-segment
@@ -98,7 +99,11 @@ export function createBarLayout(args) {
98
99
  const seriesXFn = resolveAccessor(group.series.x);
99
100
  const seriesYFn = resolveAccessor(group.series.y);
100
101
  const row = group.series.data.find((d) => seriesXFn(d) === category);
101
- running += row ? Number(seriesYFn(row)) : 0;
102
+ if (row) {
103
+ const raw = seriesYFn(row);
104
+ const fill = args.gapFillMaps().get(group.series.name)?.get(row);
105
+ running += fill ? fill.value : isGapValue(raw) ? 0 : Number(raw);
106
+ }
102
107
  }
103
108
  sums.set(category, prefix);
104
109
  }
@@ -13,7 +13,7 @@
13
13
  import type { BasePlotProps } from '../../types/plots/props';
14
14
  import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
15
15
  import type { SeriesProps, DataProps } from '../../types/plots/data/common';
16
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
16
+ import type { AxisBasedStylesConfig } from '../../types/layout/styles';
17
17
  import type { CellSegmentStyle } from '../../types/plots/segments/common';
18
18
  import type { AxisBasedMarkersConfig } from '../../types/markers/common';
19
19
 
@@ -29,7 +29,7 @@
29
29
  type Props = BasePlotProps<
30
30
  SeriesProps<TData> | DataProps<TData>,
31
31
  TData,
32
- BasePlotStylesConfig,
32
+ AxisBasedStylesConfig,
33
33
  CellSegmentStyle,
34
34
  AxisBasedMarkersConfig,
35
35
  AxisBasedScalesConfig<TData>
@@ -1,10 +1,10 @@
1
1
  import type { BasePlotProps } from '../../types/plots/props';
2
2
  import type { AxisBasedScalesConfig } from '../../types/layout/scales';
3
3
  import type { SeriesProps, DataProps } from '../../types/plots/data/common';
4
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
4
+ import type { AxisBasedStylesConfig } from '../../types/layout/styles';
5
5
  import type { AxisBasedMarkersConfig } from '../../types/markers/common';
6
6
  declare function $$render<TData extends Record<string, unknown>>(): {
7
- props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStylesConfig, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
7
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, AxisBasedStylesConfig, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
8
8
  exports: {};
9
9
  bindings: "";
10
10
  slots: {};
@@ -19,23 +19,19 @@
19
19
  import type { BasePlotProps } from '../../types/plots/props';
20
20
  import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
21
21
  import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
22
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
22
+ import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
23
23
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
24
24
  import type { LineMarkersConfig } from '../../types/markers/line';
25
- import type { GapsConfig } from '../../types/plots/props';
26
25
  import type { LegendDisabledStyle } from '../../types/layout/legend';
27
26
 
28
27
  type Props = BasePlotProps<
29
28
  SeriesProps<TData> | DataProps<TData>,
30
29
  TData,
31
- BasePlotStylesConfig,
30
+ GapsAwarePlotStylesConfig<LineSegmentStyle>,
32
31
  LineSegmentStyle,
33
32
  LineMarkersConfig,
34
33
  AxisBasedScalesConfig<TData>
35
- > & {
36
- /** Fills leading/internal/trailing missing-data holes with an interpolated, distinctly-styled bridge, resolved per series like `segments` — see {@link GapsConfig}. */
37
- gaps?: GapsConfig<LineSegmentStyle>;
38
- };
34
+ >;
39
35
 
40
36
  let {
41
37
  width,
@@ -48,12 +44,16 @@
48
44
  styles = {},
49
45
  segments = {},
50
46
  markers = [],
51
- gaps = {},
52
47
  scales = {},
53
48
  margins,
54
49
  tooltip = undefined,
55
50
  }: Props = $props();
56
51
 
52
+ // Fills leading/internal/trailing missing-data holes with an
53
+ // interpolated, distinctly-styled bridge, resolved per series like
54
+ // `segments` — see `GapsAwarePlotStylesConfig` (`types/layout/styles.ts`).
55
+ const gaps = $derived(styles.gaps ?? {});
56
+
57
57
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
58
58
  const legendInteraction = $derived(getLegendInteraction());
59
59
 
@@ -172,7 +172,7 @@
172
172
  getY={(d) => Number(yAcc(d))}
173
173
  {hover}
174
174
  {scales}
175
- margins={labelMargin.resolvedMargins}
175
+ {...labelMargin.plotProps}
176
176
  xTickRotate={scales?.x?.tickRotate}
177
177
  markers={[...gapMarkers, ...lineMarkers, ...markers]}
178
178
  seriesData={resolvedSeries}
@@ -1,15 +1,11 @@
1
1
  import type { BasePlotProps } from '../../types/plots/props';
2
2
  import type { AxisBasedScalesConfig } from '../../types/layout/scales';
3
3
  import type { SeriesProps, DataProps } from '../../types/plots/data/common';
4
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
4
+ import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
5
5
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
6
6
  import type { LineMarkersConfig } from '../../types/markers/line';
7
- import type { GapsConfig } from '../../types/plots/props';
8
7
  declare function $$render<TData extends Record<string, unknown>>(): {
9
- props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStylesConfig, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>> & {
10
- /** Fills leading/internal/trailing missing-data holes with an interpolated, distinctly-styled bridge, resolved per series like `segments` — see {@link GapsConfig}. */
11
- gaps?: GapsConfig<LineSegmentStyle>;
12
- };
8
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, GapsAwarePlotStylesConfig<LineSegmentStyle>, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>>;
13
9
  exports: {};
14
10
  bindings: "";
15
11
  slots: {};
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ import { untrack } from 'svelte';
2
3
  import { SvelteMap } from 'svelte/reactivity';
3
4
  import { Text, usePlot } from 'svelteplot';
4
5
  import { resolveAccessor } from '../utils/accessors';
@@ -97,8 +98,22 @@
97
98
  // changes the rendered label layout so this re-measures.
98
99
  void lastPoints;
99
100
  void values;
100
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
101
- const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
101
+ // The scale range itself is read `untrack`ed on purpose: this margin
102
+ // watcher's own report (see labelOverflow.svelte.ts) grows the plot's
103
+ // margin to fit the labels, which feeds back into svelteplot's own
104
+ // auto-margin sizing and nudges this same range — occasionally by a
105
+ // permanent, never-settling ±1px each render. Treating the range as a
106
+ // tracked dependency here reran the whole watcher every single time it
107
+ // twitched, which (when it never settles) never let the watcher's own
108
+ // "poll until stable" loop finish — an infinite effect-restart loop
109
+ // that tripped Svelte's effect_update_depth_exceeded guard. Reading it
110
+ // untracked still measures against the *current* range on every
111
+ // re-run triggered by `lastPoints`/`values` above; it just stops the
112
+ // range's own fluctuations from being what triggers those re-runs.
113
+ const { xRange, yRange } = untrack(() => ({
114
+ xRange: plot.scales.x?.fn?.range?.()?.map(Number),
115
+ yRange: plot.scales.y?.fn?.range?.()?.map(Number),
116
+ }));
102
117
  if (!xRange || !yRange) return undefined;
103
118
  return {
104
119
  left: Math.min(xRange[0], xRange[1]),
@@ -1,5 +1,5 @@
1
1
  import type { Series } from '../../types/plots/data/common';
2
- import type { GapsConfig } from '../../types/plots/props';
2
+ import type { GapsConfig } from '../../types/plots/gaps';
3
3
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
4
4
  import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/common';
5
5
  /**
@@ -8,13 +8,14 @@
8
8
  import { buildPyramidBarMarkers } from './buildPyramidBarMarkers';
9
9
  import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
10
10
  import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
11
+ import { isGapValue, resolveGapsForSeries, resolveDiscreteGapFills } from '../utils/gaps';
11
12
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
12
13
  import ValueLabels from './ValueLabels.svelte';
13
14
  import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
14
15
  import type { BasePlotProps } from '../../types/plots/props';
15
16
  import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
16
17
  import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
17
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
18
+ import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
18
19
  import type { ValueAnchor } from '../../types/plots/constants';
19
20
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
20
21
  import type { AxisBasedMarkersConfig } from '../../types/markers/common';
@@ -39,7 +40,7 @@
39
40
  type Props = BasePlotProps<
40
41
  SeriesProps<TData> | DataProps<TData>,
41
42
  TData,
42
- BasePlotStylesConfig,
43
+ GapsAwarePlotStylesConfig<BarSegmentStyle>,
43
44
  BarSegmentStyle,
44
45
  AxisBasedMarkersConfig,
45
46
  AxisBasedScalesConfig<TData>
@@ -112,6 +113,20 @@
112
113
  return s.side === 'left' ? -v : v;
113
114
  }
114
115
 
116
+ // Signs an already-resolved magnitude (e.g. a gap-fill's interpolated
117
+ // value, which has no row of its own to read via `signedValue`).
118
+ function signFor(s: InternalSeries, magnitude: number): number {
119
+ return s.side === 'left' ? -magnitude : magnitude;
120
+ }
121
+
122
+ // Fills leading/internal/trailing missing-data holes with an
123
+ // interpolated, distinctly-styled bar, resolved per series like
124
+ // `segments` — see `GapsAwarePlotStylesConfig` (`types/layout/styles.ts`).
125
+ const gaps = $derived(styles.gaps ?? {});
126
+ const gapFillMaps = $derived(
127
+ new Map(resolvedSeries.map((s) => [s.name, resolveDiscreteGapFills(s, resolveGapsForSeries(gaps, s.name))])),
128
+ );
129
+
115
130
  // svelteplot's `Pointer` rebuilds its quadtree from shallow-cloned rows
116
131
  // (`{ ...d }`), never the original references, so a row's owning series
117
132
  // can't be recovered by identity once hover is active. Pre-compute
@@ -129,17 +144,19 @@
129
144
  resolvedSeries.flatMap((s) => {
130
145
  const getCategory = resolveAccessor(s.x);
131
146
  const getMagnitude = resolveAccessor(s.y);
132
- return s.data.map((d) => {
133
- const v = Math.abs(Number(getMagnitude(d)));
134
- return {
135
- ...d,
136
- __side: s.side,
137
- __x: s.side === 'left' ? -v : v,
138
- __y: getCategory(d) as AxisValue,
139
- __label: formatValue(v, s.name),
140
- __series: s.name,
141
- } as TaggedRow;
142
- });
147
+ return s.data
148
+ .filter((d) => !isGapValue(getMagnitude(d)))
149
+ .map((d) => {
150
+ const v = Math.abs(Number(getMagnitude(d)));
151
+ return {
152
+ ...d,
153
+ __side: s.side,
154
+ __x: s.side === 'left' ? -v : v,
155
+ __y: getCategory(d) as AxisValue,
156
+ __label: formatValue(v, s.name),
157
+ __series: s.name,
158
+ } as TaggedRow;
159
+ });
143
160
  }),
144
161
  );
145
162
 
@@ -174,7 +191,13 @@
174
191
  const maxMagnitude = $derived.by(() => {
175
192
  const vals = resolvedSeries.flatMap((s) => {
176
193
  const getMagnitude = resolveAccessor(s.y);
177
- return s.data.map((d) => Math.abs(Number(getMagnitude(d))));
194
+ const fillMap = gapFillMaps.get(s.name);
195
+ return s.data.flatMap((d) => {
196
+ const raw = getMagnitude(d);
197
+ if (!isGapValue(raw)) return [Math.abs(Number(raw))];
198
+ const fill = fillMap?.get(d);
199
+ return fill ? [Math.abs(fill.value)] : [];
200
+ });
178
201
  });
179
202
  return vals.length ? Math.max(...vals) : 1;
180
203
  });
@@ -207,8 +230,10 @@
207
230
  buildPyramidBarMarkers({
208
231
  groupedSeries,
209
232
  valueFor: signedValue,
233
+ signFor,
210
234
  colorFor: (s) => (s.side === 'left' ? leftColor : rightColor),
211
235
  disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
236
+ gapFillMaps,
212
237
  }),
213
238
  );
214
239
 
@@ -1,10 +1,10 @@
1
1
  import type { BasePlotProps } from '../../types/plots/props';
2
2
  import type { AxisBasedScalesConfig } from '../../types/layout/scales';
3
3
  import type { SeriesProps, DataProps } from '../../types/plots/data/common';
4
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
4
+ import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
5
5
  import type { AxisBasedMarkersConfig } from '../../types/markers/common';
6
6
  declare function $$render<TData extends Record<string, unknown>>(): {
7
- props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStylesConfig, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
7
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, GapsAwarePlotStylesConfig<import("../..").AreaStyle>, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
8
8
  exports: {};
9
9
  bindings: "";
10
10
  slots: {};
@@ -3,6 +3,7 @@
3
3
  import { resolveAccessor } from '../utils/accessors';
4
4
  import { disabledFillOverride } from '../utils/legendDisabled';
5
5
  import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
6
+ import { isGapValue } from '../utils/gaps';
6
7
  import type { Series } from '../../types/plots/data/common';
7
8
  import type { ValuesStyle } from '../../types/layout/styles';
8
9
  import type { ValueAnchor } from '../../types/plots/constants';
@@ -70,6 +71,9 @@
70
71
  const isLeft = $derived(group.series.side === 'left');
71
72
  const getCategory = $derived(resolveAccessor(group.series.x));
72
73
  const getMagnitude = $derived(resolveAccessor(group.series.y));
74
+ // A gap row — missing data, whether or not `styles.gaps` fills it with a
75
+ // synthetic bar — never gets a value label, same as LinePlot's `lastPoints`.
76
+ const labelData = $derived(group.series.data.filter((d) => !isGapValue(getMagnitude(d))));
73
77
  const signedValue = $derived((d: TData) => {
74
78
  const v = Math.abs(Number(getMagnitude(d)));
75
79
  return isLeft ? -v : v;
@@ -102,7 +106,7 @@
102
106
  boundary crossing instead (pointerenter/leave fire on every ancestor). -->
103
107
  <g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
104
108
  <Text
105
- data={group.series.data}
109
+ data={labelData}
106
110
  x={xFn}
107
111
  y={getCategory}
108
112
  text={(d: TData) => formatValue(Math.abs(Number(getMagnitude(d))), group.series.name)}
@@ -2,18 +2,25 @@ import type { Series } from '../../types/plots/data/common';
2
2
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
3
3
  import type { VisualGroup } from '../../types/plots/segments/config';
4
4
  import type { BarMarkerConfig } from '../../types/markers/common';
5
+ import type { DiscreteGapFill } from '../utils/gaps';
5
6
  /**
6
7
  * Every `'bar'` marker for one PyramidPlot render — reuses `BarMarkerConfig`
7
8
  * directly, always horizontal (category on y, signed magnitude on x) and
8
9
  * with no insets. Emits no `role: 'hitArea'` markers, since PyramidPlot's
9
- * hover matches nearest points rather than hit-testing category bands.
10
+ * hover matches nearest points rather than hit-testing category bands. A
11
+ * row whose value is missing and *not* filled by a `styles.gaps` zone is
12
+ * skipped entirely; a filled one gets one additional, distinctly-styled
13
+ * marker per zone kind, at its interpolated value.
10
14
  */
11
15
  export declare function buildPyramidBarMarkers<TData extends Record<string, unknown>, S extends Series<TData>>(args: {
12
16
  groupedSeries: VisualGroup<S, BarSegmentStyle>[];
13
17
  valueFor: (series: S, d: TData) => number;
18
+ /** Signs an already-resolved magnitude (e.g. a gap-fill's interpolated value, which has no row of its own to read via `valueFor`). */
19
+ signFor: (series: S, magnitude: number) => number;
14
20
  colorFor: (series: S) => string;
15
21
  disabledFillFor: (seriesName: string) => {
16
22
  fill?: string;
17
23
  fillOpacity?: number;
18
24
  } | undefined;
25
+ gapFillMaps: Map<string, Map<TData, DiscreteGapFill<BarSegmentStyle>>>;
19
26
  }): BarMarkerConfig[];