@fundar/data-chart-telling 0.0.29 → 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 +2 -0
- package/dist/configuration/themes/index.d.ts +88 -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 +80 -57
- 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 +9 -9
- package/dist/plots/line/Plot.svelte.d.ts +2 -6
- package/dist/plots/line/ValueLabels.svelte +17 -2
- package/dist/plots/line/buildGapMarkers.d.ts +1 -1
- 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 +20 -0
- package/dist/plots/utils/gaps.js +67 -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 +11 -1
- 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 +17 -2
- package/dist/types/plots/props.d.ts +1 -10
- package/package.json +1 -1
|
@@ -1,89 +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
|
-
const
|
|
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
|
-
};
|
|
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
41
|
if (isHorizontal) {
|
|
42
42
|
marker.y = xFn;
|
|
43
|
-
marker.x1 =
|
|
44
|
-
marker.x2 =
|
|
43
|
+
marker.x1 = baselineFn;
|
|
44
|
+
marker.x2 = topFn;
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
47
|
marker.x = xFn;
|
|
48
|
-
marker.y1 =
|
|
49
|
-
marker.y2 =
|
|
48
|
+
marker.y1 = baselineFn;
|
|
49
|
+
marker.y2 = topFn;
|
|
50
50
|
}
|
|
51
|
-
segmentMarkers.push(marker);
|
|
52
|
-
}
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
for (const seg of group.visualSegments) {
|
|
56
|
-
const marker = {
|
|
57
|
-
type: 'bar',
|
|
58
|
-
role: 'segment',
|
|
59
|
-
isHorizontal,
|
|
60
|
-
data: seg.data,
|
|
61
|
-
style: {
|
|
62
|
-
fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
|
|
63
|
-
fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
if (isHorizontal) {
|
|
67
|
-
marker.y = xFn;
|
|
68
|
-
marker.x1 = barLayout.valueBaseline;
|
|
69
|
-
marker.x2 = yFn;
|
|
70
51
|
}
|
|
71
52
|
else {
|
|
72
|
-
marker.x = xFn;
|
|
73
|
-
marker.y1 = barLayout.valueBaseline;
|
|
74
|
-
marker.y2 = yFn;
|
|
75
|
-
}
|
|
76
|
-
if (barLayout.layout === 'grouped') {
|
|
77
53
|
if (isHorizontal) {
|
|
78
|
-
marker.
|
|
79
|
-
marker.
|
|
54
|
+
marker.y = xFn;
|
|
55
|
+
marker.x1 = barLayout.valueBaseline;
|
|
56
|
+
marker.x2 = valueFn;
|
|
80
57
|
}
|
|
81
58
|
else {
|
|
82
|
-
marker.
|
|
83
|
-
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
|
+
}
|
|
84
72
|
}
|
|
85
73
|
}
|
|
86
|
-
|
|
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);
|
|
87
110
|
}
|
|
88
111
|
});
|
|
89
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: {};
|
|
@@ -19,23 +19,19 @@
|
|
|
19
19
|
import type { BasePlotProps } from '../../types/plots/props';
|
|
20
20
|
import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
21
21
|
import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
|
|
22
|
-
import type {
|
|
22
|
+
import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
|
|
23
23
|
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
24
24
|
import type { LineMarkersConfig } from '../../types/markers/line';
|
|
25
|
-
import type { GapsConfig } from '../../types/plots/props';
|
|
26
25
|
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
27
26
|
|
|
28
27
|
type Props = BasePlotProps<
|
|
29
28
|
SeriesProps<TData> | DataProps<TData>,
|
|
30
29
|
TData,
|
|
31
|
-
|
|
30
|
+
GapsAwarePlotStylesConfig<LineSegmentStyle>,
|
|
32
31
|
LineSegmentStyle,
|
|
33
32
|
LineMarkersConfig,
|
|
34
33
|
AxisBasedScalesConfig<TData>
|
|
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
|
-
};
|
|
34
|
+
>;
|
|
39
35
|
|
|
40
36
|
let {
|
|
41
37
|
width,
|
|
@@ -48,12 +44,16 @@
|
|
|
48
44
|
styles = {},
|
|
49
45
|
segments = {},
|
|
50
46
|
markers = [],
|
|
51
|
-
gaps = {},
|
|
52
47
|
scales = {},
|
|
53
48
|
margins,
|
|
54
49
|
tooltip = undefined,
|
|
55
50
|
}: Props = $props();
|
|
56
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
|
+
|
|
57
57
|
const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
58
58
|
const legendInteraction = $derived(getLegendInteraction());
|
|
59
59
|
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
getY={(d) => Number(yAcc(d))}
|
|
173
173
|
{hover}
|
|
174
174
|
{scales}
|
|
175
|
-
|
|
175
|
+
{...labelMargin.plotProps}
|
|
176
176
|
xTickRotate={scales?.x?.tickRotate}
|
|
177
177
|
markers={[...gapMarkers, ...lineMarkers, ...markers]}
|
|
178
178
|
seriesData={resolvedSeries}
|
|
@@ -1,15 +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
|
-
import type { GapsConfig } from '../../types/plots/props';
|
|
8
7
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
9
|
-
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, 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
|
-
};
|
|
8
|
+
props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, GapsAwarePlotStylesConfig<LineSegmentStyle>, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>>;
|
|
13
9
|
exports: {};
|
|
14
10
|
bindings: "";
|
|
15
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]),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Series } from '../../types/plots/data/common';
|
|
2
|
-
import type { GapsConfig } from '../../types/plots/
|
|
2
|
+
import type { GapsConfig } from '../../types/plots/gaps';
|
|
3
3
|
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
4
4
|
import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/common';
|
|
5
5
|
/**
|
|
@@ -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>
|