@cfasim-ui/docs 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/charts/BarChart/BarChart.md +54 -1
- package/charts/BarChart/BarChart.vue +248 -48
- package/charts/ChartMenu/ChartMenu.vue +3 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +8 -2
- package/charts/ChoroplethMap/ChoroplethMap.vue +102 -11
- package/charts/LineChart/LineChart.md +226 -9
- package/charts/LineChart/LineChart.vue +381 -120
- package/charts/_shared/ChartAnnotations.vue +56 -17
- package/charts/_shared/annotations.ts +14 -9
- package/charts/_shared/chartProps.ts +39 -0
- package/charts/_shared/dateAxis.ts +507 -0
- package/charts/_shared/fullscreen.css +51 -0
- package/charts/_shared/index.ts +25 -1
- package/charts/_shared/useChartFoundation.ts +57 -27
- package/charts/_shared/useChartFullscreen.ts +104 -0
- package/charts/_shared/useChartMenu.ts +19 -4
- package/charts/_shared/useChartPadding.ts +152 -18
- package/charts/_shared/useChartSize.ts +14 -5
- package/charts/hsa-mapping.ts +1 -0
- package/charts/index.ts +2 -1
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/useUrlParams.ts +203 -40
|
@@ -17,10 +17,17 @@ import "d3-transition";
|
|
|
17
17
|
import { feature, mesh, merge } from "topojson-client";
|
|
18
18
|
import type { Topology, GeometryCollection } from "topojson-specification";
|
|
19
19
|
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
20
|
-
import { fipsToHsa, hsaNames } from "./hsaMapping.js";
|
|
21
20
|
import ChartMenu from "../ChartMenu/ChartMenu.vue";
|
|
22
21
|
import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
|
|
23
22
|
import { saveSvg, savePng } from "../ChartMenu/download.js";
|
|
23
|
+
import {
|
|
24
|
+
useChartFullscreen,
|
|
25
|
+
TITLE_FONT_SIZE,
|
|
26
|
+
TITLE_LINE_HEIGHT,
|
|
27
|
+
TITLE_FONT_WEIGHT,
|
|
28
|
+
type TitleStyle,
|
|
29
|
+
type LabelStyle,
|
|
30
|
+
} from "../_shared/index.js";
|
|
24
31
|
import { placeTooltip } from "../tooltip-position.js";
|
|
25
32
|
import ChoroplethTooltip from "./ChoroplethTooltip.vue";
|
|
26
33
|
|
|
@@ -103,7 +110,15 @@ const props = withDefaults(
|
|
|
103
110
|
width?: number;
|
|
104
111
|
height?: number;
|
|
105
112
|
colorScale?: ChoroplethColorScale | ThresholdStop[] | CategoricalStop[];
|
|
113
|
+
/**
|
|
114
|
+
* Map title. `\n` in the string creates additional lines, each
|
|
115
|
+
* adding `titleStyle.lineHeight` (default 18px) of vertical space.
|
|
116
|
+
*/
|
|
106
117
|
title?: string;
|
|
118
|
+
/** Styling for the map title. See `TitleStyle`. */
|
|
119
|
+
titleStyle?: TitleStyle;
|
|
120
|
+
/** Styling for the legend (title, swatch labels, and continuous-scale ticks). */
|
|
121
|
+
legendStyle?: LabelStyle;
|
|
107
122
|
noDataColor?: string;
|
|
108
123
|
strokeColor?: string;
|
|
109
124
|
strokeWidth?: number;
|
|
@@ -230,6 +245,33 @@ const slots = useSlots();
|
|
|
230
245
|
const hasInteractiveTooltip = computed(
|
|
231
246
|
() => !!props.tooltipTrigger || !!props.tooltipFormat || !!slots.tooltip,
|
|
232
247
|
);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Inline style for the legend container. Font properties cascade to
|
|
251
|
+
* children (legend title, swatch labels, continuous-scale ticks).
|
|
252
|
+
*/
|
|
253
|
+
const legendInlineStyle = computed(() => {
|
|
254
|
+
const s = props.legendStyle;
|
|
255
|
+
const style: Record<string, string> = {};
|
|
256
|
+
if (s?.fontSize != null) style["font-size"] = `${s.fontSize}px`;
|
|
257
|
+
if (s?.fontWeight != null) style["font-weight"] = String(s.fontWeight);
|
|
258
|
+
if (s?.color != null) style.color = s.color;
|
|
259
|
+
return style;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/** Inline style for the title element, applying TitleStyle overrides. */
|
|
263
|
+
const titleInlineStyle = computed(() => {
|
|
264
|
+
const s = props.titleStyle;
|
|
265
|
+
const style: Record<string, string> = {
|
|
266
|
+
"font-size": `${s?.fontSize ?? TITLE_FONT_SIZE}px`,
|
|
267
|
+
"line-height": `${s?.lineHeight ?? TITLE_LINE_HEIGHT}px`,
|
|
268
|
+
"font-weight": String(s?.fontWeight ?? TITLE_FONT_WEIGHT),
|
|
269
|
+
"text-align": s?.align ?? "left",
|
|
270
|
+
width: "100%",
|
|
271
|
+
};
|
|
272
|
+
if (s?.color) style.color = s.color;
|
|
273
|
+
return style;
|
|
274
|
+
});
|
|
233
275
|
// Imperative path bookkeeping. Plain Maps rather than refs — Vue never reads
|
|
234
276
|
// these from a render scope, so mutating them does not trigger re-renders.
|
|
235
277
|
const pathsByFeatureId = new Map<string, SVGPathElement>();
|
|
@@ -611,7 +653,39 @@ type CountiesTopo = Topology<{
|
|
|
611
653
|
states: NamedGeometry;
|
|
612
654
|
}>;
|
|
613
655
|
|
|
656
|
+
// HSA mapping is loaded lazily — it's ~25KB gzipped and only needed when
|
|
657
|
+
// geoType or dataGeoType is "hsas". Keeps the main bundle small for users
|
|
658
|
+
// who only need states/counties maps.
|
|
659
|
+
type HsaModule = typeof import("./hsaMapping.js");
|
|
660
|
+
const hsaModule = ref<HsaModule | null>(null);
|
|
661
|
+
let hsaModulePromise: Promise<HsaModule> | null = null;
|
|
662
|
+
function loadHsaModule() {
|
|
663
|
+
if (!hsaModulePromise) {
|
|
664
|
+
hsaModulePromise = import("./hsaMapping.js").then((m) => {
|
|
665
|
+
hsaModule.value = m;
|
|
666
|
+
return m;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return hsaModulePromise;
|
|
670
|
+
}
|
|
671
|
+
watch(
|
|
672
|
+
() => {
|
|
673
|
+
if (props.geoType === "hsas" || props.dataGeoType === "hsas") return true;
|
|
674
|
+
const focus = props.focus;
|
|
675
|
+
if (!focus) return false;
|
|
676
|
+
const items = Array.isArray(focus) ? focus : [focus];
|
|
677
|
+
return items.some((f) => typeof f !== "string" && f.geoType === "hsas");
|
|
678
|
+
},
|
|
679
|
+
(needsHsa) => {
|
|
680
|
+
if (needsHsa) loadHsaModule();
|
|
681
|
+
},
|
|
682
|
+
{ immediate: true },
|
|
683
|
+
);
|
|
684
|
+
|
|
614
685
|
const hsaFeaturesGeo = computed(() => {
|
|
686
|
+
const mod = hsaModule.value;
|
|
687
|
+
if (!mod) return { type: "FeatureCollection" as const, features: [] };
|
|
688
|
+
const { fipsToHsa, hsaNames } = mod;
|
|
615
689
|
const topo = toRaw(props.topology) as unknown as CountiesTopo;
|
|
616
690
|
const countyGeometries = topo.objects.counties.geometries;
|
|
617
691
|
const groups = new Map<string, typeof countyGeometries>();
|
|
@@ -695,7 +769,7 @@ function baseToDataId(baseId: string): string | undefined {
|
|
|
695
769
|
const dataGt = props.dataGeoType;
|
|
696
770
|
if (!dataGt || dataGt === props.geoType) return baseId;
|
|
697
771
|
if (props.geoType === "counties" && dataGt === "hsas") {
|
|
698
|
-
return fipsToHsa[baseId];
|
|
772
|
+
return hsaModule.value?.fipsToHsa[baseId];
|
|
699
773
|
}
|
|
700
774
|
if (props.geoType === "counties" && dataGt === "states") {
|
|
701
775
|
return baseId.slice(0, 2);
|
|
@@ -1298,9 +1372,12 @@ const gradientCss = computed(() => {
|
|
|
1298
1372
|
return `linear-gradient(to right, ${stops})`;
|
|
1299
1373
|
});
|
|
1300
1374
|
|
|
1375
|
+
const fullscreen = useChartFullscreen();
|
|
1376
|
+
|
|
1301
1377
|
const menuItems = computed<ChartMenuItem[]>(() => {
|
|
1302
1378
|
const fname = menuFilename();
|
|
1303
1379
|
return [
|
|
1380
|
+
fullscreen.menuItem.value,
|
|
1304
1381
|
{
|
|
1305
1382
|
label: "Save as SVG",
|
|
1306
1383
|
action: () => {
|
|
@@ -1351,25 +1428,42 @@ watch(
|
|
|
1351
1428
|
// `flush: "post"` so any pending path rebuild from the watcher above has
|
|
1352
1429
|
// already run; we still use the GeoJSON pathGenerator directly so the SVG
|
|
1353
1430
|
// path tree isn't actually required, but keeping the order avoids stacking
|
|
1354
|
-
// two zoom transforms in the same tick.
|
|
1431
|
+
// two zoom transforms in the same tick. `hsaModule` is included so a
|
|
1432
|
+
// cross-geoType focus on an hsa item re-applies once the lazy module
|
|
1433
|
+
// resolves (the base pathGenerator doesn't depend on hsa data).
|
|
1355
1434
|
watch(
|
|
1356
|
-
() => [normalizedFocus.value, pathGenerator.value],
|
|
1435
|
+
() => [normalizedFocus.value, pathGenerator.value, hsaModule.value],
|
|
1357
1436
|
() => applyFocus(),
|
|
1358
1437
|
{ flush: "post" },
|
|
1359
1438
|
);
|
|
1360
1439
|
</script>
|
|
1361
1440
|
|
|
1362
1441
|
<template>
|
|
1363
|
-
<div
|
|
1442
|
+
<div
|
|
1443
|
+
ref="containerRef"
|
|
1444
|
+
:class="[
|
|
1445
|
+
'choropleth-wrapper',
|
|
1446
|
+
{ pannable: pan, 'is-fullscreen': fullscreen.isFullscreen.value },
|
|
1447
|
+
]"
|
|
1448
|
+
>
|
|
1364
1449
|
<ChartMenu v-if="menu" :items="menuItems" />
|
|
1450
|
+
<div class="chart-sr-only" aria-live="polite">
|
|
1451
|
+
{{ fullscreen.isFullscreen.value ? "Map expanded to fill window" : "" }}
|
|
1452
|
+
</div>
|
|
1365
1453
|
<!--
|
|
1366
1454
|
Title + legend live as an HTML overlay on top of the SVG so they keep
|
|
1367
1455
|
their intrinsic px sizes regardless of how the browser scales the
|
|
1368
1456
|
viewBox to fit the container.
|
|
1369
1457
|
-->
|
|
1370
1458
|
<div v-if="title || showLegend" class="choropleth-header">
|
|
1371
|
-
<div v-if="title" class="choropleth-title"
|
|
1372
|
-
|
|
1459
|
+
<div v-if="title" class="choropleth-title" :style="titleInlineStyle">
|
|
1460
|
+
{{ title }}
|
|
1461
|
+
</div>
|
|
1462
|
+
<div
|
|
1463
|
+
v-if="showLegend"
|
|
1464
|
+
class="choropleth-legend"
|
|
1465
|
+
:style="legendInlineStyle"
|
|
1466
|
+
>
|
|
1373
1467
|
<span v-if="legendTitle" class="choropleth-legend-title">
|
|
1374
1468
|
{{ legendTitle }}
|
|
1375
1469
|
</span>
|
|
@@ -1476,10 +1570,6 @@ watch(
|
|
|
1476
1570
|
cursor: grabbing;
|
|
1477
1571
|
}
|
|
1478
1572
|
|
|
1479
|
-
.choropleth-wrapper:hover :deep(.chart-menu-button) {
|
|
1480
|
-
opacity: 1;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
1573
|
.state-path {
|
|
1484
1574
|
cursor: pointer;
|
|
1485
1575
|
}
|
|
@@ -1530,6 +1620,7 @@ watch(
|
|
|
1530
1620
|
font-size: 14px;
|
|
1531
1621
|
font-weight: 600;
|
|
1532
1622
|
line-height: 1.2;
|
|
1623
|
+
white-space: pre-line;
|
|
1533
1624
|
}
|
|
1534
1625
|
|
|
1535
1626
|
.choropleth-legend {
|
|
@@ -59,6 +59,127 @@ charts, set `x` and `y` (or `data`) on each `Series`.
|
|
|
59
59
|
|
|
60
60
|
When `x` is omitted, `y`/`data` values are plotted at indices 0, 1, 2, etc.
|
|
61
61
|
|
|
62
|
+
### Dates
|
|
63
|
+
|
|
64
|
+
Pass ISO date strings (`YYYY-MM-DD`) or `Date` objects as `x` and the
|
|
65
|
+
chart auto-detects a date axis: ticks anchor to year / month / week /
|
|
66
|
+
day / hour boundaries depending on the visible range, and labels
|
|
67
|
+
format themselves accordingly. Numeric `x` arrays keep their existing
|
|
68
|
+
behavior — auto-detection is content-driven and never promotes plain
|
|
69
|
+
numbers.
|
|
70
|
+
|
|
71
|
+
<ComponentDemo>
|
|
72
|
+
<LineChart
|
|
73
|
+
:x="['2026-01-05', '2026-01-12', '2026-01-19', '2026-01-26', '2026-02-02', '2026-02-09', '2026-02-16', '2026-02-23', '2026-03-02', '2026-03-09']"
|
|
74
|
+
:y="[4, 9, 18, 32, 41, 38, 27, 18, 11, 6]"
|
|
75
|
+
:height="220"
|
|
76
|
+
y-label="% ED visits"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<template #code>
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<LineChart
|
|
83
|
+
:x="[
|
|
84
|
+
'2026-01-05',
|
|
85
|
+
'2026-01-12',
|
|
86
|
+
'2026-01-19',
|
|
87
|
+
'2026-01-26',
|
|
88
|
+
'2026-02-02',
|
|
89
|
+
'2026-02-09',
|
|
90
|
+
'2026-02-16',
|
|
91
|
+
'2026-02-23',
|
|
92
|
+
'2026-03-02',
|
|
93
|
+
'2026-03-09',
|
|
94
|
+
]"
|
|
95
|
+
:y="[4, 9, 18, 32, 41, 38, 27, 18, 11, 6]"
|
|
96
|
+
:height="220"
|
|
97
|
+
y-label="% ED visits"
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
</template>
|
|
102
|
+
</ComponentDemo>
|
|
103
|
+
|
|
104
|
+
Override the format via `x-tick-format`. Presets: `"iso"`,
|
|
105
|
+
`"iso-datetime"`, `"year"`, `"month-year"`, `"month-day"`, `"day"`,
|
|
106
|
+
`"time"`, `"datetime"`, `"medium"`. You can also pass an
|
|
107
|
+
`Intl.DateTimeFormatOptions` literal, or a function `(ms, unit?) =>
|
|
108
|
+
string` for full control.
|
|
109
|
+
|
|
110
|
+
<ComponentDemo>
|
|
111
|
+
<LineChart
|
|
112
|
+
:x="['2026-01-05', '2026-01-12', '2026-01-19', '2026-01-26', '2026-02-02']"
|
|
113
|
+
:y="[4, 9, 18, 32, 41]"
|
|
114
|
+
x-tick-format="iso"
|
|
115
|
+
:height="220"
|
|
116
|
+
y-label="% ED visits"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<template #code>
|
|
120
|
+
|
|
121
|
+
```vue
|
|
122
|
+
<LineChart
|
|
123
|
+
:x="['2026-01-05', '2026-01-12', '2026-01-19', '2026-01-26', '2026-02-02']"
|
|
124
|
+
:y="[4, 9, 18, 32, 41]"
|
|
125
|
+
x-tick-format="iso"
|
|
126
|
+
:height="220"
|
|
127
|
+
y-label="% ED visits"
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
</template>
|
|
132
|
+
</ComponentDemo>
|
|
133
|
+
|
|
134
|
+
By default the chart parses bare `YYYY-MM-DD` (and offset-less
|
|
135
|
+
`YYYY-MM-DDTHH:mm:ss`) as UTC and renders tick labels in UTC. Set
|
|
136
|
+
`timezone="local"` to interpret offset-less strings in the browser's
|
|
137
|
+
timezone instead. Strings that include an explicit offset (`Z` or
|
|
138
|
+
`+05:00`) are always parsed exactly as written.
|
|
139
|
+
|
|
140
|
+
> **Annotations on a date axis.** `ChartAnnotation.x` is always in the
|
|
141
|
+
> chart's resolved coordinate space — that's **epoch-ms** when the axis
|
|
142
|
+
> is in date mode. Pass `Date.UTC(2026, 0, 15)`, `new Date("2026-01-15").getTime()`,
|
|
143
|
+
> or `Date.parse("2026-01-15T00:00:00Z")`. Passing a small integer like
|
|
144
|
+
> `0` will place the annotation at 1970-01-01 (off-screen).
|
|
145
|
+
>
|
|
146
|
+
> **`xLabels` on a date axis.** When both `x: [...date strings]` and
|
|
147
|
+
> `xLabels` are supplied, the date axis wins — `xLabels` is ignored.
|
|
148
|
+
> Date strings carry their own labeling.
|
|
149
|
+
|
|
150
|
+
### Title
|
|
151
|
+
|
|
152
|
+
The `title` prop accepts `\n` to break across lines. Use `titleStyle`
|
|
153
|
+
to override `fontSize`, `lineHeight`, `color`, `fontWeight`, and
|
|
154
|
+
`align` (`"left" | "center" | "right"`, default `"left"`). Top padding
|
|
155
|
+
grows automatically for additional lines.
|
|
156
|
+
|
|
157
|
+
<ComponentDemo>
|
|
158
|
+
<LineChart
|
|
159
|
+
:data="[0, 4, 8, 15, 22, 30, 28, 20, 12, 5, 2]"
|
|
160
|
+
:height="220"
|
|
161
|
+
:title="'Daily ED visits\nInfluenza-like illness, last 11 days'"
|
|
162
|
+
:title-style="{ fontSize: 16, lineHeight: 20, color: '#0057b7' }"
|
|
163
|
+
x-label="Days"
|
|
164
|
+
y-label="Cases"
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
<template #code>
|
|
168
|
+
|
|
169
|
+
```vue
|
|
170
|
+
<LineChart
|
|
171
|
+
:data="[0, 4, 8, 15, 22, 30, 28, 20, 12, 5, 2]"
|
|
172
|
+
:height="220"
|
|
173
|
+
:title="'Daily ED visits\nInfluenza-like illness, last 11 days'"
|
|
174
|
+
:title-style="{ fontSize: 16, lineHeight: 20, color: '#0057b7' }"
|
|
175
|
+
x-label="Days"
|
|
176
|
+
y-label="Cases"
|
|
177
|
+
/>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
</template>
|
|
181
|
+
</ComponentDemo>
|
|
182
|
+
|
|
62
183
|
### Multiple series
|
|
63
184
|
|
|
64
185
|
<ComponentDemo>
|
|
@@ -98,6 +219,46 @@ When `x` is omitted, `y`/`data` values are plotted at indices 0, 1, 2, etc.
|
|
|
98
219
|
</template>
|
|
99
220
|
</ComponentDemo>
|
|
100
221
|
|
|
222
|
+
### Legend
|
|
223
|
+
|
|
224
|
+
Set `legend` on a series to add an entry to the inline legend strip above the plot. When the items don't all fit on one row at the chart's current width, the legend wraps to additional rows and the plot area shrinks to keep the legend clear of the data. Use `showInLegend: false` to keep a series off the legend (e.g. when its `legend` string is reused as a CSV column header) and `showInTooltip: false` to omit it from the hover tooltip.
|
|
225
|
+
|
|
226
|
+
<ComponentDemo>
|
|
227
|
+
<LineChart
|
|
228
|
+
:series="[
|
|
229
|
+
{ data: [0, 12, 28, 45, 60, 55, 40, 25, 12], color: '#0057b7', legend: 'No interventions' },
|
|
230
|
+
{ data: [0, 10, 22, 38, 50, 45, 32, 18, 8], color: '#fb7e38', legend: 'School closures' },
|
|
231
|
+
{ data: [0, 8, 18, 30, 40, 35, 25, 14, 6], color: '#ef4444', legend: 'Masking + distancing' },
|
|
232
|
+
{ data: [0, 6, 14, 24, 32, 28, 20, 10, 4], color: '#10b981', legend: 'Vaccination only' },
|
|
233
|
+
{ data: [0, 4, 10, 16, 22, 19, 13, 7, 3], color: '#6366f1', legend: 'Vaccination + masking' },
|
|
234
|
+
{ data: [0, 3, 7, 11, 15, 13, 9, 5, 2], color: '#a855f7', legend: 'All interventions combined' },
|
|
235
|
+
]"
|
|
236
|
+
:height="240"
|
|
237
|
+
x-label="Weeks"
|
|
238
|
+
y-label="Incidence"
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<template #code>
|
|
242
|
+
|
|
243
|
+
```vue
|
|
244
|
+
<LineChart
|
|
245
|
+
:series="[
|
|
246
|
+
{ data: scenarioA, color: '#0057b7', legend: 'No interventions' },
|
|
247
|
+
{ data: scenarioB, color: '#fb7e38', legend: 'School closures' },
|
|
248
|
+
{ data: scenarioC, color: '#ef4444', legend: 'Masking + distancing' },
|
|
249
|
+
{ data: scenarioD, color: '#10b981', legend: 'Vaccination only' },
|
|
250
|
+
{ data: scenarioE, color: '#6366f1', legend: 'Vaccination + masking' },
|
|
251
|
+
{ data: scenarioF, color: '#a855f7', legend: 'All interventions combined' },
|
|
252
|
+
]"
|
|
253
|
+
:height="240"
|
|
254
|
+
x-label="Weeks"
|
|
255
|
+
y-label="Incidence"
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
</template>
|
|
260
|
+
</ComponentDemo>
|
|
261
|
+
|
|
101
262
|
### Tooltip
|
|
102
263
|
|
|
103
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.
|
|
@@ -248,6 +409,46 @@ floor, and `yMin <= 0` is ignored.
|
|
|
248
409
|
</template>
|
|
249
410
|
</ComponentDemo>
|
|
250
411
|
|
|
412
|
+
### Outline
|
|
413
|
+
|
|
414
|
+
Set `outline: true` on a series to draw a page-colored stroke behind the
|
|
415
|
+
line. This is useful when lines overlap, cross dense areas, or sit on a
|
|
416
|
+
busy background — the outline keeps each series visually distinct.
|
|
417
|
+
|
|
418
|
+
<ComponentDemo>
|
|
419
|
+
<LineChart
|
|
420
|
+
:series="[
|
|
421
|
+
{ data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#fb7e38', strokeWidth: 3, outline: true },
|
|
422
|
+
{ data: [0, 12, 22, 40, 58, 58, 42, 22, 10], color: '#0057b7', strokeWidth: 3, outline: true },
|
|
423
|
+
]"
|
|
424
|
+
:height="200"
|
|
425
|
+
/>
|
|
426
|
+
|
|
427
|
+
<template #code>
|
|
428
|
+
|
|
429
|
+
```vue
|
|
430
|
+
<LineChart
|
|
431
|
+
:series="[
|
|
432
|
+
{
|
|
433
|
+
data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
|
|
434
|
+
color: '#fb7e38',
|
|
435
|
+
strokeWidth: 3,
|
|
436
|
+
outline: true,
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
data: [0, 12, 22, 40, 58, 58, 42, 22, 10],
|
|
440
|
+
color: '#0057b7',
|
|
441
|
+
strokeWidth: 3,
|
|
442
|
+
outline: true,
|
|
443
|
+
},
|
|
444
|
+
]"
|
|
445
|
+
:height="200"
|
|
446
|
+
/>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
</template>
|
|
450
|
+
</ComponentDemo>
|
|
451
|
+
|
|
251
452
|
### Many trajectories with low opacity
|
|
252
453
|
|
|
253
454
|
<ComponentDemo>
|
|
@@ -478,8 +679,9 @@ Pin callouts to data points with `annotations`. Each annotation anchors at
|
|
|
478
679
|
respects `xMin` and explicit `x` values), with a pixel
|
|
479
680
|
`offset: { x, y }` for the label position. Text supports `\n` for line
|
|
480
681
|
breaks. A curved
|
|
481
|
-
pointer line connects the anchor to the label, and the label gets
|
|
482
|
-
stroke matching the background so it stays legible over series
|
|
682
|
+
pointer line connects the anchor to the label, and the label gets an
|
|
683
|
+
outline stroke matching the background so it stays legible over series
|
|
684
|
+
lines.
|
|
483
685
|
|
|
484
686
|
<ComponentDemo>
|
|
485
687
|
<LineChart
|
|
@@ -564,9 +766,9 @@ interface ChartAnnotation {
|
|
|
564
766
|
color?: string; // text / pointer color (default: currentColor)
|
|
565
767
|
fontSize?: number; // default: 13 (matches axis labels)
|
|
566
768
|
fontWeight?: string | number; // default: "normal"
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
769
|
+
outlineColor?: string; // background-matched outline (default: var(--color-bg-0, #fff))
|
|
770
|
+
outlineWidth?: number; // default: 3
|
|
771
|
+
align?: "left" | "center" | "right"; // default: derived from offset[0] sign
|
|
570
772
|
lineColor?: string; // connector-line color override (default: color)
|
|
571
773
|
lineWidth?: number; // default: 1
|
|
572
774
|
lineDash?: string | number | readonly number[]; // SVG stroke-dasharray
|
|
@@ -619,7 +821,7 @@ positioned from the anchor as usual via `offset`.
|
|
|
619
821
|
y: 30,
|
|
620
822
|
offset: { x: -8, y: -6 },
|
|
621
823
|
text: 'Max',
|
|
622
|
-
|
|
824
|
+
align: 'right',
|
|
623
825
|
pointer: 'ruleFromLeft',
|
|
624
826
|
lineDash: '4 3',
|
|
625
827
|
},
|
|
@@ -649,7 +851,7 @@ positioned from the anchor as usual via `offset`.
|
|
|
649
851
|
y: 30,
|
|
650
852
|
offset: { x: -8, y: -6 },
|
|
651
853
|
text: 'Max',
|
|
652
|
-
|
|
854
|
+
align: 'right',
|
|
653
855
|
pointer: 'ruleFromLeft',
|
|
654
856
|
lineDash: '4 3',
|
|
655
857
|
},
|
|
@@ -664,6 +866,13 @@ positioned from the anchor as usual via `offset`.
|
|
|
664
866
|
</template>
|
|
665
867
|
</ComponentDemo>
|
|
666
868
|
|
|
869
|
+
### Chart menu
|
|
870
|
+
|
|
871
|
+
The chart menu (top-right) leads with Expand / Collapse and then Save as SVG,
|
|
872
|
+
Save as PNG, and Download CSV. Expand grows the chart to fill the browser
|
|
873
|
+
window so the plot has more room; press <kbd>Esc</kbd> or pick "Collapse" to
|
|
874
|
+
return to the inline size. Set `menu="false"` to hide the trigger entirely.
|
|
875
|
+
|
|
667
876
|
### Custom CSV download
|
|
668
877
|
|
|
669
878
|
By default, the Download CSV menu item exports the chart series as CSV. Use
|
|
@@ -720,6 +929,10 @@ until the user clicks Download:
|
|
|
720
929
|
| `width` | `number` | No | — |
|
|
721
930
|
| `height` | `number` | No | — |
|
|
722
931
|
| `title` | `string` | No | — |
|
|
932
|
+
| `titleStyle` | `TitleStyle` | No | — |
|
|
933
|
+
| `axisLabelStyle` | `LabelStyle` | No | — |
|
|
934
|
+
| `tickLabelStyle` | `LabelStyle` | No | — |
|
|
935
|
+
| `legendStyle` | `LabelStyle` | No | — |
|
|
723
936
|
| `xLabel` | `string` | No | — |
|
|
724
937
|
| `yLabel` | `string` | No | — |
|
|
725
938
|
| `debounce` | `number` | No | — |
|
|
@@ -735,7 +948,7 @@ until the user clicks Download:
|
|
|
735
948
|
| `chartPadding` | `ChartPadding` | No | — |
|
|
736
949
|
| `y` | `LineChartData` | No | — |
|
|
737
950
|
| `data` | `LineChartData` | No | — |
|
|
738
|
-
| `x` | `
|
|
951
|
+
| `x` | `LineChartXInput` | No | — |
|
|
739
952
|
| `series` | `Series[]` | No | — |
|
|
740
953
|
| `areas` | `Area[]` | No | — |
|
|
741
954
|
| `areaSections` | `AreaSection[]` | No | — |
|
|
@@ -745,7 +958,10 @@ until the user clicks Download:
|
|
|
745
958
|
| `xMin` | `number` | No | — |
|
|
746
959
|
| `xTicks` | `number \| number[]` | No | — |
|
|
747
960
|
| `yTicks` | `number \| number[]` | No | — |
|
|
748
|
-
| `xTickFormat` |
|
|
961
|
+
| `xTickFormat` | `\| NumberFormat
|
|
962
|
+
\| ((value: number, index: number) => string)
|
|
963
|
+
\| DateFormat` | No | — |
|
|
964
|
+
| `timezone` | `DateTimezone` | No | — |
|
|
749
965
|
| `yTickFormat` | `NumberFormat` | No | — |
|
|
750
966
|
| `xLabels` | `string[]` | No | — |
|
|
751
967
|
| `xGrid` | `boolean` | No | — |
|
|
@@ -789,6 +1005,7 @@ interface Series {
|
|
|
789
1005
|
strokeWidth?: number;
|
|
790
1006
|
opacity?: number;
|
|
791
1007
|
line?: boolean;
|
|
1008
|
+
outline?: boolean; // page-colored stroke drawn behind the line
|
|
792
1009
|
dots?: boolean;
|
|
793
1010
|
dotRadius?: number;
|
|
794
1011
|
dotFill?: string;
|