@cfasim-ui/docs 0.4.5 → 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.
@@ -160,37 +160,149 @@ Use `value-tick-format` to format the value-axis labels. `tooltip-value-format`
160
160
  </template>
161
161
  </ComponentDemo>
162
162
 
163
+ ### Annotations
164
+
165
+ Pin callouts to specific bars with `annotations`. `x` is the category
166
+ index (fractional values land between categories — e.g. `x: 1.5` sits at
167
+ the boundary between categories 1 and 2). `y` is on the value axis. See
168
+ [`LineChart` → Annotations](./line-chart#annotations) for the full
169
+ `ChartAnnotation` shape.
170
+
171
+ <ComponentDemo>
172
+ <BarChart
173
+ :data="[12, 19, 7, 24, 16]"
174
+ :categories="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"
175
+ :annotations="[
176
+ { x: 3, y: 24, offset: { x: 18, y: -22 }, text: 'Peak day' },
177
+ ]"
178
+ :chart-padding="{ top: 32, right: 32 }"
179
+ :height="240"
180
+ x-label="Day"
181
+ y-label="Cases"
182
+ />
183
+
184
+ <template #code>
185
+
186
+ ```vue
187
+ <BarChart
188
+ :data="[12, 19, 7, 24, 16]"
189
+ :categories="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"
190
+ :annotations="[{ x: 3, y: 24, offset: { x: 18, y: -22 }, text: 'Peak day' }]"
191
+ :chart-padding="{ top: 32, right: 32 }"
192
+ :height="240"
193
+ x-label="Day"
194
+ y-label="Cases"
195
+ />
196
+ ```
197
+
198
+ </template>
199
+ </ComponentDemo>
200
+
201
+ `chart-padding` reserves extra space outside the plot so the annotation
202
+ label and pointer don't get clipped by the data area. Pass a number for
203
+ uniform padding or an object with `top` / `right` / `bottom` / `left`.
204
+
205
+ Set `pointer` to a rule value (`"ruleX"`, `"ruleY"`, `"ruleUp"`,
206
+ `"ruleDown"`, `"ruleFromLeft"`, `"ruleFromRight"`) to draw a line
207
+ through the anchor instead of the default curved connector. The first
208
+ two span the full plot; the latter four extend from one edge to the
209
+ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
210
+
211
+ <ComponentDemo>
212
+ <BarChart
213
+ :data="[12, 19, 7, 24, 16]"
214
+ :categories="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"
215
+ :annotations="[
216
+ {
217
+ x: 0,
218
+ y: 15.6,
219
+ offset: { x: 6, y: -6 },
220
+ text: 'Avg 15.6',
221
+ pointer: 'ruleY',
222
+ lineDash: '4 3',
223
+ },
224
+ {
225
+ x: 3,
226
+ y: 20,
227
+ offset: { x: 8, y: 4 },
228
+ text: 'Target hit',
229
+ pointer: 'ruleFromLeft',
230
+ lineColor: '#0a7',
231
+ },
232
+ ]"
233
+ :chart-padding="{ top: 24, right: 24 }"
234
+ :height="240"
235
+ x-label="Day"
236
+ y-label="Cases"
237
+ />
238
+
239
+ <template #code>
240
+
241
+ ```vue
242
+ <BarChart
243
+ :data="[12, 19, 7, 24, 16]"
244
+ :categories="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"
245
+ :annotations="[
246
+ {
247
+ x: 0,
248
+ y: 15.6,
249
+ offset: { x: 6, y: -6 },
250
+ text: 'Avg 15.6',
251
+ pointer: 'ruleY',
252
+ lineDash: '4 3',
253
+ },
254
+ {
255
+ x: 3,
256
+ y: 20,
257
+ offset: { x: 8, y: 4 },
258
+ text: 'Target hit',
259
+ pointer: 'ruleFromLeft',
260
+ lineColor: '#0a7',
261
+ },
262
+ ]"
263
+ :chart-padding="{ top: 24, right: 24 }"
264
+ :height="240"
265
+ x-label="Day"
266
+ y-label="Cases"
267
+ />
268
+ ```
269
+
270
+ </template>
271
+ </ComponentDemo>
272
+
163
273
  ## API
