@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,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, { tone: "danger", "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,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DIMMED_OPACITY
|
|
3
|
+
} from "./chunk-7WJ26NHR.js";
|
|
4
|
+
|
|
5
|
+
// src/charts/mark-interaction.ts
|
|
6
|
+
function prefersReducedMotion() {
|
|
7
|
+
try {
|
|
8
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function reportMarkClick(interaction, value, seriesField, row, additive) {
|
|
14
|
+
interaction.onMarkClick?.({ field: interaction.field, value, seriesField, row, additive });
|
|
15
|
+
const { crossFilter, field } = interaction;
|
|
16
|
+
if (!crossFilter || interaction.interaction === "none") return;
|
|
17
|
+
if (interaction.interaction === "highlight") {
|
|
18
|
+
const current = crossFilter.state.highlight;
|
|
19
|
+
const active = current?.field === field && current.values.includes(value);
|
|
20
|
+
crossFilter.setHighlight(active ? null : { field, values: [value] });
|
|
21
|
+
} else {
|
|
22
|
+
crossFilter.toggleValue(field, value, { additive });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function activeCategoryValues(interaction) {
|
|
26
|
+
const highlight = interaction.crossFilter?.state.highlight;
|
|
27
|
+
if (highlight && highlight.field === interaction.field) return highlight.values;
|
|
28
|
+
return interaction.crossFilter?.valuesFor(interaction.field) ?? null;
|
|
29
|
+
}
|
|
30
|
+
function markDimOpacity(active, value) {
|
|
31
|
+
if (active === null) return void 0;
|
|
32
|
+
return typeof value !== "undefined" && active.includes(value) ? void 0 : DIMMED_OPACITY;
|
|
33
|
+
}
|
|
34
|
+
function categoryOf(row, field) {
|
|
35
|
+
const value = row?.[field];
|
|
36
|
+
return typeof value === "string" || typeof value === "number" ? value : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
prefersReducedMotion,
|
|
41
|
+
reportMarkClick,
|
|
42
|
+
activeCategoryValues,
|
|
43
|
+
markDimOpacity,
|
|
44
|
+
categoryOf
|
|
45
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/chart-context.ts
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
var ChartContext = createContext(null);
|
|
4
|
+
function useChartContext() {
|
|
5
|
+
const context = useContext(ChartContext);
|
|
6
|
+
if (!context) throw new Error("Chart components must be used inside a <ChartFrame>");
|
|
7
|
+
return context;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
ChartContext,
|
|
12
|
+
useChartContext
|
|
13
|
+
};
|
|
@@ -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-6QTIAMA3.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,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useChartContext
|
|
3
|
+
} from "./chunk-IPOBQIYU.js";
|
|
4
|
+
import {
|
|
5
|
+
seriesColorVar
|
|
6
|
+
} from "./chunk-7WJ26NHR.js";
|
|
7
|
+
|
|
8
|
+
// src/chart-tooltip.tsx
|
|
9
|
+
import { cn } from "@zuilib/components/lib/cn";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
function ChartTooltipPanel({
|
|
12
|
+
active,
|
|
13
|
+
payload,
|
|
14
|
+
label,
|
|
15
|
+
labelFormat,
|
|
16
|
+
valueFormat = "number",
|
|
17
|
+
showLabel = true,
|
|
18
|
+
className
|
|
19
|
+
}) {
|
|
20
|
+
const { config, formats } = useChartContext();
|
|
21
|
+
if (!active || !payload?.length) return null;
|
|
22
|
+
const labelText = formats.resolve(labelFormat, "none")(label);
|
|
23
|
+
return /* @__PURE__ */ jsxs(
|
|
24
|
+
"div",
|
|
25
|
+
{
|
|
26
|
+
"data-slot": "chart-tooltip",
|
|
27
|
+
className: cn(
|
|
28
|
+
"bg-popover text-popover-foreground border-border min-w-32 rounded-md border px-2.5 py-1.5 shadow-md",
|
|
29
|
+
className
|
|
30
|
+
),
|
|
31
|
+
children: [
|
|
32
|
+
showLabel && labelText !== "" && /* @__PURE__ */ jsx("div", { "data-slot": "chart-tooltip-label", className: "mb-1 font-medium", children: labelText }),
|
|
33
|
+
/* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: payload.map((entry, index) => {
|
|
34
|
+
const field = entry.dataKey != null ? String(entry.dataKey) : String(entry.name ?? index);
|
|
35
|
+
const series = config[field];
|
|
36
|
+
const name = series?.label ?? String(entry.name ?? field);
|
|
37
|
+
const format = formats.resolve(series?.format, valueFormat);
|
|
38
|
+
return /* @__PURE__ */ jsxs("li", { "data-slot": "chart-tooltip-row", className: "flex items-center gap-1.5", children: [
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"span",
|
|
41
|
+
{
|
|
42
|
+
"data-slot": "chart-tooltip-chip",
|
|
43
|
+
"aria-hidden": true,
|
|
44
|
+
className: "size-2.5 shrink-0 rounded-xs",
|
|
45
|
+
style: { backgroundColor: series ? seriesColorVar(field) : entry.color }
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: name }),
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground ms-auto font-mono font-medium tabular-nums", children: format(entry.value) })
|
|
50
|
+
] }, `${field}-${index}`);
|
|
51
|
+
}) })
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
var chart_tooltip_default = ChartTooltipPanel;
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
ChartTooltipPanel,
|
|
60
|
+
chart_tooltip_default
|
|
61
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useOptionalCrossFilter
|
|
3
|
+
} from "./chunk-WVWGHGZE.js";
|
|
4
|
+
import {
|
|
5
|
+
fillLabel
|
|
6
|
+
} from "./chunk-UN3XPBZ5.js";
|
|
7
|
+
import {
|
|
8
|
+
useChartContext
|
|
9
|
+
} from "./chunk-IPOBQIYU.js";
|
|
10
|
+
import {
|
|
11
|
+
seriesColorVar
|
|
12
|
+
} from "./chunk-7WJ26NHR.js";
|
|
13
|
+
|
|
14
|
+
// src/chart-legend.tsx
|
|
15
|
+
import { cn } from "@zuilib/components/lib/cn";
|
|
16
|
+
import { focusRingClasses } from "@zuilib/components/lib/focus";
|
|
17
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
function ChartLegendList({ payload, className }) {
|
|
19
|
+
const { config, labels, hiddenSeries, onLegendClick, categoryField, categories } = useChartContext();
|
|
20
|
+
const crossFilter = useOptionalCrossFilter();
|
|
21
|
+
const categoryEntries = categories?.map((c) => ({ key: String(c.value), name: String(c.value), color: c.color, series: false })) ?? (payload ?? []).map((entry) => {
|
|
22
|
+
const value = String(entry.value ?? "");
|
|
23
|
+
return { key: value, name: value, color: entry.color, series: false };
|
|
24
|
+
});
|
|
25
|
+
const entries = categoryField ? categoryEntries : Object.keys(config).map((field) => ({
|
|
26
|
+
key: field,
|
|
27
|
+
name: config[field].label,
|
|
28
|
+
color: seriesColorVar(field),
|
|
29
|
+
series: true
|
|
30
|
+
}));
|
|
31
|
+
if (!entries.length) return null;
|
|
32
|
+
const active = categoryField && crossFilter ? crossFilter.valuesFor(categoryField) : null;
|
|
33
|
+
return /* @__PURE__ */ jsx(
|
|
34
|
+
"ul",
|
|
35
|
+
{
|
|
36
|
+
"data-slot": "chart-legend",
|
|
37
|
+
"aria-label": labels.legend,
|
|
38
|
+
className: cn("flex flex-wrap items-center justify-center gap-x-3 gap-y-1 pt-2", className),
|
|
39
|
+
children: entries.map((entry) => {
|
|
40
|
+
const off = entry.series ? hiddenSeries.includes(entry.key) : active !== null && !active.includes(entry.key);
|
|
41
|
+
return /* @__PURE__ */ jsx("li", { "data-slot": "chart-legend-item", className: "flex", children: /* @__PURE__ */ jsxs(
|
|
42
|
+
"button",
|
|
43
|
+
{
|
|
44
|
+
type: "button",
|
|
45
|
+
"aria-pressed": !off,
|
|
46
|
+
"aria-label": fillLabel(labels.toggleSeries, { series: entry.name }),
|
|
47
|
+
"data-slot": "chart-legend-button",
|
|
48
|
+
className: cn(
|
|
49
|
+
"text-muted-foreground flex cursor-pointer items-center gap-1.5 rounded-sm px-1 py-0.5 text-xs transition-opacity",
|
|
50
|
+
focusRingClasses,
|
|
51
|
+
off && "opacity-45"
|
|
52
|
+
),
|
|
53
|
+
onClick: (event) => {
|
|
54
|
+
if (entry.series) {
|
|
55
|
+
onLegendClick(entry.key, { additive: event.shiftKey });
|
|
56
|
+
} else if (categoryField) {
|
|
57
|
+
crossFilter?.toggleValue(categoryField, entry.key, { additive: event.shiftKey });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
"span",
|
|
63
|
+
{
|
|
64
|
+
"data-slot": "chart-legend-chip",
|
|
65
|
+
"aria-hidden": true,
|
|
66
|
+
className: "size-2.5 shrink-0 rounded-xs",
|
|
67
|
+
style: { backgroundColor: entry.color }
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
entry.name
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
) }, entry.key);
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
var chart_legend_default = ChartLegendList;
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
ChartLegendList,
|
|
82
|
+
chart_legend_default
|
|
83
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/labels.ts
|
|
2
|
+
var defaultChartLabels = {
|
|
3
|
+
noData: "No data",
|
|
4
|
+
loading: "Loading chart\u2026",
|
|
5
|
+
chartError: "Chart failed to load",
|
|
6
|
+
toggleSeries: "Toggle {series}",
|
|
7
|
+
legend: "Chart legend",
|
|
8
|
+
filterBy: "Filter by {value}"
|
|
9
|
+
};
|
|
10
|
+
function fillLabel(template, values) {
|
|
11
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
defaultChartLabels,
|
|
16
|
+
fillLabel
|
|
17
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/cross-filter.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// src/cross-filter-state.ts
|
|
12
|
+
function toggleValueIn(state, field, value, options) {
|
|
13
|
+
const entry = state.filters.find((f) => f.field === field);
|
|
14
|
+
const others = state.filters.filter((f) => f.field !== field);
|
|
15
|
+
const has = entry?.values.includes(value) ?? false;
|
|
16
|
+
let values;
|
|
17
|
+
if (options?.additive) {
|
|
18
|
+
values = has ? (entry?.values ?? []).filter((v) => v !== value) : [...entry?.values ?? [], value];
|
|
19
|
+
} else {
|
|
20
|
+
values = has && entry?.values.length === 1 ? [] : [value];
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
filters: values.length ? [...others, { field, values }] : others
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/cross-filter.tsx
|
|
29
|
+
import { jsx } from "react/jsx-runtime";
|
|
30
|
+
var defaultCrossFilterState = { filters: [], highlight: null };
|
|
31
|
+
var CrossFilterContext = createContext(null);
|
|
32
|
+
function CrossFilterProvider({ state, defaultState, onStateChange, children }) {
|
|
33
|
+
const [inner, setInner] = useState(defaultState ?? defaultCrossFilterState);
|
|
34
|
+
const controlled = state !== void 0;
|
|
35
|
+
const current = controlled ? state : inner;
|
|
36
|
+
const currentRef = useRef(current);
|
|
37
|
+
currentRef.current = current;
|
|
38
|
+
const emitRef = useRef(onStateChange);
|
|
39
|
+
emitRef.current = onStateChange;
|
|
40
|
+
const emit = useCallback(
|
|
41
|
+
(next) => {
|
|
42
|
+
currentRef.current = next;
|
|
43
|
+
if (!controlled) setInner(next);
|
|
44
|
+
emitRef.current?.(next);
|
|
45
|
+
},
|
|
46
|
+
[controlled]
|
|
47
|
+
);
|
|
48
|
+
const toggleValue = useCallback(
|
|
49
|
+
(field, value, options) => emit(toggleValueIn(currentRef.current, field, value, options)),
|
|
50
|
+
[emit]
|
|
51
|
+
);
|
|
52
|
+
const clearField = useCallback(
|
|
53
|
+
(field) => emit({ ...currentRef.current, filters: currentRef.current.filters.filter((f) => f.field !== field) }),
|
|
54
|
+
[emit]
|
|
55
|
+
);
|
|
56
|
+
const clearAll = useCallback(() => emit(defaultCrossFilterState), [emit]);
|
|
57
|
+
const setHighlight = useCallback(
|
|
58
|
+
(highlight) => emit({ ...currentRef.current, highlight }),
|
|
59
|
+
[emit]
|
|
60
|
+
);
|
|
61
|
+
const result = useMemo(
|
|
62
|
+
() => ({
|
|
63
|
+
state: current,
|
|
64
|
+
filtered: current.filters.some((f) => f.values.length > 0),
|
|
65
|
+
toggleValue,
|
|
66
|
+
clearField,
|
|
67
|
+
clearAll,
|
|
68
|
+
setHighlight,
|
|
69
|
+
valuesFor: (field) => current.filters.find((f) => f.field === field)?.values ?? null
|
|
70
|
+
}),
|
|
71
|
+
[current, toggleValue, clearField, clearAll, setHighlight]
|
|
72
|
+
);
|
|
73
|
+
return /* @__PURE__ */ jsx(CrossFilterContext.Provider, { value: result, children });
|
|
74
|
+
}
|
|
75
|
+
function useCrossFilter() {
|
|
76
|
+
const context = useContext(CrossFilterContext);
|
|
77
|
+
if (!context) throw new Error("useCrossFilter must be used inside a <CrossFilterProvider>");
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
80
|
+
function useOptionalCrossFilter() {
|
|
81
|
+
return useContext(CrossFilterContext);
|
|
82
|
+
}
|
|
83
|
+
var cross_filter_default = CrossFilterProvider;
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
defaultCrossFilterState,
|
|
87
|
+
useCrossFilter,
|
|
88
|
+
useOptionalCrossFilter,
|
|
89
|
+
cross_filter_default
|
|
90
|
+
};
|