@fundar/data-chart-telling 0.0.26 → 0.0.28

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.
Files changed (58) hide show
  1. package/dist/charts/BaseChart.svelte +18 -10
  2. package/dist/charts/BaseChart.svelte.d.ts +4 -2
  3. package/dist/charts/heatmap/Chart.svelte +2 -2
  4. package/dist/charts/pyramid/Chart.svelte +2 -2
  5. package/dist/configuration/config.svelte.js +26 -0
  6. package/dist/configuration/themes/dark.js +4 -0
  7. package/dist/configuration/themes/index.d.ts +64 -0
  8. package/dist/index.d.ts +6 -2
  9. package/dist/index.js +2 -0
  10. package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -1
  11. package/dist/layout/facet/FacetIndicator.svelte +31 -0
  12. package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
  13. package/dist/layout/facet/FacetLayout.svelte +249 -24
  14. package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
  15. package/dist/layout/legend/ContinuousSection.svelte +1 -1
  16. package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
  17. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  18. package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
  19. package/dist/layout/legend/LegendLayout.svelte +1 -1
  20. package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
  21. package/dist/layout/legend/interaction.svelte.d.ts +9 -1
  22. package/dist/layout/legend/interaction.svelte.js +9 -1
  23. package/dist/layout/timeline/Timeline.svelte +1 -1
  24. package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
  25. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  26. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  27. package/dist/layout/tooltip/controller.svelte.js +16 -2
  28. package/dist/markers/HoverMarker.svelte +2 -2
  29. package/dist/plots/bar/Plot.svelte +43 -4
  30. package/dist/plots/bar/ValueLabels.svelte +69 -23
  31. package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
  32. package/dist/plots/heatmap/Plot.svelte +4 -4
  33. package/dist/plots/heatmap/ValueLabels.svelte +3 -3
  34. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
  35. package/dist/plots/line/Plot.svelte +34 -29
  36. package/dist/plots/line/ValueLabels.svelte +73 -51
  37. package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
  38. package/dist/plots/pyramid/Plot.svelte +41 -6
  39. package/dist/plots/pyramid/ValueLabels.svelte +70 -24
  40. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
  41. package/dist/plots/scatter/Plot.svelte +40 -4
  42. package/dist/plots/scatter/ValueLabels.svelte +90 -47
  43. package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
  44. package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
  45. package/dist/plots/utils/labelOverflow.svelte.js +116 -0
  46. package/dist/plots/utils/legendDisabled.d.ts +1 -1
  47. package/dist/types/charts/common.d.ts +3 -50
  48. package/dist/types/charts/props.d.ts +3 -2
  49. package/dist/types/configuration/styling.d.ts +19 -0
  50. package/dist/types/layout/facet.d.ts +41 -0
  51. package/dist/types/layout/legend.d.ts +65 -1
  52. package/dist/types/layout/styles.d.ts +1 -12
  53. package/dist/types/layout/timeline.d.ts +32 -0
  54. package/dist/types/layout/timeline.js +1 -0
  55. package/dist/utils/interpolate.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/dist/types/charts/legend.d.ts +0 -60
  58. /package/dist/types/{charts/legend.js → layout/facet.js} +0 -0
@@ -1,39 +1,77 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
2
  import { Text, usePlot } from 'svelteplot';
3
3
  import { SvelteMap } from 'svelte/reactivity';
4
+ import { resolveAccessor } from '../utils/accessors';
5
+ import { disabledFillOverride } from '../utils/legendDisabled';
6
+ import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
4
7
  import { resolveEdgeAwareAnchor, estimateTextWidth } from '../../utils/edgeAwareAnchor';
5
- import type { AxisValue } from '../../types/layout/scales';
8
+ import type { Series } from '../../types/plots/data/common';
6
9
  import type { ValuesStyle } from '../../types/layout/styles';
10
+ import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
11
+ import type { VisualGroup } from '../../types/plots/segments/config';
12
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
13
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
7
14
 