164
274
 
165
275
  ## Props
166
276
 
167
277
  | Prop | Type | Required | Default |
168
278
  |------|------|----------|---------|
169
- | `data` | `BarChartData` | No | — |
170
- | `y` | `BarChartData` | No | — |
171
- | `series` | `BarSeries[]` | No | — |
172
- | `categories` | `readonly string[]` | No | — |
173
- | `orientation` | `"vertical" \| "horizontal"` | No | `"vertical"` |
174
- | `layout` | `"grouped" \| "stacked"` | No | `"grouped"` |
175
279
  | `width` | `number` | No | — |
176
280
  | `height` | `number` | No | — |
177
281
  | `title` | `string` | No | — |
178
282
  | `xLabel` | `string` | No | — |
179
283
  | `yLabel` | `string` | No | — |
180
- | `valueMin` | `number` | No | `0` |
181
- | `valueTicks` | `number \| number[]` | No | — |
182
- | `valueTickFormat` | `(value: number) =&gt; string` | No | — |
183
- | `tooltipValueFormat` | `(value: number) =&gt; string` | No | — |
184
- | `categoryFormat` | `(label: string, index: number) =&gt; string` | No | — |
185
- | `barPadding` | `number` | No | `0.2` |
186
- | `groupGap` | `number` | No | `1` |
187
284
  | `debounce` | `number` | No | — |
188
285
  | `menu` | `boolean \| string` | No | `true` |
189
- | `valueGrid` | `boolean` | No | `true` |
190
286
  | `tooltipData` | `ArrayLike&lt;unknown&gt;` | No | — |
191
287
  | `tooltipTrigger` | `"hover" \| "click"` | No | — |
