@fundar/data-chart-telling 0.0.28 → 0.0.30

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 (49) hide show
  1. package/dist/charts/bar/Chart.svelte +2 -2
  2. package/dist/charts/bar/Chart.svelte.d.ts +2 -2
  3. package/dist/charts/line/Chart.svelte +2 -2
  4. package/dist/charts/line/Chart.svelte.d.ts +2 -2
  5. package/dist/charts/pyramid/Chart.svelte +2 -2
  6. package/dist/charts/pyramid/Chart.svelte.d.ts +3 -2
  7. package/dist/configuration/config.svelte.js +6 -0
  8. package/dist/configuration/themes/index.d.ts +168 -0
  9. package/dist/index.d.ts +4 -3
  10. package/dist/layout/coordinates/CoordinatesLayout.svelte +10 -5
  11. package/dist/layout/coordinates/CoordinatesLayout.svelte.d.ts +2 -0
  12. package/dist/layout/facet/FacetLayout.svelte +77 -20
  13. package/dist/layout/facet/FacetLayout.svelte.d.ts +2 -1
  14. package/dist/layout/plot/BasePlotLayout.svelte +19 -0
  15. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +17 -0
  16. package/dist/plots/bar/Plot.svelte +13 -2
  17. package/dist/plots/bar/Plot.svelte.d.ts +2 -2
  18. package/dist/plots/bar/ValueLabels.svelte +5 -1
  19. package/dist/plots/bar/buildBarMarkers.d.ts +10 -3
  20. package/dist/plots/bar/buildBarMarkers.js +86 -55
  21. package/dist/plots/bar/hoverPoints.js +2 -1
  22. package/dist/plots/bar/layout.svelte.d.ts +2 -0
  23. package/dist/plots/bar/layout.svelte.js +6 -1
  24. package/dist/plots/heatmap/Plot.svelte +2 -2
  25. package/dist/plots/heatmap/Plot.svelte.d.ts +2 -2
  26. package/dist/plots/line/Plot.svelte +39 -15
  27. package/dist/plots/line/Plot.svelte.d.ts +2 -2
  28. package/dist/plots/line/ValueLabels.svelte +17 -2
  29. package/dist/plots/line/buildGapMarkers.d.ts +21 -0
  30. package/dist/plots/line/buildGapMarkers.js +68 -0
  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 +52 -0
  39. package/dist/plots/utils/gaps.js +156 -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 +19 -0
  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 +54 -0
  47. package/dist/types/plots/gaps.js +1 -0
  48. package/dist/types/plots/props.d.ts +1 -1
  49. package/package.json +1 -1
@@ -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[];
@@ -1,23 +1,34 @@
1
1
  import { resolveAccessor } from '../utils/accessors';
2
+ import { getConfiguration } from '../../configuration/config.svelte';
3
+ import { isGapValue, resolveGapAreaStyle } from '../utils/gaps';
4
+ const GAP_KINDS = ['start', 'middle', 'end'];
2
5
  /**
3
6
  * Every `'bar'` marker for one PyramidPlot render — reuses `BarMarkerConfig`
4
7
  * directly, always horizontal (category on y, signed magnitude on x) and
5
8
  * with no insets. Emits no `role: 'hitArea'` markers, since PyramidPlot's
6
- * hover matches nearest points rather than hit-testing category bands.
9
+ * hover matches nearest points rather than hit-testing category bands. A
10
+ * row whose value is missing and *not* filled by a `styles.gaps` zone is
11
+ * skipped entirely; a filled one gets one additional, distinctly-styled
12
+ * marker per zone kind, at its interpolated value.
7
13
  */