8
15
  /**
9
- * Renders every point's value label in one mark scatter has no
10
- * per-series endpoint to single out (unlike line), so this covers the
11
- * whole flat dataset at once, mirroring heatmap's own `ValueLabels`.
12
- * Labels the Y value, matching the "Y is the value axis" convention every
13
- * other plot kind uses. Placement is edge-aware: a label whose default
16
+ * Renders one series' point value labels one `<Text>` mark per series
17
+ * (mirrors line/bar/pyramid's own `ValueLabels`), so a legend-disabled or
18
+ * hover-isolated series can carry its own constant `fillOpacity`. Labels
19
+ * the Y value, matching the "Y is the value axis" convention every other
20
+ * plot kind uses. Placement is edge-aware: a label whose default
14
21
  * anchor/offset would push it past the plot's own content box flips to
15
- * the opposite side instead of clipping. Deliberately read-only against
16
- * `usePlot()`'s live scale doesn't register into svelteplot's shared
17
- * margin-auto-sizing context, which can spike CPU/memory into
18
- * unresponsiveness when many `<Plot>` instances mount/unmount together
19
- * (e.g. switching from a multi-story docs page to a single story).
22
+ * the opposite side instead of clipping `onOverflow` below is a second,
23
+ * coarser line of defense for whatever the flip still can't fit (e.g. a
24
+ * corner point with no room in any direction).
20
25
  */
21
26
  let {
22
- data,
23
- getX,
24
- getY,
25
- fmtVal,
27
+ group,
28
+ formatValue,
26
29
  values,
27
30
  defaultColor,
31
+ disabled,
32
+ onHoverSeries,
33
+ onHoverEnd,
34
+ onOverflow,
28
35
  }: {
29
- data: TData[];
30
- getX: (d: TData) => AxisValue;
31
- getY: (d: TData) => AxisValue;
32
- fmtVal: (value: number, seriesName: string) => string;
36
+ group: VisualGroup<Series<TData>, ScatterSegmentStyle>;
37
+ formatValue: (value: number, seriesName: string) => string;
33
38
  values: ValuesStyle | undefined;
34
39
  defaultColor: string;
40
+ /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
41
+ disabled?: LegendDisabledStyle;
42
+ /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
43
+ onHoverSeries?: () => void;
44
+ onHoverEnd?: () => void;
45
+ /** Reports how far this series' labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
46
+ onOverflow?: (overflow: MarginOverflow) => void;
35
47
  } = $props();
36
48
 
49
+ const data = $derived(group.series.data);
50
+ const getX = $derived(resolveAccessor(group.series.x));
51
+ const getY = $derived(resolveAccessor(group.series.y));
52
+ const plot = usePlot();
53
+ let groupEl: SVGGElement | undefined = $state();
54
+
55
+ watchLabelOverflow({
56
+ el: () => groupEl,
57
+ bounds: () => {
58
+ // getBBox() inside isn't itself tracked — depend on whatever actually
59
+ // changes the rendered label layout so this re-measures.
60
+ void data;
61
+ void values;
62
+ const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
63
+ const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
64
+ if (!xRange || !yRange) return undefined;
65
+ return {
66
+ left: Math.min(xRange[0], xRange[1]),
67
+ right: Math.max(xRange[0], xRange[1]),
68
+ top: Math.min(yRange[0], yRange[1]),
69
+ bottom: Math.max(yRange[0], yRange[1]),
70
+ };
71
+ },
72
+ onOverflow: (overflow) => onOverflow?.(overflow),
73
+ });
74
+
37
75
  const font = $derived(values?.fontStyle);
38
76
  const preferredTextAnchor = $derived(
39
77
  font?.textAnchor === 'outside' || font?.textAnchor === undefined ? 'start' : font.textAnchor,
@@ -42,11 +80,10 @@
42
80
  const gapX = $derived(Math.abs(font?.dx ?? 8));
43
81
  const gapY = $derived(Math.abs(font?.dy ?? 0));
44
82
  const fontSize = $derived(typeof font?.fontSize === 'number' ? font.fontSize : 11);
45
-
46
- const plot = usePlot();
83
+ const fillOverride = $derived(disabledFillOverride(disabled));
47
84
 
48
85
  function textOf(d: TData): string {
49
- return fmtVal(Number(getY(d)), '');
86
+ return formatValue(Number(getY(d)), group.series.name);
50
87
  }
51
88
 
52
89
  /** One placement per row, keyed by row reference — avoids recomputing the same scale/edge lookup for each of the four accessor channels below. */
@@ -79,28 +116,34 @@
79
116
  });
