@fundar/data-chart-telling 0.0.38 → 0.0.40

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.
@@ -1,187 +1,212 @@
1
- <script
2
- lang="ts"
3
- generics="TData extends Record<string, unknown>"
4
- >
5
- import { resolveAccessor } from '../utils/accessors';
6
- import { getConfiguration } from '../../configuration/config.svelte';
7
- import { paletteColor } from '../../utils/color';
8
- import { buildSeries } from '../../utils/grouping';
9
- import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
10
- import { disabledStrokeOverride, disabledDotsOverride } from '../utils/legendDisabled';
11
- import { buildLineMarkers } from './buildLineMarkers';
12
- import { buildGapMarkers } from './buildGapMarkers';
13
- import { isGapValue } from '../utils/gaps';
14
- import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
15
- import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
16
- import ValueLabels from './ValueLabels.svelte';
17
- import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
18
- import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
19
- import type { BasePlotProps } from '../../types/plots/props';
20
- import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
21
- import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
22
- import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
23
- import type { LineSegmentStyle } from '../../types/plots/segments/common';
24
- import type { LineMarkersConfig } from '../../types/markers/line';
25
- import type { LegendDisabledStyle } from '../../types/layout/legend';
26
-
27
- type Props = BasePlotProps<
28
- SeriesProps<TData> | DataProps<TData>,
29
- TData,
30
- GapsAwarePlotStylesConfig<LineSegmentStyle>,
31
- LineSegmentStyle,
32
- LineMarkersConfig,
33
- AxisBasedScalesConfig<TData>
34
- >;
35
-
36
- let {
37
- width,
38
- height,
39
- series,
40
- data,
41
- x,
42
- y,
43
- z,
44
- styles = {},
45
- segments = {},
46
- markers = [],
47
- scales = {},
48
- margins,
49
- tooltip = undefined,
50
- }: Props = $props();
51
-
52
- // Fills missing-data gaps with an interpolated, distinctly-styled bridge,
53
- // resolved per series.
54
- const gaps = $derived(styles.gaps ?? {});
55
-
56
- const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
57
- const legendInteraction = $derived(getLegendInteraction());
58
-
59
- $effect(() => { validateSegments(segments); });
60
-
61
- const groupedSeries = $derived(
62
- buildGroupedSeries<Series<TData>, LineSegmentStyle>(resolvedSeries, segments),
63
- );
64
-
65
- const cfg = $derived(getConfiguration());
66
- const showVals = $derived(styles.values?.show ?? false);
67
- const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
68
-
69
- // Dims every other series like a legend toggle when a value label is
70
- // hovered, local to this `<Plot>` instance.
71
- let hoverIsolatedSeries = $state<string | null>(null);
72
-
73
- function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
74
- return (
75
- legendInteraction?.disabledSeries.get(name) ??
76
- (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
77
- ? { opacity: cfg.legend.disabledOpacity }
78
- : undefined)
79
- );
80
- }
81
-
82
- const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
83
- const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
84
- const flat = $derived(resolvedSeries.flatMap((s) => s.data));
85
- // Legend-disabled series and gap rows are excluded from hover candidates.
86
- const hoverPoints = $derived(
87
- resolvedSeries.flatMap((s, idx) => {
88
- if (legendInteraction?.disabledSeries.has(s.name)) return [];
89
- const sx = resolveAccessor(s.x);
90
- const sy = resolveAccessor(s.y);
91
- return s.data
92
- .filter((d) => !isGapValue(sy(d)))
93
- .map((d) => ({
94
- x: sx(d) as AxisValue,
95
- y: Number(sy(d)),
96
- row: d,
97
- label: String(sy(d)),
98
- series: s.name,
99
- color: paletteColor(idx, cfg.palette),
100
- }));
101
- }),
102
- );
103
-
104
- const hover = $derived({
105
- active: tooltip != null || hasHoverMarker(markers),
106
- points: hoverPoints,
107
- label: (r: TData) => String(yAcc(r)),
108
- strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'x'),
109
- sync: resolveHoverSync(tooltip?.sync, markers),
110
- tooltip,
111
- });
112
-
113
- const lineMarkers = $derived(
114
- buildLineMarkers({
115
- groupedSeries,
116
- disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
117
- disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name)),
118
- }),
119
- );
120
-
121
- const gapMarkers = $derived(
122
- buildGapMarkers<TData>({
123
- seriesList: resolvedSeries,
124
- gaps,
125
- colorFor: (seriesIndex) => paletteColor(seriesIndex, cfg.palette),
126
- }),
127
- );
128
-
129
- const scopedGroups = $derived(
130
- buildScopedMarkerGroups<TData, Series<TData>, LineSegmentStyle>({
131
- groupedSeries,
132
- segments,
133
- colorFor: (_series, seriesIndex) => paletteColor(seriesIndex, cfg.palette),
134
- segmentColorFor: (style, defaultColor) => style?.stroke?.stroke ?? defaultColor,
135
- }),
136
- );
137
-
138
- const lastPoints = $derived(
139
- resolvedSeries
140
- .map((s, idx) => {
141
- const sy = resolveAccessor(s.y);
142
- const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
143
- if (lastRow === undefined) return undefined;
144
- const group = scopedGroups.find((g) => g.name === s.name);
145
- const matchedSeg = group?.segments.find((seg) => seg.match(lastRow));
146
- const color =
147
- matchedSeg?.color ?? group?.segments[0]?.color ?? group?.color ?? paletteColor(idx, cfg.palette);
148
- return { series: s, lastRow, color };
149
- })
150
- .filter((p): p is { series: Series<TData>; lastRow: TData; color: string } => p !== undefined),
151
- );
152
-
153
- // Grows the plot's margin to fit value labels that overflow the content box.
154
- const labelMargin = createLabelMarginTracker(() => margins);
1
+ <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ import { resolveAccessor } from '../utils/accessors';
3
+ import { getConfiguration } from '../../configuration/config.svelte';
4
+ import { paletteColor } from '../../utils/color';
5
+ import { buildSeries } from '../../utils/grouping';
6
+ import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
7
+ import { disabledStrokeOverride, disabledDotsOverride } from '../utils/legendDisabled';
8
+ import { buildLineMarkers } from './buildLineMarkers';
9
+ import { buildGapMarkers } from './buildGapMarkers';
10
+ import { isGapValue } from '../utils/gaps';
11
+ import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
12
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
13
+ import ValueLabels from './ValueLabels.svelte';
14
+ import {
15
+ hasHoverMarker,
16
+ resolveHoverStrategy,
17
+ resolveHoverSync
18
+ } from '../../layout/tooltip/utils';
19
+ import {
20
+ buildGroupedSeries,
21
+ validateSegments,
22
+ buildScopedMarkerGroups
23
+ } from '../utils/segments';
24
+ import type { BasePlotProps } from '../../types/plots/props';
25
+ import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
26
+ import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
27
+ import type { GapsAwarePlotStylesConfig } from '../../types/layout/styles';
28
+ import type { LineSegmentStyle } from '../../types/plots/segments/common';
29
+ import type { LineMarkersConfig } from '../../types/markers/line';
30
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
31
+
32
+ type Props = BasePlotProps<
33
+ SeriesProps<TData> | DataProps<TData>,
34
+ TData,
35
+ GapsAwarePlotStylesConfig<LineSegmentStyle>,
36
+ LineSegmentStyle,
37
+ LineMarkersConfig,
38
+ AxisBasedScalesConfig<TData>
39
+ >;
40
+
41
+ let {
42
+ width,
43
+ height,
44
+ series,
45
+ data,
46
+ x,
47
+ y,
48
+ z,
49
+ styles = {},
50
+ segments = {},
51
+ markers = [],
52
+ scales = {},
53
+ margins,
54
+ tooltip = undefined
55
+ }: Props = $props();
56
+
57
+ // Fills missing-data gaps with an interpolated, distinctly-styled bridge,
58
+ // resolved per series.
59
+ const gaps = $derived(styles.gaps ?? {});
60
+
61
+ const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
62
+ const legendInteraction = $derived(getLegendInteraction());
63
+
64
+ $effect(() => {
65
+ validateSegments(segments);
66
+ });
67
+
68
+ const groupedSeries = $derived(
69
+ buildGroupedSeries<Series<TData>, LineSegmentStyle>(resolvedSeries, segments)
70
+ );
71
+
72
+ const cfg = $derived(getConfiguration());
73
+ const showVals = $derived(styles.values?.show ?? false);
74
+ const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
75
+
76
+ // Dims every other series like a legend toggle when a value label is
77
+ // hovered, local to this `<Plot>` instance.
78
+ let hoverIsolatedSeries = $state<string | null>(null);
79
+
80
+ function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
81
+ return (
82
+ legendInteraction?.disabledSeries.get(name) ??
83
+ (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
84
+ ? { opacity: cfg.legend.disabledOpacity }
85
+ : undefined)
86
+ );
87
+ }
88
+
89
+ const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
90
+ const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
91
+ const flat = $derived(resolvedSeries.flatMap((s) => s.data));
92
+ // Legend-disabled series and gap rows are excluded from hover candidates.
93
+ const hoverPoints = $derived(
94
+ resolvedSeries.flatMap((s, idx) => {
95
+ if (legendInteraction?.disabledSeries.has(s.name)) return [];
96
+ const sx = resolveAccessor(s.x);
97
+ const sy = resolveAccessor(s.y);
98
+ return s.data
99
+ .filter((d) => !isGapValue(sy(d)))
100
+ .map((d) => ({
101
+ x: sx(d) as AxisValue,
102
+ y: Number(sy(d)),
103
+ row: d,
104
+ label: String(sy(d)),
105
+ series: s.name,
106
+ color: paletteColor(idx, cfg.palette)
107
+ }));
108
+ })
109
+ );
110
+
111
+ const hover = $derived({
112
+ active: tooltip != null || hasHoverMarker(markers),
113
+ points: hoverPoints,
114
+ label: (r: TData) => String(yAcc(r)),
115
+ strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'x'),
116
+ sync: resolveHoverSync(tooltip?.sync, markers),
117
+ tooltip
118
+ });
119
+
120
+ const lineMarkers = $derived(
121
+ buildLineMarkers({
122
+ groupedSeries,
123
+ disabledStrokeFor: (name) => disabledStrokeOverride(effectiveDisabledFor(name)),
124
+ disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name))
125
+ })
126
+ );
127
+
128
+ const gapMarkers = $derived(
129
+ buildGapMarkers<TData>({
130
+ seriesList: resolvedSeries,
131
+ gaps,
132
+ colorFor: (seriesIndex) => paletteColor(seriesIndex, cfg.palette)
133
+ })
134
+ );
135
+
136
+ const scopedGroups = $derived(
137
+ buildScopedMarkerGroups<TData, Series<TData>, LineSegmentStyle>({
138
+ groupedSeries,
139
+ segments,
140
+ colorFor: (_series, seriesIndex) => paletteColor(seriesIndex, cfg.palette),
141
+ segmentColorFor: (style, defaultColor) => style?.stroke?.stroke ?? defaultColor
142
+ })
143
+ );
144
+
145
+ const lastPoints = $derived(
146
+ resolvedSeries
147
+ .map((s, idx) => {
148
+ const sy = resolveAccessor(s.y);
149
+ const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
150
+ if (lastRow === undefined) return undefined;
151
+ const group = scopedGroups.find((g) => g.name === s.name);
152
+ const matchedSeg = group?.segments.find((seg) => seg.match(lastRow));
153
+ const color =
154
+ matchedSeg?.color ??
155
+ group?.segments[0]?.color ??
156
+ group?.color ??
157
+ paletteColor(idx, cfg.palette);
158
+ return { series: s, lastRow, color };
159
+ })
160
+ .filter((p): p is { series: Series<TData>; lastRow: TData; color: string } => p !== undefined)
161
+ );
162
+
163
+ // Grows the plot's margin to fit value labels that overflow the content
164
+ // box. Resets on `resolvedSeries` (e.g. a timeline scrubber revealing a
165
+ // different slice of rows) so overflow measured against an earlier frame's
166
+ // labels doesn't linger and inflate the margin once that frame is gone —
167
+ // see `createLabelMarginTracker`'s own doc comment.
168
+ const labelMargin = createLabelMarginTracker(
169
+ () => margins,
170
+ () => resolvedSeries
171
+ );
155
172
  </script>
