@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.
- package/dist/charts/bar/Chart.svelte +2 -2
- package/dist/charts/bar/Chart.svelte.d.ts +2 -2
- package/dist/charts/line/Chart.svelte +2 -2
- package/dist/charts/line/Chart.svelte.d.ts +2 -2
- package/dist/charts/pyramid/Chart.svelte +2 -2
- package/dist/charts/pyramid/Chart.svelte.d.ts +3 -2
- package/dist/configuration/config.svelte.js +6 -0
- package/dist/configuration/themes/index.d.ts +168 -0
- package/dist/index.d.ts +4 -3
- package/dist/layout/coordinates/CoordinatesLayout.svelte +10 -5
- package/dist/layout/coordinates/CoordinatesLayout.svelte.d.ts +2 -0
- package/dist/layout/facet/FacetLayout.svelte +77 -20
- package/dist/layout/facet/FacetLayout.svelte.d.ts +2 -1
- package/dist/layout/plot/BasePlotLayout.svelte +19 -0
- package/dist/layout/plot/BasePlotLayout.svelte.d.ts +17 -0
- package/dist/plots/bar/Plot.svelte +13 -2
- package/dist/plots/bar/Plot.svelte.d.ts +2 -2
- package/dist/plots/bar/ValueLabels.svelte +5 -1
- package/dist/plots/bar/buildBarMarkers.d.ts +10 -3
- package/dist/plots/bar/buildBarMarkers.js +86 -55
- package/dist/plots/bar/hoverPoints.js +2 -1
- package/dist/plots/bar/layout.svelte.d.ts +2 -0
- package/dist/plots/bar/layout.svelte.js +6 -1
- package/dist/plots/heatmap/Plot.svelte +2 -2
- package/dist/plots/heatmap/Plot.svelte.d.ts +2 -2
- package/dist/plots/line/Plot.svelte +39 -15
- package/dist/plots/line/Plot.svelte.d.ts +2 -2
- package/dist/plots/line/ValueLabels.svelte +17 -2
- package/dist/plots/line/buildGapMarkers.d.ts +21 -0
- package/dist/plots/line/buildGapMarkers.js +68 -0
- package/dist/plots/pyramid/Plot.svelte +39 -14
- package/dist/plots/pyramid/Plot.svelte.d.ts +2 -2
- package/dist/plots/pyramid/ValueLabels.svelte +5 -1
- package/dist/plots/pyramid/buildPyramidBarMarkers.d.ts +8 -1
- package/dist/plots/pyramid/buildPyramidBarMarkers.js +43 -3
- package/dist/plots/scatter/Plot.svelte +2 -2
- package/dist/plots/scatter/Plot.svelte.d.ts +2 -2
- package/dist/plots/utils/gaps.d.ts +52 -0
- package/dist/plots/utils/gaps.js +156 -0
- package/dist/plots/utils/labelOverflow.svelte.d.ts +9 -0
- package/dist/plots/utils/labelOverflow.svelte.js +11 -0
- package/dist/types/charts/props.d.ts +11 -2
- package/dist/types/configuration/styling.d.ts +19 -0
- package/dist/types/layout/facet.d.ts +2 -0
- package/dist/types/layout/styles.d.ts +29 -11
- package/dist/types/plots/gaps.d.ts +54 -0
- package/dist/types/plots/gaps.js +1 -0
- package/dist/types/plots/props.d.ts +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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={
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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 {
|
|
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?:
|
|
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;
|