@cfasim-ui/docs 0.6.1 → 0.6.3
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 +26 -0
- package/charts/BarChart/BarChart.vue +10 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +87 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +196 -11
- package/charts/LineChart/LineChart.md +75 -0
- package/charts/LineChart/LineChart.vue +10 -0
- package/charts/_shared/ChartAnnotations.vue +32 -17
- package/charts/_shared/chartProps.ts +15 -0
- package/components/SidebarLayout/SidebarLayout.md +2 -0
- package/components/SidebarLayout/SidebarLayout.vue +2 -1
- package/index.json +1 -1
- package/package.json +1 -1
|
@@ -699,6 +699,30 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
699
699
|
</template>
|
|
700
700
|
</ComponentDemo>
|
|
701
701
|
|
|
702
|
+
## Accessibility
|
|
703
|
+
|
|
704
|
+
The chart's individual bars aren't exposed to assistive tech, so the chart
|
|
705
|
+
announces itself with a single accessible name. When the chart has a `title`,
|
|
706
|
+
its root element gets `role="figure"` and an `aria-label` set to the title, so
|
|
707
|
+
screen readers announce it as a labeled figure while the menu and download
|
|
708
|
+
controls stay reachable.
|
|
709
|
+
|
|
710
|
+
Set `ariaLabel` to give screen readers a fuller summary than the visible title
|
|
711
|
+
(it overrides `title` for the accessible name only):
|
|
712
|
+
|
|
713
|
+
```vue
|
|
714
|
+
<BarChart
|
|
715
|
+
:data="cases"
|
|
716
|
+
:categories="regions"
|
|
717
|
+
title="Cases by region"
|
|
718
|
+
aria-label="Cases by region: North 120, South 80, West 45"
|
|
719
|
+
/>
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Pass `role` to override the default, e.g. `role="img"` to expose the chart as a
|
|
723
|
+
single image (note this hides the inner menu/download controls from assistive
|
|
724
|
+
tech).
|
|
725
|
+
|
|
702
726
|
## API
|
|
703
727
|
|
|
704
728
|
## Props
|
|
@@ -709,6 +733,8 @@ anchor. `lineColor`, `lineWidth`, and `lineDash` style the line.
|
|
|
709
733
|
| `height` | `number` | No | — |
|
|
710
734
|
| `title` | `string` | No | — |
|
|
711
735
|
| `titleStyle` | `TitleStyle` | No | — |
|
|
736
|
+
| `role` | `string` | No | — |
|
|
737
|
+
| `ariaLabel` | `string` | No | — |
|
|
712
738
|
| `axisLabelStyle` | `LabelStyle` | No | — |
|
|
713
739
|
| `tickLabelStyle` | `LabelStyle` | No | — |
|
|
714
740
|
| `legendStyle` | `LabelStyle` | No | — |
|
|
@@ -286,6 +286,14 @@ const props = withDefaults(defineProps<BarChartProps>(), {
|
|
|
286
286
|
valueAxis: true,
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
+
// Accessible name for the whole chart; falls back to the visible title.
|
|
290
|
+
const chartAriaLabel = computed(() => props.ariaLabel ?? props.title);
|
|
291
|
+
// Label the chart as a figure when it has a name (keeps inner controls
|
|
292
|
+
// reachable, unlike role="img"). An explicit `role` prop always wins.
|
|
293
|
+
const chartRole = computed(
|
|
294
|
+
() => props.role ?? (chartAriaLabel.value ? "figure" : undefined),
|
|
295
|
+
);
|
|
296
|
+
|
|
289
297
|
// The template root is a <Teleport>, so fallthrough attrs (class, style,
|
|
290
298
|
// data-*, id…) can't auto-inherit — forward them onto the wrapper manually.
|
|
291
299
|
defineOptions({ inheritAttrs: false });
|
|
@@ -1475,6 +1483,8 @@ const columnHeaders = computed<ColumnHeader[]>(() => {
|
|
|
1475
1483
|
class="bar-chart-wrapper"
|
|
1476
1484
|
:class="{ 'is-fullscreen': isFullscreen }"
|
|
1477
1485
|
:style="fullscreenStyle"
|
|
1486
|
+
:role="chartRole || undefined"
|
|
1487
|
+
:aria-label="chartAriaLabel || undefined"
|
|
1478
1488
|
>
|
|
1479
1489
|
<ChartMenu
|
|
1480
1490
|
v-if="menu"
|
|
@@ -258,6 +258,8 @@ Use an array of `CategoricalStop` objects to map string values to colors. Each s
|
|
|
258
258
|
|
|
259
259
|
Set `geoType="counties"` to render county-level data using 5-digit FIPS codes. State borders are drawn on top for context. Use `pan` and `zoom` props to enable interactive navigation — useful for dense county data.
|
|
260
260
|
|
|
261
|
+
On touch devices, add `two-finger-pan` so a single finger scrolls the page (the map sets `touch-action: pan-y`) and two fingers pan/pinch-zoom the map — tap-to-select still works with one finger.
|
|
262
|
+
|
|
261
263
|
<ComponentDemo>
|
|
262
264
|
<ChoroplethMap
|
|
263
265
|
:topology="countiesTopo"
|
|
@@ -356,6 +358,58 @@ Set `geoType="hsas"` to render Health Service Area boundaries. HSAs are dissolve
|
|
|
356
358
|
</template>
|
|
357
359
|
</ComponentDemo>
|
|
358
360
|
|
|
361
|
+
### Single-state map (`state`)
|
|
362
|
+
|
|
363
|
+
Set the `state` prop to render just one state's outline with its `counties` or
|
|
364
|
+
`hsas` inside it — no surrounding states — and the projection zooms to fit it.
|
|
365
|
+
Accepts a state **name** (`"California"`) or a 2-digit **FIPS code** (`"06"`).
|
|
366
|
+
This needs a counties topology (the same `us-atlas/counties-10m.json`). `data`
|
|
367
|
+
can stay national; only features inside the selected state are drawn and
|
|
368
|
+
colored.
|
|
369
|
+
|
|
370
|
+
<ComponentDemo>
|
|
371
|
+
<ChoroplethMap
|
|
372
|
+
:topology="countiesTopo"
|
|
373
|
+
geo-type="counties"
|
|
374
|
+
state="California"
|
|
375
|
+
:data="[
|
|
376
|
+
{ id: '06037', value: 100 },
|
|
377
|
+
{ id: '06073', value: 80 },
|
|
378
|
+
{ id: '06059', value: 65 },
|
|
379
|
+
{ id: '06065', value: 55 },
|
|
380
|
+
{ id: '06001', value: 45 },
|
|
381
|
+
{ id: '06085', value: 40 },
|
|
382
|
+
]"
|
|
383
|
+
title="California cases by county"
|
|
384
|
+
:legend-title="'Cases'"
|
|
385
|
+
:height="400"
|
|
386
|
+
/>
|
|
387
|
+
|
|
388
|
+
<template #code>
|
|
389
|
+
|
|
390
|
+
```vue
|
|
391
|
+
<ChoroplethMap
|
|
392
|
+
:topology="countiesTopo"
|
|
393
|
+
geo-type="counties"
|
|
394
|
+
state="California"
|
|
395
|
+
:data="[
|
|
396
|
+
{ id: '06037', value: 100 }, // Los Angeles
|
|
397
|
+
{ id: '06073', value: 80 }, // San Diego
|
|
398
|
+
{ id: '06059', value: 65 }, // Orange
|
|
399
|
+
]"
|
|
400
|
+
title="California cases by county"
|
|
401
|
+
:legend-title="'Cases'"
|
|
402
|
+
:height="400"
|
|
403
|
+
/>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
</template>
|
|
407
|
+
</ComponentDemo>
|
|
408
|
+
|
|
409
|
+
Switch `geo-type="hsas"` to fill the same state with Health Service Areas
|
|
410
|
+
instead. An unrecognized `state` value logs a warning and falls back to the
|
|
411
|
+
full national map.
|
|
412
|
+
|
|
359
413
|
### Click to focus (`v-model:focus`)
|
|
360
414
|
|
|
361
415
|
Bind the `focus` prop to pan and zoom to a specific feature. Pass a feature
|
|
@@ -365,6 +419,10 @@ the focused feature toggles back off. If a tooltip is configured, focusing
|
|
|
365
419
|
shows that feature's tooltip. Users can pan/zoom freely around the focused
|
|
366
420
|
area; the built-in **Reset** button clears focus and snaps back.
|
|
367
421
|
|
|
422
|
+
Set `:focus-zoom="false"` to highlight (and draw cross-geoType overlays)
|
|
423
|
+
**without** panning or zooming — useful for a click-to-select interaction
|
|
424
|
+
where the map should stay put while a side panel shows the details.
|
|
425
|
+
|
|
368
426
|
Selection works on touch too: a single-finger **tap** on a feature emits
|
|
369
427
|
`stateClick` and toggles focus, while drags (pan) and pinches (zoom) are left
|
|
370
428
|
to the map. Hover tooltips stay off on touch for performance.
|
|
@@ -687,6 +745,30 @@ set `tooltip-trigger`.
|
|
|
687
745
|
</template>
|
|
688
746
|
</ComponentDemo>
|
|
689
747
|
|
|
748
|
+
## Accessibility
|
|
749
|
+
|
|
750
|
+
The map's individual regions aren't exposed to assistive tech, so the map
|
|
751
|
+
announces itself with a single accessible name. When it has a `title`, the root
|
|
752
|
+
element gets `role="figure"` and an `aria-label` set to the title, so screen
|
|
753
|
+
readers announce it as a labeled figure while the menu and reset controls stay
|
|
754
|
+
reachable.
|
|
755
|
+
|
|
756
|
+
Set `ariaLabel` to give screen readers a fuller summary than the visible title
|
|
757
|
+
(it overrides `title` for the accessible name only):
|
|
758
|
+
|
|
759
|
+
```vue
|
|
760
|
+
<ChoroplethMap
|
|
761
|
+
:topology="usStates"
|
|
762
|
+
:data="cases"
|
|
763
|
+
title="Cases by state"
|
|
764
|
+
aria-label="US map shaded by case count per state, highest in the Southeast"
|
|
765
|
+
/>
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Pass `role` to override the default, e.g. `role="img"` to expose the map as a
|
|
769
|
+
single image (note this hides the inner menu/reset controls from assistive
|
|
770
|
+
tech).
|
|
771
|
+
|
|
690
772
|
## Props
|
|
691
773
|
|
|
692
774
|
| Prop | Type | Required | Default |
|
|
@@ -695,11 +777,14 @@ set `tooltip-trigger`.
|
|
|
695
777
|
| `data` | `StateData[]` | No | — |
|
|
696
778
|
| `geoType` | `GeoType` | No | `"states"` |
|
|
697
779
|
| `dataGeoType` | `GeoType` | No | — |
|
|
780
|
+
| `state` | `string` | No | — |
|
|
698
781
|
| `width` | `number` | No | — |
|
|
699
782
|
| `height` | `number` | No | — |
|
|
700
783
|
| `colorScale` | `ChoroplethColorScale \| ThresholdStop[] \| CategoricalStop[]` | No | — |
|
|
701
784
|
| `title` | `string` | No | — |
|
|
702
785
|
| `titleStyle` | `TitleStyle` | No | — |
|
|
786
|
+
| `role` | `string` | No | — |
|
|
787
|
+
| `ariaLabel` | `string` | No | — |
|
|
703
788
|
| `legendStyle` | `LabelStyle` | No | — |
|
|
704
789
|
| `noDataColor` | `string` | No | `"#ddd"` |
|
|
705
790
|
| `strokeColor` | `string` | No | `"#fff"` |
|
|
@@ -709,6 +794,7 @@ set `tooltip-trigger`.
|
|
|
709
794
|
| `legendTitle` | `string` | No | — |
|
|
710
795
|
| `zoom` | `boolean` | No | `false` |
|
|
711
796
|
| `pan` | `boolean` | No | `false` |
|
|
797
|
+
| `twoFingerPan` | `boolean` | No | `false` |
|
|
712
798
|
| `tooltipTrigger` | `"hover" \| "click"` | No | — |
|
|
713
799
|
| `tooltipFormat` | `(data: {
|
|
714
800
|
id: string` | No | — |
|
|
@@ -718,6 +804,7 @@ set `tooltip-trigger`.
|
|
|
718
804
|
| `tooltipClamp` | `"none" \| "chart" \| "window"` | No | `"chart"` |
|
|
719
805
|
| `focus` | `FocusValue` | No | — |
|
|
720
806
|
| `focusZoomLevel` | `number` | No | `4` |
|
|
807
|
+
| `focusZoom` | `boolean` | No | `true` |
|
|
721
808
|
| `fullscreenTarget` | `string \| HTMLElement` | No | — |
|
|
722
809
|
|
|
723
810
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
toRaw,
|
|
9
9
|
useSlots,
|
|
10
10
|
} from "vue";
|
|
11
|
-
import { geoPath, geoAlbersUsa } from "d3-geo";
|
|
11
|
+
import { geoPath, geoAlbersUsa, geoMercator } from "d3-geo";
|
|
12
12
|
import { zoom as d3Zoom, zoomIdentity } from "d3-zoom";
|
|
13
13
|
import { select } from "d3-selection";
|
|
14
14
|
// Side-effect import: enables `selection.transition()` on d3 selections so
|
|
@@ -107,6 +107,15 @@ const props = withDefaults(
|
|
|
107
107
|
* When unset, data ids must match the base `geoType`.
|
|
108
108
|
*/
|
|
109
109
|
dataGeoType?: GeoType;
|
|
110
|
+
/**
|
|
111
|
+
* Scope the map to a single state: render only that state's outline with
|
|
112
|
+
* its `counties` or `hsas` inside it (no surrounding states), and refit
|
|
113
|
+
* the projection to zoom to it. Accepts a state name ("California") or a
|
|
114
|
+
* 2-digit FIPS code ("06"). Requires a counties topology when `geoType`
|
|
115
|
+
* is `"counties"` or `"hsas"`. If the value matches no state, the full
|
|
116
|
+
* national map is rendered and a warning is logged.
|
|
117
|
+
*/
|
|
118
|
+
state?: string;
|
|
110
119
|
width?: number;
|
|
111
120
|
height?: number;
|
|
112
121
|
colorScale?: ChoroplethColorScale | ThresholdStop[] | CategoricalStop[];
|
|
@@ -117,6 +126,21 @@ const props = withDefaults(
|
|
|
117
126
|
title?: string;
|
|
118
127
|
/** Styling for the map title. See `TitleStyle`. */
|
|
119
128
|
titleStyle?: TitleStyle;
|
|
129
|
+
/**
|
|
130
|
+
* ARIA role for the map's root element. Defaults to `"figure"` when an
|
|
131
|
+
* accessible name is present (from `ariaLabel` or `title`), so screen
|
|
132
|
+
* readers announce the map as a labeled figure while its controls (menu,
|
|
133
|
+
* reset) stay reachable. Pass `"img"` to expose it as a single image
|
|
134
|
+
* (which hides the inner controls from assistive tech), or your own role.
|
|
135
|
+
*/
|
|
136
|
+
role?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Accessible name for the map, announced by screen readers via the root
|
|
139
|
+
* element's `aria-label`. Defaults to the `title` prop. The individual
|
|
140
|
+
* regions aren't exposed to assistive tech, so set this to a short summary
|
|
141
|
+
* of what the map shows.
|
|
142
|
+
*/
|
|
143
|
+
ariaLabel?: string;
|
|
120
144
|
/** Styling for the legend (title, swatch labels, and continuous-scale ticks). */
|
|
121
145
|
legendStyle?: LabelStyle;
|
|
122
146
|
noDataColor?: string;
|
|
@@ -131,6 +155,14 @@ const props = withDefaults(
|
|
|
131
155
|
zoom?: boolean;
|
|
132
156
|
/** Enable click-and-drag panning. Default: false */
|
|
133
157
|
pan?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Require two fingers to pan/zoom on touch devices. Default `false`.
|
|
160
|
+
* When `true`, a single finger scrolls the page (the map sets
|
|
161
|
+
* `touch-action: pan-y` and ignores one-finger drags) while two fingers
|
|
162
|
+
* pan and pinch-zoom the map. Single-finger tap-to-select still works.
|
|
163
|
+
* Has no effect with a mouse.
|
|
164
|
+
*/
|
|
165
|
+
twoFingerPan?: boolean;
|
|
134
166
|
/** Tooltip activation mode */
|
|
135
167
|
tooltipTrigger?: "hover" | "click";
|
|
136
168
|
/**
|
|
@@ -174,6 +206,14 @@ const props = withDefaults(
|
|
|
174
206
|
focus?: FocusValue;
|
|
175
207
|
/** Scale factor applied when `focus` is set. Default: 4 */
|
|
176
208
|
focusZoomLevel?: number;
|
|
209
|
+
/**
|
|
210
|
+
* Whether setting `focus` pans/zooms to fit the focused feature.
|
|
211
|
+
* Default `true`. Set `false` to highlight (and draw cross-geoType
|
|
212
|
+
* overlays) without changing the current pan/zoom — useful for a
|
|
213
|
+
* click-to-select interaction where the map should stay put. The
|
|
214
|
+
* built-in Reset button is unaffected.
|
|
215
|
+
*/
|
|
216
|
+
focusZoom?: boolean;
|
|
177
217
|
/**
|
|
178
218
|
* Where to teleport the map while expanded (the Expand menu item). A
|
|
179
219
|
* CSS selector or element; defaults to `body`. Moving it to the
|
|
@@ -193,11 +233,21 @@ const props = withDefaults(
|
|
|
193
233
|
legend: true,
|
|
194
234
|
zoom: false,
|
|
195
235
|
pan: false,
|
|
236
|
+
twoFingerPan: false,
|
|
196
237
|
tooltipClamp: "chart",
|
|
197
238
|
focusZoomLevel: 4,
|
|
239
|
+
focusZoom: true,
|
|
198
240
|
},
|
|
199
241
|
);
|
|
200
242
|
|
|
243
|
+
// Accessible name for the whole map; falls back to the visible title.
|
|
244
|
+
const chartAriaLabel = computed(() => props.ariaLabel ?? props.title);
|
|
245
|
+
// Label the map as a figure when it has a name (keeps inner controls
|
|
246
|
+
// reachable, unlike role="img"). An explicit `role` prop always wins.
|
|
247
|
+
const chartRole = computed(
|
|
248
|
+
() => props.role ?? (chartAriaLabel.value ? "figure" : undefined),
|
|
249
|
+
);
|
|
250
|
+
|
|
201
251
|
// The template root is a <Teleport>, so fallthrough attrs (class, style,
|
|
202
252
|
// data-*, id…) can't auto-inherit — forward them onto the wrapper manually.
|
|
203
253
|
defineOptions({ inheritAttrs: false });
|
|
@@ -425,16 +475,28 @@ function setupZoom() {
|
|
|
425
475
|
// Dynamic filter: re-evaluated per event, so toggling `focus`,
|
|
426
476
|
// `zoom`, or `pan` doesn't require tearing down the zoom behavior.
|
|
427
477
|
// When focus is active we always allow drag + wheel so users can
|
|
428
|
-
// explore away from the focused area regardless of `zoom`/`pan
|
|
478
|
+
// explore away from the focused area regardless of `zoom`/`pan` — but
|
|
479
|
+
// not in highlight-only mode (`focusZoom=false`), where focus is purely
|
|
480
|
+
// visual and shouldn't grant interaction the props didn't ask for.
|
|
429
481
|
// Programmatic `.transform()` calls bypass this filter entirely.
|
|
430
482
|
zoomBehavior.filter((event) => {
|
|
431
|
-
const focused =
|
|
483
|
+
const focused =
|
|
484
|
+
normalizedFocus.value.length > 0 && props.focusZoom !== false;
|
|
432
485
|
const allowZoom = !!props.zoom || focused;
|
|
433
486
|
const allowPan = !!props.pan || focused;
|
|
434
487
|
if (event.type === "wheel" || event.type === "dblclick") {
|
|
435
488
|
if (!allowZoom) return false;
|
|
436
489
|
} else if (event.type === "mousedown" || event.type === "touchstart") {
|
|
437
490
|
if (!allowPan) return false;
|
|
491
|
+
// Two-finger-only mode: ignore single-finger touches so the page can
|
|
492
|
+
// scroll; a second finger drives the map (pan + pinch-zoom).
|
|
493
|
+
if (
|
|
494
|
+
props.twoFingerPan &&
|
|
495
|
+
event.type === "touchstart" &&
|
|
496
|
+
(event as TouchEvent).touches.length < 2
|
|
497
|
+
) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
438
500
|
} else if (!allowZoom && !allowPan) {
|
|
439
501
|
return false;
|
|
440
502
|
}
|
|
@@ -541,6 +603,13 @@ function applyFocus() {
|
|
|
541
603
|
return;
|
|
542
604
|
}
|
|
543
605
|
|
|
606
|
+
// Highlight-only mode: the strokes + overlays above are applied; skip the
|
|
607
|
+
// pan/zoom transform so the current view stays put (click-to-select).
|
|
608
|
+
if (props.focusZoom === false) {
|
|
609
|
+
focusApplied = true;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
544
613
|
const svg = select(svgRef.value);
|
|
545
614
|
// Always cancel any in-flight transition first — d3-transition queues
|
|
546
615
|
// same-named transitions rather than replacing them, so rapid focus
|
|
@@ -667,6 +736,19 @@ watch(
|
|
|
667
736
|
},
|
|
668
737
|
);
|
|
669
738
|
|
|
739
|
+
// Switching the scoped `state` refits the projection to a different region,
|
|
740
|
+
// so any leftover pan/zoom transform would leave the new map off-center.
|
|
741
|
+
// Reset it instantly (no animation) when the region changes.
|
|
742
|
+
watch(
|
|
743
|
+
() => props.state,
|
|
744
|
+
() => {
|
|
745
|
+
if (!svgRef.value || !zoomBehavior) return;
|
|
746
|
+
const svg = select(svgRef.value);
|
|
747
|
+
svg.interrupt();
|
|
748
|
+
zoomBehavior.transform(svg, zoomIdentity);
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
|
|
670
752
|
// Canonical internal coordinate system. All layout (projection, legend,
|
|
671
753
|
// title) is computed at this size; the SVG's viewBox makes the browser
|
|
672
754
|
// scale the entire canvas to whatever the container provides, so there's no
|
|
@@ -692,6 +774,58 @@ type CountiesTopo = Topology<{
|
|
|
692
774
|
states: NamedGeometry;
|
|
693
775
|
}>;
|
|
694
776
|
|
|
777
|
+
type StateFeature = GeoJSON.Feature<GeoJSON.Geometry | null, { name?: string }>;
|
|
778
|
+
|
|
779
|
+
// ─── Single-state scoping (`state` prop) ─────────────────────────────────
|
|
780
|
+
//
|
|
781
|
+
// Resolved directly from the topology's `states` object (not from
|
|
782
|
+
// featuresByGeoType) so it stays free of the featuresGeo → featuresById →
|
|
783
|
+
// featuresByGeoType chain — that chain reads `stateFips`, so depending on it
|
|
784
|
+
// here would form a reactive cycle.
|
|
785
|
+
const statesFeatures = computed<StateFeature[]>(() => {
|
|
786
|
+
const topo = toRaw(props.topology) as unknown as {
|
|
787
|
+
objects?: { states?: NamedGeometry };
|
|
788
|
+
};
|
|
789
|
+
const statesObj = topo?.objects?.states;
|
|
790
|
+
if (!statesObj) return [];
|
|
791
|
+
const fc = feature(topo as unknown as Topology, statesObj) as
|
|
792
|
+
| GeoJSON.FeatureCollection<GeoJSON.Geometry | null, { name?: string }>
|
|
793
|
+
| StateFeature;
|
|
794
|
+
return fc.type === "FeatureCollection" ? fc.features : [fc];
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// 2-digit FIPS for the active `state` prop, or null when unset/unresolved.
|
|
798
|
+
const stateFips = computed<string | null>(() => {
|
|
799
|
+
const s = props.state?.trim();
|
|
800
|
+
if (!s) return null;
|
|
801
|
+
if (/^\d{1,2}$/.test(s)) return s.padStart(2, "0");
|
|
802
|
+
const match = statesFeatures.value.find((f) => f.properties?.name === s);
|
|
803
|
+
return match?.id != null ? String(match.id).padStart(2, "0") : null;
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// The single state's GeoJSON feature — drives the outline path and the
|
|
807
|
+
// projection fit in single-state mode.
|
|
808
|
+
const stateOutlineFeature = computed<StateFeature | null>(() => {
|
|
809
|
+
const fips = stateFips.value;
|
|
810
|
+
if (!fips) return null;
|
|
811
|
+
return (
|
|
812
|
+
statesFeatures.value.find((f) => String(f.id).padStart(2, "0") === fips) ??
|
|
813
|
+
null
|
|
814
|
+
);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
watch(
|
|
818
|
+
() => [props.state, stateFips.value] as const,
|
|
819
|
+
([state, fips]) => {
|
|
820
|
+
if (state && state.trim() && !fips) {
|
|
821
|
+
console.warn(
|
|
822
|
+
`[ChoroplethMap] state="${state}" matched no state name or FIPS code; rendering the full map.`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
{ immediate: true },
|
|
827
|
+
);
|
|
828
|
+
|
|
695
829
|
// HSA mapping is loaded lazily — it's ~25KB gzipped and only needed when
|
|
696
830
|
// geoType or dataGeoType is "hsas". Keeps the main bundle small for users
|
|
697
831
|
// who only need states/counties maps.
|
|
@@ -727,10 +861,14 @@ const hsaFeaturesGeo = computed(() => {
|
|
|
727
861
|
const { fipsToHsa, hsaNames } = mod;
|
|
728
862
|
const topo = toRaw(props.topology) as unknown as CountiesTopo;
|
|
729
863
|
const countyGeometries = topo.objects.counties.geometries;
|
|
864
|
+
const scopeFips = stateFips.value;
|
|
730
865
|
const groups = new Map<string, typeof countyGeometries>();
|
|
731
866
|
|
|
732
867
|
for (const geom of countyGeometries) {
|
|
733
868
|
const fips = String(geom.id).padStart(5, "0");
|
|
869
|
+
// Single-state mode: drop counties outside the state before grouping, so
|
|
870
|
+
// the resulting HSAs only cover the selected state.
|
|
871
|
+
if (scopeFips && fips.slice(0, 2) !== scopeFips) continue;
|
|
734
872
|
const hsaCode = fipsToHsa[fips];
|
|
735
873
|
if (!hsaCode) continue;
|
|
736
874
|
if (!groups.has(hsaCode)) groups.set(hsaCode, []);
|
|
@@ -751,30 +889,71 @@ const hsaFeaturesGeo = computed(() => {
|
|
|
751
889
|
});
|
|
752
890
|
|
|
753
891
|
const featuresGeo = computed(() => {
|
|
892
|
+
// hsaFeaturesGeo already honors `state` (it scopes the source counties).
|
|
754
893
|
if (props.geoType === "hsas") return hsaFeaturesGeo.value;
|
|
894
|
+
const scopeFips = stateFips.value;
|
|
755
895
|
if (props.geoType === "counties") {
|
|
756
896
|
const topo = toRaw(props.topology) as unknown as CountiesTopo;
|
|
757
|
-
|
|
897
|
+
const fc = feature(topo, topo.objects.counties);
|
|
898
|
+
if (!scopeFips) return fc;
|
|
899
|
+
return {
|
|
900
|
+
type: "FeatureCollection" as const,
|
|
901
|
+
features: fc.features.filter(
|
|
902
|
+
(f) => String(f.id).padStart(5, "0").slice(0, 2) === scopeFips,
|
|
903
|
+
),
|
|
904
|
+
};
|
|
758
905
|
}
|
|
759
906
|
const topo = toRaw(props.topology) as unknown as StatesTopo;
|
|
760
|
-
|
|
907
|
+
const fc = feature(topo, topo.objects.states);
|
|
908
|
+
if (!scopeFips) return fc;
|
|
909
|
+
return {
|
|
910
|
+
type: "FeatureCollection" as const,
|
|
911
|
+
features: fc.features.filter(
|
|
912
|
+
(f) => String(f.id).padStart(2, "0") === scopeFips,
|
|
913
|
+
),
|
|
914
|
+
};
|
|
761
915
|
});
|
|
762
916
|
|
|
763
917
|
const stateBordersPath = computed(() => {
|
|
764
918
|
if (props.geoType !== "counties" && props.geoType !== "hsas") return null;
|
|
919
|
+
// Single-state mode: trace just the selected state's outline instead of
|
|
920
|
+
// the full national state-border mesh.
|
|
921
|
+
if (stateFips.value) return stateOutlineFeature.value;
|
|
765
922
|
const topo = toRaw(props.topology) as unknown as CountiesTopo;
|
|
766
923
|
return mesh(topo, topo.objects.states, (a, b) => a !== b);
|
|
767
924
|
});
|
|
768
925
|
|
|
769
|
-
|
|
770
|
-
|
|
926
|
+
// Breathing room (canonical px) around a single state so its outline isn't
|
|
927
|
+
// flush against the SVG edge. Only applied in single-state mode.
|
|
928
|
+
const STATE_FIT_INSET = 12;
|
|
929
|
+
|
|
930
|
+
const projection = computed(() => {
|
|
931
|
+
const outline = stateOutlineFeature.value;
|
|
932
|
+
if (stateFips.value && outline) {
|
|
933
|
+
const extent: [[number, number], [number, number]] = [
|
|
934
|
+
[STATE_FIT_INSET, STATE_FIT_INSET],
|
|
935
|
+
[width.value - STATE_FIT_INSET, height.value - STATE_FIT_INSET],
|
|
936
|
+
];
|
|
937
|
+
const albers = geoAlbersUsa().fitExtent(extent, outline);
|
|
938
|
+
// geoAlbersUsa only covers the 50 states + DC (it handles Alaska and
|
|
939
|
+
// Hawaii via insets). The island territories — Puerto Rico, Guam, the
|
|
940
|
+
// US Virgin Islands, American Samoa, the N. Mariana Islands — fall
|
|
941
|
+
// outside it and project to null, so fitExtent yields a NaN transform
|
|
942
|
+
// and every path renders as "MNaN,NaN…". Detect that by projecting the
|
|
943
|
+
// outline's centroid and fall back to a plain Mercator that can render
|
|
944
|
+
// any region.
|
|
945
|
+
const c = geoPath(albers).centroid(outline);
|
|
946
|
+
if (Number.isFinite(c[0]) && Number.isFinite(c[1])) return albers;
|
|
947
|
+
return geoMercator().fitExtent(extent, outline);
|
|
948
|
+
}
|
|
949
|
+
return geoAlbersUsa().fitExtent(
|
|
771
950
|
[
|
|
772
951
|
[0, 0],
|
|
773
952
|
[width.value, height.value],
|
|
774
953
|
],
|
|
775
954
|
featuresGeo.value,
|
|
776
|
-
)
|
|
777
|
-
);
|
|
955
|
+
);
|
|
956
|
+
});
|
|
778
957
|
|
|
779
958
|
const pathGenerator = computed(() => geoPath(projection.value));
|
|
780
959
|
|
|
@@ -1481,9 +1660,12 @@ const menuItems = computed<ChartMenuItem[]>(() => {
|
|
|
1481
1660
|
// Registered last so the eagerly-evaluated source getters can read every
|
|
1482
1661
|
// computed defined above without hitting a TDZ.
|
|
1483
1662
|
|
|
1484
|
-
// Geometry / projection / tooltip-mode → full rebuild.
|
|
1663
|
+
// Geometry / projection / tooltip-mode → full rebuild. `featuresGeo` is
|
|
1664
|
+
// watched explicitly because in single-state mode the projection is pinned
|
|
1665
|
+
// to the static state outline, so an async change to the feature set (e.g.
|
|
1666
|
+
// the lazy HSA module resolving) wouldn't otherwise change `pathGenerator`.
|
|
1485
1667
|
watch(
|
|
1486
|
-
() => [pathGenerator.value, hasInteractiveTooltip.value],
|
|
1668
|
+
() => [featuresGeo.value, pathGenerator.value, hasInteractiveTooltip.value],
|
|
1487
1669
|
() => rebuildPaths(),
|
|
1488
1670
|
);
|
|
1489
1671
|
|
|
@@ -1535,6 +1717,8 @@ watch(
|
|
|
1535
1717
|
{ pannable: pan, 'is-fullscreen': fullscreen.isFullscreen.value },
|
|
1536
1718
|
]"
|
|
1537
1719
|
:style="fullscreen.fullscreenStyle.value"
|
|
1720
|
+
:role="chartRole || undefined"
|
|
1721
|
+
:aria-label="chartAriaLabel || undefined"
|
|
1538
1722
|
>
|
|
1539
1723
|
<ChartMenu
|
|
1540
1724
|
v-if="menu"
|
|
@@ -1596,6 +1780,7 @@ watch(
|
|
|
1596
1780
|
ref="svgRef"
|
|
1597
1781
|
:viewBox="`0 0 ${width} ${height}`"
|
|
1598
1782
|
preserveAspectRatio="xMidYMid meet"
|
|
1783
|
+
:style="twoFingerPan ? { touchAction: 'pan-y' } : undefined"
|
|
1599
1784
|
>
|
|
1600
1785
|
<!--
|
|
1601
1786
|
Path elements are created imperatively in `rebuildPaths()`; Vue never
|
|
@@ -888,6 +888,56 @@ Markers compose (`**_bold italic_**`).
|
|
|
888
888
|
</template>
|
|
889
889
|
</ComponentDemo>
|
|
890
890
|
|
|
891
|
+
Multi-line callouts attach to whichever edge of the text faces the anchor:
|
|
892
|
+
text **above** the point connects to its bottom edge, text **below**
|
|
893
|
+
connects to its top, and text to the side connects to its near vertical
|
|
894
|
+
edge — so the pointer never cuts through the lines. The demo below anchors
|
|
895
|
+
the same two-line label at one point and fans it out in every direction.
|
|
896
|
+
|
|
897
|
+
<ComponentDemo>
|
|
898
|
+
<LineChart
|
|
899
|
+
:data="[2, 10, 7, 6, 8, 4, 9]"
|
|
900
|
+
:annotations="[
|
|
901
|
+
{ x: 3, y: 6, offset: { x: 0, y: -86 }, text: 'Cluster\ndetected' },
|
|
902
|
+
{ x: 3, y: 6, offset: { x: 62, y: -62 }, text: 'Cluster\ndetected' },
|
|
903
|
+
{ x: 3, y: 6, offset: { x: 96, y: 0 }, text: 'Cluster\ndetected' },
|
|
904
|
+
{ x: 3, y: 6, offset: { x: 62, y: 62 }, text: 'Cluster\ndetected' },
|
|
905
|
+
{ x: 3, y: 6, offset: { x: 0, y: 86 }, text: 'Cluster\ndetected' },
|
|
906
|
+
{ x: 3, y: 6, offset: { x: -62, y: 62 }, text: 'Cluster\ndetected' },
|
|
907
|
+
{ x: 3, y: 6, offset: { x: -96, y: 0 }, text: 'Cluster\ndetected' },
|
|
908
|
+
{ x: 3, y: 6, offset: { x: -62, y: -62 }, text: 'Cluster\ndetected' },
|
|
909
|
+
]"
|
|
910
|
+
:chart-padding="{ top: 70, right: 120, bottom: 50, left: 120 }"
|
|
911
|
+
:height="340"
|
|
912
|
+
x-label="Days"
|
|
913
|
+
y-label="Cases"
|
|
914
|
+
/>
|
|
915
|
+
|
|
916
|
+
<template #code>
|
|
917
|
+
|
|
918
|
+
```vue
|
|
919
|
+
<LineChart
|
|
920
|
+
:data="[2, 10, 7, 6, 8, 4, 9]"
|
|
921
|
+
:annotations="[
|
|
922
|
+
{ x: 3, y: 6, offset: { x: 0, y: -86 }, text: 'Cluster\ndetected' },
|
|
923
|
+
{ x: 3, y: 6, offset: { x: 62, y: -62 }, text: 'Cluster\ndetected' },
|
|
924
|
+
{ x: 3, y: 6, offset: { x: 96, y: 0 }, text: 'Cluster\ndetected' },
|
|
925
|
+
{ x: 3, y: 6, offset: { x: 62, y: 62 }, text: 'Cluster\ndetected' },
|
|
926
|
+
{ x: 3, y: 6, offset: { x: 0, y: 86 }, text: 'Cluster\ndetected' },
|
|
927
|
+
{ x: 3, y: 6, offset: { x: -62, y: 62 }, text: 'Cluster\ndetected' },
|
|
928
|
+
{ x: 3, y: 6, offset: { x: -96, y: 0 }, text: 'Cluster\ndetected' },
|
|
929
|
+
{ x: 3, y: 6, offset: { x: -62, y: -62 }, text: 'Cluster\ndetected' },
|
|
930
|
+
]"
|
|
931
|
+
:chart-padding="{ top: 70, right: 120, bottom: 50, left: 120 }"
|
|
932
|
+
:height="340"
|
|
933
|
+
x-label="Days"
|
|
934
|
+
y-label="Cases"
|
|
935
|
+
/>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
</template>
|
|
939
|
+
</ComponentDemo>
|
|
940
|
+
|
|
891
941
|
```ts
|
|
892
942
|
interface ChartAnnotation {
|
|
893
943
|
x: number; // anchor in data coords (x-axis)
|
|
@@ -1063,6 +1113,29 @@ until the user clicks Download:
|
|
|
1063
1113
|
<LineChart :data="cases" :csv="() => buildCsv(cases, dates)" />
|
|
1064
1114
|
```
|
|
1065
1115
|
|
|
1116
|
+
## Accessibility
|
|
1117
|
+
|
|
1118
|
+
The chart's individual marks aren't exposed to assistive tech, so the chart
|
|
1119
|
+
announces itself with a single accessible name. When the chart has a `title`,
|
|
1120
|
+
its root element gets `role="figure"` and an `aria-label` set to the title, so
|
|
1121
|
+
screen readers announce it as a labeled figure while the menu and download
|
|
1122
|
+
controls stay reachable.
|
|
1123
|
+
|
|
1124
|
+
Set `ariaLabel` to give screen readers a fuller summary than the visible title
|
|
1125
|
+
(it overrides `title` for the accessible name only):
|
|
1126
|
+
|
|
1127
|
+
```vue
|
|
1128
|
+
<LineChart
|
|
1129
|
+
:data="cases"
|
|
1130
|
+
title="Daily ED visits"
|
|
1131
|
+
aria-label="Daily ED visits over 11 days, rising from 12 to a peak of 48"
|
|
1132
|
+
/>
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
Pass `role` to override the default, e.g. `role="img"` to expose the chart as a
|
|
1136
|
+
single image (note this hides the inner menu/download controls from assistive
|
|
1137
|
+
tech).
|
|
1138
|
+
|
|
1066
1139
|
## Props
|
|
1067
1140
|
|
|
1068
1141
|
| Prop | Type | Required | Default |
|
|
@@ -1071,6 +1144,8 @@ until the user clicks Download:
|
|
|
1071
1144
|
| `height` | `number` | No | — |
|
|
1072
1145
|
| `title` | `string` | No | — |
|
|
1073
1146
|
| `titleStyle` | `TitleStyle` | No | — |
|
|
1147
|
+
| `role` | `string` | No | — |
|
|
1148
|
+
| `ariaLabel` | `string` | No | — |
|
|
1074
1149
|
| `axisLabelStyle` | `LabelStyle` | No | — |
|
|
1075
1150
|
| `tickLabelStyle` | `LabelStyle` | No | — |
|
|
1076
1151
|
| `legendStyle` | `LabelStyle` | No | — |
|
|
@@ -240,6 +240,14 @@ const props = withDefaults(defineProps<LineChartProps>(), {
|
|
|
240
240
|
yScaleType: "linear",
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
// Accessible name for the whole chart; falls back to the visible title.
|
|
244
|
+
const chartAriaLabel = computed(() => props.ariaLabel ?? props.title);
|
|
245
|
+
// Label the chart as a figure when it has a name (keeps inner controls
|
|
246
|
+
// reachable, unlike role="img"). An explicit `role` prop always wins.
|
|
247
|
+
const chartRole = computed(
|
|
248
|
+
() => props.role ?? (chartAriaLabel.value ? "figure" : undefined),
|
|
249
|
+
);
|
|
250
|
+
|
|
243
251
|
// The template root is a <Teleport>, so fallthrough attrs (class, style,
|
|
244
252
|
// data-*, id…) can't auto-inherit — forward them onto the wrapper manually.
|
|
245
253
|
defineOptions({ inheritAttrs: false });
|
|
@@ -1048,6 +1056,8 @@ const positionedLegendItems = computed(() => {
|
|
|
1048
1056
|
class="line-chart-wrapper"
|
|
1049
1057
|
:class="{ 'is-fullscreen': isFullscreen }"
|
|
1050
1058
|
:style="fullscreenStyle"
|
|
1059
|
+
:role="chartRole || undefined"
|
|
1060
|
+
:aria-label="chartAriaLabel || undefined"
|
|
1051
1061
|
>
|
|
1052
1062
|
<ChartMenu
|
|
1053
1063
|
v-if="menu"
|
|
@@ -36,9 +36,9 @@ const ANCHOR_GAP_PX = 4;
|
|
|
36
36
|
const LABEL_GAP_PX = 6;
|
|
37
37
|
const LINE_HEIGHT_RATIO = 1.2;
|
|
38
38
|
// Ratio of font-size that puts the pointer endpoint at the visual center
|
|
39
|
-
// of
|
|
40
|
-
//
|
|
41
|
-
const
|
|
39
|
+
// of a text line (between baseline and cap-height). Lands on the x-height
|
|
40
|
+
// middle for most fonts.
|
|
41
|
+
const LINE_CENTER_RATIO = 0.35;
|
|
42
42
|
|
|
43
43
|
interface TextRun {
|
|
44
44
|
text: string;
|
|
@@ -139,6 +139,8 @@ const items = computed<RenderedAnnotation[]>(() => {
|
|
|
139
139
|
a.align ?? (offsetX > 0 ? "left" : offsetX < 0 ? "right" : "center");
|
|
140
140
|
const textAnchor = alignMap[align];
|
|
141
141
|
|
|
142
|
+
const lines = a.text.split("\n").map(parseInline);
|
|
143
|
+
|
|
142
144
|
let rule: RenderedAnnotation["rule"];
|
|
143
145
|
let pointerPath = "";
|
|
144
146
|
let arrowTip: RenderedAnnotation["arrowTip"];
|
|
@@ -152,6 +154,7 @@ const items = computed<RenderedAnnotation[]>(() => {
|
|
|
152
154
|
labelX,
|
|
153
155
|
labelY,
|
|
154
156
|
fontSize,
|
|
157
|
+
lines.length,
|
|
155
158
|
pointer as "curved" | "straight" | "none",
|
|
156
159
|
);
|
|
157
160
|
pointerPath = built.path;
|
|
@@ -159,7 +162,7 @@ const items = computed<RenderedAnnotation[]>(() => {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
out.push({
|
|
162
|
-
lines
|
|
165
|
+
lines,
|
|
163
166
|
textX: labelX,
|
|
164
167
|
textY: labelY,
|
|
165
168
|
textAnchor,
|
|
@@ -220,12 +223,17 @@ function computeRule(
|
|
|
220
223
|
* - When the label has offset in only one dimension, the line is
|
|
221
224
|
* straight (vertical or horizontal) ending at the label's baseline.
|
|
222
225
|
* - When both dimensions have offset, the line is a quarter-arc:
|
|
223
|
-
* a quadratic Bezier with the control point at `(anchorX,
|
|
224
|
-
* where `
|
|
225
|
-
* (slightly above
|
|
226
|
-
* vertically toward the label's row, then bends horizontally
|
|
227
|
-
*
|
|
228
|
-
*
|
|
226
|
+
* a quadratic Bezier with the control point at `(anchorX, targetY)`
|
|
227
|
+
* where `targetY` is the visual center of the text line nearest the
|
|
228
|
+
* anchor (slightly above its baseline). The curve emerges from the
|
|
229
|
+
* anchor vertically toward the label's row, then bends horizontally
|
|
230
|
+
* into the label.
|
|
231
|
+
*
|
|
232
|
+
* `targetY` tracks the *near* edge of the text block: the first line
|
|
233
|
+
* when the label sits below the anchor (connector points up at the top
|
|
234
|
+
* line), the last line when the label sits above the anchor (connector
|
|
235
|
+
* points up at the bottom line) — so multi-line text above the point
|
|
236
|
+
* connects to its bottom edge instead of having the line cut through it.
|
|
229
237
|
*/
|
|
230
238
|
interface PointerGeom {
|
|
231
239
|
path: string;
|
|
@@ -242,23 +250,30 @@ function buildPointerPath(
|
|
|
242
250
|
lx: number,
|
|
243
251
|
ly: number,
|
|
244
252
|
fontSize: number,
|
|
253
|
+
lineCount: number,
|
|
245
254
|
pointer: "curved" | "straight" | "none",
|
|
246
255
|
): PointerGeom {
|
|
247
256
|
if (pointer === "none") return { path: "" };
|
|
248
257
|
const dx = lx - ax;
|
|
249
258
|
const dy = ly - ay;
|
|
250
259
|
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
260
|
+
// Aim at the visual center of the text line nearest the anchor: the
|
|
261
|
+
// first (top) line when the label is below the anchor, the last
|
|
262
|
+
// (bottom) line when the label is above it. That keeps the connector
|
|
263
|
+
// attached to the block's near edge instead of cutting through the
|
|
264
|
+
// lower lines of text that sits above the point.
|
|
265
|
+
const lineHeight = fontSize * LINE_HEIGHT_RATIO;
|
|
266
|
+
const nearestBaseline = dy < 0 ? ly + (lineCount - 1) * lineHeight : ly;
|
|
267
|
+
const targetY = nearestBaseline - fontSize * LINE_CENTER_RATIO;
|
|
254
268
|
|
|
255
269
|
// Pure horizontal or vertical → straight line at baseline, no curve.
|
|
256
270
|
// Force straight when explicitly requested.
|
|
257
271
|
if (dx === 0 || dy === 0 || pointer === "straight") {
|
|
258
|
-
// For straight pointers, aim at
|
|
259
|
-
//
|
|
260
|
-
// already at exactly the same Y (pure horizontal
|
|
261
|
-
// horizontal case stays on the baseline so it's a
|
|
272
|
+
// For straight pointers, aim at the near-line center (so multi-line
|
|
273
|
+
// text above the point connects to its bottom edge) — but only when
|
|
274
|
+
// the anchor isn't already at exactly the same Y (pure horizontal
|
|
275
|
+
// offset). The pure horizontal case stays on the baseline so it's a
|
|
276
|
+
// real horizontal line.
|
|
262
277
|
const ey = dy === 0 ? ly : targetY;
|
|
263
278
|
const segDx = lx - ax;
|
|
264
279
|
const segDy = ey - ay;
|
|
@@ -103,6 +103,21 @@ export interface ChartCommonProps {
|
|
|
103
103
|
title?: string;
|
|
104
104
|
/** Styling for the chart title. See `TitleStyle`. */
|
|
105
105
|
titleStyle?: TitleStyle;
|
|
106
|
+
/**
|
|
107
|
+
* ARIA role for the chart's root element. Defaults to `"figure"` when an
|
|
108
|
+
* accessible name is present (from `ariaLabel` or `title`), so screen
|
|
109
|
+
* readers announce the chart as a labeled figure while its controls (menu,
|
|
110
|
+
* download) stay reachable. Pass `"img"` to expose it as a single image
|
|
111
|
+
* (which hides the inner controls from assistive tech), or your own role.
|
|
112
|
+
*/
|
|
113
|
+
role?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Accessible name for the chart, announced by screen readers via the root
|
|
116
|
+
* element's `aria-label`. Defaults to the `title` prop. The individual
|
|
117
|
+
* marks aren't exposed to assistive tech, so set this to a short summary of
|
|
118
|
+
* what the chart shows.
|
|
119
|
+
*/
|
|
120
|
+
ariaLabel?: string;
|
|
106
121
|
/** Styling for axis labels (the `xLabel` / `yLabel` text). */
|
|
107
122
|
axisLabelStyle?: LabelStyle;
|
|
108
123
|
/** Styling for axis tick labels (the numbers/categories on each axis). */
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A responsive two-panel layout with a collapsible sidebar and main content area. On mobile, the sidebar becomes an overlay.
|
|
4
4
|
|
|
5
|
+
When the sidebar is collapsed it is marked [`inert`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert), so its contents (including the collapse button) are removed from the tab order and the accessibility tree while visually hidden. The expand button is likewise `inert` while the sidebar is open. This keeps keyboard and screen-reader focus on whichever control is actually visible.
|
|
6
|
+
|
|
5
7
|
## Demo
|
|
6
8
|
|
|
7
9
|
<a href="/cfa-simulator/docs/demos/sidebar-layout/index.html" target="_blank">Open in full window ↗</a>
|
|
@@ -86,7 +86,7 @@ if (globals) {
|
|
|
86
86
|
<template>
|
|
87
87
|
<div class="SidebarLayout" :data-collapsed="collapsed">
|
|
88
88
|
<div class="SidebarRail">
|
|
89
|
-
<aside class="Sidebar">
|
|
89
|
+
<aside class="Sidebar" :inert="collapsed">
|
|
90
90
|
<div class="SidebarScroll">
|
|
91
91
|
<div class="SidebarHeader">
|
|
92
92
|
<button
|
|
@@ -105,6 +105,7 @@ if (globals) {
|
|
|
105
105
|
<button
|
|
106
106
|
type="button"
|
|
107
107
|
class="Toggle Toggle--expand"
|
|
108
|
+
:inert="!collapsed"
|
|
108
109
|
aria-label="Expand sidebar"
|
|
109
110
|
title="Expand sidebar"
|
|
110
111
|
@click="toggle"
|
package/index.json
CHANGED