8
14
  export function buildPyramidBarMarkers(args) {
9
- const { groupedSeries, valueFor, colorFor, disabledFillFor } = args;
15
+ const { groupedSeries, valueFor, signFor, colorFor, disabledFillFor, gapFillMaps } = args;
16
+ const gapDefaults = getConfiguration().pyramid.gap;
10
17
  const markers = [];
11
18
  for (const group of groupedSeries) {
12
19
  const getCategory = resolveAccessor(group.series.x);
20
+ const getMagnitude = resolveAccessor(group.series.y);
13
21
  const defaultColor = colorFor(group.series);
14
22
  const disabledFill = disabledFillFor(group.series.name);
15
23
  for (const seg of group.visualSegments) {
24
+ const realData = seg.data.filter((d) => !isGapValue(getMagnitude(d)));
25
+ if (realData.length === 0)
26
+ continue;
16
27
  markers.push({
17
28
  type: 'bar',
18
29
  role: 'segment',
19
30
  isHorizontal: true,
20
- data: seg.data,
31
+ data: realData,
21
32
  y: getCategory,
22
33
  x1: 0,
23
34
  x2: (d) => valueFor(group.series, d),
@@ -27,6 +38,35 @@ export function buildPyramidBarMarkers(args) {
27
38
  },
28
39
  });
29
40
  }
41
+ const fillMap = gapFillMaps.get(group.series.name) ?? new Map();
42
+ for (const kind of GAP_KINDS) {
43
+ const filledRows = group.series.data.filter((d) => fillMap.get(d)?.kind === kind);
44
+ if (filledRows.length === 0)
45
+ continue;
46
+ const base = resolveGapAreaStyle(fillMap.get(filledRows[0]).style, defaultColor, gapDefaults);
47
+ // svelteplot shallow-clones each row before invoking a mark's own
48
+ // geometry accessors, so a `fillMap.get(d)` lookup inside `x2` would
49
+ // see a clone, not the original key — tag the resolved value onto the
50
+ // row as a plain field instead, which survives that clone.
51
+ const taggedData = filledRows.map((d) => ({
52
+ ...d,
53
+ __gapValue: signFor(group.series, Math.abs(fillMap.get(d).value)),
54
+ }));
55
+ markers.push({
56
+ type: 'bar',
57
+ role: 'segment',
58
+ isHorizontal: true,
59
+ data: taggedData,
60
+ y: getCategory,
61
+ x1: 0,
62
+ x2: (d) => d.__gapValue,
63
+ style: {
64
+ ...base,
65
+ fill: disabledFill?.fill ?? base.fill,
66
+ fillOpacity: disabledFill?.fillOpacity ?? base.fillOpacity,
67
+ },
68
+ });
69
+ }
30
70
  }
31
71
  return markers;
32
72
  }
@@ -20,7 +20,7 @@
20
20
  import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
21
21
  import type { Series } from '../../types/plots/data/common';
22
22
  import type { ScatterDataProps } from '../../types/plots/data/scatter';
23
- import type { BasePlotStylesConfig } from '../../types/layout/styles';
23
+ import type { AxisBasedStylesConfig } from '../../types/layout/styles';
24
24
  import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
25
25
  import type { AxisBasedMarkersConfig } from '../../types/markers/common';
26
26
  import type { LegendDisabledStyle } from '../../types/layout/legend';
@@ -36,7 +36,7 @@
36
36
  type Props = BasePlotProps<
37
37
  ScatterDataProps<TData>,
38
38
  TData,
39
- BasePlotStylesConfig,
39
+ AxisBasedStylesConfig,
40
40
  ScatterSegmentStyle,
41
41
  AxisBasedMarkersConfig,
42
42
  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 { ScatterDataProps } from '../../types/plots/data/scatter';
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<ScatterDataProps<TData>, TData, BasePlotStylesConfig, import("../..").DotStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
7
+ props: BasePlotProps<ScatterDataProps<TData>, TData, AxisBasedStylesConfig, import("../..").DotStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
8
8
  exports: {};
9
9
  bindings: "";
10
10
  slots: {};
