@fundar/data-chart-telling 0.0.12 → 0.0.14
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.
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { hasHoverMarker, findHoverMarker } from '../tooltip/utils';
|
|
8
8
|
import HoverMarker from '../../plots/markers/HoverMarker.svelte';
|
|
9
9
|
import Tooltip from '../tooltip/Tooltip.svelte';
|
|
10
|
-
import type { AxisValue, Series, ScalesConfig, MarginConfig } from '../../types/plots/common';
|
|
10
|
+
import type { AxisValue, AxisScale, Series, ScalesConfig, MarginConfig } from '../../types/plots/common';
|
|
11
11
|
import type { Marker } from '../../types/plots/markers';
|
|
12
12
|
import type { HoverStrategy, HoverDisplayPoint, HoverPoint, TooltipRuntime } from '../../types/layout/tooltip';
|
|
13
13
|
|
|
@@ -98,6 +98,51 @@
|
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
// ── Axis ticks / domain ──────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const xValues = $derived(data.map(getX));
|
|
104
|
+
const yValues = $derived(data.map(getY));
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Caps the requested tick count to the axis's own number of distinct
|
|
108
|
+
* values, but never above what the pixel width would naturally request —
|
|
109
|
+
* so a narrow domain (e.g. 5 whole years) never gets subdivided into
|
|
110
|
+
* fractional ticks (2020, 2020.5, 2021, …) just to fill the space. Skipped
|
|
111
|
+
* whenever the caller already controls tick density directly.
|
|
112
|
+
*/
|
|
113
|
+
function autoTickCount(scale: AxisScale | undefined, values: AxisValue[], span: number, spacing: number, min: number): number | undefined {
|
|
114
|
+
if (scale?.ticks != null || scale?.tickSpacing != null || scale?.interval != null) return undefined;
|
|
115
|
+
const uniqueCount = new Set(values.map(String)).size;
|
|
116
|
+
const natural = Math.max(min, Math.round(span / spacing));
|
|
117
|
+
return uniqueCount >= 2 && uniqueCount < natural ? uniqueCount : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const xTickCount = $derived(
|
|
121
|
+
autoTickCount(scales?.x, xValues, Math.max(0, width - computedMargins.left - computedMargins.right), 80, 3),
|
|
122
|
+
);
|
|
123
|
+
const yTickCount = $derived(
|
|
124
|
+
autoTickCount(scales?.y, yValues, Math.max(0, height - computedMargins.top - computedMargins.bottom), 50, 2),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* svelteplot's own `sort` scale option only toggles a fixed ascending sort
|
|
129
|
+
* (lexicographic for strings) — it can't take a custom comparator. When the
|
|
130
|
+
* caller supplies one via `scales.x.sort`/`scales.y.sort` for a string-valued
|
|
131
|
+
* axis (e.g. dates formatted as text), compute the ordered domain ourselves
|
|
132
|
+
* and hand it to the scale as an explicit `domain`, which svelteplot does
|
|
133
|
+
* respect as-is for ordinal scales.
|
|
134
|
+
*/
|
|
135
|
+
function sortedScale<T>(scale: T, scaleConfig: AxisScale | undefined, values: AxisValue[]): T {
|
|
136
|
+
const sort = scaleConfig?.sort;
|
|
137
|
+
if (!sort || scaleConfig?.domain || values.length === 0) return scale;
|
|
138
|
+
if (!values.every((v) => typeof v === 'string')) return scale;
|
|
139
|
+
const domain = [...new Set(values)].sort(sort);
|
|
140
|
+
return { ...(scale as object), domain } as T;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const resolvedX = $derived(sortedScale(x, scales?.x, xValues));
|
|
144
|
+
const resolvedY = $derived(sortedScale(y, scales?.y, yValues));
|
|
145
|
+
|
|
101
146
|
// ── Hover / tooltip ───────────────────────────────────────────────────────
|
|
102
147
|
const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
|
|
103
148
|
|
|
@@ -165,9 +210,21 @@
|
|
|
165
210
|
onpointermove={hoverActive ? ctrl.onMove : undefined}
|
|
166
211
|
onpointerleave={hoverActive ? ctrl.onLeave : undefined}
|
|
167
212
|
>
|
|
168
|
-
<Plot {width} {height} margin={computedMargins} {
|
|
169
|
-
<AxisX
|
|
170
|
-
|
|
213
|
+
<Plot {width} {height} margin={computedMargins} x={resolvedX} y={resolvedY}>
|
|
214
|
+
<AxisX
|
|
215
|
+
tickCount={xTickCount}
|
|
216
|
+
tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize}
|
|
217
|
+
tickPadding={scales?.x?.tickPadding ?? cfg.axis.xTickPadding}
|
|
218
|
+
tickFontSize={cfg.axis.fontSize}
|
|
219
|
+
titleFontSize={scales?.x?.titleFontSize ?? cfg.axis.titleFontSize}
|
|
220
|
+
/>
|
|
221
|
+
<AxisY
|
|
222
|
+
tickCount={yTickCount}
|
|
223
|
+
tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
|
|
224
|
+
tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding}
|
|
225
|
+
tickFontSize={cfg.axis.fontSize}
|
|
226
|
+
titleFontSize={scales?.y?.titleFontSize ?? cfg.axis.titleFontSize}
|
|
227
|
+
/>
|
|
171
228
|
{@render children({
|
|
172
229
|
matchedPoints: ctrl.points,
|
|
173
230
|
ruleX: ctrl.ruleX,
|
|
@@ -219,7 +276,7 @@
|
|
|
219
276
|
</Plot>
|
|
220
277
|
|
|
221
278
|
{#if tooltip && ctrl.showTooltip && ctrl.tooltipPosition}
|
|
222
|
-
<Tooltip x={ctrl.tooltipPosition?.x ?? 0} y={ctrl.tooltipPosition?.y ?? 0}>
|
|
279
|
+
<Tooltip x={ctrl.tooltipPosition?.x ?? 0} y={ctrl.tooltipPosition?.y ?? 0} bounds={{ width, height }}>
|
|
223
280
|
{#if tooltip.content}
|
|
224
281
|
{@render tooltip.content({ rows: ctrl.matchedRows.map((p) => p.row) })}
|
|
225
282
|
{:else}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* parent. It is styled entirely from the `--dct-tooltip-*` variables that
|
|
7
7
|
* {@link ChartContainer} stamps from the config, so it themes for free. Plots
|
|
8
8
|
* that implement hovering can render this with their own content snippet.
|
|
9
|
+
*
|
|
10
|
+
* Placement is boundary-aware: it prefers sitting centered above the point,
|
|
11
|
+
* but flips below when it would overflow the top and shifts horizontally
|
|
12
|
+
* when it would overflow the left/right, so it always stays clear of the
|
|
13
|
+
* `bounds` box (typically the plot's own width/height).
|
|
9
14
|
*/
|
|
10
15
|
let {
|
|
11
16
|
x,
|
|
@@ -13,6 +18,7 @@
|
|
|
13
18
|
visible = true,
|
|
14
19
|
background,
|
|
15
20
|
color,
|
|
21
|
+
bounds,
|
|
16
22
|
children,
|
|
17
23
|
}: {
|
|
18
24
|
x: number;
|
|
@@ -20,15 +26,39 @@
|
|
|
20
26
|
visible?: boolean;
|
|
21
27
|
background?: string;
|
|
22
28
|
color?: string;
|
|
29
|
+
/** Box the tooltip must stay within — usually the hosting plot's width/height. */
|
|
30
|
+
bounds?: { width: number; height: number };
|
|
23
31
|
children: Snippet;
|
|
24
32
|
} = $props();
|
|
33
|
+
|
|
34
|
+
const GAP = 10;
|
|
35
|
+
const EDGE_MARGIN = 6;
|
|
36
|
+
|
|
37
|
+
let boxWidth = $state(0);
|
|
38
|
+
let boxHeight = $state(0);
|
|
39
|
+
|
|
40
|
+
const placement = $derived.by(() => {
|
|
41
|
+
const boundsWidth = bounds?.width ?? Infinity;
|
|
42
|
+
const boundsHeight = bounds?.height ?? Infinity;
|
|
43
|
+
|
|
44
|
+
let top = y - GAP - boxHeight;
|
|
45
|
+
if (top < EDGE_MARGIN) top = y + GAP;
|
|
46
|
+
top = Math.min(Math.max(top, EDGE_MARGIN), Math.max(EDGE_MARGIN, boundsHeight - boxHeight - EDGE_MARGIN));
|
|
47
|
+
|
|
48
|
+
let left = x - boxWidth / 2;
|
|
49
|
+
left = Math.min(Math.max(left, EDGE_MARGIN), Math.max(EDGE_MARGIN, boundsWidth - boxWidth - EDGE_MARGIN));
|
|
50
|
+
|
|
51
|
+
return { left, top };
|
|
52
|
+
});
|
|
25
53
|
</script>
|
|
26
54
|
|
|
27
55
|
{#if visible}
|
|
28
56
|
<div
|
|
29
57
|
class="dct-tooltip"
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
bind:clientWidth={boxWidth}
|
|
59
|
+
bind:clientHeight={boxHeight}
|
|
60
|
+
style:left="{placement.left}px"
|
|
61
|
+
style:top="{placement.top}px"
|
|
32
62
|
style:background={background}
|
|
33
63
|
style:color={color}
|
|
34
64
|
>
|
|
@@ -40,7 +70,6 @@
|
|
|
40
70
|
.dct-tooltip {
|
|
41
71
|
position: absolute;
|
|
42
72
|
z-index: 10;
|
|
43
|
-
transform: translate(-50%, -110%);
|
|
44
73
|
pointer-events: none;
|
|
45
74
|
background: var(--dct-tooltip-bg, #111827);
|
|
46
75
|
color: var(--dct-tooltip-color, #fff);
|
|
@@ -5,6 +5,11 @@ type $$ComponentProps = {
|
|
|
5
5
|
visible?: boolean;
|
|
6
6
|
background?: string;
|
|
7
7
|
color?: string;
|
|
8
|
+
/** Box the tooltip must stay within — usually the hosting plot's width/height. */
|
|
9
|
+
bounds?: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
8
13
|
children: Snippet;
|
|
9
14
|
};
|
|
10
15
|
declare const Tooltip: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -47,7 +47,7 @@ export function createHoverController(args) {
|
|
|
47
47
|
return cursor != null;
|
|
48
48
|
return h.sync;
|
|
49
49
|
});
|
|
50
|
-
const tooltipText = $derived(matchedRows.length ? matchedRows.map((p) => args.format()(p.row)).join('\n
|
|
50
|
+
const tooltipText = $derived(matchedRows.length ? matchedRows.map((p) => args.format()(p.row)).join('\n') : '');
|
|
51
51
|
function onPointer(data) {
|
|
52
52
|
const h = args.hover();
|
|
53
53
|
if (data.length) {
|
|
@@ -42,10 +42,7 @@ export type AxisScale = {
|
|
|
42
42
|
titleFontSize?: number;
|
|
43
43
|
base?: number;
|
|
44
44
|
constant?: number;
|
|
45
|
-
sort?: (
|
|
46
|
-
channel: string;
|
|
47
|
-
order: 'ascending' | 'descending';
|
|
48
|
-
};
|
|
45
|
+
sort?: (a: AxisValue, b: AxisValue) => number;
|
|
49
46
|
grid?: boolean;
|
|
50
47
|
tickRotate?: number;
|
|
51
48
|
tickFormat?: false | Intl.NumberFormatOptions | Intl.DateTimeFormatOptions | ((d: unknown, index: number, ticks: unknown[]) => string | string[]);
|