nuxt-charts 3.0.0 → 3.1.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.
Files changed (49) hide show
  1. package/README.md +53 -6
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +56 -3
  4. package/dist/runtime/assets/components.css +1 -1
  5. package/dist/runtime/assets/theme.css +1 -1
  6. package/dist/runtime/components/AreaChart.vue +1 -0
  7. package/dist/runtime/components/BarChart.vue +1 -0
  8. package/dist/runtime/components/DataChart.vue +91 -0
  9. package/dist/runtime/components/DataChart.vue.d.ts +27 -0
  10. package/dist/runtime/components/DottedMap.vue +261 -0
  11. package/dist/runtime/components/DottedMap.vue.d.ts +45 -0
  12. package/dist/runtime/components/LineChart.vue +1 -0
  13. package/dist/runtime/components/MetricChart.vue +322 -0
  14. package/dist/runtime/components/MetricChart.vue.d.ts +54 -0
  15. package/dist/runtime/components/TopoJSONMap.vue +273 -0
  16. package/dist/runtime/components/TopoJSONMap.vue.d.ts +45 -0
  17. package/dist/runtime/components/internal/CartesianFrame.vue +73 -4
  18. package/dist/runtime/components/internal/CartesianFrame.vue.d.ts +5 -0
  19. package/dist/runtime/components/internal/ChartAccessibility.vue.d.ts +1 -1
  20. package/dist/runtime/components/internal/CubeBarShape.vue.d.ts +1 -1
  21. package/dist/runtime/components/internal/MapCanvas.vue +297 -0
  22. package/dist/runtime/components/internal/MapCanvas.vue.d.ts +79 -0
  23. package/dist/runtime/components/internal/VariantBarShape.vue.d.ts +2 -2
  24. package/dist/runtime/data/world.json +182 -0
  25. package/dist/runtime/maps.d.ts +2 -0
  26. package/dist/runtime/maps.js +1 -0
  27. package/dist/runtime/spec/csv.d.ts +14 -0
  28. package/dist/runtime/spec/csv.js +13 -0
  29. package/dist/runtime/spec/index.d.ts +11 -0
  30. package/dist/runtime/spec/index.js +5 -0
  31. package/dist/runtime/spec/metric.d.ts +130 -0
  32. package/dist/runtime/spec/metric.js +63 -0
  33. package/dist/runtime/spec/resolve.d.ts +44 -0
  34. package/dist/runtime/spec/resolve.js +41 -0
  35. package/dist/runtime/spec/transform.d.ts +58 -0
  36. package/dist/runtime/spec/transform.js +199 -0
  37. package/dist/runtime/spec/types.d.ts +132 -0
  38. package/dist/runtime/spec/types.js +2 -0
  39. package/dist/runtime/types/charts.d.ts +131 -0
  40. package/dist/runtime/types/maps.d.ts +237 -0
  41. package/dist/runtime/types/maps.js +0 -0
  42. package/dist/runtime/types.d.ts +3 -0
  43. package/dist/runtime/utils/axis.d.ts +18 -0
  44. package/dist/runtime/utils/axis.js +44 -1
  45. package/dist/runtime/utils/dottedMap.d.ts +14 -0
  46. package/dist/runtime/utils/dottedMap.js +69 -0
  47. package/dist/runtime/utils/maps.d.ts +33 -0
  48. package/dist/runtime/utils/maps.js +77 -0
  49. package/package.json +23 -3