@@ -0,0 +1,52 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { GapZonesConfig, GapZoneConfig } from '../../types/plots/gaps';
3
+ import type { AreaStyle } from '../../types/plots/styling';
4
+ /**
5
+ * Resolves one series' zone config out of `LinePlot`'s full `gaps` prop: a
6
+ * named entry (keyed by the series' own `name`) applies only to that
7
+ * series; `'default'` fills in whichever zone the named entry itself leaves
8
+ * unset. Unlike `segments`' own specific/default resolution (which
9
+ * concatenates two rule lists), this merges at zone granularity — there's
10
+ * no list to concatenate, just three independent slots.
11
+ */
12
+ export declare function resolveGapsForSeries<TStyle>(gaps: Record<string, GapZonesConfig<TStyle>>, seriesName: string): GapZonesConfig<TStyle>;
13
+ /** A point sampled along a gap-fill bridge, already resolved to plot data-space numbers. */
14
+ export type GapPoint = {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ export type GapRun<TStyle> = {
19
+ kind: 'start' | 'middle' | 'end';
20
+ zone: GapZoneConfig<TStyle>;
21
+ points: GapPoint[];
22
+ };
23
+ /** A row counts as a gap when its resolved value is `null`, `undefined`, or `NaN`. */
24
+ export declare function isGapValue(v: unknown): boolean;
25
+ /**
26
+ * Detects a series' missing-data runs (see {@link GapZonesConfig}'s doc
27
+ * comment for what counts as missing) and, for every run whose zone is
28
+ * configured, builds the eased bridge points that visually fill it. Rows
29
+ * are sorted by `x` first, so an out-of-order series is handled the same as
30
+ * a sorted one. A series with no valid (non-missing) rows at all produces
31
+ * no runs — there's nothing to bridge from or to.
32
+ */
33
+ export declare function resolveGapRuns<T extends Record<string, unknown>, TStyle>(series: Series<T>, gaps: GapZonesConfig<TStyle>): GapRun<TStyle>[];
34
+ /** One gap row's resolved fill — the interpolated value to render instead of the missing one, and which zone produced it. */
35
+ export type DiscreteGapFill<TStyle> = {
36
+ value: number;
37
+ kind: 'start' | 'middle' | 'end';
38
+ style?: TStyle;
39
+ };
40
+ /**
41
+ * Bar/pyramid analog of `resolveGapRuns`: a category axis has no continuous
42
+ * bridge to sample, so this returns one interpolated value per *individual*
43
+ * gap row instead. Runs are detected by `series.data`'s own array order
44
+ * (not a numeric `x` sort like `resolveGapRuns` — a category axis is often
45
+ * nominal, so there's no meaningful numeric ordering to sort by). Keyed by
46
+ * row object reference rather than category, since a series' `y` accessor
47
+ * may be an arbitrary function — there's no general way to write the
48
+ * interpolated value back onto a row, so callers look it up here instead.
49
+ */
50
+ export declare function resolveDiscreteGapFills<T extends Record<string, unknown>, TStyle>(series: Series<T>, gaps: GapZonesConfig<TStyle>): Map<T, DiscreteGapFill<TStyle>>;
51
+ /** Field-by-field style fallback for a discrete gap-fill marker — mirrors how `buildGapMarkers.ts` merges a line gap zone's style under `cfg.line.gap`. */
52
+ export declare function resolveGapAreaStyle(zoneStyle: AreaStyle | undefined, defaultColor: string, gapDefaults: AreaStyle): AreaStyle;
@@ -0,0 +1,156 @@
1
+ import { resolveAccessor } from './accessors';
2
+ import { resolveEasing } from '../../utils/interpolate';
3
+ const DEFAULT_KEY = 'default';
4
+ /**
5
+ * Resolves one series' zone config out of `LinePlot`'s full `gaps` prop: a
6
+ * named entry (keyed by the series' own `name`) applies only to that
7
+ * series; `'default'` fills in whichever zone the named entry itself leaves
8
+ * unset. Unlike `segments`' own specific/default resolution (which
9
+ * concatenates two rule lists), this merges at zone granularity — there's
10
+ * no list to concatenate, just three independent slots.
11
+ */
12
+ export function resolveGapsForSeries(gaps, seriesName) {
13
+ const specific = gaps[seriesName];
14
+ const defaults = gaps[DEFAULT_KEY];
15
+ if (!specific)
16
+ return defaults ?? {};
17
+ if (!defaults)
18
+ return specific;
19
+ return {
20
+ start: specific.start ?? defaults.start,
21
+ middle: specific.middle ?? defaults.middle,
22
+ end: specific.end ?? defaults.end,
23
+ };
24
+ }
25
+ const BRIDGE_STEPS = 24;
26
+ /** A row counts as a gap when its resolved value is `null`, `undefined`, or `NaN`. */
27
+ export function isGapValue(v) {
28
+ return v === null || v === undefined || (typeof v === 'number' && Number.isNaN(v));
29
+ }
30
+ /** Samples `BRIDGE_STEPS + 1` points between two endpoints, x moving linearly and y eased by the zone's `interpolate`. */
31
+ function buildBridge(from, to, zone) {
32
+ const ease = resolveEasing(zone.interpolate);
33
+ const points = [];
34
+ for (let i = 0; i <= BRIDGE_STEPS; i++) {
35
+ const t = i / BRIDGE_STEPS;
36
+ points.push({ x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * ease(t) });
37
+ }
38
+ return points;
39
+ }
40
+ /**
41
+ * Detects a series' missing-data runs (see {@link GapZonesConfig}'s doc
42
+ * comment for what counts as missing) and, for every run whose zone is
43
+ * configured, builds the eased bridge points that visually fill it. Rows
44
+ * are sorted by `x` first, so an out-of-order series is handled the same as
45
+ * a sorted one. A series with no valid (non-missing) rows at all produces
46
+ * no runs — there's nothing to bridge from or to.
47
+ */
48
+ export function resolveGapRuns(series, gaps) {
49
+ if (!gaps.start && !gaps.middle && !gaps.end)
50
+ return [];
51
+ const xFn = resolveAccessor(series.x);
52
+ const yFn = resolveAccessor(series.y);
53
+ const sorted = [...series.data].sort((a, b) => Number(xFn(a)) - Number(xFn(b)));
54
+ const pt = (row) => ({ x: Number(xFn(row)), y: Number(yFn(row)) });
55
+ const validIdx = sorted.reduce((acc, row, idx) => {
56
+ if (!isGapValue(yFn(row)))
57
+ acc.push(idx);
58
+ return acc;
59
+ }, []);
60
+ if (validIdx.length === 0)
61
+ return [];
62
+ const runs = [];
63
+ const firstValid = validIdx[0];
64
+ if (firstValid > 0 && gaps.start) {
65
+ const to = pt(sorted[firstValid]);
66
+ const from = { x: Number(gaps.start.x), y: gaps.start.y != null ? Number(gaps.start.y) : to.y };
67
+ runs.push({ kind: 'start', zone: gaps.start, points: buildBridge(from, to, gaps.start) });
68
+ }
69
+ if (gaps.middle) {
70
+ for (let k = 0; k < validIdx.length - 1; k++) {
71
+ const a = validIdx[k];
72
+ const b = validIdx[k + 1];
73
+ if (b - a > 1) {
74
+ runs.push({
75
+ kind: 'middle',
76
+ zone: gaps.middle,
77
+ points: buildBridge(pt(sorted[a]), pt(sorted[b]), gaps.middle),
78
+ });
79
+ }
80
+ }
81
+ }
82
+ const lastValid = validIdx[validIdx.length - 1];
83
+ if (lastValid < sorted.length - 1 && gaps.end) {
84
+ const from = pt(sorted[lastValid]);
85
+ const to = { x: Number(gaps.end.x), y: gaps.end.y != null ? Number(gaps.end.y) : from.y };
86
+ runs.push({ kind: 'end', zone: gaps.end, points: buildBridge(from, to, gaps.end) });
87
+ }
88
+ return runs;
89
+ }
90
+ /**
91
+ * Bar/pyramid analog of `resolveGapRuns`: a category axis has no continuous
92
+ * bridge to sample, so this returns one interpolated value per *individual*
93
+ * gap row instead. Runs are detected by `series.data`'s own array order
94
+ * (not a numeric `x` sort like `resolveGapRuns` — a category axis is often
95
+ * nominal, so there's no meaningful numeric ordering to sort by). Keyed by
96
+ * row object reference rather than category, since a series' `y` accessor
97
+ * may be an arbitrary function — there's no general way to write the
98
+ * interpolated value back onto a row, so callers look it up here instead.
99
+ */
100
+ export function resolveDiscreteGapFills(series, gaps) {
101
+ const result = new Map();
102
+ if (!gaps.start && !gaps.middle && !gaps.end)
103
+ return result;
104
+ const yFn = resolveAccessor(series.y);
105
+ const rows = series.data;
106
+ const valueAt = (i) => Number(yFn(rows[i]));
107
+ const validIdx = rows.reduce((acc, row, i) => {
108
+ if (!isGapValue(yFn(row)))
109
+ acc.push(i);
110
+ return acc;
111
+ }, []);
112
+ if (validIdx.length === 0)
113
+ return result;
114
+ function fillRun(indices, from, to, zone, kind) {
115
+ const ease = resolveEasing(zone.interpolate);
116
+ const span = indices.length + 1;
117
+ indices.forEach((idx, i) => {
118
+ const t = (i + 1) / span;
119
+ result.set(rows[idx], { value: from + (to - from) * ease(t), kind, style: zone.style });
120
+ });
121
+ }
122
+ const firstValid = validIdx[0];
123
+ if (firstValid > 0 && gaps.start) {
124
+ const to = valueAt(firstValid);
125
+ const from = gaps.start.y != null ? Number(gaps.start.y) : to;
126
+ fillRun(Array.from({ length: firstValid }, (_, i) => i), from, to, gaps.start, 'start');
127
+ }
128
+ if (gaps.middle) {
129
+ for (let k = 0; k < validIdx.length - 1; k++) {
130
+ const a = validIdx[k];
131
+ const b = validIdx[k + 1];
132
+ if (b - a > 1) {
133
+ fillRun(Array.from({ length: b - a - 1 }, (_, i) => a + 1 + i), valueAt(a), valueAt(b), gaps.middle, 'middle');
134
+ }
135
+ }
136
+ }
137
+ const lastValid = validIdx[validIdx.length - 1];
138
+ if (lastValid < rows.length - 1 && gaps.end) {
139
+ const from = valueAt(lastValid);
140
+ const to = gaps.end.y != null ? Number(gaps.end.y) : from;
141
+ fillRun(Array.from({ length: rows.length - 1 - lastValid }, (_, i) => lastValid + 1 + i), from, to, gaps.end, 'end');
142
+ }
143
+ return result;
144
+ }
145
+ /** Field-by-field style fallback for a discrete gap-fill marker — mirrors how `buildGapMarkers.ts` merges a line gap zone's style under `cfg.line.gap`. */
146
+ export function resolveGapAreaStyle(zoneStyle, defaultColor, gapDefaults) {
147
+ return {
148
+ fill: zoneStyle?.fill ?? gapDefaults.fill ?? defaultColor,
149
+ fillOpacity: zoneStyle?.fillOpacity ?? gapDefaults.fillOpacity ?? 1,
150
+ stroke: zoneStyle?.stroke ?? gapDefaults.stroke,
151
+ strokeWidth: zoneStyle?.strokeWidth ?? gapDefaults.strokeWidth,
152
+ strokeOpacity: zoneStyle?.strokeOpacity ?? gapDefaults.strokeOpacity,
153
+ strokeDasharray: zoneStyle?.strokeDasharray ?? gapDefaults.strokeDasharray,
154
+ borderRadius: zoneStyle?.borderRadius ?? gapDefaults.borderRadius,
155
+ };
156
+ }
@@ -48,4 +48,13 @@ export declare function createLabelMarginTracker(margins: () => MarginConfig | u
48
48
  bottom: number | "auto";
49
49
  left: number | "auto";
50
50
  }> | undefined;
51
+ readonly plotProps: {
52
+ margins: Partial<{
53
+ top: number | "auto";
54
+ right: number | "auto";
55
+ bottom: number | "auto";
56
+ left: number | "auto";
57
+ }> | undefined;
58
+ marginRemountKey: string;
59
+ };
51
60
  };
