@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.
- package/charts/BarChart/BarChart.md +126 -14
- package/charts/BarChart/BarChart.vue +113 -131
- package/charts/ChoroplethMap/ChoroplethMap.md +153 -1
- package/charts/ChoroplethMap/ChoroplethMap.vue +369 -106
- package/charts/LineChart/LineChart.md +209 -14
- package/charts/LineChart/LineChart.vue +118 -146
- package/charts/_shared/ChartAnnotations.vue +346 -0
- package/charts/_shared/annotations.ts +101 -0
- package/charts/_shared/chartProps.ts +75 -0
- package/charts/_shared/index.ts +15 -0
- package/charts/_shared/useChartFoundation.ts +124 -0
- package/charts/_shared/useChartPadding.ts +74 -10
- package/charts/index.ts +5 -0
- package/components/ParamEditor/ParamEditor.md +78 -0
- package/components/ParamEditor/ParamEditor.vue +39 -0
- package/components/ParamEditor/ParamEditorImpl.vue +355 -0
- package/components/SidebarLayout/SidebarLayout.vue +10 -9
- package/components/index.ts +5 -0
- package/index.json +18 -1
- package/package.json +1 -1
- package/wasm/index.ts +1 -1
- package/wasm/wasmWorkerApi.ts +38 -6
|
@@ -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
|
+
}
|
package/charts/_shared/index.ts
CHANGED
|
@@ -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
|
+
}
|