@fundar/data-chart-telling 0.0.1

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 (106) hide show
  1. package/README.md +250 -0
  2. package/dist/charts/Chart.svelte +125 -0
  3. package/dist/charts/Chart.svelte.d.ts +42 -0
  4. package/dist/charts/bar/Chart.svelte +104 -0
  5. package/dist/charts/bar/Chart.svelte.d.ts +27 -0
  6. package/dist/charts/container/ChartContainer.svelte +127 -0
  7. package/dist/charts/container/ChartContainer.svelte.d.ts +14 -0
  8. package/dist/charts/heatmap/Chart.svelte +107 -0
  9. package/dist/charts/heatmap/Chart.svelte.d.ts +30 -0
  10. package/dist/charts/line/Chart.svelte +106 -0
  11. package/dist/charts/line/Chart.svelte.d.ts +27 -0
  12. package/dist/charts/pyramid/Chart.svelte +127 -0
  13. package/dist/charts/pyramid/Chart.svelte.d.ts +31 -0
  14. package/dist/configuration/config.svelte.d.ts +12 -0
  15. package/dist/configuration/config.svelte.js +159 -0
  16. package/dist/configuration/themes/compact.d.ts +2 -0
  17. package/dist/configuration/themes/compact.js +9 -0
  18. package/dist/configuration/themes/dark.d.ts +2 -0
  19. package/dist/configuration/themes/dark.js +17 -0
  20. package/dist/configuration/themes/default.d.ts +3 -0
  21. package/dist/configuration/themes/default.js +2 -0
  22. package/dist/configuration/themes/editorial.d.ts +2 -0
  23. package/dist/configuration/themes/editorial.js +14 -0
  24. package/dist/configuration/themes/index.d.ts +481 -0
  25. package/dist/configuration/themes/index.js +16 -0
  26. package/dist/index.d.ts +32 -0
  27. package/dist/index.js +27 -0
  28. package/dist/layout/facet/FacetLayout.svelte +97 -0
  29. package/dist/layout/facet/FacetLayout.svelte.d.ts +35 -0
  30. package/dist/layout/legend/ContinuousSection.svelte +95 -0
  31. package/dist/layout/legend/ContinuousSection.svelte.d.ts +7 -0
  32. package/dist/layout/legend/DiscreteSection.svelte +87 -0
  33. package/dist/layout/legend/DiscreteSection.svelte.d.ts +7 -0
  34. package/dist/layout/legend/LegendLayout.svelte +47 -0
  35. package/dist/layout/legend/LegendLayout.svelte.d.ts +7 -0
  36. package/dist/layout/plot/PlotLayout.svelte +236 -0
  37. package/dist/layout/plot/PlotLayout.svelte.d.ts +70 -0
  38. package/dist/layout/timeline/Timeline.svelte +86 -0
  39. package/dist/layout/timeline/Timeline.svelte.d.ts +11 -0
  40. package/dist/layout/timeline/TimelineLayout.svelte +181 -0
  41. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +38 -0
  42. package/dist/layout/tooltip/Tooltip.svelte +55 -0
  43. package/dist/layout/tooltip/Tooltip.svelte.d.ts +12 -0
  44. package/dist/layout/tooltip/controller.svelte.d.ts +52 -0
  45. package/dist/layout/tooltip/controller.svelte.js +128 -0
  46. package/dist/layout/tooltip/hover.svelte.d.ts +7 -0
  47. package/dist/layout/tooltip/hover.svelte.js +28 -0
  48. package/dist/layout/tooltip/utils.d.ts +41 -0
  49. package/dist/layout/tooltip/utils.js +78 -0
  50. package/dist/plots/bar/Plot.svelte +176 -0
  51. package/dist/plots/bar/Plot.svelte.d.ts +26 -0
  52. package/dist/plots/heatmap/Plot.svelte +175 -0
  53. package/dist/plots/heatmap/Plot.svelte.d.ts +26 -0
  54. package/dist/plots/line/Plot.svelte +232 -0
  55. package/dist/plots/line/Plot.svelte.d.ts +26 -0
  56. package/dist/plots/markers/DeltaMarker.svelte +104 -0
  57. package/dist/plots/markers/DeltaMarker.svelte.d.ts +30 -0
  58. package/dist/plots/markers/DotMarker.svelte +41 -0
  59. package/dist/plots/markers/DotMarker.svelte.d.ts +31 -0
  60. package/dist/plots/markers/HoverMarker.svelte +78 -0
  61. package/dist/plots/markers/HoverMarker.svelte.d.ts +22 -0
  62. package/dist/plots/markers/PeakMarker.svelte +80 -0
  63. package/dist/plots/markers/PeakMarker.svelte.d.ts +13 -0
  64. package/dist/plots/markers/TextMarker.svelte +55 -0
  65. package/dist/plots/markers/TextMarker.svelte.d.ts +32 -0
  66. package/dist/plots/pyramid/Plot.svelte +282 -0
  67. package/dist/plots/pyramid/Plot.svelte.d.ts +26 -0
  68. package/dist/plots/utils/accessors.d.ts +6 -0
  69. package/dist/plots/utils/accessors.js +10 -0
  70. package/dist/plots/utils/delta.d.ts +12 -0
  71. package/dist/plots/utils/delta.js +69 -0
  72. package/dist/plots/utils/segments.d.ts +33 -0
  73. package/dist/plots/utils/segments.js +153 -0
  74. package/dist/stores/device.d.ts +7 -0
  75. package/dist/stores/device.js +39 -0
  76. package/dist/types/charts/common.d.ts +64 -0
  77. package/dist/types/charts/common.js +1 -0
  78. package/dist/types/charts/interpolate.d.ts +2 -0
  79. package/dist/types/charts/interpolate.js +1 -0
  80. package/dist/types/charts/legend.d.ts +29 -0
  81. package/dist/types/charts/legend.js +1 -0
  82. package/dist/types/charts/props.d.ts +35 -0
  83. package/dist/types/charts/props.js +1 -0
  84. package/dist/types/configuration/styling.d.ts +97 -0
  85. package/dist/types/configuration/styling.js +1 -0
  86. package/dist/types/layout/tooltip.d.ts +88 -0
  87. package/dist/types/layout/tooltip.js +1 -0
  88. package/dist/types/plots/common.d.ts +100 -0
  89. package/dist/types/plots/common.js +1 -0
  90. package/dist/types/plots/delta.d.ts +19 -0
  91. package/dist/types/plots/delta.js +1 -0
  92. package/dist/types/plots/markers.d.ts +56 -0
  93. package/dist/types/plots/markers.js +1 -0
  94. package/dist/types/plots/props.d.ts +23 -0
  95. package/dist/types/plots/props.js +1 -0
  96. package/dist/types/plots/styles.d.ts +176 -0
  97. package/dist/types/plots/styles.js +1 -0
  98. package/dist/types/viewport.d.ts +4 -0
  99. package/dist/types/viewport.js +1 -0
  100. package/dist/utils/color.d.ts +5 -0
  101. package/dist/utils/color.js +42 -0
  102. package/dist/utils/grouping.d.ts +14 -0
  103. package/dist/utils/grouping.js +36 -0
  104. package/dist/utils/interpolate.d.ts +30 -0
  105. package/dist/utils/interpolate.js +71 -0
  106. package/package.json +76 -0
