@object-ui/plugin-charts 3.1.5 → 3.3.1

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +24 -0
  3. package/dist/{AdvancedChartImpl-DmHTUUVD.js → AdvancedChartImpl-DxaZtNlE.js} +1214 -1206
  4. package/dist/{BarChart-XZkfLmcU.js → BarChart-BQS4sYHd.js} +3859 -3766
  5. package/dist/{ChartImpl-0VlpsMWG.js → ChartImpl-BaXisyXJ.js} +6 -6
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +207 -63
  8. package/dist/index.umd.cjs +19 -19
  9. package/dist/{jsx-runtime-C8d0IhUE.js → jsx-runtime-Caia9pQX.js} +1 -1
  10. package/dist/packages/plugin-charts/src/ObjectChart.d.ts +31 -0
  11. package/package.json +34 -10
  12. package/.turbo/turbo-build.log +0 -26
  13. package/dist/src/ObjectChart.d.ts +0 -12
  14. package/examples/chart-examples.ts +0 -54
  15. package/src/AdvancedChartImpl.tsx +0 -309
  16. package/src/ChartContainerImpl.tsx +0 -353
  17. package/src/ChartImpl.tsx +0 -91
  18. package/src/ChartRenderer.tsx +0 -112
  19. package/src/ObjectChart.stories.tsx +0 -104
  20. package/src/ObjectChart.tsx +0 -145
  21. package/src/__tests__/ObjectChart.aggregation.test.ts +0 -166
  22. package/src/__tests__/ObjectChart.dataFetch.test.tsx +0 -303
  23. package/src/index.test.ts +0 -136
  24. package/src/index.tsx +0 -172
  25. package/src/types.ts +0 -68
  26. package/tsconfig.json +0 -17
  27. package/vite.config.ts +0 -61
  28. package/vitest.config.ts +0 -13
  29. package/vitest.setup.ts +0 -1
  30. /package/dist/{src → packages/plugin-charts/src}/AdvancedChartImpl.d.ts +0 -0
  31. /package/dist/{src → packages/plugin-charts/src}/ChartContainerImpl.d.ts +0 -0
  32. /package/dist/{src → packages/plugin-charts/src}/ChartImpl.d.ts +0 -0
  33. /package/dist/{src → packages/plugin-charts/src}/ChartRenderer.d.ts +0 -0
  34. /package/dist/{src → packages/plugin-charts/src}/ObjectChart.stories.d.ts +0 -0
  35. /package/dist/{src → packages/plugin-charts/src}/index.d.ts +0 -0
  36. /package/dist/{src → packages/plugin-charts/src}/types.d.ts +0 -0
