@matthieumordrel/chart-studio 0.2.3 → 0.2.5
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 +42 -42
- package/dist/core/chart-capabilities.d.mts +15 -0
- package/dist/core/chart-capabilities.mjs +23 -0
- package/dist/core/colors.mjs +5 -5
- package/dist/core/config-utils.mjs +6 -2
- package/dist/core/date-range-presets.d.mts +12 -0
- package/dist/core/date-range-presets.mjs +152 -0
- package/dist/core/define-chart-schema.d.mts +26 -94
- package/dist/core/define-chart-schema.mjs +26 -34
- package/dist/core/infer-columns.d.mts +2 -2
- package/dist/core/infer-columns.mjs +4 -2
- package/dist/core/metric-utils.mjs +13 -5
- package/dist/core/pipeline-data-points.mjs +4 -1
- package/dist/core/schema-builder.mjs +335 -0
- package/dist/core/schema-builder.types.d.mts +279 -0
- package/dist/core/types.d.mts +61 -17
- package/dist/core/use-chart-options.d.mts +7 -4
- package/dist/core/use-chart.d.mts +4 -4
- package/dist/core/use-chart.mjs +70 -40
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/ui/chart-canvas.d.mts +8 -4
- package/dist/ui/chart-canvas.mjs +347 -29
- package/dist/ui/chart-context.d.mts +11 -4
- package/dist/ui/chart-context.mjs +3 -0
- package/dist/ui/chart-date-range-badge.mjs +2 -2
- package/dist/ui/chart-date-range-panel.mjs +19 -101
- package/dist/ui/chart-date-range.mjs +3 -3
- package/dist/ui/chart-debug.d.mts +6 -2
- package/dist/ui/chart-debug.mjs +5 -1
- package/dist/ui/chart-group-by-selector.d.mts +3 -1
- package/dist/ui/chart-group-by-selector.mjs +4 -1
- package/dist/ui/chart-metric-selector.mjs +2 -2
- package/dist/ui/chart-select.mjs +9 -10
- package/dist/ui/chart-source-switcher.d.mts +3 -1
- package/dist/ui/chart-source-switcher.mjs +4 -2
- package/dist/ui/chart-time-bucket-selector.d.mts +3 -1
- package/dist/ui/chart-time-bucket-selector.mjs +4 -1
- package/dist/ui/chart-toolbar-overflow.mjs +48 -26
- package/dist/ui/chart-toolbar.d.mts +6 -2
- package/dist/ui/chart-toolbar.mjs +4 -0
- package/dist/ui/chart-type-selector.d.mts +7 -2
- package/dist/ui/chart-type-selector.mjs +155 -20
- package/dist/ui/chart-x-axis-selector.d.mts +3 -1
- package/dist/ui/chart-x-axis-selector.mjs +4 -1
- package/dist/ui/percent-stacked.mjs +36 -0
- package/dist/ui/theme.css +54 -49
- package/package.json +7 -6
|
@@ -1,86 +1,14 @@
|
|
|
1
|
+
import { DATE_RANGE_PRESETS, getPresetLabel } from "../core/date-range-presets.mjs";
|
|
1
2
|
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/ui/chart-date-range-panel.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Date range panel content — reusable by both ChartDateRange (inside a popover)
|
|
7
7
|
* and ChartToolbarOverflow (rendered inline).
|
|
8
8
|
*
|
|
9
|
-
* Shows preset buttons (All time, Last 7 days, etc.), a reference date
|
|
9
|
+
* Shows preset buttons (Auto, All time, Last 7 days, etc.), a reference date
|
|
10
10
|
* column picker, and custom date inputs.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* Build the list of date range presets relative to "now".
|
|
14
|
-
*
|
|
15
|
-
* "All time" → null (no date filtering)
|
|
16
|
-
* Other presets → { from: Date, to: null } (bounded filter)
|
|
17
|
-
*/
|
|
18
|
-
function getPresets() {
|
|
19
|
-
return [
|
|
20
|
-
{
|
|
21
|
-
label: "All time",
|
|
22
|
-
buildFilter: () => null
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: "Last 7 days",
|
|
26
|
-
buildFilter: () => ({
|
|
27
|
-
from: daysAgo(7),
|
|
28
|
-
to: null
|
|
29
|
-
})
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
label: "Last 30 days",
|
|
33
|
-
buildFilter: () => ({
|
|
34
|
-
from: daysAgo(30),
|
|
35
|
-
to: null
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: "Last 3 months",
|
|
40
|
-
buildFilter: () => ({
|
|
41
|
-
from: monthsAgo(3),
|
|
42
|
-
to: null
|
|
43
|
-
})
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
label: "Last 6 months",
|
|
47
|
-
buildFilter: () => ({
|
|
48
|
-
from: monthsAgo(6),
|
|
49
|
-
to: null
|
|
50
|
-
})
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
label: "Last 12 months",
|
|
54
|
-
buildFilter: () => ({
|
|
55
|
-
from: monthsAgo(12),
|
|
56
|
-
to: null
|
|
57
|
-
})
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
label: "Year to date",
|
|
61
|
-
buildFilter: () => ({
|
|
62
|
-
from: startOfYear(),
|
|
63
|
-
to: null
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
function daysAgo(n) {
|
|
69
|
-
const d = /* @__PURE__ */ new Date();
|
|
70
|
-
d.setDate(d.getDate() - n);
|
|
71
|
-
d.setHours(0, 0, 0, 0);
|
|
72
|
-
return d;
|
|
73
|
-
}
|
|
74
|
-
function monthsAgo(n) {
|
|
75
|
-
const d = /* @__PURE__ */ new Date();
|
|
76
|
-
d.setMonth(d.getMonth() - n);
|
|
77
|
-
d.setHours(0, 0, 0, 0);
|
|
78
|
-
return d;
|
|
79
|
-
}
|
|
80
|
-
function startOfYear() {
|
|
81
|
-
const d = /* @__PURE__ */ new Date();
|
|
82
|
-
return new Date(d.getFullYear(), 0, 1);
|
|
83
|
-
}
|
|
84
12
|
/** Format a Date as YYYY-MM-DD for native date input value. */
|
|
85
13
|
function toInputValue(date) {
|
|
86
14
|
if (!date) return "";
|
|
@@ -93,24 +21,14 @@ function fromInputValue(value) {
|
|
|
93
21
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
94
22
|
}
|
|
95
23
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
24
|
+
* Resolve the display label for the current date range state.
|
|
25
|
+
*
|
|
26
|
+
* When a preset is active, returns the preset label.
|
|
27
|
+
* When no preset is active (custom range), returns "Custom".
|
|
99
28
|
*/
|
|
100
|
-
function resolvePresetLabel(
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
for (const preset of presets) {
|
|
104
|
-
const pf = preset.buildFilter();
|
|
105
|
-
if (pf === null) continue;
|
|
106
|
-
if (sameDay(pf.from, filter.from) && sameDay(pf.to, filter.to)) return preset.label;
|
|
107
|
-
}
|
|
108
|
-
return "Custom";
|
|
109
|
-
}
|
|
110
|
-
function sameDay(a, b) {
|
|
111
|
-
if (!a && !b) return true;
|
|
112
|
-
if (!a || !b) return false;
|
|
113
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
29
|
+
function resolvePresetLabel(dateRangePreset) {
|
|
30
|
+
if (dateRangePreset === null) return "Custom";
|
|
31
|
+
return getPresetLabel(dateRangePreset);
|
|
114
32
|
}
|
|
115
33
|
/**
|
|
116
34
|
* Date range panel content (no popover wrapper).
|
|
@@ -119,13 +37,11 @@ function sameDay(a, b) {
|
|
|
119
37
|
* @property className - Additional CSS classes
|
|
120
38
|
*/
|
|
121
39
|
function ChartDateRangePanel({ onClose, className }) {
|
|
122
|
-
const { dateRangeFilter, setDateRangeFilter, referenceDateId, setReferenceDateId, availableDateColumns } = useChartContext();
|
|
123
|
-
const presets = useMemo(() => getPresets(), []);
|
|
124
|
-
const activeLabel = resolvePresetLabel(dateRangeFilter);
|
|
40
|
+
const { dateRangePreset, setDateRangePreset, dateRangeFilter, setDateRangeFilter, referenceDateId, setReferenceDateId, availableDateColumns } = useChartContext();
|
|
125
41
|
const hasMultipleDateColumns = availableDateColumns.length > 1;
|
|
126
|
-
const handlePreset = (
|
|
127
|
-
|
|
128
|
-
|
|
42
|
+
const handlePreset = (presetId) => {
|
|
43
|
+
setDateRangePreset(presetId);
|
|
44
|
+
onClose?.();
|
|
129
45
|
};
|
|
130
46
|
const handleCustomFrom = (value) => {
|
|
131
47
|
setDateRangeFilter({
|
|
@@ -166,12 +82,14 @@ function ChartDateRangePanel({ onClose, className }) {
|
|
|
166
82
|
children: "Range"
|
|
167
83
|
}), /* @__PURE__ */ jsx("div", {
|
|
168
84
|
className: "grid grid-cols-2 gap-1",
|
|
169
|
-
children:
|
|
85
|
+
children: DATE_RANGE_PRESETS.map((preset) => {
|
|
86
|
+
const isActive = dateRangePreset === preset.id;
|
|
170
87
|
return /* @__PURE__ */ jsx("button", {
|
|
171
|
-
onClick: () => handlePreset(preset),
|
|
172
|
-
|
|
88
|
+
onClick: () => handlePreset(preset.id),
|
|
89
|
+
title: preset.description,
|
|
90
|
+
className: `rounded-md px-2 py-1.5 text-left text-xs transition-colors ${isActive ? "bg-primary/10 font-medium text-primary" : "text-foreground hover:bg-muted"}`,
|
|
173
91
|
children: preset.label
|
|
174
|
-
}, preset.
|
|
92
|
+
}, preset.id);
|
|
175
93
|
})
|
|
176
94
|
})]
|
|
177
95
|
}),
|
|
@@ -24,12 +24,12 @@ function formatDate(date) {
|
|
|
24
24
|
* Also serves as the reference date column picker when multiple date columns exist.
|
|
25
25
|
*/
|
|
26
26
|
function ChartDateRange({ className }) {
|
|
27
|
-
const { dateRange,
|
|
27
|
+
const { dateRange, dateRangePreset, availableDateColumns } = useChartContext();
|
|
28
28
|
const [isOpen, setIsOpen] = useState(false);
|
|
29
29
|
const triggerRef = useRef(null);
|
|
30
30
|
if (availableDateColumns.length === 0) return null;
|
|
31
|
-
const activeLabel = resolvePresetLabel(
|
|
32
|
-
const isFiltered =
|
|
31
|
+
const activeLabel = resolvePresetLabel(dateRangePreset);
|
|
32
|
+
const isFiltered = dateRangePreset !== "all-time";
|
|
33
33
|
const hasRange = dateRange?.min && dateRange?.max;
|
|
34
34
|
return /* @__PURE__ */ jsxs("div", {
|
|
35
35
|
className,
|
|
@@ -5,12 +5,16 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
5
5
|
* Debug panel — shows raw data, transformed data, series, and current state.
|
|
6
6
|
* Drop `<ChartDebug />` inside a `<Chart>` to inspect what's happening.
|
|
7
7
|
*/
|
|
8
|
-
/** Debug panel that renders chart internals as formatted JSON.
|
|
8
|
+
/** Debug panel that renders chart internals as formatted JSON.
|
|
9
|
+
*
|
|
10
|
+
* @param className - Additional CSS classes for the debug panel.
|
|
11
|
+
* @param defaultOpen - Whether the debug panel should be open by default. (default: false)
|
|
12
|
+
*/
|
|
9
13
|
declare function ChartDebug({
|
|
10
14
|
className,
|
|
11
15
|
defaultOpen
|
|
12
16
|
}: {
|
|
13
|
-
className?: string;
|
|
17
|
+
/** Additional CSS classes for the debug panel. */className?: string; /** Whether the debug panel should be open by default. (default: false) */
|
|
14
18
|
defaultOpen?: boolean;
|
|
15
19
|
}): react_jsx_runtime0.JSX.Element;
|
|
16
20
|
//#endregion
|
package/dist/ui/chart-debug.mjs
CHANGED
|
@@ -112,7 +112,11 @@ function TabButton({ tab, isActive, onClick }) {
|
|
|
112
112
|
children: tab.label
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
/** Debug panel that renders chart internals as formatted JSON.
|
|
115
|
+
/** Debug panel that renders chart internals as formatted JSON.
|
|
116
|
+
*
|
|
117
|
+
* @param className - Additional CSS classes for the debug panel.
|
|
118
|
+
* @param defaultOpen - Whether the debug panel should be open by default. (default: false)
|
|
119
|
+
*/
|
|
116
120
|
function ChartDebug({ className, defaultOpen = false }) {
|
|
117
121
|
const chart = useChartContext();
|
|
118
122
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
@@ -6,9 +6,11 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
/** Custom dropdown to select the groupBy column. */
|
|
8
8
|
declare function ChartGroupBySelector({
|
|
9
|
-
className
|
|
9
|
+
className,
|
|
10
|
+
hideIcon
|
|
10
11
|
}: {
|
|
11
12
|
className?: string;
|
|
13
|
+
hideIcon?: boolean;
|
|
12
14
|
}): react_jsx_runtime0.JSX.Element | null;
|
|
13
15
|
//#endregion
|
|
14
16
|
export { ChartGroupBySelector };
|
|
@@ -2,12 +2,13 @@ import { CHART_TYPE_CONFIG } from "../core/chart-capabilities.mjs";
|
|
|
2
2
|
import { useChartContext } from "./chart-context.mjs";
|
|
3
3
|
import { ChartSelect } from "./chart-select.mjs";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { Layers } from "lucide-react";
|
|
5
6
|
//#region src/ui/chart-group-by-selector.tsx
|
|
6
7
|
/**
|
|
7
8
|
* GroupBy selector — premium custom dropdown replacing native <select>.
|
|
8
9
|
*/
|
|
9
10
|
/** Custom dropdown to select the groupBy column. */
|
|
10
|
-
function ChartGroupBySelector({ className }) {
|
|
11
|
+
function ChartGroupBySelector({ className, hideIcon }) {
|
|
11
12
|
const { chartType, groupById, setGroupBy, availableGroupBys } = useChartContext();
|
|
12
13
|
if (!CHART_TYPE_CONFIG[chartType].supportsGrouping || availableGroupBys.length === 0) return null;
|
|
13
14
|
const options = [{
|
|
@@ -22,6 +23,8 @@ function ChartGroupBySelector({ className }) {
|
|
|
22
23
|
options,
|
|
23
24
|
onChange: (v) => setGroupBy(v || null),
|
|
24
25
|
ariaLabel: "Group by",
|
|
26
|
+
icon: Layers,
|
|
27
|
+
hideIcon,
|
|
25
28
|
className
|
|
26
29
|
});
|
|
27
30
|
}
|
|
@@ -4,7 +4,7 @@ import { ChartDropdownPanel } from "./chart-dropdown.mjs";
|
|
|
4
4
|
import { ChartMetricPanel } from "./chart-metric-panel.mjs";
|
|
5
5
|
import { useRef, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { ChevronDown,
|
|
7
|
+
import { ChevronDown, MoveVertical } from "lucide-react";
|
|
8
8
|
//#region src/ui/chart-metric-selector.tsx
|
|
9
9
|
/**
|
|
10
10
|
* Metric selector — trigger button + popover wrapping ChartMetricPanel.
|
|
@@ -28,7 +28,7 @@ function ChartMetricSelector({ className }) {
|
|
|
28
28
|
className: `inline-flex h-7 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20 ${isActive ? "border-primary/30 bg-primary/5 text-primary shadow-sm shadow-primary/5 hover:bg-primary/8" : "border-border/50 bg-background text-muted-foreground shadow-sm hover:border-border hover:bg-muted/30 hover:shadow hover:text-foreground"}`,
|
|
29
29
|
"aria-label": "Metric",
|
|
30
30
|
children: [
|
|
31
|
-
/* @__PURE__ */ jsx(
|
|
31
|
+
/* @__PURE__ */ jsx(MoveVertical, { className: "h-3 w-3" }),
|
|
32
32
|
/* @__PURE__ */ jsx("span", { children: label }),
|
|
33
33
|
isActive && !includeZeros && /* @__PURE__ */ jsx("span", {
|
|
34
34
|
className: "rounded bg-muted px-1 py-px text-[9px] font-normal text-muted-foreground",
|
package/dist/ui/chart-select.mjs
CHANGED
|
@@ -4,11 +4,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
import { ChevronDown } from "lucide-react";
|
|
5
5
|
//#region src/ui/chart-select.tsx
|
|
6
6
|
/**
|
|
7
|
-
* Custom select dropdown — premium replacement for native <select> elements.
|
|
8
|
-
* Uses fixed positioning so the options list works correctly even inside
|
|
9
|
-
* overflow-hidden containers (e.g. the toolbar overflow panel).
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
7
|
* Premium styled select dropdown with highlight-only selection styling.
|
|
13
8
|
*
|
|
14
9
|
* @property value - Currently selected value
|
|
@@ -17,7 +12,7 @@ import { ChevronDown } from "lucide-react";
|
|
|
17
12
|
* @property ariaLabel - Accessible label for the trigger
|
|
18
13
|
* @property className - Additional CSS classes
|
|
19
14
|
*/
|
|
20
|
-
function ChartSelect({ value, options, onChange, ariaLabel, className }) {
|
|
15
|
+
function ChartSelect({ value, options, onChange, ariaLabel, icon: Icon, hideIcon, className }) {
|
|
21
16
|
const [isOpen, setIsOpen] = useState(false);
|
|
22
17
|
const triggerRef = useRef(null);
|
|
23
18
|
const selected = options.find((o) => o.value === value);
|
|
@@ -37,10 +32,14 @@ function ChartSelect({ value, options, onChange, ariaLabel, className }) {
|
|
|
37
32
|
onClick: handleToggle,
|
|
38
33
|
className: "inline-flex h-7 items-center gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 text-xs font-medium text-foreground shadow-sm transition-all hover:border-border hover:bg-muted/30 hover:shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20",
|
|
39
34
|
"aria-label": ariaLabel,
|
|
40
|
-
children: [
|
|
41
|
-
className: "
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
children: [
|
|
36
|
+
Icon && !hideIcon && /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
37
|
+
/* @__PURE__ */ jsx("span", {
|
|
38
|
+
className: "truncate",
|
|
39
|
+
children: selected?.label ?? value
|
|
40
|
+
}),
|
|
41
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: `h-3 w-3 shrink-0 text-muted-foreground/60 transition-transform ${isOpen ? "rotate-180" : ""}` })
|
|
42
|
+
]
|
|
44
43
|
}), /* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
45
44
|
isOpen,
|
|
46
45
|
onClose: () => setIsOpen(false),
|
|
@@ -14,9 +14,11 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
14
14
|
* - Multi source → dropdown to switch between sources
|
|
15
15
|
*/
|
|
16
16
|
declare function ChartSourceSwitcher({
|
|
17
|
-
className
|
|
17
|
+
className,
|
|
18
|
+
hideIcon
|
|
18
19
|
}: {
|
|
19
20
|
className?: string;
|
|
21
|
+
hideIcon?: boolean;
|
|
20
22
|
}): react_jsx_runtime0.JSX.Element;
|
|
21
23
|
//#endregion
|
|
22
24
|
export { ChartSourceSwitcher };
|
|
@@ -19,14 +19,14 @@ function formatCount(n) {
|
|
|
19
19
|
* - Single source → read-only badge: "[icon] Jobs · 1,247 records"
|
|
20
20
|
* - Multi source → dropdown to switch between sources
|
|
21
21
|
*/
|
|
22
|
-
function ChartSourceSwitcher({ className }) {
|
|
22
|
+
function ChartSourceSwitcher({ className, hideIcon }) {
|
|
23
23
|
const { hasMultipleSources, sources, activeSourceId, setActiveSource, recordCount } = useChartContext();
|
|
24
24
|
if (!hasMultipleSources) {
|
|
25
25
|
const label = sources[0]?.label ?? "Unnamed Source";
|
|
26
26
|
return /* @__PURE__ */ jsxs("div", {
|
|
27
27
|
className: `inline-flex items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground ${className ?? ""}`,
|
|
28
28
|
children: [
|
|
29
|
-
/* @__PURE__ */ jsx(Database, { className: "h-3 w-3 shrink-0" }),
|
|
29
|
+
!hideIcon && /* @__PURE__ */ jsx(Database, { className: "h-3 w-3 shrink-0" }),
|
|
30
30
|
/* @__PURE__ */ jsx("span", {
|
|
31
31
|
className: "font-medium text-foreground",
|
|
32
32
|
children: label
|
|
@@ -47,6 +47,8 @@ function ChartSourceSwitcher({ className }) {
|
|
|
47
47
|
})),
|
|
48
48
|
onChange: (v) => setActiveSource(v),
|
|
49
49
|
ariaLabel: "Data source",
|
|
50
|
+
icon: Database,
|
|
51
|
+
hideIcon,
|
|
50
52
|
className
|
|
51
53
|
});
|
|
52
54
|
}
|
|
@@ -7,9 +7,11 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
7
7
|
*/
|
|
8
8
|
/** Custom dropdown to select time granularity. */
|
|
9
9
|
declare function ChartTimeBucketSelector({
|
|
10
|
-
className
|
|
10
|
+
className,
|
|
11
|
+
hideIcon
|
|
11
12
|
}: {
|
|
12
13
|
className?: string;
|
|
14
|
+
hideIcon?: boolean;
|
|
13
15
|
}): react_jsx_runtime0.JSX.Element | null;
|
|
14
16
|
//#endregion
|
|
15
17
|
export { ChartTimeBucketSelector };
|
|
@@ -2,6 +2,7 @@ import { CHART_TYPE_CONFIG } from "../core/chart-capabilities.mjs";
|
|
|
2
2
|
import { useChartContext } from "./chart-context.mjs";
|
|
3
3
|
import { ChartSelect } from "./chart-select.mjs";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { Clock } from "lucide-react";
|
|
5
6
|
//#region src/ui/chart-time-bucket-selector.tsx
|
|
6
7
|
/**
|
|
7
8
|
* Time bucket selector — premium custom dropdown.
|
|
@@ -16,7 +17,7 @@ const BUCKET_LABELS = {
|
|
|
16
17
|
year: "Year"
|
|
17
18
|
};
|
|
18
19
|
/** Custom dropdown to select time granularity. */
|
|
19
|
-
function ChartTimeBucketSelector({ className }) {
|
|
20
|
+
function ChartTimeBucketSelector({ className, hideIcon }) {
|
|
20
21
|
const { chartType, isTimeSeries, timeBucket, setTimeBucket, availableTimeBuckets } = useChartContext();
|
|
21
22
|
if (!isTimeSeries || !CHART_TYPE_CONFIG[chartType].supportsTimeBucketing || availableTimeBuckets.length === 0) return null;
|
|
22
23
|
return /* @__PURE__ */ jsx(ChartSelect, {
|
|
@@ -27,6 +28,8 @@ function ChartTimeBucketSelector({ className }) {
|
|
|
27
28
|
})),
|
|
28
29
|
onChange: setTimeBucket,
|
|
29
30
|
ariaLabel: "Time granularity",
|
|
31
|
+
icon: Clock,
|
|
32
|
+
hideIcon,
|
|
30
33
|
className
|
|
31
34
|
});
|
|
32
35
|
}
|
|
@@ -7,22 +7,25 @@ import { ChartMetricPanel } from "./chart-metric-panel.mjs";
|
|
|
7
7
|
import { CONTROL_IDS, CONTROL_REGISTRY, SECTIONS } from "./toolbar-types.mjs";
|
|
8
8
|
import { useRef, useState } from "react";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { ArrowLeft, ChevronRight, Ellipsis, Eraser } from "lucide-react";
|
|
10
|
+
import { ArrowLeft, BarChart3, Calendar, ChevronRight, Clock, Database, Ellipsis, Eraser, Filter, Layers, MoveHorizontal, MoveVertical } from "lucide-react";
|
|
11
11
|
//#region src/ui/chart-toolbar-overflow.tsx
|
|
12
|
-
/**
|
|
13
|
-
* Overflow menu for toolbar controls — Notion-inspired configuration panel.
|
|
14
|
-
*
|
|
15
|
-
* Renders an ellipsis button that opens a panel with all non-pinned controls.
|
|
16
|
-
* Simple controls (selects, toggle buttons) render inline in rows.
|
|
17
|
-
* Complex controls (metric, filters, date range) show as clickable rows that
|
|
18
|
-
* navigate to a full-page detail view within the panel (Notion-style drill-down).
|
|
19
|
-
*/
|
|
20
12
|
/** Controls that drill-down into a detail page instead of rendering inline. */
|
|
21
13
|
const COMPLEX_CONTROLS = new Set([
|
|
22
14
|
"metric",
|
|
23
15
|
"filters",
|
|
24
16
|
"dateRange"
|
|
25
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
|
+
};
|
|
26
29
|
/**
|
|
27
30
|
* Ellipsis overflow menu with Notion-style drill-down navigation.
|
|
28
31
|
* Main view shows all controls. Clicking a complex control replaces
|
|
@@ -157,16 +160,13 @@ function DetailPage({ controlId, onBack }) {
|
|
|
157
160
|
* Used for selects and toggle buttons that work inline without popovers.
|
|
158
161
|
*/
|
|
159
162
|
function SimpleControlRow({ controlId }) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
163
|
+
const Component = CONTROL_REGISTRY[controlId].component;
|
|
164
|
+
const Icon = CONTROL_ICONS[controlId];
|
|
162
165
|
return /* @__PURE__ */ jsxs("div", {
|
|
163
|
-
className: "flex items-center gap-3 rounded-lg px-
|
|
164
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
165
|
-
className: "
|
|
166
|
-
children:
|
|
167
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
168
|
-
className: "ml-auto flex min-w-0 items-center",
|
|
169
|
-
children: /* @__PURE__ */ jsx(Component, {})
|
|
166
|
+
className: "flex items-center gap-3 rounded-lg px-2 py-1.5",
|
|
167
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("div", {
|
|
168
|
+
className: "flex min-w-0 items-center",
|
|
169
|
+
children: /* @__PURE__ */ jsx(Component, { hideIcon: true })
|
|
170
170
|
})]
|
|
171
171
|
});
|
|
172
172
|
}
|
|
@@ -175,17 +175,14 @@ function SimpleControlRow({ controlId }) {
|
|
|
175
175
|
* Shows the control label, current value summary, and a chevron.
|
|
176
176
|
*/
|
|
177
177
|
function ComplexControlRow({ controlId, onNavigate }) {
|
|
178
|
-
const
|
|
178
|
+
const Icon = CONTROL_ICONS[controlId];
|
|
179
179
|
return /* @__PURE__ */ jsxs("button", {
|
|
180
180
|
onClick: onNavigate,
|
|
181
181
|
className: "flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted/50",
|
|
182
182
|
children: [
|
|
183
|
+
Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
183
184
|
/* @__PURE__ */ jsx("div", {
|
|
184
|
-
className: "
|
|
185
|
-
children: entry.label
|
|
186
|
-
}),
|
|
187
|
-
/* @__PURE__ */ jsx("div", {
|
|
188
|
-
className: "ml-auto truncate text-xs text-foreground",
|
|
185
|
+
className: "truncate text-xs text-foreground",
|
|
189
186
|
children: /* @__PURE__ */ jsx(ControlSummary, { controlId })
|
|
190
187
|
}),
|
|
191
188
|
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })
|
|
@@ -194,14 +191,39 @@ function ComplexControlRow({ controlId, onNavigate }) {
|
|
|
194
191
|
}
|
|
195
192
|
/** Summary text for a complex control in the main menu. */
|
|
196
193
|
function ControlSummary({ controlId }) {
|
|
197
|
-
const { metric, columns, filters,
|
|
194
|
+
const { metric, columns, filters, dateRange, dateRangePreset } = useChartContext();
|
|
198
195
|
switch (controlId) {
|
|
199
196
|
case "metric": return /* @__PURE__ */ jsx("span", { children: getMetricLabel(metric, columns) });
|
|
200
197
|
case "filters": {
|
|
201
198
|
const count = [...filters.values()].reduce((sum, set) => sum + set.size, 0);
|
|
202
199
|
return /* @__PURE__ */ jsx("span", { children: count > 0 ? `${count} active` : "None" });
|
|
203
200
|
}
|
|
204
|
-
case "dateRange":
|
|
201
|
+
case "dateRange": {
|
|
202
|
+
const label = resolvePresetLabel(dateRangePreset);
|
|
203
|
+
if (dateRange?.min && dateRange?.max) {
|
|
204
|
+
const fmt = (d) => d.toLocaleDateString("en-US", {
|
|
205
|
+
month: "short",
|
|
206
|
+
day: "numeric",
|
|
207
|
+
year: "2-digit"
|
|
208
|
+
});
|
|
209
|
+
return /* @__PURE__ */ jsxs("span", { children: [
|
|
210
|
+
label,
|
|
211
|
+
/* @__PURE__ */ jsx("span", {
|
|
212
|
+
className: "text-muted-foreground/40",
|
|
213
|
+
children: " · "
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ jsxs("span", {
|
|
216
|
+
className: "font-normal",
|
|
217
|
+
children: [
|
|
218
|
+
fmt(dateRange.min),
|
|
219
|
+
" – ",
|
|
220
|
+
fmt(dateRange.max)
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
] });
|
|
224
|
+
}
|
|
225
|
+
return /* @__PURE__ */ jsx("span", { children: label });
|
|
226
|
+
}
|
|
205
227
|
default: return null;
|
|
206
228
|
}
|
|
207
229
|
}
|
|
@@ -10,8 +10,8 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
10
10
|
* @property hidden - Control IDs to completely hide (not in toolbar, not in overflow)
|
|
11
11
|
*/
|
|
12
12
|
type ChartToolbarProps = {
|
|
13
|
-
className?: string;
|
|
14
|
-
pinned?: readonly ControlId[];
|
|
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
15
|
hidden?: readonly ControlId[];
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
@@ -19,6 +19,10 @@ type ChartToolbarProps = {
|
|
|
19
19
|
*
|
|
20
20
|
* Controls are rendered in registry order. Each sub-component still
|
|
21
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)
|
|
22
26
|
*/
|
|
23
27
|
declare function ChartToolbar({
|
|
24
28
|
className,
|
|
@@ -32,6 +32,10 @@ const DEFAULT_PINNED_CONTROLS = ["dateRange"];
|
|
|
32
32
|
*
|
|
33
33
|
* Controls are rendered in registry order. Each sub-component still
|
|
34
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)
|
|
35
39
|
*/
|
|
36
40
|
function ChartToolbar({ className, pinned = DEFAULT_PINNED_CONTROLS, hidden = [] }) {
|
|
37
41
|
const pinnedSet = useMemo(() => new Set(pinned), [pinned]);
|
|
@@ -2,9 +2,14 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
2
2
|
|
|
3
3
|
//#region src/ui/chart-type-selector.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* Chart type selector — inline toggle buttons
|
|
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%
|
|
6
11
|
*/
|
|
7
|
-
/** Inline toggle buttons
|
|
12
|
+
/** Inline toggle buttons with variant dropdown for chart type selection. */
|
|
8
13
|
declare function ChartTypeSelector({
|
|
9
14
|
className
|
|
10
15
|
}: {
|