@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,101 @@
|
|
|
1
|
+
import { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { ChartColumn, ChartInstance, ChartInstanceFromSchemaDefinition, ChartSchemaDefinition, Metric } from "@matthieumordrel/chart-studio";
|
|
3
|
+
|
|
4
|
+
//#region src/ui/chart-context.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Type-erased chart instance stored in React context.
|
|
7
|
+
* This keeps the default UI primitives honest and safe for both single-source
|
|
8
|
+
* and multi-source charts.
|
|
9
|
+
*/
|
|
10
|
+
type ChartContextChart = Omit<ChartInstance<unknown, string>, 'columns' | 'filters'> & {
|
|
11
|
+
columns: readonly ChartColumn<any, string>[];
|
|
12
|
+
filters: Map<string, Set<string>>;
|
|
13
|
+
};
|
|
14
|
+
type AnyChartInstance = {
|
|
15
|
+
activeSourceId: string;
|
|
16
|
+
setActiveSource: (...args: any[]) => unknown;
|
|
17
|
+
hasMultipleSources: boolean;
|
|
18
|
+
sources: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}>;
|
|
22
|
+
chartType: ChartContextChart['chartType'];
|
|
23
|
+
setChartType: (...args: any[]) => unknown;
|
|
24
|
+
availableChartTypes: ChartContextChart['availableChartTypes'];
|
|
25
|
+
xAxisId: string | null;
|
|
26
|
+
setXAxis: (...args: any[]) => unknown;
|
|
27
|
+
availableXAxes: ChartContextChart['availableXAxes'];
|
|
28
|
+
groupById: string | null;
|
|
29
|
+
setGroupBy: (...args: any[]) => unknown;
|
|
30
|
+
availableGroupBys: ChartContextChart['availableGroupBys'];
|
|
31
|
+
isGroupByOptional: boolean;
|
|
32
|
+
metric: Metric<any>;
|
|
33
|
+
setMetric: (...args: any[]) => unknown;
|
|
34
|
+
availableMetrics: ChartContextChart['availableMetrics'];
|
|
35
|
+
timeBucket: ChartContextChart['timeBucket'];
|
|
36
|
+
setTimeBucket: (...args: any[]) => unknown;
|
|
37
|
+
availableTimeBuckets: ChartContextChart['availableTimeBuckets'];
|
|
38
|
+
isTimeSeries: boolean;
|
|
39
|
+
connectNulls: boolean;
|
|
40
|
+
dataScopeControl: ChartContextChart['dataScopeControl'];
|
|
41
|
+
filters: Map<any, Set<string>>;
|
|
42
|
+
toggleFilter: (...args: any[]) => unknown;
|
|
43
|
+
clearFilter: (...args: any[]) => unknown;
|
|
44
|
+
clearAllFilters: () => void;
|
|
45
|
+
availableFilters: ChartContextChart['availableFilters'];
|
|
46
|
+
sorting: ChartContextChart['sorting'];
|
|
47
|
+
setSorting: (...args: any[]) => unknown;
|
|
48
|
+
dateRange: ChartContextChart['dateRange'];
|
|
49
|
+
referenceDateId: string | null;
|
|
50
|
+
setReferenceDateId: (...args: any[]) => unknown;
|
|
51
|
+
availableDateColumns: ChartContextChart['availableDateColumns'];
|
|
52
|
+
dateRangePreset: ChartContextChart['dateRangePreset'];
|
|
53
|
+
setDateRangePreset: (...args: any[]) => unknown;
|
|
54
|
+
dateRangeFilter: ChartContextChart['dateRangeFilter'];
|
|
55
|
+
setDateRangeFilter: (...args: any[]) => unknown;
|
|
56
|
+
transformedData: ChartContextChart['transformedData'];
|
|
57
|
+
series: ChartContextChart['series'];
|
|
58
|
+
columns: readonly ChartColumn<any, string>[];
|
|
59
|
+
rawData: readonly unknown[];
|
|
60
|
+
recordCount: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Hook to access the chart instance from context.
|
|
64
|
+
* Must be used within a `<Chart>` provider.
|
|
65
|
+
*
|
|
66
|
+
* This hook stays intentionally broad so the default UI primitives remain safe
|
|
67
|
+
* for both single-source and multi-source charts.
|
|
68
|
+
*/
|
|
69
|
+
declare function useChartContext(): ChartContextChart;
|
|
70
|
+
/**
|
|
71
|
+
* Typed single-source chart context escape hatch for inferred charts.
|
|
72
|
+
* React cannot infer provider generics through arbitrary subtrees, so callers
|
|
73
|
+
* provide the row type (and optional schema type) explicitly.
|
|
74
|
+
*/
|
|
75
|
+
declare function useTypedChartContext<T, const TSchema extends ChartSchemaDefinition<T, any> | undefined = undefined>(): ChartInstanceFromSchemaDefinition<T, TSchema>;
|
|
76
|
+
/**
|
|
77
|
+
* Root provider component. Wraps children with the chart instance context.
|
|
78
|
+
*
|
|
79
|
+
* @param chart - The chart instance to share
|
|
80
|
+
* @param children - The children to render
|
|
81
|
+
* @param className - Additional CSS classes for the chart container
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <Chart chart={chart}>
|
|
86
|
+
* <ChartToolbar />
|
|
87
|
+
* <ChartCanvas />
|
|
88
|
+
* </Chart>
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function Chart({
|
|
92
|
+
chart,
|
|
93
|
+
children,
|
|
94
|
+
className
|
|
95
|
+
}: {
|
|
96
|
+
/** The chart instance to share. Create it with `useChart()` first.*/chart: AnyChartInstance; /** The children to render. Can be any UI primitives from the UI package. */
|
|
97
|
+
children: ReactNode; /** Additional CSS classes for the chart container. */
|
|
98
|
+
className?: string;
|
|
99
|
+
}): ReactElement;
|
|
100
|
+
//#endregion
|
|
101
|
+
export { Chart, useChartContext, useTypedChartContext };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/ui/chart-context.tsx
|
|
4
|
+
/**
|
|
5
|
+
* React context for sharing the chart instance across composable UI components.
|
|
6
|
+
*/
|
|
7
|
+
const ChartContext = createContext(null);
|
|
8
|
+
/**
|
|
9
|
+
* Check whether a candidate column ID exists in the current chart.
|
|
10
|
+
*/
|
|
11
|
+
function isKnownColumnId(columnIds, columnId) {
|
|
12
|
+
return columnIds.has(columnId);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create the broad-but-safe chart shape shared through React context.
|
|
16
|
+
*/
|
|
17
|
+
function createChartContextChart(chart) {
|
|
18
|
+
const columnIds = new Set(chart.columns.map((column) => column.id));
|
|
19
|
+
return {
|
|
20
|
+
activeSourceId: chart.activeSourceId,
|
|
21
|
+
setActiveSource: chart.setActiveSource,
|
|
22
|
+
hasMultipleSources: chart.hasMultipleSources,
|
|
23
|
+
sources: chart.sources,
|
|
24
|
+
chartType: chart.chartType,
|
|
25
|
+
setChartType: chart.setChartType,
|
|
26
|
+
availableChartTypes: chart.availableChartTypes,
|
|
27
|
+
xAxisId: chart.xAxisId,
|
|
28
|
+
setXAxis: (columnId) => {
|
|
29
|
+
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
30
|
+
chart.setXAxis(columnId);
|
|
31
|
+
},
|
|
32
|
+
availableXAxes: chart.availableXAxes,
|
|
33
|
+
groupById: chart.groupById,
|
|
34
|
+
setGroupBy: (columnId) => {
|
|
35
|
+
if (columnId === null) {
|
|
36
|
+
chart.setGroupBy(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
40
|
+
chart.setGroupBy(columnId);
|
|
41
|
+
},
|
|
42
|
+
availableGroupBys: chart.availableGroupBys,
|
|
43
|
+
isGroupByOptional: chart.isGroupByOptional,
|
|
44
|
+
metric: chart.metric,
|
|
45
|
+
setMetric: (metric) => {
|
|
46
|
+
if (metric.kind === "aggregate" && !isKnownColumnId(columnIds, metric.columnId)) throw new Error(`Unknown metric column ID: "${metric.columnId}"`);
|
|
47
|
+
chart.setMetric(metric);
|
|
48
|
+
},
|
|
49
|
+
availableMetrics: chart.availableMetrics,
|
|
50
|
+
timeBucket: chart.timeBucket,
|
|
51
|
+
setTimeBucket: chart.setTimeBucket,
|
|
52
|
+
availableTimeBuckets: chart.availableTimeBuckets,
|
|
53
|
+
isTimeSeries: chart.isTimeSeries,
|
|
54
|
+
connectNulls: chart.connectNulls,
|
|
55
|
+
dataScopeControl: chart.dataScopeControl,
|
|
56
|
+
filters: new Map(chart.filters),
|
|
57
|
+
toggleFilter: (columnId, value) => {
|
|
58
|
+
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
59
|
+
chart.toggleFilter(columnId, value);
|
|
60
|
+
},
|
|
61
|
+
clearFilter: (columnId) => {
|
|
62
|
+
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
63
|
+
chart.clearFilter(columnId);
|
|
64
|
+
},
|
|
65
|
+
clearAllFilters: chart.clearAllFilters,
|
|
66
|
+
availableFilters: chart.availableFilters,
|
|
67
|
+
sorting: chart.sorting,
|
|
68
|
+
setSorting: chart.setSorting,
|
|
69
|
+
dateRange: chart.dateRange,
|
|
70
|
+
referenceDateId: chart.referenceDateId,
|
|
71
|
+
setReferenceDateId: (columnId) => {
|
|
72
|
+
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
73
|
+
chart.setReferenceDateId(columnId);
|
|
74
|
+
},
|
|
75
|
+
availableDateColumns: chart.availableDateColumns,
|
|
76
|
+
dateRangePreset: chart.dateRangePreset,
|
|
77
|
+
setDateRangePreset: chart.setDateRangePreset,
|
|
78
|
+
dateRangeFilter: chart.dateRangeFilter,
|
|
79
|
+
setDateRangeFilter: chart.setDateRangeFilter,
|
|
80
|
+
transformedData: chart.transformedData,
|
|
81
|
+
series: chart.series,
|
|
82
|
+
columns: chart.columns,
|
|
83
|
+
rawData: chart.rawData,
|
|
84
|
+
recordCount: chart.recordCount
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function useChartContext() {
|
|
88
|
+
const ctx = useContext(ChartContext);
|
|
89
|
+
if (!ctx) throw new Error("useChartContext must be used within a <Chart> provider");
|
|
90
|
+
return ctx.chart;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Typed single-source chart context escape hatch for inferred charts.
|
|
94
|
+
* React cannot infer provider generics through arbitrary subtrees, so callers
|
|
95
|
+
* provide the row type (and optional schema type) explicitly.
|
|
96
|
+
*/
|
|
97
|
+
function useTypedChartContext() {
|
|
98
|
+
const ctx = useContext(ChartContext);
|
|
99
|
+
if (!ctx) throw new Error("useTypedChartContext must be used within a <Chart> provider");
|
|
100
|
+
if (ctx.chart.hasMultipleSources) throw new Error("useTypedChartContext only supports single-source charts right now. Multi-source charts stay broad because the active source schema can change.");
|
|
101
|
+
return ctx.typedChart;
|
|
102
|
+
}
|
|
103
|
+
function Chart({ chart, children, className }) {
|
|
104
|
+
const contextValue = useMemo(() => ({
|
|
105
|
+
chart: createChartContextChart(chart),
|
|
106
|
+
typedChart: chart
|
|
107
|
+
}), [chart]);
|
|
108
|
+
return /* @__PURE__ */ jsx(ChartContext.Provider, {
|
|
109
|
+
value: contextValue,
|
|
110
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
111
|
+
className,
|
|
112
|
+
children
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { Chart, useChartContext, useTypedChartContext };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-date-range-badge.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Read-only badge that always displays the current date range in the toolbar.
|
|
6
|
+
*
|
|
7
|
+
* Shows the preset label (e.g. "All time") and the computed min–max range.
|
|
8
|
+
* Non-interactive — purely informational so users always know the date window.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Read-only badge showing the active date range preset and computed bounds.
|
|
12
|
+
* Renders nothing if no date columns are available.
|
|
13
|
+
*/
|
|
14
|
+
declare function ChartDateRangeBadge({
|
|
15
|
+
className
|
|
16
|
+
}: {
|
|
17
|
+
className?: string;
|
|
18
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ChartDateRangeBadge };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { resolvePresetLabel } from "./chart-date-range-panel.mjs";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Calendar } from "lucide-react";
|
|
5
|
+
//#region src/ui/chart-date-range-badge.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Read-only badge that always displays the current date range in the toolbar.
|
|
8
|
+
*
|
|
9
|
+
* Shows the preset label (e.g. "All time") and the computed min–max range.
|
|
10
|
+
* Non-interactive — purely informational so users always know the date window.
|
|
11
|
+
*/
|
|
12
|
+
/** Format a Date into a compact, readable string (e.g. "Jan 5, 25"). */
|
|
13
|
+
function formatDate(date) {
|
|
14
|
+
return date.toLocaleDateString("en-US", {
|
|
15
|
+
month: "short",
|
|
16
|
+
day: "numeric",
|
|
17
|
+
year: "2-digit"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read-only badge showing the active date range preset and computed bounds.
|
|
22
|
+
* Renders nothing if no date columns are available.
|
|
23
|
+
*/
|
|
24
|
+
function ChartDateRangeBadge({ className }) {
|
|
25
|
+
const { dateRange, dateRangePreset, availableDateColumns } = useChartContext();
|
|
26
|
+
if (availableDateColumns.length === 0) return null;
|
|
27
|
+
const activeLabel = resolvePresetLabel(dateRangePreset);
|
|
28
|
+
const hasRange = dateRange?.min && dateRange?.max;
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: `inline-flex h-7 items-center gap-1.5 rounded-lg border border-border/50 bg-muted/30 px-2.5 text-xs text-muted-foreground ${className ?? ""}`,
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3 shrink-0" }),
|
|
33
|
+
/* @__PURE__ */ jsx("span", {
|
|
34
|
+
className: "font-medium",
|
|
35
|
+
children: activeLabel
|
|
36
|
+
}),
|
|
37
|
+
hasRange && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
38
|
+
className: "text-muted-foreground/40",
|
|
39
|
+
children: "·"
|
|
40
|
+
}), /* @__PURE__ */ jsxs("span", { children: [
|
|
41
|
+
formatDate(dateRange.min),
|
|
42
|
+
" – ",
|
|
43
|
+
formatDate(dateRange.max)
|
|
44
|
+
] })] })
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ChartDateRangeBadge };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-date-range-panel.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Date range panel content (no popover wrapper).
|
|
6
|
+
*
|
|
7
|
+
* @property onClose - Optional callback when user selects a preset
|
|
8
|
+
* @property className - Additional CSS classes
|
|
9
|
+
*/
|
|
10
|
+
declare function ChartDateRangePanel({
|
|
11
|
+
onClose,
|
|
12
|
+
className
|
|
13
|
+
}: {
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): react_jsx_runtime0.JSX.Element;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ChartDateRangePanel };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { DATE_RANGE_PRESETS, getPresetLabel } from "@matthieumordrel/chart-studio/_internal";
|
|
4
|
+
//#region src/ui/chart-date-range-panel.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Date range panel content — reusable by both ChartDateRange (inside a popover)
|
|
7
|
+
* and ChartToolbarOverflow (rendered inline).
|
|
8
|
+
*
|
|
9
|
+
* Shows preset buttons (Auto, All time, Last 7 days, etc.), a reference date
|
|
10
|
+
* column picker, and custom date inputs.
|
|
11
|
+
*/
|
|
12
|
+
/** Format a Date as YYYY-MM-DD for native date input value. */
|
|
13
|
+
function toInputValue(date) {
|
|
14
|
+
if (!date) return "";
|
|
15
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
16
|
+
}
|
|
17
|
+
/** Parse a YYYY-MM-DD string into a Date, or null if empty/invalid. */
|
|
18
|
+
function fromInputValue(value) {
|
|
19
|
+
if (!value) return null;
|
|
20
|
+
const d = /* @__PURE__ */ new Date(value + "T00:00:00");
|
|
21
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
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".
|
|
28
|
+
*/
|
|
29
|
+
function resolvePresetLabel(dateRangePreset) {
|
|
30
|
+
if (dateRangePreset === null) return "Custom";
|
|
31
|
+
return getPresetLabel(dateRangePreset);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Date range panel content (no popover wrapper).
|
|
35
|
+
*
|
|
36
|
+
* @property onClose - Optional callback when user selects a preset
|
|
37
|
+
* @property className - Additional CSS classes
|
|
38
|
+
*/
|
|
39
|
+
function ChartDateRangePanel({ onClose, className }) {
|
|
40
|
+
const { dateRangePreset, setDateRangePreset, dateRangeFilter, setDateRangeFilter, referenceDateId, setReferenceDateId, availableDateColumns } = useChartContext();
|
|
41
|
+
const hasMultipleDateColumns = availableDateColumns.length > 1;
|
|
42
|
+
const handlePreset = (presetId) => {
|
|
43
|
+
setDateRangePreset(presetId);
|
|
44
|
+
onClose?.();
|
|
45
|
+
};
|
|
46
|
+
const handleCustomFrom = (value) => {
|
|
47
|
+
setDateRangeFilter({
|
|
48
|
+
from: fromInputValue(value),
|
|
49
|
+
to: dateRangeFilter?.to ?? null
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const handleCustomTo = (value) => {
|
|
53
|
+
const to = fromInputValue(value);
|
|
54
|
+
setDateRangeFilter({
|
|
55
|
+
from: dateRangeFilter?.from ?? null,
|
|
56
|
+
to
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
60
|
+
className,
|
|
61
|
+
children: [
|
|
62
|
+
hasMultipleDateColumns && /* @__PURE__ */ jsxs("div", {
|
|
63
|
+
className: "mb-3",
|
|
64
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
65
|
+
className: "mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
66
|
+
children: "Date field"
|
|
67
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
68
|
+
className: "h-7 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring",
|
|
69
|
+
value: referenceDateId ?? "",
|
|
70
|
+
onChange: (e) => setReferenceDateId(e.target.value),
|
|
71
|
+
"aria-label": "Reference date column",
|
|
72
|
+
children: availableDateColumns.map((col) => /* @__PURE__ */ jsx("option", {
|
|
73
|
+
value: col.id,
|
|
74
|
+
children: col.label
|
|
75
|
+
}, col.id))
|
|
76
|
+
})]
|
|
77
|
+
}),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", {
|
|
79
|
+
className: "mb-3",
|
|
80
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
81
|
+
className: "mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
82
|
+
children: "Range"
|
|
83
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
84
|
+
className: "grid grid-cols-2 gap-1",
|
|
85
|
+
children: DATE_RANGE_PRESETS.map((preset) => {
|
|
86
|
+
const isActive = dateRangePreset === preset.id;
|
|
87
|
+
return /* @__PURE__ */ jsx("button", {
|
|
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"}`,
|
|
91
|
+
children: preset.label
|
|
92
|
+
}, preset.id);
|
|
93
|
+
})
|
|
94
|
+
})]
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
97
|
+
className: "mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
|
|
98
|
+
children: "Custom range"
|
|
99
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
100
|
+
className: "flex items-center gap-2",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("input", {
|
|
103
|
+
type: "date",
|
|
104
|
+
className: "h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring",
|
|
105
|
+
value: toInputValue(dateRangeFilter?.from ?? null),
|
|
106
|
+
onChange: (e) => handleCustomFrom(e.target.value),
|
|
107
|
+
"aria-label": "From date"
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "text-xs text-muted-foreground",
|
|
111
|
+
children: "–"
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsx("input", {
|
|
114
|
+
type: "date",
|
|
115
|
+
className: "h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring",
|
|
116
|
+
value: toInputValue(dateRangeFilter?.to ?? null),
|
|
117
|
+
onChange: (e) => handleCustomTo(e.target.value),
|
|
118
|
+
"aria-label": "To date"
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
})] })
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { ChartDateRangePanel, resolvePresetLabel };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-date-range.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Date range control — interactive button that shows the current date range
|
|
6
|
+
* and opens a popover wrapping ChartDateRangePanel.
|
|
7
|
+
*
|
|
8
|
+
* Acts as both a display and a filter control for the reference date column.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Interactive date range control with presets and custom date inputs.
|
|
12
|
+
* Also serves as the reference date column picker when multiple date columns exist.
|
|
13
|
+
*/
|
|
14
|
+
declare function ChartDateRange({
|
|
15
|
+
className
|
|
16
|
+
}: {
|
|
17
|
+
className?: string;
|
|
18
|
+
}): react_jsx_runtime0.JSX.Element | null;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ChartDateRange };
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { useRef, useState } from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Calendar } from "lucide-react";
|
|
7
|
+
//#region src/ui/chart-date-range.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Date range control — interactive button that shows the current date range
|
|
10
|
+
* and opens a popover wrapping ChartDateRangePanel.
|
|
11
|
+
*
|
|
12
|
+
* Acts as both a display and a filter control for the reference date column.
|
|
13
|
+
*/
|
|
14
|
+
/** Format a Date into a compact, readable string (e.g. "Jan 5, 25"). */
|
|
15
|
+
function formatDate(date) {
|
|
16
|
+
return date.toLocaleDateString("en-US", {
|
|
17
|
+
month: "short",
|
|
18
|
+
day: "numeric",
|
|
19
|
+
year: "2-digit"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Interactive date range control with presets and custom date inputs.
|
|
24
|
+
* Also serves as the reference date column picker when multiple date columns exist.
|
|
25
|
+
*/
|
|
26
|
+
function ChartDateRange({ className }) {
|
|
27
|
+
const { dateRange, dateRangePreset, availableDateColumns } = useChartContext();
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
if (availableDateColumns.length === 0) return null;
|
|
31
|
+
const activeLabel = resolvePresetLabel(dateRangePreset);
|
|
32
|
+
const isFiltered = dateRangePreset !== "all-time";
|
|
33
|
+
const hasRange = dateRange?.min && dateRange?.max;
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className,
|
|
36
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
37
|
+
ref: triggerRef,
|
|
38
|
+
onClick: () => setIsOpen(!isOpen),
|
|
39
|
+
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 ${isFiltered ? "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"}`,
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3" }),
|
|
42
|
+
/* @__PURE__ */ jsx("span", { children: activeLabel }),
|
|
43
|
+
hasRange && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
44
|
+
className: "text-muted-foreground/40",
|
|
45
|
+
children: "·"
|
|
46
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
47
|
+
className: "font-normal",
|
|
48
|
+
children: [
|
|
49
|
+
formatDate(dateRange.min),
|
|
50
|
+
" – ",
|
|
51
|
+
formatDate(dateRange.max)
|
|
52
|
+
]
|
|
53
|
+
})] })
|
|
54
|
+
]
|
|
55
|
+
}), /* @__PURE__ */ jsx(ChartDropdownPanel, {
|
|
56
|
+
isOpen,
|
|
57
|
+
onClose: () => setIsOpen(false),
|
|
58
|
+
triggerRef,
|
|
59
|
+
align: "right",
|
|
60
|
+
width: 288,
|
|
61
|
+
className: "p-3",
|
|
62
|
+
children: /* @__PURE__ */ jsx(ChartDateRangePanel, { onClose: () => setIsOpen(false) })
|
|
63
|
+
})]
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { ChartDateRange };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/ui/chart-debug.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Debug panel — shows raw data, transformed data, series, and current state.
|
|
6
|
+
* Drop `<ChartDebug />` inside a `<Chart>` to inspect what's happening.
|
|
7
|
+
*/
|
|
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
|
+
*/
|
|
13
|
+
declare function ChartDebug({
|
|
14
|
+
className,
|
|
15
|
+
defaultOpen
|
|
16
|
+
}: {
|
|
17
|
+
/** Additional CSS classes for the debug panel. */className?: string; /** Whether the debug panel should be open by default. (default: false) */
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
}): react_jsx_runtime0.JSX.Element;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ChartDebug };
|