@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,25 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-metric-panel.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Metric panel content — reusable by both ChartMetricSelector (inside a popover)
|
|
6
|
+
* and ChartToolbarOverflow (rendered inline).
|
|
7
|
+
*
|
|
8
|
+
* Shows a "Count" option plus grouped number columns with aggregate buttons
|
|
9
|
+
* (Sum, Avg, Min, Max) and an "Exclude zeros" toggle per column.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Metric panel content (no popover wrapper).
|
|
13
|
+
*
|
|
14
|
+
* @property onClose - Optional callback when user makes a definitive selection (e.g. "Count")
|
|
15
|
+
* @property className - Additional CSS classes
|
|
16
|
+
*/
|
|
17
|
+
declare function ChartMetricPanel({
|
|
18
|
+
onClose,
|
|
19
|
+
className
|
|
20
|
+
}: {
|
|
21
|
+
onClose?: () => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
}): react_jsx_runtime0.JSX.Element;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ChartMetricPanel };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { DEFAULT_METRIC, isAggregateMetric } from "@matthieumordrel/chart-studio/_internal";
|
|
5
|
+
import { ArrowDownToLine, ArrowUpToLine, Divide, Hash, Sigma } from "lucide-react";
|
|
6
|
+
//#region src/ui/chart-metric-panel.tsx
|
|
7
|
+
const AGGREGATE_OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
fn: "sum",
|
|
10
|
+
label: "Sum",
|
|
11
|
+
shortLabel: "Sum",
|
|
12
|
+
icon: /* @__PURE__ */ jsx(Sigma, { className: "h-3.5 w-3.5 shrink-0" })
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
fn: "avg",
|
|
16
|
+
label: "Average",
|
|
17
|
+
shortLabel: "Avg",
|
|
18
|
+
icon: /* @__PURE__ */ jsx(Divide, { className: "h-3.5 w-3.5 shrink-0" })
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
fn: "min",
|
|
22
|
+
label: "Minimum",
|
|
23
|
+
shortLabel: "Min",
|
|
24
|
+
icon: /* @__PURE__ */ jsx(ArrowDownToLine, { className: "h-3.5 w-3.5 shrink-0" })
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
fn: "max",
|
|
28
|
+
label: "Maximum",
|
|
29
|
+
shortLabel: "Max",
|
|
30
|
+
icon: /* @__PURE__ */ jsx(ArrowUpToLine, { className: "h-3.5 w-3.5 shrink-0" })
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
/** Aggregates where the "Exclude zeros" toggle is relevant */
|
|
34
|
+
const ZERO_TOGGLE_AGGREGATES = new Set([
|
|
35
|
+
"avg",
|
|
36
|
+
"min",
|
|
37
|
+
"max"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Extract number columns from the chart column list.
|
|
41
|
+
*/
|
|
42
|
+
function getNumberColumns(columns) {
|
|
43
|
+
return columns.filter((column) => column.type === "number");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Group the allowed aggregate metrics by number column while preserving column order.
|
|
47
|
+
*/
|
|
48
|
+
function getMetricColumnGroups(availableMetrics, columns) {
|
|
49
|
+
const aggregateMap = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const metric of availableMetrics) {
|
|
51
|
+
if (metric.kind !== "aggregate") continue;
|
|
52
|
+
const aggregates = aggregateMap.get(metric.columnId) ?? /* @__PURE__ */ new Set();
|
|
53
|
+
aggregates.add(metric.aggregate);
|
|
54
|
+
aggregateMap.set(metric.columnId, aggregates);
|
|
55
|
+
}
|
|
56
|
+
return getNumberColumns(columns).filter((column) => aggregateMap.has(column.id)).map((column) => ({
|
|
57
|
+
columnId: column.id,
|
|
58
|
+
label: column.label,
|
|
59
|
+
aggregates: [...aggregateMap.get(column.id) ?? /* @__PURE__ */ new Set()]
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/** A single number column section with aggregate buttons + optional zero toggle. */
|
|
63
|
+
function MetricColumnGroup({ group, isActiveColumn, activeAggregate, includeZeros, onSelectAggregate, onToggleZeros }) {
|
|
64
|
+
const isZeroToggleEnabled = isActiveColumn && activeAggregate !== null && ZERO_TOGGLE_AGGREGATES.has(activeAggregate);
|
|
65
|
+
const isZeroToggleDisabled = isActiveColumn && activeAggregate === "sum";
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
67
|
+
className: "space-y-2",
|
|
68
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
69
|
+
className: "flex items-center justify-between",
|
|
70
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
71
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
72
|
+
children: group.label
|
|
73
|
+
}), isActiveColumn && /* @__PURE__ */ jsxs("button", {
|
|
74
|
+
onClick: isZeroToggleDisabled ? void 0 : onToggleZeros,
|
|
75
|
+
className: `flex items-center gap-1.5 text-[10px] transition-colors ${isZeroToggleDisabled ? "cursor-not-allowed text-muted-foreground/30" : "text-muted-foreground hover:text-foreground"}`,
|
|
76
|
+
disabled: isZeroToggleDisabled,
|
|
77
|
+
title: isZeroToggleDisabled ? "Not applicable for Sum" : "Exclude zero values from calculation",
|
|
78
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
79
|
+
className: `flex h-3 w-3 shrink-0 items-center justify-center rounded-sm border transition-colors ${isZeroToggleEnabled && !includeZeros ? "border-primary bg-primary" : isZeroToggleDisabled ? "border-muted-foreground/20" : "border-muted-foreground/30"}`,
|
|
80
|
+
children: isZeroToggleEnabled && !includeZeros && /* @__PURE__ */ jsx("svg", {
|
|
81
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
82
|
+
width: "8",
|
|
83
|
+
height: "8",
|
|
84
|
+
viewBox: "0 0 24 24",
|
|
85
|
+
fill: "none",
|
|
86
|
+
stroke: "white",
|
|
87
|
+
strokeWidth: "3",
|
|
88
|
+
strokeLinecap: "round",
|
|
89
|
+
strokeLinejoin: "round",
|
|
90
|
+
children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
|
|
91
|
+
})
|
|
92
|
+
}), "Excl. zeros"]
|
|
93
|
+
})]
|
|
94
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
95
|
+
className: "grid grid-cols-2 gap-1.5",
|
|
96
|
+
children: AGGREGATE_OPTIONS.filter((opt) => group.aggregates.includes(opt.fn)).map((opt) => {
|
|
97
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
98
|
+
onClick: () => onSelectAggregate(opt.fn),
|
|
99
|
+
className: `inline-flex h-8 items-center justify-center gap-2 rounded-md border text-xs transition-colors ${isActiveColumn && activeAggregate === opt.fn ? "border-primary/50 bg-primary/10 font-medium text-primary" : "border-transparent bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
100
|
+
title: opt.label,
|
|
101
|
+
children: [opt.icon, opt.shortLabel]
|
|
102
|
+
}, opt.fn);
|
|
103
|
+
})
|
|
104
|
+
})]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Metric panel content (no popover wrapper).
|
|
109
|
+
*
|
|
110
|
+
* @property onClose - Optional callback when user makes a definitive selection (e.g. "Count")
|
|
111
|
+
* @property className - Additional CSS classes
|
|
112
|
+
*/
|
|
113
|
+
function ChartMetricPanel({ onClose, className }) {
|
|
114
|
+
const { metric, setMetric, columns, availableMetrics } = useChartContext();
|
|
115
|
+
const countMetricEnabled = useMemo(() => availableMetrics.some((candidate) => candidate.kind === "count"), [availableMetrics]);
|
|
116
|
+
const metricColumnGroups = useMemo(() => getMetricColumnGroups(availableMetrics, columns), [availableMetrics, columns]);
|
|
117
|
+
const isCount = metric.kind === "count";
|
|
118
|
+
const includeZeros = isAggregateMetric(metric) ? metric.includeZeros ?? true : true;
|
|
119
|
+
const handleSelectCount = () => {
|
|
120
|
+
if (!countMetricEnabled) return;
|
|
121
|
+
setMetric(DEFAULT_METRIC);
|
|
122
|
+
onClose?.();
|
|
123
|
+
};
|
|
124
|
+
const handleSelectAggregate = (columnId, fn) => {
|
|
125
|
+
setMetric({
|
|
126
|
+
kind: "aggregate",
|
|
127
|
+
columnId,
|
|
128
|
+
aggregate: fn,
|
|
129
|
+
includeZeros: isAggregateMetric(metric) && metric.columnId === columnId ? includeZeros : true
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
const handleToggleZeros = () => {
|
|
133
|
+
if (!isAggregateMetric(metric)) return;
|
|
134
|
+
setMetric({
|
|
135
|
+
...metric,
|
|
136
|
+
includeZeros: !includeZeros
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
140
|
+
className,
|
|
141
|
+
children: [
|
|
142
|
+
countMetricEnabled && /* @__PURE__ */ jsxs("button", {
|
|
143
|
+
onClick: handleSelectCount,
|
|
144
|
+
className: `flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-xs transition-colors ${isCount ? "bg-primary/10 font-medium text-primary" : "text-foreground hover:bg-muted"}`,
|
|
145
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
146
|
+
className: `flex h-6 w-6 items-center justify-center rounded-md ${isCount ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground"}`,
|
|
147
|
+
children: /* @__PURE__ */ jsx(Hash, { className: "h-3.5 w-3.5" })
|
|
148
|
+
}), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
149
|
+
className: "font-medium",
|
|
150
|
+
children: "Count"
|
|
151
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
152
|
+
className: "text-[10px] text-muted-foreground",
|
|
153
|
+
children: "Number of items"
|
|
154
|
+
})] })]
|
|
155
|
+
}),
|
|
156
|
+
countMetricEnabled && metricColumnGroups.length > 0 && /* @__PURE__ */ jsx("div", { className: "my-4 border-t border-border" }),
|
|
157
|
+
/* @__PURE__ */ jsx("div", {
|
|
158
|
+
className: "space-y-4",
|
|
159
|
+
children: metricColumnGroups.map((group) => /* @__PURE__ */ jsx(MetricColumnGroup, {
|
|
160
|
+
group,
|
|
161
|
+
isActiveColumn: isAggregateMetric(metric) && metric.columnId === group.columnId,
|
|
162
|
+
activeAggregate: isAggregateMetric(metric) && metric.columnId === group.columnId ? metric.aggregate : null,
|
|
163
|
+
includeZeros,
|
|
164
|
+
onSelectAggregate: (fn) => handleSelectAggregate(group.columnId, fn),
|
|
165
|
+
onToggleZeros: handleToggleZeros
|
|
166
|
+
}, group.columnId))
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { ChartMetricPanel };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-metric-selector.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Metric selector — trigger button + popover wrapping ChartMetricPanel.
|
|
6
|
+
*
|
|
7
|
+
* Follows the same dropdown pattern as ChartFilters / ChartDateRange.
|
|
8
|
+
*/
|
|
9
|
+
/** Styled popover to select the Y-axis metric with grouped aggregate buttons. */
|
|
10
|
+
declare function ChartMetricSelector({
|
|
11
|
+
className
|
|
12
|
+
}: {
|
|
13
|
+
className?: string;
|
|
14
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ChartMetricSelector };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartDropdownPanel } from "./chart-dropdown.mjs";
|
|
3
|
+
import { ChartMetricPanel } from "./chart-metric-panel.mjs";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { getMetricLabel, isAggregateMetric } from "@matthieumordrel/chart-studio/_internal";
|
|
7
|
+
import { ChevronDown, MoveVertical } from "lucide-react";
|
|
8
|
+
//#region src/ui/chart-metric-selector.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Metric selector — trigger button + popover wrapping ChartMetricPanel.
|
|
11
|
+
*
|
|
12
|
+
* Follows the same dropdown pattern as ChartFilters / ChartDateRange.
|
|
13
|
+
*/
|
|
14
|
+
/** Styled popover to select the Y-axis metric with grouped aggregate buttons. */
|
|
15
|
+
function ChartMetricSelector({ className }) {
|
|
16
|
+
const { metric, availableMetrics, columns } = useChartContext();
|
|
17
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
18
|
+
const triggerRef = useRef(null);
|
|
19
|
+
if (availableMetrics.length <= 1) return null;
|
|
20
|
+
const isActive = !(metric.kind === "count");
|
|
21
|
+
const includeZeros = isAggregateMetric(metric) ? metric.includeZeros ?? true : true;
|
|
22
|
+
const label = getMetricLabel(metric, columns);
|
|
23
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
24
|
+
className,
|
|
25
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
26
|
+
ref: triggerRef,
|
|
27
|
+
onClick: () => setIsOpen(!isOpen),
|
|
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
|
+
"aria-label": "Metric",
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx(MoveVertical, { className: "h-3 w-3" }),
|
|
32
|
+
/* @__PURE__ */ jsx("span", { children: label }),
|
|
33
|
+
isActive && !includeZeros && /* @__PURE__ */ jsx("span", {
|
|
34
|
+
className: "rounded bg-muted px-1 py-px text-[9px] font-normal text-muted-foreground",
|
|
35
|
+
children: "excl. 0"
|
|
36
|
+
}),
|
|
37
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/50 transition-transform ${isOpen ? "rotate-180" : ""}` })
|
|
38
|
+
]
|
|
39
|
+
}), /* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
40
|
+
isOpen,
|
|
41
|
+
onClose: () => setIsOpen(false),
|
|
42
|
+
triggerRef,
|
|
43
|
+
width: 288,
|
|
44
|
+
className: "p-4",
|
|
45
|
+
children: /* @__PURE__ */ jsx(ChartMetricPanel, { onClose: () => setIsOpen(false) })
|
|
46
|
+
})]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ChartMetricSelector };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ChartDropdownPanel } from "./chart-dropdown.mjs";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
//#region src/ui/chart-select.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Premium styled select dropdown with highlight-only selection styling.
|
|
8
|
+
*
|
|
9
|
+
* @property value - Currently selected value
|
|
10
|
+
* @property options - Array of { value, label } options
|
|
11
|
+
* @property onChange - Callback when selection changes
|
|
12
|
+
* @property ariaLabel - Accessible label for the trigger
|
|
13
|
+
* @property className - Additional CSS classes
|
|
14
|
+
*/
|
|
15
|
+
function ChartSelect({ value, options, onChange, ariaLabel, icon: Icon, hideIcon, className }) {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
+
const triggerRef = useRef(null);
|
|
18
|
+
const selected = options.find((o) => o.value === value);
|
|
19
|
+
/** Toggle the dropdown. */
|
|
20
|
+
const handleToggle = () => {
|
|
21
|
+
setIsOpen((current) => !current);
|
|
22
|
+
};
|
|
23
|
+
/** Select an option and close the dropdown. */
|
|
24
|
+
const handleSelect = (optionValue) => {
|
|
25
|
+
onChange(optionValue);
|
|
26
|
+
setIsOpen(false);
|
|
27
|
+
};
|
|
28
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className,
|
|
30
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
31
|
+
ref: triggerRef,
|
|
32
|
+
onClick: handleToggle,
|
|
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",
|
|
34
|
+
"aria-label": ariaLabel,
|
|
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
|
+
]
|
|
43
|
+
}), /* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
44
|
+
isOpen,
|
|
45
|
+
onClose: () => setIsOpen(false),
|
|
46
|
+
triggerRef,
|
|
47
|
+
minWidth: "trigger",
|
|
48
|
+
className: "p-1",
|
|
49
|
+
children: options.map((option) => /* @__PURE__ */ jsx("button", {
|
|
50
|
+
onClick: () => handleSelect(option.value),
|
|
51
|
+
className: `flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs transition-colors ${option.value === value ? "bg-primary/8 font-medium text-primary" : "text-foreground hover:bg-muted/60"}`,
|
|
52
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
53
|
+
className: "truncate",
|
|
54
|
+
children: option.label
|
|
55
|
+
})
|
|
56
|
+
}, option.value))
|
|
57
|
+
})]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { ChartSelect };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-source-switcher.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Data source control — adapts based on single vs multi-source.
|
|
6
|
+
*
|
|
7
|
+
* Single source: read-only badge showing source label + record count.
|
|
8
|
+
* Multi source: dropdown to switch between data sources.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Data source display/switcher.
|
|
12
|
+
*
|
|
13
|
+
* - Single source → read-only badge: "[icon] Jobs · 1,247 records"
|
|
14
|
+
* - Multi source → dropdown to switch between sources
|
|
15
|
+
*/
|
|
16
|
+
declare function ChartSourceSwitcher({
|
|
17
|
+
className,
|
|
18
|
+
hideIcon
|
|
19
|
+
}: {
|
|
20
|
+
className?: string;
|
|
21
|
+
hideIcon?: boolean;
|
|
22
|
+
}): react_jsx_runtime0.JSX.Element;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ChartSourceSwitcher };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartSelect } from "./chart-select.mjs";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Database } from "lucide-react";
|
|
5
|
+
//#region src/ui/chart-source-switcher.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Data source control — adapts based on single vs multi-source.
|
|
8
|
+
*
|
|
9
|
+
* Single source: read-only badge showing source label + record count.
|
|
10
|
+
* Multi source: dropdown to switch between data sources.
|
|
11
|
+
*/
|
|
12
|
+
/** Format a number with locale-aware separators (e.g. 1,247). */
|
|
13
|
+
function formatCount(n) {
|
|
14
|
+
return n.toLocaleString("en-US");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Data source display/switcher.
|
|
18
|
+
*
|
|
19
|
+
* - Single source → read-only badge: "[icon] Jobs · 1,247 records"
|
|
20
|
+
* - Multi source → dropdown to switch between sources
|
|
21
|
+
*/
|
|
22
|
+
function ChartSourceSwitcher({ className, hideIcon }) {
|
|
23
|
+
const { hasMultipleSources, sources, activeSourceId, setActiveSource, recordCount } = useChartContext();
|
|
24
|
+
if (!hasMultipleSources) {
|
|
25
|
+
const label = sources[0]?.label ?? "Unnamed Source";
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: `inline-flex items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground ${className ?? ""}`,
|
|
28
|
+
children: [
|
|
29
|
+
!hideIcon && /* @__PURE__ */ jsx(Database, { className: "h-3 w-3 shrink-0" }),
|
|
30
|
+
/* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "font-medium text-foreground",
|
|
32
|
+
children: label
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx("span", {
|
|
35
|
+
className: "text-muted-foreground/40",
|
|
36
|
+
children: "·"
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { children: [formatCount(recordCount), " records"] })
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return /* @__PURE__ */ jsx(ChartSelect, {
|
|
43
|
+
value: activeSourceId,
|
|
44
|
+
options: sources.map((source) => ({
|
|
45
|
+
value: source.id,
|
|
46
|
+
label: `Source: ${source.label}`
|
|
47
|
+
})),
|
|
48
|
+
onChange: (v) => setActiveSource(v),
|
|
49
|
+
ariaLabel: "Data source",
|
|
50
|
+
icon: Database,
|
|
51
|
+
hideIcon,
|
|
52
|
+
className
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ChartSourceSwitcher };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-time-bucket-selector.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Time bucket selector — premium custom dropdown.
|
|
6
|
+
* Only renders when the X-axis is a date column.
|
|
7
|
+
*/
|
|
8
|
+
/** Custom dropdown to select time granularity. */
|
|
9
|
+
declare function ChartTimeBucketSelector({
|
|
10
|
+
className,
|
|
11
|
+
hideIcon
|
|
12
|
+
}: {
|
|
13
|
+
className?: string;
|
|
14
|
+
hideIcon?: boolean;
|
|
15
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ChartTimeBucketSelector };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { ChartSelect } from "./chart-select.mjs";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { CHART_TYPE_CONFIG } from "@matthieumordrel/chart-studio/_internal";
|
|
5
|
+
import { Clock } from "lucide-react";
|
|
6
|
+
//#region src/ui/chart-time-bucket-selector.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Time bucket selector — premium custom dropdown.
|
|
9
|
+
* Only renders when the X-axis is a date column.
|
|
10
|
+
*/
|
|
11
|
+
/** Labels for each time bucket. */
|
|
12
|
+
const BUCKET_LABELS = {
|
|
13
|
+
day: "Day",
|
|
14
|
+
week: "Week",
|
|
15
|
+
month: "Month",
|
|
16
|
+
quarter: "Quarter",
|
|
17
|
+
year: "Year"
|
|
18
|
+
};
|
|
19
|
+
/** Custom dropdown to select time granularity. */
|
|
20
|
+
function ChartTimeBucketSelector({ className, hideIcon }) {
|
|
21
|
+
const { chartType, isTimeSeries, timeBucket, setTimeBucket, availableTimeBuckets } = useChartContext();
|
|
22
|
+
if (!isTimeSeries || !CHART_TYPE_CONFIG[chartType].supportsTimeBucketing || availableTimeBuckets.length <= 1) return null;
|
|
23
|
+
return /* @__PURE__ */ jsx(ChartSelect, {
|
|
24
|
+
value: timeBucket,
|
|
25
|
+
options: availableTimeBuckets.map((bucket) => ({
|
|
26
|
+
value: bucket,
|
|
27
|
+
label: BUCKET_LABELS[bucket]
|
|
28
|
+
})),
|
|
29
|
+
onChange: setTimeBucket,
|
|
30
|
+
ariaLabel: "Time granularity",
|
|
31
|
+
icon: Clock,
|
|
32
|
+
hideIcon,
|
|
33
|
+
className
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { ChartTimeBucketSelector };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ControlId } from "./toolbar-types.mjs";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/chart-toolbar-overflow.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Props for ChartToolbarOverflow.
|
|
7
|
+
*
|
|
8
|
+
* @property pinned - Controls shown outside the overflow (excluded from menu)
|
|
9
|
+
* @property hidden - Controls completely hidden everywhere
|
|
10
|
+
* @property className - Additional CSS classes
|
|
11
|
+
*/
|
|
12
|
+
type ChartToolbarOverflowProps = {
|
|
13
|
+
pinned: ReadonlySet<ControlId>;
|
|
14
|
+
hidden: ReadonlySet<ControlId>;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Ellipsis overflow menu with Notion-style drill-down navigation.
|
|
19
|
+
* Main view shows all controls. Clicking a complex control replaces
|
|
20
|
+
* the panel content with that control's detail page + back button.
|
|
21
|
+
*/
|
|
22
|
+
declare function ChartToolbarOverflow({
|
|
23
|
+
pinned,
|
|
24
|
+
hidden,
|
|
25
|
+
className
|
|
26
|
+
}: ChartToolbarOverflowProps): react_jsx_runtime0.JSX.Element | null;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { ChartToolbarOverflow };
|