@cfasim-ui/charts 0.7.7 → 0.8.0

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 (63) hide show
  1. package/dist/BarChart/BarChart.d.ts +9 -26
  2. package/dist/ChartMenu/ChartMenu.d.ts +3 -2
  3. package/dist/ChartTooltip/ChartTooltip.d.ts +9 -12
  4. package/dist/ChoroplethMap/ChoroplethMap.d.ts +28 -189
  5. package/dist/ChoroplethMap/ChoroplethTooltip.d.ts +20 -27
  6. package/dist/ChoroplethMap/canvasLayer.d.ts +27 -3
  7. package/dist/DataTable/DataTable.d.ts +3 -2
  8. package/dist/LineChart/LineChart.d.ts +9 -26
  9. package/dist/_shared/ChartAnnotations.d.ts +3 -2
  10. package/dist/_shared/ChartZoomControls.d.ts +3 -2
  11. package/dist/_shared/index.d.ts +2 -1
  12. package/dist/_shared/mapTheme.d.ts +159 -0
  13. package/dist/_shared/mapTheme.test.d.ts +1 -0
  14. package/dist/index.css +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1710 -1432
  17. package/docs/BarChart.md +776 -0
  18. package/docs/ChoroplethMap.md +1267 -0
  19. package/docs/DataTable.md +386 -0
  20. package/docs/LineChart.md +1267 -0
  21. package/docs/index.json +56 -0
  22. package/package.json +25 -21
  23. package/src/BarChart/BarChart.md +743 -0
  24. package/src/BarChart/BarChart.vue +1901 -0
  25. package/src/ChartMenu/ChartMenu.vue +220 -0
  26. package/src/ChartMenu/download.ts +120 -0
  27. package/src/ChartTooltip/ChartTooltip.vue +97 -0
  28. package/src/ChoroplethMap/ChoroplethMap.md +1227 -0
  29. package/src/ChoroplethMap/ChoroplethMap.vue +3676 -0
  30. package/src/ChoroplethMap/ChoroplethTooltip.vue +103 -0
  31. package/src/ChoroplethMap/canvasLayer.ts +373 -0
  32. package/src/ChoroplethMap/cityLayout.ts +261 -0
  33. package/src/ChoroplethMap/hsaMapping.ts +4116 -0
  34. package/src/DataTable/DataTable.md +372 -0
  35. package/src/DataTable/DataTable.vue +406 -0
  36. package/src/LineChart/LineChart.md +1225 -0
  37. package/src/LineChart/LineChart.vue +1555 -0
  38. package/src/_shared/ChartAnnotations.vue +420 -0
  39. package/src/_shared/ChartZoomControls.vue +138 -0
  40. package/src/_shared/annotations.ts +106 -0
  41. package/src/_shared/axes.ts +69 -0
  42. package/src/_shared/chartProps.ts +201 -0
  43. package/src/_shared/computeTicks.ts +42 -0
  44. package/src/_shared/contrast.ts +100 -0
  45. package/src/_shared/dateAxis.ts +501 -0
  46. package/src/_shared/index.ts +78 -0
  47. package/src/_shared/mapTheme.ts +551 -0
  48. package/src/_shared/scale.ts +86 -0
  49. package/src/_shared/seriesCsv.ts +68 -0
  50. package/src/_shared/touch.ts +8 -0
  51. package/src/_shared/useChartFoundation.ts +175 -0
  52. package/src/_shared/useChartFullscreen.ts +254 -0
  53. package/src/_shared/useChartMenu.ts +111 -0
  54. package/src/_shared/useChartPadding.ts +235 -0
  55. package/src/_shared/useChartSize.ts +58 -0
  56. package/src/_shared/useChartTooltip.ts +205 -0
  57. package/src/env.d.ts +4 -0
  58. package/src/hsa-mapping.ts +1 -0
  59. package/src/index.ts +41 -0
  60. package/src/tooltip-position.ts +55 -0
  61. package/src/us-cities/data.ts +1371 -0
  62. package/src/us-cities/index.ts +122 -0
  63. package/src/us-cities.ts +7 -0
