@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.
- package/dist/configuration/config.svelte.js +4 -0
- package/dist/configuration/themes/index.d.ts +80 -0
- package/dist/layout/coordinates/CoordinatesLayout.svelte +3 -1
- package/dist/plots/bar/buildBarMarkers.js +28 -20
- package/dist/plots/line/Plot.svelte +37 -13
- package/dist/plots/line/Plot.svelte.d.ts +5 -1
- package/dist/plots/line/buildGapMarkers.d.ts +21 -0
- package/dist/plots/line/buildGapMarkers.js +68 -0
- package/dist/plots/utils/gaps.d.ts +32 -0
- package/dist/plots/utils/gaps.js +89 -0
- package/dist/types/configuration/styling.d.ts +9 -0
- package/dist/types/plots/gaps.d.ts +39 -0
- package/dist/types/plots/gaps.js +1 -0
- package/dist/types/plots/props.d.ts +9 -0
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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) =>
|
|
103
|
-
|
|
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
|