@object-ui/plugin-charts 3.3.0 → 3.3.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.
@@ -1,323 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import * as React from "react"
10
- import {
11
- Bar,
12
- BarChart,
13
- Line,
14
- LineChart,
15
- Area,
16
- AreaChart,
17
- Pie,
18
- PieChart,
19
- Radar,
20
- RadarChart,
21
- PolarGrid,
22
- PolarAngleAxis,
23
- PolarRadiusAxis,
24
- Scatter,
25
- ScatterChart,
26
- ZAxis,
27
- Cell,
28
- XAxis,
29
- YAxis,
30
- CartesianGrid,
31
- } from 'recharts';
32
- import {
33
- ChartContainer,
34
- ChartTooltip,
35
- ChartTooltipContent,
36
- ChartLegend,
37
- ChartLegendContent,
38
- ChartConfig
39
- } from './ChartContainerImpl';
40
-
41
- // Default color fallback for chart series
42
- const DEFAULT_CHART_COLOR = 'hsl(var(--primary))';
43
-
44
- // Simple color map for Tailwind names (Mock - ideal would be computed styles)
45
- const TW_COLORS: Record<string, string> = {
46
- slate: '#64748b',
47
- gray: '#6b7280',
48
- zinc: '#71717a',
49
- neutral: '#737373',
50
- stone: '#78716c',
51
- red: '#ef4444',
52
- orange: '#f97316',
53
- amber: '#f59e0b',
54
- yellow: '#eab308',
55
- lime: '#84cc16',
56
- green: '#22c55e',
57
- emerald: '#10b981',
58
- teal: '#14b8a6',
59
- cyan: '#06b6d4',
60
- sky: '#0ea5e9',
61
- blue: '#3b82f6',
62
- indigo: '#6366f1',
63
- violet: '#8b5cf6',
64
- purple: '#a855f7',
65
- fuchsia: '#d946ef',
66
- pink: '#ec4899',
67
- rose: '#f43f5e',
68
- };
69
-
70
- const resolveColor = (color: string) => TW_COLORS[color] || color;
71
-
72
- export interface AdvancedChartImplProps {
73
- chartType?: 'bar' | 'line' | 'area' | 'pie' | 'donut' | 'radar' | 'scatter' | 'combo';
74
- data?: Array<Record<string, any>>;
75
- config?: ChartConfig;
76
- xAxisKey?: string;
77
- series?: Array<{ dataKey: string; chartType?: 'bar' | 'line' | 'area' }>;
78
- className?: string;
79
- }
80
-
81
- /**
82
- * AdvancedChartImpl - The heavy implementation that imports Recharts with full features
83
- * This component is lazy-loaded to avoid including Recharts in the initial bundle
84
- */
85
- export default function AdvancedChartImpl({
86
- chartType = 'bar',
87
- data: rawData = [],
88
- config = {},
89
- xAxisKey = 'name',
90
- series = [],
91
- className = '',
92
- }: AdvancedChartImplProps) {
93
- const data = Array.isArray(rawData) ? rawData : [];
94
- const [isMobile, setIsMobile] = React.useState(false);
95
-
96
- React.useEffect(() => {
97
- const checkMobile = () => setIsMobile(window.innerWidth < 640);
98
- checkMobile();
99
- window.addEventListener('resize', checkMobile);
100
- return () => window.removeEventListener('resize', checkMobile);
101
- }, []);
102
- const ChartComponent = {
103
- bar: BarChart,
104
- line: LineChart,
105
- area: AreaChart,
106
- pie: PieChart,
107
- donut: PieChart,
108
- radar: RadarChart,
109
- scatter: ScatterChart,
110
- combo: BarChart,
111
- }[chartType] || BarChart;
112
-
113
- // Memoize whether any X-axis label is long enough to warrant angle rotation
114
- const hasLongLabels = React.useMemo(
115
- () => data.some((d: any) => String(d[xAxisKey] || '').length > 5),
116
- [data, xAxisKey],
117
- );
118
-
119
- // Helper function to get color palette
120
- const getPalette = () => [
121
- 'hsl(var(--chart-1))',
122
- 'hsl(var(--chart-2))',
123
- 'hsl(var(--chart-3))',
124
- 'hsl(var(--chart-4))',
125
- 'hsl(var(--chart-5))'
126
- ];
127
-
128
- // Pie and Donut charts
129
- if (chartType === 'pie' || chartType === 'donut') {
130
- const innerRadius = chartType === 'donut' ? 60 : 0;
131
- return (
132
- <ChartContainer config={config} className={className}>
133
- <PieChart>
134
- <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
135
- <Pie
136
- data={data}
137
- dataKey={series[0]?.dataKey || 'value'}
138
- nameKey={xAxisKey || 'name'}
139
- innerRadius={innerRadius}
140
- strokeWidth={5}
141
- paddingAngle={2}
142
- outerRadius={80}
143
- >
144
- {data.map((entry, index) => {
145
- // 1. Try config by nameKey (category)
146
- let c = config[entry[xAxisKey]]?.color;
147
-
148
- // 2. Fallback to palette
149
- if (!c) {
150
- const palette = getPalette();
151
- c = palette[index % palette.length];
152
- }
153
-
154
- return <Cell key={`cell-${index}`} fill={resolveColor(c)} />;
155
- })}
156
- </Pie>
157
- <ChartLegend
158
- content={<ChartLegendContent nameKey={xAxisKey} />}
159
- {...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
160
- />
161
- </PieChart>
162
- </ChartContainer>
163
- );
164
- }
165
-
166
- // Radar chart
167
- if (chartType === 'radar') {
168
- return (
169
- <ChartContainer config={config} className={className}>
170
- <RadarChart data={data}>
171
- <PolarGrid />
172
- <PolarAngleAxis dataKey={xAxisKey} />
173
- <PolarRadiusAxis />
174
- <ChartTooltip content={<ChartTooltipContent />} />
175
- <ChartLegend
176
- content={<ChartLegendContent />}
177
- {...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
178
- />
179
- {series.map((s: any) => {
180
- const color = resolveColor(config[s.dataKey]?.color || DEFAULT_CHART_COLOR);
181
- return (
182
- <Radar
183
- key={s.dataKey}
184
- dataKey={s.dataKey}
185
- stroke={color}
186
- fill={color}
187
- fillOpacity={0.6}
188
- />
189
- );
190
- })}
191
- </RadarChart>
192
- </ChartContainer>
193
- );
194
- }
195
-
196
- // Scatter chart
197
- if (chartType === 'scatter') {
198
- return (
199
- <ChartContainer config={config} className={className}>
200
- <ScatterChart>
201
- <CartesianGrid vertical={false} />
202
- <XAxis
203
- type="number"
204
- dataKey={xAxisKey}
205
- name={String(config[xAxisKey]?.label || xAxisKey)}
206
- tickLine={false}
207
- axisLine={false}
208
- interval={isMobile ? Math.ceil(data.length / 5) : 0}
209
- />
210
- <YAxis
211
- type="number"
212
- dataKey={series[0]?.dataKey || 'value'}
213
- name={String(config[series[0]?.dataKey]?.label || series[0]?.dataKey)}
214
- tickLine={false}
215
- axisLine={false}
216
- />
217
- <ZAxis type="number" range={[60, 400]} />
218
- <ChartTooltip content={<ChartTooltipContent />} />
219
- <ChartLegend
220
- content={<ChartLegendContent />}
221
- {...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
222
- />
223
- {series.map((s: any, index: number) => {
224
- const palette = getPalette();
225
- const color = resolveColor(config[s.dataKey]?.color || palette[index % palette.length]);
226
- return (
227
- <Scatter
228
- key={s.dataKey}
229
- name={config[s.dataKey]?.label || s.dataKey}
230
- data={data}
231
- fill={color}
232
- />
233
- );
234
- })}
235
- </ScatterChart>
236
- </ChartContainer>
237
- );
238
- }
239
-
240
- // Combo chart (mixed bar + line on same chart)
241
- if (chartType === 'combo') {
242
- return (
243
- <ChartContainer config={config} className={className}>
244
- <BarChart data={data}>
245
- <CartesianGrid vertical={false} />
246
- <XAxis
247
- dataKey={xAxisKey}
248
- tickLine={false}
249
- tickMargin={10}
250
- axisLine={false}
251
- interval={isMobile ? Math.ceil(data.length / 5) : 0}
252
- tickFormatter={(value) => {
253
- if (!value || typeof value !== 'string') return value;
254
- if (isMobile && value.length > 8) return value.slice(0, 8) + '…';
255
- return value;
256
- }}
257
- {...(!isMobile && hasLongLabels && { angle: -35, textAnchor: 'end', height: 60 })}
258
- />
259
- <YAxis yAxisId="left" tickLine={false} axisLine={false} />
260
- <YAxis yAxisId="right" orientation="right" tickLine={false} axisLine={false} />
261
- <ChartTooltip content={<ChartTooltipContent />} />
262
- <ChartLegend
263
- content={<ChartLegendContent />}
264
- {...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
265
- />
266
- {series.map((s: any, index: number) => {
267
- const color = resolveColor(config[s.dataKey]?.color || DEFAULT_CHART_COLOR);
268
- const seriesType = s.chartType || (index === 0 ? 'bar' : 'line');
269
- const yAxisId = seriesType === 'bar' ? 'left' : 'right';
270
-
271
- if (seriesType === 'line') {
272
- return <Line key={s.dataKey} yAxisId={yAxisId} type="monotone" dataKey={s.dataKey} stroke={color} strokeWidth={2} dot={false} />;
273
- }
274
- if (seriesType === 'area') {
275
- return <Area key={s.dataKey} yAxisId={yAxisId} type="monotone" dataKey={s.dataKey} fill={color} stroke={color} fillOpacity={0.4} />;
276
- }
277
- return <Bar key={s.dataKey} yAxisId={yAxisId} dataKey={s.dataKey} fill={color} radius={4} />;
278
- })}
279
- </BarChart>
280
- </ChartContainer>
281
- );
282
- }
283
-
284
- return (
285
- <ChartContainer config={config} className={className}>
286
- <ChartComponent data={data}>
287
- <CartesianGrid vertical={false} />
288
- <XAxis
289
- dataKey={xAxisKey}
290
- tickLine={false}
291
- tickMargin={10}
292
- axisLine={false}
293
- interval={isMobile ? Math.ceil(data.length / 5) : 0}
294
- tickFormatter={(value) => {
295
- if (!value || typeof value !== 'string') return value;
296
- if (isMobile && value.length > 8) return value.slice(0, 8) + '…';
297
- return value;
298
- }}
299
- {...(!isMobile && hasLongLabels && { angle: -35, textAnchor: 'end', height: 60 })}
300
- />
301
- <ChartTooltip content={<ChartTooltipContent />} />
302
- <ChartLegend
303
- content={<ChartLegendContent />}
304
- {...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
305
- />
306
- {series.map((s: any) => {
307
- const color = resolveColor(config[s.dataKey]?.color || DEFAULT_CHART_COLOR);
308
-
309
- if (chartType === 'bar') {
310
- return <Bar key={s.dataKey} dataKey={s.dataKey} fill={color} radius={4} />;
311
- }
312
- if (chartType === 'line') {
313
- return <Line key={s.dataKey} type="monotone" dataKey={s.dataKey} stroke={color} strokeWidth={2} dot={false} />;
314
- }
315
- if (chartType === 'area') {
316
- return <Area key={s.dataKey} type="monotone" dataKey={s.dataKey} fill={color} stroke={color} fillOpacity={0.4} />;
317
- }
318
- return null;
319
- })}
320
- </ChartComponent>
321
- </ChartContainer>
322
- );
323
- }
@@ -1,353 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- "use client"
10
-
11
- import * as React from "react"
12
- import { ResponsiveContainer, Tooltip, Legend } from "recharts"
13
-
14
- // Utility function to merge class names (inline to avoid external dependency)
15
- const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
16
-
17
- // Format: { THEME_NAME: CSS_SELECTOR }
18
- const THEMES = { light: "", dark: ".dark" } as const
19
-
20
- export type ChartConfig = {
21
- [k in string]: {
22
- label?: React.ReactNode
23
- icon?: React.ComponentType
24
- } & (
25
- | { color?: string; theme?: never }
26
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
27
- )
28
- }
29
-
30
- type ChartContextProps = {
31
- config: ChartConfig
32
- }
33
-
34
- const ChartContext = React.createContext<ChartContextProps | null>(null)
35
-
36
- function useChart() {
37
- const context = React.useContext(ChartContext)
38
-
39
- if (!context) {
40
- throw new Error("useChart must be used within a <ChartContainer />")
41
- }
42
-
43
- return context
44
- }
45
-
46
- function ChartContainer({
47
- id,
48
- className,
49
- children,
50
- config,
51
- ...props
52
- }: React.ComponentProps<"div"> & {
53
- config: ChartConfig
54
- children: React.ComponentProps<
55
- typeof ResponsiveContainer
56
- >["children"]
57
- }) {
58
- const uniqueId = React.useId()
59
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
60
-
61
- return (
62
- <ChartContext.Provider value={{ config }}>
63
- <div
64
- data-slot="chart"
65
- data-chart={chartId}
66
- className={cn(
67
- "flex w-full h-[350px] justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
68
- className
69
- )}
70
- {...props}
71
- >
72
- <ChartStyle id={chartId} config={config} />
73
- <ResponsiveContainer width="100%" height="100%">
74
- {children}
75
- </ResponsiveContainer>
76
- </div>
77
- </ChartContext.Provider>
78
- )
79
- }
80
-
81
- const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
82
- const colorConfig = Object.entries(config).filter(
83
- ([, config]) => config.theme || config.color
84
- )
85
-
86
- if (!colorConfig.length) {
87
- return null
88
- }
89
-
90
- return (
91
- <style
92
- dangerouslySetInnerHTML={{
93
- __html: Object.entries(THEMES)
94
- .map(
95
- ([theme, prefix]) => `
96
- ${prefix} [data-chart=${id}] {
97
- ${colorConfig
98
- .map(([key, itemConfig]) => {
99
- const color =
100
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
101
- itemConfig.color
102
- return color ? ` --color-${key}: ${color};` : null
103
- })
104
- .join("\n")}
105
- }
106
- `
107
- )
108
- .join("\n"),
109
- }}
110
- />
111
- )
112
- }
113
-
114
- const ChartTooltip = Tooltip
115
-
116
- function ChartTooltipContent({
117
- active,
118
- payload,
119
- className,
120
- indicator = "dot",
121
- hideLabel = false,
122
- hideIndicator = false,
123
- label,
124
- labelFormatter,
125
- labelClassName,
126
- formatter,
127
- color,
128
- nameKey,
129
- labelKey,
130
- }: any) {
131
- const { config } = useChart()
132
-
133
- const tooltipLabel = React.useMemo(() => {
134
- if (hideLabel || !payload?.length) {
135
- return null
136
- }
137
-
138
- const [item] = payload
139
- const key = `${labelKey || item?.dataKey || item?.name || "value"}`
140
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
141
- const value =
142
- !labelKey && typeof label === "string"
143
- ? config[label as keyof typeof config]?.label || label
144
- : itemConfig?.label
145
-
146
- if (labelFormatter) {
147
- return (
148
- <div className={cn("font-medium", labelClassName)}>
149
- {labelFormatter(value, payload)}
150
- </div>
151
- )
152
- }
153
-
154
- if (!value) {
155
- return null
156
- }
157
-
158
- return <div className={cn("font-medium", labelClassName)}>{value}</div>
159
- }, [
160
- label,
161
- labelFormatter,
162
- payload,
163
- hideLabel,
164
- labelClassName,
165
- config,
166
- labelKey,
167
- ])
168
-
169
- if (!active || !payload?.length) {
170
- return null
171
- }
172
-
173
- const nestLabel = payload.length === 1 && indicator !== "dot"
174
-
175
- return (
176
- <div
177
- className={cn(
178
- "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
179
- className
180
- )}
181
- >
182
- {!nestLabel ? tooltipLabel : null}
183
- <div className="grid gap-1.5">
184
- {payload
185
- .filter((item: any) => item.type !== "none")
186
- .map((item: any, index: number) => {
187
- const key = `${nameKey || item.name || item.dataKey || "value"}`
188
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
189
- const indicatorColor = color || item.payload.fill || item.color
190
-
191
- return (
192
- <div
193
- key={item.dataKey}
194
- className={cn(
195
- "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
196
- indicator === "dot" ? "items-center" : ""
197
- )}
198
- >
199
- {formatter && item?.value !== undefined && item.name ? (
200
- formatter(item.value, item.name, item, index, item.payload)
201
- ) : (
202
- <>
203
- {itemConfig?.icon ? (
204
- <itemConfig.icon />
205
- ) : (
206
- !hideIndicator && (
207
- <div
208
- className={cn(
209
- "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
210
- indicator === "dot" ? "h-2.5 w-2.5" : "",
211
- indicator === "line" ? "w-1" : "",
212
- indicator === "dashed" ? "w-0 border-[1.5px] border-dashed bg-transparent" : "",
213
- (nestLabel && indicator === "dashed") ? "my-0.5" : "",
214
- )}
215
- style={
216
-
217
- {
218
- "--color-bg": indicatorColor,
219
- "--color-border": indicatorColor,
220
- } as React.CSSProperties
221
- }
222
- />
223
- )
224
- )}
225
- <div
226
- className={cn(
227
- "flex flex-1 justify-between leading-none",
228
- nestLabel ? "items-end" : "items-center"
229
- )}
230
- >
231
- <div className="grid gap-1.5">
232
- {nestLabel ? tooltipLabel : null}
233
- <span className="text-muted-foreground">
234
- {itemConfig?.label || item.name}
235
- </span>
236
- </div>
237
- {item.value && (
238
- <span className="text-foreground font-mono font-medium tabular-nums">
239
- {item.value.toLocaleString()}
240
- </span>
241
- )}
242
- </div>
243
- </>
244
- )}
245
- </div>
246
- )
247
- })}
248
- </div>
249
- </div>
250
- )
251
- }
252
-
253
- const ChartLegend = Legend
254
-
255
- function ChartLegendContent({
256
- className,
257
- hideIcon = false,
258
- payload,
259
- verticalAlign = "bottom",
260
- nameKey,
261
- }: any) {
262
- const { config } = useChart()
263
-
264
- if (!payload?.length) {
265
- return null
266
- }
267
-
268
- return (
269
- <div
270
- className={cn(
271
- "flex items-center justify-center gap-4",
272
- verticalAlign === "top" ? "pb-3" : "pt-3",
273
- className
274
- )}
275
- >
276
- {payload
277
- .filter((item: any) => item.type !== "none")
278
- .map((item: any) => {
279
- const key = `${nameKey || item.dataKey || "value"}`
280
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
281
-
282
- return (
283
- <div
284
- key={item.value}
285
- className={cn(
286
- "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
287
- )}
288
- >
289
- {itemConfig?.icon && !hideIcon ? (
290
- <itemConfig.icon />
291
- ) : (
292
- <div
293
- className="h-2 w-2 shrink-0 rounded-[2px]"
294
- style={{
295
- backgroundColor: item.color,
296
- }}
297
- />
298
- )}
299
- {itemConfig?.label}
300
- </div>
301
- )
302
- })}
303
- </div>
304
- )
305
- }
306
-
307
- // Helper to extract item config from a payload.
308
- function getPayloadConfigFromPayload(
309
- config: ChartConfig,
310
- payload: unknown,
311
- key: string
312
- ) {
313
- if (typeof payload !== "object" || payload === null) {
314
- return undefined
315
- }
316
-
317
- const payloadPayload =
318
- "payload" in payload &&
319
- typeof payload.payload === "object" &&
320
- payload.payload !== null
321
- ? payload.payload
322
- : undefined
323
-
324
- let configLabelKey: string = key
325
-
326
- if (
327
- key in payload &&
328
- typeof payload[key as keyof typeof payload] === "string"
329
- ) {
330
- configLabelKey = payload[key as keyof typeof payload] as string
331
- } else if (
332
- payloadPayload &&
333
- key in payloadPayload &&
334
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335
- ) {
336
- configLabelKey = payloadPayload[
337
- key as keyof typeof payloadPayload
338
- ] as string
339
- }
340
-
341
- return configLabelKey in config
342
- ? config[configLabelKey]
343
- : config[key as keyof typeof config]
344
- }
345
-
346
- export {
347
- ChartContainer,
348
- ChartTooltip,
349
- ChartTooltipContent,
350
- ChartLegend,
351
- ChartLegendContent,
352
- ChartStyle,
353
- }