@@ -109,8 +109,19 @@ export function createLabelMarginTracker(margins) {
109
109
  out.bottom = measured.bottom;
110
110
  return out;
111
111
  });
112
+ // Bundles `resolvedMargins` with a remount key derived from `measured`,
113
+ // ready to spread straight onto `<BasePlotLayout>` as its `margins`/
114
+ // `marginRemountKey` props (see that prop's doc comment for *why* a
115
+ // remount key is needed here at all) — keeps the caller from re-deriving
116
+ // the same key by hand, and from having to know it's `measured` that key
117
+ // must track.
118
+ const plotProps = $derived.by(() => ({
119
+ margins: resolvedMargins,
120
+ marginRemountKey: JSON.stringify(measured),
121
+ }));
112
122
  return {
113
123
  report,
114
124
  get resolvedMargins() { return resolvedMargins; },
125
+ get plotProps() { return plotProps; },
115
126
  };
116
127
  }
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { AxisValue, AxisBasedScalesConfig } from '../layout/scales';
3
3
  import type { Accessor } from '../plots/data/common';
4
- import type { BasePlotStylesConfig } from '../layout/styles';
4
+ import type { AxisBasedStylesConfig, GapsAwarePlotStylesConfig } from '../layout/styles';
5
5
  import type { SegmentsConfig, MarginConfig } from '../plots/props';
