@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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
|
|
14
14
|
import { buildHoverPoints } from './hoverPoints';
|
|
15
15
|
import { buildBarMarkers } from './buildBarMarkers';
|
|
16
|
+
import { resolveGapsForSeries, resolveDiscreteGapFills } from '../utils/gaps';
|
|
16
17
|
import ValueLabels from './ValueLabels.svelte';
|
|
17
18
|
import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
|
|
18
19
|
import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
21
22
|
import type { AxisScale, AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
22
23
|
import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
23
|
-
import type {
|
|
24
|
+
import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
|
|
24
25
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
25
26
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
26
27
|
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
type Props = BasePlotProps<
|
|
34
35
|
SeriesProps<TData> | DataProps<TData>,
|
|
35
36
|
TData,
|
|
36
|
-
|
|
37
|
+
GapsAwarePlotStylesConfig<BarSegmentStyle>,
|
|
37
38
|
BarSegmentStyle,
|
|
38
39
|
AxisBasedMarkersConfig,
|
|
39
40
|
AxisBasedScalesConfig<TData>
|
|
@@ -68,6 +69,14 @@
|
|
|
68
69
|
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
69
70
|
const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
|
|
70
71
|
|
|
72
|
+
// Fills leading/internal/trailing missing-data holes with an
|
|
73
|
+
// interpolated, distinctly-styled bar, resolved per series like
|
|
74
|
+
// `segments` — see `GapsAwarePlotStylesConfig` (`types/layout/styles.ts`).
|
|
75
|
+
const gaps = $derived(styles.gaps ?? {});
|
|
76
|
+
const gapFillMaps = $derived(
|
|
77
|
+
new Map(resolvedSeries.map((s) => [s.name, resolveDiscreteGapFills(s, resolveGapsForSeries(gaps, s.name))])),
|
|
78
|
+
);
|
|
79
|
+
|
|
71
80
|
// `xAcc`/`yAcc` are always data-semantic (category/value) — independent of
|
|
72
81
|
// which *visual* axis ends up drawing them, decided by `categoricalAxis`.
|
|
73
82
|
const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
|
|
@@ -148,6 +157,7 @@
|
|
|
148
157
|
flat: () => flat,
|
|
149
158
|
xAcc: () => xAcc,
|
|
150
159
|
groupedSeries: () => groupedSeries,
|
|
160
|
+
gapFillMaps: () => gapFillMaps,
|
|
151
161
|
});
|
|
152
162
|
|
|
153
163
|
// `x`/`y` here are visual (matched against BasePlotLayout's own getX/getY,
|
|
@@ -228,6 +238,7 @@
|
|
|
228
238
|
active: hover.active,
|
|
229
239
|
setHoverMatch,
|
|
230
240
|
clearHoverMatch,
|
|
241
|
+
gapFillMaps,
|
|
231
242
|
}),
|
|
232
243
|
...markers,
|
|
233
244
|
]}
|
|
@@ -2,9 +2,9 @@ import type { AxisBasedMarkersConfig } from '../../types/markers/common';
|
|
|
2
2
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
3
3
|
import type { AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
4
4
|
import type { SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
5
|
-
import type {
|
|
5
|
+
import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
|
|
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';
|
|
@@ -77,6 +78,9 @@
|
|
|
77
78
|
|
|
78
79
|
const xFn = $derived(resolveAccessor(group.series.x));
|
|
79
80
|
const yFn = $derived(resolveAccessor(group.series.y));
|
|
81
|
+
// A gap row — missing data, whether or not `styles.gaps` fills it with a
|
|
82
|
+
// synthetic bar — never gets a value label, same as LinePlot's `lastPoints`.
|
|
83
|
+
const labelData = $derived(group.series.data.filter((d) => !isGapValue(yFn(d))));
|
|
80
84
|
|
|
81
85
|
const valueTextFn = $derived(
|
|
82
86
|
barLayout.layout === 'stacked'
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
boundary crossing instead (pointerenter/leave fire on every ancestor). -->
|
|
114
118
|
<g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
|
|
115
119
|
<Text
|
|
116
|
-
data={
|
|
120
|
+
data={labelData}
|
|
117
121
|
{...isHorizontal ? { y: xFn, x: valueTextFn } : { x: xFn, y: valueTextFn }}
|
|
118
122
|
text={(d: TData) => formatValue(Number(yFn(d)), group.series.name)}
|
|
119
123
|
dx={font?.dx ?? (isHorizontal ? defaultOffset : groupOffset.dx)}
|
|
@@ -4,11 +4,17 @@ import type { VisualGroup } from '../../types/plots/segments/config';
|
|
|
4
4
|
import type { HoverPoint } from '../../types/layout/tooltip';
|
|
5
5
|
import type { BarMarkerConfig } from '../../types/markers/common';
|
|
6
6
|
import type { BarLayout } from './layout.svelte';
|
|
7
|
+
import type { DiscreteGapFill } from '../utils/gaps';
|
|
7
8
|
/**
|
|
8
9
|
* Every `'bar'` marker for one BarPlot render: one `role: 'segment'` marker
|
|
9
|
-
* per series (stacked) or per visual segment (grouped/overlap),
|
|
10
|
-
* `role: '
|
|
11
|
-
*
|
|
10
|
+
* per series (stacked) or per visual segment (grouped/overlap), one
|
|
11
|
+
* additional `role: 'segment'` marker per series per gap zone kind for any
|
|
12
|
+
* row `styles.gaps` fills (distinctly styled, drawn at its interpolated
|
|
13
|
+
* value — see `resolveDiscreteGapFills`), plus one `role: 'hitArea'` marker
|
|
14
|
+
* per category, so a chart never gets segment markers without their hit
|
|
15
|
+
* areas. A row whose value is missing and *not* filled by a gap zone is
|
|
16
|
+
* skipped entirely — no bar drawn, rather than the broken zero/`NaN`
|
|
17
|
+
* geometry a raw missing value would otherwise produce.
|
|
12
18
|
*
|
|
13
19
|
* `'hitArea'` geometry is left unresolved (just `category` + `data`) —
|
|
14
20
|
* resolving it to pixels needs svelteplot's live scale, only available
|
|
@@ -27,4 +33,5 @@ export declare function buildBarMarkers<TData extends Record<string, unknown>>(a
|
|
|
27
33
|
active: boolean;
|
|
28
34
|
setHoverMatch: (rows: TData[]) => void;
|
|
29
35
|
clearHoverMatch: () => void;
|
|
36
|
+
gapFillMaps: Map<string, Map<TData, DiscreteGapFill<BarSegmentStyle>>>;
|
|
30
37
|
}): BarMarkerConfig[];
|
|
@@ -1,81 +1,112 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
2
|
import { paletteColor } from '../../utils/color';
|
|
3
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
|
+
import { isGapValue, resolveGapAreaStyle } from '../utils/gaps';
|
|
5
|
+
const GAP_KINDS = ['start', 'middle', 'end'];
|
|
3
6
|
/**
|
|
4
7
|
* Every `'bar'` marker for one BarPlot render: one `role: 'segment'` marker
|
|
5
|
-
* per series (stacked) or per visual segment (grouped/overlap),
|
|
6
|
-
* `role: '
|
|
7
|
-
*
|
|
8
|
+
* per series (stacked) or per visual segment (grouped/overlap), one
|
|
9
|
+
* additional `role: 'segment'` marker per series per gap zone kind for any
|
|
10
|
+
* row `styles.gaps` fills (distinctly styled, drawn at its interpolated
|
|
11
|
+
* value — see `resolveDiscreteGapFills`), plus one `role: 'hitArea'` marker
|
|
12
|
+
* per category, so a chart never gets segment markers without their hit
|
|
13
|
+
* areas. A row whose value is missing and *not* filled by a gap zone is
|
|
14
|
+
* skipped entirely — no bar drawn, rather than the broken zero/`NaN`
|
|
15
|
+
* geometry a raw missing value would otherwise produce.
|
|
8
16
|
*
|
|
9
17
|
* `'hitArea'` geometry is left unresolved (just `category` + `data`) —
|
|
10
18
|
* resolving it to pixels needs svelteplot's live scale, only available
|
|
11
19
|
* where the marker actually renders.
|
|
12
20
|
*/
|
|
13
21
|
export function buildBarMarkers(args) {
|
|
14
|
-
const { groupedSeries, barLayout, isHorizontal, hoverPoints, palette, disabledFillFor, active, setHoverMatch, clearHoverMatch } = args;
|
|
22
|
+
const { groupedSeries, barLayout, isHorizontal, hoverPoints, palette, disabledFillFor, active, setHoverMatch, clearHoverMatch, gapFillMaps } = args;
|
|
23
|
+
const gapDefaults = getConfiguration().bar.gap;
|
|
15
24
|
const segmentMarkers = [];
|
|
16
25
|
groupedSeries.forEach((group, seriesIndex) => {
|
|
17
26
|
const xFn = resolveAccessor(group.series.x);
|
|
18
27
|
const yFn = resolveAccessor(group.series.y);
|
|
19
28
|
const defaultColor = paletteColor(seriesIndex, palette);
|
|
20
29
|
const disabledFill = disabledFillFor(group.series.name);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
marker.y2 = stackedTopFn;
|
|
43
|
-
}
|
|
44
|
-
segmentMarkers.push(marker);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
for (const seg of group.visualSegments) {
|
|
48
|
-
const marker = {
|
|
49
|
-
type: 'bar',
|
|
50
|
-
role: 'segment',
|
|
51
|
-
isHorizontal,
|
|
52
|
-
data: seg.data,
|
|
53
|
-
style: {
|
|
54
|
-
fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
|
|
55
|
-
fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
if (isHorizontal) {
|
|
59
|
-
marker.y = xFn;
|
|
60
|
-
marker.x1 = barLayout.valueBaseline;
|
|
61
|
-
marker.x2 = yFn;
|
|
30
|
+
// Places one series' bar(s) — real or gap-fill — using whichever
|
|
31
|
+
// layout mode (stacked baseline, or flat baseline + grouped insets) is
|
|
32
|
+
// active, so a gap-fill bar always lands in exactly the same slot a
|
|
33
|
+
// real bar for that row would.
|
|
34
|
+
function makeMarker(data, valueFn, style) {
|
|
35
|
+
if (data.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const marker = { type: 'bar', role: 'segment', isHorizontal, data, style };
|
|
38
|
+
if (barLayout.layout === 'stacked') {
|
|
39
|
+
const baselineFn = (d) => barLayout.stackedBaseline(xFn(d), seriesIndex);
|
|
40
|
+
const topFn = (d) => baselineFn(d) + valueFn(d);
|
|
41
|
+
if (isHorizontal) {
|
|
42
|
+
marker.y = xFn;
|
|
43
|
+
marker.x1 = baselineFn;
|
|
44
|
+
marker.x2 = topFn;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
marker.x = xFn;
|
|
48
|
+
marker.y1 = baselineFn;
|
|
49
|
+
marker.y2 = topFn;
|
|
50
|
+
}
|
|
62
51
|
}
|
|
63
52
|
else {
|
|
64
|
-
marker.x = xFn;
|
|
65
|
-
marker.y1 = barLayout.valueBaseline;
|
|
66
|
-
marker.y2 = yFn;
|
|
67
|
-
}
|
|
68
|
-
if (barLayout.layout === 'grouped') {
|
|
69
53
|
if (isHorizontal) {
|
|
70
|
-
marker.
|
|
71
|
-
marker.
|
|
54
|
+
marker.y = xFn;
|
|
55
|
+
marker.x1 = barLayout.valueBaseline;
|
|
56
|
+
marker.x2 = valueFn;
|
|
72
57
|
}
|
|
73
58
|
else {
|
|
74
|
-
marker.
|
|
75
|
-
marker.
|
|
59
|
+
marker.x = xFn;
|
|
60
|
+
marker.y1 = barLayout.valueBaseline;
|
|
61
|
+
marker.y2 = valueFn;
|
|
62
|
+
}
|
|
63
|
+
if (barLayout.layout === 'grouped') {
|
|
64
|
+
if (isHorizontal) {
|
|
65
|
+
marker.insetTop = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
|
|
66
|
+
marker.insetBottom = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
marker.insetLeft = seriesIndex * barLayout.seriesStep + barLayout.seriesGap / 2;
|
|
70
|
+
marker.insetRight = (barLayout.seriesCount - 1 - seriesIndex) * barLayout.seriesStep + barLayout.seriesGap / 2;
|
|
71
|
+
}
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
|
-
|
|
74
|
+
return marker;
|
|
75
|
+
}
|
|
76
|
+
// Mirrors the pre-existing behavior for both branches: a series-scoped
|
|
77
|
+
// `segments` catch-all style (e.g. an explicit `fill`) wins over the
|
|
78
|
+
// index-derived `defaultColor`.
|
|
79
|
+
for (const seg of group.visualSegments) {
|
|
80
|
+
const realData = seg.data.filter((d) => !isGapValue(yFn(d)));
|
|
81
|
+
const style = {
|
|
82
|
+
fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
|
|
83
|
+
fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
|
|
84
|
+
};
|
|
85
|
+
const valueFn = barLayout.layout === 'stacked' ? (d) => Number(yFn(d)) : yFn;
|
|
86
|
+
const marker = makeMarker(realData, valueFn, style);
|
|
87
|
+
if (marker)
|
|
88
|
+
segmentMarkers.push(marker);
|
|
89
|
+
}
|
|
90
|
+
const fillMap = gapFillMaps.get(group.series.name) ?? new Map();
|
|
91
|
+
for (const kind of GAP_KINDS) {
|
|
92
|
+
const filledRows = group.series.data.filter((d) => fillMap.get(d)?.kind === kind);
|
|
93
|
+
if (filledRows.length === 0)
|
|
94
|
+
continue;
|
|
95
|
+
const base = resolveGapAreaStyle(fillMap.get(filledRows[0]).style, defaultColor, gapDefaults);
|
|
96
|
+
const gapStyle = {
|
|
97
|
+
...base,
|
|
98
|
+
fill: disabledFill?.fill ?? base.fill,
|
|
99
|
+
fillOpacity: disabledFill?.fillOpacity ?? base.fillOpacity,
|
|
100
|
+
};
|
|
101
|
+
// svelteplot shallow-clones each row before invoking a mark's own
|
|
102
|
+
// geometry accessors (same reason PyramidPlot's `flat` tags rows
|
|
103
|
+
// instead of relying on identity) — a `fillMap.get(d)` lookup inside
|
|
104
|
+
// `valueFn` would see a clone, not the original key. Tagging the
|
|
105
|
+
// resolved value onto the row as a plain field survives that clone.
|
|
106
|
+
const taggedData = filledRows.map((d) => ({ ...d, __gapValue: fillMap.get(d).value }));
|
|
107
|
+
const marker = makeMarker(taggedData, (d) => d.__gapValue, gapStyle);
|
|
108
|
+
if (marker)
|
|
109
|
+
segmentMarkers.push(marker);
|
|
79
110
|
}
|
|
80
111
|
});
|
|
81
112
|
const byCategory = new Map();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
2
|
import { paletteColor } from '../../utils/color';
|
|
3
|
+
import { isGapValue } from '../utils/gaps';
|
|
3
4
|
/**
|
|
4
5
|
* Builds hover-marker candidate points for every row across every series.
|
|
5
6
|
*
|
|
@@ -15,7 +16,7 @@ export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, palette
|
|
|
15
16
|
return resolvedSeries.flatMap((s, idx) => {
|
|
16
17
|
const sx = resolveAccessor(s.x);
|
|
17
18
|
const sy = resolveAccessor(s.y);
|
|
18
|
-
return s.data.map((d) => {
|
|
19
|
+
return s.data.filter((d) => !isGapValue(sy(d))).map((d) => {
|
|
19
20
|
const category = sx(d);
|
|
20
21
|
const rawValue = Number(sy(d));
|
|
21
22
|
const value = geometry.layout === 'stacked' ? geometry.stackedBaseline(category, idx) + rawValue : rawValue;
|
|
@@ -3,6 +3,7 @@ import type { Series } from '../../types/plots/data/common';
|
|
|
3
3
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
4
4
|
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
5
5
|
import type { MarginConfig } from '../../types/plots/props';
|
|
6
|
+
import type { DiscreteGapFill } from '../utils/gaps';
|
|
6
7
|
export type BarSeriesLayout = 'overlap' | 'grouped' | 'stacked';
|
|
7
8
|
/**
|
|
8
9
|
* Reactive geometry for BarPlot's series-layout modes — the single source of
|
|
@@ -20,6 +21,7 @@ export declare function createBarLayout<TData extends Record<string, unknown>>(a
|
|
|
20
21
|
flat: () => TData[];
|
|
21
22
|
xAcc: () => (d: TData) => AxisValue;
|
|
22
23
|
groupedSeries: () => VisualGroup<Series<TData>, BarSegmentStyle>[];
|
|
24
|
+
gapFillMaps: () => Map<string, Map<TData, DiscreteGapFill<BarSegmentStyle>>>;
|
|
23
25
|
}): {
|
|
24
26
|
readonly layout: BarSeriesLayout;
|
|
25
27
|
readonly valueBaseline: number;
|
|
@@ -2,6 +2,7 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
|
2
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
3
3
|
import { resolveAccessor } from '../utils/accessors';
|
|
4
4
|
import { numericMargin } from '../../layout/plot/margins';
|
|
5
|
+
import { isGapValue } from '../utils/gaps';
|
|
5
6
|
/**
|
|
6
7
|
* Reactive geometry for BarPlot's series-layout modes — the single source of
|
|
7
8
|
* truth for "where does series N's bar actually sit," shared by bar-segment
|
|
@@ -98,7 +99,11 @@ export function createBarLayout(args) {
|
|
|
98
99
|
const seriesXFn = resolveAccessor(group.series.x);
|
|
99
100
|
const seriesYFn = resolveAccessor(group.series.y);
|
|
100
101
|
const row = group.series.data.find((d) => seriesXFn(d) === category);
|
|
101
|
-
|
|
102
|
+
if (row) {
|
|
103
|
+
const raw = seriesYFn(row);
|
|
104
|
+
const fill = args.gapFillMaps().get(group.series.name)?.get(row);
|
|
105
|
+
running += fill ? fill.value : isGapValue(raw) ? 0 : Number(raw);
|
|
106
|
+
}
|
|
102
107
|
}
|
|
103
108
|
sums.set(category, prefix);
|
|
104
109
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
14
14
|
import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
15
15
|
import type { SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
16
|
-
import type {
|
|
16
|
+
import type { AxisBasedStylesConfig } from '../../types/layout/styles';
|
|
17
17
|
import type { CellSegmentStyle } from '../../types/plots/segments/common';
|
|
18
18
|
import type { AxisBasedMarkersConfig } from '../../types/markers/common';
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
type Props = BasePlotProps<
|
|
30
30
|
SeriesProps<TData> | DataProps<TData>,
|
|
31
31
|
TData,
|
|
32
|
-
|
|
32
|
+
AxisBasedStylesConfig,
|
|
33
33
|
CellSegmentStyle,
|
|
34
34
|
AxisBasedMarkersConfig,
|
|
35
35
|
AxisBasedScalesConfig<TData>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
2
2
|
import type { AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
3
3
|
import type { SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
4
|
-
import type {
|
|
4
|
+
import type { AxisBasedStylesConfig } from '../../types/layout/styles';
|
|
5
5
|
import type { AxisBasedMarkersConfig } from '../../types/markers/common';
|
|
6
6
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
7
|
-
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData,
|
|
7
|
+
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, AxisBasedStylesConfig, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
|
|
8
8
|
exports: {};
|
|
9
9
|
bindings: "";
|
|
10
10
|
slots: {};
|
|
@@ -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';
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
18
20
|
import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
19
21
|
import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
20
|
-
import type {
|
|
22
|
+
import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
|
|
21
23
|
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
22
24
|
import type { LineMarkersConfig } from '../../types/markers/line';
|
|
23
25
|
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
type Props = BasePlotProps<
|
|
26
28
|
SeriesProps<TData> | DataProps<TData>,
|
|
27
29
|
TData,
|
|
28
|
-
|
|
30
|
+
GapsAwarePlotStylesConfig<LineSegmentStyle>,
|
|
29
31
|
LineSegmentStyle,
|
|
30
32
|
LineMarkersConfig,
|
|
31
33
|
AxisBasedScalesConfig<TData>
|
|
@@ -47,6 +49,11 @@
|
|
|
47
49
|
tooltip = undefined,
|
|
48
50
|
}: Props = $props();
|
|
49
51
|
|
|
52
|
+
// Fills leading/internal/trailing missing-data holes with an
|
|
53
|
+
// interpolated, distinctly-styled bridge, resolved per series like
|
|
54
|
+
// `segments` — see `GapsAwarePlotStylesConfig` (`types/layout/styles.ts`).
|
|
55
|
+
const gaps = $derived(styles.gaps ?? {});
|
|
56
|
+
|
|
50
57
|
const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
51
58
|
const legendInteraction = $derived(getLegendInteraction());
|
|
52
59
|
|
|
@@ -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,
|
|
@@ -148,9 +172,9 @@
|
|
|
148
172
|
getY={(d) => Number(yAcc(d))}
|
|
149
173
|
{hover}
|
|
150
174
|
{scales}
|
|
151
|
-
|
|
175
|
+
{...labelMargin.plotProps}
|
|
152
176
|
xTickRotate={scales?.x?.tickRotate}
|
|
153
|
-
markers={[...lineMarkers, ...markers]}
|
|
177
|
+
markers={[...gapMarkers, ...lineMarkers, ...markers]}
|
|
154
178
|
seriesData={resolvedSeries}
|
|
155
179
|
{scopedGroups}
|
|
156
180
|
>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
2
2
|
import type { AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
3
3
|
import type { SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
4
|
-
import type {
|
|
4
|
+
import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
|
|
5
5
|
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
6
6
|
import type { LineMarkersConfig } from '../../types/markers/line';
|
|
7
7
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
8
|
-
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData,
|
|
8
|
+
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, GapsAwarePlotStylesConfig<LineSegmentStyle>, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>>;
|
|
9
9
|
exports: {};
|
|
10
10
|
bindings: "";
|
|
11
11
|
slots: {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { SvelteMap } from 'svelte/reactivity';
|
|
3
4
|
import { Text, usePlot } from 'svelteplot';
|
|
4
5
|
import { resolveAccessor } from '../utils/accessors';
|
|
@@ -97,8 +98,22 @@
|
|
|
97
98
|
// changes the rendered label layout so this re-measures.
|
|
98
99
|
void lastPoints;
|
|
99
100
|
void values;
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
// The scale range itself is read `untrack`ed on purpose: this margin
|
|
102
|
+
// watcher's own report (see labelOverflow.svelte.ts) grows the plot's
|
|
103
|
+
// margin to fit the labels, which feeds back into svelteplot's own
|
|
104
|
+
// auto-margin sizing and nudges this same range — occasionally by a
|
|
105
|
+
// permanent, never-settling ±1px each render. Treating the range as a
|
|
106
|
+
// tracked dependency here reran the whole watcher every single time it
|
|
107
|
+
// twitched, which (when it never settles) never let the watcher's own
|
|
108
|
+
// "poll until stable" loop finish — an infinite effect-restart loop
|
|
109
|
+
// that tripped Svelte's effect_update_depth_exceeded guard. Reading it
|
|
110
|
+
// untracked still measures against the *current* range on every
|
|
111
|
+
// re-run triggered by `lastPoints`/`values` above; it just stops the
|
|
112
|
+
// range's own fluctuations from being what triggers those re-runs.
|
|
113
|
+
const { xRange, yRange } = untrack(() => ({
|
|
114
|
+
xRange: plot.scales.x?.fn?.range?.()?.map(Number),
|
|
115
|
+
yRange: plot.scales.y?.fn?.range?.()?.map(Number),
|
|
116
|
+
}));
|
|
102
117
|
if (!xRange || !yRange) return undefined;
|
|
103
118
|
return {
|
|
104
119
|
left: Math.min(xRange[0], xRange[1]),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Series } from '../../types/plots/data/common';
|
|
2
|
+
import type { GapsConfig } from '../../types/plots/gaps';
|
|
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
|
+
}
|