80
117
  </script>
81
118
 
82
- <Text
83
- {data}
84
- x={getX}
85
- y={getY}
86
- text={(d: TData) => textOf(d)}
87
- fill={font?.fill ?? defaultColor}
88
- {fontSize}
89
- fontWeight={font?.fontWeight ?? 500}
90
- fontStyle={font?.fontStyle}
91
- textAnchor={(d: TData) => placements.get(d)?.textAnchor ?? preferredTextAnchor}
92
- lineAnchor={
93
- // svelteplot's own `lineAnchor` accessor type isn't parameterized over the mark's row type like its siblings are.
94
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
- (d: any) => placements.get(d)?.lineAnchor ?? preferredLineAnchor
96
- }
97
- dx={(d: TData) => placements.get(d)?.dx ?? gapX}
98
- dy={(d: TData) => placements.get(d)?.dy ?? gapY}
99
- lineHeight={font?.lineHeight}
100
- rotate={font?.rotate}
101
- stroke={font?.stroke}
102
- strokeWidth={font?.strokeWidth}
103
- paintOrder={font?.paintOrder}
104
- class={font?.class}
105
- textClass={font?.textClass}
106
- />
119
+ <!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
120
+ despite declaring them in its types — a wrapping <g> catches the
121
+ boundary crossing instead (pointerenter/leave fire on every ancestor). -->
122
+ <g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
123
+ <Text
124
+ {data}
125
+ x={getX}
126
+ y={getY}
127
+ text={(d: TData) => textOf(d)}
128
+ fill={font?.fill ?? defaultColor}
129
+ fillOpacity={fillOverride?.fillOpacity}
130
+ {fontSize}
131
+ fontWeight={font?.fontWeight ?? 500}
132
+ fontStyle={font?.fontStyle}
133
+ textAnchor={(d: TData) => placements.get(d)?.textAnchor ?? preferredTextAnchor}
134
+ lineAnchor={
135
+ // svelteplot's own `lineAnchor` accessor type isn't parameterized over the mark's row type like its siblings are.
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ (d: any) => placements.get(d)?.lineAnchor ?? preferredLineAnchor
138
+ }
139
+ dx={(d: TData) => placements.get(d)?.dx ?? gapX}
140
+ dy={(d: TData) => placements.get(d)?.dy ?? gapY}
141
+ lineHeight={font?.lineHeight}
142
+ rotate={font?.rotate}
143
+ stroke={font?.stroke}
144
+ strokeWidth={font?.strokeWidth}
145
+ paintOrder={font?.paintOrder}
146
+ class={font?.class}
147
+ textClass={font?.textClass}
148
+ />
149
+ </g>
@@ -1,13 +1,22 @@
1
- import type { AxisValue } from '../../types/layout/scales';
1
+ import type { Series } from '../../types/plots/data/common';
2
2
  import type { ValuesStyle } from '../../types/layout/styles';
