@matthieumordrel/chart-studio-ui 0.5.2
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 +35 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.mjs +18 -0
- package/dist/theme.css +67 -0
- package/dist/ui/chart-axis-ticks.mjs +65 -0
- package/dist/ui/chart-canvas.d.mts +40 -0
- package/dist/ui/chart-canvas.mjs +872 -0
- package/dist/ui/chart-context.d.mts +101 -0
- package/dist/ui/chart-context.mjs +117 -0
- package/dist/ui/chart-date-range-badge.d.mts +20 -0
- package/dist/ui/chart-date-range-badge.mjs +49 -0
- package/dist/ui/chart-date-range-panel.d.mts +18 -0
- package/dist/ui/chart-date-range-panel.mjs +126 -0
- package/dist/ui/chart-date-range.d.mts +20 -0
- package/dist/ui/chart-date-range.mjs +67 -0
- package/dist/ui/chart-debug.d.mts +21 -0
- package/dist/ui/chart-debug.mjs +172 -0
- package/dist/ui/chart-dropdown.mjs +92 -0
- package/dist/ui/chart-filters-panel.d.mts +26 -0
- package/dist/ui/chart-filters-panel.mjs +258 -0
- package/dist/ui/chart-filters.d.mts +18 -0
- package/dist/ui/chart-filters.mjs +48 -0
- package/dist/ui/chart-group-by-selector.d.mts +16 -0
- package/dist/ui/chart-group-by-selector.mjs +32 -0
- package/dist/ui/chart-metric-panel.d.mts +25 -0
- package/dist/ui/chart-metric-panel.mjs +172 -0
- package/dist/ui/chart-metric-selector.d.mts +16 -0
- package/dist/ui/chart-metric-selector.mjs +50 -0
- package/dist/ui/chart-select.mjs +61 -0
- package/dist/ui/chart-source-switcher.d.mts +24 -0
- package/dist/ui/chart-source-switcher.mjs +56 -0
- package/dist/ui/chart-time-bucket-selector.d.mts +17 -0
- package/dist/ui/chart-time-bucket-selector.mjs +37 -0
- package/dist/ui/chart-toolbar-overflow.d.mts +28 -0
- package/dist/ui/chart-toolbar-overflow.mjs +223 -0
- package/dist/ui/chart-toolbar.d.mts +33 -0
- package/dist/ui/chart-toolbar.mjs +60 -0
- package/dist/ui/chart-type-selector.d.mts +19 -0
- package/dist/ui/chart-type-selector.mjs +173 -0
- package/dist/ui/chart-x-axis-selector.d.mts +16 -0
- package/dist/ui/chart-x-axis-selector.mjs +28 -0
- package/dist/ui/index.d.mts +18 -0
- package/dist/ui/percent-stacked.mjs +36 -0
- package/dist/ui/toolbar-types.d.mts +7 -0
- package/dist/ui/toolbar-types.mjs +83 -0
- package/package.json +55 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartDropdownPanel } from "./chart-dropdown.mjs";
|
|
3
|
+
import { ChartDateRangePanel, resolvePresetLabel } from "./chart-date-range-panel.mjs";
|
|
4
|
+
import { ChartFiltersPanel } from "./chart-filters-panel.mjs";
|
|
5
|
+
import { ChartMetricPanel } from "./chart-metric-panel.mjs";
|
|
6
|
+
import { CONTROL_IDS, CONTROL_REGISTRY, SECTIONS } from "./toolbar-types.mjs";
|
|
7
|
+
import { useRef, useState } from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { getMetricLabel } from "@matthieumordrel/chart-studio/_internal";
|
|
10
|
+
import { ArrowLeft, BarChart3, Calendar, ChevronRight, Clock, Database, Ellipsis, Eraser, Filter, Layers, MoveHorizontal, MoveVertical } from "lucide-react";
|
|
11
|
+
//#region src/ui/chart-toolbar-overflow.tsx
|
|
12
|
+
/** Controls that drill-down into a detail page instead of rendering inline. */
|
|
13
|
+
const COMPLEX_CONTROLS = new Set([
|
|
14
|
+
"metric",
|
|
15
|
+
"filters",
|
|
16
|
+
"dateRange"
|
|
17
|
+
]);
|
|
18
|
+
/** Icon for each control in the overflow menu. */
|
|
19
|
+
const CONTROL_ICONS = {
|
|
20
|
+
source: Database,
|
|
21
|
+
xAxis: MoveHorizontal,
|
|
22
|
+
chartType: BarChart3,
|
|
23
|
+
groupBy: Layers,
|
|
24
|
+
timeBucket: Clock,
|
|
25
|
+
metric: MoveVertical,
|
|
26
|
+
filters: Filter,
|
|
27
|
+
dateRange: Calendar
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Ellipsis overflow menu with Notion-style drill-down navigation.
|
|
31
|
+
* Main view shows all controls. Clicking a complex control replaces
|
|
32
|
+
* the panel content with that control's detail page + back button.
|
|
33
|
+
*/
|
|
34
|
+
function ChartToolbarOverflow({ pinned, hidden, className }) {
|
|
35
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
36
|
+
/** null = main menu, ControlId = detail page for that control */
|
|
37
|
+
const [activePage, setActivePage] = useState(null);
|
|
38
|
+
const triggerRef = useRef(null);
|
|
39
|
+
const sectionGroups = SECTIONS.map((section) => {
|
|
40
|
+
return {
|
|
41
|
+
section,
|
|
42
|
+
controls: CONTROL_IDS.filter((id) => {
|
|
43
|
+
if (pinned.has(id) || hidden.has(id)) return false;
|
|
44
|
+
return CONTROL_REGISTRY[id].section === section.id;
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
}).filter((g) => g.controls.length > 0);
|
|
48
|
+
if (sectionGroups.length === 0) return null;
|
|
49
|
+
const handleClose = () => {
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
setActivePage(null);
|
|
52
|
+
};
|
|
53
|
+
const handleToggle = () => {
|
|
54
|
+
if (isOpen) {
|
|
55
|
+
handleClose();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setIsOpen(true);
|
|
59
|
+
};
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
61
|
+
className,
|
|
62
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
63
|
+
ref: triggerRef,
|
|
64
|
+
onClick: handleToggle,
|
|
65
|
+
className: `inline-flex h-7 w-7 items-center justify-center rounded-md border transition-colors ${isOpen ? "border-primary/50 bg-primary/10 text-primary" : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
66
|
+
"aria-label": "Chart settings",
|
|
67
|
+
children: /* @__PURE__ */ jsx(Ellipsis, { className: "h-4 w-4" })
|
|
68
|
+
}), /* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
69
|
+
isOpen,
|
|
70
|
+
onClose: handleClose,
|
|
71
|
+
triggerRef,
|
|
72
|
+
align: "right",
|
|
73
|
+
width: 320,
|
|
74
|
+
repositionKey: activePage ?? "main",
|
|
75
|
+
className: "flex min-h-[280px] max-h-[min(480px,80vh)] flex-col",
|
|
76
|
+
children: activePage === null ? /* @__PURE__ */ jsx(MainMenu, {
|
|
77
|
+
sectionGroups,
|
|
78
|
+
onNavigate: setActivePage
|
|
79
|
+
}) : /* @__PURE__ */ jsx(DetailPage, {
|
|
80
|
+
controlId: activePage,
|
|
81
|
+
onBack: () => setActivePage(null)
|
|
82
|
+
})
|
|
83
|
+
})]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Main menu showing all controls organized by section. */
|
|
87
|
+
function MainMenu({ sectionGroups, onNavigate }) {
|
|
88
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
89
|
+
className: "border-b border-border px-4 py-3",
|
|
90
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
91
|
+
className: "text-xs font-semibold text-foreground",
|
|
92
|
+
children: "Chart configuration"
|
|
93
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
94
|
+
className: "mt-0.5 text-[10px] text-muted-foreground",
|
|
95
|
+
children: "Customize how your data is displayed"
|
|
96
|
+
})]
|
|
97
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
98
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-2",
|
|
99
|
+
onWheel: (e) => e.stopPropagation(),
|
|
100
|
+
children: sectionGroups.map(({ section, controls }, groupIdx) => /* @__PURE__ */ jsxs("div", { children: [groupIdx > 0 && /* @__PURE__ */ jsx("div", { className: "mx-2 my-1.5 border-t border-border" }), /* @__PURE__ */ jsxs("div", {
|
|
101
|
+
className: "px-2 pb-1 pt-2",
|
|
102
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
103
|
+
className: "mb-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
104
|
+
children: section.label
|
|
105
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
106
|
+
className: "space-y-1",
|
|
107
|
+
children: controls.map((id) => COMPLEX_CONTROLS.has(id) ? /* @__PURE__ */ jsx(ComplexControlRow, {
|
|
108
|
+
controlId: id,
|
|
109
|
+
onNavigate: () => onNavigate(id)
|
|
110
|
+
}, id) : /* @__PURE__ */ jsx(SimpleControlRow, { controlId: id }, id))
|
|
111
|
+
})]
|
|
112
|
+
})] }, section.id))
|
|
113
|
+
})] });
|
|
114
|
+
}
|
|
115
|
+
/** Full-page detail view for a complex control, with back navigation. */
|
|
116
|
+
function DetailPage({ controlId, onBack }) {
|
|
117
|
+
const entry = CONTROL_REGISTRY[controlId];
|
|
118
|
+
const { filters, clearAllFilters } = useChartContext();
|
|
119
|
+
const filterActiveCount = controlId === "filters" ? [...filters.values()].reduce((sum, set) => sum + set.size, 0) : 0;
|
|
120
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
121
|
+
className: "flex items-center gap-2 border-b border-border px-3 py-2.5",
|
|
122
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
123
|
+
onClick: onBack,
|
|
124
|
+
className: "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
125
|
+
"aria-label": "Back",
|
|
126
|
+
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
127
|
+
}), controlId === "filters" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
128
|
+
className: "min-w-0 flex-1 truncate text-xs font-semibold text-foreground",
|
|
129
|
+
children: "Filters"
|
|
130
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
131
|
+
onClick: () => clearAllFilters(),
|
|
132
|
+
disabled: filterActiveCount === 0,
|
|
133
|
+
className: "shrink-0 rounded-md p-1.5 text-muted-foreground transition-colors enabled:hover:bg-muted enabled:hover:text-foreground disabled:opacity-0",
|
|
134
|
+
"aria-label": "Clear all filters",
|
|
135
|
+
children: /* @__PURE__ */ jsx(Eraser, { className: "h-3.5 w-3.5" })
|
|
136
|
+
})] }) : /* @__PURE__ */ jsx("div", {
|
|
137
|
+
className: "text-xs font-semibold text-foreground",
|
|
138
|
+
children: entry.label
|
|
139
|
+
})]
|
|
140
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
141
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-3",
|
|
142
|
+
onWheel: (e) => e.stopPropagation(),
|
|
143
|
+
children: [
|
|
144
|
+
controlId === "metric" && /* @__PURE__ */ jsx(ChartMetricPanel, {}),
|
|
145
|
+
controlId === "filters" && /* @__PURE__ */ jsx(ChartFiltersPanel, { showHeader: false }),
|
|
146
|
+
controlId === "dateRange" && /* @__PURE__ */ jsx(ChartDateRangePanel, {})
|
|
147
|
+
]
|
|
148
|
+
})] });
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A simple control row — label on the left, component on the right.
|
|
152
|
+
* Used for selects and toggle buttons that work inline without popovers.
|
|
153
|
+
*/
|
|
154
|
+
function SimpleControlRow({ controlId }) {
|
|
155
|
+
const Component = CONTROL_REGISTRY[controlId].component;
|
|
156
|
+
const Icon = CONTROL_ICONS[controlId];
|
|
157
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
158
|
+
className: "flex items-center gap-3 rounded-lg px-2 py-1.5",
|
|
159
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("div", {
|
|
160
|
+
className: "flex min-w-0 items-center",
|
|
161
|
+
children: /* @__PURE__ */ jsx(Component, { hideIcon: true })
|
|
162
|
+
})]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A clickable row for complex controls — navigates to detail page on click.
|
|
167
|
+
* Shows the control label, current value summary, and a chevron.
|
|
168
|
+
*/
|
|
169
|
+
function ComplexControlRow({ controlId, onNavigate }) {
|
|
170
|
+
const Icon = CONTROL_ICONS[controlId];
|
|
171
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
172
|
+
onClick: onNavigate,
|
|
173
|
+
className: "flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted/50",
|
|
174
|
+
children: [
|
|
175
|
+
Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
176
|
+
/* @__PURE__ */ jsx("div", {
|
|
177
|
+
className: "truncate text-xs text-foreground",
|
|
178
|
+
children: /* @__PURE__ */ jsx(ControlSummary, { controlId })
|
|
179
|
+
}),
|
|
180
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Summary text for a complex control in the main menu. */
|
|
185
|
+
function ControlSummary({ controlId }) {
|
|
186
|
+
const { metric, columns, filters, dateRange, dateRangePreset } = useChartContext();
|
|
187
|
+
switch (controlId) {
|
|
188
|
+
case "metric": return /* @__PURE__ */ jsx("span", { children: getMetricLabel(metric, columns) });
|
|
189
|
+
case "filters": {
|
|
190
|
+
const count = [...filters.values()].reduce((sum, set) => sum + set.size, 0);
|
|
191
|
+
return /* @__PURE__ */ jsx("span", { children: count > 0 ? `${count} active` : "None" });
|
|
192
|
+
}
|
|
193
|
+
case "dateRange": {
|
|
194
|
+
const label = resolvePresetLabel(dateRangePreset);
|
|
195
|
+
if (dateRange?.min && dateRange?.max) {
|
|
196
|
+
const fmt = (d) => d.toLocaleDateString("en-US", {
|
|
197
|
+
month: "short",
|
|
198
|
+
day: "numeric",
|
|
199
|
+
year: "2-digit"
|
|
200
|
+
});
|
|
201
|
+
return /* @__PURE__ */ jsxs("span", { children: [
|
|
202
|
+
label,
|
|
203
|
+
/* @__PURE__ */ jsx("span", {
|
|
204
|
+
className: "text-muted-foreground/40",
|
|
205
|
+
children: " · "
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ jsxs("span", {
|
|
208
|
+
className: "font-normal",
|
|
209
|
+
children: [
|
|
210
|
+
fmt(dateRange.min),
|
|
211
|
+
" – ",
|
|
212
|
+
fmt(dateRange.max)
|
|
213
|
+
]
|
|
214
|
+
})
|
|
215
|
+
] });
|
|
216
|
+
}
|
|
217
|
+
return /* @__PURE__ */ jsx("span", { children: label });
|
|
218
|
+
}
|
|
219
|
+
default: return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { ChartToolbarOverflow };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ControlId } from "./toolbar-types.mjs";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/chart-toolbar.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Props for ChartToolbar.
|
|
7
|
+
*
|
|
8
|
+
* @property className - Additional CSS classes for the toolbar container
|
|
9
|
+
* @property pinned - Control IDs to always show in the toolbar row (outside the overflow menu)
|
|
10
|
+
* @property hidden - Control IDs to completely hide (not in toolbar, not in overflow)
|
|
11
|
+
*/
|
|
12
|
+
type ChartToolbarProps = {
|
|
13
|
+
/** Additional CSS classes for the toolbar container. */className?: string; /** Control IDs to always show in the toolbar row (outside the overflow menu) */
|
|
14
|
+
pinned?: readonly ControlId[]; /** Control IDs to completely hide (not in toolbar, not in overflow) */
|
|
15
|
+
hidden?: readonly ControlId[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Composable toolbar with pinned controls and an ellipsis overflow menu.
|
|
19
|
+
*
|
|
20
|
+
* Controls are rendered in registry order. Each sub-component still
|
|
21
|
+
* auto-hides when not relevant (e.g. time bucket only shows for date X-axis).
|
|
22
|
+
*
|
|
23
|
+
* @param className - Additional CSS classes for the toolbar container
|
|
24
|
+
* @param pinned - Control IDs to always show in the toolbar row (outside the overflow menu)
|
|
25
|
+
* @param hidden - Control IDs to completely hide (not in toolbar, not in overflow)
|
|
26
|
+
*/
|
|
27
|
+
declare function ChartToolbar({
|
|
28
|
+
className,
|
|
29
|
+
pinned,
|
|
30
|
+
hidden
|
|
31
|
+
}: ChartToolbarProps): react_jsx_runtime0.JSX.Element;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { ChartToolbar };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CONTROL_IDS, CONTROL_REGISTRY } from "./toolbar-types.mjs";
|
|
2
|
+
import { ChartToolbarOverflow } from "./chart-toolbar-overflow.mjs";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/ui/chart-toolbar.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Composable, configurable toolbar with pinned controls and an ellipsis
|
|
8
|
+
* overflow menu
|
|
9
|
+
*
|
|
10
|
+
* Default behavior: the date range stays pinned so the active time window is
|
|
11
|
+
* always visible, while the remaining controls live inside the overflow menu.
|
|
12
|
+
* Developers can pin additional controls to the toolbar row and hide others.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // Date range pinned by default, everything else in overflow
|
|
17
|
+
* <ChartToolbar />
|
|
18
|
+
*
|
|
19
|
+
* // Pin chart type and group-by to the toolbar row
|
|
20
|
+
* <ChartToolbar pinned={['chartType', 'groupBy']} />
|
|
21
|
+
*
|
|
22
|
+
* // Hide time bucket and x-axis entirely
|
|
23
|
+
* <ChartToolbar hidden={['timeBucket', 'xAxis']} />
|
|
24
|
+
*
|
|
25
|
+
* // Combine both
|
|
26
|
+
* <ChartToolbar pinned={['chartType', 'metric']} hidden={['source']} />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_PINNED_CONTROLS = ["dateRange"];
|
|
30
|
+
/**
|
|
31
|
+
* Composable toolbar with pinned controls and an ellipsis overflow menu.
|
|
32
|
+
*
|
|
33
|
+
* Controls are rendered in registry order. Each sub-component still
|
|
34
|
+
* auto-hides when not relevant (e.g. time bucket only shows for date X-axis).
|
|
35
|
+
*
|
|
36
|
+
* @param className - Additional CSS classes for the toolbar container
|
|
37
|
+
* @param pinned - Control IDs to always show in the toolbar row (outside the overflow menu)
|
|
38
|
+
* @param hidden - Control IDs to completely hide (not in toolbar, not in overflow)
|
|
39
|
+
*/
|
|
40
|
+
function ChartToolbar({ className, pinned = DEFAULT_PINNED_CONTROLS, hidden = [] }) {
|
|
41
|
+
const pinnedSet = useMemo(() => new Set(pinned), [pinned]);
|
|
42
|
+
const hiddenSet = useMemo(() => new Set(hidden), [hidden]);
|
|
43
|
+
const pinnedControls = CONTROL_IDS.filter((id) => pinnedSet.has(id) && !hiddenSet.has(id));
|
|
44
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
45
|
+
className: `flex items-start justify-between gap-2 ${className ?? ""}`,
|
|
46
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
47
|
+
className: "flex min-w-0 flex-1 flex-wrap items-center gap-2",
|
|
48
|
+
children: pinnedControls.map((id) => {
|
|
49
|
+
const Component = CONTROL_REGISTRY[id].component;
|
|
50
|
+
return /* @__PURE__ */ jsx(Component, {}, id);
|
|
51
|
+
})
|
|
52
|
+
}), /* @__PURE__ */ jsx(ChartToolbarOverflow, {
|
|
53
|
+
pinned: pinnedSet,
|
|
54
|
+
hidden: hiddenSet,
|
|
55
|
+
className: "shrink-0 self-start"
|
|
56
|
+
})]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { ChartToolbar };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-type-selector.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Chart type selector — inline toggle buttons with variant dropdown.
|
|
6
|
+
*
|
|
7
|
+
* Primary types: Bar, Line, Area, Pie, Donut.
|
|
8
|
+
* Types with variants show a small chevron that opens a dropdown:
|
|
9
|
+
* Bar → Stacked, Grouped, 100%
|
|
10
|
+
* Area → Stacked, 100%
|
|
11
|
+
*/
|
|
12
|
+
/** Inline toggle buttons with variant dropdown for chart type selection. */
|
|
13
|
+
declare function ChartTypeSelector({
|
|
14
|
+
className
|
|
15
|
+
}: {
|
|
16
|
+
className?: string;
|
|
17
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ChartTypeSelector };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartDropdownPanel } from "./chart-dropdown.mjs";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
//#region src/ui/chart-type-selector.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Chart type selector — inline toggle buttons with variant dropdown.
|
|
9
|
+
*
|
|
10
|
+
* Primary types: Bar, Line, Area, Pie, Donut.
|
|
11
|
+
* Types with variants show a small chevron that opens a dropdown:
|
|
12
|
+
* Bar → Stacked, Grouped, 100%
|
|
13
|
+
* Area → Stacked, 100%
|
|
14
|
+
*/
|
|
15
|
+
const CHART_TYPE_GROUPS = [
|
|
16
|
+
{
|
|
17
|
+
primary: "bar",
|
|
18
|
+
label: "Bar",
|
|
19
|
+
variants: [
|
|
20
|
+
{
|
|
21
|
+
type: "bar",
|
|
22
|
+
label: "Stacked"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "grouped-bar",
|
|
26
|
+
label: "Grouped"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "percent-bar",
|
|
30
|
+
label: "100%"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
primary: "line",
|
|
36
|
+
label: "Line",
|
|
37
|
+
variants: []
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
primary: "area",
|
|
41
|
+
label: "Area",
|
|
42
|
+
variants: [{
|
|
43
|
+
type: "area",
|
|
44
|
+
label: "Stacked"
|
|
45
|
+
}, {
|
|
46
|
+
type: "percent-area",
|
|
47
|
+
label: "100%"
|
|
48
|
+
}]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
primary: "pie",
|
|
52
|
+
label: "Pie",
|
|
53
|
+
variants: []
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
primary: "donut",
|
|
57
|
+
label: "Donut",
|
|
58
|
+
variants: []
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
primary: "table",
|
|
62
|
+
label: "Table",
|
|
63
|
+
variants: []
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
function buildVisibleGroups(availableChartTypes) {
|
|
67
|
+
const available = new Set(availableChartTypes);
|
|
68
|
+
const result = [];
|
|
69
|
+
for (const group of CHART_TYPE_GROUPS) if (group.variants.length === 0) {
|
|
70
|
+
if (available.has(group.primary)) result.push({
|
|
71
|
+
...group,
|
|
72
|
+
visibleVariants: []
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
const visibleVariants = group.variants.filter((v) => available.has(v.type));
|
|
76
|
+
if (visibleVariants.length > 0) result.push({
|
|
77
|
+
...group,
|
|
78
|
+
visibleVariants
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function findGroupForType(chartType) {
|
|
84
|
+
return CHART_TYPE_GROUPS.find((g) => g.primary === chartType || g.variants.some((v) => v.type === chartType));
|
|
85
|
+
}
|
|
86
|
+
/** Inline toggle buttons with variant dropdown for chart type selection. */
|
|
87
|
+
function ChartTypeSelector({ className }) {
|
|
88
|
+
const { chartType, setChartType, availableChartTypes } = useChartContext();
|
|
89
|
+
const [openGroup, setOpenGroup] = useState(null);
|
|
90
|
+
const visibleGroups = useMemo(() => buildVisibleGroups(availableChartTypes), [availableChartTypes]);
|
|
91
|
+
const activeGroup = useMemo(() => {
|
|
92
|
+
const staticGroup = findGroupForType(chartType);
|
|
93
|
+
if (!staticGroup) return void 0;
|
|
94
|
+
return visibleGroups.find((g) => g.primary === staticGroup.primary);
|
|
95
|
+
}, [chartType, visibleGroups]);
|
|
96
|
+
if (visibleGroups.length <= 1 && (activeGroup?.visibleVariants.length ?? 0) <= 1) return null;
|
|
97
|
+
return /* @__PURE__ */ jsx("div", {
|
|
98
|
+
className: `inline-flex items-center rounded-lg border border-border/50 bg-background p-0.5 shadow-sm ${className ?? ""}`,
|
|
99
|
+
role: "tablist",
|
|
100
|
+
"aria-label": "Chart type",
|
|
101
|
+
children: visibleGroups.map((group) => {
|
|
102
|
+
const isActive = activeGroup?.primary === group.primary;
|
|
103
|
+
return /* @__PURE__ */ jsx(ChartTypeButton, {
|
|
104
|
+
group,
|
|
105
|
+
isActive,
|
|
106
|
+
hasVariants: group.visibleVariants.length > 1,
|
|
107
|
+
isDropdownOpen: openGroup === group.primary,
|
|
108
|
+
chartType,
|
|
109
|
+
onSelect: () => {
|
|
110
|
+
if (isActive) return;
|
|
111
|
+
if (availableChartTypes.includes(group.primary)) setChartType(group.primary);
|
|
112
|
+
else if (group.visibleVariants.length > 0) setChartType(group.visibleVariants[0].type);
|
|
113
|
+
},
|
|
114
|
+
onToggleDropdown: () => setOpenGroup(openGroup === group.primary ? null : group.primary),
|
|
115
|
+
onSelectVariant: (type) => {
|
|
116
|
+
setChartType(type);
|
|
117
|
+
setOpenGroup(null);
|
|
118
|
+
},
|
|
119
|
+
onCloseDropdown: () => setOpenGroup(null)
|
|
120
|
+
}, group.primary);
|
|
121
|
+
})
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function ChartTypeButton({ group, isActive, hasVariants, isDropdownOpen, chartType, onSelect, onToggleDropdown, onSelectVariant, onCloseDropdown }) {
|
|
125
|
+
const triggerRef = useRef(null);
|
|
126
|
+
if (!hasVariants) return /* @__PURE__ */ jsx("button", {
|
|
127
|
+
role: "tab",
|
|
128
|
+
"aria-selected": isActive,
|
|
129
|
+
onClick: onSelect,
|
|
130
|
+
className: `rounded-md px-2.5 py-1 text-xs font-medium transition-all ${isActive ? "bg-primary/10 text-primary" : "text-muted-foreground hover:bg-muted/40 hover:text-foreground"}`,
|
|
131
|
+
children: group.label
|
|
132
|
+
});
|
|
133
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
134
|
+
ref: triggerRef,
|
|
135
|
+
className: `relative flex items-center rounded-md transition-all ${isActive ? "bg-primary/10" : "hover:bg-muted/40"}`,
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ jsx("button", {
|
|
138
|
+
role: "tab",
|
|
139
|
+
"aria-selected": isActive,
|
|
140
|
+
onClick: () => {
|
|
141
|
+
if (isActive) onToggleDropdown();
|
|
142
|
+
else onSelect();
|
|
143
|
+
},
|
|
144
|
+
className: `py-1 pl-2.5 pr-1 text-xs font-medium transition-colors ${isActive ? "text-primary" : "text-muted-foreground hover:text-foreground"}`,
|
|
145
|
+
children: group.label
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ jsx("button", {
|
|
148
|
+
"aria-label": `${group.label} options`,
|
|
149
|
+
onClick: (e) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
if (!isActive) onSelect();
|
|
152
|
+
onToggleDropdown();
|
|
153
|
+
},
|
|
154
|
+
className: `py-1 pr-2 pl-0.5 transition-colors ${isActive ? "text-primary/60 hover:text-primary" : "text-muted-foreground/40 hover:text-muted-foreground"}`,
|
|
155
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: `h-2.5 w-2.5 transition-transform ${isDropdownOpen ? "rotate-180" : ""}` })
|
|
156
|
+
}),
|
|
157
|
+
/* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
158
|
+
isOpen: isDropdownOpen,
|
|
159
|
+
onClose: onCloseDropdown,
|
|
160
|
+
triggerRef,
|
|
161
|
+
minWidth: "trigger",
|
|
162
|
+
className: "p-1",
|
|
163
|
+
children: group.visibleVariants.map((variant) => /* @__PURE__ */ jsx("button", {
|
|
164
|
+
onClick: () => onSelectVariant(variant.type),
|
|
165
|
+
className: `flex w-full items-center rounded-md px-2.5 py-1.5 text-xs transition-colors ${variant.type === chartType ? "bg-primary/8 font-medium text-primary" : "text-foreground hover:bg-muted/60"}`,
|
|
166
|
+
children: variant.label
|
|
167
|
+
}, variant.type))
|
|
168
|
+
})
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { ChartTypeSelector };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-x-axis-selector.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* X-axis selector — premium custom dropdown for choosing which column drives the X-axis.
|
|
6
|
+
*/
|
|
7
|
+
/** Custom dropdown to select the X-axis column. */
|
|
8
|
+
declare function ChartXAxisSelector({
|
|
9
|
+
className,
|
|
10
|
+
hideIcon
|
|
11
|
+
}: {
|
|
12
|
+
className?: string;
|
|
13
|
+
hideIcon?: boolean;
|
|
14
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ChartXAxisSelector };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartSelect } from "./chart-select.mjs";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { MoveHorizontal } from "lucide-react";
|
|
5
|
+
//#region src/ui/chart-x-axis-selector.tsx
|
|
6
|
+
/**
|
|
7
|
+
* X-axis selector — premium custom dropdown for choosing which column drives the X-axis.
|
|
8
|
+
*/
|
|
9
|
+
/** Custom dropdown to select the X-axis column. */
|
|
10
|
+
function ChartXAxisSelector({ className, hideIcon }) {
|
|
11
|
+
const { xAxisId, setXAxis, availableXAxes } = useChartContext();
|
|
12
|
+
if (availableXAxes.length <= 1) return null;
|
|
13
|
+
const options = availableXAxes.map((col) => ({
|
|
14
|
+
value: col.id,
|
|
15
|
+
label: `X-axis: ${col.label}`
|
|
16
|
+
}));
|
|
17
|
+
return /* @__PURE__ */ jsx(ChartSelect, {
|
|
18
|
+
value: xAxisId ?? "",
|
|
19
|
+
options,
|
|
20
|
+
onChange: (v) => setXAxis(v),
|
|
21
|
+
ariaLabel: "X-axis",
|
|
22
|
+
icon: MoveHorizontal,
|
|
23
|
+
hideIcon,
|
|
24
|
+
className
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { ChartXAxisSelector };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Chart, useChartContext, useTypedChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartCanvas } from "./chart-canvas.mjs";
|
|
3
|
+
import { ControlId } from "./toolbar-types.mjs";
|
|
4
|
+
import { ChartToolbar } from "./chart-toolbar.mjs";
|
|
5
|
+
import { ChartToolbarOverflow } from "./chart-toolbar-overflow.mjs";
|
|
6
|
+
import { ChartSourceSwitcher } from "./chart-source-switcher.mjs";
|
|
7
|
+
import { ChartTypeSelector } from "./chart-type-selector.mjs";
|
|
8
|
+
import { ChartGroupBySelector } from "./chart-group-by-selector.mjs";
|
|
9
|
+
import { ChartTimeBucketSelector } from "./chart-time-bucket-selector.mjs";
|
|
10
|
+
import { ChartMetricSelector } from "./chart-metric-selector.mjs";
|
|
11
|
+
import { ChartMetricPanel } from "./chart-metric-panel.mjs";
|
|
12
|
+
import { ChartXAxisSelector } from "./chart-x-axis-selector.mjs";
|
|
13
|
+
import { ChartDateRange } from "./chart-date-range.mjs";
|
|
14
|
+
import { ChartDateRangeBadge } from "./chart-date-range-badge.mjs";
|
|
15
|
+
import { ChartDateRangePanel } from "./chart-date-range-panel.mjs";
|
|
16
|
+
import { ChartFilters } from "./chart-filters.mjs";
|
|
17
|
+
import { ChartFiltersPanel } from "./chart-filters-panel.mjs";
|
|
18
|
+
import { ChartDebug } from "./chart-debug.mjs";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/ui/percent-stacked.ts
|
|
2
|
+
/**
|
|
3
|
+
* Recharts stacked graphical entries expose [lower, upper] bounds after
|
|
4
|
+
* stackOffset="expand". The visible segment size is upper - lower.
|
|
5
|
+
*/
|
|
6
|
+
function getPercentStackedProportion(entry) {
|
|
7
|
+
const value = entry.value;
|
|
8
|
+
if (Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number") return value[1] - value[0];
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Recharts tooltip payload entries are rebuilt from the raw transformed row,
|
|
13
|
+
* so value stays as the raw metric and payload contains the full bucket.
|
|
14
|
+
*/
|
|
15
|
+
function getPercentStackedProportionFromPayload(payload, dataKey, seriesKeys) {
|
|
16
|
+
if (!payload || typeof payload !== "object") return null;
|
|
17
|
+
const point = payload;
|
|
18
|
+
const rawValue = point[dataKey];
|
|
19
|
+
if (typeof rawValue !== "number") return null;
|
|
20
|
+
let total = 0;
|
|
21
|
+
for (const seriesKey of seriesKeys) {
|
|
22
|
+
const seriesValue = point[seriesKey];
|
|
23
|
+
if (typeof seriesValue === "number") total += seriesValue;
|
|
24
|
+
}
|
|
25
|
+
if (total <= 0) return null;
|
|
26
|
+
return rawValue / total;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Prefer the raw-row calculation because it works for both tooltip payloads
|
|
30
|
+
* and label entries, then fall back to stacked bounds when present.
|
|
31
|
+
*/
|
|
32
|
+
function getPercentStackedDisplayValue(entry, dataKey, seriesKeys) {
|
|
33
|
+
return getPercentStackedProportionFromPayload(entry.payload, dataKey, seriesKeys) ?? getPercentStackedProportion(entry);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { getPercentStackedDisplayValue };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/ui/toolbar-types.d.ts
|
|
2
|
+
/** All valid toolbar control identifiers. */
|
|
3
|
+
declare const CONTROL_IDS: readonly ["source", "xAxis", "chartType", "groupBy", "timeBucket", "metric", "filters", "dateRange"];
|
|
4
|
+
/** Union type of all valid toolbar control identifiers. */
|
|
5
|
+
type ControlId = (typeof CONTROL_IDS)[number];
|
|
6
|
+
//#endregion
|
|
7
|
+
export { ControlId };
|