@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 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
- - **Batteries Included**: Pivot tables, 6 chart types, Excel-like features out of the box
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
- - **One-Time License**: No subscriptions — pay once, use forever
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
- | Pivot table | ❌ | ✅ |
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 (Pro) |
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 basic grid features
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 [conversation, setConversation] = (0, import_react.useState)(() => loadFromStorage());
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)(null);
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
- if (configRef.current.dataSourceLoader) {
274
- try {
275
- const { data, schema } = await configRef.current.dataSourceLoader(dataSourceId);
276
- if (schema) {
277
- setSchemas((prev) => new Map(prev).set(dataSourceId, schema));
278
- }
279
- if (data && data.length > 0) {
280
- setLastLoadedData(data);
281
- onDataLoaded?.({
282
- data,
283
- query: `SELECT * FROM ${dataSource.table} LIMIT 100`,
284
- dataSourceId,
285
- rowCount: data.length
286
- });
287
- }
288
- } catch (err) {
289
- console.warn("Failed to load data source:", err);
290
- }
291
- } else if (configRef.current.demoMode) {
292
- const demoSchema = (0, import_tinypivot_core.getDemoSchema)(dataSourceId);
293
- if (demoSchema) {
294
- setSchemas((prev) => new Map(prev).set(dataSourceId, demoSchema));
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, fetchSchema, fetchSampleData, onConversationUpdate, onDataLoaded]);
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: "Pro Feature" }),
4719
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { children: "Pivot Table functionality requires a Pro license." }),
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)(