3
+ import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
4
+ import type { VisualGroup } from '../../types/plots/segments/config';
5
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
6
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
3
7
  declare function $$render<TData extends Record<string, unknown>>(): {
4
8
  props: {
5
- data: TData[];
6
- getX: (d: TData) => AxisValue;
7
- getY: (d: TData) => AxisValue;
8
- fmtVal: (value: number, seriesName: string) => string;
9
+ group: VisualGroup<Series<TData>, ScatterSegmentStyle>;
10
+ formatValue: (value: number, seriesName: string) => string;
9
11
  values: ValuesStyle | undefined;
10
12
  defaultColor: string;
13
+ /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
14
+ disabled?: LegendDisabledStyle;
15
+ /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
16
+ onHoverSeries?: () => void;
17
+ onHoverEnd?: () => void;
18
+ /** Reports how far this series' labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
19
+ onOverflow?: (overflow: MarginOverflow) => void;
11
20
  };
12
21
  exports: {};
13
22
  bindings: "";
@@ -0,0 +1,51 @@
1
+ import type { MarginConfig } from '../../types/plots/props';
2
+ /** How far a value label's rendered box extends past the plot's own content box, in pixels, on each side (0 = no overflow). */
3
+ export type MarginOverflow = {
4
+ left: number;
5
+ right: number;
6
+ top: number;
7
+ bottom: number;
8
+ };
9
+ /**
10
+ * Watches an SVG group's rendered bounding box against the plot's own
11
+ * content box (`bounds`) and reports how far it overflows on each side.
12
+ * Polled across successive paints until two consecutive readings agree,
13
+ * rather than trusted after a single frame: the group's own resolved
14
+ * geometry (driven by svelteplot's separate reactive graph) and svelteplot's
15
+ * own axis auto-margins can take a variable number of frames to settle —
16
+ * more on a page with many other components competing for layout (e.g. a
17
+ * docs page) than in an isolated story. Measuring too early can catch
18
+ * content still at its pre-layout default position; capped so a layout that
19
+ * genuinely never stabilizes still reports its last reading instead of
20
+ * polling forever. Also waits for web fonts to finish loading, since glyph
21
+ * metrics (and therefore `getBBox()`) can still reflect a fallback font's
22
+ * narrower widths until the real font swaps in.
23
+ */
24
+ export declare function watchLabelOverflow(args: {
25
+ el: () => SVGGElement | undefined;
26
+ bounds: () => {
27
+ left: number;
28
+ right: number;
29
+ top: number;
30
+ bottom: number;
31
+ } | undefined;
32
+ onOverflow: (overflow: MarginOverflow) => void;
33
+ }): void;
34
+ /**
35
+ * Tracks value-label overflow reported by (possibly several, e.g. one per
36
+ * series) {@link watchLabelOverflow} calls and grows a local margin override
37
+ * to fit it — monotonically, since shrinking it back would re-expose the
38
+ * overflow that caused the growth in the first place, oscillating between
39
+ * "shrink margin → overflow reappears → grow margin → …". Each side is only
40
+ * ever set when the caller's own `margins` prop hasn't already pinned it, so
41
+ * an explicit caller override always wins over this auto-grown one.
42
+ */
43
+ export declare function createLabelMarginTracker(margins: () => MarginConfig | undefined): {
44
+ report: (overflow: MarginOverflow) => void;
45
+ readonly resolvedMargins: Partial<{
46
+ top: number | "auto";
47
+ right: number | "auto";
48
+ bottom: number | "auto";
49
+ left: number | "auto";
50
+ }> | undefined;
51
+ };
@@ -0,0 +1,116 @@
1
+ const NO_OVERFLOW = { left: 0, right: 0, top: 0, bottom: 0 };
2
+ /**
3
+ * Watches an SVG group's rendered bounding box against the plot's own
4
+ * content box (`bounds`) and reports how far it overflows on each side.
5
+ * Polled across successive paints until two consecutive readings agree,
6
+ * rather than trusted after a single frame: the group's own resolved
7
+ * geometry (driven by svelteplot's separate reactive graph) and svelteplot's
8
+ * own axis auto-margins can take a variable number of frames to settle —
9
+ * more on a page with many other components competing for layout (e.g. a
10
+ * docs page) than in an isolated story. Measuring too early can catch
11
+ * content still at its pre-layout default position; capped so a layout that
12
+ * genuinely never stabilizes still reports its last reading instead of
13
+ * polling forever. Also waits for web fonts to finish loading, since glyph
14
+ * metrics (and therefore `getBBox()`) can still reflect a fallback font's
15
+ * narrower widths until the real font swaps in.
16
+ */
17
+ export function watchLabelOverflow(args) {
18
+ $effect(() => {
19
+ const el = args.el();
20
+ const bounds = args.bounds();
21
+ const reportOverflow = args.onOverflow;
22
+ if (!el || !bounds)
23
+ return;
24
+ const MAX_ATTEMPTS = 30;
25
+ let cancelled = false;
26
+ let frame;
27
+ let attempt = 0;
28
+ let lastReading;
29
+ function measure() {
30
+ if (cancelled)
31
+ return;
32
+ let box;
33
+ try {
34
+ box = el.getBBox();
35
+ }
36
+ catch {
37
+ return;
38
+ }
39
+ const overflow = {
40
+ left: Math.ceil(Math.max(0, bounds.left - box.x)),
41
+ right: Math.ceil(Math.max(0, box.x + box.width - bounds.right)),
42
+ top: Math.ceil(Math.max(0, bounds.top - box.y)),
43
+ bottom: Math.ceil(Math.max(0, box.y + box.height - bounds.bottom)),
44
+ };
45
+ attempt += 1;
46
+ const stable = lastReading != null &&
47
+ lastReading.left === overflow.left &&
48
+ lastReading.right === overflow.right &&
49
+ lastReading.top === overflow.top &&
50
+ lastReading.bottom === overflow.bottom;
51
+ lastReading = overflow;
52
+ if (stable || attempt >= MAX_ATTEMPTS) {
53
+ reportOverflow(overflow);
54
+ }
55
+ else {
56
+ frame = requestAnimationFrame(measure);
57
+ }
58
+ }
59
+ const fontsReady = document.fonts?.ready ?? Promise.resolve();
60
+ fontsReady.then(() => {
61
+ if (cancelled)
62
+ return;
63
+ frame = requestAnimationFrame(measure);
64
+ });
65
+ return () => {
66
+ cancelled = true;
67
+ if (frame !== undefined)
68
+ cancelAnimationFrame(frame);
69
+ };
70
+ });
71
+ }
72
+ /**
73
+ * Tracks value-label overflow reported by (possibly several, e.g. one per
74
+ * series) {@link watchLabelOverflow} calls and grows a local margin override
75
+ * to fit it — monotonically, since shrinking it back would re-expose the
76
+ * overflow that caused the growth in the first place, oscillating between
77
+ * "shrink margin → overflow reappears → grow margin → …". Each side is only
78
+ * ever set when the caller's own `margins` prop hasn't already pinned it, so
79
+ * an explicit caller override always wins over this auto-grown one.
80
+ */
81
+ export function createLabelMarginTracker(margins) {
82
+ let measured = $state(NO_OVERFLOW);
83
+ function report(overflow) {
84
+ const next = {
85
+ left: Math.max(measured.left, overflow.left),
86
+ right: Math.max(measured.right, overflow.right),
87
+ top: Math.max(measured.top, overflow.top),
88
+ bottom: Math.max(measured.bottom, overflow.bottom),
89
+ };
90
+ if (next.left !== measured.left ||
91
+ next.right !== measured.right ||
92
+ next.top !== measured.top ||
93
+ next.bottom !== measured.bottom) {
94
+ measured = next;
95
+ }
96
+ }
97
+ const resolvedMargins = $derived.by(() => {
98
+ const base = margins();
99
+ if (measured.left <= 0 && measured.right <= 0 && measured.top <= 0 && measured.bottom <= 0)
100
+ return base;
101
+ const out = { ...base };
102
+ if (measured.right > 0 && base?.right === undefined)
103
+ out.right = measured.right;
104
+ if (measured.left > 0 && base?.left === undefined)
105
+ out.left = measured.left;
106
+ if (measured.top > 0 && base?.top === undefined)
107
+ out.top = measured.top;
108
+ if (measured.bottom > 0 && base?.bottom === undefined)
109
+ out.bottom = measured.bottom;
110
+ return out;
111
+ });
112
+ return {
113
+ report,
114
+ get resolvedMargins() { return resolvedMargins; },
115
+ };
116
+ }
@@ -1,4 +1,4 @@
1
- import type { LegendDisabledStyle } from '../../types/charts/legend';
1
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
2
2
  import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
3
3
  /**
4
4
  * The three per-mark override layers a legend "disabled" state can apply,
@@ -1,66 +1,19 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { AxisValue } from '../layout/scales';
3
- import type { Accessor } from '../plots/data/common';
4
- import type { EasingName, EasingFn } from './interpolate';
5
- export type TimeValue = number;
6
- export type Orientation = 'horizontal' | 'vertical';
7
- export type FacetColumns = number | {
8
- desktop: number;
9
- mobile: number;
10
- };
2
+ import type { TimeValue } from '../layout/timeline';
3
+ import type { LegendPlotContext } from '../layout/legend';
11
4
  export type ChartBoxContext = {
12
5
  width: number;
13
6
  height: number;
14
7
  };
15
8
  export type ChartBoxSnippet = Snippet<[ChartBoxContext]>;
16
- export type FacetCellContext<TRow extends Record<string, unknown>> = {
17
- facetValue: string;
18
- data: TRow[];
19
- width: number;
20
- height: number;
21
- };
22
- export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
23
- FacetCellContext<TRow>
24
- ]>;
25
- export type TimelineChildContext<TRow extends Record<string, unknown>> = {
26
- selectedValue: TimeValue;
27
- data: TRow[];
28
- width: number;
29
- height: number;
30
- };
31
- export type TimelineChildSnippet<TRow extends Record<string, unknown>> = Snippet<[
32
- TimelineChildContext<TRow>
33
- ]>;
34
9
  export type ChartPlotContext<TRow extends Record<string, unknown>> = {
35
10
  data: TRow[];
36
11
  width: number;
37
12
  height: number;
38
13
  facetValue?: string;
39
14
  selectedValue?: TimeValue;
15
+ legend?: LegendPlotContext;
40
16
  };
41
17
  export type ChartPlotSnippet<TRow extends Record<string, unknown>> = Snippet<[
42
18
  ChartPlotContext<TRow>
43
19
  ]>;
44
- export type TimelineInterpolation<TRow extends Record<string, unknown>> = {
45
- key?: Accessor<TRow, unknown>;
46
- value?: Accessor<TRow, AxisValue>;
47
- create?: (lower: TRow, upper: TRow, x: TimeValue, value: number) => TRow;
48
- progressEasing?: EasingName | EasingFn;
49
- segmentEasing?: EasingName | EasingFn;
50
- };
51
- export type TimelineConfig<TRow extends Record<string, unknown>> = {
52
- accessor: Accessor<TRow, TimeValue>;
53
- progress: number;
54
- values?: TimeValue[];
55
- orientation?: Orientation;
56
- show?: boolean;
57
- thickness?: number;
58
- filterMode?: 'exact' | 'upTo';
59
- interpolate?: boolean | TimelineInterpolation<TRow>;
60
- };
61
- export type FacetConfig<TRow extends Record<string, unknown>> = {
62
- by: Accessor<TRow, unknown>;
63
- columns?: FacetColumns;
64
- values?: string[];
65
- gap?: number;
66
- };
@@ -5,8 +5,9 @@ import type { BasePlotStylesConfig } from '../layout/styles';
5
5
  import type { SegmentsConfig, MarginConfig } from '../plots/props';
6
6
  import type { AxisBasedMarkersConfig } from '../markers/common';
7
7
  import type { TooltipProp } from '../layout/tooltip';
8
- import type { LegendSection } from './legend';
9
- import type { FacetConfig, TimelineConfig } from './common';
8
+ import type { LegendSection } from '../layout/legend';
9
+ import type { FacetConfig } from '../layout/facet';
10
+ import type { TimelineConfig } from '../layout/timeline';
10
11
  /**
11
12
  * Common props accepted by every chart component (bar, line, heatmap, pyramid,
12
13
  * …). The `TSegmentStyle` parameter lets each chart narrow `segments` to its
@@ -118,6 +118,25 @@ export type ChartConfig = {
118
118
  font: FontStyle;
119
119
  area: AreaStyle;
120
120
  };
121
+ facet: {
122
+ /** Position badge shown in `carousel`/`paginated` facet modes. */
123
+ indicator: {
124
+ background: string;
125
+ color: string;
126
+ fontSize: string;
127
+ padding: string;
128
+ radius: string;
129
+ };
130
+ /** Prev/next buttons shown in `paginated` facet mode. */
131
+ navButton: {
132
+ background: string;
133
+ color: string;
134
+ /** Button diameter (width and height). */
135
+ size: string;
136
+ fontSize: string;
137
+ disabledOpacity: number;
138
+ };
139
+ };
121
140
  };
