@zuilib/charts 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/dist/authoring.d.ts +47 -0
- package/dist/authoring.js +11 -0
- package/dist/chart-defaults.d.ts +60 -0
- package/dist/chart-defaults.js +24 -0
- package/dist/chart-frame-B9fxUxGG.d.ts +90 -0
- package/dist/chart-frame.d.ts +4 -0
- package/dist/chart-frame.js +14 -0
- package/dist/chart-legend.d.ts +25 -0
- package/dist/chart-legend.js +13 -0
- package/dist/chart-spec-CH0tAkLG.d.ts +226 -0
- package/dist/chart-tooltip.d.ts +44 -0
- package/dist/chart-tooltip.js +11 -0
- package/dist/chart.d.ts +78 -0
- package/dist/chart.js +24 -0
- package/dist/chunk-3VGLTPGL.js +19 -0
- package/dist/chunk-6QTIAMA3.js +169 -0
- package/dist/chunk-7WABBHMT.js +88 -0
- package/dist/chunk-7WJ26NHR.js +48 -0
- package/dist/chunk-BFTWWI46.js +148 -0
- package/dist/chunk-D7K23UFP.js +43 -0
- package/dist/chunk-G4FXJJ66.js +169 -0
- package/dist/chunk-HNJKYOPE.js +45 -0
- package/dist/chunk-IPOBQIYU.js +13 -0
- package/dist/chunk-MJDIQVJX.js +148 -0
- package/dist/chunk-PV27RZSV.js +61 -0
- package/dist/chunk-SHQZRND7.js +83 -0
- package/dist/chunk-UN3XPBZ5.js +17 -0
- package/dist/chunk-WVWGHGZE.js +90 -0
- package/dist/chunk-YMPZODMC.js +1084 -0
- package/dist/cross-filter.d.ts +62 -0
- package/dist/cross-filter.js +13 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +7 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +93 -0
- package/dist/interop.d.ts +46 -0
- package/dist/interop.js +12 -0
- package/dist/registry.d.ts +111 -0
- package/dist/registry.js +25 -0
- package/package.json +98 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { F as FormatToken } from './chart-spec-CH0tAkLG.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Themed tooltip panel for charts. Recharts' `Tooltip` wires it into the
|
|
6
|
+
* chart; pass `content={<ChartTooltipPanel />}` to render it on ZUI tokens:
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Tooltip } from 'recharts'
|
|
10
|
+
*
|
|
11
|
+
* <Tooltip cursor={tooltipCursorDefaults} content={<ChartTooltipPanel />} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface TooltipEntry {
|
|
16
|
+
dataKey?: string | number;
|
|
17
|
+
name?: string | number;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
color?: string;
|
|
20
|
+
payload?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface ChartTooltipPanelProps {
|
|
23
|
+
/** Injected by Recharts. */
|
|
24
|
+
active?: boolean;
|
|
25
|
+
/** Injected by Recharts. */
|
|
26
|
+
payload?: TooltipEntry[];
|
|
27
|
+
/** Injected by Recharts: the label row's value (the x value). */
|
|
28
|
+
label?: string | number;
|
|
29
|
+
/** Format of the label row (the x value). @default 'none' */
|
|
30
|
+
labelFormat?: FormatToken;
|
|
31
|
+
/** Format for rows whose series declares none. @default 'number' */
|
|
32
|
+
valueFormat?: FormatToken;
|
|
33
|
+
/** Renders the label row. @default true */
|
|
34
|
+
showLabel?: boolean;
|
|
35
|
+
/** Extra classes on the panel. */
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tooltip panel on popover tokens: a color chip per row, values through the
|
|
40
|
+
* chart's formatters.
|
|
41
|
+
*/
|
|
42
|
+
declare function ChartTooltipPanel({ active, payload, label, labelFormat, valueFormat, showLabel, className, }: ChartTooltipPanelProps): ReactNode;
|
|
43
|
+
|
|
44
|
+
export { ChartTooltipPanel, type ChartTooltipPanelProps, ChartTooltipPanel as default };
|
package/dist/chart.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { c as ChartLabels, C as ChartConfig } from './chart-frame-B9fxUxGG.js';
|
|
3
|
+
import { FormatterFn, FormatterFactory } from './format.js';
|
|
4
|
+
import { e as ChartSpec, d as ChartRow, c as ChartMarkEvent } from './chart-spec-CH0tAkLG.js';
|
|
5
|
+
import { ChartRegistry, CustomChartSpec, AnyChartSpec } from './registry.js';
|
|
6
|
+
import './authoring.js';
|
|
7
|
+
import './cross-filter.js';
|
|
8
|
+
import 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The config-driven renderer: one component that turns a serializable
|
|
12
|
+
* `ChartSpec` plus rows into a themed, interactive chart. Inside a
|
|
13
|
+
* `CrossFilterProvider`, mark clicks cross-filter; without one they still
|
|
14
|
+
* report through `onMarkClick`. Dispatch runs through a `ChartRegistry`
|
|
15
|
+
* (the built-ins by default), so apps register their own chart types
|
|
16
|
+
* instead of forking the component.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface ChartProps {
|
|
20
|
+
/** The serializable chart description. */
|
|
21
|
+
spec: ChartSpec;
|
|
22
|
+
/** The rows behind the chart. */
|
|
23
|
+
rows: ChartRow[];
|
|
24
|
+
/**
|
|
25
|
+
* The chart types the spec may name. Defaults to the built-ins; pass
|
|
26
|
+
* `createChartRegistry(defaultChartTypes, yourDefs)` to add or override
|
|
27
|
+
* types without forking the component.
|
|
28
|
+
*/
|
|
29
|
+
registry?: ChartRegistry;
|
|
30
|
+
/** Custom formatter tokens the spec may name (functions stay out of the spec). */
|
|
31
|
+
formats?: Record<string, FormatterFn | FormatterFactory>;
|
|
32
|
+
/** BCP 47 tag for formatted values. @default the user's locale */
|
|
33
|
+
locale?: string;
|
|
34
|
+
/** Merged onto the container. */
|
|
35
|
+
className?: string;
|
|
36
|
+
/** Fixed pixel size (tests, SSR); responsive otherwise. */
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
/** Responsive width/height ratio when `height` is left out. @default 16/9 */
|
|
40
|
+
aspect?: number;
|
|
41
|
+
/** Renders a skeleton instead of the chart. @default false */
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
/** Renders an error state instead of the chart. */
|
|
44
|
+
error?: ReactNode;
|
|
45
|
+
/** Renders when `rows` is empty. @default the `noData` label */
|
|
46
|
+
emptyState?: ReactNode;
|
|
47
|
+
/** Accessible name of the figure. @default the spec's title */
|
|
48
|
+
ariaLabel?: string;
|
|
49
|
+
/** Series fields hidden by the legend (controlled; kept internally when left out). */
|
|
50
|
+
hiddenSeries?: string[];
|
|
51
|
+
/** Series fields hidden initially when uncontrolled. @default [] */
|
|
52
|
+
defaultHiddenSeries?: string[];
|
|
53
|
+
/** Reports the next hidden set on a legend interaction. */
|
|
54
|
+
onHiddenSeriesChange?: (fields: string[]) => void;
|
|
55
|
+
/** Every mark click, whether or not a `CrossFilterProvider` is present. */
|
|
56
|
+
onMarkClick?: (event: ChartMarkEvent) => void;
|
|
57
|
+
/** String overrides for another language. */
|
|
58
|
+
labels?: Partial<ChartLabels>;
|
|
59
|
+
/** Mark entrance animation. @default true, unless the user prefers reduced motion */
|
|
60
|
+
animate?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `<Chart>` with an app-extended registry: `spec` may be any registered
|
|
64
|
+
* `type`. The custom-spec escape hatch only opens together with an
|
|
65
|
+
* explicit `registry`, so built-in specs keep full type checking.
|
|
66
|
+
*/
|
|
67
|
+
interface CustomChartProps extends Omit<ChartProps, 'spec' | 'registry'> {
|
|
68
|
+
spec: CustomChartSpec;
|
|
69
|
+
registry: ChartRegistry;
|
|
70
|
+
}
|
|
71
|
+
/** The series config (labels, palette slots, formats) a spec implies. */
|
|
72
|
+
declare function configFromSpec(spec: AnyChartSpec, registry?: ChartRegistry): ChartConfig;
|
|
73
|
+
/** The field mark clicks and the category legend cross-filter on. */
|
|
74
|
+
declare function crossFilterFieldOf(spec: AnyChartSpec, registry?: ChartRegistry): string;
|
|
75
|
+
declare function Chart(props: ChartProps): ReactNode;
|
|
76
|
+
declare function Chart(props: CustomChartProps): ReactNode;
|
|
77
|
+
|
|
78
|
+
export { Chart, type ChartProps, type CustomChartProps, configFromSpec, crossFilterFieldOf, Chart as default };
|
package/dist/chart.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
Chart,
|
|
4
|
+
chart_default,
|
|
5
|
+
configFromSpec,
|
|
6
|
+
crossFilterFieldOf
|
|
7
|
+
} from "./chunk-BFTWWI46.js";
|
|
8
|
+
import "./chunk-YMPZODMC.js";
|
|
9
|
+
import "./chunk-HNJKYOPE.js";
|
|
10
|
+
import "./chunk-G4FXJJ66.js";
|
|
11
|
+
import "./chunk-7WABBHMT.js";
|
|
12
|
+
import "./chunk-3VGLTPGL.js";
|
|
13
|
+
import "./chunk-PV27RZSV.js";
|
|
14
|
+
import "./chunk-SHQZRND7.js";
|
|
15
|
+
import "./chunk-WVWGHGZE.js";
|
|
16
|
+
import "./chunk-UN3XPBZ5.js";
|
|
17
|
+
import "./chunk-IPOBQIYU.js";
|
|
18
|
+
import "./chunk-7WJ26NHR.js";
|
|
19
|
+
export {
|
|
20
|
+
Chart,
|
|
21
|
+
configFromSpec,
|
|
22
|
+
crossFilterFieldOf,
|
|
23
|
+
chart_default as default
|
|
24
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/dev.ts
|
|
2
|
+
function isDev() {
|
|
3
|
+
try {
|
|
4
|
+
return process.env.NODE_ENV !== "production";
|
|
5
|
+
} catch {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
var warned = /* @__PURE__ */ new Set();
|
|
10
|
+
function warnOnce(key, message) {
|
|
11
|
+
if (!isDev() || warned.has(key)) return;
|
|
12
|
+
warned.add(key);
|
|
13
|
+
console.warn(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
isDev,
|
|
18
|
+
warnOnce
|
|
19
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFormatters
|
|
3
|
+
} from "./chunk-7WABBHMT.js";
|
|
4
|
+
import {
|
|
5
|
+
defaultChartLabels
|
|
6
|
+
} from "./chunk-UN3XPBZ5.js";
|
|
7
|
+
import {
|
|
8
|
+
ChartContext
|
|
9
|
+
} from "./chunk-IPOBQIYU.js";
|
|
10
|
+
import {
|
|
11
|
+
CHART_COLOR_COUNT,
|
|
12
|
+
seriesColorProperty
|
|
13
|
+
} from "./chunk-7WJ26NHR.js";
|
|
14
|
+
|
|
15
|
+
// src/chart-frame.tsx
|
|
16
|
+
import {
|
|
17
|
+
Children,
|
|
18
|
+
cloneElement,
|
|
19
|
+
forwardRef,
|
|
20
|
+
isValidElement,
|
|
21
|
+
useCallback,
|
|
22
|
+
useId,
|
|
23
|
+
useMemo,
|
|
24
|
+
useState
|
|
25
|
+
} from "react";
|
|
26
|
+
import { ResponsiveContainer } from "recharts";
|
|
27
|
+
import Alert from "@zuilib/components/alert";
|
|
28
|
+
import { cn } from "@zuilib/components/lib/cn";
|
|
29
|
+
|
|
30
|
+
// src/legend-toggle.ts
|
|
31
|
+
function legendToggle(allFields, hidden, field, options) {
|
|
32
|
+
if (options?.additive) {
|
|
33
|
+
return hidden.includes(field) ? hidden.filter((f) => f !== field) : [...hidden, field];
|
|
34
|
+
}
|
|
35
|
+
const visible = allFields.filter((f) => !hidden.includes(f));
|
|
36
|
+
const isolated = visible.length === 1 && visible[0] === field;
|
|
37
|
+
return isolated ? [] : allFields.filter((f) => f !== field);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/chart-frame.tsx
|
|
41
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
42
|
+
var ChartFrame = forwardRef(
|
|
43
|
+
({
|
|
44
|
+
config,
|
|
45
|
+
children,
|
|
46
|
+
width,
|
|
47
|
+
height,
|
|
48
|
+
aspect = 16 / 9,
|
|
49
|
+
loading = false,
|
|
50
|
+
error,
|
|
51
|
+
emptyState,
|
|
52
|
+
empty = false,
|
|
53
|
+
ariaLabel,
|
|
54
|
+
labels: labelOverrides,
|
|
55
|
+
formats: customFormats,
|
|
56
|
+
locale,
|
|
57
|
+
hiddenSeries,
|
|
58
|
+
defaultHiddenSeries,
|
|
59
|
+
onHiddenSeriesChange,
|
|
60
|
+
categoryField,
|
|
61
|
+
categories,
|
|
62
|
+
className,
|
|
63
|
+
style,
|
|
64
|
+
...props
|
|
65
|
+
}, ref) => {
|
|
66
|
+
const chartId = useId().replace(/:/g, "");
|
|
67
|
+
const labels = useMemo(() => ({ ...defaultChartLabels, ...labelOverrides }), [labelOverrides]);
|
|
68
|
+
const formats = useMemo(() => createFormatters(locale, customFormats), [locale, customFormats]);
|
|
69
|
+
const [innerHidden, setInnerHidden] = useState(defaultHiddenSeries ?? []);
|
|
70
|
+
const hidden = hiddenSeries ?? innerHidden;
|
|
71
|
+
const allFields = useMemo(() => Object.keys(config), [config]);
|
|
72
|
+
const onLegendClick = useCallback(
|
|
73
|
+
(field, options) => {
|
|
74
|
+
const next = legendToggle(allFields, hiddenSeries ?? innerHidden, field, options);
|
|
75
|
+
if (hiddenSeries === void 0) setInnerHidden(next);
|
|
76
|
+
onHiddenSeriesChange?.(next);
|
|
77
|
+
},
|
|
78
|
+
[allFields, hiddenSeries, innerHidden, onHiddenSeriesChange]
|
|
79
|
+
);
|
|
80
|
+
const context = useMemo(
|
|
81
|
+
() => ({ config, formats, labels, hiddenSeries: hidden, onLegendClick, categoryField, categories }),
|
|
82
|
+
[config, formats, labels, hidden, onLegendClick, categoryField, categories]
|
|
83
|
+
);
|
|
84
|
+
const colorStyle = useMemo(() => {
|
|
85
|
+
const vars = {};
|
|
86
|
+
Object.entries(config).forEach(([field, series], index) => {
|
|
87
|
+
const slot = Math.min(index + 1, CHART_COLOR_COUNT);
|
|
88
|
+
vars[seriesColorProperty(field)] = series.color ?? (series.theme ? series.theme.light : `var(--chart-${slot})`);
|
|
89
|
+
});
|
|
90
|
+
return vars;
|
|
91
|
+
}, [config]);
|
|
92
|
+
const themed = useMemo(
|
|
93
|
+
() => Object.entries(config).filter((entry) => !!entry[1].theme && !entry[1].color),
|
|
94
|
+
[config]
|
|
95
|
+
);
|
|
96
|
+
const fixedSize = typeof width === "number" && typeof height === "number";
|
|
97
|
+
let body;
|
|
98
|
+
if (loading) {
|
|
99
|
+
body = /* @__PURE__ */ jsx(
|
|
100
|
+
"div",
|
|
101
|
+
{
|
|
102
|
+
"data-slot": "chart-skeleton",
|
|
103
|
+
role: "status",
|
|
104
|
+
"aria-label": labels.loading,
|
|
105
|
+
className: "bg-muted h-full min-h-32 w-full animate-pulse rounded-md motion-reduce:animate-none"
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
} else if (error !== void 0 && error !== null && error !== false) {
|
|
109
|
+
body = /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-slot": "chart-error", children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Alert.Title, { children: labels.chartError }),
|
|
111
|
+
typeof error === "boolean" ? null : /* @__PURE__ */ jsx(Alert.Description, { children: error })
|
|
112
|
+
] });
|
|
113
|
+
} else if (empty) {
|
|
114
|
+
body = /* @__PURE__ */ jsx(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
"data-slot": "chart-empty",
|
|
118
|
+
className: "text-muted-foreground flex h-full min-h-32 w-full items-center justify-center text-sm",
|
|
119
|
+
children: emptyState ?? labels.noData
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
const chart = Children.only(children);
|
|
124
|
+
if (fixedSize && isValidElement(chart)) {
|
|
125
|
+
body = cloneElement(chart, { width, height });
|
|
126
|
+
} else {
|
|
127
|
+
body = /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", ...height !== void 0 ? { height } : { aspect }, children: chart });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return /* @__PURE__ */ jsxs(
|
|
131
|
+
"div",
|
|
132
|
+
{
|
|
133
|
+
ref,
|
|
134
|
+
"data-slot": "chart",
|
|
135
|
+
"data-chart": chartId,
|
|
136
|
+
role: "figure",
|
|
137
|
+
"aria-label": ariaLabel,
|
|
138
|
+
className: cn(
|
|
139
|
+
"text-foreground text-xs [&_.recharts-surface]:outline-none [&_.recharts-sector]:outline-none",
|
|
140
|
+
fixedSize ? "" : "w-full",
|
|
141
|
+
className
|
|
142
|
+
),
|
|
143
|
+
style: { ...colorStyle, ...style },
|
|
144
|
+
...props,
|
|
145
|
+
children: [
|
|
146
|
+
themed.length > 0 && /* @__PURE__ */ jsx(
|
|
147
|
+
"style",
|
|
148
|
+
{
|
|
149
|
+
dangerouslySetInnerHTML: {
|
|
150
|
+
__html: [
|
|
151
|
+
`[data-chart="${chartId}"]{${themed.map(([field, s]) => `${seriesColorProperty(field)}:${s.theme.light};`).join("")}}`,
|
|
152
|
+
`.dark [data-chart="${chartId}"]{${themed.map(([field, s]) => `${seriesColorProperty(field)}:${s.theme.dark};`).join("")}}`
|
|
153
|
+
].join("\n")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
/* @__PURE__ */ jsx(ChartContext.Provider, { value: context, children: body })
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
ChartFrame.displayName = "ChartFrame";
|
|
164
|
+
var chart_frame_default = ChartFrame;
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
ChartFrame,
|
|
168
|
+
chart_frame_default
|
|
169
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDev
|
|
3
|
+
} from "./chunk-3VGLTPGL.js";
|
|
4
|
+
|
|
5
|
+
// src/format.ts
|
|
6
|
+
function toNumber(value) {
|
|
7
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
8
|
+
return typeof n === "number" && Number.isFinite(n) ? n : null;
|
|
9
|
+
}
|
|
10
|
+
function toDate(value) {
|
|
11
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
12
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
13
|
+
const d = new Date(value);
|
|
14
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function numberFormatter(locale, options) {
|
|
19
|
+
const nf = new Intl.NumberFormat(locale, options);
|
|
20
|
+
return (value) => {
|
|
21
|
+
const n = toNumber(value);
|
|
22
|
+
return n === null ? String(value ?? "") : nf.format(n);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function dateFormatter(locale, options) {
|
|
26
|
+
const df = new Intl.DateTimeFormat(locale, options);
|
|
27
|
+
return (value) => {
|
|
28
|
+
const d = toDate(value);
|
|
29
|
+
return d === null ? String(value ?? "") : df.format(d);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var DATE_STYLES = {
|
|
33
|
+
short: { month: "short", day: "numeric" },
|
|
34
|
+
medium: { year: "numeric", month: "short", day: "numeric" },
|
|
35
|
+
long: { year: "numeric", month: "long", day: "numeric" }
|
|
36
|
+
};
|
|
37
|
+
var DATETIME_STYLES = {
|
|
38
|
+
short: { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" },
|
|
39
|
+
medium: { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }
|
|
40
|
+
};
|
|
41
|
+
var builtins = {
|
|
42
|
+
none: () => (value) => String(value ?? ""),
|
|
43
|
+
number: (_arg, locale) => numberFormatter(locale, { maximumFractionDigits: 2 }),
|
|
44
|
+
compact: (_arg, locale) => numberFormatter(locale, { notation: "compact", maximumFractionDigits: 1 }),
|
|
45
|
+
/** Expects a 0–1 fraction (matches `stacked: 'percent'` output). */
|
|
46
|
+
percent: (_arg, locale) => numberFormatter(locale, { style: "percent", maximumFractionDigits: 1 }),
|
|
47
|
+
currency: (arg, locale) => numberFormatter(locale, { style: "currency", currency: arg || "USD", maximumFractionDigits: 0 }),
|
|
48
|
+
date: (arg, locale) => dateFormatter(locale, DATE_STYLES[arg] ?? DATE_STYLES.medium),
|
|
49
|
+
datetime: (arg, locale) => dateFormatter(locale, DATETIME_STYLES[arg] ?? DATETIME_STYLES.medium)
|
|
50
|
+
};
|
|
51
|
+
var fallbackFormatter = (value) => String(value ?? "");
|
|
52
|
+
function createFormatters(locale, custom) {
|
|
53
|
+
const entries = { ...builtins, ...custom };
|
|
54
|
+
const cache = /* @__PURE__ */ new Map();
|
|
55
|
+
function build(token) {
|
|
56
|
+
const sep = token.indexOf(":");
|
|
57
|
+
const name = sep === -1 ? token : token.slice(0, sep);
|
|
58
|
+
const arg = sep === -1 ? "" : token.slice(sep + 1);
|
|
59
|
+
const entry = entries[name];
|
|
60
|
+
if (!entry) return null;
|
|
61
|
+
const made = entry(arg, locale);
|
|
62
|
+
return typeof made === "function" ? made : entry;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
resolve(token, fallback) {
|
|
66
|
+
const name = token ?? fallback;
|
|
67
|
+
if (!name || name === "none") return fallbackFormatter;
|
|
68
|
+
const cached = cache.get(name);
|
|
69
|
+
if (cached) return cached;
|
|
70
|
+
let fn = null;
|
|
71
|
+
try {
|
|
72
|
+
fn = build(name);
|
|
73
|
+
} catch {
|
|
74
|
+
fn = null;
|
|
75
|
+
}
|
|
76
|
+
if (!fn) {
|
|
77
|
+
if (isDev()) console.warn(`@zuilib/charts: unknown format token "${name}"; falling back to String`);
|
|
78
|
+
fn = fallback && fallback !== name ? this.resolve(fallback) : fallbackFormatter;
|
|
79
|
+
}
|
|
80
|
+
cache.set(name, fn);
|
|
81
|
+
return fn;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
createFormatters
|
|
88
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/chart-defaults.ts
|
|
2
|
+
var CHART_COLOR_COUNT = 8;
|
|
3
|
+
function chartColorVar(slot) {
|
|
4
|
+
const n = Math.min(Math.max(Math.trunc(slot), 1), CHART_COLOR_COUNT);
|
|
5
|
+
return `var(--chart-${n})`;
|
|
6
|
+
}
|
|
7
|
+
function seriesColorVar(field) {
|
|
8
|
+
return `var(--color-${sanitizeField(field)})`;
|
|
9
|
+
}
|
|
10
|
+
function seriesColorProperty(field) {
|
|
11
|
+
return `--color-${sanitizeField(field)}`;
|
|
12
|
+
}
|
|
13
|
+
function sanitizeField(field) {
|
|
14
|
+
return field.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
15
|
+
}
|
|
16
|
+
var xAxisDefaults = {
|
|
17
|
+
tickLine: false,
|
|
18
|
+
axisLine: false,
|
|
19
|
+
tickMargin: 8,
|
|
20
|
+
tick: { fill: "var(--muted-foreground)", fontSize: 12 }
|
|
21
|
+
};
|
|
22
|
+
var yAxisDefaults = {
|
|
23
|
+
tickLine: false,
|
|
24
|
+
axisLine: false,
|
|
25
|
+
tickMargin: 8,
|
|
26
|
+
tick: { fill: "var(--muted-foreground)", fontSize: 12 }
|
|
27
|
+
};
|
|
28
|
+
var cartesianGridDefaults = {
|
|
29
|
+
stroke: "var(--border)",
|
|
30
|
+
strokeDasharray: "3 3",
|
|
31
|
+
vertical: false
|
|
32
|
+
};
|
|
33
|
+
var tooltipCursorDefaults = { fill: "var(--accent)", opacity: 0.5 };
|
|
34
|
+
var tooltipCrosshairDefaults = { stroke: "var(--border)", strokeWidth: 1 };
|
|
35
|
+
var DIMMED_OPACITY = 0.35;
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
CHART_COLOR_COUNT,
|
|
39
|
+
chartColorVar,
|
|
40
|
+
seriesColorVar,
|
|
41
|
+
seriesColorProperty,
|
|
42
|
+
xAxisDefaults,
|
|
43
|
+
yAxisDefaults,
|
|
44
|
+
cartesianGridDefaults,
|
|
45
|
+
tooltipCursorDefaults,
|
|
46
|
+
tooltipCrosshairDefaults,
|
|
47
|
+
DIMMED_OPACITY
|
|
48
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultChartRegistry
|
|
3
|
+
} from "./chunk-YMPZODMC.js";
|
|
4
|
+
import {
|
|
5
|
+
prefersReducedMotion
|
|
6
|
+
} from "./chunk-HNJKYOPE.js";
|
|
7
|
+
import {
|
|
8
|
+
chart_frame_default
|
|
9
|
+
} from "./chunk-G4FXJJ66.js";
|
|
10
|
+
import {
|
|
11
|
+
createFormatters
|
|
12
|
+
} from "./chunk-7WABBHMT.js";
|
|
13
|
+
import {
|
|
14
|
+
warnOnce
|
|
15
|
+
} from "./chunk-3VGLTPGL.js";
|
|
16
|
+
import {
|
|
17
|
+
useOptionalCrossFilter
|
|
18
|
+
} from "./chunk-WVWGHGZE.js";
|
|
19
|
+
import {
|
|
20
|
+
chartColorVar
|
|
21
|
+
} from "./chunk-7WJ26NHR.js";
|
|
22
|
+
|
|
23
|
+
// src/chart.tsx
|
|
24
|
+
import { useMemo, useState } from "react";
|
|
25
|
+
import { jsx } from "react/jsx-runtime";
|
|
26
|
+
function configFromSpec(spec, registry = defaultChartRegistry) {
|
|
27
|
+
const fromDef = registry.types.get(spec.type)?.configFrom?.(spec);
|
|
28
|
+
if (fromDef) return fromDef;
|
|
29
|
+
if ("series" in spec && Array.isArray(spec.series)) {
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
spec.series.map((series) => [
|
|
32
|
+
series.field,
|
|
33
|
+
{
|
|
34
|
+
label: series.label ?? series.field,
|
|
35
|
+
color: series.paletteSlot ? chartColorVar(series.paletteSlot) : void 0,
|
|
36
|
+
format: series.format
|
|
37
|
+
}
|
|
38
|
+
])
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
function crossFilterFieldOf(spec, registry = defaultChartRegistry) {
|
|
44
|
+
const field = registry.types.get(spec.type)?.crossFilterField?.(spec);
|
|
45
|
+
if (field !== void 0) return field;
|
|
46
|
+
const xKey = spec.xKey;
|
|
47
|
+
return typeof xKey === "string" ? xKey : "";
|
|
48
|
+
}
|
|
49
|
+
function categoriesOf(spec, rows, registry) {
|
|
50
|
+
const field = categoryFieldOf(spec, registry);
|
|
51
|
+
if (!field) return void 0;
|
|
52
|
+
const values = [];
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const value = row[field];
|
|
55
|
+
if ((typeof value === "string" || typeof value === "number") && !values.includes(value)) values.push(value);
|
|
56
|
+
}
|
|
57
|
+
return values.map((value, index) => ({ value, color: chartColorVar(index + 1) }));
|
|
58
|
+
}
|
|
59
|
+
function categoryFieldOf(spec, registry) {
|
|
60
|
+
return registry.types.get(spec.type)?.categoryField?.(spec);
|
|
61
|
+
}
|
|
62
|
+
function renderSpec(props, registry) {
|
|
63
|
+
const def = registry.types.get(props.spec.type);
|
|
64
|
+
if (!def) {
|
|
65
|
+
warnOnce(`type-${props.spec.type}`, `@zuilib/charts: unknown chart type "${props.spec.type}"`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return /* @__PURE__ */ jsx(def.render, { ...props });
|
|
69
|
+
}
|
|
70
|
+
function Chart({
|
|
71
|
+
spec,
|
|
72
|
+
rows,
|
|
73
|
+
registry,
|
|
74
|
+
formats,
|
|
75
|
+
locale,
|
|
76
|
+
className,
|
|
77
|
+
width,
|
|
78
|
+
height,
|
|
79
|
+
aspect,
|
|
80
|
+
loading,
|
|
81
|
+
error,
|
|
82
|
+
emptyState,
|
|
83
|
+
ariaLabel,
|
|
84
|
+
hiddenSeries,
|
|
85
|
+
defaultHiddenSeries,
|
|
86
|
+
onHiddenSeriesChange,
|
|
87
|
+
onMarkClick,
|
|
88
|
+
labels,
|
|
89
|
+
animate
|
|
90
|
+
}) {
|
|
91
|
+
const chartRegistry = registry ?? defaultChartRegistry;
|
|
92
|
+
const crossFilter = useOptionalCrossFilter();
|
|
93
|
+
const formatters = useMemo(() => createFormatters(locale, formats), [locale, formats]);
|
|
94
|
+
const config = useMemo(() => configFromSpec(spec, chartRegistry), [spec, chartRegistry]);
|
|
95
|
+
const [innerHidden, setInnerHidden] = useState(defaultHiddenSeries ?? []);
|
|
96
|
+
const hidden = hiddenSeries ?? innerHidden;
|
|
97
|
+
const onHiddenChange = (next) => {
|
|
98
|
+
if (hiddenSeries === void 0) setInnerHidden(next);
|
|
99
|
+
onHiddenSeriesChange?.(next);
|
|
100
|
+
};
|
|
101
|
+
const markInteraction = {
|
|
102
|
+
field: crossFilterFieldOf(spec, chartRegistry),
|
|
103
|
+
crossFilter,
|
|
104
|
+
interaction: spec.interaction ?? "filter",
|
|
105
|
+
onMarkClick
|
|
106
|
+
};
|
|
107
|
+
return /* @__PURE__ */ jsx(
|
|
108
|
+
chart_frame_default,
|
|
109
|
+
{
|
|
110
|
+
config,
|
|
111
|
+
className,
|
|
112
|
+
width,
|
|
113
|
+
height,
|
|
114
|
+
aspect,
|
|
115
|
+
loading,
|
|
116
|
+
error,
|
|
117
|
+
emptyState,
|
|
118
|
+
empty: rows.length === 0,
|
|
119
|
+
ariaLabel: ariaLabel ?? spec.title,
|
|
120
|
+
labels,
|
|
121
|
+
formats,
|
|
122
|
+
locale,
|
|
123
|
+
hiddenSeries: hidden,
|
|
124
|
+
onHiddenSeriesChange: onHiddenChange,
|
|
125
|
+
categoryField: categoryFieldOf(spec, chartRegistry),
|
|
126
|
+
categories: useMemo(() => categoriesOf(spec, rows, chartRegistry), [spec, rows, chartRegistry]),
|
|
127
|
+
children: renderSpec(
|
|
128
|
+
{
|
|
129
|
+
spec,
|
|
130
|
+
rows,
|
|
131
|
+
formats: formatters,
|
|
132
|
+
hiddenSeries: hidden,
|
|
133
|
+
markInteraction,
|
|
134
|
+
animate: animate ?? !prefersReducedMotion()
|
|
135
|
+
},
|
|
136
|
+
chartRegistry
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
var chart_default = Chart;
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
configFromSpec,
|
|
145
|
+
crossFilterFieldOf,
|
|
146
|
+
Chart,
|
|
147
|
+
chart_default
|
|
148
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/interop.ts
|
|
2
|
+
function toColumnFilters(state) {
|
|
3
|
+
return state.filters.filter((f) => f.values.length > 0).map((f) => ({ id: f.field, value: [...f.values] }));
|
|
4
|
+
}
|
|
5
|
+
function fromColumnFilters(columnFilters, fields) {
|
|
6
|
+
if (!Array.isArray(columnFilters)) return [];
|
|
7
|
+
const entries = [];
|
|
8
|
+
for (const field of fields) {
|
|
9
|
+
const filter = columnFilters.find((f) => f && typeof f === "object" && f.id === field);
|
|
10
|
+
if (!filter || !Array.isArray(filter.value)) continue;
|
|
11
|
+
const values = filter.value.filter(
|
|
12
|
+
(v) => typeof v === "string" || typeof v === "number"
|
|
13
|
+
);
|
|
14
|
+
if (values.length) entries.push({ field, values });
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
function toHighlight(rows, field) {
|
|
19
|
+
if (!Array.isArray(rows)) return null;
|
|
20
|
+
const values = [
|
|
21
|
+
...new Set(
|
|
22
|
+
rows.map((row) => row && typeof row === "object" ? row[field] : void 0).filter((v) => typeof v === "string" || typeof v === "number")
|
|
23
|
+
)
|
|
24
|
+
];
|
|
25
|
+
return values.length ? { field, values } : null;
|
|
26
|
+
}
|
|
27
|
+
function applyFilters(rows, state, options) {
|
|
28
|
+
const active = state.filters.filter((f) => f.values.length > 0 && f.field !== options?.except);
|
|
29
|
+
if (!active.length) return [...rows];
|
|
30
|
+
return rows.filter(
|
|
31
|
+
(row) => active.every((f) => {
|
|
32
|
+
const value = row?.[f.field];
|
|
33
|
+
return (typeof value === "string" || typeof value === "number") && f.values.includes(value);
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
toColumnFilters,
|
|
40
|
+
fromColumnFilters,
|
|
41
|
+
toHighlight,
|
|
42
|
+
applyFilters
|
|
43
|
+
};
|