@fundar/data-chart-telling 0.0.39 → 0.0.41

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,232 @@
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, MarginConfig } 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 = $bindable(),
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
+ );
172
+
173
+ // Writes the resolved margin back into the bindable `margins` prop once it
174
+ // settles, so a caller binding it (`bind:margins`) can capture and reuse a
175
+ // fixed margin later. `lastWritten` is a plain variable (not `$state`) so
176
+ // the effect only depends on `resolvedMargins`, never on `margins` itself.
177
+ let lastWritten: MarginConfig | undefined;
178
+ $effect(() => {
179
+ const next = labelMargin.resolvedMargins;
180
+ if (!next) return;
181
+ const changed =
182
+ !lastWritten ||
183
+ next.top !== lastWritten.top ||
184
+ next.right !== lastWritten.right ||
185
+ next.bottom !== lastWritten.bottom ||
186
+ next.left !== lastWritten.left;
187
+ if (changed) {
188
+ lastWritten = { top: next.top, right: next.right, bottom: next.bottom, left: next.left };
189
+ margins = next;
190
+ }
191
+ });
155
192
  </script>
156
193
 
157
194
  <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}
195
+ {width}
196
+ {height}
197
+ {styles}
198
+ data={flat}
199
+ getX={xAcc}
200
+ getY={(d) => Number(yAcc(d))}
201
+ {hover}
202
+ {scales}
203
+ {...labelMargin.plotProps}
204
+ xTickRotate={scales?.x?.tickRotate}
205
+ markers={[...gapMarkers, ...lineMarkers, ...markers]}
206
+ seriesData={resolvedSeries}
207
+ {scopedGroups}
171
208
  >
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}
209
+ {#snippet children({ numericMargins })}
210
+ {#if showVals}
211
+ <ValueLabels
212
+ {lastPoints}
213
+ values={styles.values}
214
+ {width}
215
+ {height}
216
+ {numericMargins}
217
+ onOverflow={labelMargin.report}
218
+ disabledFor={effectiveDisabledFor}
219
+ onHoverSeries={hoverIsolate
220
+ ? (name) => {
221
+ hoverIsolatedSeries = name;
222
+ }
223
+ : undefined}
224
+ onHoverEnd={hoverIsolate
225
+ ? () => {
226
+ hoverIsolatedSeries = null;
227
+ }
228
+ : undefined}
229
+ />
230
+ {/if}
231
+ {/snippet}
187
232
  </BasePlotLayout>
@@ -7,7 +7,7 @@ import type { LineMarkersConfig } from '../../types/markers/line';
7
7
  declare function $$render<TData extends Record<string, unknown>>(): {
8
8
  props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, GapsAwarePlotStylesConfig<LineSegmentStyle>, LineSegmentStyle, LineMarkersConfig, AxisBasedScalesConfig<TData>>;
9
9
  exports: {};
10
- bindings: "";
10
+ bindings: "margins";
11
11
  slots: {};
12
12
  events: {};
13
13
  };
@@ -15,7 +15,7 @@ declare class __sveltets_Render<TData extends Record<string, unknown>> {
15
15
  props(): ReturnType<typeof $$render<TData>>['props'];
16
16
  events(): ReturnType<typeof $$render<TData>>['events'];
17
17
  slots(): ReturnType<typeof $$render<TData>>['slots'];
18
- bindings(): "";
18
+ bindings(): "margins";
19
19
  exports(): {};
20
20
  }
21
21
  interface $$IsomorphicComponent {