@@ -0,0 +1,45 @@
1
+ import type { DottedMapProps, MapDot, MapPin, MapZoom, MapMarkSlot, MapOverlaySlot, DottedMapTooltipSlot } from "../types/maps.js";
2
+ type __VLS_Slots = {
3
+ tooltip?: (props: DottedMapTooltipSlot) => unknown;
4
+ pin?: (props: MapMarkSlot<MapPin> & {
5
+ points: MapDot[];
6
+ }) => unknown;
7
+ dot?: (props: MapMarkSlot<MapDot> & {
8
+ points: MapDot[];
9
+ }) => unknown;
10
+ legend?: (props: {
11
+ items: Array<{
12
+ value: string;
13
+ color?: string;
14
+ }>;
15
+ }) => unknown;
16
+ default?: (props: MapOverlaySlot & {
17
+ points: MapDot[];
18
+ }) => unknown;
19
+ };
20
+ declare const __VLS_base: import("vue").DefineComponent<DottedMapProps, {
21
+ zoomIn: () => void | undefined;
22
+ zoomOut: () => void | undefined;
23
+ zoomTo: (k: number) => void | undefined;
24
+ resetView: () => void | undefined;
25
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ mouseenter: (values: MapPin | MapDot, event: MouseEvent | FocusEvent) => any;
27
+ mouseleave: (values: MapPin | MapDot, event: MouseEvent | FocusEvent) => any;
28
+ zoom: (transform: MapZoom) => any;
29
+ "pin-click": (pin: MapPin, event: MouseEvent | KeyboardEvent) => any;
30
+ "point-click": (event: MouseEvent | KeyboardEvent, point: MapDot) => any;
31
+ }, string, import("vue").PublicProps, Readonly<DottedMapProps> & Readonly<{
32
+ onMouseenter?: ((values: MapPin | MapDot, event: MouseEvent | FocusEvent) => any) | undefined;
33
+ onMouseleave?: ((values: MapPin | MapDot, event: MouseEvent | FocusEvent) => any) | undefined;
34
+ onZoom?: ((transform: MapZoom) => any) | undefined;
35
+ "onPin-click"?: ((pin: MapPin, event: MouseEvent | KeyboardEvent) => any) | undefined;
36
+ "onPoint-click"?: ((event: MouseEvent | KeyboardEvent, point: MapDot) => any) | undefined;
37
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
39
+ declare const _default: typeof __VLS_export;
40
+ export default _default;
41
+ type __VLS_WithSlots<T, S> = T & {
42
+ new (): {
43
+ $slots: S;
44
+ };
45
+ };
@@ -109,6 +109,7 @@ defineSlots();
109
109
  :frame-class="frameClass"
110
110
  skeleton-shape="wave"
111
111
  v-bind="props"
112
+ :value-stacked="stacked === true"
112
113
  >
113
114
  <!--
114
115
  Built with `h()` so the filter lands in the SVG namespace and the
@@ -0,0 +1,322 @@
1
+ <script setup>
2
+ import { computed, ref, useId, watch } from "vue";
3
+ import DataChart from "./DataChart.vue";
4
+ import { transform } from "../spec/transform";
5
+ import { formatBucket, seriesLabel } from "../spec/resolve";
6
+ import { toCsv } from "../spec/csv";
7
+ import {
8
+ DEFAULT_RANGES,
9
+ formatChange,
10
+ formatMetricValue,
11
+ metricDelta,
12
+ metricSpec,
13
+ resolveRange
14
+ } from "../spec/metric";
15
+ const props = defineProps({
16
+ data: { type: Array, required: true },
17
+ title: { type: String, required: false },
18
+ description: { type: String, required: false },
19
+ x: { type: String, required: true },
20
+ y: { type: String, required: false },
21
+ series: { type: String, required: false },
22
+ aggregate: { type: String, required: false, default: "sum" },
23
+ interval: { type: String, required: false },
24
+ compare: { type: String, required: false },
25
+ type: { type: String, required: false, default: "line" },
26
+ format: { type: String, required: false, default: "number" },
27
+ currency: { type: String, required: false, default: "EUR" },
28
+ locale: { type: String, required: false },
29
+ formatOptions: { type: null, required: false },
30
+ valueFormatter: { type: Function, required: false },
31
+ inverseSentiment: { type: Boolean, required: false },
32
+ ranges: { type: Array, required: false },
33
+ range: { type: String, required: false },
34
+ window: { type: Object, required: false },
35
+ anchor: { type: [String, Number, Date], required: false },
36
+ height: { type: Number, required: false, default: 220 },
37
+ stacked: { type: Boolean, required: false },
38
+ hideLegend: { type: Boolean, required: false },
39
+ theme: { type: Object, required: false },
40
+ showTable: { type: Boolean, required: false },
41
+ exportable: { type: Boolean, required: false },
42
+ exportFilename: { type: String, required: false },
43
+ hideValue: { type: Boolean, required: false },
44
+ ariaLabel: { type: String, required: false },
45
+ ariaDescription: { type: String, required: false },
46
+ accessibleDataTable: { type: Boolean, required: false },
47
+ maxDataPoints: { type: Number, required: false },
48
+ emptyLabel: { type: String, required: false },
49
+ error: { type: String, required: false },
50
+ legendVariant: { type: String, required: false },
51
+ tooltipVariant: { type: String, required: false },
52
+ tooltipRoundness: { type: String, required: false },
53
+ loading: { type: Boolean, required: false },
54
+ loadingLabel: { type: String, required: false }
55
+ });
56
+ const rangeModel = defineModel("range", { type: null, ...{ default: void 0 } });
57
+ const emit = defineEmits(["update:window", "export"]);
58
+ const ranges = computed(() => props.ranges ?? DEFAULT_RANGES);
59
+ const activeRange = computed(() => {
60
+ const list = ranges.value;
61
+ if (!list.length) return void 0;
62
+ return list.find((range) => range.key === rangeModel.value) ?? list[1] ?? list[0];
63
+ });
64
+ const window = computed(() => {
65
+ if (props.window) return props.window;
66
+ const range = activeRange.value;
67
+ if (!range) return void 0;
68
+ return resolveRange(range, props.anchor);
69
+ });
70
+ watch(window, (value) => {
71
+ if (value) emit("update:window", value);
72
+ }, { immediate: true });
73
+ const interval = computed(() => props.interval ?? activeRange.value?.interval ?? "day");
74
+ const spec = computed(
75
+ () => metricSpec({
76
+ type: props.type,
77
+ x: props.x,
78
+ y: props.y,
79
+ series: props.series,
80
+ aggregate: props.aggregate,
81
+ interval: interval.value,
82
+ compare: props.compare,
83
+ // With no ranges and no explicit window, the metric covers all of history:
84
+ // an empty range would filter every row out, which reads as "no data" when
85
+ // the truth is "no window was asked for".
86
+ range: window.value ?? { from: "0000-01-01", to: "9999-12-31" },
87
+ label: props.title
88
+ })
89
+ );
90
+ const result = computed(() => transform(props.data ?? [], spec.value));
91
+ const formatValue = computed(() => {
92
+ if (props.valueFormatter) return props.valueFormatter;
93
+ return (value) => formatMetricValue(value, {
94
+ format: props.format,
95
+ currency: props.currency,
96
+ locale: props.locale,
97
+ options: props.formatOptions
98
+ });
99
+ });
100
+ const headline = computed(() => {
101
+ const { total, data, series } = result.value;
102
+ const aggregate = props.aggregate;
103
+ if (aggregate === "sum" || aggregate === "count") return total;
104
+ const primary = series.find((key) => key !== "compare");
105
+ if (!primary) return total;
106
+ const values = data.map((datum) => datum[primary]).filter((value) => typeof value === "number");
107
+ if (!values.length) return 0;
108
+ if (aggregate === "min") return Math.min(...values);
109
+ if (aggregate === "max") return Math.max(...values);
110
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
111
+ });
112
+ const headlineText = computed(() => formatValue.value(headline.value));
113
+ const delta = computed(() => {
114
+ const change = result.value.change;
115
+ const text = formatChange(change, props.locale);
116
+ if (text === void 0) return void 0;
117
+ return { text, ...metricDelta(change, props.inverseSentiment) };
118
+ });
119
+ const compareLabel = computed(
120
+ () => props.compare === "previous-year" ? "vs previous year" : "vs previous period"
121
+ );
122
+ const absoluteChange = computed(() => {
123
+ const { total, compareTotal } = result.value;
124
+ if (compareTotal === void 0) return void 0;
125
+ const difference = total - compareTotal;
126
+ const sign = difference > 0 ? "+" : difference < 0 ? "\u2212" : "";
127
+ return `${sign}${formatValue.value(Math.abs(difference))}`;
128
+ });
129
+ const isEmpty = computed(
130
+ () => !props.loading && !props.error && result.value.data.length === 0
131
+ );
132
+ const showingTable = ref(false);
133
+ const tableColumns = computed(
134
+ () => result.value.series.map((key) => ({ key, label: seriesLabel(key, spec.value) }))
135
+ );
136
+ const tableRows = computed(
137
+ () => result.value.data.map((datum) => ({
138
+ x: formatBucket(datum.x, interval.value, props.locale),
139
+ values: result.value.series.map((key) => {
140
+ const value = datum[key];
141
+ return typeof value === "number" ? formatValue.value(value) : "\u2014";
142
+ })
143
+ }))
144
+ );
145
+ function slugify(value) {
146
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "metric";
147
+ }
148
+ function exportCsv() {
149
+ const headers = Object.fromEntries(
150
+ tableColumns.value.map((column) => [column.key, column.label])
151
+ );
152
+ const csv = toCsv(result.value.data, result.value.series, headers, props.x);
153
+ emit("export", csv);
154
+ if (typeof document === "undefined") return;
155
+ const blob = new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" });
156
+ const url = URL.createObjectURL(blob);
157
+ const link = document.createElement("a");
158
+ link.href = url;
159
+ link.download = props.exportFilename ?? `${slugify(props.title ?? "metric")}.csv`;
160
+ link.click();
161
+ setTimeout(() => URL.revokeObjectURL(url), 0);
162
+ }
163
+ const titleId = useId();
164
+ const valueId = useId();
165
+ const valueLabel = computed(() => {
166
+ const parts = [props.title, headlineText.value].filter(Boolean);
167
+ if (delta.value) {
168
+ const direction = delta.value.direction === "up" ? "up" : delta.value.direction === "down" ? "down" : "unchanged";
169
+ parts.push(`${direction} ${delta.value.text} ${compareLabel.value}`);
170
+ }
171
+ return parts.join(", ");
172
+ });
173
+ defineExpose({ result, spec, exportCsv });
174
+ defineSlots();
175
+ </script>
176
+
177
+ <template>
178
+ <section
179
+ class="vc-metric"
180
+ :aria-labelledby="title ? titleId : void 0"
181
+ :aria-busy="loading ? 'true' : void 0"
182
+ >
183
+ <header class="vc-metric__header">
184
+ <slot name="header">
185
+ <div class="vc-metric__heading">
186
+ <h3 v-if="title" :id="titleId" class="vc-metric__title">{{ title }}</h3>
187
+ <p v-if="description" class="vc-metric__description">{{ description }}</p>
188
+ </div>
189
+
190
+ <slot
191
+ name="value"
192
+ :value="headline"
193
+ :formatted="headlineText"
194
+ :change="result.change"
195
+ >
196
+ <div v-if="!hideValue" class="vc-metric__value-row">
197
+ <!--
198
+ The skeleton stands in for the number rather than showing a stale
199
+ or zero total while data is in flight: a headline that renders
200
+ "€0" and then jumps is worse than one that visibly waits.
201
+ -->
202
+ <div v-if="loading" class="vc-metric__value-skeleton" aria-hidden="true" />
203
+ <template v-else>
204
+ <p :id="valueId" class="vc-metric__value" :aria-label="valueLabel">
205
+ {{ headlineText }}
206
+ </p>
207
+ <p
208
+ v-if="delta"
209
+ class="vc-metric__delta"
210
+ :data-sentiment="delta.sentiment"
211
+ aria-hidden="true"
212
+ >
213
+ <span class="vc-metric__delta-arrow">{{
214
+ delta.direction === "up" ? "\u2191" : delta.direction === "down" ? "\u2193" : "\u2192"
215
+ }}</span>
216
+ <span>{{ delta.text }}</span>
217
+ <span v-if="absoluteChange" class="vc-metric__delta-absolute">({{ absoluteChange }})</span>
218
+ <span class="vc-metric__delta-label">{{ compareLabel }}</span>
219
+ </p>
220
+ </template>
221
+ </div>
222
+ </slot>
223
+ </slot>
224
+ </header>
225
+
226
+ <!--
227
+ The table replaces the chart rather than sitting beside it, so the two
228
+ never disagree about how much vertical space the card takes — a card that
229
+ grows on toggle shoves everything below it down the page.
230
+ -->
231
+ <div v-if="showingTable" class="vc-metric__table-wrap" :style="{ minHeight: `${height}px` }">
232
+ <table class="vc-metric__table">
233
+ <caption class="vc-sr-only">{{ title ?? "Metric data" }}</caption>
234
+ <thead>
235
+ <tr>
236
+ <th scope="col">{{ x }}</th>
237
+ <th v-for="column in tableColumns" :key="column.key" scope="col">{{ column.label }}</th>
238
+ </tr>
239
+ </thead>
240
+ <tbody>
241
+ <tr v-for="(row, index) in tableRows" :key="index">
242
+ <th scope="row">{{ row.x }}</th>
243
+ <td v-for="(value, column) in row.values" :key="column">{{ value }}</td>
244
+ </tr>
245
+ </tbody>
246
+ </table>
247
+ </div>
248
+
249
+ <DataChart
250
+ v-else
251
+ :data="data"
252
+ :spec="spec"
253
+ :height="height"
254
+ :locale="locale"
255
+ :stacked="stacked"
256
+ :hide-legend="hideLegend ?? (!compare && !series)"
257
+ :theme="theme"
258
+ :value-formatter="formatValue"
259
+ :loading="loading"
260
+ :loading-label="loadingLabel"
261
+ :error="error"
262
+ :empty-label="emptyLabel"
263
+ :aria-label="ariaLabel ?? title"
264
+ :aria-description="ariaDescription ?? (delta ? valueLabel : void 0)"
265
+ :accessible-data-table="accessibleDataTable"
266
+ :max-data-points="maxDataPoints"
267
+ :legend-variant="legendVariant"
268
+ :tooltip-variant="tooltipVariant"
269
+ :tooltip-roundness="tooltipRoundness"
270
+ />
271
+
272
+ <footer v-if="ranges.length || showTable || exportable || $slots.actions" class="vc-metric__footer">
273
+ <!--
274
+ A radiogroup rather than a row of buttons: the ranges are one mutually
275
+ exclusive choice, and that is what lets a screen reader announce
276
+ "30D, selected, 2 of 4" instead of four unrelated buttons.
277
+ -->
278
+ <div
279
+ v-if="ranges.length && !window"
280
+ class="vc-metric__ranges"
281
+ role="radiogroup"
282
+ aria-label="Time range"
283
+ >
284
+ <button
285
+ v-for="range in ranges"
286
+ :key="range.key"
287
+ type="button"
288
+ role="radio"
289
+ class="vc-metric__range"
290
+ :aria-checked="range.key === activeRange?.key"
291
+ :aria-label="range.title ?? range.label"
292
+ :data-active="range.key === activeRange?.key ? 'true' : void 0"
293
+ @click="rangeModel = range.key"
294
+ >
295
+ {{ range.label }}
296
+ </button>
297
+ </div>
298
+
299
+ <div class="vc-metric__actions">
300
+ <slot name="actions" />
301
+ <button
302
+ v-if="showTable"
303
+ type="button"
304
+ class="vc-metric__action"
305
+ :aria-pressed="showingTable"
306
+ @click="showingTable = !showingTable"
307
+ >
308
+ {{ showingTable ? "Chart" : "Table" }}
309
+ </button>
310
+ <button
311
+ v-if="exportable"
312
+ type="button"
313
+ class="vc-metric__action"
314
+ :disabled="isEmpty"
315
+ @click="exportCsv"
316
+ >
317
+ Export
318
+ </button>
319
+ </div>
320
+ </footer>
321
+ </section>
322
+ </template>
@@ -0,0 +1,54 @@
1
+ import type { MetricChartProps } from "../types/charts.js";
2
+ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<(MetricChartProps<T> & {
4
+ /**
5
+ * Selected range key.
6
+ *
7
+ * `v-model:range` is optional: without it the component keeps its own
8
+ * selection, so a metric card works as a drop-in without the host having to
9
+ * own state it does not care about. With it, several cards can share one
10
+ * selection — which is what the eventual `MetricGrid` will use.
11
+ */
12
+ range?: string | undefined;
13
+ }) & {
14
+ "onUpdate:window"?: ((window: {
15
+ from: string;
16
+ to: string;
17
+ }) => any) | undefined;
18
+ onExport?: ((csv: string) => any) | undefined;
19
+ "onUpdate:range"?: ((value: string | undefined) => any) | undefined;
20
+ }> & (typeof globalThis extends {
21
+ __VLS_PROPS_FALLBACK: infer P;
22
+ } ? P : {});
23
+ expose: (exposed: import("vue").ShallowUnwrapRef<{
24
+ result: import("vue").ComputedRef<import("../spec/transform.js").TransformResult>;
25
+ spec: import("vue").ComputedRef<import("../spec/types.js").ChartSpec>;
26
+ exportCsv: () => void;
27
+ }>) => void;
28
+ attrs: any;
29
+ slots: {
30
+ /** Replaces the whole header (title, description, value, delta). */
31
+ header?: () => unknown;
32
+ /** Replaces the headline value and its delta only. */
33
+ value?: (props: {
34
+ value: number;
35
+ formatted: string;
36
+ change: number | undefined;
37
+ }) => unknown;
38
+ /** Extra controls in the footer, beside the table and export buttons. */
39
+ actions?: () => unknown;
40
+ };
41
+ emit: (((evt: "update:window", window: {
42
+ from: string;
43
+ to: string;
44
+ }) => void) & ((evt: "export", csv: string) => void)) & ((event: "update:range", value: string | undefined) => void);
45
+ }>) => import("vue").VNode & {
46
+ __ctx?: Awaited<typeof __VLS_setup>;
47
+ };
48
+ declare const _default: typeof __VLS_export;
49
+ export default _default;
50
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
51
+ [K in keyof T]: T[K];
52
+ } : {
53
+ [K in keyof T as K]: T[K];
54
+ }) & {};