156
173
 
157
174
  <BasePlotLayout
158
- {width}
159
- {height}
160
- {styles}
161
- data={flat}
162
- getX={xAcc}
163
- getY={(d) => Number(yAcc(d))}
164
- {hover}
165
- {scales}
166
- {...labelMargin.plotProps}
167
- xTickRotate={scales?.x?.tickRotate}
168
- markers={[...gapMarkers, ...lineMarkers, ...markers]}
169
- seriesData={resolvedSeries}
170
- {scopedGroups}
175
+ {width}
176
+ {height}
177
+ {styles}
178
+ data={flat}
179
+ getX={xAcc}
180
+ getY={(d) => Number(yAcc(d))}
181
+ {hover}
182
+ {scales}
183
+ {...labelMargin.plotProps}
184
+ xTickRotate={scales?.x?.tickRotate}
185
+ markers={[...gapMarkers, ...lineMarkers, ...markers]}
186
+ seriesData={resolvedSeries}
187
+ {scopedGroups}
171
188
  >
172
- {#snippet children({ numericMargins })}
173
- {#if showVals}
174
- <ValueLabels
175
- {lastPoints}
176
- values={styles.values}
177
- {width}
178
- {height}
179
- {numericMargins}
180
- onOverflow={labelMargin.report}
181
- disabledFor={effectiveDisabledFor}
182
- onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
183
- onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
184
- />
185
- {/if}
186
- {/snippet}
189
+ {#snippet children({ numericMargins })}
190
+ {#if showVals}
191
+ <ValueLabels
192
+ {lastPoints}
193
+ values={styles.values}
194
+ {width}
195
+ {height}
196
+ {numericMargins}
197
+ onOverflow={labelMargin.report}
198
+ disabledFor={effectiveDisabledFor}
199
+ onHoverSeries={hoverIsolate
200
+ ? (name) => {
201
+ hoverIsolatedSeries = name;
202
+ }
203
+ : undefined}
204
+ onHoverEnd={hoverIsolate
205
+ ? () => {
206
+ hoverIsolatedSeries = null;
207
+ }
208
+ : undefined}
209
+ />
210
+ {/if}
211
+ {/snippet}
187
212
  </BasePlotLayout>