@kanaries/graphic-walker 0.4.1 → 0.4.3

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 (66) hide show
  1. package/dist/App.d.ts +5 -1
  2. package/dist/assets/buildMetricTable.worker-5555966a.js.map +1 -0
  3. package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +22 -0
  4. package/dist/components/leafletRenderer/POIRenderer.d.ts +20 -0
  5. package/dist/components/leafletRenderer/encodings.d.ts +7 -0
  6. package/dist/components/leafletRenderer/geoConfigPanel.d.ts +3 -0
  7. package/dist/components/leafletRenderer/index.d.ts +15 -0
  8. package/dist/components/leafletRenderer/tooltip.d.ts +9 -0
  9. package/dist/components/leafletRenderer/utils.d.ts +2 -0
  10. package/dist/components/pivotTable/index.d.ts +2 -1
  11. package/dist/components/pivotTable/inteface.d.ts +6 -2
  12. package/dist/components/pivotTable/leftTree.d.ts +1 -0
  13. package/dist/components/pivotTable/topTree.d.ts +2 -0
  14. package/dist/components/pivotTable/utils.d.ts +1 -2
  15. package/dist/config.d.ts +3 -2
  16. package/dist/graphic-walker.es.js +37811 -30386
  17. package/dist/graphic-walker.es.js.map +1 -1
  18. package/dist/graphic-walker.umd.js +145 -137
  19. package/dist/graphic-walker.umd.js.map +1 -1
  20. package/dist/interfaces.d.ts +28 -0
  21. package/dist/renderer/specRenderer.d.ts +2 -1
  22. package/dist/services.d.ts +7 -1
  23. package/dist/store/commonStore.d.ts +6 -0
  24. package/dist/store/visualSpecStore.d.ts +180 -4
  25. package/dist/utils/save.d.ts +1 -0
  26. package/dist/workers/buildPivotTable.d.ts +7 -0
  27. package/package.json +14 -2
  28. package/src/App.tsx +18 -4
  29. package/src/components/leafletRenderer/ChoroplethRenderer.tsx +312 -0
  30. package/src/components/leafletRenderer/POIRenderer.tsx +189 -0
  31. package/src/components/leafletRenderer/encodings.ts +194 -0
  32. package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
  33. package/src/components/leafletRenderer/index.tsx +70 -0
  34. package/src/components/leafletRenderer/tooltip.tsx +24 -0
  35. package/src/components/leafletRenderer/utils.ts +52 -0
  36. package/src/components/pivotTable/index.tsx +171 -67
  37. package/src/components/pivotTable/inteface.ts +6 -2
  38. package/src/components/pivotTable/leftTree.tsx +24 -11
  39. package/src/components/pivotTable/metricTable.tsx +15 -10
  40. package/src/components/pivotTable/topTree.tsx +50 -17
  41. package/src/components/pivotTable/utils.ts +70 -11
  42. package/src/components/visualConfig/index.tsx +17 -1
  43. package/src/config.ts +27 -16
  44. package/src/dataSource/table.tsx +7 -11
  45. package/src/fields/aestheticFields.tsx +4 -0
  46. package/src/fields/fieldsContext.tsx +3 -0
  47. package/src/fields/posFields/index.tsx +8 -2
  48. package/src/global.d.ts +4 -4
  49. package/src/index.tsx +11 -9
  50. package/src/interfaces.ts +35 -0
  51. package/src/locales/en-US.json +27 -2
  52. package/src/locales/ja-JP.json +27 -2
  53. package/src/locales/zh-CN.json +27 -2
  54. package/src/renderer/hooks.ts +1 -1
  55. package/src/renderer/index.tsx +24 -1
  56. package/src/renderer/pureRenderer.tsx +27 -13
  57. package/src/renderer/specRenderer.tsx +46 -30
  58. package/src/services.ts +32 -23
  59. package/src/shadow-dom.tsx +7 -0
  60. package/src/store/commonStore.ts +29 -1
  61. package/src/store/visualSpecStore.ts +38 -23
  62. package/src/utils/save.ts +28 -1
  63. package/src/utils/vegaApiExport.ts +3 -0
  64. package/src/visualSettings/index.tsx +58 -6
  65. package/src/workers/buildMetricTable.worker.js +27 -0
  66. package/src/workers/buildPivotTable.ts +27 -0
