@fundar/data-chart-telling 0.0.49 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configuration/config.svelte.js +7 -2
- package/dist/configuration/themes/index.d.ts +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/legend/DiscreteBadgeItem.svelte +21 -8
- package/dist/layout/legend/DiscreteBadgeItem.svelte.d.ts +1 -0
- package/dist/layout/legend/DiscreteSection.svelte +22 -9
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +2 -1
- package/dist/layout/legend/LegendLayout.svelte +7 -2
- package/dist/layout/scales/ScalesLayout.svelte +87 -10
- package/dist/layout/scales/buildScale.d.ts +18 -1
- package/dist/layout/scales/buildScale.js +49 -2
- package/dist/layout/scales/niceDomain.d.ts +6 -0
- package/dist/layout/scales/niceDomain.js +34 -0
- package/dist/layout/tooltip/controller.svelte.js +23 -11
- package/dist/layout/tooltip/utils.js +3 -0
- package/dist/markers/BarMarker.svelte +145 -84
- package/dist/markers/HoverMarker.svelte +41 -1
- package/dist/markers/HoverMarker.svelte.d.ts +7 -1
- package/dist/plots/bar/Plot.svelte +46 -20
- package/dist/plots/bar/buildBarMarkers.d.ts +4 -14
- package/dist/plots/bar/buildBarMarkers.js +73 -53
- package/dist/plots/bar/hoverPoints.d.ts +1 -1
- package/dist/plots/bar/hoverPoints.js +2 -3
- package/dist/plots/bar/layout.svelte.d.ts +4 -0
- package/dist/plots/bar/layout.svelte.js +62 -17
- package/dist/plots/line/Plot.svelte +29 -14
- package/dist/plots/line/buildLineMarkers.d.ts +1 -0
- package/dist/plots/line/buildLineMarkers.js +3 -4
- package/dist/plots/pyramid/Plot.svelte +10 -6
- package/dist/plots/scatter/Plot.svelte +25 -11
- package/dist/plots/scatter/buildScatterMarkers.d.ts +1 -0
- package/dist/plots/scatter/buildScatterMarkers.js +3 -4
- package/dist/plots/utils/legendDisabled.d.ts +13 -1
- package/dist/plots/utils/legendDisabled.js +27 -0
- package/dist/types/configuration/styling.d.ts +12 -2
- package/dist/types/layout/legend.d.ts +15 -10
- package/dist/types/layout/tooltip.d.ts +2 -1
- package/dist/types/markers/common.d.ts +14 -2
- package/package.json +1 -1
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
import { isGapValue, resolveGapAreaStyle } from '../utils/gaps';
|
|
5
4
|
const GAP_KINDS = ['start', 'middle', 'end'];
|
|
6
5
|
/**
|
|
7
|
-
* Every `'bar'` marker for one BarPlot render:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
16
|
-
*
|
|
17
|
-
* `'hitArea'` geometry is left unresolved (just `category` + `data`) —
|
|
18
|
-
* resolving it to pixels needs svelteplot's live scale, only available
|
|
19
|
-
* where the marker actually renders.
|
|
6
|
+
* Every `'bar'` marker for one BarPlot render: visible `role: 'segment'`
|
|
7
|
+
* bars (real data + gap fills) plus one `role: 'hitArea'` hit region per row,
|
|
8
|
+
* sized to that row's own segment.
|
|
20
9
|
*/
|
|
21
10
|
export function buildBarMarkers(args) {
|
|
22
|
-
const { groupedSeries, barLayout, isHorizontal, hoverPoints,
|
|
11
|
+
const { groupedSeries, barLayout, isHorizontal, hoverPoints, colorFor, disabledFillFor, active, setHoverMatch, clearHoverMatch, gapFillMaps } = args;
|
|
23
12
|
const gapDefaults = getConfiguration().bar.gap;
|
|
13
|
+
// Series names eligible for hover (excludes legend-disabled ones).
|
|
14
|
+
const hoverEligibleSeries = new Set(hoverPoints.map((p) => p.series));
|
|
24
15
|
const segmentMarkers = [];
|
|
16
|
+
const hitAreaMarkers = [];
|
|
25
17
|
groupedSeries.forEach((group, seriesIndex) => {
|
|
26
18
|
const xFn = resolveAccessor(group.series.x);
|
|
27
19
|
const yFn = resolveAccessor(group.series.y);
|
|
28
|
-
const defaultColor =
|
|
20
|
+
const defaultColor = colorFor(group.series.name);
|
|
29
21
|
const disabledFill = disabledFillFor(group.series.name);
|
|
30
|
-
//
|
|
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.
|
|
22
|
+
// Builds one series' visible bar geometry (real or gap-fill data).
|
|
34
23
|
function makeMarker(data, valueFn, style) {
|
|
35
24
|
if (data.length === 0)
|
|
36
25
|
return null;
|
|
@@ -61,31 +50,81 @@ export function buildBarMarkers(args) {
|
|
|
61
50
|
marker.y2 = valueFn;
|
|
62
51
|
}
|
|
63
52
|
if (barLayout.layout === 'grouped') {
|
|
53
|
+
const { start, end } = barLayout.groupInsetFraction(seriesIndex);
|
|
64
54
|
if (isHorizontal) {
|
|
65
|
-
marker.insetTop =
|
|
66
|
-
marker.insetBottom =
|
|
55
|
+
marker.insetTop = start;
|
|
56
|
+
marker.insetBottom = end;
|
|
67
57
|
}
|
|
68
58
|
else {
|
|
69
|
-
marker.insetLeft =
|
|
70
|
-
marker.insetRight =
|
|
59
|
+
marker.insetLeft = start;
|
|
60
|
+
marker.insetRight = end;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return marker;
|
|
65
|
+
}
|
|
66
|
+
// Builds one row's hit-test geometry — same shape as `makeMarker`, for one row.
|
|
67
|
+
function makeHitAreaMarker(d, valueFn) {
|
|
68
|
+
const marker = {
|
|
69
|
+
type: 'bar',
|
|
70
|
+
role: 'hitArea',
|
|
71
|
+
isHorizontal,
|
|
72
|
+
data: [d],
|
|
73
|
+
category: xFn(d)
|
|
74
|
+
};
|
|
75
|
+
if (barLayout.layout === 'stacked') {
|
|
76
|
+
const baseline = barLayout.stackedBaseline(xFn(d), seriesIndex);
|
|
77
|
+
const top = baseline + valueFn(d);
|
|
78
|
+
if (isHorizontal) {
|
|
79
|
+
marker.x1 = baseline;
|
|
80
|
+
marker.x2 = top;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
marker.y1 = baseline;
|
|
84
|
+
marker.y2 = top;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (isHorizontal) {
|
|
89
|
+
marker.x1 = barLayout.valueBaseline;
|
|
90
|
+
marker.x2 = valueFn(d);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
marker.y1 = barLayout.valueBaseline;
|
|
94
|
+
marker.y2 = valueFn(d);
|
|
95
|
+
}
|
|
96
|
+
if (barLayout.layout === 'grouped') {
|
|
97
|
+
const { start, end } = barLayout.groupInsetFraction(seriesIndex);
|
|
98
|
+
if (isHorizontal) {
|
|
99
|
+
marker.insetTop = start;
|
|
100
|
+
marker.insetBottom = end;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
marker.insetLeft = start;
|
|
104
|
+
marker.insetRight = end;
|
|
71
105
|
}
|
|
72
106
|
}
|
|
73
107
|
}
|
|
74
108
|
return marker;
|
|
75
109
|
}
|
|
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
110
|
for (const seg of group.visualSegments) {
|
|
80
111
|
const realData = seg.data.filter((d) => !isGapValue(yFn(d)));
|
|
81
112
|
const style = {
|
|
82
113
|
fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
|
|
83
|
-
fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1
|
|
114
|
+
fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1
|
|
84
115
|
};
|
|
85
116
|
const valueFn = barLayout.layout === 'stacked' ? (d) => Number(yFn(d)) : yFn;
|
|
86
117
|
const marker = makeMarker(realData, valueFn, style);
|
|
87
118
|
if (marker)
|
|
88
119
|
segmentMarkers.push(marker);
|
|
120
|
+
if (hoverEligibleSeries.has(group.series.name)) {
|
|
121
|
+
for (const d of realData) {
|
|
122
|
+
const hitMarker = makeHitAreaMarker(d, valueFn);
|
|
123
|
+
hitMarker.onpointerenter = active ? () => setHoverMatch([d]) : undefined;
|
|
124
|
+
hitMarker.onpointerleave = active ? clearHoverMatch : undefined;
|
|
125
|
+
hitAreaMarkers.push(hitMarker);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
89
128
|
}
|
|
90
129
|
const fillMap = gapFillMaps.get(group.series.name) ?? new Map();
|
|
91
130
|
for (const kind of GAP_KINDS) {
|
|
@@ -96,36 +135,17 @@ export function buildBarMarkers(args) {
|
|
|
96
135
|
const gapStyle = {
|
|
97
136
|
...base,
|
|
98
137
|
fill: disabledFill?.fill ?? base.fill,
|
|
99
|
-
fillOpacity: disabledFill?.fillOpacity ?? base.fillOpacity
|
|
138
|
+
fillOpacity: disabledFill?.fillOpacity ?? base.fillOpacity
|
|
100
139
|
};
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const taggedData = filledRows.map((d) => ({ ...d, __gapValue: fillMap.get(d).value }));
|
|
140
|
+
// Tags each row with its resolved gap-fill value as a plain field.
|
|
141
|
+
const taggedData = filledRows.map((d) => ({
|
|
142
|
+
...d,
|
|
143
|
+
__gapValue: fillMap.get(d).value
|
|
144
|
+
}));
|
|
107
145
|
const marker = makeMarker(taggedData, (d) => d.__gapValue, gapStyle);
|
|
108
146
|
if (marker)
|
|
109
147
|
segmentMarkers.push(marker);
|
|
110
148
|
}
|
|
111
149
|
});
|
|
112
|
-
const byCategory = new Map();
|
|
113
|
-
for (const p of hoverPoints) {
|
|
114
|
-
const key = isHorizontal ? p.y : p.x;
|
|
115
|
-
const list = byCategory.get(key);
|
|
116
|
-
if (list)
|
|
117
|
-
list.push(p);
|
|
118
|
-
else
|
|
119
|
-
byCategory.set(key, [p]);
|
|
120
|
-
}
|
|
121
|
-
const hitAreaMarkers = [...byCategory.entries()].map(([category, points]) => ({
|
|
122
|
-
type: 'bar',
|
|
123
|
-
role: 'hitArea',
|
|
124
|
-
isHorizontal,
|
|
125
|
-
category,
|
|
126
|
-
data: points.map((p) => p.row),
|
|
127
|
-
onpointerenter: active ? () => setHoverMatch(points.map((p) => p.row)) : undefined,
|
|
128
|
-
onpointerleave: active ? clearHoverMatch : undefined,
|
|
129
|
-
}));
|
|
130
150
|
return [...segmentMarkers, ...hitAreaMarkers];
|
|
131
151
|
}
|
|
@@ -12,4 +12,4 @@ import type { BarLayout } from './layout.svelte';
|
|
|
12
12
|
* height instead of `dx`/`dy`, since that offset is representable as a
|
|
13
13
|
* regular data value (unlike 'grouped''s pixel-only slice offset).
|
|
14
14
|
*/
|
|
15
|
-
export declare function buildHoverPoints<TData extends Record<string, unknown>>(resolvedSeries: Series<TData>[], isHorizontal: boolean, geometry: Pick<BarLayout<TData>, 'layout' | 'stackedBaseline' | 'groupOffset'>,
|
|
15
|
+
export declare function buildHoverPoints<TData extends Record<string, unknown>>(resolvedSeries: Series<TData>[], isHorizontal: boolean, geometry: Pick<BarLayout<TData>, 'layout' | 'stackedBaseline' | 'groupOffset'>, colorFor: (seriesName: string) => string): HoverPoint<TData>[];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { isGapValue } from '../utils/gaps';
|
|
4
3
|
/**
|
|
5
4
|
* Builds hover-marker candidate points for every row across every series.
|
|
@@ -12,7 +11,7 @@ import { isGapValue } from '../utils/gaps';
|
|
|
12
11
|
* height instead of `dx`/`dy`, since that offset is representable as a
|
|
13
12
|
* regular data value (unlike 'grouped''s pixel-only slice offset).
|
|
14
13
|
*/
|
|
15
|
-
export function buildHoverPoints(resolvedSeries, isHorizontal, geometry,
|
|
14
|
+
export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, colorFor) {
|
|
16
15
|
return resolvedSeries.flatMap((s, idx) => {
|
|
17
16
|
const sx = resolveAccessor(s.x);
|
|
18
17
|
const sy = resolveAccessor(s.y);
|
|
@@ -29,7 +28,7 @@ export function buildHoverPoints(resolvedSeries, isHorizontal, geometry, palette
|
|
|
29
28
|
row: d,
|
|
30
29
|
label: String(sy(d)),
|
|
31
30
|
series: s.name,
|
|
32
|
-
color:
|
|
31
|
+
color: colorFor(s.name),
|
|
33
32
|
};
|
|
34
33
|
});
|
|
35
34
|
});
|
|
@@ -34,5 +34,9 @@ export declare function createBarLayout<TData extends Record<string, unknown>>(a
|
|
|
34
34
|
dx: number;
|
|
35
35
|
dy: number;
|
|
36
36
|
};
|
|
37
|
+
groupInsetFraction: (seriesIndex: number) => {
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
export type BarLayout<TData extends Record<string, unknown>> = ReturnType<typeof createBarLayout<TData>>;
|
|
@@ -58,7 +58,12 @@ export function createBarLayout(args) {
|
|
|
58
58
|
* band-scale formula (and 0.15 default padding) svelteplot itself uses,
|
|
59
59
|
* so the category axis's own `paddingInner`/`paddingOuter` — the space
|
|
60
60
|
* *between* categories — is reflected here too, not just svelteplot's
|
|
61
|
-
* real render.
|
|
61
|
+
* real render. In practice `categoricalScale` always carries a resolved
|
|
62
|
+
* `paddingInner`/`paddingOuter` by the time it gets here — `Plot.svelte`
|
|
63
|
+
* backs its own category axis with the theme's `bar.paddingInner`/
|
|
64
|
+
* `paddingOuter` before either this estimate or the real svelteplot
|
|
65
|
+
* scale sees it, so the two stay in sync; the literal fallback below is
|
|
66
|
+
* only a defensive last resort.
|
|
62
67
|
*/
|
|
63
68
|
const estimatedBandwidth = $derived.by(() => {
|
|
64
69
|
const categoryCount = new SvelteSet(args.flat().map((d) => args.xAcc()(d))).size;
|
|
@@ -66,19 +71,25 @@ export function createBarLayout(args) {
|
|
|
66
71
|
return 0;
|
|
67
72
|
const baseMargins = { ...cfg.margins, ...args.margins() };
|
|
68
73
|
const innerSize = args.isHorizontal()
|
|
69
|
-
? Math.max(0, args.height() -
|
|
70
|
-
|
|
74
|
+
? Math.max(0, args.height() -
|
|
75
|
+
numericMargin('top', baseMargins.top) -
|
|
76
|
+
numericMargin('bottom', baseMargins.bottom))
|
|
77
|
+
: Math.max(0, args.width() -
|
|
78
|
+
numericMargin('left', baseMargins.left) -
|
|
79
|
+
numericMargin('right', baseMargins.right));
|
|
71
80
|
const paddingInner = categoricalScale?.paddingInner ?? categoricalScale?.padding ?? 0.15;
|
|
72
81
|
const paddingOuter = categoricalScale?.paddingOuter ?? categoricalScale?.padding ?? 0.15;
|
|
73
82
|
const step = innerSize / (categoryCount - paddingInner + 2 * paddingOuter);
|
|
74
83
|
return step * (1 - paddingInner);
|
|
75
84
|
});
|
|
76
85
|
const seriesCount = $derived(args.groupedSeries().length);
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
86
|
+
// `seriesPadding` (0–1, space *within* a group, between its series)
|
|
87
|
+
// reserves a fraction of each series' own slice as a gap split evenly on
|
|
88
|
+
// either side of the bar.
|
|
89
|
+
const seriesGapFraction = $derived(Math.max(0, Math.min(1, args.scales().z?.seriesPadding ?? cfg.bar.seriesPadding)));
|
|
90
|
+
// Each series gets an equal slice of the band.
|
|
80
91
|
const seriesStep = $derived(seriesCount > 0 ? estimatedBandwidth / seriesCount : 0);
|
|
81
|
-
const seriesGap = $derived(
|
|
92
|
+
const seriesGap = $derived(seriesGapFraction * seriesStep);
|
|
82
93
|
/**
|
|
83
94
|
* Per-category cumulative sums for 'stacked' layout — `stackedPrefixSums.get(category)[i]`
|
|
84
95
|
* is the sum of series `0..i-1`'s value at that category, i.e. series `i`'s
|
|
@@ -114,24 +125,58 @@ export function createBarLayout(args) {
|
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
116
127
|
* Pixel offset for a 'grouped' series' own slice of the category band —
|
|
117
|
-
* shared by bar segments
|
|
118
|
-
*
|
|
119
|
-
* this series' bar actually renders.
|
|
128
|
+
* shared by bar segments, value labels, and hover markers. `dy` is
|
|
129
|
+
* negated for horizontal bars, matching how `insetBottom` positions them.
|
|
120
130
|
*/
|
|
121
131
|
function groupOffset(seriesIndex) {
|
|
122
132
|
if (layout !== 'grouped')
|
|
123
133
|
return { dx: 0, dy: 0 };
|
|
124
134
|
const offset = -(estimatedBandwidth / 2) + seriesIndex * seriesStep + seriesStep / 2;
|
|
125
|
-
return args.isHorizontal() ? { dx: 0, dy: offset } : { dx: offset, dy: 0 };
|
|
135
|
+
return args.isHorizontal() ? { dx: 0, dy: -offset } : { dx: offset, dy: 0 };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A 'grouped' series' own slice of the category band, as fractions (0–1)
|
|
139
|
+
* of the *whole* band rather than pixels — unlike {@link groupOffset},
|
|
140
|
+
* which bakes in `estimatedBandwidth`'s own guess, these stay exact
|
|
141
|
+
* regardless of how far off that guess ends up: `BarMarker` multiplies
|
|
142
|
+
* them by the band's real, svelteplot-measured width at render time
|
|
143
|
+
* (only available once mounted inside the actual `<Plot>`, which this
|
|
144
|
+
* module isn't), so a bar's own visible width/position is never off by
|
|
145
|
+
* the estimate's error the way `estimatedBandwidth`-derived pixels can be
|
|
146
|
+
* — most noticeable in a small faceted cell, where a fixed margin
|
|
147
|
+
* estimate is proportionally least accurate.
|
|
148
|
+
*/
|
|
149
|
+
function groupInsetFraction(seriesIndex) {
|
|
150
|
+
if (layout !== 'grouped' || seriesCount === 0)
|
|
151
|
+
return { start: 0, end: 0 };
|
|
152
|
+
const sliceFraction = 1 / seriesCount;
|
|
153
|
+
const gapFraction = (seriesGapFraction * sliceFraction) / 2;
|
|
154
|
+
return {
|
|
155
|
+
start: seriesIndex * sliceFraction + gapFraction,
|
|
156
|
+
end: (seriesCount - 1 - seriesIndex) * sliceFraction + gapFraction
|
|
157
|
+
};
|
|
126
158
|
}
|
|
127
159
|
return {
|
|
128
|
-
get layout() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
get
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
get layout() {
|
|
161
|
+
return layout;
|
|
162
|
+
},
|
|
163
|
+
get valueBaseline() {
|
|
164
|
+
return valueBaseline;
|
|
165
|
+
},
|
|
166
|
+
get estimatedBandwidth() {
|
|
167
|
+
return estimatedBandwidth;
|
|
168
|
+
},
|
|
169
|
+
get seriesCount() {
|
|
170
|
+
return seriesCount;
|
|
171
|
+
},
|
|
172
|
+
get seriesStep() {
|
|
173
|
+
return seriesStep;
|
|
174
|
+
},
|
|
175
|
+
get seriesGap() {
|
|
176
|
+
return seriesGap;
|
|
177
|
+
},
|
|
134
178
|
stackedBaseline,
|
|
135
179
|
groupOffset,
|
|
180
|
+
groupInsetFraction
|
|
136
181
|
};
|
|
137
182
|
}
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledStrokeOverride,
|
|
9
|
+
disabledDotsOverride,
|
|
10
|
+
resolveEffectiveDisabledStyle,
|
|
11
|
+
omitDisabledSeries,
|
|
12
|
+
buildSeriesColorIndex
|
|
13
|
+
} from '../utils/legendDisabled';
|
|
8
14
|
import { buildLineMarkers } from './buildLineMarkers';
|
|
9
15
|
import { buildGapMarkers } from './buildGapMarkers';
|
|
10
16
|
import { isGapValue } from '../utils/gaps';
|
|
@@ -64,8 +70,16 @@
|
|
|
64
70
|
// resolved per series.
|
|
65
71
|
const gaps = $derived(styles.gaps ?? {});
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const cfg = $derived(getConfiguration());
|
|
68
74
|
const legendInteraction = $derived(getLegendInteraction());
|
|
75
|
+
// Unfiltered series list, for stable per-name color indexing.
|
|
76
|
+
const fullSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
77
|
+
const colorIndex = $derived(buildSeriesColorIndex(fullSeries));
|
|
78
|
+
function colorForSeries(name: string): string {
|
|
79
|
+
return paletteColor(colorIndex.get(name) ?? 0, cfg.palette);
|
|
80
|
+
}
|
|
81
|
+
// Drops series toggled off with `disabled: { mode: 'omit' }`.
|
|
82
|
+
const resolvedSeries = $derived(omitDisabledSeries(fullSeries, legendInteraction));
|
|
69
83
|
|
|
70
84
|
$effect(() => {
|
|
71
85
|
validateSegments(segments);
|
|
@@ -75,7 +89,6 @@
|
|
|
75
89
|
buildGroupedSeries<Series<TData>, LineSegmentStyle>(resolvedSeries, segments)
|
|
76
90
|
);
|
|
77
91
|
|
|
78
|
-
const cfg = $derived(getConfiguration());
|
|
79
92
|
const showVals = $derived(styles.values?.show ?? false);
|
|
80
93
|
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
81
94
|
const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'start');
|
|
@@ -86,11 +99,12 @@
|
|
|
86
99
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
87
100
|
|
|
88
101
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
89
|
-
return (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
return resolveEffectiveDisabledStyle(
|
|
103
|
+
name,
|
|
104
|
+
legendInteraction,
|
|
105
|
+
cfg.legend.disabledOpacity,
|
|
106
|
+
hoverIsolate,
|
|
107
|
+
hoverIsolatedSeries
|
|
94
108
|
);
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -99,7 +113,7 @@
|
|
|
99
113
|
const flat = $derived(resolvedSeries.flatMap((s) => s.data));
|
|
100
114
|
// Legend-disabled series and gap rows are excluded from hover candidates.
|
|
101
115
|
const hoverPoints = $derived(
|
|
102
|
-
resolvedSeries.flatMap((s
|
|
116
|
+
resolvedSeries.flatMap((s) => {
|
|
103
117
|
if (legendInteraction?.disabledSeries.has(s.name)) return [];
|
|
104
118
|
const sx = resolveAccessor(s.x);
|
|
105
119
|
const sy = resolveAccessor(s.y);
|
|
@@ -111,7 +125,7 @@
|
|
|
111
125
|
row: d,
|
|
112
126
|
label: String(sy(d)),
|
|
113
127
|
series: s.name,
|
|
114
|
-
color:
|
|
128
|
+
color: colorForSeries(s.name)
|
|
115
129
|
}));
|
|
116
130
|
})
|
|
117
131
|
);
|
|
@@ -128,6 +142,7 @@
|
|
|
128
142
|
const lineMarkers = $derived(
|
|
129
143
|
buildLineMarkers({
|
|
130
144
|
groupedSeries,
|
|
145
|
+
colorFor: colorForSeries,
|
|
131
146
|
disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
|
|
132
147
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
133
148
|
})
|
|
@@ -137,7 +152,7 @@
|
|
|
137
152
|
buildGapMarkers<TData>({
|
|
138
153
|
seriesList: resolvedSeries,
|
|
139
154
|
gaps,
|
|
140
|
-
colorFor: (seriesIndex) =>
|
|
155
|
+
colorFor: (seriesIndex) => colorForSeries(resolvedSeries[seriesIndex].name),
|
|
141
156
|
disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
|
|
142
157
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
143
158
|
})
|
|
@@ -147,14 +162,14 @@
|
|
|
147
162
|
buildScopedMarkerGroups<TData, Series<TData>, LineSegmentStyle>({
|
|
148
163
|
groupedSeries,
|
|
149
164
|
segments,
|
|
150
|
-
colorFor: (
|
|
165
|
+
colorFor: (series) => colorForSeries(series.name),
|
|
151
166
|
segmentColorFor: (style, defaultColor) => style?.stroke?.stroke ?? defaultColor
|
|
152
167
|
})
|
|
153
168
|
);
|
|
154
169
|
|
|
155
170
|
const lastPoints = $derived(
|
|
156
171
|
resolvedSeries
|
|
157
|
-
.map((s
|
|
172
|
+
.map((s) => {
|
|
158
173
|
const sy = resolveAccessor(s.y);
|
|
159
174
|
const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
|
|
160
175
|
if (lastRow === undefined) return undefined;
|
|
@@ -164,7 +179,7 @@
|
|
|
164
179
|
matchedSeg?.color ??
|
|
165
180
|
group?.segments[0]?.color ??
|
|
166
181
|
group?.color ??
|
|
167
|
-
|
|
182
|
+
colorForSeries(s.name);
|
|
168
183
|
return { series: s, lastRow, color };
|
|
169
184
|
})
|
|
170
185
|
.filter((p): p is { series: Series<TData>; lastRow: TData; color: string } => p !== undefined)
|
|
@@ -10,6 +10,7 @@ import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/comm
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function buildLineMarkers<TData extends Record<string, unknown>>(args: {
|
|
12
12
|
groupedSeries: VisualGroup<Series<TData>, LineSegmentStyle>[];
|
|
13
|
+
colorFor: (seriesName: string) => string;
|
|
13
14
|
disabledStrokeFor: (seriesName: string) => StrokeStyle | undefined;
|
|
14
15
|
disabledDotsFor: (seriesName: string) => DotStyle | undefined;
|
|
15
16
|
}): (LineMarkerConfig | DotMarkerConfig)[];
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
/**
|
|
5
4
|
* Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
|
|
@@ -7,11 +6,11 @@ import { getConfiguration } from '../../configuration/config.svelte';
|
|
|
7
6
|
* `'dot'` marker right after it when that segment sets `style.dots`.
|
|
8
7
|
*/
|
|
9
8
|
export function buildLineMarkers(args) {
|
|
10
|
-
const { groupedSeries, disabledStrokeFor, disabledDotsFor } = args;
|
|
9
|
+
const { groupedSeries, colorFor, disabledStrokeFor, disabledDotsFor } = args;
|
|
11
10
|
const cfg = getConfiguration();
|
|
12
11
|
const markers = [];
|
|
13
|
-
groupedSeries.forEach((group
|
|
14
|
-
const defaultColor =
|
|
12
|
+
groupedSeries.forEach((group) => {
|
|
13
|
+
const defaultColor = colorFor(group.series.name);
|
|
15
14
|
const disabledStroke = disabledStrokeFor(group.series.name);
|
|
16
15
|
const disabledDots = disabledDotsFor(group.series.name);
|
|
17
16
|
const xFn = resolveAccessor(group.series.x);
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledFillOverride,
|
|
9
|
+
resolveEffectiveDisabledStyle
|
|
10
|
+
} from '../utils/legendDisabled';
|
|
8
11
|
import { buildPyramidBarMarkers } from './buildPyramidBarMarkers';
|
|
9
12
|
import {
|
|
10
13
|
createLabelMarginTracker,
|
|
@@ -90,11 +93,12 @@
|
|
|
90
93
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
91
94
|
|
|
92
95
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
93
|
-
return (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
return resolveEffectiveDisabledStyle(
|
|
97
|
+
name,
|
|
98
|
+
legendInteraction,
|
|
99
|
+
cfg.legend.disabledOpacity,
|
|
100
|
+
hoverIsolate,
|
|
101
|
+
hoverIsolatedSeries
|
|
98
102
|
);
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
import { paletteColor } from '../../utils/color';
|
|
5
5
|
import { buildSeries } from '../../utils/grouping';
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
disabledDotsOverride,
|
|
9
|
+
resolveEffectiveDisabledStyle,
|
|
10
|
+
omitDisabledSeries,
|
|
11
|
+
buildSeriesColorIndex
|
|
12
|
+
} from '../utils/legendDisabled';
|
|
8
13
|
import { buildScatterMarkers } from './buildScatterMarkers';
|
|
9
14
|
import { resolveRadiusPaddedDomain } from './resolveRadiusPaddedDomain';
|
|
10
15
|
import {
|
|
@@ -67,8 +72,16 @@
|
|
|
67
72
|
tooltip = undefined
|
|
68
73
|
}: Props = $props();
|
|
69
74
|
|
|
70
|
-
const
|
|
75
|
+
const cfg = $derived(getConfiguration());
|
|
71
76
|
const legendInteraction = $derived(getLegendInteraction());
|
|
77
|
+
// Unfiltered series list, for stable per-name color indexing.
|
|
78
|
+
const fullSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
|
|
79
|
+
const colorIndex = $derived(buildSeriesColorIndex(fullSeries));
|
|
80
|
+
function colorForSeries(name: string): string {
|
|
81
|
+
return paletteColor(colorIndex.get(name) ?? 0, cfg.palette);
|
|
82
|
+
}
|
|
83
|
+
// Drops series toggled off with `disabled: { mode: 'omit' }`.
|
|
84
|
+
const resolvedSeries = $derived(omitDisabledSeries(fullSeries, legendInteraction));
|
|
72
85
|
|
|
73
86
|
$effect(() => {
|
|
74
87
|
validateSegments(segments);
|
|
@@ -81,7 +94,6 @@
|
|
|
81
94
|
buildGroupedSeries<Series<TData>, ScatterSegmentStyle>(resolvedSeries, segments, false)
|
|
82
95
|
);
|
|
83
96
|
|
|
84
|
-
const cfg = $derived(getConfiguration());
|
|
85
97
|
const showVals = $derived(styles.values?.show ?? false);
|
|
86
98
|
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
87
99
|
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
@@ -93,11 +105,12 @@
|
|
|
93
105
|
let hoverIsolatedSeries = $state<string | null>(null);
|
|
94
106
|
|
|
95
107
|
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
96
|
-
return (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
return resolveEffectiveDisabledStyle(
|
|
109
|
+
name,
|
|
110
|
+
legendInteraction,
|
|
111
|
+
cfg.legend.disabledOpacity,
|
|
112
|
+
hoverIsolate,
|
|
113
|
+
hoverIsolatedSeries
|
|
101
114
|
);
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -108,7 +121,7 @@
|
|
|
108
121
|
// A legend-disabled series is dimmed on the mark itself, but never surfaces
|
|
109
122
|
// in hover/tooltip — its points are simply absent from the candidate list.
|
|
110
123
|
const hoverPoints = $derived(
|
|
111
|
-
resolvedSeries.flatMap((s
|
|
124
|
+
resolvedSeries.flatMap((s) => {
|
|
112
125
|
if (legendInteraction?.disabledSeries.has(s.name)) return [];
|
|
113
126
|
const sx = resolveAccessor(s.x);
|
|
114
127
|
const sy = resolveAccessor(s.y);
|
|
@@ -118,7 +131,7 @@
|
|
|
118
131
|
row: d,
|
|
119
132
|
label: `${sx(d)}, ${sy(d)}`,
|
|
120
133
|
series: s.name,
|
|
121
|
-
color:
|
|
134
|
+
color: colorForSeries(s.name)
|
|
122
135
|
}));
|
|
123
136
|
})
|
|
124
137
|
);
|
|
@@ -138,6 +151,7 @@
|
|
|
138
151
|
buildScatterMarkers({
|
|
139
152
|
groupedSeries,
|
|
140
153
|
r,
|
|
154
|
+
colorFor: colorForSeries,
|
|
141
155
|
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
|
|
142
156
|
})
|
|
143
157
|
);
|
|
@@ -146,7 +160,7 @@
|
|
|
146
160
|
buildScopedMarkerGroups<TData, Series<TData>, ScatterSegmentStyle>({
|
|
147
161
|
groupedSeries,
|
|
148
162
|
segments,
|
|
149
|
-
colorFor: (
|
|
163
|
+
colorFor: (series) => colorForSeries(series.name),
|
|
150
164
|
segmentColorFor: (style, defaultColor) => style?.dotFill ?? defaultColor
|
|
151
165
|
})
|
|
152
166
|
);
|
|
@@ -7,5 +7,6 @@ import type { DotMarkerConfig } from '../../types/markers/common';
|
|
|
7
7
|
export declare function buildScatterMarkers<TData extends Record<string, unknown>>(args: {
|
|
8
8
|
groupedSeries: VisualGroup<Series<TData>, ScatterSegmentStyle>[];
|
|
9
9
|
r?: Accessor<TData, number> | number;
|
|
10
|
+
colorFor: (seriesName: string) => string;
|
|
10
11
|
disabledDotsFor: (seriesName: string) => DotStyle | undefined;
|
|
11
12
|
}): DotMarkerConfig[];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { resolveAccessor } from '../utils/accessors';
|
|
2
|
-
import { paletteColor } from '../../utils/color';
|
|
3
2
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
4
3
|
/** Every `'dot'` marker for one ScatterPlot render: one per visual segment. */
|
|
5
4
|
export function buildScatterMarkers(args) {
|
|
6
|
-
const { groupedSeries, r, disabledDotsFor } = args;
|
|
5
|
+
const { groupedSeries, r, colorFor, disabledDotsFor } = args;
|
|
7
6
|
const cfg = getConfiguration();
|
|
8
7
|
const rFn = r === undefined || typeof r === 'number' ? r : resolveAccessor(r);
|
|
9
8
|
const markers = [];
|
|
10
|
-
groupedSeries.forEach((group
|
|
11
|
-
const defaultColor =
|
|
9
|
+
groupedSeries.forEach((group) => {
|
|
10
|
+
const defaultColor = colorFor(group.series.name);
|
|
12
11
|
const disabledDots = disabledDotsFor(group.series.name);
|
|
13
12
|
const xFn = resolveAccessor(group.series.x);
|
|
14
13
|
const yFn = resolveAccessor(group.series.y);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
1
|
+
import type { LegendDisabledAction, LegendDisabledStyle, LegendInteractionStore } from '../../types/layout/legend';
|
|
2
2
|
import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
|
|
3
|
+
/** Resolves a {@link LegendDisabledAction} into the style a mark should render with — `'omit'` resolves to fully transparent. */
|
|
4
|
+
export declare function resolveDisabledStyle(action: LegendDisabledAction | undefined, fallbackOpacity: number): LegendDisabledStyle | undefined;
|
|
5
|
+
/** Drops series toggled off with `disabled: { mode: 'omit' }` from a series list. */
|
|
6
|
+
export declare function omitDisabledSeries<S extends {
|
|
7
|
+
name: string;
|
|
8
|
+
}>(series: S[], legendInteraction: LegendInteractionStore | null | undefined): S[];
|
|
9
|
+
/** Builds a per-series-name → palette index from the full series list, before {@link omitDisabledSeries} filters it. */
|
|
10
|
+
export declare function buildSeriesColorIndex<S extends {
|
|
11
|
+
name: string;
|
|
12
|
+
}>(series: S[]): Map<string, number>;
|
|
13
|
+
/** Resolves the disabled style for a series by name: legend-off state, falling back to hover-isolate dim. */
|
|
14
|
+
export declare function resolveEffectiveDisabledStyle(name: string, legendInteraction: LegendInteractionStore | null | undefined, disabledOpacity: number, hoverIsolate: boolean, hoverIsolatedSeries: string | null): LegendDisabledStyle | undefined;
|
|
3
15
|
/**
|
|
4
16
|
* The three per-mark override layers a legend "disabled" state can apply,
|
|
5
17
|
* derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
|