@smallwebco/tinypivot-react 1.0.74 → 1.0.79
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 +10 -8
- package/dist/index.cjs +91 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +92 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# @smallwebco/tinypivot-react
|
|
2
2
|
|
|
3
|
-
A lightweight data grid with pivot tables, charts, and optional AI-powered data exploration for React. **Under 40KB gzipped** — 10x smaller than AG Grid.
|
|
3
|
+
A lightweight data grid with free pivot tables, Pro charts, and optional AI-powered data exploration for React. **Under 40KB gzipped** — 10x smaller than AG Grid.
|
|
4
4
|
|
|
5
5
|
**[Live Demo](https://tiny-pivot.com)** · **[Buy License](https://tiny-pivot.com/#pricing)**
|
|
6
6
|
|
|
7
7
|
## Why TinyPivot?
|
|
8
8
|
|
|
9
9
|
- **Lightweight**: Under 40KB gzipped vs 500KB+ for AG Grid
|
|
10
|
-
- **
|
|
10
|
+
- **Free Pivot Tables**: Sum aggregations, totals, and calculated fields included
|
|
11
|
+
- **Pro Upgrade**: Advanced aggregations, charts, AI Data Analyst, and no watermark
|
|
11
12
|
- **AI Data Analyst** (Pro): Natural language queries with BYOK — use your own OpenAI/Anthropic key
|
|
12
|
-
- **
|
|
13
|
+
- **Lifetime License**: No subscriptions — buy once, use forever
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -56,11 +57,12 @@ export default function App() {
|
|
|
56
57
|
| Column resizing | ✅ | ✅ |
|
|
57
58
|
| Clipboard (Ctrl+C) | ✅ | ✅ |
|
|
58
59
|
| Dark mode | ✅ | ✅ |
|
|
60
|
+
| Pivot table with Sum aggregation | ✅ | ✅ |
|
|
61
|
+
| Row/column totals | ✅ | ✅ |
|
|
62
|
+
| Calculated fields with formulas | ✅ | ✅ |
|
|
59
63
|
| **AI Data Analyst** (natural language, BYOK) | ❌ | ✅ |
|
|
60
64
|
| **Chart Builder** (6 chart types) | ❌ | ✅ |
|
|
61
|
-
|
|
|
62
|
-
| Aggregations (Sum, Avg, etc.) | ❌ | ✅ |
|
|
63
|
-
| Row/column totals | ❌ | ✅ |
|
|
65
|
+
| Advanced aggregations (Count, Avg, Min, Max, Unique, Median, Std Dev, %) | ❌ | ✅ |
|
|
64
66
|
| No watermark | ❌ | ✅ |
|
|
65
67
|
|
|
66
68
|
## Props
|
|
@@ -70,7 +72,7 @@ export default function App() {
|
|
|
70
72
|
| `data` | `Record<string, unknown>[]` | **required** | Array of data objects |
|
|
71
73
|
| `loading` | `boolean` | `false` | Show loading spinner |
|
|
72
74
|
| `fontSize` | `'xs' \| 'sm' \| 'base'` | `'xs'` | Font size preset |
|
|
73
|
-
| `showPivot` | `boolean` | `true` | Show pivot toggle
|
|
75
|
+
| `showPivot` | `boolean` | `true` | Show pivot toggle |
|
|
74
76
|
| `enableExport` | `boolean` | `true` | Show CSV export button |
|
|
75
77
|
| `enableSearch` | `boolean` | `true` | Show global search |
|
|
76
78
|
| `enablePagination` | `boolean` | `false` | Enable pagination |
|
|
@@ -180,7 +182,7 @@ See the [full documentation](https://github.com/Small-Web-Co/tinypivot) for comp
|
|
|
180
182
|
|
|
181
183
|
## License
|
|
182
184
|
|
|
183
|
-
- **Free Tier**: MIT License for
|
|
185
|
+
- **Free Tier**: MIT License for core grid and pivot features
|
|
184
186
|
- **Pro Features**: Commercial license required
|
|
185
187
|
|
|
186
188
|
**[Purchase at tiny-pivot.com/#pricing](https://tiny-pivot.com/#pricing)**
|
package/dist/index.cjs
CHANGED
|
@@ -99,14 +99,22 @@ function useAIAnalyst(options) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}, [storageKey]);
|
|
102
|
-
const
|
|
102
|
+
const initialConversationRef = (0, import_react.useRef)(null);
|
|
103
|
+
if (!initialConversationRef.current) {
|
|
104
|
+
initialConversationRef.current = loadFromStorage();
|
|
105
|
+
}
|
|
106
|
+
const [conversation, setConversation] = (0, import_react.useState)(initialConversationRef.current);
|
|
103
107
|
const [schemas, setSchemas] = (0, import_react.useState)(/* @__PURE__ */ new Map());
|
|
104
108
|
const [allSchemas, setAllSchemas] = (0, import_react.useState)([]);
|
|
105
109
|
const [isLoading, setIsLoading] = (0, import_react.useState)(false);
|
|
106
110
|
const [error, setError] = (0, import_react.useState)(null);
|
|
107
|
-
const [lastLoadedData, setLastLoadedData] = (0, import_react.useState)(
|
|
111
|
+
const [lastLoadedData, setLastLoadedData] = (0, import_react.useState)(
|
|
112
|
+
() => (0, import_tinypivot_core.getLatestConversationData)(initialConversationRef.current)
|
|
113
|
+
);
|
|
108
114
|
const [discoveredDataSources, setDiscoveredDataSources] = (0, import_react.useState)([]);
|
|
109
115
|
const [isLoadingTables, setIsLoadingTables] = (0, import_react.useState)(false);
|
|
116
|
+
const dataSourceLoadPromisesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
117
|
+
const hydratedPersistedSelectionRef = (0, import_react.useRef)(false);
|
|
110
118
|
const effectiveDataSources = (0, import_react.useMemo)(() => {
|
|
111
119
|
if (config.dataSources && config.dataSources.length > 0) {
|
|
112
120
|
return config.dataSources;
|
|
@@ -251,6 +259,59 @@ function useAIAnalyst(options) {
|
|
|
251
259
|
console.warn("Failed to fetch sample data:", err);
|
|
252
260
|
}
|
|
253
261
|
}, [onDataLoaded]);
|
|
262
|
+
const loadDataSourceState = (0, import_react.useCallback)(async (dataSource) => {
|
|
263
|
+
const currentConfig = configRef.current;
|
|
264
|
+
if (currentConfig.dataSourceLoader) {
|
|
265
|
+
const { data, schema } = await currentConfig.dataSourceLoader(dataSource.id);
|
|
266
|
+
if (schema) {
|
|
267
|
+
setSchemas((prev) => new Map(prev).set(dataSource.id, schema));
|
|
268
|
+
}
|
|
269
|
+
if (data && data.length > 0) {
|
|
270
|
+
setLastLoadedData(data);
|
|
271
|
+
onDataLoaded?.({
|
|
272
|
+
data,
|
|
273
|
+
query: `SELECT * FROM ${dataSource.table} LIMIT 100`,
|
|
274
|
+
dataSourceId: dataSource.id,
|
|
275
|
+
rowCount: data.length
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (currentConfig.demoMode) {
|
|
281
|
+
const demoSchema = (0, import_tinypivot_core.getDemoSchema)(dataSource.id);
|
|
282
|
+
if (demoSchema) {
|
|
283
|
+
setSchemas((prev) => new Map(prev).set(dataSource.id, demoSchema));
|
|
284
|
+
}
|
|
285
|
+
const initialData = (0, import_tinypivot_core.getInitialDemoData)(dataSource.id);
|
|
286
|
+
if (initialData) {
|
|
287
|
+
setLastLoadedData(initialData);
|
|
288
|
+
onDataLoaded?.({
|
|
289
|
+
data: initialData,
|
|
290
|
+
query: `SELECT * FROM ${dataSource.table} LIMIT 10`,
|
|
291
|
+
dataSourceId: dataSource.id,
|
|
292
|
+
rowCount: initialData.length
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (currentConfig.endpoint) {
|
|
298
|
+
await fetchSchema(dataSource);
|
|
299
|
+
await fetchSampleData(dataSource);
|
|
300
|
+
}
|
|
301
|
+
}, [fetchSchema, fetchSampleData, onDataLoaded]);
|
|
302
|
+
const ensureDataSourceState = (0, import_react.useCallback)(async (dataSource) => {
|
|
303
|
+
const existingLoad = dataSourceLoadPromisesRef.current.get(dataSource.id);
|
|
304
|
+
if (existingLoad) {
|
|
305
|
+
return existingLoad;
|
|
306
|
+
}
|
|
307
|
+
const loadPromise = loadDataSourceState(dataSource).catch((err) => {
|
|
308
|
+
console.warn("Failed to load data source:", err);
|
|
309
|
+
}).finally(() => {
|
|
310
|
+
dataSourceLoadPromisesRef.current.delete(dataSource.id);
|
|
311
|
+
});
|
|
312
|
+
dataSourceLoadPromisesRef.current.set(dataSource.id, loadPromise);
|
|
313
|
+
return loadPromise;
|
|
314
|
+
}, [loadDataSourceState]);
|
|
254
315
|
const selectDataSource = (0, import_react.useCallback)(async (dataSourceId) => {
|
|
255
316
|
const dataSource = effectiveDataSources.find((ds) => ds.id === dataSourceId);
|
|
256
317
|
if (!dataSource) {
|
|
@@ -270,44 +331,30 @@ What would you like to know about this data?`
|
|
|
270
331
|
onConversationUpdate?.({ conversation: withMessage });
|
|
271
332
|
return withMessage;
|
|
272
333
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
const initialData = (0, import_tinypivot_core.getInitialDemoData)(dataSourceId);
|
|
297
|
-
if (initialData) {
|
|
298
|
-
setLastLoadedData(initialData);
|
|
299
|
-
onDataLoaded?.({
|
|
300
|
-
data: initialData,
|
|
301
|
-
query: `SELECT * FROM ${dataSource.table} LIMIT 10`,
|
|
302
|
-
dataSourceId,
|
|
303
|
-
rowCount: initialData.length
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
} else if (configRef.current.endpoint) {
|
|
307
|
-
await fetchSchema(dataSource);
|
|
308
|
-
await fetchSampleData(dataSource);
|
|
334
|
+
await ensureDataSourceState(dataSource);
|
|
335
|
+
}, [effectiveDataSources, ensureDataSourceState, onConversationUpdate]);
|
|
336
|
+
(0, import_react.useEffect)(() => {
|
|
337
|
+
if (hydratedPersistedSelectionRef.current) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const initialConversation = initialConversationRef.current;
|
|
341
|
+
const initialDataSourceId = initialConversation?.dataSourceId;
|
|
342
|
+
if (!initialDataSourceId) {
|
|
343
|
+
hydratedPersistedSelectionRef.current = true;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const dataSource = effectiveDataSources.find((ds) => ds.id === initialDataSourceId);
|
|
347
|
+
if (!dataSource) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const hasPersistedPreviewData = !!(0, import_tinypivot_core.getLatestConversationData)(initialConversation);
|
|
351
|
+
const hasSchema = schemas.has(initialDataSourceId);
|
|
352
|
+
const hasPreviewData = !!lastLoadedData?.length || hasPersistedPreviewData;
|
|
353
|
+
hydratedPersistedSelectionRef.current = true;
|
|
354
|
+
if (!hasSchema || !hasPreviewData) {
|
|
355
|
+
void ensureDataSourceState(dataSource);
|
|
309
356
|
}
|
|
310
|
-
}, [effectiveDataSources,
|
|
357
|
+
}, [effectiveDataSources, ensureDataSourceState, lastLoadedData, schemas]);
|
|
311
358
|
const callAIEndpoint = (0, import_react.useCallback)(async (userInput, currentConversation, currentSchemas, currentDataSources, currentAllSchemas) => {
|
|
312
359
|
if (!configRef.current.endpoint) {
|
|
313
360
|
throw new Error("No endpoint configured. Set `endpoint` in AI analyst config.");
|
|
@@ -710,6 +757,7 @@ What would you like to know about this data?`
|
|
|
710
757
|
return null;
|
|
711
758
|
}, [conversation.dataSourceId, effectiveDataSources, onError]);
|
|
712
759
|
const clearConversation = (0, import_react.useCallback)(() => {
|
|
760
|
+
hydratedPersistedSelectionRef.current = true;
|
|
713
761
|
const newConv = (0, import_tinypivot_core.createConversation)(configRef.current.sessionId);
|
|
714
762
|
setConversation(newConv);
|
|
715
763
|
setError(null);
|
|
@@ -720,7 +768,9 @@ What would you like to know about this data?`
|
|
|
720
768
|
return { ...conversation };
|
|
721
769
|
}, [conversation]);
|
|
722
770
|
const importConversation = (0, import_react.useCallback)((conv) => {
|
|
771
|
+
hydratedPersistedSelectionRef.current = true;
|
|
723
772
|
setConversation(conv);
|
|
773
|
+
setLastLoadedData((0, import_tinypivot_core.getLatestConversationData)(conv));
|
|
724
774
|
onConversationUpdate?.({ conversation: conv });
|
|
725
775
|
}, [onConversationUpdate]);
|
|
726
776
|
return {
|
|
@@ -4715,9 +4765,8 @@ function PivotSkeleton({
|
|
|
4715
4765
|
d: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
4716
4766
|
}
|
|
4717
4767
|
) }),
|
|
4718
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { children: "
|
|
4719
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Pivot
|
|
4720
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href: "https://tiny-pivot.com/#pricing", target: "_blank", rel: "noopener noreferrer", className: "vpg-pro-link", children: "Get Pro License \u2192" })
|
|
4768
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { children: "Pivot Unavailable" }),
|
|
4769
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Pivot mode could not be enabled in this session. Try reloading the page." })
|
|
4721
4770
|
] }) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
4722
4771
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "vpg-config-bar", children: [
|
|
4723
4772
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|