@@ -0,0 +1,11 @@
1
+ import type { Orientation, TimeValue } from '../../types/charts/common';
2
+ type $$ComponentProps = {
3
+ currentValue: TimeValue;
4
+ values: TimeValue[];
5
+ orientation?: Orientation;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ declare const Timeline: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Timeline = ReturnType<typeof Timeline>;
11
+ export default Timeline;
@@ -0,0 +1,181 @@
1
+ <script lang="ts" generics="TRow extends Record<string, unknown>">
2
+ import { resolveAccessor } from '../../plots/utils/accessors';
3
+ import Timeline from './Timeline.svelte';
4
+ import { resolveEasing, interpolateSeries } from '../../utils/interpolate';
5
+ import type {
6
+ Orientation,
7
+ TimeValue,
8
+ TimelineChildSnippet,
9
+ TimelineInterpolation,
10
+ } from '../../types/charts/common';
11
+ import type { Accessor } from '../../types/plots/common';
12
+
13
+ /**
14
+ * Couples a scrubber `progress` to a discrete sequence of time steps. It picks
15
+ * the selected step, filters the data down to it, splits the available box
16
+ * between the plot and the timeline gutter, and renders both. The plot itself
17
+ * is supplied as the `child` snippet, so this layout is plot-agnostic.
18
+ */
19
+ let {
20
+ data,
21
+ accessor,
22
+ progress,
23
+ values,
24
+ orientation = 'vertical',
25
+ thickness = 60,
26
+ filterMode = 'exact',
27
+ interpolate,
28
+ width,
29
+ height,
30
+ child,
31
+ }: {
32
+ data: TRow[];
33
+ accessor: Accessor<TRow, TimeValue>;
34
+ progress: number;
35
+ values?: TimeValue[];
36
+ orientation?: Orientation;
37
+ thickness?: number;
38
+ filterMode?: 'exact' | 'upTo';
39
+ interpolate?: TimelineInterpolation<TRow>;
40
+ width: number;
41
+ height: number;
42
+ child: TimelineChildSnippet<TRow>;
43
+ } = $props();
44
+
45
+ const getTime = $derived(resolveAccessor<TRow>(accessor));
46
+
47
+ const steps = $derived.by(() => {
48
+ if (values?.length) return [...values].sort((a, b) => a - b);
49
+ return [...new Set(data.map(getTime))].sort((a, b) => a - b);
50
+ });
51
+
52
+ const progressEasingFn = $derived(resolveEasing(interpolate?.progressEasing));
53
+ const segmentEasingFn = $derived(resolveEasing(interpolate?.segmentEasing));
54
+
55
+ /** Continuous scrub position across the full step domain — only meaningful when `interpolate` is set. */
56
+ const currentTime = $derived.by(() => {
57
+ if (!steps.length) return 0;
58
+ const t0 = steps[0];
59
+ const t1 = steps[steps.length - 1];
60
+ return t0 + (t1 - t0) * progressEasingFn(progress);
61
+ });
62
+
63
+ const selectedValue = $derived.by(() => {
64
+ if (!steps.length) return 0;
65
+ if (interpolate) {
66
+ return steps.reduce((best, s) =>
67
+ Math.abs(s - currentTime) < Math.abs(best - currentTime) ? s : best,
68
+ );
69
+ }
70
+ const idx = Math.min(
71
+ steps.length - 1,
72
+ Math.max(0, Math.round(progress * (steps.length - 1))),
73
+ );
74
+ return steps[idx];
75
+ });
76
+
77
+ function defaultCreate(_lower: TRow, upper: TRow, x: TimeValue, value: number): TRow {
78
+ const valueField = interpolate?.value;
79
+ if (typeof accessor !== 'string' || typeof valueField !== 'string') {
80
+ console.warn(
81
+ 'TimelineLayout: cannot auto-build an interpolated row when `accessor` or `interpolate.value` is a function — pass `interpolate.create` explicitly.',
82
+ );
83
+ return upper;
84
+ }
85
+ return { ...upper, [accessor]: x, [valueField]: value } as TRow;
86
+ }
87
+
88
+ const filtered = $derived.by(() => {
89
+ const discrete = () =>
90
+ filterMode === 'upTo'
91
+ ? data.filter((d) => getTime(d) <= selectedValue)
92
+ : data.filter((d) => getTime(d) === selectedValue);
93
+
94
+ if (!interpolate) return discrete();
95
+ if (!interpolate.value) {
96
+ console.warn('TimelineLayout: `interpolate.value` is required to tween — falling back to discrete filtering.');
97
+ return discrete();
98
+ }
99
+
100
+ const getKey = resolveAccessor<TRow>(interpolate.key ?? (() => '__all__'));
101
+ const getValue = resolveAccessor<TRow>(interpolate.value) as (d: TRow) => number;
102
+ const create = interpolate.create ?? defaultCreate;
103
+
104
+ const groups = new Map<string, TRow[]>();
105
+ for (const row of data) {
106
+ const key = String(getKey(row));
107
+ const bucket = groups.get(key);
108
+ if (bucket) bucket.push(row);
109
+ else groups.set(key, [row]);
110
+ }
111
+
112
+ const result: TRow[] = [];
113
+ for (const rows of groups.values()) {
114
+ const tweened = interpolateSeries({
115
+ rows,
116
+ x: currentTime,
117
+ getTime,
118
+ getValue,
119
+ create,
120
+ segmentEasing: segmentEasingFn,
121
+ });
122
+ if (filterMode === 'upTo') result.push(...tweened);
123
+ else if (tweened.length) result.push(tweened[tweened.length - 1]);
124
+ }
125
+ return result;
126
+ });
127
+
128
+ const isVertical = $derived(orientation === 'vertical');
129
+ const gap = 12;
130
+ const innerWidth = $derived(
131
+ Math.max(0, isVertical ? width - thickness - gap : width),
132
+ );
133
+ const innerHeight = $derived(
134
+ Math.max(0, isVertical ? height : height - thickness - gap),
135
+ );
136
+ </script>
137
+
138
+ {#if isVertical}
139
+ <div class="dct-timeline dct-timeline--vertical" style:width="{width}px" style:height="{height}px">
140
+ <div class="dct-timeline__plot">
141
+ {@render child({ selectedValue, data: filtered, width: innerWidth, height: innerHeight })}
142
+ </div>
143
+ <div class="dct-timeline__gutter" style:width="{thickness}px">
144
+ <Timeline currentValue={selectedValue} values={steps} orientation="vertical" width={thickness} height={innerHeight} />
145
+ </div>
146
+ </div>
147
+ {:else}
148
+ <div class="dct-timeline dct-timeline--horizontal" style:width="{width}px" style:height="{height}px">
149
+ <div class="dct-timeline__plot">
150
+ {@render child({ selectedValue, data: filtered, width: innerWidth, height: innerHeight })}
151
+ </div>
152
+ <div class="dct-timeline__gutter" style:height="{thickness}px">
153
+ <Timeline currentValue={selectedValue} values={steps} orientation="horizontal" width={innerWidth} height={thickness} />
154
+ </div>
155
+ </div>
156
+ {/if}
157
+
158
+ <style>
159
+ .dct-timeline {
160
+ display: flex;
161
+ }
162
+ .dct-timeline--vertical {
163
+ flex-direction: row;
164
+ gap: 12px;
165
+ }
166
+ .dct-timeline--horizontal {
167
+ flex-direction: column;
168
+ gap: 12px;
169
+ }
170
+ .dct-timeline__plot {
171
+ flex: 1 1 auto;
172
+ min-width: 0;
173
+ min-height: 0;
174
+ }
175
+ .dct-timeline__gutter {
176
+ flex: 0 0 auto;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+ </style>
@@ -0,0 +1,38 @@
1
+ import type { Orientation, TimeValue, TimelineChildSnippet, TimelineInterpolation } from '../../types/charts/common';
2
+ import type { Accessor } from '../../types/plots/common';
3
+ declare function $$render<TRow extends Record<string, unknown>>(): {
4
+ props: {
5
+ data: TRow[];
6
+ accessor: Accessor<TRow, TimeValue>;
7
+ progress: number;
8
+ values?: TimeValue[];
9
+ orientation?: Orientation;
10
+ thickness?: number;
11
+ filterMode?: "exact" | "upTo";
12
+ interpolate?: TimelineInterpolation<TRow>;
13
+ width: number;
14
+ height: number;
15
+ child: TimelineChildSnippet<TRow>;
16
+ };
17
+ exports: {};
18
+ bindings: "";
19
+ slots: {};
20
+ events: {};
21
+ };
22
+ declare class __sveltets_Render<TRow extends Record<string, unknown>> {
23
+ props(): ReturnType<typeof $$render<TRow>>['props'];
24
+ events(): ReturnType<typeof $$render<TRow>>['events'];
25
+ slots(): ReturnType<typeof $$render<TRow>>['slots'];
26
+ bindings(): "";
27
+ exports(): {};
28
+ }
29
+ interface $$IsomorphicComponent {
30
+ new <TRow extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TRow>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TRow>['props']>, ReturnType<__sveltets_Render<TRow>['events']>, ReturnType<__sveltets_Render<TRow>['slots']>> & {
31
+ $$bindings?: ReturnType<__sveltets_Render<TRow>['bindings']>;
32
+ } & ReturnType<__sveltets_Render<TRow>['exports']>;
33
+ <TRow extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TRow>['props']> & {}): ReturnType<__sveltets_Render<TRow>['exports']>;
34
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
35
+ }
36
+ declare const TimelineLayout: $$IsomorphicComponent;
37
+ type TimelineLayout<TRow extends Record<string, unknown>> = InstanceType<typeof TimelineLayout<TRow>>;
38
+ export default TimelineLayout;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * Presentational tooltip positioned at `(x, y)` within a `position: relative`
6
+ * parent. It is styled entirely from the `--dct-tooltip-*` variables that
7
+ * {@link ChartContainer} stamps from the config, so it themes for free. Plots
8
+ * that implement hovering can render this with their own content snippet.
9
+ */
10
+ let {
11
+ x,
12
+ y,
13
+ visible = true,
14
+ background,
15
+ color,
16
+ children,
17
+ }: {
18
+ x: number;
19
+ y: number;
20
+ visible?: boolean;
21
+ background?: string;
22
+ color?: string;
23
+ children: Snippet;
24
+ } = $props();
25
+ </script>
26
+
27
+ {#if visible}
28
+ <div
29
+ class="dct-tooltip"
30
+ style:left="{x}px"
31
+ style:top="{y}px"
32
+ style:background={background}
33
+ style:color={color}
34
+ >
35
+ {@render children()}
36
+ </div>
37
+ {/if}
38
+
39
+ <style>
40
+ .dct-tooltip {
41
+ position: absolute;
42
+ z-index: 10;
43
+ transform: translate(-50%, -110%);
44
+ pointer-events: none;
45
+ background: var(--dct-tooltip-bg, #111827);
46
+ color: var(--dct-tooltip-color, #fff);
47
+ font-size: var(--dct-tooltip-size, 0.8125rem);
48
+ padding: var(--dct-tooltip-pad, 0.5rem 0.75rem);
49
+ border-radius: var(--dct-tooltip-radius, 0.375rem);
50
+ border: var(--dct-tooltip-border, none);
51
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
52
+ white-space: pre-line;
53
+ line-height: 1.35;
54
+ }
55
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ x: number;
4
+ y: number;
5
+ visible?: boolean;
6
+ background?: string;
7
+ color?: string;
8
+ children: Snippet;
9
+ };
10
+ declare const Tooltip: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Tooltip = ReturnType<typeof Tooltip>;
12
+ export default Tooltip;
@@ -0,0 +1,52 @@
1
+ import type { HoverStore, HoverPoint } from '../../types/layout/tooltip';
2
+ import type { AxisValue } from '../../types/plots/common';
3
+ /**
4
+ * The plot-agnostic hover state machine shared by every plot. Given reactive
5
+ * accessors for the current hover store, this facet's id, and the local points,
6
+ * it produces everything a plot needs to render: the highlight points, the
7
+ * crosshair rule positions, and the floating-tooltip state — plus the event
8
+ * handlers that feed (and broadcast to) the shared store.
9
+ *
10
+ * Kept generic so line / bar / heatmap reuse it unchanged; only the points and
11
+ * accessors they pass in differ.
12
+ */
13
+ export declare function createHoverController<T extends Record<string, unknown>>(args: {
14
+ hover: () => HoverStore;
15
+ facetId: () => string;
16
+ points: () => HoverPoint<T>[];
17
+ getX: () => (d: T) => AxisValue;
18
+ getY: () => (d: T) => AxisValue;
19
+ /** Short label drawn next to the highlight dot (e.g. the value). */
20
+ label: () => (row: T) => string;
21
+ format: () => (row: T) => string;
22
+ /** Named subset of series the tooltip text/content should be limited to. */
23
+ seriesFilter?: () => string[] | undefined;
24
+ /** This facet's own box, used to convert the cursor to/from a sync-broadcast fraction. */
25
+ width: () => number;
26
+ height: () => number;
27
+ }): {
28
+ onPointer: (data: T[]) => void;
29
+ onMove: (e: PointerEvent) => void;
30
+ onLeave: () => void;
31
+ readonly points: {
32
+ x: AxisValue;
33
+ y: AxisValue;
34
+ label: string;
35
+ series: string | undefined;
36
+ }[];
37
+ readonly matchedRows: HoverPoint<T>[];
38
+ readonly ruleX: AxisValue | null;
39
+ readonly ruleY: AxisValue | null;
40
+ readonly impliesRuleX: boolean;
41
+ readonly impliesRuleY: boolean;
42
+ readonly showTooltip: boolean;
43
+ readonly tooltipText: string;
44
+ readonly tooltipPosition: {
45
+ x: number;
46
+ y: number;
47
+ } | null;
48
+ readonly cursor: {
49
+ x: number;
50
+ y: number;
51
+ } | null;
52
+ };
@@ -0,0 +1,128 @@
1
+ import { eq, matchByStrategy, impliesRuleX, impliesRuleY, filterBySeries, } from './utils';
2
+ /**
3
+ * The plot-agnostic hover state machine shared by every plot. Given reactive
4
+ * accessors for the current hover store, this facet's id, and the local points,
5
+ * it produces everything a plot needs to render: the highlight points, the
6
+ * crosshair rule positions, and the floating-tooltip state — plus the event
7
+ * handlers that feed (and broadcast to) the shared store.
8
+ *
9
+ * Kept generic so line / bar / heatmap reuse it unchanged; only the points and
10
+ * accessors they pass in differ.
11
+ */
12
+ export function createHoverController(args) {
13
+ let cursor = $state(null);
14
+ // svelteplot's `Pointer` hands back cloned rows (not the original
15
+ // references), so a row's series can't be looked up by identity — find it
16
+ // by value against the locally-known, already-tagged points instead.
17
+ const seriesForRow = (row) => {
18
+ const x = args.getX()(row);
19
+ const y = args.getY()(row);
20
+ return args.points().find((p) => eq(p.x, x) && eq(p.y, y))?.series;
21
+ };
22
+ // Points matched in THIS facet. Non-source facets only react when sync is
23
+ // enabled. The strategy in the shared store determines how points are found.
24
+ const matched = $derived.by(() => {
25
+ const h = args.hover();
26
+ const isSource = h.source === args.facetId();
27
+ if (!isSource && !h.sync)
28
+ return [];
29
+ return matchByStrategy(args.points(), h);
30
+ });
31
+ const matchedRows = $derived(filterBySeries(matched, args.seriesFilter?.()));
32
+ // Points highlighted in THIS facet for the in-SVG marker — no series
33
+ // filtering, since that's the tooltip text's concern, not the marker's.
34
+ const highlight = $derived(matched.map(({ x, y, label, series }) => ({ x, y, label, series })));
35
+ // Raw hover position — available whenever there are matched points.
36
+ // PlotLayout gates actual display with impliesRuleX/Y + the marker's ruleX/Y props.
37
+ const ruleX = $derived(matched.length > 0 ? args.hover().activeX : null);
38
+ const ruleY = $derived(matched.length > 0 ? args.hover().activeY : null);
39
+ // Whether the current strategy implies a crosshair rule on each axis by default.
40
+ const _impliesRuleX = $derived(impliesRuleX(args.hover().strategy));
41
+ const _impliesRuleY = $derived(impliesRuleY(args.hover().strategy));
42
+ const showTooltip = $derived.by(() => {
43
+ if (matchedRows.length === 0)
44
+ return false;
45
+ const h = args.hover();
46
+ if (h.source === args.facetId())
47
+ return cursor != null;
48
+ return h.sync;
49
+ });
50
+ const tooltipText = $derived(matchedRows.length ? matchedRows.map((p) => args.format()(p.row)).join('\n\n') : '');
51
+ function onPointer(data) {
52
+ const h = args.hover();
53
+ if (data.length) {
54
+ h.activeX = args.getX()(data[0]);
55
+ h.activeY = args.getY()(data[0]);
56
+ h.activeSeries = seriesForRow(data[0]) ?? null;
57
+ h.source = args.facetId();
58
+ }
59
+ else if (h.source === args.facetId()) {
60
+ h.activeX = null;
61
+ h.activeY = null;
62
+ h.activeSeries = null;
63
+ h.source = null;
64
+ }
65
+ }
66
+ function onMove(e) {
67
+ const rect = e.currentTarget.getBoundingClientRect();
68
+ const x = e.clientX - rect.left;
69
+ const y = e.clientY - rect.top;
70
+ cursor = { x, y };
71
+ const w = args.width();
72
+ const h = args.height();
73
+ if (w > 0 && h > 0)
74
+ args.hover().cursorFrac = { x: x / w, y: y / h };
75
+ }
76
+ function onLeave() {
77
+ cursor = null;
78
+ const h = args.hover();
79
+ if (h.source === args.facetId()) {
80
+ h.activeX = null;
81
+ h.activeY = null;
82
+ h.activeSeries = null;
83
+ h.cursorFrac = null;
84
+ h.source = null;
85
+ }
86
+ }
87
+ const tooltipPosition = $derived.by(() => {
88
+ if (cursor)
89
+ return cursor;
90
+ const frac = args.hover().cursorFrac;
91
+ return frac ? { x: frac.x * args.width(), y: frac.y * args.height() } : null;
92
+ });
93
+ return {
94
+ onPointer,
95
+ onMove,
96
+ onLeave,
97
+ get points() {
98
+ return highlight;
99
+ },
100
+ get matchedRows() {
101
+ return matchedRows;
102
+ },
103
+ get ruleX() {
104
+ return ruleX;
105
+ },
106
+ get ruleY() {
107
+ return ruleY;
108
+ },
109
+ get impliesRuleX() {
110
+ return _impliesRuleX;
111
+ },
112
+ get impliesRuleY() {
113
+ return _impliesRuleY;
114
+ },
115
+ get showTooltip() {
116
+ return showTooltip;
117
+ },
118
+ get tooltipText() {
119
+ return tooltipText;
120
+ },
121
+ get tooltipPosition() {
122
+ return tooltipPosition;
123
+ },
124
+ get cursor() {
125
+ return cursor;
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,7 @@
1
+ import type { HoverStore } from '../../types/layout/tooltip';
2
+ /** Create + register the shared hover store. Call during component init. */
3
+ export declare function provideHover(): HoverStore;
4
+ /** Read the shared hover store, or `null` when there is no provider. */
5
+ export declare function getHover(): HoverStore | null;
6
+ /** A private hover store for a plot used outside of a {@link Chart}. */
7
+ export declare function createHover(): HoverStore;
@@ -0,0 +1,28 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const HOVER_KEY = Symbol('dct-hover');
3
+ function blank() {
4
+ return {
5
+ activeX: null,
6
+ activeY: null,
7
+ activeSeries: null,
8
+ cursorFrac: null,
9
+ source: null,
10
+ strategy: 'x',
11
+ sync: false,
12
+ };
13
+ }
14
+ /** Create + register the shared hover store. Call during component init. */
15
+ export function provideHover() {
16
+ const store = $state(blank());
17
+ setContext(HOVER_KEY, store);
18
+ return store;
19
+ }
20
+ /** Read the shared hover store, or `null` when there is no provider. */
21
+ export function getHover() {
22
+ return getContext(HOVER_KEY) ?? null;
23
+ }
24
+ /** A private hover store for a plot used outside of a {@link Chart}. */
25
+ export function createHover() {
26
+ const store = $state(blank());
27
+ return store;
28
+ }
@@ -0,0 +1,41 @@
1
+ import type { AxisValue } from '../../types/plots/common';
2
+ import type { Marker, HoverMarkerConfig } from '../../types/plots/markers';
3
+ import type { HoverStore, HoverStrategy, HoverPoint, TooltipProp, TooltipOptions } from '../../types/layout/tooltip';
4
+ /** Value equality that also handles `Date` (compared by timestamp). */
5
+ export declare function eq(a: AxisValue, b: AxisValue): boolean;
6
+ /**
7
+ * Given the shared hover state, return the local points that should be
8
+ * highlighted using the store's current strategy.
9
+ */
10
+ export declare function matchByStrategy<T>(points: HoverPoint<T>[], hover: HoverStore): HoverPoint<T>[];
11
+ /** Whether the strategy implies an x crosshair rule by default. */
12
+ export declare function impliesRuleX(strategy: HoverStrategy): boolean;
13
+ /** Whether the strategy implies a y crosshair rule by default. */
14
+ export declare function impliesRuleY(strategy: HoverStrategy): boolean;
15
+ /** Normalize the author-facing prop into a resolved object. */
16
+ export declare function resolveTooltip<TRow extends Record<string, unknown>>(tooltip: TooltipProp<TRow> | undefined, defaultStrategy: HoverStrategy): {
17
+ enabled: boolean;
18
+ strategy: HoverStrategy;
19
+ sync: boolean;
20
+ format?: (row: TRow) => string;
21
+ series?: string[];
22
+ content?: TooltipOptions<TRow>['content'];
23
+ };
24
+ /**
25
+ * Resolve the strategy that should drive the shared hover store:
26
+ * the tooltip's own `strategy` wins when tooltip text is enabled, else the first
27
+ * hover marker's `strategy`, else the chart kind's default.
28
+ */
29
+ export declare function resolveHoverStrategy(tooltipStrategy: HoverStrategy | undefined, markers: Marker[] | undefined, defaultStrategy: HoverStrategy): HoverStrategy;
30
+ /**
31
+ * Resolve whether the hover should be broadcast across facets:
32
+ * the tooltip's own `sync` wins when tooltip text is enabled, else the first
33
+ * hover marker's `sync`, else false.
34
+ */
35
+ export declare function resolveHoverSync(tooltipSync: boolean | undefined, markers: Marker[] | undefined): boolean;
36
+ /** Whether any marker in the list is a hover marker. */
37
+ export declare function hasHoverMarker(markers: Marker[] | undefined): boolean;
38
+ /** First hover marker in the list, if any. */
39
+ export declare function findHoverMarker(markers: Marker[] | undefined): HoverMarkerConfig | undefined;
40
+ /** Narrow matched points down to a named subset of series (no-op if `series` is empty/undefined). */
41
+ export declare function filterBySeries<T>(points: HoverPoint<T>[], series?: string[]): HoverPoint<T>[];
@@ -0,0 +1,78 @@
1
+ /** Value equality that also handles `Date` (compared by timestamp). */
2
+ export function eq(a, b) {
3
+ if (a instanceof Date && b instanceof Date)
4
+ return +a === +b;
5
+ return a === b;
6
+ }
7
+ /**
8
+ * Given the shared hover state, return the local points that should be
9
+ * highlighted using the store's current strategy.
10
+ */
11
+ export function matchByStrategy(points, hover) {
12
+ const { strategy, activeX, activeY, activeSeries } = hover;
13
+ if (strategy === 'x') {
14
+ return activeX == null ? [] : points.filter((p) => eq(p.x, activeX));
15
+ }
16
+ if (strategy === 'y') {
17
+ return activeY == null ? [] : points.filter((p) => eq(p.y, activeY));
18
+ }
19
+ // 'punctual': exact XY match, respecting series identity when both sides carry it
20
+ if (activeX == null || activeY == null)
21
+ return [];
22
+ return points.filter((p) => eq(p.x, activeX) &&
23
+ eq(p.y, activeY) &&
24
+ (activeSeries == null || p.series == null || p.series === activeSeries));
25
+ }
26
+ /** Whether the strategy implies an x crosshair rule by default. */
27
+ export function impliesRuleX(strategy) {
28
+ return strategy === 'x' || strategy === 'punctual';
29
+ }
30
+ /** Whether the strategy implies a y crosshair rule by default. */
31
+ export function impliesRuleY(strategy) {
32
+ return strategy === 'y' || strategy === 'punctual';
33
+ }
34
+ /** Normalize the author-facing prop into a resolved object. */
35
+ export function resolveTooltip(tooltip, defaultStrategy) {
36
+ if (!tooltip)
37
+ return { enabled: false, strategy: defaultStrategy, sync: false };
38
+ if (tooltip === true)
39
+ return { enabled: true, strategy: defaultStrategy, sync: false };
40
+ return {
41
+ enabled: true,
42
+ strategy: tooltip.strategy ?? defaultStrategy,
43
+ sync: tooltip.sync ?? false,
44
+ format: tooltip.format,
45
+ series: tooltip.series,
46
+ content: tooltip.content,
47
+ };
48
+ }
49
+ /**
50
+ * Resolve the strategy that should drive the shared hover store:
51
+ * the tooltip's own `strategy` wins when tooltip text is enabled, else the first
52
+ * hover marker's `strategy`, else the chart kind's default.
53
+ */
54
+ export function resolveHoverStrategy(tooltipStrategy, markers, defaultStrategy) {
55
+ return tooltipStrategy ?? findHoverMarker(markers)?.strategy ?? defaultStrategy;
56
+ }
57
+ /**
58
+ * Resolve whether the hover should be broadcast across facets:
59
+ * the tooltip's own `sync` wins when tooltip text is enabled, else the first
60
+ * hover marker's `sync`, else false.
61
+ */
62
+ export function resolveHoverSync(tooltipSync, markers) {
63
+ return tooltipSync ?? findHoverMarker(markers)?.sync ?? false;
64
+ }
65
+ /** Whether any marker in the list is a hover marker. */
66
+ export function hasHoverMarker(markers) {
67
+ return (markers ?? []).some((m) => m.type === 'hover');
68
+ }
69
+ /** First hover marker in the list, if any. */
70
+ export function findHoverMarker(markers) {
71
+ return (markers ?? []).find((m) => m.type === 'hover');
72
+ }
73
+ /** Narrow matched points down to a named subset of series (no-op if `series` is empty/undefined). */
74
+ export function filterBySeries(points, series) {
75
+ if (!series || series.length === 0)
76
+ return points;
77
+ return points.filter((p) => p.series != null && series.includes(p.series));
78
+ }