@cfasim-ui/docs 0.4.4 → 0.4.6

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.
@@ -0,0 +1,346 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type { ChartAnnotation } from "./annotations.js";
4
+ import type { ChartBounds } from "./useChartPadding.js";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ annotations?: readonly ChartAnnotation[];
9
+ /**
10
+ * Project an annotation's `(x, y)` (data coordinates) to pixel
11
+ * coordinates on the chart canvas. Return `null` to drop the
12
+ * annotation (e.g. an off-projection point on a map).
13
+ */
14
+ project: (x: number, y: number) => { x: number; y: number } | null;
15
+ /**
16
+ * Pixel-space bounds of the plot area. Required for rule annotations
17
+ * so the line can span the full plot. When omitted, rule annotations
18
+ * are skipped.
19
+ */
20
+ bounds?: ChartBounds;
21
+ }>(),
22
+ {
23
+ annotations: () => [],
24
+ bounds: undefined,
25
+ },
26
+ );
27
+
28
+ // Match the x/y axis label styling so annotations blend in by default.
29
+ const DEFAULT_FONT_SIZE = 13;
30
+ const DEFAULT_FONT_WEIGHT = "normal";
31
+ const BOLD_FONT_WEIGHT = 700;
32
+ const DEFAULT_HALO_COLOR = "var(--color-bg-0, #fff)";
33
+ const DEFAULT_HALO_WIDTH = 3;
34
+ const DEFAULT_LINE_WIDTH = 1;
35
+ const ANCHOR_GAP_PX = 4;
36
+ const LABEL_GAP_PX = 6;
37
+ const LINE_HEIGHT_RATIO = 1.2;
38
+ // Ratio of font-size that puts the pointer endpoint at the visual center
39
+ // of the first text line (between baseline and cap-height). Lands on the
40
+ // x-height middle for most fonts.
41
+ const FIRST_LINE_CENTER_RATIO = 0.35;
42
+ // Nudge the start of the curve a few pixels in the offset direction so
43
+ // it doesn't sit directly on top of axis lines or gridlines at the
44
+ // anchor.
45
+ const START_NUDGE_PX = 3;
46
+
47
+ interface TextRun {
48
+ text: string;
49
+ bold: boolean;
50
+ italic: boolean;
51
+ }
52
+
53
+ interface RenderedAnnotation {
54
+ lines: TextRun[][];
55
+ textX: number;
56
+ textY: number;
57
+ textAnchor: "start" | "middle" | "end";
58
+ dy: number;
59
+ fontSize: number;
60
+ fontWeight: string | number;
61
+ color: string;
62
+ haloColor: string;
63
+ haloWidth: number;
64
+ pointerPath: string;
65
+ lineColor: string;
66
+ lineWidth: number;
67
+ lineDash?: string;
68
+ arrow: boolean;
69
+ rule?: { x1: number; y1: number; x2: number; y2: number };
70
+ }
71
+
72
+ function resolveDash(
73
+ dash: string | number | readonly number[] | undefined,
74
+ ): string | undefined {
75
+ if (dash === undefined) return undefined;
76
+ if (typeof dash === "number") return `${dash} ${dash}`;
77
+ if (typeof dash === "string") return dash;
78
+ return dash.join(" ");
79
+ }
80
+
81
+ /**
82
+ * Parse a single line for `**bold**` and `_italic_` runs. Markers
83
+ * compose (`**_both_**`) and are forgiving — an unclosed marker carries
84
+ * its state through the rest of the line.
85
+ */
86
+ function parseInline(line: string): TextRun[] {
87
+ const out: TextRun[] = [];
88
+ let bold = false;
89
+ let italic = false;
90
+ let buf = "";
91
+ const flush = () => {
92
+ if (buf) out.push({ text: buf, bold, italic });
93
+ buf = "";
94
+ };
95
+ for (let i = 0; i < line.length; i++) {
96
+ const ch = line[i];
97
+ if (ch === "*" && line[i + 1] === "*") {
98
+ flush();
99
+ bold = !bold;
100
+ i++;
101
+ } else if (ch === "_") {
102
+ flush();
103
+ italic = !italic;
104
+ } else {
105
+ buf += ch;
106
+ }
107
+ }
108
+ flush();
109
+ return out.length === 0 ? [{ text: "", bold: false, italic: false }] : out;
110
+ }
111
+
112
+ const items = computed<RenderedAnnotation[]>(() => {
113
+ const out: RenderedAnnotation[] = [];
114
+ for (const a of props.annotations) {
115
+ const projected = props.project(a.x, a.y);
116
+ if (!projected) continue;
117
+ if (!isFinite(projected.x) || !isFinite(projected.y)) continue;
118
+
119
+ const pointer = a.pointer ?? "curved";
120
+ const isRule = pointer.startsWith("rule");
121
+
122
+ // Rule pointers require known plot bounds.
123
+ if (isRule && !props.bounds) continue;
124
+
125
+ const { x: offsetX, y: offsetY } = a.offset;
126
+ const labelX = projected.x + offsetX;
127
+ const labelY = projected.y + offsetY;
128
+ const color = a.color ?? "currentColor";
129
+ const fontSize = a.fontSize ?? DEFAULT_FONT_SIZE;
130
+ const fontWeight = a.fontWeight ?? DEFAULT_FONT_WEIGHT;
131
+ const haloColor = a.haloColor ?? DEFAULT_HALO_COLOR;
132
+ const haloWidth = a.haloWidth ?? DEFAULT_HALO_WIDTH;
133
+ const lineColor = a.lineColor ?? color;
134
+ const lineWidth = a.lineWidth ?? DEFAULT_LINE_WIDTH;
135
+ const lineDash = resolveDash(a.lineDash);
136
+ const textAnchor =
137
+ a.textAnchor ?? (offsetX > 0 ? "start" : offsetX < 0 ? "end" : "middle");
138
+
139
+ let rule: RenderedAnnotation["rule"];
140
+ let pointerPath = "";
141
+ if (isRule && props.bounds) {
142
+ rule = computeRule(pointer, projected.x, projected.y, props.bounds);
143
+ } else {
144
+ pointerPath = buildPointerPath(
145
+ projected.x,
146
+ projected.y,
147
+ labelX,
148
+ labelY,
149
+ fontSize,
150
+ pointer as "curved" | "straight" | "none",
151
+ );
152
+ }
153
+
154
+ out.push({
155
+ lines: a.text.split("\n").map(parseInline),
156
+ textX: labelX,
157
+ textY: labelY,
158
+ textAnchor,
159
+ dy: fontSize * LINE_HEIGHT_RATIO,
160
+ fontSize,
161
+ fontWeight,
162
+ color,
163
+ haloColor,
164
+ haloWidth,
165
+ pointerPath,
166
+ lineColor,
167
+ lineWidth,
168
+ lineDash,
169
+ arrow: !isRule && (a.arrow ?? true),
170
+ rule,
171
+ });
172
+ }
173
+ return out;
174
+ });
175
+
176
+ /**
177
+ * Endpoints for a rule line through the anchor.
178
+ * - `ruleX` / `ruleY`: full plot span on the named axis.
179
+ * - `ruleUp` / `ruleDown` / `ruleFromLeft` / `ruleFromRight`: partial
180
+ * rule from one plot edge in to the anchor.
181
+ */
182
+ function computeRule(
183
+ pointer: string,
184
+ ax: number,
185
+ ay: number,
186
+ b: ChartBounds,
187
+ ): { x1: number; y1: number; x2: number; y2: number } {
188
+ switch (pointer) {
189
+ case "ruleX":
190
+ return { x1: ax, y1: b.top, x2: ax, y2: b.bottom };
191
+ case "ruleY":
192
+ return { x1: b.left, y1: ay, x2: b.right, y2: ay };
193
+ case "ruleUp":
194
+ return { x1: ax, y1: b.bottom, x2: ax, y2: ay };
195
+ case "ruleDown":
196
+ return { x1: ax, y1: b.top, x2: ax, y2: ay };
197
+ case "ruleFromLeft":
198
+ return { x1: b.left, y1: ay, x2: ax, y2: ay };
199
+ case "ruleFromRight":
200
+ return { x1: b.right, y1: ay, x2: ax, y2: ay };
201
+ default:
202
+ return { x1: ax, y1: ay, x2: ax, y2: ay };
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Build the pointer line from the anchor to the label.
208
+ *
209
+ * - When the label has offset in only one dimension, the line is
210
+ * straight (vertical or horizontal) ending at the label's baseline.
211
+ * - When both dimensions have offset, the line is a quarter-arc:
212
+ * a quadratic Bezier with the control point at `(anchorX, firstLineY)`
213
+ * where `firstLineY` is the visual center of the first line of text
214
+ * (slightly above the baseline). The curve emerges from the anchor
215
+ * vertically toward the label's row, then bends horizontally into the
216
+ * label so the endpoint reads as pointing at the first line — not at
217
+ * the bottom of a multi-line block.
218
+ */
219
+ function buildPointerPath(
220
+ ax: number,
221
+ ay: number,
222
+ lx: number,
223
+ ly: number,
224
+ fontSize: number,
225
+ pointer: "curved" | "straight" | "none",
226
+ ): string {
227
+ if (pointer === "none") return "";
228
+ const dx = lx - ax;
229
+ const dy = ly - ay;
230
+
231
+ // Target the visual center of the first line so multi-line text
232
+ // doesn't have the pointer dive below the whole block.
233
+ const targetY = ly - fontSize * FIRST_LINE_CENTER_RATIO;
234
+
235
+ // Pure horizontal or vertical → straight line at baseline, no curve.
236
+ // Force straight when explicitly requested.
237
+ if (dx === 0 || dy === 0 || pointer === "straight") {
238
+ // For straight pointers, aim at first-line-center (so multi-line text
239
+ // still points at the first line) — but only when the anchor isn't
240
+ // already at exactly the same Y (pure horizontal offset). The pure
241
+ // horizontal case stays on the baseline so it's a real horizontal line.
242
+ const ey = dy === 0 ? ly : targetY;
243
+ const segDx = lx - ax;
244
+ const segDy = ey - ay;
245
+ const len = Math.hypot(segDx, segDy);
246
+ if (len <= ANCHOR_GAP_PX + LABEL_GAP_PX) return "";
247
+ const ux = segDx / len;
248
+ const uy = segDy / len;
249
+ const sx = ax + ux * ANCHOR_GAP_PX;
250
+ const sy = ay + uy * ANCHOR_GAP_PX;
251
+ const ex = lx - ux * LABEL_GAP_PX;
252
+ const eyClamped = ey - uy * LABEL_GAP_PX;
253
+ return `M${sx},${sy} L${ex},${eyClamped}`;
254
+ }
255
+
256
+ const adjDy = targetY - ay;
257
+
258
+ // Skip the curve if one dimension is too small to clear its gap.
259
+ if (Math.abs(adjDy) <= ANCHOR_GAP_PX || Math.abs(dx) <= LABEL_GAP_PX) {
260
+ return "";
261
+ }
262
+
263
+ const xDir = Math.sign(dx);
264
+ const yDir = Math.sign(adjDy);
265
+ // Nudge the start horizontally toward the label so the line doesn't
266
+ // sit on top of axis/grid lines passing through the anchor.
267
+ const sx = ax + xDir * START_NUDGE_PX;
268
+ const sy = ay + yDir * ANCHOR_GAP_PX;
269
+ const ex = lx - xDir * LABEL_GAP_PX;
270
+ const ey = targetY;
271
+ // Control sits at (sx, targetY) so the curve emerges from the nudged
272
+ // start tangent vertically and lands on the label horizontally —
273
+ // a clean quarter-arc shape.
274
+ return `M${sx},${sy} Q${sx},${targetY} ${ex},${ey}`;
275
+ }
276
+ </script>
277
+
278
+ <template>
279
+ <defs>
280
+ <marker
281
+ id="chart-annotation-arrow"
282
+ viewBox="0 0 8 8"
283
+ refX="7"
284
+ refY="4"
285
+ markerWidth="6"
286
+ markerHeight="6"
287
+ orient="auto-start-reverse"
288
+ markerUnits="userSpaceOnUse"
289
+ >
290
+ <path d="M0,0 L8,4 L0,8 Z" fill="context-stroke" />
291
+ </marker>
292
+ </defs>
293
+ <g class="chart-annotations" pointer-events="none">
294
+ <template v-for="(item, i) in items" :key="i">
295
+ <line
296
+ v-if="item.rule"
297
+ :x1="item.rule.x1"
298
+ :y1="item.rule.y1"
299
+ :x2="item.rule.x2"
300
+ :y2="item.rule.y2"
301
+ :stroke="item.lineColor"
302
+ :stroke-width="item.lineWidth"
303
+ :stroke-dasharray="item.lineDash"
304
+ stroke-linecap="round"
305
+ />
306
+ <path
307
+ v-if="item.pointerPath"
308
+ :d="item.pointerPath"
309
+ fill="none"
310
+ :stroke="item.lineColor"
311
+ :style="{ color: item.lineColor }"
312
+ :stroke-width="item.lineWidth"
313
+ :stroke-dasharray="item.lineDash"
314
+ stroke-linecap="round"
315
+ :marker-start="item.arrow ? 'url(#chart-annotation-arrow)' : undefined"
316
+ />
317
+ <text
318
+ :x="item.textX"
319
+ :y="item.textY"
320
+ :text-anchor="item.textAnchor"
321
+ :font-size="item.fontSize"
322
+ :font-weight="item.fontWeight"
323
+ :fill="item.color"
324
+ :stroke="item.haloColor"
325
+ :stroke-width="item.haloWidth"
326
+ stroke-linejoin="round"
327
+ paint-order="stroke fill"
328
+ >
329
+ <tspan
330
+ v-for="(line, li) in item.lines"
331
+ :key="li"
332
+ :x="item.textX"
333
+ :dy="li === 0 ? 0 : item.dy"
334
+ >
335
+ <tspan
336
+ v-for="(run, ri) in line"
337
+ :key="ri"
338
+ :font-weight="run.bold ? BOLD_FONT_WEIGHT : undefined"
339
+ :font-style="run.italic ? 'italic' : undefined"
340
+ >{{ run.text }}</tspan
341
+ >
342
+ </tspan>
343
+ </text>
344
+ </template>
345
+ </g>
346
+ </template>
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared annotation API for charts. Each chart projects an annotation's
3
+ * data-space `(x, y)` to pixels with its own scales and hands the resolved
4
+ * positions to `ChartAnnotations.vue` for rendering.
5
+ */
6
+
7
+ export interface ChartAnnotation {
8
+ /**
9
+ * Anchor x position in data coordinates. For `LineChart` this is the
10
+ * same x-space as the series data; for `BarChart` it's the category
11
+ * index (fractional values land between categories).
12
+ */
13
+ x: number;
14
+ /** Anchor y position in data coordinates (value axis). */
15
+ y: number;
16
+ /**
17
+ * Label text.
18
+ * - `\n` produces a line break.
19
+ * - `**bold**` renders a run in bold.
20
+ * - `_italic_` renders a run in italic.
21
+ * - The two compose: `**_both_**`.
22
+ */
23
+ text: string;
24
+ /**
25
+ * Pixel offset from the anchor to the label's reference position.
26
+ * Positive `x` = right, positive `y` = down (screen-space).
27
+ */
28
+ offset: { x: number; y: number };
29
+ /** Text and pointer-line color. Defaults to `currentColor`. */
30
+ color?: string;
31
+ /** Font size in pixels. Default: 13 (matches axis labels). */
32
+ fontSize?: number;
33
+ /**
34
+ * Base font weight applied to all non-bold runs. Default: `"normal"`
35
+ * (matches axis labels). `**bold**` runs in `text` always render at
36
+ * weight 700.
37
+ */
38
+ fontWeight?: string | number;
39
+ /**
40
+ * Halo (stroke) color drawn behind the text so the label stays legible
41
+ * against busy chart elements. Defaults to `var(--color-bg-0, #fff)` so
42
+ * it matches the page background out of the box.
43
+ */
44
+ haloColor?: string;
45
+ /** Halo stroke width in pixels. Default: 3. */
46
+ haloWidth?: number;
47
+ /**
48
+ * SVG text-anchor for the label. When omitted, derived from the sign of
49
+ * `offset.x`: positive → `start`, negative → `end`, zero → `middle`.
50
+ */
51
+ textAnchor?: "start" | "middle" | "end";
52
+ /** Pointer- or rule-line color override. Defaults to `color`. */
53
+ lineColor?: string;
54
+ /** Pointer- or rule-line width in pixels. Default: 1. */
55
+ lineWidth?: number;
56
+ /**
57
+ * SVG `stroke-dasharray` for the pointer or rule line. Accepts the
58
+ * raw string form (`"4 4"`), a single number (uniform dash/gap), or
59
+ * an array of numbers. Default: solid line.
60
+ */
61
+ lineDash?: string | number | readonly number[];
62
+ /**
63
+ * Connector shape between anchor and label.
64
+ * - `"curved"` (default): quarter-arc emerging vertically from the
65
+ * anchor, landing horizontally at the label.
66
+ * - `"straight"`: single straight line from anchor to label.
67
+ * - `"none"`: no connector — just the text label is rendered.
68
+ * - `"ruleX"`: vertical rule at the annotation's `x` value spanning
69
+ * the full plot height. Label still positions via `offset`.
70
+ * - `"ruleY"`: horizontal rule at the annotation's `y` value spanning
71
+ * the full plot width.
72
+ * - `"ruleUp"`: vertical rule from the plot's bottom edge up to the
73
+ * anchor.
74
+ * - `"ruleDown"`: vertical rule from the plot's top edge down to the
75
+ * anchor.
76
+ * - `"ruleFromLeft"`: horizontal rule from the plot's left edge in to
77
+ * the anchor.
78
+ * - `"ruleFromRight"`: horizontal rule from the plot's right edge in
79
+ * to the anchor.
80
+ *
81
+ * When the offset is purely horizontal or vertical (and `pointer`
82
+ * isn't `"none"` or a rule), the pointer is always straight regardless
83
+ * of this setting. Rule pointers ignore `arrow`.
84
+ */
85
+ pointer?:
86
+ | "curved"
87
+ | "straight"
88
+ | "none"
89
+ | "ruleX"
90
+ | "ruleY"
91
+ | "ruleUp"
92
+ | "ruleDown"
93
+ | "ruleFromLeft"
94
+ | "ruleFromRight";
95
+ /**
96
+ * Whether to draw a small filled triangle at the anchor end of the
97
+ * connector line. Defaults to `true`. Set to `false` for an
98
+ * uncapped line. Ignored for rule pointers.
99
+ */
100
+ arrow?: boolean;
101
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Prop / slot / emit types shared by LineChart and BarChart. Component
3
+ * authors compose these with chart-specific props via TypeScript
4
+ * intersection (e.g. `defineProps<ChartCommonProps & MyExtraProps>()`).
5
+ */
6
+
7
+ import type { ChartAnnotation } from "./annotations.js";
8
+ import type { ChartPadding } from "./useChartPadding.js";
9
+
10
+ /**
11
+ * Props common to every cartesian chart component. Anything specific to
12
+ * the chart type (series shape, layout, value-axis details) lives on the
13
+ * component itself.
14
+ */
15
+ export interface ChartCommonProps {
16
+ width?: number;
17
+ height?: number;
18
+ title?: string;
19
+ xLabel?: string;
20
+ yLabel?: string;
21
+ debounce?: number;
22
+ menu?: boolean | string;
23
+ /**
24
+ * Custom per-index data forwarded to the `tooltip` slot. Accepts a
25
+ * plain array or any `ArrayLike` (e.g. a typed-array column).
26
+ */
27
+ tooltipData?: ArrayLike<unknown>;
28
+ /** Tooltip activation mode. */
29
+ tooltipTrigger?: "hover" | "click";
30
+ /** Boundary for tooltip flip/clamp. Default: `"chart"`. */
31
+ tooltipClamp?: "none" | "chart" | "window";
32
+ /**
33
+ * Formatter for numeric values shown in the default tooltip. Receives
34
+ * the raw value. When omitted, the chart falls back to its value-axis
35
+ * tick formatter, then `formatTick`.
36
+ */
37
+ tooltipValueFormat?: (value: number) => string;
38
+ /**
39
+ * Custom CSV content (string or function) for the Download CSV menu
40
+ * item. When omitted, CSV is generated from the chart's series.
41
+ */
42
+ csv?: string | (() => string);
43
+ /** Filename (without extension) for SVG, PNG, and CSV downloads. */
44
+ filename?: string;
45
+ /**
46
+ * Show a plain text link below the chart to download CSV. Pass `true`
47
+ * for the default label or a string to customize.
48
+ */
49
+ downloadLink?: boolean | string;
50
+ /** Annotations rendered as the top layer of the chart. */
51
+ annotations?: readonly ChartAnnotation[];
52
+ /**
53
+ * Extra padding (pixels) added around the plot. Number = same on all
54
+ * sides; object = per-side. Useful for giving annotations or other
55
+ * overlays room to extend past the data area without clipping.
56
+ */
57
+ chartPadding?: ChartPadding;
58
+ }
59
+
60
+ /** Payload emitted on `hover` from a cartesian chart. */
61
+ export type ChartHoverPayload = { index: number } | null;
62
+
63
+ /** One per-series value passed to the tooltip slot. */
64
+ export interface ChartTooltipValue {
65
+ value: number;
66
+ color: string;
67
+ seriesIndex: number;
68
+ }
69
+
70
+ /** Properties common to every chart's tooltip slot. */
71
+ export interface ChartTooltipBaseProps {
72
+ index: number;
73
+ values: ChartTooltipValue[];
74
+ data: unknown;
75
+ }
@@ -11,6 +11,8 @@ export {
11
11
  useChartPadding,
12
12
  INLINE_LEGEND_HEIGHT,
13
13
  type ChartPaddingOptions,
14
+ type ChartPadding,
15
+ type ChartBounds,
14
16
  } from "./useChartPadding.js";
15
17
  export {
16
18
  useChartTooltip,
@@ -18,3 +20,16 @@ export {
18
20
  } from "./useChartTooltip.js";
19
21
  export { useChartMenu, type ChartMenuOptions } from "./useChartMenu.js";
20
22
  export { seriesToCsv, categoricalToCsv, type CsvSeries } from "./seriesCsv.js";
23
+ export { default as ChartAnnotations } from "./ChartAnnotations.vue";
24
+ export type { ChartAnnotation } from "./annotations.js";
25
+ export {
26
+ useChartFoundation,
27
+ makeTooltipValueFormatter,
28
+ type ChartFoundationOptions,
29
+ } from "./useChartFoundation.js";
30
+ export type {
31
+ ChartCommonProps,
32
+ ChartHoverPayload,
33
+ ChartTooltipValue,
34
+ ChartTooltipBaseProps,
35
+ } from "./chartProps.js";
@@ -0,0 +1,124 @@
1
+ import { computed } from "vue";
2
+ import { formatTick } from "./axes.js";
3
+ import { useChartSize } from "./useChartSize.js";
4
+ import { useChartPadding, type ChartPadding } from "./useChartPadding.js";
5
+ import { useChartTooltip } from "./useChartTooltip.js";
6
+ import type { TooltipClamp } from "../tooltip-position.js";
7
+ import { useChartMenu } from "./useChartMenu.js";
8
+
9
+ const DEFAULT_WIDTH_FALLBACK = 400;
10
+ const DEFAULT_HEIGHT = 200;
11
+
12
+ export interface ChartFoundationOptions {
13
+ // Reactive getters for the shared chart props.
14
+ width: () => number | undefined;
15
+ height: () => number | undefined;
16
+ title: () => string | undefined;
17
+ xLabel: () => string | undefined;
18
+ yLabel: () => string | undefined;
19
+ debounce: () => number | undefined;
20
+ menu: () => boolean | string | undefined;
21
+ tooltipTrigger: () => "hover" | "click" | undefined;
22
+ tooltipClamp: () => TooltipClamp | undefined;
23
+ filename: () => string | undefined;
24
+ downloadLink: () => boolean | string | undefined;
25
+ chartPadding: () => ChartPadding | undefined;
26
+ // Chart-specific hooks that the composable can't infer.
27
+ hasInlineLegend: () => boolean;
28
+ hasTooltipSlot: () => boolean;
29
+ getCsv: () => string;
30
+ pointerToIndex: (clientX: number, clientY: number) => number | null;
31
+ onHover: (payload: { index: number } | null) => void;
32
+ }
33
+
34
+ /**
35
+ * Wires up the shared chart plumbing — size measurement, padding, tooltip
36
+ * interaction, and the menu/download wiring — that every cartesian chart
37
+ * needs. Returns the reactive values and refs each chart's template
38
+ * consumes.
39
+ */
40
+ export function useChartFoundation(opts: ChartFoundationOptions) {
41
+ const { containerRef, measuredWidth } = useChartSize({
42
+ debounce: opts.debounce,
43
+ });
44
+
45
+ const width = computed(
46
+ () => opts.width() ?? (measuredWidth.value || DEFAULT_WIDTH_FALLBACK),
47
+ );
48
+ const height = computed(() => opts.height() ?? DEFAULT_HEIGHT);
49
+
50
+ const { padding, legendY, innerW, innerH, bounds } = useChartPadding({
51
+ title: opts.title,
52
+ xLabel: opts.xLabel,
53
+ yLabel: opts.yLabel,
54
+ hasInlineLegend: opts.hasInlineLegend,
55
+ width: () => width.value,
56
+ height: () => height.value,
57
+ extraPadding: opts.chartPadding,
58
+ });
59
+
60
+ const {
61
+ hoverIndex,
62
+ tooltipRef,
63
+ tooltipPos,
64
+ handlers: tooltipHandlers,
65
+ } = useChartTooltip({
66
+ enabled: opts.hasTooltipSlot,
67
+ trigger: opts.tooltipTrigger,
68
+ clamp: () => opts.tooltipClamp() ?? "chart",
69
+ pointerToIndex: opts.pointerToIndex,
70
+ containerRef,
71
+ onHover: opts.onHover,
72
+ });
73
+
74
+ const {
75
+ svgRef,
76
+ items: menuItems,
77
+ downloadLinkText,
78
+ csvHref,
79
+ resolvedFilename: menuFilename,
80
+ } = useChartMenu({
81
+ filename: opts.filename,
82
+ legacyMenuLabel: opts.menu,
83
+ getCsv: opts.getCsv,
84
+ downloadLink: opts.downloadLink,
85
+ });
86
+
87
+ return {
88
+ containerRef,
89
+ svgRef,
90
+ width,
91
+ height,
92
+ padding,
93
+ legendY,
94
+ innerW,
95
+ innerH,
96
+ bounds,
97
+ hoverIndex,
98
+ tooltipRef,
99
+ tooltipPos,
100
+ tooltipHandlers,
101
+ menuItems,
102
+ downloadLinkText,
103
+ csvHref,
104
+ menuFilename,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Build a tooltip value formatter that prefers `tooltipValueFormat`,
110
+ * falls back to the chart's axis tick formatter, and finally to
111
+ * `formatTick`. Both chart components use the same precedence order.
112
+ */
113
+ export function makeTooltipValueFormatter(
114
+ tooltipFormat: () => ((v: number) => string) | undefined,
115
+ axisFormat: () => ((v: number) => string) | undefined,
116
+ ): (v: number) => string {
117
+ return (v: number) => {
118
+ const tf = tooltipFormat();
119
+ if (tf) return tf(v);
120
+ const af = axisFormat();
121
+ if (af) return af(v);
122
+ return formatTick(v);
123
+ };
124
+ }