@@ -0,0 +1,420 @@
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_OUTLINE_COLOR = "var(--color-bg-0, #fff)";
33
+ const DEFAULT_OUTLINE_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 a text line (between baseline and cap-height). Lands on the x-height
40
+ // middle for most fonts.
41
+ const LINE_CENTER_RATIO = 0.35;
42
+
43
+ interface TextRun {
44
+ text: string;
45
+ bold: boolean;
46
+ italic: boolean;
47
+ }
48
+
49
+ interface RenderedAnnotation {
50
+ lines: TextRun[][];
51
+ textX: number;
52
+ textY: number;
53
+ textAnchor: "start" | "middle" | "end";
54
+ dy: number;
55
+ fontSize: number;
56
+ fontWeight: string | number;
57
+ color: string;
58
+ outlineColor: string;
59
+ outlineWidth: number;
60
+ pointerPath: string;
61
+ lineColor: string;
62
+ lineWidth: number;
63
+ lineDash?: string;
64
+ arrow: boolean;
65
+ // Inline arrow geometry. Rendered as a triangle with explicit fill so it
66
+ // renders correctly in Safari (which doesn't support `context-stroke` on
67
+ // SVG markers). Present only when an arrow should be drawn.
68
+ arrowTip?: { x: number; y: number; angle: number };
69
+ arrowTransform: string;
70
+ rule?: { x1: number; y1: number; x2: number; y2: number };
71
+ }
72
+
73
+ function resolveDash(
74
+ dash: string | number | readonly number[] | undefined,
75
+ ): string | undefined {
76
+ if (dash === undefined) return undefined;
77
+ if (typeof dash === "number") return `${dash} ${dash}`;
78
+ if (typeof dash === "string") return dash;
79
+ return dash.join(" ");
80
+ }
81
+
82
+ /**
83
+ * Parse a single line for `**bold**` and `_italic_` runs. Markers
84
+ * compose (`**_both_**`) and are forgiving — an unclosed marker carries
85
+ * its state through the rest of the line.
86
+ */
87
+ function parseInline(line: string): TextRun[] {
88
+ const out: TextRun[] = [];
89
+ let bold = false;
90
+ let italic = false;
91
+ let buf = "";
92
+ const flush = () => {
93
+ if (buf) out.push({ text: buf, bold, italic });
94
+ buf = "";
95
+ };
96
+ for (let i = 0; i < line.length; i++) {
97
+ const ch = line[i];
98
+ if (ch === "*" && line[i + 1] === "*") {
99
+ flush();
100
+ bold = !bold;
101
+ i++;
102
+ } else if (ch === "_") {
103
+ flush();
104
+ italic = !italic;
105
+ } else {
106
+ buf += ch;
107
+ }
108
+ }
109
+ flush();
110
+ return out.length === 0 ? [{ text: "", bold: false, italic: false }] : out;
111
+ }
112
+
113
+ const items = computed<RenderedAnnotation[]>(() => {
114
+ const out: RenderedAnnotation[] = [];
115
+ for (const a of props.annotations) {
116
+ const projected = props.project(a.x, a.y);
117
+ if (!projected) continue;
118
+ if (!isFinite(projected.x) || !isFinite(projected.y)) continue;
119
+
120
+ const pointer = a.pointer ?? "curved";
121
+ const isRule = pointer.startsWith("rule");
122
+
123
+ // Rule pointers require known plot bounds.
124
+ if (isRule && !props.bounds) continue;
125
+
126
+ const { x: offsetX, y: offsetY } = a.offset;
127
+ const labelX = projected.x + offsetX;
128
+ const labelY = projected.y + offsetY;
129
+ const color = a.color ?? "currentColor";
130
+ const fontSize = a.fontSize ?? DEFAULT_FONT_SIZE;
131
+ const fontWeight = a.fontWeight ?? DEFAULT_FONT_WEIGHT;
132
+ const outlineColor = a.outlineColor ?? DEFAULT_OUTLINE_COLOR;
133
+ const outlineWidth = a.outlineWidth ?? DEFAULT_OUTLINE_WIDTH;
134
+ const lineColor = a.lineColor ?? color;
135
+ const lineWidth = a.lineWidth ?? DEFAULT_LINE_WIDTH;
136
+ const lineDash = resolveDash(a.lineDash);
137
+ const alignMap = { left: "start", center: "middle", right: "end" } as const;
138
+ const align =
139
+ a.align ?? (offsetX > 0 ? "left" : offsetX < 0 ? "right" : "center");
140
+ const textAnchor = alignMap[align];
141
+
142
+ const lines = a.text.split("\n").map(parseInline);
143
+
144
+ let rule: RenderedAnnotation["rule"];
145
+ let pointerPath = "";
146
+ let arrowTip: RenderedAnnotation["arrowTip"];
147
+ const wantArrow = !isRule && (a.arrow ?? true);
148
+ if (isRule && props.bounds) {
149
+ rule = computeRule(pointer, projected.x, projected.y, props.bounds);
150
+ } else {
151
+ const built = buildPointerPath(
152
+ projected.x,
153
+ projected.y,
154
+ labelX,
155
+ labelY,
156
+ fontSize,
157
+ lines.length,
158
+ pointer as "curved" | "straight" | "none",
159
+ );
160
+ pointerPath = built.path;
161
+ if (wantArrow && built.arrow) arrowTip = built.arrow;
162
+ }
163
+
164
+ out.push({
165
+ lines,
166
+ textX: labelX,
167
+ textY: labelY,
168
+ textAnchor,
169
+ dy: fontSize * LINE_HEIGHT_RATIO,
170
+ fontSize,
171
+ fontWeight,
172
+ color,
173
+ outlineColor,
174
+ outlineWidth,
175
+ pointerPath,
176
+ lineColor,
177
+ lineWidth,
178
+ lineDash,
179
+ arrow: wantArrow,
180
+ arrowTip,
181
+ arrowTransform: arrowTip
182
+ ? `translate(${arrowTip.x} ${arrowTip.y}) rotate(${arrowTip.angle})`
183
+ : "",
184
+ rule,
185
+ });
186
+ }
187
+ return out;
188
+ });
189
+
190
+ /**
191
+ * Endpoints for a rule line through the anchor.
192
+ * - `ruleX` / `ruleY`: full plot span on the named axis.
193
+ * - `ruleUp` / `ruleDown` / `ruleFromLeft` / `ruleFromRight`: partial
194
+ * rule from one plot edge in to the anchor.
195
+ */
196
+ function computeRule(
197
+ pointer: string,
198
+ ax: number,
199
+ ay: number,
200
+ b: ChartBounds,
201
+ ): { x1: number; y1: number; x2: number; y2: number } {
202
+ switch (pointer) {
203
+ case "ruleX":
204
+ return { x1: ax, y1: b.top, x2: ax, y2: b.bottom };
205
+ case "ruleY":
206
+ return { x1: b.left, y1: ay, x2: b.right, y2: ay };
207
+ case "ruleUp":
208
+ return { x1: ax, y1: b.bottom, x2: ax, y2: ay };
209
+ case "ruleDown":
210
+ return { x1: ax, y1: b.top, x2: ax, y2: ay };
211
+ case "ruleFromLeft":
212
+ return { x1: b.left, y1: ay, x2: ax, y2: ay };
213
+ case "ruleFromRight":
214
+ return { x1: b.right, y1: ay, x2: ax, y2: ay };
215
+ default:
216
+ return { x1: ax, y1: ay, x2: ax, y2: ay };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Build the pointer line from the anchor to the label.
222
+ *
223
+ * - When the label has offset in only one dimension, the line is
224
+ * straight (vertical or horizontal) ending at the label's baseline.
225
+ * - When both dimensions have offset, the line is a quarter-arc:
226
+ * a quadratic Bezier with the control point at `(anchorX, targetY)`
227
+ * where `targetY` is the visual center of the text line nearest the
228
+ * anchor (slightly above its baseline). The curve emerges from the
229
+ * anchor vertically toward the label's row, then bends horizontally
230
+ * into the label.
231
+ *
232
+ * `targetY` tracks the *near* edge of the text block: the first line
233
+ * when the label sits below the anchor (connector points up at the top
234
+ * line), the last line when the label sits above the anchor (connector
235
+ * points up at the bottom line) — so multi-line text above the point
236
+ * connects to its bottom edge instead of having the line cut through it.
237
+ */
238
+ interface PointerGeom {
239
+ path: string;
240
+ // Tip position and rotation (degrees) for the inline arrow head. The
241
+ // arrow points opposite the path's start tangent — matching the look of
242
+ // `marker-start` with `orient="auto-start-reverse"`, but rendered as a
243
+ // plain triangle so the color works in Safari.
244
+ arrow?: { x: number; y: number; angle: number };
245
+ }
246
+
247
+ function buildPointerPath(
248
+ ax: number,
249
+ ay: number,
250
+ lx: number,
251
+ ly: number,
252
+ fontSize: number,
253
+ lineCount: number,
254
+ pointer: "curved" | "straight" | "none",
255
+ ): PointerGeom {
256
+ if (pointer === "none") return { path: "" };
257
+ const dx = lx - ax;
258
+ const dy = ly - ay;
259
+
260
+ // Aim at the visual center of the text line nearest the anchor: the
261
+ // first (top) line when the label is below the anchor, the last
262
+ // (bottom) line when the label is above it. That keeps the connector
263
+ // attached to the block's near edge instead of cutting through the
264
+ // lower lines of text that sits above the point.
265
+ const lineHeight = fontSize * LINE_HEIGHT_RATIO;
266
+ const nearestBaseline = dy < 0 ? ly + (lineCount - 1) * lineHeight : ly;
267
+ const targetY = nearestBaseline - fontSize * LINE_CENTER_RATIO;
268
+
269
+ // Pure horizontal or vertical → straight line at baseline, no curve.
270
+ // Force straight when explicitly requested.
271
+ if (dx === 0 || dy === 0 || pointer === "straight") {
272
+ // For straight pointers, aim at the near-line center (so multi-line
273
+ // text above the point connects to its bottom edge) — but only when
274
+ // the anchor isn't already at exactly the same Y (pure horizontal
275
+ // offset). The pure horizontal case stays on the baseline so it's a
276
+ // real horizontal line.
277
+ const ey = dy === 0 ? ly : targetY;
278
+ const segDx = lx - ax;
279
+ const segDy = ey - ay;
280
+ const len = Math.hypot(segDx, segDy);
281
+ if (len <= ANCHOR_GAP_PX + LABEL_GAP_PX) return { path: "" };
282
+ const ux = segDx / len;
283
+ const uy = segDy / len;
284
+ const sx = ax + ux * ANCHOR_GAP_PX;
285
+ const sy = ay + uy * ANCHOR_GAP_PX;
286
+ const ex = lx - ux * LABEL_GAP_PX;
287
+ const eyClamped = ey - uy * LABEL_GAP_PX;
288
+ // Arrow points back toward the anchor (opposite of (ux, uy)).
289
+ const angle = (Math.atan2(-uy, -ux) * 180) / Math.PI;
290
+ return {
291
+ path: `M${sx},${sy} L${ex},${eyClamped}`,
292
+ arrow: { x: sx, y: sy, angle },
293
+ };
294
+ }
295
+
296
+ const adjDy = targetY - ay;
297
+
298
+ // Skip the curve if one dimension is too small to clear its gap.
299
+ if (Math.abs(adjDy) <= ANCHOR_GAP_PX || Math.abs(dx) <= LABEL_GAP_PX) {
300
+ return { path: "" };
301
+ }
302
+
303
+ const xDir = Math.sign(dx);
304
+ const yDir = Math.sign(adjDy);
305
+ // Start the curve directly above/below the anchor so the arrow head
306
+ // lines up with the data point rather than sitting off to one side.
307
+ const sx = ax;
308
+ const sy = ay + yDir * ANCHOR_GAP_PX;
309
+ const ex = lx - xDir * LABEL_GAP_PX;
310
+ const ey = targetY;
311
+ // Control sits at (sx, targetY) so the curve emerges from the start
312
+ // tangent vertically and lands on the label horizontally —
313
+ // a clean quarter-arc shape. The start tangent is (0, yDir), so the
314
+ // arrow points (0, -yDir) — straight up when yDir=1, down when yDir=-1.
315
+ const angle = yDir > 0 ? -90 : 90;
316
+ return {
317
+ path: `M${sx},${sy} Q${sx},${targetY} ${ex},${ey}`,
318
+ arrow: { x: sx, y: sy, angle },
319
+ };
320
+ }
321
+ </script>
322
+
323
+ <template>
324
+ <g class="chart-annotations" pointer-events="none">
325
+ <template v-for="(item, i) in items" :key="i">
326
+ <!--
327
+ Outline pass. Each shape is rendered first in `outlineColor` at a
328
+ wider stroke so the rule line, pointer, and arrow tip stay legible
329
+ over busy series. Outline elements carry `class="annotation-outline"`
330
+ so consumers (and tests) can target the "real" shape directly.
331
+ Outlines deliberately drop `stroke-dasharray` — a dashed outline
332
+ would expose gaps where legibility matters most.
333
+ -->
334
+ <line
335
+ v-if="item.rule && item.outlineWidth > 0"
336
+ v-bind="item.rule"
337
+ class="annotation-outline"
338
+ :stroke="item.outlineColor"
339
+ :stroke-width="item.lineWidth + item.outlineWidth"
340
+ stroke-linecap="round"
341
+ />
342
+ <path
343
+ v-if="item.pointerPath && item.outlineWidth > 0"
344
+ :d="item.pointerPath"
345
+ class="annotation-outline"
346
+ fill="none"
347
+ :stroke="item.outlineColor"
348
+ :stroke-width="item.lineWidth + item.outlineWidth"
349
+ stroke-linecap="round"
350
+ />
351
+ <polygon
352
+ v-if="item.arrowTip && item.outlineWidth > 0"
353
+ points="0,0 -6,-3 -6,3"
354
+ class="annotation-outline"
355
+ :fill="item.outlineColor"
356
+ :stroke="item.outlineColor"
357
+ :stroke-width="item.outlineWidth"
358
+ stroke-linejoin="round"
359
+ :transform="item.arrowTransform"
360
+ />
361
+ <!-- Actual shapes. -->
362
+ <line
363
+ v-if="item.rule"
364
+ v-bind="item.rule"
365
+ :stroke="item.lineColor"
366
+ :stroke-width="item.lineWidth"
367
+ :stroke-dasharray="item.lineDash"
368
+ stroke-linecap="round"
369
+ />
370
+ <path
371
+ v-if="item.pointerPath"
372
+ :d="item.pointerPath"
373
+ fill="none"
374
+ :stroke="item.lineColor"
375
+ :stroke-width="item.lineWidth"
376
+ :stroke-dasharray="item.lineDash"
377
+ stroke-linecap="round"
378
+ />
379
+ <!--
380
+ Inline arrow head. Drawn as an explicit triangle (not via
381
+ `<marker>`) because Safari does not implement `context-stroke` on
382
+ marker fills, so a shared marker rendered as black instead of the
383
+ line color.
384
+ -->
385
+ <polygon
386
+ v-if="item.arrowTip"
387
+ points="0,0 -6,-3 -6,3"
388
+ :fill="item.lineColor"
389
+ :transform="item.arrowTransform"
390
+ />
391
+ <text
392
+ :x="item.textX"
393
+ :y="item.textY"
394
+ :text-anchor="item.textAnchor"
395
+ :font-size="item.fontSize"
396
+ :font-weight="item.fontWeight"
397
+ :fill="item.color"
398
+ :stroke="item.outlineColor"
399
+ :stroke-width="item.outlineWidth"
400
+ stroke-linejoin="round"
401
+ paint-order="stroke fill"
402
+ >
403
+ <tspan
404
+ v-for="(line, li) in item.lines"
405
+ :key="li"
406
+ :x="item.textX"
407
+ :dy="li === 0 ? 0 : item.dy"
408
+ >
409
+ <tspan
410
+ v-for="(run, ri) in line"
411
+ :key="ri"
412
+ :font-weight="run.bold ? BOLD_FONT_WEIGHT : undefined"
413
+ :font-style="run.italic ? 'italic' : undefined"
414
+ >{{ run.text }}</tspan
415
+ >
416
+ </tspan>
417
+ </text>
418
+ </template>
419
+ </g>
420
+ </template>
@@ -0,0 +1,138 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ canZoomIn?: boolean;
5
+ canZoomOut?: boolean;
6
+ /** Reset is a no-op at the identity transform — disable it there. */
7
+ canReset?: boolean;
8
+ /** Insets the stack further from the corner while the chart fills the
9
+ * window, matching the ChartMenu trigger area. */
10
+ isFullscreen?: boolean;
11
+ }>(),
12
+ { canZoomIn: true, canZoomOut: true, canReset: true, isFullscreen: false },
13
+ );
14
+
15
+ const emit = defineEmits<{
16
+ (e: "zoomIn"): void;
17
+ (e: "zoomOut"): void;
18
+ (e: "reset"): void;
19
+ }>();
20
+ </script>
21
+
22
+ <template>
23
+ <div
24
+ class="chart-zoom-controls"
25
+ :class="{ 'chart-zoom-controls--expanded': isFullscreen }"
26
+ >
27
+ <button
28
+ type="button"
29
+ class="chart-zoom-button"
30
+ aria-label="Zoom in"
31
+ :disabled="!canZoomIn"
32
+ @click="emit('zoomIn')"
33
+ >
34
+ <svg
35
+ width="14"
36
+ height="14"
37
+ viewBox="0 0 14 14"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-width="1.5"
41
+ stroke-linecap="round"
42
+ aria-hidden="true"
43
+ >
44
+ <path d="M7 2v10M2 7h10" />
45
+ </svg>
46
+ </button>
47
+ <button
48
+ type="button"
49
+ class="chart-zoom-button"
50
+ aria-label="Zoom out"
51
+ :disabled="!canZoomOut"
52
+ @click="emit('zoomOut')"
53
+ >
54
+ <svg
55
+ width="14"
56
+ height="14"
57
+ viewBox="0 0 14 14"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="1.5"
61
+ stroke-linecap="round"
62
+ aria-hidden="true"
63
+ >
64
+ <path d="M2 7h10" />
65
+ </svg>
66
+ </button>
67
+ <button
68
+ type="button"
69
+ class="chart-zoom-button"
70
+ aria-label="Reset view"
71
+ :disabled="!canReset"
72
+ @click="emit('reset')"
73
+ >
74
+ <!-- Counterclockwise "reset" arrow (rotate-ccw). -->
75
+ <svg
76
+ width="14"
77
+ height="14"
78
+ viewBox="0 0 14 14"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ stroke-width="1.5"
82
+ stroke-linecap="round"
83
+ stroke-linejoin="round"
84
+ aria-hidden="true"
85
+ >
86
+ <path
87
+ d="M1.75 7a5.25 5.25 0 1 0 5.25-5.25c-1.48 0-2.9.6-3.94 1.6L1.75 4.7"
88
+ />
89
+ <path d="M1.75 1.75v2.95h2.95" />
90
+ </svg>
91
+ </button>
92
+ </div>
93
+ </template>
94
+
95
+ <style scoped>
96
+ .chart-zoom-controls {
97
+ position: absolute;
98
+ top: 0.5em;
99
+ left: 0.5em;
100
+ z-index: 1;
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: 4px;
104
+ }
105
+
106
+ /* Mirror .chart-menu-trigger-area--expanded: while the chart fills the
107
+ window the wrapper's padding doesn't move absolute children, so inset
108
+ further for modal-style breathing room. */
109
+ .chart-zoom-controls--expanded {
110
+ top: 1.25em;
111
+ left: 1.25em;
112
+ }
113
+
114
+ /* Same look as .chart-menu-button, but always visible — no hover reveal. */
115
+ .chart-zoom-button {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ width: 28px;
120
+ height: 28px;
121
+ padding: 0;
122
+ border: 1px solid var(--color-border, #e5e7eb);
123
+ border-radius: 0.25em;
124
+ background: var(--color-bg-0, #fff);
125
+ color: var(--color-text-secondary, #555);
126
+ cursor: pointer;
127
+ }
128
+
129
+ .chart-zoom-button:hover:not(:disabled) {
130
+ background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
131
+ color: var(--color-text);
132
+ }
133
+
134
+ .chart-zoom-button:disabled {
135
+ opacity: 0.4;
136
+ cursor: default;
137
+ }
138
+ </style>
@@ -0,0 +1,106 @@
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
+ * Color of the legibility outline drawn behind the text, pointer/rule
41
+ * line, and arrow tip. Defaults to `var(--color-bg-0, #fff)` so it
42
+ * matches the page background out of the box.
43
+ */
44
+ outlineColor?: string;
45
+ /**
46
+ * Outline stroke width in pixels. Applied to the text (via paint-order
47
+ * stroke), and added to `lineWidth` for the pointer/rule line and
48
+ * arrow tip outline pass. Set to `0` to disable the outline. Default: 3.
49
+ */
50
+ outlineWidth?: number;
51
+ /**
52
+ * Horizontal alignment of the label relative to its anchor position.
53
+ * When omitted, derived from the sign of `offset.x`: positive →
54
+ * `"left"`, negative → `"right"`, zero → `"center"`.
55
+ */
56
+ align?: "left" | "center" | "right";
57
+ /** Pointer- or rule-line color override. Defaults to `color`. */
58
+ lineColor?: string;
59
+ /** Pointer- or rule-line width in pixels. Default: 1. */
60
+ lineWidth?: number;
61
+ /**
62
+ * SVG `stroke-dasharray` for the pointer or rule line. Accepts the
63
+ * raw string form (`"4 4"`), a single number (uniform dash/gap), or
64
+ * an array of numbers. Default: solid line.
65
+ */
66
+ lineDash?: string | number | readonly number[];
67
+ /**
68
+ * Connector shape between anchor and label.
69
+ * - `"curved"` (default): quarter-arc emerging vertically from the
70
+ * anchor, landing horizontally at the label.
71
+ * - `"straight"`: single straight line from anchor to label.
72
+ * - `"none"`: no connector — just the text label is rendered.
73
+ * - `"ruleX"`: vertical rule at the annotation's `x` value spanning
74
+ * the full plot height. Label still positions via `offset`.
75
+ * - `"ruleY"`: horizontal rule at the annotation's `y` value spanning
76
+ * the full plot width.
77
+ * - `"ruleUp"`: vertical rule from the plot's bottom edge up to the
78
+ * anchor.
79
+ * - `"ruleDown"`: vertical rule from the plot's top edge down to the
80
+ * anchor.
81
+ * - `"ruleFromLeft"`: horizontal rule from the plot's left edge in to
82
+ * the anchor.
83
+ * - `"ruleFromRight"`: horizontal rule from the plot's right edge in
84
+ * to the anchor.
85
+ *
86
+ * When the offset is purely horizontal or vertical (and `pointer`
87
+ * isn't `"none"` or a rule), the pointer is always straight regardless
88
+ * of this setting. Rule pointers ignore `arrow`.
89
+ */
90
+ pointer?:
91
+ | "curved"
92
+ | "straight"
93
+ | "none"
94
+ | "ruleX"
95
+ | "ruleY"
96
+ | "ruleUp"
97
+ | "ruleDown"
98
+ | "ruleFromLeft"
99
+ | "ruleFromRight";
100
+ /**
101
+ * Whether to draw a small filled triangle at the anchor end of the
102
+ * connector line. Defaults to `true`. Set to `false` for an
103
+ * uncapped line. Ignored for rule pointers.
104
+ */
105
+ arrow?: boolean;
106
+ }