@@ -0,0 +1,197 @@
1
+ import { observer } from 'mobx-react-lite';
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { runInAction } from 'mobx';
5
+ import { useGlobalStore } from '../../store';
6
+ import Modal from '../modal';
7
+ import PrimaryButton from '../button/primary';
8
+ import DefaultButton from '../button/default';
9
+ import type { Topology } from '../../interfaces';
10
+
11
+ const GeoConfigPanel: React.FC = (props) => {
12
+ const { commonStore, vizStore } = useGlobalStore();
13
+ const { showGeoJSONConfigPanel } = commonStore;
14
+ const { visualConfig } = vizStore;
15
+ const { geoKey, geojson } = visualConfig;
16
+ const { t: tGlobal } = useTranslation('translation');
17
+ const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
18
+
19
+ const [dataMode, setDataMode] = useState<'GeoJSON' | 'TopoJSON'>('GeoJSON');
20
+ const [featureId, setFeatureId] = useState('');
21
+ const [url, setUrl] = useState('');
22
+ const [geoJSON, setGeoJSON] = useState('');
23
+ const [topoJSON, setTopoJSON] = useState('');
24
+ const [topoJSONKey, setTopoJSONKey] = useState('');
25
+
26
+ const defaultTopoJSONKey = useMemo(() => {
27
+ try {
28
+ const value = JSON.parse(topoJSON) as Topology;
29
+ return Object.keys(value.objects)[0] || '';
30
+ } catch (error) {
31
+ return '';
32
+ }
33
+ }, [topoJSON]);
34
+
35
+ useEffect(() => {
36
+ setFeatureId(geoKey || '');
37
+ }, [geoKey]);
38
+
39
+ useEffect(() => {
40
+ setGeoJSON(geojson ? JSON.stringify(geojson, null, 2) : '');
41
+ }, [geojson]);
42
+
43
+ return (
44
+ <Modal
45
+ show={showGeoJSONConfigPanel}
46
+ onClose={() => {
47
+ commonStore.setShowGeoJSONConfigPanel(false);
48
+ }}
49
+ >
50
+ <div>
51
+ <h2 className="text-lg mb-4">{t('geography')}</h2>
52
+ <div>
53
+ <div className="my-2">
54
+ <label className="block text-xs font-medium leading-6 text-gray-900">{t('geography_settings.geoKey')}</label>
55
+ <div className="mt-1">
56
+ <input
57
+ type="text"
58
+ className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
59
+ value={featureId}
60
+ onChange={(e) => {
61
+ setFeatureId(e.target.value);
62
+ }}
63
+ />
64
+ </div>
65
+ </div>
66
+ <div className="my-2">
67
+ <label className="block text-xs font-medium leading-6 text-gray-900">{t(`geography_settings.${dataMode.toLowerCase()}`)}</label>
68
+ <div className="mt-1 flex flex-col space-y-2">
69
+ <div role="radiogroup">
70
+ <div className="flex items-center space-x-2">
71
+ <input
72
+ type="radio"
73
+ name="dataMode"
74
+ id="geojson"
75
+ className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 dark:bg-gray-800 dark:border-gray-600"
76
+ checked={dataMode === 'GeoJSON'}
77
+ onChange={() => {
78
+ setDataMode('GeoJSON');
79
+ }}
80
+ />
81
+ <label htmlFor="geojson" className="text-xs whitespace-nowrap">
82
+ {t('geography_settings.geojson')}
83
+ </label>
84
+ <input
85
+ type="radio"
86
+ name="dataMode"
87
+ id="topojson"
88
+ className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 dark:bg-gray-800 dark:border-gray-600"
89
+ checked={dataMode === 'TopoJSON'}
90
+ onChange={() => {
91
+ setDataMode('TopoJSON');
92
+ }}
93
+ />
94
+ <label htmlFor="topojson" className="text-xs whitespace-nowrap">
95
+ {t('geography_settings.topojson')}
96
+ </label>
97
+ </div>
98
+ </div>
99
+ <div className="flex items-center space-x-2">
100
+ <label className="text-xs whitespace-nowrap capitalize">
101
+ {t('geography_settings.href', { format: dataMode.toLowerCase() })}
102
+ </label>
103
+ <input
104
+ type="text"
105
+ className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
106
+ value={url}
107
+ placeholder={t('geography_settings.hrefPlaceholder', { format: dataMode.toLowerCase() })}
108
+ onChange={(e) => {
109
+ setUrl(e.target.value);
110
+ }}
111
+ />
112
+ <DefaultButton
113
+ text={t('geography_settings.load')}
114
+ className="mr-2"
115
+ onClick={() => {
116
+ if (url) {
117
+ fetch(url)
118
+ .then((res) => res.json())
119
+ .then((json) => {
120
+ (dataMode === 'GeoJSON' ? setGeoJSON : setTopoJSON)(
121
+ JSON.stringify(json, null, 2)
122
+ );
123
+ });
124
+ }
125
+ }}
126
+ />
127
+ </div>
128
+ <textarea
129
+ className="block w-full h-40 rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 resize-none"
130
+ value={dataMode === 'GeoJSON' ? geoJSON : topoJSON}
131
+ placeholder={t('geography_settings.jsonInputPlaceholder', { format: dataMode.toLowerCase() })}
132
+ onChange={(e) => {
133
+ (dataMode === 'GeoJSON' ? setGeoJSON : setTopoJSON)(e.target.value);
134
+ }}
135
+ />
136
+ {dataMode === 'TopoJSON' && (
137
+ <div className="flex items-center space-x-2">
138
+ <label className="text-xs whitespace-nowrap capitalize">
139
+ {t('geography_settings.objectKey')}
140
+ </label>
141
+ <input
142
+ type="text"
143
+ className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
144
+ value={topoJSONKey}
145
+ placeholder={defaultTopoJSONKey}
146
+ onChange={(e) => {
147
+ setTopoJSONKey(e.target.value);
148
+ }}
149
+ />
150
+ </div>
151
+ )}
152
+ </div>
153
+ </div>
154
+ </div>
155
+ <div className="mt-4">
156
+ <PrimaryButton
157
+ text={tGlobal('actions.confirm')}
158
+ className="mr-2"
159
+ onClick={() => {
160
+ try {
161
+ const json = JSON.parse(dataMode === 'GeoJSON' ? geoJSON : topoJSON);
162
+ if (dataMode === 'TopoJSON') {
163
+ runInAction(() => {
164
+ vizStore.setGeographicData({
165
+ type: 'TopoJSON',
166
+ data: json,
167
+ objectKey: topoJSONKey || defaultTopoJSONKey,
168
+ }, featureId);
169
+ });
170
+ } else {
171
+ runInAction(() => {
172
+ vizStore.setGeographicData({
173
+ type: 'GeoJSON',
174
+ data: json,
175
+ }, featureId);
176
+ });
177
+ }
178
+ commonStore.setShowGeoJSONConfigPanel(false);
179
+ } catch (err) {
180
+ console.error(err);
181
+ }
182
+ }}
183
+ />
184
+ <DefaultButton
185
+ text={tGlobal('actions.cancel')}
186
+ className="mr-2"
187
+ onClick={() => {
188
+ commonStore.setShowGeoJSONConfigPanel(false);
189
+ }}
190
+ />
191
+ </div>
192
+ </div>
193
+ </Modal>
194
+ );
195
+ };
196
+
197
+ export default observer(GeoConfigPanel);
@@ -0,0 +1,70 @@
1
+ import React, { forwardRef, useMemo } from "react";
2
+ import type { DeepReadonly, DraggableFieldState, IRow, IVisualConfig, VegaGlobalConfig } from "../../interfaces";
3
+ import POIRenderer from "./POIRenderer";
4
+ import ChoroplethRenderer from "./ChoroplethRenderer";
5
+
6
+
7
+ export interface ILeafletRendererProps {
8
+ name?: string;
9
+ vegaConfig?: VegaGlobalConfig;
10
+ draggableFieldState: DeepReadonly<DraggableFieldState>;
11
+ visualConfig: DeepReadonly<IVisualConfig>;
12
+ data: IRow[];
13
+ }
14
+
15
+ export interface ILeafletRendererRef {}
16
+
17
+ export const LEAFLET_DEFAULT_WIDTH = 800;
18
+ export const LEAFLET_DEFAULT_HEIGHT = 600;
19
+
20
+ const LeafletRenderer = forwardRef<ILeafletRendererRef, ILeafletRendererProps>(function LeafletRenderer (props, ref) {
21
+ const { name, draggableFieldState, data, visualConfig, vegaConfig = {} } = props;
22
+ const { latitude: [lat], longitude: [lng], geoId: [geoId], dimensions, measures, size: [size], color: [color], opacity: [opacity], text: [text], details } = draggableFieldState;
23
+ const { defaultAggregated, geoms: [markType], geojson, geoKey = '', scaleIncludeUnmatchedChoropleth = false } = visualConfig;
24
+ const allFields = useMemo(() => [...dimensions, ...measures], [dimensions, measures]);
25
+ const latField = useMemo(() => allFields.find((f) => f.geoRole === 'latitude'), [allFields]);
26
+ const lngField = useMemo(() => allFields.find((f) => f.geoRole === 'longitude'), [allFields]);
27
+ const latitude = useMemo(() => lat ?? latField, [lat, latField]);
28
+ const longitude = useMemo(() => lng ?? lngField, [lng, lngField]);
29
+
30
+ if (markType === 'poi') {
31
+ return (
32
+ <POIRenderer
33
+ name={name}
34
+ data={data}
35
+ allFields={allFields}
36
+ defaultAggregated={defaultAggregated}
37
+ latitude={latitude}
38
+ longitude={longitude}
39
+ color={color}
40
+ opacity={opacity}
41
+ size={size}
42
+ details={details}
43
+ vegaConfig={vegaConfig}
44
+ />
45
+ );
46
+ } else if (markType === 'choropleth') {
47
+ return (
48
+ <ChoroplethRenderer
49
+ name={name}
50
+ data={data}
51
+ allFields={allFields}
52
+ features={geojson}
53
+ geoKey={geoKey}
54
+ defaultAggregated={defaultAggregated}
55
+ geoId={geoId}
56
+ color={color}
57
+ opacity={opacity}
58
+ text={text}
59
+ details={details}
60
+ vegaConfig={vegaConfig}
61
+ scaleIncludeUnmatchedChoropleth={scaleIncludeUnmatchedChoropleth}
62
+ />
63
+ );
64
+ }
65
+
66
+ return null;
67
+ });
68
+
69
+
70
+ export default LeafletRenderer;
@@ -0,0 +1,24 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import type { DeepReadonly, IViewField, VegaGlobalConfig } from "../../interfaces";
3
+ import { useDisplayValueFormatter } from "./utils";
4
+
5
+
6
+ export interface ITooltipContentProps {
7
+ allFields: readonly DeepReadonly<IViewField>[];
8
+ vegaConfig: VegaGlobalConfig;
9
+ field: DeepReadonly<IViewField>;
10
+ value: unknown;
11
+ }
12
+
13
+ export const TooltipContent = memo<ITooltipContentProps>(function TooltipContent ({ allFields, vegaConfig, field, value }) {
14
+ const { fid, analyticType, aggName } = field;
15
+ const fieldDisplayLabel = useMemo(() => {
16
+ const name = allFields.find(f => f.fid === fid)?.name ?? fid;
17
+ return analyticType === 'measure' && aggName ? `${aggName}(${name})` : name;
18
+ }, [allFields, fid, analyticType, aggName]);
19
+ const formatter = useDisplayValueFormatter(field.semanticType, vegaConfig);
20
+
21
+ return (
22
+ <p>{fieldDisplayLabel}: {formatter(value)}</p>
23
+ );
24
+ });
@@ -0,0 +1,52 @@
1
+ import { useMemo } from "react";
2
+ import { timeFormat as tFormat } from "d3-time-format";
3
+ import { format } from "d3-format";
4
+ import type { Config as VlConfig } from 'vega-lite';
5
+ import type { ISemanticType, VegaGlobalConfig } from "../../interfaces";
6
+
7
+
8
+ const defaultFormatter = (value: unknown): string => `${value}`;
9
+
10
+ export const useDisplayValueFormatter = (semanticType: ISemanticType, vegaConfig: VegaGlobalConfig): (value: unknown) => string => {
11
+ const { timeFormat = "%b %d, %Y", numberFormat } = vegaConfig as Partial<VlConfig>;
12
+ const timeFormatter = useMemo<(value: unknown) => string>(() => {
13
+ const tf = tFormat(timeFormat);
14
+ return (value: unknown) => {
15
+ if (typeof value !== 'number' && typeof value !== 'string') {
16
+ return '';
17
+ }
18
+ const date = new Date(value);
19
+ if (isNaN(date.getTime())) {
20
+ return 'Invalid Date';
21
+ }
22
+ return tf(date);
23
+ };
24
+ }, [timeFormat]);
25
+ const numberFormatter = useMemo<(value: unknown) => string>(() => {
26
+ if (!numberFormat) {
27
+ return (value: unknown) => {
28
+ if (typeof value !== 'number') {
29
+ return '';
30
+ }
31
+ return value.toLocaleString();
32
+ };
33
+ }
34
+ const nf = format(numberFormat);
35
+ return (value: unknown) => {
36
+ if (typeof value !== 'number') {
37
+ return '';
38
+ }
39
+ return nf(value);
40
+ };
41
+ }, [numberFormat]);
42
+ const formatter = useMemo(() => {
43
+ if (semanticType === 'quantitative') {
44
+ return numberFormatter;
45
+ } else if (semanticType === 'temporal') {
46
+ return timeFormatter;
47
+ } else {
48
+ return defaultFormatter;
49
+ }
50
+ }, [semanticType, numberFormatter, timeFormatter]);
51
+ return formatter;
52
+ };
@@ -1,12 +1,16 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import { StoreWrapper, useGlobalStore } from '../../store';
3
- import { PivotTableDataProps, PivotTableStoreWrapper, usePivotTableStore } from './store';
1
+ import React, { useEffect, useMemo, useState, useRef } from 'react';
2
+ import { useGlobalStore } from '../../store';
3
+ import { buildPivotTableService } from '../../services';
4
+ import { toWorkflow } from '../../utils/workflow';
5
+ import { dataQueryServer } from '../../computation/serverComputation';
6
+ import { useAppRootContext } from '../../components/appRoot';
4
7
  import { observer } from 'mobx-react-lite';
