@cfasim-ui/docs 0.4.10 → 0.4.12
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 +54 -1
- package/charts/BarChart/BarChart.vue +248 -48
- package/charts/ChartMenu/ChartMenu.vue +3 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +8 -2
- package/charts/ChoroplethMap/ChoroplethMap.vue +102 -11
- package/charts/LineChart/LineChart.md +226 -9
- package/charts/LineChart/LineChart.vue +381 -120
- package/charts/_shared/ChartAnnotations.vue +56 -17
- package/charts/_shared/annotations.ts +14 -9
- package/charts/_shared/chartProps.ts +39 -0
- package/charts/_shared/dateAxis.ts +507 -0
- package/charts/_shared/fullscreen.css +51 -0
- package/charts/_shared/index.ts +25 -1
- package/charts/_shared/useChartFoundation.ts +57 -27
- package/charts/_shared/useChartFullscreen.ts +104 -0
- package/charts/_shared/useChartMenu.ts +19 -4
- package/charts/_shared/useChartPadding.ts +152 -18
- package/charts/_shared/useChartSize.ts +14 -5
- package/charts/hsa-mapping.ts +1 -0
- package/charts/index.ts +2 -1
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/useUrlParams.ts +203 -40
|
@@ -160,6 +160,54 @@ Use `value-tick-format` to format the value-axis labels. `tooltip-value-format`
|
|
|
160
160
|
</template>
|
|
161
161
|
</ComponentDemo>
|
|
162
162
|
|
|
163
|
+
### Date categories
|
|
164
|
+
|
|
165
|
+
When every entry of `categories` parses as a date (`YYYY-MM-DD` string
|
|
166
|
+
or `Date` instance), labels switch to date-aware formatting and the
|
|
167
|
+
chart thins them to clean date boundaries (week / month / year
|
|
168
|
+
depending on range). Bar positions stay ordinal — every category gets
|
|
169
|
+
a bar — only the labels are thinned.
|
|
170
|
+
|
|
171
|
+
<ComponentDemo>
|
|
172
|
+
<BarChart
|
|
173
|
+
:data="[12, 18, 24, 35, 41, 38, 29, 21, 14, 9, 7, 4]"
|
|
174
|
+
:categories="['2026-01-05','2026-01-12','2026-01-19','2026-01-26','2026-02-02','2026-02-09','2026-02-16','2026-02-23','2026-03-02','2026-03-09','2026-03-16','2026-03-23']"
|
|
175
|
+
:height="220"
|
|
176
|
+
y-label="% ED visits"
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
<template #code>
|
|
180
|
+
|
|
181
|
+
```vue
|
|
182
|
+
<BarChart
|
|
183
|
+
:data="[12, 18, 24, 35, 41, 38, 29, 21, 14, 9, 7, 4]"
|
|
184
|
+
:categories="[
|
|
185
|
+
'2026-01-05',
|
|
186
|
+
'2026-01-12',
|
|
187
|
+
'2026-01-19',
|
|
188
|
+
'2026-01-26',
|
|
189
|
+
'2026-02-02',
|
|
190
|
+
'2026-02-09',
|
|
191
|
+
'2026-02-16',
|
|
192
|
+
'2026-02-23',
|
|
193
|
+
'2026-03-02',
|
|
194
|
+
'2026-03-09',
|
|
195
|
+
'2026-03-16',
|
|
196
|
+
'2026-03-23',
|
|
197
|
+
]"
|
|
198
|
+
:height="220"
|
|
199
|
+
y-label="% ED visits"
|
|
200
|
+
/>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
</template>
|
|
204
|
+
</ComponentDemo>
|
|
205
|
+
|
|
206
|
+
Use `date-format` to override the auto-picked preset. Same values as
|
|
207
|
+
LineChart's `x-tick-format` (e.g. `"iso"`, `"month-year"`, an
|
|
208
|
+
`Intl.DateTimeFormatOptions` object, or a `(ms, unit?) => string`
|
|
209
|
+
function).
|
|
210
|
+
|
|
163
211
|
### Logarithmic value axis
|
|
164
212
|
|
|
165
213
|
Set `value-scale-type="log"` to switch the value axis to base-10 log
|
|
@@ -313,6 +361,10 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
313
361
|
| `width` | `number` | No | — |
|
|
314
362
|
| `height` | `number` | No | — |
|
|
315
363
|
| `title` | `string` | No | — |
|
|
364
|
+
| `titleStyle` | `TitleStyle` | No | — |
|
|
365
|
+
| `axisLabelStyle` | `LabelStyle` | No | — |
|
|
366
|
+
| `tickLabelStyle` | `LabelStyle` | No | — |
|
|
367
|
+
| `legendStyle` | `LabelStyle` | No | — |
|
|
316
368
|
| `xLabel` | `string` | No | — |
|
|
317
369
|
| `yLabel` | `string` | No | — |
|
|
318
370
|
| `debounce` | `number` | No | — |
|
|
@@ -329,7 +381,7 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
329
381
|
| `data` | `BarChartData` | No | — |
|
|
330
382
|
| `y` | `BarChartData` | No | — |
|
|
331
383
|
| `series` | `BarSeries[]` | No | — |
|
|
332
|
-
| `categories` | `readonly string[]` | No | — |
|
|
384
|
+
| `categories` | `readonly (string \| Date)[]` | No | — |
|
|
333
385
|
| `orientation` | `"vertical" \| "horizontal"` | No | `"vertical"` |
|
|
334
386
|
| `layout` | `"grouped" \| "stacked"` | No | `"grouped"` |
|
|
335
387
|
| `valueMin` | `number` | No | `0` |
|
|
@@ -337,6 +389,7 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
337
389
|
| `valueTicks` | `number \| number[]` | No | — |
|
|
338
390
|
| `valueTickFormat` | `NumberFormat` | No | — |
|
|
339
391
|
| `categoryFormat` | `(label: string, index: number) => string` | No | — |
|
|
392
|
+
| `dateFormat` | `DateFormat` | No | — |
|
|
340
393
|
| `barPadding` | `number` | No | `0.2` |
|
|
341
394
|
| `groupGap` | `number` | No | `1` |
|
|
342
395
|
| `valueGrid` | `boolean` | No | `true` |
|
|
@@ -13,10 +13,25 @@ import {
|
|
|
13
13
|
useChartFoundation,
|
|
14
14
|
makeTooltipValueFormatter,
|
|
15
15
|
ChartAnnotations,
|
|
16
|
+
INLINE_LEGEND_ROW_HEIGHT,
|
|
17
|
+
TITLE_LINE_HEIGHT,
|
|
18
|
+
TITLE_FONT_SIZE,
|
|
19
|
+
TITLE_FONT_WEIGHT,
|
|
20
|
+
AXIS_LABEL_FONT_SIZE,
|
|
21
|
+
TICK_LABEL_FONT_SIZE,
|
|
22
|
+
TICK_LABEL_OPACITY,
|
|
23
|
+
LEGEND_FONT_SIZE,
|
|
24
|
+
resolveLabelStyle,
|
|
25
|
+
parseDate,
|
|
26
|
+
isAllDates,
|
|
27
|
+
pickDateTicks,
|
|
28
|
+
formatDate,
|
|
16
29
|
type ChartData,
|
|
30
|
+
type DateFormat,
|
|
17
31
|
type ChartCommonProps,
|
|
18
32
|
type ChartHoverPayload,
|
|
19
33
|
type ChartTooltipBaseProps,
|
|
34
|
+
type ChartTooltipValue,
|
|
20
35
|
} from "../_shared/index.js";
|
|
21
36
|
|
|
22
37
|
export type BarChartData = ChartData;
|
|
@@ -29,6 +44,16 @@ export interface BarSeries {
|
|
|
29
44
|
opacity?: number;
|
|
30
45
|
/** Label shown in the inline legend. */
|
|
31
46
|
legend?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Whether this series appears in the inline legend. Defaults to true.
|
|
49
|
+
* Has no effect when `legend` is unset (no legend entry to begin with).
|
|
50
|
+
*/
|
|
51
|
+
showInLegend?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether this series contributes a value to the tooltip. Defaults to
|
|
54
|
+
* true. The bars are still drawn.
|
|
55
|
+
*/
|
|
56
|
+
showInTooltip?: boolean;
|
|
32
57
|
}
|
|
33
58
|
|
|
34
59
|
interface BarChartProps extends ChartCommonProps {
|
|
@@ -41,8 +66,11 @@ interface BarChartProps extends ChartCommonProps {
|
|
|
41
66
|
/**
|
|
42
67
|
* Category labels for the categorical axis. Length should match the
|
|
43
68
|
* longest series. When omitted, indices (0, 1, 2, ...) are used.
|
|
69
|
+
* Accepts date strings or `Date` objects: when every category parses
|
|
70
|
+
* as a date, label formatting switches to date mode (bar positions
|
|
71
|
+
* stay ordinal — they are not time-proportional).
|
|
44
72
|
*/
|
|
45
|
-
categories?: readonly string[];
|
|
73
|
+
categories?: readonly (string | Date)[];
|
|
46
74
|
/** "vertical" (default, aka column) draws upright bars; "horizontal" draws sideways. */
|
|
47
75
|
orientation?: "vertical" | "horizontal";
|
|
48
76
|
/** "grouped" (default) places series side-by-side; "stacked" stacks them. */
|
|
@@ -71,6 +99,16 @@ interface BarChartProps extends ChartCommonProps {
|
|
|
71
99
|
valueTickFormat?: NumberFormat;
|
|
72
100
|
/** Formatter for category-axis labels. Receives the resolved category string. */
|
|
73
101
|
categoryFormat?: (label: string, index: number) => string;
|
|
102
|
+
/**
|
|
103
|
+
* Date-tick formatter for date-mode category labels (auto-detected
|
|
104
|
+
* when every entry of `categories` parses as a date). Ignored unless
|
|
105
|
+
* the axis is in date mode. Accepts a `DateFormat` — preset name,
|
|
106
|
+
* `Intl.DateTimeFormatOptions`, or `(ms, unit?) => string`. When
|
|
107
|
+
* omitted, the formatter is picked from the chosen tick unit (e.g.
|
|
108
|
+
* monthly ticks → `"month-year"`). Has no effect when
|
|
109
|
+
* `categoryFormat` is also set.
|
|
110
|
+
*/
|
|
111
|
+
dateFormat?: DateFormat;
|
|
74
112
|
/**
|
|
75
113
|
* Fraction of each category slot reserved as gap between groups (0..1).
|
|
76
114
|
* Default 0.2 — i.e. bars/groups fill 80% of their slot.
|
|
@@ -104,8 +142,6 @@ defineSlots<{
|
|
|
104
142
|
tooltip?(props: ChartTooltipBaseProps & { category: string }): unknown;
|
|
105
143
|
}>();
|
|
106
144
|
|
|
107
|
-
const hasInlineLegend = computed(() => allSeries.value.some((s) => s.legend));
|
|
108
|
-
|
|
109
145
|
const EMPTY_DATA: readonly number[] = [];
|
|
110
146
|
|
|
111
147
|
type ResolvedSeries = {
|
|
@@ -113,6 +149,8 @@ type ResolvedSeries = {
|
|
|
113
149
|
color?: string;
|
|
114
150
|
opacity?: number;
|
|
115
151
|
legend?: string;
|
|
152
|
+
showInLegend?: boolean;
|
|
153
|
+
showInTooltip?: boolean;
|
|
116
154
|
};
|
|
117
155
|
|
|
118
156
|
function resolveSeries(s: BarSeries): ResolvedSeries {
|
|
@@ -121,6 +159,8 @@ function resolveSeries(s: BarSeries): ResolvedSeries {
|
|
|
121
159
|
color: s.color,
|
|
122
160
|
opacity: s.opacity,
|
|
123
161
|
legend: s.legend,
|
|
162
|
+
showInLegend: s.showInLegend,
|
|
163
|
+
showInTooltip: s.showInTooltip,
|
|
124
164
|
};
|
|
125
165
|
}
|
|
126
166
|
|
|
@@ -144,11 +184,34 @@ const categoryLabels = computed<string[]>(() => {
|
|
|
144
184
|
const n = categoryCount.value;
|
|
145
185
|
const labels = new Array<string>(n);
|
|
146
186
|
for (let i = 0; i < n; i++) {
|
|
147
|
-
|
|
187
|
+
const c = props.categories?.[i];
|
|
188
|
+
if (c instanceof Date) {
|
|
189
|
+
// Stable string form for CSV / tooltip. Date axis formatting
|
|
190
|
+
// (visible labels) goes through `categoryTickItems` separately.
|
|
191
|
+
labels[i] = c.toISOString().slice(0, 10);
|
|
192
|
+
} else {
|
|
193
|
+
labels[i] = c ?? String(i);
|
|
194
|
+
}
|
|
148
195
|
}
|
|
149
196
|
return labels;
|
|
150
197
|
});
|
|
151
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Parallel array of epoch-ms timestamps when every category parses as
|
|
201
|
+
* a date, otherwise `null`. Drives both date-aware tick thinning and
|
|
202
|
+
* the default tick label formatter.
|
|
203
|
+
*/
|
|
204
|
+
const categoryDatesMs = computed<number[] | null>(() => {
|
|
205
|
+
const cats = props.categories;
|
|
206
|
+
if (!cats || cats.length === 0) return null;
|
|
207
|
+
if (!isAllDates(cats, "utc")) return null;
|
|
208
|
+
const out = new Array<number>(cats.length);
|
|
209
|
+
for (let i = 0; i < cats.length; i++) {
|
|
210
|
+
out[i] = parseDate(cats[i], "utc") ?? NaN;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
});
|
|
214
|
+
|
|
152
215
|
const isVertical = computed(() => props.orientation === "vertical");
|
|
153
216
|
|
|
154
217
|
/** Extent of the value axis (across all series, accounting for stacking). */
|
|
@@ -439,8 +502,49 @@ interface CategoryTickItem {
|
|
|
439
502
|
}
|
|
440
503
|
|
|
441
504
|
const categoryTickItems = computed<CategoryTickItem[]>(() => {
|
|
442
|
-
const out: CategoryTickItem[] = [];
|
|
443
505
|
const n = categoryCount.value;
|
|
506
|
+
const dateMs = categoryDatesMs.value;
|
|
507
|
+
|
|
508
|
+
// Date mode: thin labels to the closest category index for each
|
|
509
|
+
// tick boundary picked by `pickDateTicks`. Bar positions stay
|
|
510
|
+
// ordinal — only the labels become date-aware.
|
|
511
|
+
if (dateMs && dateMs.length > 0) {
|
|
512
|
+
let min = Infinity;
|
|
513
|
+
let max = -Infinity;
|
|
514
|
+
for (const m of dateMs) {
|
|
515
|
+
if (!Number.isFinite(m)) continue;
|
|
516
|
+
if (m < min) min = m;
|
|
517
|
+
if (m > max) max = m;
|
|
518
|
+
}
|
|
519
|
+
if (!Number.isFinite(min) || min === max) return [];
|
|
520
|
+
const span = isVertical.value ? innerW.value : innerH.value;
|
|
521
|
+
const target = Math.max(2, Math.floor(span / 80));
|
|
522
|
+
const picked = pickDateTicks(min, max, target, "utc");
|
|
523
|
+
const unit = picked.unit;
|
|
524
|
+
const items: CategoryTickItem[] = [];
|
|
525
|
+
const seen = new Set<number>();
|
|
526
|
+
for (const tickMs of picked.values) {
|
|
527
|
+
let nearest = -1;
|
|
528
|
+
let best = Infinity;
|
|
529
|
+
for (let i = 0; i < dateMs.length; i++) {
|
|
530
|
+
const d = Math.abs(dateMs[i] - tickMs);
|
|
531
|
+
if (d < best) {
|
|
532
|
+
best = d;
|
|
533
|
+
nearest = i;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (nearest < 0 || seen.has(nearest)) continue;
|
|
537
|
+
seen.add(nearest);
|
|
538
|
+
const center = slotStart(nearest) + slotSize.value / 2;
|
|
539
|
+
const label = props.categoryFormat
|
|
540
|
+
? props.categoryFormat(categoryLabels.value[nearest], nearest)
|
|
541
|
+
: formatDate(dateMs[nearest], props.dateFormat, "utc", unit);
|
|
542
|
+
items.push({ label, pos: center, anchor: "middle" });
|
|
543
|
+
}
|
|
544
|
+
return items;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const out: CategoryTickItem[] = [];
|
|
444
548
|
const fmt = (label: string, i: number) =>
|
|
445
549
|
props.categoryFormat ? props.categoryFormat(label, i) : label;
|
|
446
550
|
for (let i = 0; i < n; i++) {
|
|
@@ -462,12 +566,16 @@ interface InlineLegendItem {
|
|
|
462
566
|
const inlineLegendItems = computed<InlineLegendItem[]>(() => {
|
|
463
567
|
const items: InlineLegendItem[] = [];
|
|
464
568
|
allSeries.value.forEach((s, i) => {
|
|
465
|
-
if (!s.legend) return;
|
|
569
|
+
if (!s.legend || s.showInLegend === false) return;
|
|
466
570
|
items.push({ label: s.legend, color: s.color ?? defaultColor(i) });
|
|
467
571
|
});
|
|
468
572
|
return items;
|
|
469
573
|
});
|
|
470
574
|
|
|
575
|
+
const inlineLegendLabels = computed(() =>
|
|
576
|
+
inlineLegendItems.value.map((item) => item.label),
|
|
577
|
+
);
|
|
578
|
+
|
|
471
579
|
function toCsv(): string {
|
|
472
580
|
if (typeof props.csv === "function") return props.csv();
|
|
473
581
|
if (typeof props.csv === "string") return props.csv;
|
|
@@ -514,6 +622,7 @@ const {
|
|
|
514
622
|
height,
|
|
515
623
|
padding,
|
|
516
624
|
legendY,
|
|
625
|
+
inlineLegendLayout,
|
|
517
626
|
innerW,
|
|
518
627
|
innerH,
|
|
519
628
|
bounds,
|
|
@@ -525,10 +634,12 @@ const {
|
|
|
525
634
|
downloadLinkText,
|
|
526
635
|
csvHref,
|
|
527
636
|
menuFilename,
|
|
637
|
+
isFullscreen,
|
|
528
638
|
} = useChartFoundation({
|
|
529
639
|
width: () => props.width,
|
|
530
640
|
height: () => props.height,
|
|
531
641
|
title: () => props.title,
|
|
642
|
+
titleStyle: () => props.titleStyle,
|
|
532
643
|
xLabel: () => props.xLabel,
|
|
533
644
|
yLabel: () => props.yLabel,
|
|
534
645
|
debounce: () => props.debounce,
|
|
@@ -538,30 +649,84 @@ const {
|
|
|
538
649
|
filename: () => props.filename,
|
|
539
650
|
downloadLink: () => props.downloadLink,
|
|
540
651
|
chartPadding: () => props.chartPadding,
|
|
541
|
-
|
|
652
|
+
inlineLegendLabels: () => inlineLegendLabels.value,
|
|
542
653
|
hasTooltipSlot: () => hasTooltipSlot.value,
|
|
543
654
|
getCsv: toCsv,
|
|
544
655
|
pointerToIndex,
|
|
545
656
|
onHover: (payload) => emit("hover", payload),
|
|
546
657
|
});
|
|
547
658
|
|
|
659
|
+
/** Resolved style for the x/y axis labels. */
|
|
660
|
+
const axisLabelResolved = computed(() =>
|
|
661
|
+
resolveLabelStyle(props.axisLabelStyle, { fontSize: AXIS_LABEL_FONT_SIZE }),
|
|
662
|
+
);
|
|
663
|
+
/** Resolved style for the axis tick labels. */
|
|
664
|
+
const tickLabelResolved = computed(() =>
|
|
665
|
+
resolveLabelStyle(props.tickLabelStyle, {
|
|
666
|
+
fontSize: TICK_LABEL_FONT_SIZE,
|
|
667
|
+
fillOpacity: TICK_LABEL_OPACITY,
|
|
668
|
+
}),
|
|
669
|
+
);
|
|
670
|
+
/** Resolved style for inline legend item labels. */
|
|
671
|
+
const legendResolved = computed(() =>
|
|
672
|
+
resolveLabelStyle(props.legendStyle, { fontSize: LEGEND_FONT_SIZE }),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
/** Resolved title style with defaults applied. */
|
|
676
|
+
const titleResolved = computed(() => {
|
|
677
|
+
const s = props.titleStyle;
|
|
678
|
+
const align = s?.align ?? "left";
|
|
679
|
+
const b = bounds.value;
|
|
680
|
+
const x =
|
|
681
|
+
align === "left"
|
|
682
|
+
? b.left
|
|
683
|
+
: align === "right"
|
|
684
|
+
? b.right
|
|
685
|
+
: b.left + b.width / 2;
|
|
686
|
+
const anchor =
|
|
687
|
+
align === "left" ? "start" : align === "right" ? "end" : "middle";
|
|
688
|
+
return {
|
|
689
|
+
lines: (props.title ?? "").split("\n"),
|
|
690
|
+
fontSize: s?.fontSize ?? TITLE_FONT_SIZE,
|
|
691
|
+
lineHeight: s?.lineHeight ?? TITLE_LINE_HEIGHT,
|
|
692
|
+
fontWeight: s?.fontWeight ?? TITLE_FONT_WEIGHT,
|
|
693
|
+
color: s?.color ?? "currentColor",
|
|
694
|
+
x,
|
|
695
|
+
anchor,
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
|
|
548
699
|
const hoveredCategoryLabel = computed(() => {
|
|
549
700
|
const i = hoverIndex.value;
|
|
550
701
|
if (i === null) return undefined;
|
|
702
|
+
// In date mode, format the tooltip label so it honors `dateFormat`
|
|
703
|
+
// (otherwise the user would see the raw ISO string while the ticks
|
|
704
|
+
// render with a nicer preset).
|
|
705
|
+
const dateMs = categoryDatesMs.value;
|
|
706
|
+
if (dateMs && Number.isFinite(dateMs[i])) {
|
|
707
|
+
return formatDate(dateMs[i], props.dateFormat, "utc");
|
|
708
|
+
}
|
|
551
709
|
return categoryLabels.value[i];
|
|
552
710
|
});
|
|
553
711
|
|
|
554
712
|
const hoverSlotProps = computed(() => {
|
|
555
713
|
const idx = hoverIndex.value;
|
|
556
714
|
if (idx === null) return null;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
715
|
+
const series = allSeries.value;
|
|
716
|
+
const values: ChartTooltipValue[] = [];
|
|
717
|
+
for (let i = 0; i < series.length; i++) {
|
|
718
|
+
const s = series[i];
|
|
719
|
+
if (s.showInTooltip === false) continue;
|
|
720
|
+
values.push({
|
|
561
721
|
value: Number(s.data[idx] ?? NaN),
|
|
562
722
|
color: s.color ?? defaultColor(i),
|
|
563
723
|
seriesIndex: i,
|
|
564
|
-
})
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
index: idx,
|
|
728
|
+
category: categoryLabels.value[idx] ?? String(idx),
|
|
729
|
+
values,
|
|
565
730
|
data: props.tooltipData?.[idx] ?? null,
|
|
566
731
|
};
|
|
567
732
|
});
|
|
@@ -586,39 +751,72 @@ const hoverBand = computed(() => {
|
|
|
586
751
|
h: slotSize.value,
|
|
587
752
|
};
|
|
588
753
|
});
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Legend items joined with their wrapped pixel positions. `x` is the
|
|
757
|
+
* left edge of the indicator; `y` is the center of the row.
|
|
758
|
+
*/
|
|
759
|
+
const positionedLegendItems = computed(() => {
|
|
760
|
+
const positions = inlineLegendLayout.value.positions;
|
|
761
|
+
const pad = padding.value.left;
|
|
762
|
+
const baseY = legendY.value;
|
|
763
|
+
return inlineLegendItems.value.map((item, i) => {
|
|
764
|
+
const pos = positions[i];
|
|
765
|
+
return {
|
|
766
|
+
...item,
|
|
767
|
+
x: pad + pos.x,
|
|
768
|
+
y: baseY + pos.row * INLINE_LEGEND_ROW_HEIGHT,
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
});
|
|
589
772
|
</script>
|
|
590
773
|
|
|
591
774
|
<template>
|
|
592
|
-
<div
|
|
775
|
+
<div
|
|
776
|
+
ref="containerRef"
|
|
777
|
+
class="bar-chart-wrapper"
|
|
778
|
+
:class="{ 'is-fullscreen': isFullscreen }"
|
|
779
|
+
>
|
|
593
780
|
<ChartMenu v-if="menu" :items="menuItems" />
|
|
781
|
+
<div class="chart-sr-only" aria-live="polite">
|
|
782
|
+
{{ isFullscreen ? "Chart expanded to fill window" : "" }}
|
|
783
|
+
</div>
|
|
594
784
|
<svg ref="svgRef" :width="width" :height="height">
|
|
595
785
|
<!-- title -->
|
|
596
786
|
<text
|
|
597
787
|
v-if="title"
|
|
598
|
-
:x="
|
|
599
|
-
:y="
|
|
600
|
-
text-anchor="
|
|
601
|
-
font-size="
|
|
602
|
-
font-weight="
|
|
603
|
-
fill="
|
|
788
|
+
:x="titleResolved.x"
|
|
789
|
+
:y="titleResolved.lineHeight"
|
|
790
|
+
:text-anchor="titleResolved.anchor"
|
|
791
|
+
:font-size="titleResolved.fontSize"
|
|
792
|
+
:font-weight="titleResolved.fontWeight"
|
|
793
|
+
:fill="titleResolved.color"
|
|
604
794
|
>
|
|
605
|
-
|
|
795
|
+
<tspan
|
|
796
|
+
v-for="(line, i) in titleResolved.lines"
|
|
797
|
+
:key="i"
|
|
798
|
+
:x="titleResolved.x"
|
|
799
|
+
:dy="i === 0 ? 0 : titleResolved.lineHeight"
|
|
800
|
+
>
|
|
801
|
+
{{ line }}
|
|
802
|
+
</tspan>
|
|
606
803
|
</text>
|
|
607
804
|
<!-- inline legend -->
|
|
608
|
-
<g v-if="
|
|
609
|
-
<template v-for="(item, i) in
|
|
805
|
+
<g v-if="positionedLegendItems.length > 0">
|
|
806
|
+
<template v-for="(item, i) in positionedLegendItems" :key="'ileg' + i">
|
|
610
807
|
<rect
|
|
611
|
-
:x="
|
|
612
|
-
:y="
|
|
808
|
+
:x="item.x"
|
|
809
|
+
:y="item.y - 5"
|
|
613
810
|
width="12"
|
|
614
811
|
height="10"
|
|
615
812
|
:fill="item.color"
|
|
616
813
|
/>
|
|
617
814
|
<text
|
|
618
|
-
:x="
|
|
619
|
-
:y="
|
|
620
|
-
font-size="
|
|
621
|
-
fill="
|
|
815
|
+
:x="item.x + 18"
|
|
816
|
+
:y="item.y + 4"
|
|
817
|
+
:font-size="legendResolved.fontSize"
|
|
818
|
+
:fill="legendResolved.fill"
|
|
819
|
+
:font-weight="legendResolved.fontWeight"
|
|
622
820
|
>
|
|
623
821
|
{{ item.label }}
|
|
624
822
|
</text>
|
|
@@ -675,9 +873,10 @@ const hoverBand = computed(() => {
|
|
|
675
873
|
:y="tick.pos"
|
|
676
874
|
text-anchor="end"
|
|
677
875
|
dominant-baseline="middle"
|
|
678
|
-
font-size="
|
|
679
|
-
fill="
|
|
680
|
-
|
|
876
|
+
:font-size="tickLabelResolved.fontSize"
|
|
877
|
+
:fill="tickLabelResolved.fill"
|
|
878
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
879
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
681
880
|
>
|
|
682
881
|
{{ tick.value }}
|
|
683
882
|
</text>
|
|
@@ -690,9 +889,10 @@ const hoverBand = computed(() => {
|
|
|
690
889
|
:x="tick.pos"
|
|
691
890
|
:y="padding.top + innerH + 16"
|
|
692
891
|
text-anchor="middle"
|
|
693
|
-
font-size="
|
|
694
|
-
fill="
|
|
695
|
-
|
|
892
|
+
:font-size="tickLabelResolved.fontSize"
|
|
893
|
+
:fill="tickLabelResolved.fill"
|
|
894
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
895
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
696
896
|
>
|
|
697
897
|
{{ tick.value }}
|
|
698
898
|
</text>
|
|
@@ -704,8 +904,9 @@ const hoverBand = computed(() => {
|
|
|
704
904
|
:y="0"
|
|
705
905
|
:transform="`translate(14, ${padding.top + innerH / 2}) rotate(-90)`"
|
|
706
906
|
text-anchor="middle"
|
|
707
|
-
font-size="
|
|
708
|
-
fill="
|
|
907
|
+
:font-size="axisLabelResolved.fontSize"
|
|
908
|
+
:fill="axisLabelResolved.fill"
|
|
909
|
+
:font-weight="axisLabelResolved.fontWeight"
|
|
709
910
|
>
|
|
710
911
|
{{ yLabel }}
|
|
711
912
|
</text>
|
|
@@ -718,9 +919,10 @@ const hoverBand = computed(() => {
|
|
|
718
919
|
:x="tick.pos"
|
|
719
920
|
:y="padding.top + innerH + 16"
|
|
720
921
|
:text-anchor="tick.anchor"
|
|
721
|
-
font-size="
|
|
722
|
-
fill="
|
|
723
|
-
|
|
922
|
+
:font-size="tickLabelResolved.fontSize"
|
|
923
|
+
:fill="tickLabelResolved.fill"
|
|
924
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
925
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
724
926
|
>
|
|
725
927
|
{{ tick.label }}
|
|
726
928
|
</text>
|
|
@@ -734,9 +936,10 @@ const hoverBand = computed(() => {
|
|
|
734
936
|
:y="tick.pos"
|
|
735
937
|
text-anchor="end"
|
|
736
938
|
dominant-baseline="middle"
|
|
737
|
-
font-size="
|
|
738
|
-
fill="
|
|
739
|
-
|
|
939
|
+
:font-size="tickLabelResolved.fontSize"
|
|
940
|
+
:fill="tickLabelResolved.fill"
|
|
941
|
+
:font-weight="tickLabelResolved.fontWeight"
|
|
942
|
+
:fill-opacity="tickLabelResolved.fillOpacity"
|
|
740
943
|
>
|
|
741
944
|
{{ tick.label }}
|
|
742
945
|
</text>
|
|
@@ -747,8 +950,9 @@ const hoverBand = computed(() => {
|
|
|
747
950
|
:x="padding.left + innerW / 2"
|
|
748
951
|
:y="height - 4"
|
|
749
952
|
text-anchor="middle"
|
|
750
|
-
font-size="
|
|
751
|
-
fill="
|
|
953
|
+
:font-size="axisLabelResolved.fontSize"
|
|
954
|
+
:fill="axisLabelResolved.fill"
|
|
955
|
+
:font-weight="axisLabelResolved.fontWeight"
|
|
752
956
|
>
|
|
753
957
|
{{ xLabel }}
|
|
754
958
|
</text>
|
|
@@ -837,10 +1041,6 @@ const hoverBand = computed(() => {
|
|
|
837
1041
|
width: 100%;
|
|
838
1042
|
}
|
|
839
1043
|
|
|
840
|
-
.bar-chart-wrapper:hover :deep(.chart-menu-button) {
|
|
841
|
-
opacity: 1;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
1044
|
.bar-chart-tooltip-label {
|
|
845
1045
|
font-weight: 600;
|
|
846
1046
|
margin-bottom: 0.25em;
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
export interface ChartMenuItem {
|
|
11
11
|
label: string;
|
|
12
12
|
action: () => void;
|
|
13
|
+
/** Sets aria-pressed on the menu item — use for toggle-style items. */
|
|
14
|
+
ariaPressed?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const props = withDefaults(
|
|
@@ -72,6 +74,7 @@ const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
|
72
74
|
v-for="item in items"
|
|
73
75
|
:key="item.label"
|
|
74
76
|
class="chart-menu-item"
|
|
77
|
+
:aria-pressed="item.ariaPressed"
|
|
75
78
|
@select="item.action"
|
|
76
79
|
>
|
|
77
80
|
{{ item.label }}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
3
|
import countiesTopoForPerf from "us-atlas/counties-10m.json";
|
|
4
|
-
import { fipsToHsa } from "@cfasim-ui/charts";
|
|
4
|
+
import { fipsToHsa } from "@cfasim-ui/charts/hsa-mapping";
|
|
5
5
|
|
|
6
6
|
// Build one row per county (~3,143) with a deterministic-ish value so the
|
|
7
7
|
// perf example can render every region with a custom tooltip.
|
|
@@ -478,6 +478,10 @@ feature (e.g. an HSA from a county via `fipsToHsa`). Pass both as a
|
|
|
478
478
|
parent HSA renders on top as a dashed overlay (`stroke: "#666"` here —
|
|
479
479
|
default is white).
|
|
480
480
|
|
|
481
|
+
`fipsToHsa` and `hsaNames` ship from the `@cfasim-ui/charts/hsa-mapping`
|
|
482
|
+
subpath so consumers that don't need HSA lookups don't pay for the
|
|
483
|
+
~25 KB of mapping data.
|
|
484
|
+
|
|
481
485
|
<ComponentDemo>
|
|
482
486
|
<ChoroplethMap
|
|
483
487
|
:topology="countiesTopo"
|
|
@@ -512,7 +516,7 @@ default is white).
|
|
|
512
516
|
```vue
|
|
513
517
|
<script setup>
|
|
514
518
|
import { ref, computed } from "vue";
|
|
515
|
-
import { fipsToHsa } from "@cfasim-ui/charts";
|
|
519
|
+
import { fipsToHsa } from "@cfasim-ui/charts/hsa-mapping";
|
|
516
520
|
|
|
517
521
|
const focusedCounty = ref(null);
|
|
518
522
|
const focus = computed(() => {
|
|
@@ -691,6 +695,8 @@ set `tooltip-trigger`.
|
|
|
691
695
|
| `height` | `number` | No | — |
|
|
692
696
|
| `colorScale` | `ChoroplethColorScale \| ThresholdStop[] \| CategoricalStop[]` | No | — |
|
|
693
697
|
| `title` | `string` | No | — |
|
|
698
|
+
| `titleStyle` | `TitleStyle` | No | — |
|
|
699
|
+
| `legendStyle` | `LabelStyle` | No | — |
|
|
694
700
|
| `noDataColor` | `string` | No | `"#ddd"` |
|
|
695
701
|
| `strokeColor` | `string` | No | `"#fff"` |
|
|
696
702
|
| `strokeWidth` | `number` | No | `0.5` |
|