@cfasim-ui/docs 0.5.0 → 0.6.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.
- package/charts/BarChart/BarChart.md +3 -0
- package/charts/BarChart/BarChart.vue +342 -317
- package/charts/ChartMenu/ChartMenu.vue +75 -5
- package/charts/ChoroplethMap/ChoroplethMap.md +5 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +193 -88
- package/charts/LineChart/LineChart.md +21 -4
- package/charts/LineChart/LineChart.vue +398 -367
- package/charts/_shared/chartProps.ts +10 -0
- package/charts/_shared/useChartFoundation.ts +15 -0
- package/charts/_shared/useChartFullscreen.ts +66 -9
- package/charts/_shared/useChartMenu.ts +13 -6
- package/charts/_shared/useChartTooltip.ts +57 -4
- package/charts/index.ts +0 -2
- package/components/ButtonGroup/ButtonGroup.md +64 -0
- package/components/ButtonGroup/ButtonGroup.vue +91 -0
- package/components/MultiSelect/MultiSelect.md +143 -0
- package/components/MultiSelect/MultiSelect.vue +338 -0
- package/components/ParamEditor/ParamEditor.md +19 -0
- package/components/ParamEditor/ParamEditor.vue +4 -0
- package/components/ParamEditor/ParamEditorImpl.vue +3 -3
- package/components/ToggleGroup/ToggleGroup.md +163 -0
- package/components/ToggleGroup/ToggleGroup.vue +143 -0
- package/components/index.ts +5 -0
- package/index.json +47 -1
- package/package.json +1 -1
- package/charts/_shared/fullscreen.css +0 -51
|
@@ -19,18 +19,48 @@ const props = withDefaults(
|
|
|
19
19
|
items: ChartMenuItem[];
|
|
20
20
|
/** Force the dropdown style even when only one item is provided. */
|
|
21
21
|
forceDropdown?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* When the chart is expanded, the menu trigger is replaced by a
|
|
24
|
+
* persistent close (✕) button that emits `close`.
|
|
25
|
+
*/
|
|
26
|
+
isFullscreen?: boolean;
|
|
22
27
|
}>(),
|
|
23
|
-
{ forceDropdown: false },
|
|
28
|
+
{ forceDropdown: false, isFullscreen: false },
|
|
24
29
|
);
|
|
25
30
|
|
|
31
|
+
const emit = defineEmits<{ (e: "close"): void }>();
|
|
32
|
+
|
|
26
33
|
const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
27
34
|
</script>
|
|
28
35
|
|
|
29
36
|
<template>
|
|
30
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
class="chart-menu-trigger-area"
|
|
39
|
+
:class="{ 'chart-menu-trigger-area--expanded': isFullscreen }"
|
|
40
|
+
>
|
|
41
|
+
<!-- Expanded: a close button replaces the menu trigger -->
|
|
42
|
+
<button
|
|
43
|
+
v-if="isFullscreen"
|
|
44
|
+
class="chart-menu-button chart-close-button"
|
|
45
|
+
aria-label="Collapse"
|
|
46
|
+
@click="emit('close')"
|
|
47
|
+
>
|
|
48
|
+
<svg
|
|
49
|
+
width="16"
|
|
50
|
+
height="16"
|
|
51
|
+
viewBox="0 0 16 16"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
stroke-width="1.75"
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
31
61
|
<!-- Single item: plain button -->
|
|
32
62
|
<button
|
|
33
|
-
v-if="!useDropdown()"
|
|
63
|
+
v-else-if="!useDropdown()"
|
|
34
64
|
class="chart-menu-button chart-menu-single"
|
|
35
65
|
:aria-label="items[0].label"
|
|
36
66
|
@click="items[0].action"
|
|
@@ -88,11 +118,19 @@ const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
|
88
118
|
<style scoped>
|
|
89
119
|
.chart-menu-trigger-area {
|
|
90
120
|
position: absolute;
|
|
91
|
-
top: 0;
|
|
92
|
-
right: 0;
|
|
121
|
+
top: 0.5em;
|
|
122
|
+
right: 0.5em;
|
|
93
123
|
z-index: 1;
|
|
94
124
|
}
|
|
95
125
|
|
|
126
|
+
/* In fullscreen the button anchors to the viewport corner (the wrapper's
|
|
127
|
+
content padding doesn't move an absolutely-positioned child), so inset it
|
|
128
|
+
further for modal-style breathing room. */
|
|
129
|
+
.chart-menu-trigger-area--expanded {
|
|
130
|
+
top: 1.25em;
|
|
131
|
+
right: 1.25em;
|
|
132
|
+
}
|
|
133
|
+
|
|
96
134
|
.chart-menu-button {
|
|
97
135
|
display: flex;
|
|
98
136
|
align-items: center;
|
|
@@ -112,6 +150,11 @@ const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
|
112
150
|
opacity: 1;
|
|
113
151
|
}
|
|
114
152
|
|
|
153
|
+
/* The close button is always visible while the chart is expanded. */
|
|
154
|
+
.chart-close-button {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
115
158
|
.chart-menu-button:hover {
|
|
116
159
|
background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
|
|
117
160
|
color: var(--color-text);
|
|
@@ -119,6 +162,33 @@ const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
|
119
162
|
</style>
|
|
120
163
|
|
|
121
164
|
<style>
|
|
165
|
+
/* Reveal the menu button when the user hovers/focuses anywhere in the
|
|
166
|
+
chart. These reference the parent wrapper classes, so they can't be
|
|
167
|
+
scoped to ChartMenu. (In fullscreen the menu button is swapped for the
|
|
168
|
+
always-visible close button, so no rule is needed there.) */
|
|
169
|
+
.line-chart-wrapper:hover .chart-menu-button,
|
|
170
|
+
.bar-chart-wrapper:hover .chart-menu-button,
|
|
171
|
+
.choropleth-wrapper:hover .chart-menu-button,
|
|
172
|
+
.line-chart-wrapper:focus-within .chart-menu-button,
|
|
173
|
+
.bar-chart-wrapper:focus-within .chart-menu-button,
|
|
174
|
+
.choropleth-wrapper:focus-within .chart-menu-button {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Visually-hidden live region used by each chart wrapper to announce
|
|
179
|
+
expansion. Lives here so it ships once with the shared menu. */
|
|
180
|
+
.chart-sr-only {
|
|
181
|
+
position: absolute;
|
|
182
|
+
width: 1px;
|
|
183
|
+
height: 1px;
|
|
184
|
+
padding: 0;
|
|
185
|
+
margin: -1px;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
clip: rect(0, 0, 0, 0);
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
border: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
122
192
|
.chart-menu-content {
|
|
123
193
|
z-index: 100;
|
|
124
194
|
background: var(--color-bg-0);
|
|
@@ -365,6 +365,10 @@ the focused feature toggles back off. If a tooltip is configured, focusing
|
|
|
365
365
|
shows that feature's tooltip. Users can pan/zoom freely around the focused
|
|
366
366
|
area; the built-in **Reset** button clears focus and snaps back.
|
|
367
367
|
|
|
368
|
+
Selection works on touch too: a single-finger **tap** on a feature emits
|
|
369
|
+
`stateClick` and toggles focus, while drags (pan) and pinches (zoom) are left
|
|
370
|
+
to the map. Hover tooltips stay off on touch for performance.
|
|
371
|
+
|
|
368
372
|
Counties are tiny without a zoom — focus is a natural fit for drill-down.
|
|
369
373
|
|
|
370
374
|
<ComponentDemo>
|
|
@@ -714,6 +718,7 @@ set `tooltip-trigger`.
|
|
|
714
718
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
715
719
|
| `focus` | `FocusValue` | No | — |
|
|
716
720
|
| `focusZoomLevel` | `number` | No | `4` |
|
|
721
|
+
| `fullscreenTarget` | `string \| HTMLElement` | No | — |
|
|
717
722
|
|
|
718
723
|
|
|
719
724
|
### StateData
|
|
@@ -174,6 +174,15 @@ const props = withDefaults(
|
|
|
174
174
|
focus?: FocusValue;
|
|
175
175
|
/** Scale factor applied when `focus` is set. Default: 4 */
|
|
176
176
|
focusZoomLevel?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Where to teleport the map while expanded (the Expand menu item). A
|
|
179
|
+
* CSS selector or element; defaults to `body`. Moving it to the
|
|
180
|
+
* document root keeps `position: fixed` resolving against the viewport
|
|
181
|
+
* instead of being trapped by an ancestor's `transform`/`filter`/
|
|
182
|
+
* `contain`/`perspective` or stacking context. Set this when your app
|
|
183
|
+
* doesn't mount at the document root.
|
|
184
|
+
*/
|
|
185
|
+
fullscreenTarget?: string | HTMLElement;
|
|
177
186
|
}>(),
|
|
178
187
|
{
|
|
179
188
|
geoType: "states",
|
|
@@ -189,6 +198,10 @@ const props = withDefaults(
|
|
|
189
198
|
},
|
|
190
199
|
);
|
|
191
200
|
|
|
201
|
+
// The template root is a <Teleport>, so fallthrough attrs (class, style,
|
|
202
|
+
// data-*, id…) can't auto-inherit — forward them onto the wrapper manually.
|
|
203
|
+
defineOptions({ inheritAttrs: false });
|
|
204
|
+
|
|
192
205
|
const emit = defineEmits<{
|
|
193
206
|
(
|
|
194
207
|
e: "stateClick",
|
|
@@ -308,10 +321,33 @@ let pendingMoveX = 0;
|
|
|
308
321
|
let pendingMoveY = 0;
|
|
309
322
|
let pendingMoveFrame = 0;
|
|
310
323
|
|
|
324
|
+
// Touch tap-to-select. Touch devices get no synthesized-click guarantee
|
|
325
|
+
// (iOS treats the first tap on a hover target as a hover, and the d3-zoom
|
|
326
|
+
// touch listeners can swallow the synthetic click), so we resolve taps from
|
|
327
|
+
// the raw touch events ourselves. A gesture counts as a tap when a single
|
|
328
|
+
// finger lifts close to where it landed, soon after — anything longer or
|
|
329
|
+
// draggier is a pan/long-press and is left to d3-zoom.
|
|
330
|
+
const TAP_SLOP = 10; // px of movement allowed between touchstart and touchend
|
|
331
|
+
const TAP_MAX_MS = 600; // longer presses aren't taps
|
|
332
|
+
let tapStart: {
|
|
333
|
+
x: number;
|
|
334
|
+
y: number;
|
|
335
|
+
time: number;
|
|
336
|
+
featId: string | null;
|
|
337
|
+
} | null = null;
|
|
338
|
+
|
|
311
339
|
function setupInteraction() {
|
|
312
|
-
if (isTouchDevice) return;
|
|
313
340
|
const g = mapGroupRef.value;
|
|
314
341
|
if (!g) return;
|
|
342
|
+
// Tap-to-select is wired on every device. `touchend` is non-passive so a
|
|
343
|
+
// confirmed tap can preventDefault and suppress the compatibility
|
|
344
|
+
// click/hover the browser would otherwise synthesize (the double-fire and
|
|
345
|
+
// iOS first-tap-hover sources).
|
|
346
|
+
g.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
347
|
+
g.addEventListener("touchend", onTouchEnd);
|
|
348
|
+
g.addEventListener("touchcancel", onTouchCancel, { passive: true });
|
|
349
|
+
// Hover + tooltip stay off on touch (stroke-width churn degrades zoom/pan).
|
|
350
|
+
if (isTouchDevice) return;
|
|
315
351
|
g.addEventListener("click", onDelegatedEvent);
|
|
316
352
|
g.addEventListener("mouseover", onDelegatedEvent);
|
|
317
353
|
g.addEventListener("mousemove", onDelegatedMouseMove);
|
|
@@ -321,6 +357,9 @@ function setupInteraction() {
|
|
|
321
357
|
function teardownInteraction() {
|
|
322
358
|
const g = mapGroupRef.value;
|
|
323
359
|
if (!g) return;
|
|
360
|
+
g.removeEventListener("touchstart", onTouchStart);
|
|
361
|
+
g.removeEventListener("touchend", onTouchEnd);
|
|
362
|
+
g.removeEventListener("touchcancel", onTouchCancel);
|
|
324
363
|
g.removeEventListener("click", onDelegatedEvent);
|
|
325
364
|
g.removeEventListener("mouseover", onDelegatedEvent);
|
|
326
365
|
g.removeEventListener("mousemove", onDelegatedMouseMove);
|
|
@@ -1146,6 +1185,18 @@ function eventToFeatureId(target: EventTarget | null): string | null {
|
|
|
1146
1185
|
return el ? ((el as HTMLElement).dataset.featId ?? null) : null;
|
|
1147
1186
|
}
|
|
1148
1187
|
|
|
1188
|
+
// Emits stateClick plus the baked-in click-to-focus toggle so
|
|
1189
|
+
// `v-model:focus="ref"` Just Works: selecting the currently focused feature
|
|
1190
|
+
// clears focus (emits null); selecting any other feature emits its id. With
|
|
1191
|
+
// a `focus` array, any selection of a member clears everything — parents
|
|
1192
|
+
// wanting fine-grained multi-select handle merging via `@update:focus`.
|
|
1193
|
+
// Shared by the mouse-click and touch-tap paths.
|
|
1194
|
+
function emitSelection(data: TooltipPayload) {
|
|
1195
|
+
emit("stateClick", { id: data.id, name: data.name, value: data.value });
|
|
1196
|
+
const wasFocused = focusedBaseIds(normalizedFocus.value).has(data.id);
|
|
1197
|
+
emit("update:focus", wasFocused ? null : data.id);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1149
1200
|
function onDelegatedEvent(event: Event) {
|
|
1150
1201
|
if (isZooming) return;
|
|
1151
1202
|
const me = event as MouseEvent;
|
|
@@ -1155,14 +1206,7 @@ function onDelegatedEvent(event: Event) {
|
|
|
1155
1206
|
if (!data) return;
|
|
1156
1207
|
const payload = { id: data.id, name: data.name, value: data.value };
|
|
1157
1208
|
if (event.type === "click") {
|
|
1158
|
-
|
|
1159
|
-
// Click-to-focus toggle, baked in so `v-model:focus="ref"` Just Works:
|
|
1160
|
-
// clicking the currently focused feature clears focus (emits null);
|
|
1161
|
-
// clicking any other feature emits its id. With a `focus` array, any
|
|
1162
|
-
// click on a member clears everything — parents wanting fine-grained
|
|
1163
|
-
// multi-select handle merging themselves via `@update:focus`.
|
|
1164
|
-
const wasFocused = focusedBaseIds(normalizedFocus.value).has(data.id);
|
|
1165
|
-
emit("update:focus", wasFocused ? null : data.id);
|
|
1209
|
+
emitSelection(data);
|
|
1166
1210
|
} else if (event.type === "mouseover") {
|
|
1167
1211
|
setHover(pathsByFeatureId.get(featId)!);
|
|
1168
1212
|
if (hasInteractiveTooltip.value)
|
|
@@ -1182,6 +1226,44 @@ function onDelegatedMouseOut(event: MouseEvent) {
|
|
|
1182
1226
|
clearHover();
|
|
1183
1227
|
}
|
|
1184
1228
|
|
|
1229
|
+
function onTouchStart(event: TouchEvent) {
|
|
1230
|
+
// Single-finger only — a second touch is a pinch-zoom, not a selection.
|
|
1231
|
+
if (event.touches.length !== 1) {
|
|
1232
|
+
tapStart = null;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const t = event.touches[0]!;
|
|
1236
|
+
tapStart = {
|
|
1237
|
+
x: t.clientX,
|
|
1238
|
+
y: t.clientY,
|
|
1239
|
+
time: event.timeStamp,
|
|
1240
|
+
featId: eventToFeatureId(event.target),
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function onTouchEnd(event: TouchEvent) {
|
|
1245
|
+
const start = tapStart;
|
|
1246
|
+
tapStart = null;
|
|
1247
|
+
// No feature under the initial touch, or a second finger joined in.
|
|
1248
|
+
if (!start || !start.featId || event.touches.length > 0) return;
|
|
1249
|
+
const t = event.changedTouches[0];
|
|
1250
|
+
if (!t) return;
|
|
1251
|
+
// Moved too far (a pan) or held too long (a long-press) → not a tap.
|
|
1252
|
+
if (Math.abs(t.clientX - start.x) > TAP_SLOP) return;
|
|
1253
|
+
if (Math.abs(t.clientY - start.y) > TAP_SLOP) return;
|
|
1254
|
+
if (event.timeStamp - start.time > TAP_MAX_MS) return;
|
|
1255
|
+
const data = tooltipDataById.get(start.featId);
|
|
1256
|
+
if (!data) return;
|
|
1257
|
+
// Suppress the synthetic click/hover the browser would replay from this
|
|
1258
|
+
// tap, so selection fires exactly once and never via iOS's hover-first tap.
|
|
1259
|
+
event.preventDefault();
|
|
1260
|
+
emitSelection(data);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function onTouchCancel() {
|
|
1264
|
+
tapStart = null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1185
1267
|
// ─── Imperative SVG path management ──────────────────────────────────────
|
|
1186
1268
|
//
|
|
1187
1269
|
// 3,000+ counties are too many to round-trip through Vue's render scheduler
|
|
@@ -1372,7 +1454,9 @@ const gradientCss = computed(() => {
|
|
|
1372
1454
|
return `linear-gradient(to right, ${stops})`;
|
|
1373
1455
|
});
|
|
1374
1456
|
|
|
1375
|
-
const fullscreen = useChartFullscreen(
|
|
1457
|
+
const fullscreen = useChartFullscreen({
|
|
1458
|
+
target: () => props.fullscreenTarget,
|
|
1459
|
+
});
|
|
1376
1460
|
|
|
1377
1461
|
const menuItems = computed<ChartMenuItem[]>(() => {
|
|
1378
1462
|
const fname = menuFilename();
|
|
@@ -1439,70 +1523,81 @@ watch(
|
|
|
1439
1523
|
</script>
|
|
1440
1524
|
|
|
1441
1525
|
<template>
|
|
1442
|
-
<
|
|
1443
|
-
|
|
1444
|
-
:
|
|
1445
|
-
'choropleth-wrapper',
|
|
1446
|
-
{ pannable: pan, 'is-fullscreen': fullscreen.isFullscreen.value },
|
|
1447
|
-
]"
|
|
1526
|
+
<Teleport
|
|
1527
|
+
:to="fullscreen.teleportTarget.value"
|
|
1528
|
+
:disabled="!fullscreen.isFullscreen.value"
|
|
1448
1529
|
>
|
|
1449
|
-
<
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1530
|
+
<div
|
|
1531
|
+
ref="containerRef"
|
|
1532
|
+
v-bind="$attrs"
|
|
1533
|
+
:class="[
|
|
1534
|
+
'choropleth-wrapper',
|
|
1535
|
+
{ pannable: pan, 'is-fullscreen': fullscreen.isFullscreen.value },
|
|
1536
|
+
]"
|
|
1537
|
+
:style="fullscreen.fullscreenStyle.value"
|
|
1538
|
+
>
|
|
1539
|
+
<ChartMenu
|
|
1540
|
+
v-if="menu"
|
|
1541
|
+
:items="menuItems"
|
|
1542
|
+
:is-fullscreen="fullscreen.isFullscreen.value"
|
|
1543
|
+
@close="fullscreen.exit"
|
|
1544
|
+
/>
|
|
1545
|
+
<div class="chart-sr-only" aria-live="polite">
|
|
1546
|
+
{{ fullscreen.isFullscreen.value ? "Map expanded to fill window" : "" }}
|
|
1547
|
+
</div>
|
|
1548
|
+
<!--
|
|
1454
1549
|
Title + legend live as an HTML overlay on top of the SVG so they keep
|
|
1455
1550
|
their intrinsic px sizes regardless of how the browser scales the
|
|
1456
1551
|
viewBox to fit the container.
|
|
1457
1552
|
-->
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
</span>
|
|
1470
|
-
<template v-if="isCategorical || isThreshold">
|
|
1471
|
-
<span
|
|
1472
|
-
v-for="item in discreteLegendItems"
|
|
1473
|
-
:key="item.key"
|
|
1474
|
-
class="choropleth-legend-item"
|
|
1475
|
-
>
|
|
1476
|
-
<span
|
|
1477
|
-
class="choropleth-legend-swatch"
|
|
1478
|
-
:style="{ background: item.color }"
|
|
1479
|
-
/>
|
|
1480
|
-
{{ item.label }}
|
|
1553
|
+
<div v-if="title || showLegend" class="choropleth-header">
|
|
1554
|
+
<div v-if="title" class="choropleth-title" :style="titleInlineStyle">
|
|
1555
|
+
{{ title }}
|
|
1556
|
+
</div>
|
|
1557
|
+
<div
|
|
1558
|
+
v-if="showLegend"
|
|
1559
|
+
class="choropleth-legend"
|
|
1560
|
+
:style="legendInlineStyle"
|
|
1561
|
+
>
|
|
1562
|
+
<span v-if="legendTitle" class="choropleth-legend-title">
|
|
1563
|
+
{{ legendTitle }}
|
|
1481
1564
|
</span>
|
|
1482
|
-
|
|
1483
|
-
<div v-else class="choropleth-legend-continuous">
|
|
1484
|
-
<div
|
|
1485
|
-
class="choropleth-legend-gradient"
|
|
1486
|
-
:style="{ background: gradientCss }"
|
|
1487
|
-
/>
|
|
1488
|
-
<div class="choropleth-legend-ticks">
|
|
1565
|
+
<template v-if="isCategorical || isThreshold">
|
|
1489
1566
|
<span
|
|
1490
|
-
v-for="
|
|
1491
|
-
:key="
|
|
1492
|
-
|
|
1567
|
+
v-for="item in discreteLegendItems"
|
|
1568
|
+
:key="item.key"
|
|
1569
|
+
class="choropleth-legend-item"
|
|
1493
1570
|
>
|
|
1494
|
-
|
|
1571
|
+
<span
|
|
1572
|
+
class="choropleth-legend-swatch"
|
|
1573
|
+
:style="{ background: item.color }"
|
|
1574
|
+
/>
|
|
1575
|
+
{{ item.label }}
|
|
1495
1576
|
</span>
|
|
1577
|
+
</template>
|
|
1578
|
+
<div v-else class="choropleth-legend-continuous">
|
|
1579
|
+
<div
|
|
1580
|
+
class="choropleth-legend-gradient"
|
|
1581
|
+
:style="{ background: gradientCss }"
|
|
1582
|
+
/>
|
|
1583
|
+
<div class="choropleth-legend-ticks">
|
|
1584
|
+
<span
|
|
1585
|
+
v-for="tick in continuousTicks"
|
|
1586
|
+
:key="tick.value"
|
|
1587
|
+
:style="{ left: tick.pct + '%' }"
|
|
1588
|
+
>
|
|
1589
|
+
{{ tick.value }}
|
|
1590
|
+
</span>
|
|
1591
|
+
</div>
|
|
1496
1592
|
</div>
|
|
1497
1593
|
</div>
|
|
1498
1594
|
</div>
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
<!--
|
|
1595
|
+
<svg
|
|
1596
|
+
ref="svgRef"
|
|
1597
|
+
:viewBox="`0 0 ${width} ${height}`"
|
|
1598
|
+
preserveAspectRatio="xMidYMid meet"
|
|
1599
|
+
>
|
|
1600
|
+
<!--
|
|
1506
1601
|
Path elements are created imperatively in `rebuildPaths()`; Vue never
|
|
1507
1602
|
diffs the per-feature subtree so reactive state changes don't walk
|
|
1508
1603
|
thousands of vnodes. `mapGroupRef` carries the zoom transform;
|
|
@@ -1511,32 +1606,33 @@ watch(
|
|
|
1511
1606
|
focus-overlay layer (separate group so hover-raised base paths
|
|
1512
1607
|
can't cover an overlay).
|
|
1513
1608
|
-->
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1609
|
+
<g ref="mapGroupRef">
|
|
1610
|
+
<g ref="baseGroupRef" />
|
|
1611
|
+
<g ref="overlayGroupRef" />
|
|
1612
|
+
</g>
|
|
1613
|
+
</svg>
|
|
1614
|
+
<button
|
|
1615
|
+
v-if="isZoomed"
|
|
1616
|
+
type="button"
|
|
1617
|
+
class="choropleth-reset"
|
|
1618
|
+
aria-label="Reset zoom"
|
|
1619
|
+
@click="resetZoom"
|
|
1620
|
+
>
|
|
1621
|
+
Reset
|
|
1622
|
+
</button>
|
|
1623
|
+
<ChoroplethTooltip v-if="hasInteractiveTooltip" ref="tooltipChildRef">
|
|
1624
|
+
<template #default="raw">
|
|
1625
|
+
<slot name="tooltip" v-bind="narrowSlotProps(raw)">
|
|
1626
|
+
<span v-if="tooltipFormat" v-html="tooltipFormat(raw)" />
|
|
1627
|
+
<template v-else-if="raw.value == null">{{ raw.name }}</template>
|
|
1628
|
+
<template v-else>
|
|
1629
|
+
{{ raw.name }}: {{ formatTooltipValue(raw.value) }}
|
|
1630
|
+
</template>
|
|
1631
|
+
</slot>
|
|
1632
|
+
</template>
|
|
1633
|
+
</ChoroplethTooltip>
|
|
1634
|
+
</div>
|
|
1635
|
+
</Teleport>
|
|
1540
1636
|
</template>
|
|
1541
1637
|
|
|
1542
1638
|
<style scoped>
|
|
@@ -1566,6 +1662,15 @@ watch(
|
|
|
1566
1662
|
cursor: grab;
|
|
1567
1663
|
}
|
|
1568
1664
|
|
|
1665
|
+
/* While expanded the wrapper is a flex column (inline style); the SVG keeps
|
|
1666
|
+
its prop-driven size otherwise, so stretch it to fill the expanded box. */
|
|
1667
|
+
.choropleth-wrapper.is-fullscreen svg {
|
|
1668
|
+
flex: 1 1 auto;
|
|
1669
|
+
min-height: 0;
|
|
1670
|
+
height: 100%;
|
|
1671
|
+
width: 100%;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1569
1674
|
.choropleth-wrapper.pannable svg:active {
|
|
1570
1675
|
cursor: grabbing;
|
|
1571
1676
|
}
|
|
@@ -263,6 +263,8 @@ Set `legend` on a series to add an entry to the inline legend strip above the pl
|
|
|
263
263
|
|
|
264
264
|
Hover over the chart to see a tooltip with values at each data point. Set `tooltip-trigger="hover"` to enable the built-in tooltip with crosshair and highlight dots. Use the `#tooltip` slot for custom content. Pass `tooltip-value-format` to control how numeric values render (e.g. percentages, currency); it falls back to `y-tick-format` when omitted.
|
|
265
265
|
|
|
266
|
+
On touch devices, drag **horizontally** across the chart to scrub the tooltip between points; a **vertical** swipe scrolls the page instead of getting captured by the chart.
|
|
267
|
+
|
|
266
268
|
<ComponentDemo>
|
|
267
269
|
<LineChart
|
|
268
270
|
:series="[
|
|
@@ -415,6 +417,11 @@ Set `outline: true` on a series to draw a page-colored stroke behind the
|
|
|
415
417
|
line. This is useful when lines overlap, cross dense areas, or sit on a
|
|
416
418
|
busy background — the outline keeps each series visually distinct.
|
|
417
419
|
|
|
420
|
+
Override the outline's color with `outlineColor` (any CSS color; defaults
|
|
421
|
+
to the page background `var(--color-bg-0, #fff)`) and its thickness with
|
|
422
|
+
`outlineWidth` (extra width added to the line's `strokeWidth`, split evenly
|
|
423
|
+
per side; defaults to `4`).
|
|
424
|
+
|
|
418
425
|
<ComponentDemo>
|
|
419
426
|
<LineChart
|
|
420
427
|
:series="[
|
|
@@ -928,10 +935,17 @@ positioned from the anchor as usual via `offset`.
|
|
|
928
935
|
|
|
929
936
|
### Chart menu
|
|
930
937
|
|
|
931
|
-
The chart menu (top-right) leads with
|
|
932
|
-
Save as PNG, and Download CSV.
|
|
933
|
-
window so the plot has more room;
|
|
934
|
-
|
|
938
|
+
The chart menu (top-right) leads with Fullscreen and then Save as SVG,
|
|
939
|
+
Save as PNG, and Download CSV. Fullscreen grows the chart to fill the browser
|
|
940
|
+
window so the plot has more room; while expanded the menu trigger is replaced
|
|
941
|
+
by a close (✕) button — click it or press <kbd>Esc</kbd> to return to the
|
|
942
|
+
inline size. Set `menu="false"` to hide the trigger entirely.
|
|
943
|
+
|
|
944
|
+
While expanded the chart is teleported to `<body>` so it reliably covers the
|
|
945
|
+
viewport even when an ancestor uses `transform`, `filter`, `contain`, or
|
|
946
|
+
establishes a stacking context. If your app doesn't mount at the document root
|
|
947
|
+
(e.g. inside a shadow root or a dedicated overlay container), point
|
|
948
|
+
`fullscreenTarget` at a CSS selector or element to teleport there instead.
|
|
935
949
|
|
|
936
950
|
### Custom CSV download
|
|
937
951
|
|
|
@@ -1008,6 +1022,7 @@ until the user clicks Download:
|
|
|
1008
1022
|
| `filename` | `string` | No | — |
|
|
1009
1023
|
| `downloadLink` | `boolean \| string` | No | — |
|
|
1010
1024
|
| `downloadButton` | `boolean \| string` | No | — |
|
|
1025
|
+
| `fullscreenTarget` | `string \| HTMLElement` | No | — |
|
|
1011
1026
|
| `annotations` | `readonly ChartAnnotation[]` | No | — |
|
|
1012
1027
|
| `chartPadding` | `ChartPadding` | No | — |
|
|
1013
1028
|
| `y` | `LineChartData` | No | — |
|
|
@@ -1070,6 +1085,8 @@ interface Series {
|
|
|
1070
1085
|
opacity?: number;
|
|
1071
1086
|
line?: boolean;
|
|
1072
1087
|
outline?: boolean; // page-colored stroke drawn behind the line
|
|
1088
|
+
outlineColor?: string; // outline stroke color (default: var(--color-bg-0, #fff))
|
|
1089
|
+
outlineWidth?: number; // extra width added to strokeWidth (default: 4)
|
|
1073
1090
|
dots?: boolean;
|
|
1074
1091
|
dotRadius?: number;
|
|
1075
1092
|
dotFill?: string;
|