122
141
  /** Deep-partial input accepted by `setConfiguration`. */
123
142
  export type ChartConfigInput = DeepPartial<ChartConfig>;
@@ -0,0 +1,41 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Accessor } from '../plots/data/common';
3
+ export type Responsive<T> = T | {
4
+ desktop: T;
5
+ mobile: T;
6
+ };
7
+ export type FacetColumns = Responsive<number>;
8
+ export type FacetMode = 'grid' | 'carousel' | 'paginated';
9
+ export type FacetCellContext<TRow extends Record<string, unknown>> = {
10
+ facetValue: string;
11
+ data: TRow[];
12
+ width: number;
13
+ height: number;
14
+ };
15
+ export type FacetCellSnippet<TRow extends Record<string, unknown>> = Snippet<[
16
+ FacetCellContext<TRow>
17
+ ]>;
18
+ export type FacetIndicatorContext = {
19
+ current: number;
20
+ total: number;
21
+ };
22
+ export type FacetIndicatorSnippet = Snippet<[FacetIndicatorContext]>;
23
+ export type FacetNavDirection = 'prev' | 'next';
24
+ export type FacetNavContext = {
25
+ direction: FacetNavDirection;
26
+ disabled: boolean;
27
+ onclick: () => void;
28
+ };
29
+ export type FacetNavSnippet = Snippet<[FacetNavContext]>;
30
+ export type FacetConfig<TRow extends Record<string, unknown>> = {
31
+ by: Accessor<TRow, unknown>;
32
+ columns?: FacetColumns;
33
+ values?: string[];
34
+ gap?: number;
35
+ mode?: Responsive<FacetMode>;
36
+ visible?: number;
37
+ pageSize?: number;
38
+ formatIndicator?: (current: number, total: number) => string;
39
+ indicator?: FacetIndicatorSnippet;
40
+ navButton?: FacetNavSnippet;
41
+ };
@@ -1,5 +1,64 @@
1
1
  import type { SvelteMap } from 'svelte/reactivity';