5
8
  import LeftTree from './leftTree';
6
9
  import TopTree from './topTree';
7
10
  import {
8
11
  DeepReadonly,
9
12
  DraggableFieldState,
13
+ IComputationFunction,
10
14
  IDarkMode,
11
15
  IRow,
12
16
  IThemeKey,
@@ -14,24 +18,10 @@ import {
14
18
  IVisualConfig,
15
19
  } from '../../interfaces';
16
20
  import { INestNode } from './inteface';
17
- import { buildMetricTableFromNestTree, buildNestTree } from './utils';
18
21
  import { unstable_batchedUpdates } from 'react-dom';
19
22
  import MetricTable from './metricTable';
20
23
  import { toJS } from 'mobx';
21
-
22
- // const PTStateConnector = observer(function StateWrapper (props: PivotTableProps) {
23
- // const store = usePivotTableStore();
24
- // const { vizStore } = useGlobalStore();
25
- // const { draggableFieldState } = vizStore;
26
- // const { rows, columns } = draggableFieldState;
27
- // return (
28
- // <PivotTable
29
- // {...props}
30
- // draggableFieldState={draggableFieldState}
31
- // visualConfig={visualConfig}
32
- // />
33
- // );
34
- // })
24
+ import LoadingLayer from '../loadingLayer';
35
25
 
36
26
  interface PivotTableProps {
37
27
  themeKey?: IThemeKey;
@@ -40,23 +30,31 @@ interface PivotTableProps {
40
30
  loading: boolean;
41
31
  draggableFieldState: DeepReadonly<DraggableFieldState>;
42
32
  visualConfig: DeepReadonly<IVisualConfig>;
33
+ computationFunction: IComputationFunction
43
34
  }
44
- const PivotTable: React.FC<PivotTableProps> = (props) => {
45
- const { data, draggableFieldState } = props;
46
- // const store = usePivotTableStore();
47
- // const { vizStore } = useGlobalStore();
48
- // const { draggableFieldState } = vizStore;
49
- const { rows, columns } = draggableFieldState;
35
+
36
+ const PivotTable: React.FC<PivotTableProps> = observer(function PivotTableComponent (props) {
37
+ const { data, visualConfig, loading, computationFunction } = props;
38
+ const appRef = useAppRootContext();
50
39
  const [leftTree, setLeftTree] = useState<INestNode | null>(null);
51
40
  const [topTree, setTopTree] = useState<INestNode | null>(null);
52
41
  const [metricTable, setMetricTable] = useState<any[][]>([]);
42
+ const [isLoading, setIsLoading] = useState<boolean>(false);
43
+
44
+ const { vizStore, commonStore } = useGlobalStore();
45
+ const { allFields, viewFilters, viewMeasures, sort, limit, draggableFieldState } = vizStore;
46
+ const { rows, columns } = draggableFieldState;
47
+ const { showTableSummary, defaultAggregated } = visualConfig;
48
+ const { tableCollapsedHeaderMap } = commonStore;
49
+ const aggData = useRef<IRow[]>([]);
50
+ const [ topTreeHeaderRowNum, setTopTreeHeaderRowNum ] = useState<number>(0);
53
51
 
54
52
  const dimsInRow = useMemo(() => {
55
- return rows.filter((f) => f.analyticType === 'dimension');
53
+ return toJS(rows).filter((f) => f.analyticType === 'dimension');
56
54
  }, [rows]);
57
55
 
58
56
  const dimsInColumn = useMemo(() => {
59
- return columns.filter((f) => f.analyticType === 'dimension');
57
+ return toJS(columns).filter((f) => f.analyticType === 'dimension');
60
58
  }, [columns]);
61
59
 
62
60
  const measInRow = useMemo(() => {
@@ -68,51 +66,157 @@ const PivotTable: React.FC<PivotTableProps> = (props) => {
68
66
  }, [columns]);
69
67
 
70
68
  useEffect(() => {
71
- if ((dimsInRow.length > 0 || dimsInColumn.length > 0) && data.length > 0) {
72
- const lt = buildNestTree(
73
- dimsInRow.map((d) => d.fid),
74
- data
75
- );
76
- const tt = buildNestTree(
77
- dimsInColumn.map((d) => d.fid),
78
- data
79
- );
80
- const metric = buildMetricTableFromNestTree(lt, tt, data);
81
- unstable_batchedUpdates(() => {
82
- setLeftTree(lt);
83
- setTopTree(tt);
84
- setMetricTable(metric);
85
- });
69
+ if (tableCollapsedHeaderMap.size > 0) {
70
+ // If some visual configs change, clear the collapse state
71
+ // As tableCollapsedHeaderMap is also listened, data will be reaggregated later.
72
+ commonStore.resetTableCollapsedHeader();
73
+ // This forces data to be reaggregated if showTableSummary is on, as aggregation will be skipped later.
74
+ if (showTableSummary) {
75
+ aggregateGroupbyData();
76
+ }
77
+ } else {
78
+ aggregateThenGenerate();
79
+ }
80
+ }, [data]);
81
+
82
+ useEffect(() => {
83
+ if (showTableSummary) {
84
+ // If showTableSummary is on, there is no need to generate extra queries. Directly generate new table.
85
+ generateNewTable();
86
+ } else {
87
+ aggregateThenGenerate();
86
88
  }
87
- }, [dimsInRow, dimsInColumn, data]);
89
+ }, [tableCollapsedHeaderMap]);
90
+
91
+ const aggregateThenGenerate = async() => {
92
+ await aggregateGroupbyData();
93
+ generateNewTable();
94
+ };
95
+
96
+ const generateNewTable = () => {
97
+ appRef.current?.updateRenderStatus('rendering');
98
+ setIsLoading(true);
99
+ buildPivotTableService(
100
+ dimsInRow,
101
+ dimsInColumn,
102
+ data,
103
+ aggData.current,
104
+ Array.from(tableCollapsedHeaderMap.keys()),
105
+ showTableSummary
106
+ )
107
+ .then((data) => {
108
+ const {lt, tt, metric} = data;
109
+ unstable_batchedUpdates(() => {
110
+ setLeftTree(lt);
111
+ setTopTree(tt);
112
+ setMetricTable(metric);
113
+ });
114
+ appRef.current?.updateRenderStatus('idle');
115
+ setIsLoading(false);
116
+ })
117
+ .catch((err) => {
118
+ appRef.current?.updateRenderStatus('error');
119
+ console.log(err);
120
+ setIsLoading(false);
121
+ })
122
+ };
123
+
124
+ const aggregateGroupbyData = () => {
125
+ if (dimsInRow.length === 0 && dimsInColumn.length === 0) return;
126
+ if (data.length === 0) return;
127
+ let groupbyCombListInRow:IViewField[][] = [];
128
+ let groupbyCombListInCol:IViewField[][] = [];
129
+ if (showTableSummary) {
130
+ groupbyCombListInRow = dimsInRow.map((dim, idx) => dimsInRow.slice(0, idx));
131
+ groupbyCombListInCol = dimsInColumn.map((dim, idx) => dimsInColumn.slice(0, idx));
132
+ } else {
133
+ const collapsedDimList = Array.from(tableCollapsedHeaderMap).map(([key, path]) => path[path.length - 1].key);
134
+ const collapsedDimsInRow = dimsInRow.filter((dim) => collapsedDimList.includes(dim.fid));
135
+ const collapsedDimsInColumn = dimsInColumn.filter((dim) => collapsedDimList.includes(dim.fid));
136
+ groupbyCombListInRow = collapsedDimsInRow.map((dim) => dimsInRow.slice(0, dimsInRow.indexOf(dim) + 1));
137
+ groupbyCombListInCol = collapsedDimsInColumn.map((dim) => dimsInColumn.slice(0, dimsInColumn.indexOf(dim) + 1));
138
+ }
139
+ groupbyCombListInRow.push(dimsInRow);
140
+ groupbyCombListInCol.push(dimsInColumn);
141
+ const groupbyCombList:IViewField[][] = groupbyCombListInCol.flatMap(combInCol =>
142
+ groupbyCombListInRow.map(combInRow => [...combInCol, ...combInRow])
143
+ ).slice(0, -1);
144
+ setIsLoading(true);
145
+ appRef.current?.updateRenderStatus('computing');
146
+ const groupbyPromises: Promise<IRow[]>[] = groupbyCombList.map((dimComb) => {
147
+ const workflow = toWorkflow(
148
+ viewFilters,
149
+ allFields,
150
+ dimComb,
151
+ viewMeasures,
152
+ defaultAggregated,
153
+ sort,
154
+ limit > 0 ? limit : undefined
155
+ );
156
+ return dataQueryServer(computationFunction, workflow, limit > 0 ? limit : undefined)
157
+ .catch((err) => {
158
+ appRef.current?.updateRenderStatus('error');
159
+ return [];
160
+ });
161
+ });
162
+ return new Promise<void>((resolve, reject) => {
163
+ Promise.all(groupbyPromises)
164
+ .then((result) => {
165
+ setIsLoading(false);
166
+ const finalizedData = [...result.flat()];
167
+ aggData.current = finalizedData;
168
+ resolve();
169
+ })
170
+ .catch((err) => {
171
+ console.error(err);
172
+ setIsLoading(false);
173
+ reject();
174
+ });
175
+ })
176
+
177
+ };
88
178
 
89
179
  // const { leftTree, topTree, metricTable } = store;
90
180
  return (
91
- <div className="flex">
92
- <table className="border border-gray-300 border-collapse">
93
- <thead className="border border-gray-300">
94
- {new Array(dimsInColumn.length + (measInColumn.length > 0 ? 1 : 0)).fill(0).map((_, i) => (
95
- <tr className="" key={i}>
96
- <td className="p-2 m-1 text-xs text-white border border-gray-300" colSpan={dimsInRow.length + (measInRow.length > 0 ? 1 : 0)}>_</td>
97
- </tr>
98
- ))}
99
- </thead>
100
- {leftTree && <LeftTree data={leftTree} dimsInRow={dimsInRow} measInRow={measInRow} />}
101
- </table>
102
- <table className="border border-gray-300 border-collapse">
103
- {topTree && <TopTree data={topTree} dimsInCol={dimsInColumn} measInCol={measInColumn} />}
104
- {metricTable && <MetricTable matrix={metricTable} meaInColumns={measInColumn} meaInRows={measInRow} />}
105
- </table>
181
+ <div className="relative">
182
+ {(isLoading || loading) && <LoadingLayer />}
183
+ <div className="flex">
184
+ <table className="border border-gray-300 border-collapse">
185
+ <thead className="border border-gray-300">
186
+ {new Array(topTreeHeaderRowNum).fill(0).map((_, i) => (
187
+ <tr className="" key={i}>
188
+ <td className="bg-zinc-100 dark:bg-zinc-800 text-gray-800 dark:text-gray-100 p-2 m-1 text-xs border border-gray-300" colSpan={dimsInRow.length + (measInRow.length > 0 ? 1 : 0)}>_</td>
189
+ </tr>
190
+ ))}
191
+ </thead>
192
+ {leftTree &&
193
+ <LeftTree
194
+ data={leftTree}
195
+ dimsInRow={dimsInRow}
196
+ measInRow={measInRow}
197
+ onHeaderCollapse={commonStore.updateTableCollapsedHeader.bind(commonStore)}
198
+ />}
199
+ </table>
200
+ <table className="border border-gray-300 border-collapse">
201
+ {topTree &&
202
+ <TopTree
203
+ data={topTree}
204
+ dimsInCol={dimsInColumn}
205
+ measInCol={measInColumn}
206
+ onHeaderCollapse={commonStore.updateTableCollapsedHeader.bind(commonStore)}
207
+ onTopTreeHeaderRowNumChange={(num) => setTopTreeHeaderRowNum(num)}
208
+ />}
209
+ {metricTable &&
210
+ <MetricTable
211
+ matrix={metricTable}
212
+ meaInColumns={measInColumn}
213
+ meaInRows={measInRow}
214
+ />}
215
+ </table>
216
+ </div>
106
217
  </div>
107
- );
108
- };
109
218
 
110
- export default PivotTable;
219
+ );
220
+ });
111
221
 
112
- // const PivotTableApp: React.FC<PivotTableProps> = (props) => {
113
- // return (
114
- // <PivotTableStoreWrapper {...props}>
115
- // <PivotTable />
116
- // </PivotTableStoreWrapper>
117
- // );
118
- // };
222
+ export default PivotTable;
@@ -1,8 +1,12 @@
1
1
  import { IAggregator } from "../../interfaces";
2
2
 
3
3
  export interface INestNode {
4
- key: string;
5
- value: string;
4
+ key: string | number;
5
+ value: string | number;
6
+ uniqueKey: string;
6
7
  fieldKey: string;
7
8
  children: INestNode[];
9
+ height: number;
10
+ isCollapsed: boolean;
11
+ path: Record<INestNode["fieldKey"], INestNode["value"]>[];
8
12
  }