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,2 @@
1
+ export { getMap, getPin } from "./utils/dottedMap.js";
2
+ export type * from "./types/maps";
@@ -0,0 +1 @@
1
+ export { getMap, getPin } from "./utils/dottedMap.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CSV serialization for aggregated chart data.
3
+ *
4
+ * Kept in the data layer rather than the component so a server route can emit
5
+ * the same file the download button produces.
6
+ */
7
+ import type { SpecDatum } from "./types.js";
8
+ /**
9
+ * Render aggregated data as a CSV document.
10
+ *
11
+ * `headers` maps each series key to its display label, so the export carries
12
+ * the same column names the legend shows rather than internal keys.
13
+ */
14
+ export declare function toCsv(data: readonly SpecDatum[], series: readonly string[], headers?: Record<string, string>, xHeader?: string): string;
@@ -0,0 +1,13 @@
1
+ function escapeField(value) {
2
+ if (value === null || value === void 0) return "";
3
+ let text = String(value);
4
+ if (/^[=+\-@]/.test(text)) text = `'${text}`;
5
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
6
+ }
7
+ export function toCsv(data, series, headers = {}, xHeader = "x") {
8
+ const columns = [xHeader, ...series.map((key) => headers[key] ?? key)];
9
+ const rows = data.map(
10
+ (datum) => [datum.x, ...series.map((key) => datum[key])].map(escapeField).join(",")
11
+ );
12
+ return [columns.map(escapeField).join(","), ...rows].join("\n");
13
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The data layer: chart specifications and the transform that runs them.
3
+ *
4
+ * Importable on its own (`nuxt-charts/spec`) so the same aggregation can run in
5
+ * a server route, a test, or a build step without pulling in Vue components.
6
+ */
7
+ export * from "./types.js";
8
+ export * from "./transform.js";
9
+ export * from "./resolve.js";
10
+ export * from "./csv.js";
11
+ export * from "./metric.js";
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./transform.js";
3
+ export * from "./resolve.js";
4
+ export * from "./csv.js";
5
+ export * from "./metric.js";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The metric layer: the small amount of logic that sits between "a business
3
+ * question" and a {@link ChartSpec}.
4
+ *
5
+ * Everything here is a pure function, for the same reason the transform stage
6
+ * is: a metric's value formatting and its time window are things a server
7
+ * route, a test, or a summary email may need without mounting a component.
8
+ *
9
+ * The division of labour is deliberate. This module decides *what window to
10
+ * ask for* and *how to render a number*; the transform decides *what the
11
+ * numbers are*. `<MetricChart>` composes the two and draws the chrome.
12
+ */
13
+ import type { ChartSpec, SpecAggregate, SpecChartType, SpecInterval } from "./types.js";
14
+ /**
15
+ * How a metric's values are rendered as text.
16
+ *
17
+ * A closed union rather than a formatter function, so a metric definition
18
+ * stays serializable alongside the spec it produces. A caller needing
19
+ * something else passes their own function through `valueFormatter`.
20
+ */
21
+ export type MetricFormat = "number" | "currency" | "percent" | "compact" | "duration";
22
+ /**
23
+ * A selectable time window.
24
+ *
25
+ * `days` drives the range arithmetic; `interval` is the bucket width that
26
+ * keeps the chart readable at that span — 7 days of monthly buckets would be
27
+ * a single point, and a year of daily buckets is 365 marks of noise.
28
+ */
29
+ export interface MetricRange {
30
+ /** Stable key, used as the selector's value and `v-model`. */
31
+ key: string;
32
+ /** Short label for the selector button (`30D`). */
33
+ label: string;
34
+ /** Accessible name, since `30D` alone does not read well aloud. */
35
+ title?: string;
36
+ /** Window length in days, counting back from the anchor date. */
37
+ days: number;
38
+ /** Bucket width for this window. */
39
+ interval: SpecInterval;
40
+ }
41
+ /**
42
+ * The default range set: a week, a month, a quarter, a year.
43
+ *
44
+ * These four cover the overwhelming majority of dashboard use, and their
45
+ * intervals are chosen so every window lands between roughly 7 and 90 marks —
46
+ * enough shape to read a trend, few enough to stay legible.
47
+ */
48
+ export declare const DEFAULT_RANGES: readonly MetricRange[];
49
+ /**
50
+ * The `{ from, to }` window for a range, counting back from an anchor date.
51
+ *
52
+ * Anchored in UTC to match the transform's bucketing: a window computed in
53
+ * local time can start a day either side of the buckets it is meant to select,
54
+ * which silently drops or doubles an edge bucket.
55
+ *
56
+ * The window is inclusive at both ends, so a 7-day range covers today plus the
57
+ * six days before it — what "last 7 days" means to a reader, rather than the
58
+ * eight days a naive `to - 7` would select.
59
+ */
60
+ export declare function resolveRange(range: Pick<MetricRange, "days">, anchor?: string | number | Date): {
61
+ from: string;
62
+ to: string;
63
+ };
64
+ /** Inputs a metric needs to describe itself as a spec. */
65
+ export interface MetricSpecInput {
66
+ type: SpecChartType;
67
+ x: string;
68
+ y?: string;
69
+ series?: string;
70
+ aggregate?: SpecAggregate;
71
+ interval: SpecInterval;
72
+ compare?: ChartSpec["compare"];
73
+ range: {
74
+ from: string;
75
+ to: string;
76
+ };
77
+ label?: string;
78
+ }
79
+ /**
80
+ * Assemble a {@link ChartSpec} from a metric's props.
81
+ *
82
+ * Trivial by design: the value of `<MetricChart>` is that it *produces* a
83
+ * spec rather than reimplementing aggregation, and keeping that assembly in
84
+ * one named function makes the boundary visible — and testable without Vue.
85
+ */
86
+ export declare function metricSpec(input: MetricSpecInput): ChartSpec;
87
+ /** Options for {@link formatMetricValue}. */
88
+ export interface MetricFormatOptions {
89
+ format?: MetricFormat;
90
+ /** ISO 4217 code, used only by the `currency` format. Default `EUR`. */
91
+ currency?: string;
92
+ locale?: string;
93
+ /** Overrides the format's own defaults (`maximumFractionDigits`, …). */
94
+ options?: Intl.NumberFormatOptions;
95
+ }
96
+ /**
97
+ * Render a metric value as display text.
98
+ *
99
+ * Each format carries the fraction-digit defaults that suit it, because the
100
+ * right precision is a property of what the number *means*: money shows whole
101
+ * units on a headline, a rate needs a decimal to show movement at all, and a
102
+ * compact number's whole point is brevity.
103
+ *
104
+ * `percent` takes a value already in percent units (`4.8` -> `4.8%`), not the
105
+ * `0.048` fraction `Intl`'s percent style expects. Metric fields hold rates as
106
+ * humans record them, and silently multiplying by 100 surprises every caller
107
+ * whose column is already a percentage.
108
+ */
109
+ export declare function formatMetricValue(value: number, { format, currency, locale, options }?: MetricFormatOptions): string;
110
+ /**
111
+ * Render a fractional change (`0.124`) as a signed percentage (`+12.4%`).
112
+ *
113
+ * Signed explicitly, including the `+`, because a delta's direction is the
114
+ * first thing read and an unsigned `12.4%` beside a total is ambiguous.
115
+ */
116
+ export declare function formatChange(change: number | undefined, locale?: string): string | undefined;
117
+ /**
118
+ * Which way a delta should read.
119
+ *
120
+ * `direction` is the arithmetic sign; `sentiment` is whether that sign is good
121
+ * news, which is not the same question. For latency, error rates or churn, a
122
+ * fall is the win — so the colour is driven by `sentiment` and the arrow by
123
+ * `direction`, and `inverse` flips only the former.
124
+ */
125
+ export interface MetricDelta {
126
+ direction: "up" | "down" | "flat";
127
+ sentiment: "positive" | "negative" | "neutral";
128
+ }
129
+ /** Classify a change into an arrow direction and a sentiment. */
130
+ export declare function metricDelta(change: number | undefined, inverse?: boolean): MetricDelta;
@@ -0,0 +1,63 @@
1
+ export const DEFAULT_RANGES = [
2
+ { key: "7d", label: "7D", title: "Last 7 days", days: 7, interval: "day" },
3
+ { key: "30d", label: "30D", title: "Last 30 days", days: 30, interval: "day" },
4
+ { key: "3m", label: "3M", title: "Last 3 months", days: 90, interval: "week" },
5
+ { key: "1y", label: "1Y", title: "Last 12 months", days: 365, interval: "month" }
6
+ ];
7
+ const DAY_MS = 864e5;
8
+ export function resolveRange(range, anchor) {
9
+ const now = anchor === void 0 ? Date.now() : new Date(anchor).getTime();
10
+ const end = Number.isFinite(now) ? now : Date.now();
11
+ const date = new Date(end);
12
+ const to = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
13
+ const from = to - (Math.max(1, Math.round(range.days)) - 1) * DAY_MS;
14
+ return { from: isoDate(from), to: isoDate(to) };
15
+ }
16
+ function isoDate(time) {
17
+ return new Date(time).toISOString().slice(0, 10);
18
+ }
19
+ export function metricSpec(input) {
20
+ return {
21
+ type: input.type,
22
+ dimensions: { x: input.x, y: input.y, series: input.series },
23
+ transform: { interval: input.interval, aggregate: input.aggregate ?? "sum" },
24
+ range: input.range,
25
+ // A series split and a comparison both claim the series dimension; the
26
+ // transform resolves that in favour of the split, so asking for both here
27
+ // would render a chart whose legend contradicts the header's "vs previous".
28
+ compare: input.series ? void 0 : input.compare,
29
+ label: input.label
30
+ };
31
+ }
32
+ export function formatMetricValue(value, { format = "number", currency = "EUR", locale, options } = {}) {
33
+ if (!Number.isFinite(value)) return "\u2014";
34
+ if (format === "duration") return formatDuration(value, locale, options);
35
+ const defaults = format === "currency" ? { style: "currency", currency, maximumFractionDigits: 0 } : format === "percent" ? { maximumFractionDigits: 1 } : format === "compact" ? { notation: "compact", maximumFractionDigits: 1 } : { maximumFractionDigits: 0 };
36
+ const text = new Intl.NumberFormat(locale, { ...defaults, ...options }).format(value);
37
+ return format === "percent" ? `${text}%` : text;
38
+ }
39
+ function formatDuration(value, locale, options) {
40
+ const abs = Math.abs(value);
41
+ const [scaled, unit, digits] = abs < 1e3 ? [value, "ms", 0] : abs < 6e4 ? [value / 1e3, "s", 1] : [value / 6e4, "min", 1];
42
+ const text = new Intl.NumberFormat(locale, {
43
+ maximumFractionDigits: digits,
44
+ ...options
45
+ }).format(scaled);
46
+ return `${text}${unit}`;
47
+ }
48
+ export function formatChange(change, locale) {
49
+ if (change === void 0 || !Number.isFinite(change)) return void 0;
50
+ return new Intl.NumberFormat(locale, {
51
+ style: "percent",
52
+ maximumFractionDigits: 1,
53
+ signDisplay: "exceptZero"
54
+ }).format(change);
55
+ }
56
+ export function metricDelta(change, inverse = false) {
57
+ if (change === void 0 || !Number.isFinite(change) || change === 0) {
58
+ return { direction: "flat", sentiment: "neutral" };
59
+ }
60
+ const direction = change > 0 ? "up" : "down";
61
+ const good = inverse ? change < 0 : change > 0;
62
+ return { direction, sentiment: good ? "positive" : "negative" };
63
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Spec -> renderer props.
3
+ *
4
+ * The transform stage decides *what the numbers are*; this stage decides *what
5
+ * the chart component is handed*. Separating them keeps the chart adapters
6
+ * unchanged — `DataChart` produces exactly the `categories` / `xAxis` / `yAxis`
7
+ * props a hand-written `<LineChart>` would have received.
8
+ */
9
+ import type { BulletLegendItemInterface } from "../types/shared.js";
10
+ import { type ChartSpec, type SpecInterval } from "./types.js";
11
+ /**
12
+ * Human-readable label for a series key.
13
+ *
14
+ * The synthetic keys get prose; a `series`-split value is already a real label
15
+ * (a plan name, a region) and is left exactly as the data spelled it.
16
+ */
17
+ export declare function seriesLabel(key: string, spec: ChartSpec): string;
18
+ /** `createdAt` / `created_at` / `created-at` -> `Created at`. */
19
+ export declare function titleCase(field: string): string;
20
+ /**
21
+ * Build the `categories` record the chart adapters expect.
22
+ *
23
+ * The comparison series is deliberately not given a colour: leaving it
24
+ * undefined lets `categoriesToSeries` assign the next `--vc-series-*` token,
25
+ * so a baseline picks up the host theme like any other series instead of
26
+ * hardcoding a grey that fights the design system in dark mode.
27
+ */
28
+ export declare function specCategories(series: readonly string[], spec: ChartSpec): Record<string, BulletLegendItemInterface>;
29
+ /**
30
+ * Format a bucket key (`2026-03-01`) for the x axis.
31
+ *
32
+ * Buckets are UTC-anchored, so the formatter is pinned to UTC as well —
33
+ * otherwise a viewer west of Greenwich sees every month labelled as the
34
+ * previous one.
35
+ */
36
+ export declare function formatBucket(key: string, interval: SpecInterval | undefined, locale?: string): string;
37
+ /**
38
+ * Validate a spec, returning a human-readable reason when it cannot render.
39
+ *
40
+ * Specs are expected to arrive from outside the codebase — a database row, an
41
+ * API response, eventually a model — so the failure mode has to be a message
42
+ * the caller can show, not a thrown exception mid-render.
43
+ */
44
+ export declare function validateSpec(spec: ChartSpec | undefined): string | undefined;
@@ -0,0 +1,41 @@
1
+ import { COMPARE_KEY, VALUE_KEY } from "./types.js";
2
+ export function seriesLabel(key, spec) {
3
+ if (key === COMPARE_KEY) {
4
+ return spec.compare === "previous-year" ? "Previous year" : "Previous period";
5
+ }
6
+ if (key === VALUE_KEY) {
7
+ return spec.label ?? titleCase(spec.dimensions.y ?? "Count");
8
+ }
9
+ return key;
10
+ }
11
+ export function titleCase(field) {
12
+ const spaced = field.replace(/[_-]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2").trim();
13
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
14
+ }
15
+ export function specCategories(series, spec) {
16
+ return Object.fromEntries(
17
+ series.map((key) => [key, { name: seriesLabel(key, spec) }])
18
+ );
19
+ }
20
+ export function formatBucket(key, interval, locale) {
21
+ if (!interval) return key;
22
+ const time = Date.parse(key);
23
+ if (!Number.isFinite(time)) return key;
24
+ const options = interval === "month" ? { month: "short", year: "2-digit", timeZone: "UTC" } : { month: "short", day: "numeric", timeZone: "UTC" };
25
+ return new Intl.DateTimeFormat(locale, options).format(new Date(time));
26
+ }
27
+ export function validateSpec(spec) {
28
+ if (!spec) return "No chart spec provided.";
29
+ if (!spec.dimensions?.x) return "Chart spec is missing `dimensions.x`.";
30
+ const aggregate = spec.transform?.aggregate ?? "sum";
31
+ if (aggregate !== "count" && !spec.dimensions.y) {
32
+ return `Chart spec needs \`dimensions.y\` for the \`${aggregate}\` aggregate.`;
33
+ }
34
+ if (spec.compare && !spec.range) {
35
+ return "Chart spec needs a `range` to compare against a previous period.";
36
+ }
37
+ if (spec.compare && !spec.transform?.interval) {
38
+ return "Chart spec needs `transform.interval` to align a comparison series.";
39
+ }
40
+ return void 0;
41
+ }
@@ -0,0 +1,58 @@
1
+ import { type ChartSpec, type SpecAggregate, type SpecDatum, type SpecInterval } from "./types.js";
2
+ /**
3
+ * Parse a value into a timestamp, accepting the shapes application data
4
+ * actually arrives in: `Date`, epoch milliseconds, or an ISO-ish string.
5
+ *
6
+ * Returns `undefined` rather than `NaN` so callers can treat "not a date" as
7
+ * a category instead of having to guard every arithmetic use.
8
+ */
9
+ export declare function toTimestamp(value: unknown): number | undefined;
10
+ /**
11
+ * Snap a timestamp down to the start of its bucket, in UTC.
12
+ *
13
+ * Weeks start Monday (ISO-8601), so `getUTCDay()`'s Sunday-as-0 is remapped:
14
+ * Sunday belongs to the week that began six days earlier, not the one starting
15
+ * the next day.
16
+ */
17
+ export declare function floorToInterval(time: number, interval: SpecInterval): number;
18
+ /** Step one bucket forward. Months step calendar-wise, not by a fixed 30 days. */
19
+ export declare function nextBucket(time: number, interval: SpecInterval): number;
20
+ /**
21
+ * Canonical key for a bucket: the ISO date of its first day (`2026-03-01`).
22
+ *
23
+ * A sortable, stable string rather than a formatted label, because it doubles
24
+ * as the grouping key and as the join key that aligns a comparison series with
25
+ * the current one. Display formatting happens later, at the axis.
26
+ */
27
+ export declare function bucketKey(time: number, interval: SpecInterval): string;
28
+ /** Collapse a list of numbers per the requested aggregate. */
29
+ export declare function aggregateValues(values: readonly number[], aggregate: SpecAggregate): number;
30
+ /** Step one bucket backwards. Mirrors {@link nextBucket}. */
31
+ export declare function previousBucket(time: number, interval: SpecInterval): number;
32
+ /** Result of running a spec over rows: the chart data plus what it summarises. */
33
+ export interface TransformResult {
34
+ /** Aggregated rows, one per x bucket, ready to hand to a chart component. */
35
+ data: SpecDatum[];
36
+ /** Series keys present in `data`, in render order. */
37
+ series: string[];
38
+ /** Total of the primary series over the window. */
39
+ total: number;
40
+ /** Total of the comparison series, when the spec asked for one. */
41
+ compareTotal?: number;
42
+ /**
43
+ * Fractional change from `compareTotal` to `total` (`0.124` = up 12.4%).
44
+ *
45
+ * `undefined` when there is no comparison, and when the baseline is zero —
46
+ * growth from nothing has no meaningful percentage, and reporting `Infinity`
47
+ * or a bare `100%` would both be lies.
48
+ */
49
+ change?: number;
50
+ }
51
+ /**
52
+ * Run a spec over rows and return the aggregated chart data.
53
+ *
54
+ * The single entry point for the data layer. Its output is deliberately plain:
55
+ * an array of flat records plus the series keys that index them, which is the
56
+ * exact shape the existing chart adapters already consume.
57
+ */
58
+ export declare function transform<T>(rows: readonly T[], spec: ChartSpec): TransformResult;
@@ -0,0 +1,199 @@
1
+ import { toFiniteNumber } from "../utils/data.js";
2
+ import {
3
+ COMPARE_KEY,
4
+ VALUE_KEY
5
+ } from "./types.js";
6
+ const DAY_MS = 864e5;
7
+ export function toTimestamp(value) {
8
+ if (value instanceof Date) {
9
+ const time2 = value.getTime();
10
+ return Number.isFinite(time2) ? time2 : void 0;
11
+ }
12
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
13
+ if (typeof value !== "string" || value === "") return void 0;
14
+ const time = Date.parse(value);
15
+ return Number.isFinite(time) ? time : void 0;
16
+ }
17
+ export function floorToInterval(time, interval) {
18
+ const date = new Date(time);
19
+ if (interval === "month") {
20
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
21
+ }
22
+ const midnight = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
23
+ if (interval === "day") return midnight;
24
+ const dayOfWeek = (new Date(midnight).getUTCDay() + 6) % 7;
25
+ return midnight - dayOfWeek * DAY_MS;
26
+ }
27
+ export function nextBucket(time, interval) {
28
+ if (interval === "month") {
29
+ const date = new Date(time);
30
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
31
+ }
32
+ return time + (interval === "week" ? 7 * DAY_MS : DAY_MS);
33
+ }
34
+ export function bucketKey(time, interval) {
35
+ return new Date(floorToInterval(time, interval)).toISOString().slice(0, 10);
36
+ }
37
+ export function aggregateValues(values, aggregate) {
38
+ if (aggregate === "count") return values.length;
39
+ if (values.length === 0) return 0;
40
+ switch (aggregate) {
41
+ case "average":
42
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
43
+ case "min":
44
+ return Math.min(...values);
45
+ case "max":
46
+ return Math.max(...values);
47
+ default:
48
+ return values.reduce((sum, value) => sum + value, 0);
49
+ }
50
+ }
51
+ function fieldOf(row, field) {
52
+ return row?.[field];
53
+ }
54
+ function aggregateRows(rows, xField, yField, aggregate, interval) {
55
+ const groups = /* @__PURE__ */ new Map();
56
+ for (const row of rows) {
57
+ const rawX = fieldOf(row, xField);
58
+ if (rawX === null || rawX === void 0 || rawX === "") continue;
59
+ let key;
60
+ if (interval) {
61
+ const time = toTimestamp(rawX);
62
+ if (time === void 0) continue;
63
+ key = bucketKey(time, interval);
64
+ } else {
65
+ key = String(rawX);
66
+ }
67
+ const value = yField === void 0 ? void 0 : toFiniteNumber(fieldOf(row, yField));
68
+ if (aggregate !== "count" && value === void 0) continue;
69
+ const bucket = groups.get(key) ?? [];
70
+ bucket.push(value ?? 0);
71
+ groups.set(key, bucket);
72
+ }
73
+ return new Map(
74
+ [...groups].map(([key, values]) => [key, aggregateValues(values, aggregate)])
75
+ );
76
+ }
77
+ const MAX_FILLED_BUCKETS = 5e3;
78
+ function bucketRange(from, to, interval) {
79
+ const keys = [];
80
+ let cursor = floorToInterval(from, interval);
81
+ while (cursor <= to && keys.length < MAX_FILLED_BUCKETS) {
82
+ keys.push(bucketKey(cursor, interval));
83
+ cursor = nextBucket(cursor, interval);
84
+ }
85
+ return keys;
86
+ }
87
+ function compareBucketKeys(currentKeys, interval, compare) {
88
+ if (compare === "previous-year") {
89
+ return currentKeys.map((key) => {
90
+ const date = new Date(Date.parse(key));
91
+ return bucketKey(
92
+ Date.UTC(date.getUTCFullYear() - 1, date.getUTCMonth(), date.getUTCDate()),
93
+ interval
94
+ );
95
+ });
96
+ }
97
+ let cursor = Date.parse(currentKeys[0] ?? "");
98
+ if (!Number.isFinite(cursor)) return [];
99
+ const back = [];
100
+ for (let i = 0; i < currentKeys.length; i += 1) {
101
+ cursor = previousBucket(cursor, interval);
102
+ back.unshift(cursor);
103
+ }
104
+ return back.map((time) => bucketKey(time, interval));
105
+ }
106
+ export function previousBucket(time, interval) {
107
+ if (interval === "month") {
108
+ const date = new Date(time);
109
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() - 1, 1);
110
+ }
111
+ return time - (interval === "week" ? 7 * DAY_MS : DAY_MS);
112
+ }
113
+ function resolveRange(spec) {
114
+ if (!spec.range) return void 0;
115
+ const from = toTimestamp(spec.range.from);
116
+ const to = toTimestamp(spec.range.to);
117
+ if (from === void 0 || to === void 0 || from > to) return void 0;
118
+ return [from, to];
119
+ }
120
+ function rowsInWindow(rows, xField, [from, to]) {
121
+ return rows.filter((row) => {
122
+ const time = toTimestamp(fieldOf(row, xField));
123
+ return time !== void 0 && time >= from && time <= to;
124
+ });
125
+ }
126
+ export function transform(rows, spec) {
127
+ const { x: xField, y: yField, series: seriesField } = spec.dimensions;
128
+ const aggregate = spec.transform?.aggregate ?? "sum";
129
+ const interval = spec.transform?.interval;
130
+ const fillGaps = spec.transform?.fillGaps ?? true;
131
+ const range = resolveRange(spec);
132
+ const compare = seriesField ? void 0 : spec.compare;
133
+ const windowed = range ? rowsInWindow(rows, xField, range) : [...rows];
134
+ const partitions = /* @__PURE__ */ new Map();
135
+ if (seriesField) {
136
+ for (const row of windowed) {
137
+ const raw = fieldOf(row, seriesField);
138
+ const key = raw === null || raw === void 0 || raw === "" ? "unknown" : String(raw);
139
+ const bucket = partitions.get(key) ?? [];
140
+ bucket.push(row);
141
+ partitions.set(key, bucket);
142
+ }
143
+ } else {
144
+ partitions.set(VALUE_KEY, windowed);
145
+ }
146
+ const aggregated = /* @__PURE__ */ new Map();
147
+ for (const [key, partitionRows] of partitions) {
148
+ aggregated.set(key, aggregateRows(partitionRows, xField, yField, aggregate, interval));
149
+ }
150
+ let compareByKey;
151
+ if (compare && range && interval) {
152
+ const currentKeys = bucketRange(range[0], range[1], interval);
153
+ const shiftedKeys = compareBucketKeys(currentKeys, interval, compare);
154
+ const span = shiftedKeys.length ? [Date.parse(shiftedKeys[0]), nextBucket(Date.parse(shiftedKeys.at(-1)), interval) - 1] : void 0;
155
+ const compareRows = span ? rowsInWindow(rows, xField, [span[0], span[1]]) : [];
156
+ const raw = aggregateRows(compareRows, xField, yField, aggregate, interval);
157
+ compareByKey = /* @__PURE__ */ new Map();
158
+ currentKeys.forEach((key, index) => {
159
+ const source = shiftedKeys[index];
160
+ if (source !== void 0) compareByKey.set(key, raw.get(source) ?? 0);
161
+ });
162
+ }
163
+ const keys = /* @__PURE__ */ new Set();
164
+ if (interval && fillGaps && range) {
165
+ for (const key of bucketRange(range[0], range[1], interval)) keys.add(key);
166
+ }
167
+ for (const buckets of aggregated.values()) {
168
+ for (const key of buckets.keys()) keys.add(key);
169
+ }
170
+ const seriesKeys = [...aggregated.keys()];
171
+ let ordered = [...keys];
172
+ const sort = spec.transform?.sort ?? (interval ? "asc" : "none");
173
+ if (sort !== "none") {
174
+ const totalFor = (key) => seriesKeys.reduce((sum, series) => sum + (aggregated.get(series)?.get(key) ?? 0), 0);
175
+ ordered = interval ? ordered.sort((a, b) => sort === "asc" ? a.localeCompare(b) : b.localeCompare(a)) : ordered.sort((a, b) => sort === "asc" ? totalFor(a) - totalFor(b) : totalFor(b) - totalFor(a));
176
+ }
177
+ if (spec.transform?.limit !== void 0 && spec.transform.limit > 0) {
178
+ ordered = ordered.slice(0, Math.floor(spec.transform.limit));
179
+ }
180
+ const data = ordered.map((key) => {
181
+ const datum = { x: key };
182
+ for (const series of seriesKeys) {
183
+ const value = aggregated.get(series)?.get(key);
184
+ datum[series] = value ?? (interval && fillGaps ? 0 : void 0);
185
+ }
186
+ if (compareByKey) datum[COMPARE_KEY] = compareByKey.get(key) ?? 0;
187
+ return datum;
188
+ });
189
+ const sumOf = (series) => data.reduce((sum, datum) => sum + (toFiniteNumber(datum[series]) ?? 0), 0);
190
+ const total = seriesKeys.reduce((sum, series) => sum + sumOf(series), 0);
191
+ const compareTotal = compareByKey ? sumOf(COMPARE_KEY) : void 0;
192
+ return {
193
+ data,
194
+ series: compareByKey ? [...seriesKeys, COMPARE_KEY] : seriesKeys,
195
+ total,
196
+ compareTotal,
197
+ change: compareTotal === void 0 || compareTotal === 0 ? void 0 : (total - compareTotal) / compareTotal
198
+ };
199
+ }