2
- import type { LegendDisabledStyle } from '../charts/legend';
2
+ import type { SymbolType, LineStyle } from '../plots/constants';
3
+ import type { StrokeStyle, DotStyle } from '../plots/styling';
4
+ export type LegendSectionType = 'discrete' | 'continuous';
5
+ /**
6
+ * Style applied to whatever a legend interaction has turned "off" — a
7
+ * toggled-off discrete series, or a datapoint/cell/feature outside an active
8
+ * continuous range filter. `opacity` is a blanket convenience: any
9
+ * `*Opacity` field a consumer doesn't set falls back to it, so
10
+ * `{ opacity: 0.1 }` alone is enough to read as "disabled" everywhere.
11
+ */
12
+ export type LegendDisabledStyle = {
13
+ opacity?: number;
14
+ stroke?: StrokeStyle;
15
+ dots?: DotStyle;
16
+ fill?: string;
17
+ fillOpacity?: number;
18
+ };
19
+ export type LegendItem = {
20
+ name: string;
21
+ label?: string;
22
+ color?: string;
23
+ symbol?: SymbolType;
24
+ opacity?: number;
25
+ lineStyle?: LineStyle;
26
+ /** Initial toggled-off state (uncontrolled — the user can still click it back on). */
27
+ disabled?: boolean;
28
+ };
29
+ export type DiscreteLegendSection = {
30
+ type: 'discrete';
31
+ title?: string;
32
+ items: LegendItem[];
33
+ columns?: number;
34
+ /** Enables click-to-toggle on each item. */
35
+ interactive?: boolean;
36
+ /** Style applied to a toggled-off item's series. Falls back to `cfg.legend.disabledOpacity`. */
37
+ disabledStyle?: LegendDisabledStyle;
38
+ };
39
+ export type ContinuousLegendSection = {
40
+ type: 'continuous';
41
+ title?: string;
42
+ columns?: number;
43
+ min: number;
44
+ max: number;
45
+ colorMin?: string;
46
+ colorMax?: string;
47
+ ticks?: number[];
48
+ width?: number;
49
+ format?: (v: number) => string;
50
+ /** Enables drag handles on the bar's edges to narrow the visible range. */
51
+ interactive?: boolean;
52
+ /**
53
+ * How the color ramp responds to a narrowed range: `'rescale'` reprojects
54
+ * it onto the new bounds, `'filter'` keeps the original domain and only
55
+ * excludes out-of-range values. Defaults to `'filter'`.
56
+ */
57
+ mode?: 'rescale' | 'filter';
58
+ /** Style applied to values outside the active range. Falls back to `cfg.legend.disabledOpacity`. */
59
+ disabledStyle?: LegendDisabledStyle;
60
+ };
61
+ export type LegendSection = DiscreteLegendSection | ContinuousLegendSection;
3
62
  /**
4
63
  * Shared legend-interaction state for cross-cutting legend → plot wiring.
5
64
  *
@@ -24,3 +83,8 @@ export type LegendInteractionStore = {
24
83
  disabledStyle: LegendDisabledStyle;
25
84
  } | null;
26
85
  };
86
+ /** Read-only view of which series a legend has toggled off, resolved from {@link BaseChart}'s legend-interaction store. */
87
+ export type LegendPlotContext = {
88
+ disabledSeries: ReadonlySet<string>;
89
+ isDisabled: (name: string) => boolean;
90
+ };
@@ -12,20 +12,9 @@ import type { GeoFeature } from '../plots/data/geo';
12
12
  export type ValuesStyle = {
13
13
  show?: boolean;
14
14
  anchor?: ValueAnchor;
15
- /** Called with the numeric value and the series name. Return a string to render. */
16
15
  format?: (value: number, seriesName: string) => string;
17
- /** Text styling for the label itself. */
16
+ hoverIsolate?: boolean;
18
17
  fontStyle?: FontStyle;
19
- /**
20
- * Style for the leader line drawn from a label displaced to clear an
21
- * overlap with another series' label, back to its real data point.
22
- * Labels only ever move when they'd otherwise collide — this is a no-op
23
- * style until that happens. Falls back to a neutral grey, thin, solid
24
- * stroke — deliberately not the series' own colour or stroke width, since
25
- * matching the data line reads as more data rather than as a leader,
26
- * right where several lines are already converging. Unset fields keep
27
- * that fallback.
28
- */
29
18
  strokeStyle?: StrokeStyle;
30
19
  };