@@ -1,145 +0,0 @@
1
-
2
- import React, { useState, useEffect, useContext } from 'react';
3
- import { useDataScope, SchemaRendererContext } from '@object-ui/react';
4
- import { ChartRenderer } from './ChartRenderer';
5
- import { ComponentRegistry, extractRecords } from '@object-ui/core';
6
-
7
- /**
8
- * Client-side aggregation for fetched records.
9
- * Groups records by `groupBy` field and applies the aggregation function
10
- * to the `field` values in each group.
11
- */
12
- export function aggregateRecords(
13
- records: any[],
14
- aggregate: { field: string; function: string; groupBy: string }
15
- ): any[] {
16
- const { field, function: aggFn, groupBy } = aggregate;
17
- const groups: Record<string, any[]> = {};
18
-
19
- for (const record of records) {
20
- const key = String(record[groupBy] ?? 'Unknown');
21
- if (!groups[key]) groups[key] = [];
22
- groups[key].push(record);
23
- }
24
-
25
- return Object.entries(groups).map(([key, group]) => {
26
- const values = group.map(r => Number(r[field]) || 0);
27
- let result: number;
28
-
29
- switch (aggFn) {
30
- case 'count':
31
- result = group.length;
32
- break;
33
- case 'avg':
34
- result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
35
- break;
36
- case 'min':
37
- result = values.length > 0 ? Math.min(...values) : 0;
38
- break;
39
- case 'max':
40
- result = values.length > 0 ? Math.max(...values) : 0;
41
- break;
42
- case 'sum':
43
- default:
44
- result = values.reduce((a, b) => a + b, 0);
45
- break;
46
- }
47
-
48
- return { [groupBy]: key, [field]: result };
49
- });
50
- }
51
-
52
- // Re-export extractRecords from @object-ui/core for backward compatibility
53
- export { extractRecords } from '@object-ui/core';
54
-
55
- export const ObjectChart = (props: any) => {
56
- const { schema } = props;
57
- const context = useContext(SchemaRendererContext);
58
- const dataSource = props.dataSource || context?.dataSource;
59
- const boundData = useDataScope(schema.bind);
60
-
61
- const [fetchedData, setFetchedData] = useState<any[]>([]);
62
- const [loading, setLoading] = useState(false);
63
-
64
- useEffect(() => {
65
- let isMounted = true;
66
- const fetchData = async () => {
67
- if (!dataSource || !schema.objectName) return;
68
- if (isMounted) setLoading(true);
69
- try {
70
- let data: any[];
71
-
72
- // Prefer server-side aggregation when aggregate config is provided
73
- // and dataSource supports the aggregate() method.
74
- if (schema.aggregate && typeof dataSource.aggregate === 'function') {
75
- const results = await dataSource.aggregate(schema.objectName, {
76
- field: schema.aggregate.field,
77
- function: schema.aggregate.function,
78
- groupBy: schema.aggregate.groupBy,
79
- filter: schema.filter,
80
- });
81
- data = Array.isArray(results) ? results : [];
82
- } else if (typeof dataSource.find === 'function') {
83
- // Fallback: fetch all records and aggregate client-side
84
- const results = await dataSource.find(schema.objectName, {
85
- $filter: schema.filter
86
- });
87
-
88
- data = extractRecords(results);
89
-
90
- // Apply client-side aggregation when aggregate config is provided
91
- if (schema.aggregate && data.length > 0) {
92
- data = aggregateRecords(data, schema.aggregate);
93
- }
94
- } else {
95
- return;
96
- }
97
-
98
- if (isMounted) {
99
- setFetchedData(data);
100
- }
101
- } catch (e) {
102
- console.error('[ObjectChart] Fetch error:', e);
103
- } finally {
104
- if (isMounted) setLoading(false);
105
- }
106
- };
107
-
108
- if (schema.objectName && !boundData && !schema.data) {
109
- fetchData();
110
- }
111
- return () => { isMounted = false; };
112
- }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, schema.aggregate]);
113
-
114
- const rawData = boundData || schema.data || fetchedData;
115
- const finalData = Array.isArray(rawData) ? rawData : [];
116
-
117
- // Merge data if not provided in schema
118
- const finalSchema = {
119
- ...schema,
120
- data: finalData
121
- };
122
-
123
- if (loading && finalData.length === 0) {
124
- return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>Loading chart data…</div>;
125
- }
126
-
127
- if (!dataSource && schema.objectName && finalData.length === 0) {
128
- return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>No data source available for "{schema.objectName}"</div>;
129
- }
130
-
131
- return <ChartRenderer {...props} schema={finalSchema} />;
132
- };
133
-
134
- // Register it
135
- ComponentRegistry.register('object-chart', ObjectChart, {
136
- namespace: 'plugin-charts',
137
- label: 'Object Chart',
138
- category: 'view',
139
- inputs: [
140
- { name: 'objectName', type: 'string', label: 'Object Name', required: true },
141
- { name: 'data', type: 'array', label: 'Data', description: 'Optional static data' },
142
- { name: 'filter', type: 'array', label: 'Filter' },
143
- { name: 'aggregate', type: 'object', label: 'Aggregate', description: 'Aggregation config: { field, function, groupBy }' },
144
- ]
145
- });
@@ -1,166 +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 { describe, it, expect } from 'vitest';
10
- import { aggregateRecords, extractRecords } from '../ObjectChart';
11
-
12
- describe('aggregateRecords', () => {
13
- const records = [
14
- { account: 'Acme Corp', amount: 100 },
15
- { account: 'Acme Corp', amount: 200 },
16
- { account: 'Globex', amount: 150 },
17
- { account: 'Globex', amount: 50 },
18
- { account: 'Initech', amount: 300 },
19
- ];
20
-
21
- it('should aggregate using sum', () => {
22
- const result = aggregateRecords(records, {
23
- field: 'amount',
24
- function: 'sum',
25
- groupBy: 'account',
26
- });
27
-
28
- expect(result).toHaveLength(3);
29
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(300);
30
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(200);
31
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
32
- });
33
-
34
- it('should aggregate using count', () => {
35
- const result = aggregateRecords(records, {
36
- field: 'amount',
37
- function: 'count',
38
- groupBy: 'account',
39
- });
40
-
41
- expect(result).toHaveLength(3);
42
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(2);
43
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(2);
44
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(1);
45
- });
46
-
47
- it('should aggregate using avg', () => {
48
- const result = aggregateRecords(records, {
49
- field: 'amount',
50
- function: 'avg',
51
- groupBy: 'account',
52
- });
53
-
54
- expect(result).toHaveLength(3);
55
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(150);
56
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(100);
57
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
58
- });
59
-
60
- it('should aggregate using min', () => {
61
- const result = aggregateRecords(records, {
62
- field: 'amount',
63
- function: 'min',
64
- groupBy: 'account',
65
- });
66
-
67
- expect(result).toHaveLength(3);
68
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(100);
69
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(50);
70
- });
71
-
72
- it('should aggregate using max', () => {
73
- const result = aggregateRecords(records, {
74
- field: 'amount',
75
- function: 'max',
76
- groupBy: 'account',
77
- });
78
-
79
- expect(result).toHaveLength(3);
80
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(200);
81
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(150);
82
- });
83
-
84
- it('should handle records with missing groupBy field', () => {
85
- const input = [
86
- { account: 'Acme', amount: 100 },
87
- { amount: 200 }, // missing account
88
- ];
89
-
90
- const result = aggregateRecords(input, {
91
- field: 'amount',
92
- function: 'sum',
93
- groupBy: 'account',
94
- });
95
-
96
- expect(result).toHaveLength(2);
97
- expect(result.find(r => r.account === 'Acme')?.amount).toBe(100);
98
- expect(result.find(r => r.account === 'Unknown')?.amount).toBe(200);
99
- });
100
-
101
- it('should handle empty records', () => {
102
- const result = aggregateRecords([], {
103
- field: 'amount',
104
- function: 'sum',
105
- groupBy: 'account',
106
- });
107
-
108
- expect(result).toEqual([]);
109
- });
110
-
111
- it('should handle non-numeric values gracefully', () => {
112
- const input = [
113
- { account: 'Acme', amount: 'not-a-number' },
114
- { account: 'Acme', amount: 100 },
115
- ];
116
-
117
- const result = aggregateRecords(input, {
118
- field: 'amount',
119
- function: 'sum',
120
- groupBy: 'account',
121
- });
122
-
123
- expect(result).toHaveLength(1);
124
- expect(result[0].amount).toBe(100); // non-numeric value coerced to 0, sum is 0 + 100
125
- });
126
- });
127
-
128
- describe('extractRecords', () => {
129
- const sampleData = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
130
-
131
- it('should return the array directly when results is an array', () => {
132
- expect(extractRecords(sampleData)).toEqual(sampleData);
133
- });
134
-
135
- it('should extract from results.records', () => {
136
- expect(extractRecords({ records: sampleData })).toEqual(sampleData);
137
- });
138
-
139
- it('should extract from results.data', () => {
140
- expect(extractRecords({ data: sampleData })).toEqual(sampleData);
141
- });
142
-
143
- it('should extract from results.value', () => {
144
- expect(extractRecords({ value: sampleData })).toEqual(sampleData);
145
- });
146
-
147
- it('should return empty array for null/undefined', () => {
148
- expect(extractRecords(null)).toEqual([]);
149
- expect(extractRecords(undefined)).toEqual([]);
150
- });
151
-
152
- it('should return empty array for non-array/non-object', () => {
153
- expect(extractRecords('string')).toEqual([]);
154
- expect(extractRecords(42)).toEqual([]);
155
- });
156
-
157
- it('should return empty array for object without recognized keys', () => {
158
- expect(extractRecords({ total: 100 })).toEqual([]);
159
- });
160
-
161
- it('should prefer records over data and value', () => {
162
- const records = [{ id: 1 }];
163
- const data = [{ id: 2 }];
164
- expect(extractRecords({ records, data })).toEqual(records);
165
- });
166
- });
@@ -1,303 +0,0 @@
1
- /**
2
- * Tests for ObjectChart data fetching & fault tolerance.
3
- *
4
- * Verifies that ObjectChart:
5
- * - Calls dataSource.find() when objectName is set and no bound data
6
- * - Handles missing/invalid dataSource gracefully
7
- * - Works without a SchemaRendererProvider
8
- */
9
-
10
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
- import { render, waitFor } from '@testing-library/react';
12
- import React from 'react';
13
- import { SchemaRendererProvider } from '@object-ui/react';
14
- import { ObjectChart } from '../ObjectChart';
15
-
16
- // Suppress console.error from React error boundary / fetch errors
17
- const originalConsoleError = console.error;
18
- beforeEach(() => {
19
- console.error = vi.fn();
20
- });
21
- afterEach(() => {
22
- console.error = originalConsoleError;
23
- });
24
-
25
- describe('ObjectChart data fetching', () => {
26
- it('should call dataSource.find when objectName is set and no bind path', async () => {
27
- const mockFind = vi.fn().mockResolvedValue([
28
- { stage: 'Prospect', amount: 100 },
29
- { stage: 'Proposal', amount: 200 },
30
- ]);
31
- const dataSource = { find: mockFind };
32
-
33
- render(
34
- <SchemaRendererProvider dataSource={dataSource}>
35
- <ObjectChart
36
- schema={{
37
- type: 'object-chart',
38
- objectName: 'opportunity',
39
- chartType: 'bar',
40
- xAxisKey: 'stage',
41
- series: [{ dataKey: 'amount' }],
42
- }}
43
- />
44
- </SchemaRendererProvider>
45
- );
46
-
47
- await waitFor(() => {
48
- expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
49
- });
50
- });
51
-
52
- it('should NOT call dataSource.find when schema.data is provided', () => {
53
- const mockFind = vi.fn();
54
- const dataSource = { find: mockFind };
55
-
56
- render(
57
- <SchemaRendererProvider dataSource={dataSource}>
58
- <ObjectChart
59
- schema={{
60
- type: 'object-chart',
61
- objectName: 'opportunity',
62
- chartType: 'bar',
63
- data: [{ stage: 'A', amount: 100 }],
64
- xAxisKey: 'stage',
65
- series: [{ dataKey: 'amount' }],
66
- }}
67
- />
68
- </SchemaRendererProvider>
69
- );
70
-
71
- expect(mockFind).not.toHaveBeenCalled();
72
- });
73
-
74
- it('should apply aggregation to fetched data', async () => {
75
- const mockFind = vi.fn().mockResolvedValue([
76
- { stage: 'Prospect', amount: 100 },
77
- { stage: 'Prospect', amount: 200 },
78
- { stage: 'Proposal', amount: 300 },
79
- ]);
80
- const dataSource = { find: mockFind };
81
-
82
- const { container } = render(
83
- <SchemaRendererProvider dataSource={dataSource}>
84
- <ObjectChart
85
- schema={{
86
- type: 'object-chart',
87
- objectName: 'opportunity',
88
- chartType: 'bar',
89
- xAxisKey: 'stage',
90
- series: [{ dataKey: 'amount' }],
91
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
92
- }}
93
- />
94
- </SchemaRendererProvider>
95
- );
96
-
97
- await waitFor(() => {
98
- expect(mockFind).toHaveBeenCalled();
99
- });
100
- });
101
-
102
- it('should prefer dataSource.aggregate() over find() when aggregate config is set', async () => {
103
- const mockFind = vi.fn().mockResolvedValue([]);
104
- const mockAggregate = vi.fn().mockResolvedValue([
105
- { stage: 'Prospect', amount: 300 },
106
- { stage: 'Proposal', amount: 300 },
107
- ]);
108
- const dataSource = { find: mockFind, aggregate: mockAggregate };
109
-
110
- render(
111
- <SchemaRendererProvider dataSource={dataSource}>
112
- <ObjectChart
113
- schema={{
114
- type: 'object-chart',
115
- objectName: 'opportunity',
116
- chartType: 'bar',
117
- xAxisKey: 'stage',
118
- series: [{ dataKey: 'amount' }],
119
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
120
- }}
121
- />
122
- </SchemaRendererProvider>
123
- );
124
-
125
- await waitFor(() => {
126
- expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
127
- field: 'amount',
128
- function: 'sum',
129
- groupBy: 'stage',
130
- filter: undefined,
131
- });
132
- });
133
- // find() should NOT be called when aggregate() is available
134
- expect(mockFind).not.toHaveBeenCalled();
135
- });
136
-
137
- it('should fall back to find() when aggregate() is not available', async () => {
138
- const mockFind = vi.fn().mockResolvedValue([
139
- { stage: 'Prospect', amount: 100 },
140
- { stage: 'Prospect', amount: 200 },
141
- ]);
142
- // dataSource without aggregate method
143
- const dataSource = { find: mockFind };
144
-
145
- render(
146
- <SchemaRendererProvider dataSource={dataSource}>
147
- <ObjectChart
148
- schema={{
149
- type: 'object-chart',
150
- objectName: 'opportunity',
151
- chartType: 'bar',
152
- xAxisKey: 'stage',
153
- series: [{ dataKey: 'amount' }],
154
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
155
- }}
156
- />
157
- </SchemaRendererProvider>
158
- );
159
-
160
- await waitFor(() => {
161
- expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
162
- });
163
- });
164
-
165
- it('should NOT use aggregate() when no aggregate config is set', async () => {
166
- const mockFind = vi.fn().mockResolvedValue([
167
- { stage: 'Prospect', amount: 100 },
168
- ]);
169
- const mockAggregate = vi.fn().mockResolvedValue([]);
170
- const dataSource = { find: mockFind, aggregate: mockAggregate };
171
-
172
- render(
173
- <SchemaRendererProvider dataSource={dataSource}>
174
- <ObjectChart
175
- schema={{
176
- type: 'object-chart',
177
- objectName: 'opportunity',
178
- chartType: 'bar',
179
- xAxisKey: 'stage',
180
- series: [{ dataKey: 'amount' }],
181
- }}
182
- />
183
- </SchemaRendererProvider>
184
- );
185
-
186
- await waitFor(() => {
187
- expect(mockFind).toHaveBeenCalled();
188
- });
189
- // aggregate() should NOT be called when no aggregate config
190
- expect(mockAggregate).not.toHaveBeenCalled();
191
- });
192
-
193
- it('should pass filter to aggregate() when both aggregate and filter are set', async () => {
194
- const mockAggregate = vi.fn().mockResolvedValue([
195
- { stage: 'Won', amount: 500 },
196
- ]);
197
- const dataSource = { find: vi.fn(), aggregate: mockAggregate };
198
- const filter = { status: 'active' };
199
-
200
- render(
201
- <SchemaRendererProvider dataSource={dataSource}>
202
- <ObjectChart
203
- schema={{
204
- type: 'object-chart',
205
- objectName: 'opportunity',
206
- chartType: 'bar',
207
- xAxisKey: 'stage',
208
- series: [{ dataKey: 'amount' }],
209
- filter,
210
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
211
- }}
212
- />
213
- </SchemaRendererProvider>
214
- );
215
-
216
- await waitFor(() => {
217
- expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
218
- field: 'amount',
219
- function: 'sum',
220
- groupBy: 'stage',
221
- filter: { status: 'active' },
222
- });
223
- });
224
- });
225
- });
226
-
227
- describe('ObjectChart fault tolerance', () => {
228
- it('should not crash when dataSource has no find method', () => {
229
- const { container } = render(
230
- <SchemaRendererProvider dataSource={{}}>
231
- <ObjectChart
232
- schema={{
233
- type: 'object-chart',
234
- objectName: 'opportunity',
235
- chartType: 'bar',
236
- xAxisKey: 'stage',
237
- series: [{ dataKey: 'amount' }],
238
- }}
239
- />
240
- </SchemaRendererProvider>
241
- );
242
-
243
- // Should render without crashing
244
- expect(container).toBeDefined();
245
- });
246
-
247
- it('should not crash when rendered outside SchemaRendererProvider', () => {
248
- const { container } = render(
249
- <ObjectChart
250
- schema={{
251
- type: 'object-chart',
252
- chartType: 'bar',
253
- xAxisKey: 'stage',
254
- series: [{ dataKey: 'amount' }],
255
- }}
256
- />
257
- );
258
-
259
- // Should render without crashing
260
- expect(container).toBeDefined();
261
- });
262
-
263
- it('should show "No data source available" when no dataSource and objectName set', () => {
264
- const { container } = render(
265
- <ObjectChart
266
- schema={{
267
- type: 'object-chart',
268
- objectName: 'opportunity',
269
- chartType: 'bar',
270
- xAxisKey: 'stage',
271
- series: [{ dataKey: 'amount' }],
272
- }}
273
- />
274
- );
275
-
276
- expect(container.textContent).toContain('No data source available');
277
- });
278
-
279
- it('should use dataSource prop over context when both are present', async () => {
280
- const contextFind = vi.fn().mockResolvedValue([]);
281
- const propFind = vi.fn().mockResolvedValue([{ stage: 'A', amount: 1 }]);
282
-
283
- render(
284
- <SchemaRendererProvider dataSource={{ find: contextFind }}>
285
- <ObjectChart
286
- dataSource={{ find: propFind }}
287
- schema={{
288
- type: 'object-chart',
289
- objectName: 'opportunity',
290
- chartType: 'bar',
291
- xAxisKey: 'stage',
292
- series: [{ dataKey: 'amount' }],
293
- }}
294
- />
295
- </SchemaRendererProvider>
296
- );
297
-
298
- await waitFor(() => {
299
- expect(propFind).toHaveBeenCalled();
300
- });
301
- expect(contextFind).not.toHaveBeenCalled();
302
- });
303
- });