192
288
  | `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
289
+ | `tooltipValueFormat` | `(value: number) =&gt; string` | No | — |
193
290
  | `csv` | `string \| (() =&gt; string)` | No | — |
194
291
  | `filename` | `string` | No | — |
195
292
  | `downloadLink` | `boolean \| string` | No | — |
293
+ | `annotations` | `readonly ChartAnnotation[]` | No | — |
294
+ | `chartPadding` | `ChartPadding` | No | — |
295
+ | `data` | `BarChartData` | No | — |
296
+ | `y` | `BarChartData` | No | — |
297
+ | `series` | `BarSeries[]` | No | — |
298
+ | `categories` | `readonly string[]` | No | — |
299
+ | `orientation` | `"vertical" \| "horizontal"` | No | `"vertical"` |
300
+ | `layout` | `"grouped" \| "stacked"` | No | `"grouped"` |
301
+ | `valueMin` | `number` | No | `0` |
302
+ | `valueTicks` | `number \| number[]` | No | — |
303
+ | `valueTickFormat` | `(value: number) =&gt; string` | No | — |
304
+ | `categoryFormat` | `(label: string, index: number) =&gt; string` | No | — |
305
+ | `barPadding` | `number` | No | `0.2` |
306
+ | `groupGap` | `number` | No | `1` |
307
+ | `valueGrid` | `boolean` | No | `true` |
196
308
 
@@ -6,12 +6,13 @@ import {
6
6
  formatTick,
7
7
  computeTickValues,
8
8
  categoricalToCsv,
9
- useChartSize,
10
- useChartTooltip,
11
- useChartMenu,
12
- useChartPadding,
13
- INLINE_LEGEND_HEIGHT,
9
+ useChartFoundation,
10
+ makeTooltipValueFormatter,
11
+ ChartAnnotations,
14
12
  type ChartData,
13
+ type ChartCommonProps,
14
+ type ChartHoverPayload,
15
+ type ChartTooltipBaseProps,
15
16
  } from "../_shared/index.js";
16
17
 
17
18
  export type BarChartData = ChartData;
@@ -26,117 +27,67 @@ export interface BarSeries {
26
27
  legend?: string;
27
28
  }
28
29
 
29
- const props = withDefaults(
30
- defineProps<{
31
- /** Single-series values. Equivalent to `y`. */
32
- data?: BarChartData;
33
- /** Single-series values (alias for `data`). */
34
- y?: BarChartData;
35
- /** Multi-series mode. Each series has its own values. */
36
- series?: BarSeries[];
37
- /**
38
- * Category labels for the categorical axis. Length should match the
39
- * longest series. When omitted, indices (0, 1, 2, ...) are used.
40
- */
41
- categories?: readonly string[];
42
- /** "vertical" (default, aka column) draws upright bars; "horizontal" draws sideways. */
43
- orientation?: "vertical" | "horizontal";
44
- /** "grouped" (default) places series side-by-side; "stacked" stacks them. */
45
- layout?: "grouped" | "stacked";
46
- width?: number;
47
- height?: number;
48
- title?: string;
49
- xLabel?: string;
50
- yLabel?: string;
51
- /** Force the value axis to start at this value or lower (default 0). */
52
- valueMin?: number;
53
- /**
54
- * Tick placement on the value axis (numeric). Number = interval,
55
- * array = explicit values. When omitted, ticks are chosen automatically.
56
- */
57
- valueTicks?: number | number[];
58
- /** Formatter for value-axis tick labels. */
59
- valueTickFormat?: (value: number) => string;
60
- /**
61
- * Formatter for numeric values shown in the default tooltip. Receives
62
- * the raw value. Defaults to the same tick formatter used for axes.
63
- */
64
- tooltipValueFormat?: (value: number) => string;
65
- /** Formatter for category-axis labels. Receives the resolved category string. */
66
- categoryFormat?: (label: string, index: number) => string;
67
- /**
68
- * Fraction of each category slot reserved as gap between groups (0..1).
69
- * Default 0.2 i.e. bars/groups fill 80% of their slot.
70
- */
71
- barPadding?: number;
72
- /**
73
- * Pixel gap between bars within a single category group in `grouped` layout.
74
- * Default 1.
75
- */
76
- groupGap?: number;
77
- debounce?: number;
78
- menu?: boolean | string;
79
- valueGrid?: boolean;
80
- /**
81
- * Custom per-index data passed to the tooltip slot. Accepts a plain
82
- * array or any `ArrayLike` (e.g. a typed array column from a
83
- * `ModelOutput`).
84
- */
85
- tooltipData?: ArrayLike<unknown>;
86
- /** Tooltip activation mode. */
87
- tooltipTrigger?: "hover" | "click";
88
- /** Boundary for tooltip flip/clamp. Default "chart". */
89
- tooltipClamp?: "none" | "chart" | "window";
90
- /** Custom CSV content (string or function). When omitted, generated from the bars. */
91
- csv?: string | (() => string);
92
- /** Filename (without extension) for downloaded SVG, PNG, CSV. */
93
- filename?: string;
94
- /** Show a plain text link below the chart to download the CSV. */
95
- downloadLink?: boolean | string;
96
- }>(),
97
- {
98
- orientation: "vertical",
99
- layout: "grouped",
100
- valueMin: 0,
101
- barPadding: 0.2,
102
- groupGap: 1,
103
- menu: true,
104
- tooltipClamp: "chart",
105
- valueGrid: true,
106
- },
107
- );
30
+ interface BarChartProps extends ChartCommonProps {
31
+ /** Single-series values. Equivalent to `y`. */
32
+ data?: BarChartData;
33
+ /** Single-series values (alias for `data`). */
34
+ y?: BarChartData;
35
+ /** Multi-series mode. Each series has its own values. */
36
+ series?: BarSeries[];
37
+ /**
38
+ * Category labels for the categorical axis. Length should match the
39
+ * longest series. When omitted, indices (0, 1, 2, ...) are used.
40
+ */
41
+ categories?: readonly string[];
42
+ /** "vertical" (default, aka column) draws upright bars; "horizontal" draws sideways. */
43
+ orientation?: "vertical" | "horizontal";
44
+ /** "grouped" (default) places series side-by-side; "stacked" stacks them. */
45
+ layout?: "grouped" | "stacked";
46
+ /** Force the value axis to start at this value or lower (default 0). */
47
+ valueMin?: number;
48
+ /**
49
+ * Tick placement on the value axis (numeric). Number = interval,
50
+ * array = explicit values. When omitted, ticks are chosen automatically.
51
+ */
52
+ valueTicks?: number | number[];
53
+ /** Formatter for value-axis tick labels. */
54
+ valueTickFormat?: (value: number) => string;
55
+ /** Formatter for category-axis labels. Receives the resolved category string. */
56
+ categoryFormat?: (label: string, index: number) => string;
57
+ /**
58
+ * Fraction of each category slot reserved as gap between groups (0..1).
59
+ * Default 0.2 i.e. bars/groups fill 80% of their slot.
60
+ */
61
+ barPadding?: number;
62
+ /**
63
+ * Pixel gap between bars within a single category group in `grouped` layout.
64
+ * Default 1.
65
+ */
66
+ groupGap?: number;
67
+ valueGrid?: boolean;
68
+ }
69
+
70
+ const props = withDefaults(defineProps<BarChartProps>(), {
71
+ orientation: "vertical",
72
+ layout: "grouped",
73
+ valueMin: 0,
74
+ barPadding: 0.2,
75
+ groupGap: 1,
76
+ menu: true,
77
+ tooltipClamp: "chart",
78
+ valueGrid: true,
79
+ });
108
80
 
109
81
  const emit = defineEmits<{
110
- (e: "hover", payload: { index: number } | null): void;
82
+ (e: "hover", payload: ChartHoverPayload): void;
111
83
  }>();
112
84
 
113
85
  defineSlots<{
114
- tooltip?(props: {
115
- index: number;
116
- category: string;
117
- values: { value: number; color: string; seriesIndex: number }[];
118
- data: unknown;
119
- }): unknown;
86
+ tooltip?(props: ChartTooltipBaseProps & { category: string }): unknown;
120
87
  }>();
121
88
 
122
- const { containerRef, measuredWidth } = useChartSize({
123
- debounce: () => props.debounce,
124
- });
125
-
126
- const width = computed(() => props.width ?? (measuredWidth.value || 400));
127
- const height = computed(() => props.height ?? 200);
128
-
129
89
  const hasInlineLegend = computed(() => allSeries.value.some((s) => s.legend));
130
90
 
131
- const { padding, innerW, innerH } = useChartPadding({
132
- title: () => props.title,
133
- xLabel: () => props.xLabel,
134
- yLabel: () => props.yLabel,
135
- hasInlineLegend: () => hasInlineLegend.value,
136
- width: () => width.value,
137
- height: () => height.value,
138
- });
139
-
140
91
  const EMPTY_DATA: readonly number[] = [];
141
92
 
142
93
  type ResolvedSeries = {
@@ -408,11 +359,10 @@ function defaultColor(i: number): string {
408
359
  return DEFAULT_COLORS[i % DEFAULT_COLORS.length];
409
360
  }
410
361
 
411
- function formatTooltipValue(v: number): string {
412
- if (props.tooltipValueFormat) return props.tooltipValueFormat(v);
413
- if (props.valueTickFormat) return props.valueTickFormat(v);
414
- return formatTick(v);
415
- }
362
+ const formatTooltipValue = makeTooltipValueFormatter(
363
+ () => props.tooltipValueFormat,
364
+ () => props.valueTickFormat,
365
+ );
416
366
 
417
367
  const valueTickItems = computed(() => {
418
368
  const { min, max } = valueExtent.value;
@@ -489,6 +439,20 @@ const hasTooltipSlot = computed(
489
439
  () => !!props.tooltipData || !!props.tooltipTrigger,
490
440
  );
491
441
 
442
+ function projectAnnotation(
443
+ x: number,
444
+ y: number,
445
+ ): { x: number; y: number } | null {
446
+ if (!isFinite(x) || !isFinite(y)) return null;
447
+ if (slotSize.value === 0) return null;
448
+ const base = isVertical.value ? padding.value.left : padding.value.top;
449
+ const categoricalPx = base + (x + 0.5) * slotSize.value;
450
+ const valuePx = valuePixel(y);
451
+ return isVertical.value
452
+ ? { x: categoricalPx, y: valuePx }
453
+ : { x: valuePx, y: categoricalPx };
454
+ }
455
+
492
456
  function pointerToIndex(clientX: number, clientY: number): number | null {
493
457
  const rect = containerRef.value?.getBoundingClientRect();
494
458
  if (!rect) return null;
@@ -501,30 +465,41 @@ function pointerToIndex(clientX: number, clientY: number): number | null {
501
465
  }
502
466
 
503
467
  const {
468
+ containerRef,
469
+ svgRef,
470
+ width,
471
+ height,
472
+ padding,
473
+ legendY,
474
+ innerW,
475
+ innerH,
476
+ bounds,
504
477
  hoverIndex,
505
478
  tooltipRef,
506
479
  tooltipPos,
507
- handlers: tooltipHandlers,
508
- } = useChartTooltip({
509
- enabled: () => hasTooltipSlot.value,
510
- trigger: () => props.tooltipTrigger,
511
- clamp: () => props.tooltipClamp,
512
- pointerToIndex,
513
- containerRef,
514
- onHover: (payload) => emit("hover", payload),
515
- });
516
-
517
- const {
518
- svgRef,
519
- items: menuItems,
480
+ tooltipHandlers,
481
+ menuItems,
520
482
  downloadLinkText,
521
483
  csvHref,
522
- resolvedFilename: menuFilename,
523
- } = useChartMenu({
484
+ menuFilename,
485
+ } = useChartFoundation({
486
+ width: () => props.width,
487
+ height: () => props.height,
488
+ title: () => props.title,
489
+ xLabel: () => props.xLabel,
490
+ yLabel: () => props.yLabel,
491
+ debounce: () => props.debounce,
492
+ menu: () => props.menu,
493
+ tooltipTrigger: () => props.tooltipTrigger,
494
+ tooltipClamp: () => props.tooltipClamp,
524
495
  filename: () => props.filename,
525
- legacyMenuLabel: () => props.menu,
526
- getCsv: toCsv,
527
496
  downloadLink: () => props.downloadLink,
497
+ chartPadding: () => props.chartPadding,
498
+ hasInlineLegend: () => hasInlineLegend.value,
499
+ hasTooltipSlot: () => hasTooltipSlot.value,
500
+ getCsv: toCsv,
501
+ pointerToIndex,
502
+ onHover: (payload) => emit("hover", payload),
528
503
  });
529
504
 
530
505
  const hoveredCategoryLabel = computed(() => {
@@ -591,14 +566,14 @@ const hoverBand = computed(() => {
591
566
  <template v-for="(item, i) in inlineLegendItems" :key="'ileg' + i">
592
567
  <rect
593
568
  :x="padding.left + i * 120"
594
- :y="padding.top - INLINE_LEGEND_HEIGHT / 2 - 5"
569
+ :y="legendY - 5"
595
570
  width="12"
596
571
  height="10"
597
572
  :fill="item.color"
598
573
  />
599
574
  <text
600
575
  :x="padding.left + i * 120 + 18"
601
- :y="padding.top - INLINE_LEGEND_HEIGHT / 2 + 4"
576
+ :y="legendY + 4"
602
577
  font-size="11"
603
578
  fill="currentColor"
604
579
  >
@@ -759,6 +734,13 @@ const hoverBand = computed(() => {
759
734
  style="cursor: crosshair; touch-action: none"
760
735
  v-on="tooltipHandlers"
761
736
  />
737
+ <!-- annotations (top layer) -->
738
+ <ChartAnnotations
739
+ v-if="annotations && annotations.length > 0"
740
+ :annotations="annotations"
741
+ :project="projectAnnotation"
742
+ :bounds="bounds"
743
+ />
762
744
  </svg>
763
745
  <!-- Tooltip floating content -->
764
746
  <div