@fundar/data-chart-telling 0.0.27 → 0.0.29

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.
@@ -73,6 +73,10 @@ export const DEFAULT_CONFIG = {
73
73
  line: {
74
74
  strokeWidth: 3,
75
75
  dotRadius: 4,
76
+ gap: {
77
+ stroke: { strokeOpacity: 0.6, strokeDasharray: '4 4' },
78
+ dots: {},
79
+ },
76
80
  valueLabels: {
77
81
  elbowGap: 10,
78
82
  laneStep: 7,
@@ -84,6 +84,26 @@ export declare const THEMES: {
84
84
  line?: {
85
85
  strokeWidth?: number | undefined;
86
86
  dotRadius?: number | undefined;
87
+ gap?: {
88
+ stroke?: {
89
+ stroke?: string | undefined;
90
+ strokeWidth?: number | undefined;
91
+ strokeOpacity?: number | undefined;
92
+ strokeDasharray?: (string | import("../..").LineStyle) | undefined;
93
+ strokeLinecap?: import("../..").LineCap | undefined;
94
+ strokeLinejoin?: import("../..").LineJoin | undefined;
95
+ } | undefined;
96
+ dots?: {
97
+ dotRadius?: number | undefined;
98
+ dotFill?: string | undefined;
99
+ dotFillOpacity?: number | undefined;
100
+ dotSymbol?: import("../..").SymbolType | undefined;
101
+ dotStroke?: string | undefined;
102
+ dotStrokeWidth?: number | undefined;
103
+ dotStrokeOpacity?: number | undefined;
104
+ dotStrokeDasharray?: (string | import("../..").LineStyle) | undefined;
105
+ } | undefined;
106
+ } | undefined;
87
107
  valueLabels?: {
88
108
  elbowGap?: number | undefined;
89
109
  laneStep?: number | undefined;
@@ -242,6 +262,26 @@ export declare const THEMES: {
242
262
  line?: {
243
263
  strokeWidth?: number | undefined;
244
264
  dotRadius?: number | undefined;
265
+ gap?: {
266
+ stroke?: {
267
+ stroke?: string | undefined;
268
+ strokeWidth?: number | undefined;
269
+ strokeOpacity?: number | undefined;
270
+ strokeDasharray?: (string | import("../..").LineStyle) | undefined;
271
+ strokeLinecap?: import("../..").LineCap | undefined;
272
+ strokeLinejoin?: import("../..").LineJoin | undefined;
273
+ } | undefined;
274
+ dots?: {
275
+ dotRadius?: number | undefined;
276
+ dotFill?: string | undefined;
277
+ dotFillOpacity?: number | undefined;
278
+ dotSymbol?: import("../..").SymbolType | undefined;
279
+ dotStroke?: string | undefined;
280
+ dotStrokeWidth?: number | undefined;
281
+ dotStrokeOpacity?: number | undefined;
282
+ dotStrokeDasharray?: (string | import("../..").LineStyle) | undefined;
283
+ } | undefined;
284
+ } | undefined;
245
285
  valueLabels?: {
246
286
  elbowGap?: number | undefined;
247
287
  laneStep?: number | undefined;
@@ -400,6 +440,26 @@ export declare const THEMES: {
400
440
  line?: {
401
441
  strokeWidth?: number | undefined;
402
442
  dotRadius?: number | undefined;
443
+ gap?: {
444
+ stroke?: {
445
+ stroke?: string | undefined;
446
+ strokeWidth?: number | undefined;
447
+ strokeOpacity?: number | undefined;
448
+ strokeDasharray?: (string | import("../..").LineStyle) | undefined;
449
+ strokeLinecap?: import("../..").LineCap | undefined;
450
+ strokeLinejoin?: import("../..").LineJoin | undefined;
451
+ } | undefined;
452
+ dots?: {
453
+ dotRadius?: number | undefined;
454
+ dotFill?: string | undefined;
455
+ dotFillOpacity?: number | undefined;
456
+ dotSymbol?: import("../..").SymbolType | undefined;
457
+ dotStroke?: string | undefined;
458
+ dotStrokeWidth?: number | undefined;
459
+ dotStrokeOpacity?: number | undefined;
460
+ dotStrokeDasharray?: (string | import("../..").LineStyle) | undefined;
461
+ } | undefined;
462
+ } | undefined;
403
463
  valueLabels?: {
404
464
  elbowGap?: number | undefined;
405
465
  laneStep?: number | undefined;
@@ -558,6 +618,26 @@ export declare const THEMES: {
558
618
  line?: {
559
619
  strokeWidth?: number | undefined;
560
620
  dotRadius?: number | undefined;
621
+ gap?: {
622
+ stroke?: {
623
+ stroke?: string | undefined;
624
+ strokeWidth?: number | undefined;
625
+ strokeOpacity?: number | undefined;
626
+ strokeDasharray?: (string | import("../..").LineStyle) | undefined;
627
+ strokeLinecap?: import("../..").LineCap | undefined;
628
+ strokeLinejoin?: import("../..").LineJoin | undefined;
629
+ } | undefined;
630
+ dots?: {
631
+ dotRadius?: number | undefined;
632
+ dotFill?: string | undefined;
633
+ dotFillOpacity?: number | undefined;
634
+ dotSymbol?: import("../..").SymbolType | undefined;
635
+ dotStroke?: string | undefined;
636
+ dotStrokeWidth?: number | undefined;
637
+ dotStrokeOpacity?: number | undefined;
638
+ dotStrokeDasharray?: (string | import("../..").LineStyle) | undefined;
639
+ } | undefined;
640
+ } | undefined;
561
641
  valueLabels?: {
562
642
  elbowGap?: number | undefined;
563
643
  laneStep?: number | undefined;
@@ -41,7 +41,9 @@
41
41
  children: Snippet;
42
42
  } = $props();
43
43
 
44
- const resolvedProjection = $derived(resolveProjection(coordinates?.projection, data));
44
+ const resolvedProjection = $derived(
45
+ coordinates ? resolveProjection(coordinates.projection, data) : undefined,
46
+ );
45
47
 
46
48
  const zoomEnabled = $derived(coordinates?.zoom !== false && coordinates?.zoom != null);
47
49
  const zoomConfig = $derived(typeof coordinates?.zoom === 'object' ? coordinates.zoom : undefined);
@@ -21,27 +21,35 @@ export function buildBarMarkers(args) {
21
21
  if (barLayout.layout === 'stacked') {
22
22
  const stackedBaselineFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex);
23
23
  const stackedTopFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex) + Number(yFn(d));
24
- const marker = {
25
- type: 'bar',
26
- role: 'segment',
27
- isHorizontal,
28
- data: group.series.data,
29
- style: {
30
- fill: disabledFill?.fill ?? defaultColor,
31
- fillOpacity: disabledFill?.fillOpacity ?? 1,
32
- },
33
- };
34
- if (isHorizontal) {
35
- marker.y = xFn;
36
- marker.x1 = stackedBaselineFn;
37
- marker.x2 = stackedTopFn;
38
- }
39
- else {
40
- marker.x = xFn;
41
- marker.y1 = stackedBaselineFn;
42
- marker.y2 = stackedTopFn;
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
+ };
41
+ if (isHorizontal) {
42
+ marker.y = xFn;
43
+ marker.x1 = stackedBaselineFn;
44
+ marker.x2 = stackedTopFn;
45
+ }
46
+ else {
47
+ marker.x = xFn;
48
+ marker.y1 = stackedBaselineFn;
49
+ marker.y2 = stackedTopFn;
50
+ }
51
+ segmentMarkers.push(marker);
43
52
  }
44
- segmentMarkers.push(marker);
45
53
  return;
46
54
  }
47
55
  for (const seg of group.visualSegments) {
@@ -9,6 +9,8 @@
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 { buildGapMarkers } from './buildGapMarkers';
13
+ import { isGapValue } from '../utils/gaps';
12
14
  import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
13
15
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
14
16
  import ValueLabels from './ValueLabels.svelte';
@@ -20,6 +22,7 @@
20
22
  import type { BasePlotStylesConfig } from '../../types/layout/styles';
21
23
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
22
24
  import type { LineMarkersConfig } from '../../types/markers/line';
25
+ import type { GapsConfig } from '../../types/plots/props';
23
26
  import type { LegendDisabledStyle } from '../../types/layout/legend';
24
27
 
25
28
  type Props = BasePlotProps<
@@ -29,7 +32,10 @@
29
32
  LineSegmentStyle,
30
33
  LineMarkersConfig,
31
34
  AxisBasedScalesConfig<TData>
32
- >;
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
+ };
33
39
 
34
40
  let {
35
41
  width,
@@ -42,6 +48,7 @@
42
48
  styles = {},
43
49
  segments = {},
44
50
  markers = [],
51
+ gaps = {},
45
52
  scales = {},
46
53
  margins,
47
54
  tooltip = undefined,
@@ -80,27 +87,36 @@
80
87
  const flat = $derived(resolvedSeries.flatMap((s) => s.data));
81
88
  // A legend-disabled series is dimmed on the mark itself, but never surfaces
82
89
  // in hover/tooltip — its points are simply absent from the candidate list.
90
+ // A gap row (see `gaps`) is excluded the same way — it carries no real
91
+ // value to hover.
83
92
  const hoverPoints = $derived(
84
93
  resolvedSeries.flatMap((s, idx) => {
85
94
  if (legendInteraction?.disabledSeries.has(s.name)) return [];
86
95
  const sx = resolveAccessor(s.x);
87
96
  const sy = resolveAccessor(s.y);
88
- return s.data.map((d) => ({
89
- x: sx(d) as AxisValue,
90
- y: Number(sy(d)),
91
- row: d,
92
- label: String(sy(d)),
93
- series: s.name,
94
- color: paletteColor(idx, cfg.palette),
95
- }));
97
+ return s.data
98
+ .filter((d) => !isGapValue(sy(d)))
99
+ .map((d) => ({
100
+ x: sx(d) as AxisValue,
101
+ y: Number(sy(d)),
102
+ row: d,
103
+ label: String(sy(d)),
104
+ series: s.name,
105
+ color: paletteColor(idx, cfg.palette),
106
+ }));
96
107
  }),
97
108
  );
98
109
 
99
- // Last data point per series, with its palette index for colour.
110
+ // Last *real* data point per series (skipping any trailing gap row), with
111
+ // its palette index for colour.
100
112
  const lastPoints = $derived(
101
113
  resolvedSeries
102
- .map((s, idx) => ({ series: s, lastRow: s.data[s.data.length - 1], seriesIndex: idx }))
103
- .filter(({ lastRow }) => lastRow !== undefined),
114
+ .map((s, idx) => {
115
+ const sy = resolveAccessor(s.y);
116
+ const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
117
+ return { series: s, lastRow, seriesIndex: idx };
118
+ })
119
+ .filter((p): p is { series: Series<TData>; lastRow: TData; seriesIndex: number } => p.lastRow !== undefined),
104
120
  );
105
121
 
106
122
  const hover = $derived({
@@ -120,6 +136,14 @@
120
136
  }),
121
137
  );
122
138
 
139
+ const gapMarkers = $derived(
140
+ buildGapMarkers<TData>({
141
+ seriesList: resolvedSeries,
142
+ gaps,
143
+ colorFor: (seriesIndex) => paletteColor(seriesIndex, cfg.palette),
144
+ }),
145
+ );
146
+
123
147
  const scopedGroups = $derived(
124
148
  buildScopedMarkerGroups<TData, Series<TData>, LineSegmentStyle>({
125
149
  groupedSeries,
@@ -150,7 +174,7 @@
150
174
  {scales}
151
175
  margins={labelMargin.resolvedMargins}
152
176
  xTickRotate={scales?.x?.tickRotate}
153
- markers={[...lineMarkers, ...markers]}
177
+ markers={[...gapMarkers, ...lineMarkers, ...markers]}
154
178
  seriesData={resolvedSeries}
155
179
  {scopedGroups}
156
180
  >
@@ -4,8 +4,12 @@ import type { SeriesProps, DataProps } from '../../types/plots/data/common';
4
4
  import type { BasePlotStylesConfig } 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';
7
8
  declare function $$render<TData extends Record<string, unknown>>(): {
8
- props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStylesConfig, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>>;
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
+ };
9
13
  exports: {};
10
14
  bindings: "";
11
15
  slots: {};
@@ -0,0 +1,21 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { GapsConfig } from '../../types/plots/props';
3
+ import type { LineSegmentStyle } from '../../types/plots/segments/common';
4
+ import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/common';
5
+ /**
6
+ * One `'line'` (+ `'dot'`, when its zone sets `style.dots`) marker per
7
+ * detected, configured gap run across every series — the interpolated
8
+ * bridge drawn over a missing-data hole. `gaps` is resolved per series the
9
+ * same way `segments` is (a named entry, falling back to `'default'` at
10
+ * zone granularity — see `resolveGapsForSeries`). Defaults to the series'
11
+ * own colour with `cfg.line.gap`'s stroke/dot styling (dashed and slightly
12
+ * muted out of the box), so a gap-fill reads as estimated rather than real
13
+ * data unless a zone or the global configuration overrides it. Dots (when
14
+ * requested) are drawn only at a run's two endpoints, not along every
15
+ * sampled bridge point.
16
+ */
17
+ export declare function buildGapMarkers<TData extends Record<string, unknown>>(args: {
18
+ seriesList: Series<TData>[];
19
+ gaps: GapsConfig<LineSegmentStyle>;
20
+ colorFor: (seriesIndex: number) => string;
21
+ }): (LineMarkerConfig | DotMarkerConfig)[];
@@ -0,0 +1,68 @@
1
+ import { resolveGapRuns, resolveGapsForSeries } from '../utils/gaps';
2
+ import { getConfiguration } from '../../configuration/config.svelte';
3
+ const gx = (p) => p.x;
4
+ const gy = (p) => p.y;
5
+ /**
6
+ * One `'line'` (+ `'dot'`, when its zone sets `style.dots`) marker per
7
+ * detected, configured gap run across every series — the interpolated
8
+ * bridge drawn over a missing-data hole. `gaps` is resolved per series the
9
+ * same way `segments` is (a named entry, falling back to `'default'` at
10
+ * zone granularity — see `resolveGapsForSeries`). Defaults to the series'
11
+ * own colour with `cfg.line.gap`'s stroke/dot styling (dashed and slightly
12
+ * muted out of the box), so a gap-fill reads as estimated rather than real
13
+ * data unless a zone or the global configuration overrides it. Dots (when
14
+ * requested) are drawn only at a run's two endpoints, not along every
15
+ * sampled bridge point.
16
+ */
17
+ export function buildGapMarkers(args) {
18
+ const { seriesList, gaps, colorFor } = args;
19
+ const cfg = getConfiguration();
20
+ const gapDefaults = cfg.line.gap;
21
+ const markers = [];
22
+ seriesList.forEach((series, seriesIndex) => {
23
+ const zones = resolveGapsForSeries(gaps, series.name);
24
+ const runs = resolveGapRuns(series, zones);
25
+ if (runs.length === 0)
26
+ return;
27
+ const defaultColor = colorFor(seriesIndex);
28
+ for (const run of runs) {
29
+ const strokeStyle = run.zone.style?.stroke;
30
+ const dotStyle = run.zone.style?.dots;
31
+ markers.push({
32
+ type: 'line',
33
+ data: run.points,
34
+ x: gx,
35
+ y: gy,
36
+ style: {
37
+ stroke: strokeStyle?.stroke ?? defaultColor,
38
+ strokeWidth: strokeStyle?.strokeWidth ?? gapDefaults.stroke.strokeWidth ?? cfg.line.strokeWidth,
39
+ strokeOpacity: strokeStyle?.strokeOpacity ?? gapDefaults.stroke.strokeOpacity,
40
+ strokeDasharray: strokeStyle?.strokeDasharray ?? gapDefaults.stroke.strokeDasharray,
41
+ strokeLinejoin: strokeStyle?.strokeLinejoin ?? gapDefaults.stroke.strokeLinejoin,
42
+ strokeLinecap: strokeStyle?.strokeLinecap ?? gapDefaults.stroke.strokeLinecap,
43
+ },
44
+ });
45
+ if (!dotStyle)
46
+ continue;
47
+ const dotDefaults = gapDefaults.dots;
48
+ const endpoints = [run.points[0], run.points[run.points.length - 1]];
49
+ markers.push({
50
+ type: 'dot',
51
+ data: endpoints,
52
+ x: gx,
53
+ y: gy,
54
+ style: {
55
+ dotRadius: dotStyle.dotRadius ?? dotDefaults.dotRadius ?? cfg.line.dotRadius,
56
+ dotFill: dotStyle.dotFill ?? strokeStyle?.stroke ?? dotDefaults.dotFill ?? defaultColor,
57
+ dotSymbol: dotStyle.dotSymbol ?? dotDefaults.dotSymbol ?? 'circle',
58
+ dotStroke: dotStyle.dotStroke ?? strokeStyle?.stroke ?? dotDefaults.dotStroke ?? defaultColor,
59
+ dotStrokeWidth: dotStyle.dotStrokeWidth ?? dotDefaults.dotStrokeWidth ?? 1,
60
+ dotFillOpacity: dotStyle.dotFillOpacity ?? dotDefaults.dotFillOpacity ?? 1,
61
+ dotStrokeOpacity: dotStyle.dotStrokeOpacity ?? dotDefaults.dotStrokeOpacity ?? 1,
62
+ dotStrokeDasharray: dotStyle.dotStrokeDasharray ?? dotDefaults.dotStrokeDasharray,
63
+ },
64
+ });
65
+ }
66
+ });
67
+ return markers;
68
+ }
@@ -0,0 +1,32 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { GapZonesConfig, GapZoneConfig } from '../../types/plots/gaps';
3
+ /**
4
+ * Resolves one series' zone config out of `LinePlot`'s full `gaps` prop: a
5
+ * named entry (keyed by the series' own `name`) applies only to that
6
+ * series; `'default'` fills in whichever zone the named entry itself leaves
7
+ * unset. Unlike `segments`' own specific/default resolution (which
8
+ * concatenates two rule lists), this merges at zone granularity — there's
9
+ * no list to concatenate, just three independent slots.
10
+ */
11
+ export declare function resolveGapsForSeries<TStyle>(gaps: Record<string, GapZonesConfig<TStyle>>, seriesName: string): GapZonesConfig<TStyle>;
12
+ /** A point sampled along a gap-fill bridge, already resolved to plot data-space numbers. */
13
+ export type GapPoint = {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ export type GapRun<TStyle> = {
18
+ kind: 'start' | 'middle' | 'end';
19
+ zone: GapZoneConfig<TStyle>;
20
+ points: GapPoint[];
21
+ };
22
+ /** A row counts as a gap when its resolved value is `null`, `undefined`, or `NaN`. */
23
+ export declare function isGapValue(v: unknown): boolean;
24
+ /**
25
+ * Detects a series' missing-data runs (see {@link GapZonesConfig}'s doc
26
+ * comment for what counts as missing) and, for every run whose zone is
27
+ * configured, builds the eased bridge points that visually fill it. Rows
28
+ * are sorted by `x` first, so an out-of-order series is handled the same as
29
+ * a sorted one. A series with no valid (non-missing) rows at all produces
30
+ * no runs — there's nothing to bridge from or to.
31
+ */
32
+ export declare function resolveGapRuns<T extends Record<string, unknown>, TStyle>(series: Series<T>, gaps: GapZonesConfig<TStyle>): GapRun<TStyle>[];
@@ -0,0 +1,89 @@
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
+ }
@@ -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 `gaps` prop) — 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
@@ -0,0 +1,39 @@
1
+ import type { EasingName } from '../charts/interpolate';
2
+ import type { AxisValue } from '../layout/scales';
3
+ /**
4
+ * How one gap zone bridges its hole: `interpolate` eases the synthetic
5
+ * segment's value between its two endpoints (defaults to `'linear'`, a
6
+ * straight line); `style` is the per-mark shape drawn for it
7
+ * ({@link LineSegmentStyle} for `LinePlot`), independent of that plot
8
+ * kind's own default marker style unless the caller repeats it.
9
+ */
10
+ export type GapZoneConfig<TStyle> = {
11
+ interpolate?: EasingName;
12
+ style?: TStyle;
13
+ };
14
+ /**
15
+ * A leading/trailing gap has only one real boundary — `x` (required to opt
16
+ * in) is the domain value the synthetic segment extends to, and `y` is the
17
+ * value it reaches there (defaults to holding the nearest real point's
18
+ * value, i.e. a flat extrapolation). Omitting the zone entirely leaves that
19
+ * end of the series unfilled — extrapolating without an explicit target
20
+ * would imply data that doesn't exist.
21
+ */
22
+ export type OpenGapZoneConfig<TStyle> = GapZoneConfig<TStyle> & {
23
+ x: AxisValue;
24
+ y?: AxisValue;
25
+ };
26
+ /**
27
+ * One series' gap-fill config: `start` (before its first real point),
28
+ * `middle` (any internal run of missing rows, bounded by real data on both
29
+ * sides) and `end` (after its last real point). A row counts as missing
30
+ * when its resolved value is `null`, `undefined`, or `NaN` — the same
31
+ * convention svelteplot's own `Line` mark already uses to break the line
32
+ * there. See {@link GapsConfig} (`types/plots/props.ts`) for how this is
33
+ * keyed per series on `LinePlot`'s own `gaps` prop.
34
+ */
35
+ export type GapZonesConfig<TStyle> = {
36
+ start?: OpenGapZoneConfig<TStyle>;
37
+ middle?: GapZoneConfig<TStyle>;
38
+ end?: OpenGapZoneConfig<TStyle>;
39
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { HoverPoint, HoverStrategy, TooltipOptions } from '../layout/tooltip';
2
2
  import type { Segment } from './segments/config';
3
+ import type { GapZonesConfig } from './gaps';
3
4
  import type { AxisBasedScalesConfig } from '../layout/scales';
4
5
  import type { GeoScalesConfig } from '../layout/coordinates';
5
6
  /** The `styles` prop accepted by every plot kind — its own
@@ -14,6 +15,14 @@ export type MarkersConfig<TMarker> = TMarker[];
14
15
  * area-specific segments merge on top and win on conflict.
15
16
  */
16
17
  export type SegmentsConfig<TStyle> = Record<string, Segment<TStyle>[]>;
18
+ /**
19
+ * The `gaps` prop `LinePlot` accepts — series name (or `'default'` for all
20
+ * series) to that series' {@link GapZonesConfig}. Mirrors `SegmentsConfig`'s
21
+ * series-keyed shape: a named entry applies only to that series; `'default'`
22
+ * fills in whichever of its zones (`start`/`middle`/`end`) the named entry
23
+ * itself leaves unset, at zone granularity rather than a merged style.
24
+ */
25
+ export type GapsConfig<TStyle> = Record<string, GapZonesConfig<TStyle>>;
17
26
  /**
18
27
  * Partial margin override — unset sides fall back to the theme (itself
19
28
  * `'auto'` by default). `'auto'` fits the side to its rendered content; a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"