@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
|
@@ -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) => string` | No | — |
|
|
183
|
-
| `tooltipValueFormat` | `(value: number) => string` | No | — |
|
|
184
|
-
| `categoryFormat` | `(label: string, index: number) => 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<unknown>` | No | — |
|
|
191
287
|
| `tooltipTrigger` | `"hover" \| "click"` | No | — |
|
|
192
288
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
289
|
+
| `tooltipValueFormat` | `(value: number) => string` | No | — |
|
|
193
290
|
| `csv` | `string \| (() => 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) => string` | No | — |
|
|
304
|
+
| `categoryFormat` | `(label: string, index: number) => 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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:
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
508
|
-
|
|
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
|
-
|
|
523
|
-
} =
|
|
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="
|
|
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="
|
|
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
|
|
@@ -1,6 +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
5
|
|
|
5
6
|
// Build one row per county (~3,143) with a deterministic-ish value so the
|
|
6
7
|
// perf example can render every region with a custom tooltip.
|
|
@@ -16,6 +17,19 @@ const denseCountyData = computed(() => {
|
|
|
16
17
|
// handles click-to-toggle and emits null when the focused feature is
|
|
17
18
|
// re-clicked.
|
|
18
19
|
const focused = ref(null);
|
|
20
|
+
|
|
21
|
+
// "Outline a focused feature's parent" demo: focus is a county id;
|
|
22
|
+
// we derive the parent HSA and add it to the focus array as a dashed
|
|
23
|
+
// overlay so clicking a county also outlines its HSA.
|
|
24
|
+
const focusedCounty = ref(null);
|
|
25
|
+
const parentFocus = computed(() => {
|
|
26
|
+
const fips = focusedCounty.value;
|
|
27
|
+
if (!fips) return null;
|
|
28
|
+
const hsa = fipsToHsa[fips];
|
|
29
|
+
return hsa
|
|
30
|
+
? [fips, { id: hsa, geoType: "hsas", style: "dashed", stroke: "#666" }]
|
|
31
|
+
: fips;
|
|
32
|
+
});
|
|
19
33
|
</script>
|
|
20
34
|
|
|
21
35
|
# ChoroplethMap
|
|
@@ -407,6 +421,124 @@ const focused = ref(null);
|
|
|
407
421
|
</template>
|
|
408
422
|
</ComponentDemo>
|
|
409
423
|
|
|
424
|
+
### Color by HSA, interact by county (`dataGeoType`)
|
|
425
|
+
|
|
426
|
+
Set `dataGeoType` when your data is keyed by a coarser geography than
|
|
427
|
+
the one you want to render. Each county fills with its parent HSA's
|
|
428
|
+
value (via the built-in FIPS → HSA mapping); hover, click, and `focus`
|
|
429
|
+
still operate on the county geometry, and you can layer an HSA outline
|
|
430
|
+
on top with a `FocusItem`.
|
|
431
|
+
|
|
432
|
+
<ComponentDemo>
|
|
433
|
+
<ChoroplethMap
|
|
434
|
+
:topology="countiesTopo"
|
|
435
|
+
geo-type="counties"
|
|
436
|
+
data-geo-type="hsas"
|
|
437
|
+
:pan="true"
|
|
438
|
+
:zoom="true"
|
|
439
|
+
:data="[
|
|
440
|
+
{ id: '060737', value: 80 },
|
|
441
|
+
{ id: '060723', value: 60 },
|
|
442
|
+
{ id: '060757', value: 45 },
|
|
443
|
+
{ id: '060807', value: 35 },
|
|
444
|
+
{ id: '060768', value: 25 },
|
|
445
|
+
{ id: '060774', value: 50 },
|
|
446
|
+
]"
|
|
447
|
+
:focus="[
|
|
448
|
+
{ id: '06043' },
|
|
449
|
+
{ id: '060737', geoType: 'hsas', style: 'dashed' },
|
|
450
|
+
]"
|
|
451
|
+
:focus-zoom-level="6"
|
|
452
|
+
title="HSA-keyed data on a county map"
|
|
453
|
+
:legend-title="'Cases'"
|
|
454
|
+
:height="400"
|
|
455
|
+
/>
|
|
456
|
+
|
|
457
|
+
<template #code>
|
|
458
|
+
|
|
459
|
+
```vue
|
|
460
|
+
<ChoroplethMap
|
|
461
|
+
:topology="countiesTopo"
|
|
462
|
+
geo-type="counties"
|
|
463
|
+
data-geo-type="hsas"
|
|
464
|
+
:data="hsaData"
|
|
465
|
+
:focus="[{ id: '06043' }, { id: '060737', geoType: 'hsas', style: 'dashed' }]"
|
|
466
|
+
title="HSA-keyed data on a county map"
|
|
467
|
+
/>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
</template>
|
|
471
|
+
</ComponentDemo>
|
|
472
|
+
|
|
473
|
+
### Outline a focused feature's parent
|
|
474
|
+
|
|
475
|
+
Use `v-model:focus` together with a computed that derives a parent
|
|
476
|
+
feature (e.g. an HSA from a county via `fipsToHsa`). Pass both as a
|
|
477
|
+
`FocusItem` array — the focused county lights up as usual and the
|
|
478
|
+
parent HSA renders on top as a dashed overlay (`stroke: "#666"` here —
|
|
479
|
+
default is white).
|
|
480
|
+
|
|
481
|
+
<ComponentDemo>
|
|
482
|
+
<ChoroplethMap
|
|
483
|
+
:topology="countiesTopo"
|
|
484
|
+
geo-type="counties"
|
|
485
|
+
data-geo-type="hsas"
|
|
486
|
+
:pan="true"
|
|
487
|
+
:zoom="true"
|
|
488
|
+
:focus="parentFocus"
|
|
489
|
+
@update:focus="focusedCounty = typeof $event === 'string' ? $event : null"
|
|
490
|
+
:data="[
|
|
491
|
+
{ id: '060737', value: 80 },
|
|
492
|
+
{ id: '060723', value: 60 },
|
|
493
|
+
{ id: '060757', value: 45 },
|
|
494
|
+
{ id: '060807', value: 35 },
|
|
495
|
+
{ id: '060768', value: 25 },
|
|
496
|
+
{ id: '060774', value: 50 },
|
|
497
|
+
]"
|
|
498
|
+
:focus-zoom-level="6"
|
|
499
|
+
title="Click a county to outline its HSA"
|
|
500
|
+
:legend-title="'Cases'"
|
|
501
|
+
:height="400"
|
|
502
|
+
>
|
|
503
|
+
<template #tooltip="{ name, value }">
|
|
504
|
+
<div style="font-weight: 600">{{ name }}</div>
|
|
505
|
+
<div v-if="value != null">Cases: {{ value }}</div>
|
|
506
|
+
<div v-else style="opacity: 0.6">No data</div>
|
|
507
|
+
</template>
|
|
508
|
+
</ChoroplethMap>
|
|
509
|
+
|
|
510
|
+
<template #code>
|
|
511
|
+
|
|
512
|
+
```vue
|
|
513
|
+
<script setup>
|
|
514
|
+
import { ref, computed } from "vue";
|
|
515
|
+
import { fipsToHsa } from "@cfasim-ui/charts";
|
|
516
|
+
|
|
517
|
+
const focusedCounty = ref(null);
|
|
518
|
+
const focus = computed(() => {
|
|
519
|
+
const fips = focusedCounty.value;
|
|
520
|
+
if (!fips) return null;
|
|
521
|
+
const hsa = fipsToHsa[fips];
|
|
522
|
+
return hsa
|
|
523
|
+
? [fips, { id: hsa, geoType: "hsas", style: "dashed", stroke: "#666" }]
|
|
524
|
+
: fips;
|
|
525
|
+
});
|
|
526
|
+
</script>
|
|
527
|
+
|
|
528
|
+
<ChoroplethMap
|
|
529
|
+
:topology="countiesTopo"
|
|
530
|
+
geo-type="counties"
|
|
531
|
+
data-geo-type="hsas"
|
|
532
|
+
:data="hsaData"
|
|
533
|
+
:focus="focus"
|
|
534
|
+
@update:focus="focusedCounty = typeof $event === 'string' ? $event : null"
|
|
535
|
+
title="Click a county to outline its HSA"
|
|
536
|
+
/>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
</template>
|
|
540
|
+
</ComponentDemo>
|
|
541
|
+
|
|
410
542
|
### Custom tooltip number format
|
|
411
543
|
|
|
412
544
|
Pass `tooltip-value-format` to format numeric values shown in the tooltip
|
|
@@ -554,6 +686,7 @@ set `tooltip-trigger`.
|
|
|
554
686
|
| `topology` | `Topology` | Yes | — |
|
|
555
687
|
| `data` | `StateData[]` | No | — |
|
|
556
688
|
| `geoType` | `GeoType` | No | `"states"` |
|
|
689
|
+
| `dataGeoType` | `GeoType` | No | — |
|
|
557
690
|
| `width` | `number` | No | — |
|
|
558
691
|
| `height` | `number` | No | — |
|
|
559
692
|
| `colorScale` | `ChoroplethColorScale \| ThresholdStop[] \| CategoricalStop[]` | No | — |
|
|
@@ -573,7 +706,7 @@ set `tooltip-trigger`.
|
|
|
573
706
|
| `value` | `number \| string` | No | — |
|
|
574
707
|
| `tooltipValueFormat` | `(value: number) => string` | No | — |
|
|
575
708
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
576
|
-
| `focus` | `
|
|
709
|
+
| `focus` | `FocusValue` | No | — |
|
|
577
710
|
| `focusZoomLevel` | `number` | No | `4` |
|
|
578
711
|
|
|
579
712
|
|
|
@@ -624,3 +757,22 @@ interface CategoricalStop {
|
|
|
624
757
|
color: string;
|
|
625
758
|
}
|
|
626
759
|
```
|
|
760
|
+
|
|
761
|
+
### FocusItem
|
|
762
|
+
|
|
763
|
+
The `focus` prop accepts a bare id, a `FocusItem`, or an array of either. Use objects when you want to pin features from a different `geoType` than the base map, or pick a non-default outline style.
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
interface FocusItem {
|
|
767
|
+
/** Feature id (FIPS code, HSA code) or name. */
|
|
768
|
+
id: string;
|
|
769
|
+
/** Defaults to the map's geoType. Cross-geoType items render as
|
|
770
|
+
* non-interactive outlines on top of the base map. */
|
|
771
|
+
geoType?: "states" | "counties" | "hsas";
|
|
772
|
+
/** Outline style. "solid" (default) matches the hover highlight;
|
|
773
|
+
* "dashed" uses long dashes; "dotted" uses small round dots. */
|
|
774
|
+
style?: "solid" | "dashed" | "dotted";
|
|
775
|
+
/** Stroke color for cross-geoType overlay paths. Default: "#fff". */
|
|
776
|
+
stroke?: string;
|
|
777
|
+
}
|
|
778
|
+
```
|