31
20
  /**
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { AxisValue } from './scales';
3
+ import type { Accessor } from '../plots/data/common';
4
+ import type { EasingName, EasingFn } from '../charts/interpolate';
5
+ export type TimeValue = number;
6
+ export type Orientation = 'horizontal' | 'vertical';
7
+ export type TimelineChildContext<TRow extends Record<string, unknown>> = {
8
+ selectedValue: TimeValue;
9
+ data: TRow[];
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type TimelineChildSnippet<TRow extends Record<string, unknown>> = Snippet<[
14
+ TimelineChildContext<TRow>
15
+ ]>;
16
+ export type TimelineInterpolation<TRow extends Record<string, unknown>> = {
17
+ key?: Accessor<TRow, unknown>;
18
+ value?: Accessor<TRow, AxisValue>;
19
+ create?: (lower: TRow, upper: TRow, x: TimeValue, value: number) => TRow;
20
+ progressEasing?: EasingName | EasingFn;
21
+ segmentEasing?: EasingName | EasingFn;
22
+ };
23
+ export type TimelineConfig<TRow extends Record<string, unknown>> = {
24
+ accessor: Accessor<TRow, TimeValue>;
25
+ progress: number;
26
+ values?: TimeValue[];
27
+ orientation?: Orientation;
28
+ show?: boolean;
29
+ thickness?: number;
30
+ filterMode?: 'exact' | 'upTo';
31
+ interpolate?: boolean | TimelineInterpolation<TRow>;
32
+ };
@@ -0,0 +1 @@
1
+ export {};