@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} {x} {y}>
169
- <AxisX tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize} tickPadding={scales?.x?.tickPadding ?? cfg.axis.xTickPadding} tickFontSize={cfg.axis.fontSize} titleFontSize={scales?.x?.titleFontSize ?? cfg.axis.titleFontSize} />
170
- <AxisY tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize} tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding} tickFontSize={cfg.axis.fontSize} titleFontSize={scales?.y?.titleFontSize ?? cfg.axis.titleFontSize} />
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
- style:left="{x}px"
31
- style:top="{y}px"
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\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?: ((a: unknown, b: unknown) => number) | {
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[]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"