6
6
  import type { AxisBasedMarkersConfig } from '../markers/common';
7
7
  import type { TooltipProp } from '../layout/tooltip';
@@ -30,7 +30,7 @@ export type ChartProps<TRow extends Record<string, unknown>, TSegmentStyle exten
30
30
  x: Accessor<TRow, AxisValue>;
31
31
  y: Accessor<TRow, AxisValue>;
32
32
  z?: Accessor<TRow, unknown>;
33
- styles?: BasePlotStylesConfig;
33
+ styles?: AxisBasedStylesConfig;
34
34
  segments?: SegmentsConfig<TSegmentStyle>;
35
35
  markers?: AxisBasedMarkersConfig[];
36
36
  scales?: AxisBasedScalesConfig<TRow>;
@@ -39,3 +39,12 @@ export type ChartProps<TRow extends Record<string, unknown>, TSegmentStyle exten
39
39
  timeline?: TimelineConfig<TRow>;
40
40
  tooltip?: TooltipProp<TRow>;
41
41
  };
42
+ /**
43
+ * `ChartProps` for the chart kinds with gap-fill (`BarChart`/`LineChart`/
44
+ * `PyramidChart`) — identical shape, but `styles` accepts
45
+ * `GapsAwarePlotStylesConfig<TSegmentStyle>` (adds `styles.gaps`) instead of
46
+ * the plain `AxisBasedStylesConfig` every other chart kind uses.
47
+ */
48
+ export type GapsAwareChartProps<TRow extends Record<string, unknown>, TSegmentStyle extends object = object> = Omit<ChartProps<TRow, TSegmentStyle>, 'styles'> & {
49
+ styles?: GapsAwarePlotStylesConfig<TSegmentStyle>;
50
+ };
@@ -83,6 +83,15 @@ export type ChartConfig = {
83
83
  line: {
84
84
  strokeWidth: number;
85
85
  dotRadius: number;
86
+ /**
87
+ * Default gap-fill bridge styling (`LinePlot`'s `styles.gaps`) — the
88
+ * fallback merged under whatever a gap zone's own `style` sets, the
89
+ * same way `hover.rule`/`hover.dot` back every hover marker's style.
90
+ */
91
+ gap: {
92
+ stroke: StrokeStyle;
93
+ dots: DotStyle;
94
+ };
86
95
  /**
87
96
  * End-of-line value-label declutter geometry and connector styling —
88
97
  * `LinePlot`-specific, since only line charts spread colliding
@@ -101,6 +110,16 @@ export type ChartConfig = {
101
110
  connectorWidth: number;
102
111
  };
103
112
  };
113
+ /** Default bar-mark styling. */
114
+ bar: {
115
+ /** Default gap-fill styling (`BarPlot`'s `styles.gaps`) — the fallback merged under whatever a gap zone's own `style` sets. */
116
+ gap: AreaStyle;
117
+ };
118
+ /** Default pyramid-bar styling. */
119
+ pyramid: {
120
+ /** Default gap-fill styling (`PyramidPlot`'s `styles.gaps`) — the fallback merged under whatever a gap zone's own `style` sets. */
121
+ gap: AreaStyle;
122
+ };
104
123
  /** Default scatter point geometry. */
105
124
  scatter: {
106
125
  dotRadius: number;
@@ -6,6 +6,7 @@ export type Responsive<T> = T | {
6
6
  };
7
7
  export type FacetColumns = Responsive<number>;
8
8
  export type FacetMode = 'grid' | 'carousel' | 'paginated';
9
+ export type FacetNavLayout = 'overlay' | 'below';
9
10
  export type FacetCellContext<TRow extends Record<string, unknown>> = {
10
11
  facetValue: string;
11
12
  data: TRow[];
@@ -35,6 +36,7 @@ export type FacetConfig<TRow extends Record<string, unknown>> = {
35
36
  mode?: Responsive<FacetMode>;
36
37
  visible?: number;
37
38
  pageSize?: number;
39
+ navLayout?: FacetNavLayout;
38
40
  formatIndicator?: (current: number, total: number) => string;
39
41
  indicator?: FacetIndicatorSnippet;
40
42
  navButton?: FacetNavSnippet;