@kanaries/graphic-walker 0.4.0 → 0.4.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.
Files changed (73) 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/dataTable/index.d.ts +2 -1
  4. package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +21 -0
  5. package/dist/components/leafletRenderer/POIRenderer.d.ts +19 -0
  6. package/dist/components/leafletRenderer/encodings.d.ts +7 -0
  7. package/dist/components/leafletRenderer/geoConfigPanel.d.ts +3 -0
  8. package/dist/components/leafletRenderer/index.d.ts +14 -0
  9. package/dist/components/leafletRenderer/tooltip.d.ts +9 -0
  10. package/dist/components/leafletRenderer/utils.d.ts +2 -0
  11. package/dist/components/pivotTable/index.d.ts +2 -1
  12. package/dist/components/pivotTable/inteface.d.ts +6 -2
  13. package/dist/components/pivotTable/leftTree.d.ts +1 -0
  14. package/dist/components/pivotTable/topTree.d.ts +2 -0
  15. package/dist/components/pivotTable/utils.d.ts +1 -2
  16. package/dist/config.d.ts +3 -2
  17. package/dist/graphic-walker.es.js +37802 -30402
  18. package/dist/graphic-walker.es.js.map +1 -1
  19. package/dist/graphic-walker.umd.js +145 -137
  20. package/dist/graphic-walker.umd.js.map +1 -1
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/interfaces.d.ts +27 -0
  23. package/dist/renderer/specRenderer.d.ts +2 -1
  24. package/dist/services.d.ts +7 -1
  25. package/dist/store/commonStore.d.ts +6 -0
  26. package/dist/store/visualSpecStore.d.ts +180 -4
  27. package/dist/utils/save.d.ts +1 -0
  28. package/dist/workers/buildPivotTable.d.ts +7 -0
  29. package/package.json +14 -2
  30. package/src/App.tsx +18 -4
  31. package/src/components/askViz/index.tsx +2 -1
  32. package/src/components/dataTable/index.tsx +7 -5
  33. package/src/components/leafletRenderer/ChoroplethRenderer.tsx +293 -0
  34. package/src/components/leafletRenderer/POIRenderer.tsx +170 -0
  35. package/src/components/leafletRenderer/encodings.ts +194 -0
  36. package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
  37. package/src/components/leafletRenderer/index.tsx +67 -0
  38. package/src/components/leafletRenderer/tooltip.tsx +24 -0
  39. package/src/components/leafletRenderer/utils.ts +52 -0
  40. package/src/components/limitSetting.tsx +8 -6
  41. package/src/components/pivotTable/index.tsx +171 -67
  42. package/src/components/pivotTable/inteface.ts +6 -2
  43. package/src/components/pivotTable/leftTree.tsx +24 -11
  44. package/src/components/pivotTable/metricTable.tsx +15 -10
  45. package/src/components/pivotTable/topTree.tsx +50 -17
  46. package/src/components/pivotTable/utils.ts +70 -11
  47. package/src/components/sizeSetting.tsx +9 -7
  48. package/src/components/visualConfig/index.tsx +17 -1
  49. package/src/computation/serverComputation.ts +8 -3
  50. package/src/config.ts +27 -16
  51. package/src/dataSource/table.tsx +9 -9
  52. package/src/fields/aestheticFields.tsx +4 -0
  53. package/src/fields/fieldsContext.tsx +3 -0
  54. package/src/fields/posFields/index.tsx +8 -2
  55. package/src/global.d.ts +4 -4
  56. package/src/hooks/index.ts +8 -0
  57. package/src/index.tsx +11 -9
  58. package/src/interfaces.ts +34 -0
  59. package/src/locales/en-US.json +27 -2
  60. package/src/locales/ja-JP.json +27 -2
  61. package/src/locales/zh-CN.json +27 -2
  62. package/src/renderer/hooks.ts +2 -48
  63. package/src/renderer/index.tsx +24 -1
  64. package/src/renderer/pureRenderer.tsx +26 -13
  65. package/src/renderer/specRenderer.tsx +45 -30
  66. package/src/services.ts +32 -23
  67. package/src/shadow-dom.tsx +7 -0
  68. package/src/store/commonStore.ts +29 -1
  69. package/src/store/visualSpecStore.ts +40 -24
  70. package/src/utils/save.ts +28 -1
  71. package/src/visualSettings/index.tsx +58 -7
  72. package/src/workers/buildMetricTable.worker.js +27 -0
  73. package/src/workers/buildPivotTable.ts +27 -0
@@ -0,0 +1,194 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { scaleLinear, scaleOrdinal } from "d3-scale";
3
+ import type { IRow, IViewField, VegaGlobalConfig } from "../../interfaces";
4
+ import { getMeaAggKey } from "../../utils";
5
+
6
+
7
+ export interface Scale<T> {
8
+ (record: IRow): T;
9
+ }
10
+
11
+ const DEFAULT_COLOR = "#5B8FF9";
12
+ const DEFAULT_COLOR_STEP_1 = "#EBCCFF";
13
+ const DEFAULT_COLOR_STEP_2 = "#0D1090";
14
+ const DEFAULT_SCHEME_CATEGORY = [
15
+ "#5B8FF9",
16
+ "#61DDAA",
17
+ "#65789B",
18
+ "#F6BD16",
19
+ "#7262FD",
20
+ "#78D3F8",
21
+ "#9661BC",
22
+ "#F6903D",
23
+ "#008685",
24
+ "#F08BB4",
25
+ ];
26
+
27
+ export const useColorScale = (data: IRow[], field: IViewField | null | undefined, defaultAggregate: boolean, vegaConfig: VegaGlobalConfig): Scale<string> => {
28
+ const color = (vegaConfig as any).circle?.fill || DEFAULT_COLOR;
29
+ const fixedScale = useCallback(function ColorScale (row: IRow) {
30
+ return color;
31
+ }, [color]);
32
+ const colorRange = useMemo(() => {
33
+ if ('scale' in vegaConfig && typeof vegaConfig.scale === 'object' && 'continuous' in vegaConfig.scale) {
34
+ if (Array.isArray((vegaConfig.scale?.continuous as any).range)) {
35
+ return ((vegaConfig.scale?.continuous as any).range as string[]).slice(0, 2);
36
+ }
37
+ }
38
+ return [DEFAULT_COLOR_STEP_1, DEFAULT_COLOR_STEP_2];
39
+ }, [vegaConfig]);
40
+ const schemeCategory = useMemo(() => {
41
+ if (Array.isArray(vegaConfig.range?.category)) {
42
+ return vegaConfig.range!.category as string[];
43
+ }
44
+ return DEFAULT_SCHEME_CATEGORY;
45
+ }, [vegaConfig]);
46
+ const key = useMemo(() => {
47
+ if (!field) {
48
+ return '';
49
+ }
50
+ if (defaultAggregate && field.aggName && field.analyticType === 'measure') {
51
+ return getMeaAggKey(field.fid, field.aggName);
52
+ }
53
+ return field.fid;
54
+ }, [field, defaultAggregate]);
55
+ const domain = useMemo<[number, number]>(() => {
56
+ if (!field || field.semanticType === 'nominal') {
57
+ return [0, 0];
58
+ }
59
+ return data.reduce((dom: [number, number], { [key]: cur }) => {
60
+ if (cur < dom[0]) {
61
+ dom[0] = cur;
62
+ }
63
+ if (cur > dom[1]) {
64
+ dom[1] = cur;
65
+ }
66
+ return dom;
67
+ }, [Infinity, -Infinity]);
68
+ }, [data, field, key]);
69
+ const distributions = useMemo(() => {
70
+ if (!field || field.semanticType !== 'nominal') {
71
+ return [];
72
+ }
73
+ return [...data.reduce((set: Set<string>, row) => {
74
+ set.add(row[key]);
75
+ return set;
76
+ }, new Set<string>())];
77
+ }, [data, field, key]);
78
+ const continuousScale = useMemo(() => {
79
+ const scale = scaleLinear<string, string>().domain(domain).range(colorRange);
80
+ return function ColorScale (row: IRow) {
81
+ return scale(Number(row[key]));
82
+ };
83
+ }, [domain, key, colorRange]);
84
+ const discreteScale = useMemo(() => {
85
+ const scale = scaleOrdinal<string, string>().domain(distributions).range(schemeCategory);
86
+ return function ColorScale (row: IRow) {
87
+ return scale(row[key]);
88
+ };
89
+ }, [distributions, schemeCategory]);
90
+
91
+ if (!field) {
92
+ return fixedScale;
93
+ }
94
+ if (field.semanticType === 'quantitative' || field.semanticType === 'temporal') {
95
+ // continuous
96
+ return continuousScale;
97
+ }
98
+ return discreteScale;
99
+ };
100
+
101
+ const MIN_SIZE = 2;
102
+ const MAX_SIZE = 10;
103
+ const DEFAULT_SIZE = 3;
104
+
105
+ export const useSizeScale = (data: IRow[], field: IViewField | null | undefined, defaultAggregate: boolean): Scale<number> => {
106
+ const key = useMemo(() => {
107
+ if (!field) {
108
+ return '';
109
+ }
110
+ if (defaultAggregate && field.aggName && field.analyticType === 'measure') {
111
+ return getMeaAggKey(field.fid, field.aggName);
112
+ }
113
+ return field.fid;
114
+ }, [field, defaultAggregate]);
115
+
116
+ const [domainMin, domainMax] = useMemo(() => {
117
+ if (!key) {
118
+ return [0, 0];
119
+ }
120
+ const values = data.map((row) => Number(row[key])).filter((val) => !isNaN(val));
121
+ if (values.length === 0) {
122
+ return [0, 0];
123
+ }
124
+ return values.slice(1).reduce<[number, number]>((acc, val) => {
125
+ if (val < acc[0]) {
126
+ acc[0] = val;
127
+ }
128
+ if (val > acc[1]) {
129
+ acc[1] = val;
130
+ }
131
+ return acc;
132
+ }, [values[0], values[0]]);
133
+ }, [key, data]);
134
+
135
+ return useCallback(function SizeScale (record: IRow): number {
136
+ if (!key) {
137
+ return DEFAULT_SIZE;
138
+ }
139
+ const val = Number(record[key]);
140
+ if (isNaN(val)) {
141
+ return 0;
142
+ }
143
+ const size = (val - domainMin) / (domainMax - domainMin);
144
+ return MIN_SIZE + Math.sqrt(size) * (MAX_SIZE - MIN_SIZE);
145
+ }, [key, domainMin, domainMax, defaultAggregate]);
146
+ };
147
+
148
+
149
+ const MIN_OPACITY = 0.33;
150
+ const MAX_OPACITY = 1.0;
151
+ const DEFAULT_OPACITY = 1;
152
+
153
+ export const useOpacityScale = (data: IRow[], field: IViewField | null | undefined, defaultAggregate: boolean): Scale<number> => {
154
+ const key = useMemo(() => {
155
+ if (!field) {
156
+ return '';
157
+ }
158
+ if (defaultAggregate && field.aggName && field.analyticType === 'measure') {
159
+ return getMeaAggKey(field.fid, field.aggName);
160
+ }
161
+ return field.fid;
162
+ }, [field, defaultAggregate]);
163
+
164
+ const [domainMin, domainMax] = useMemo(() => {
165
+ if (!key) {
166
+ return [0, 0];
167
+ }
168
+ const values = data.map((row) => Number(row[key])).filter((val) => !isNaN(val));
169
+ if (values.length === 0) {
170
+ return [0, 0];
171
+ }
172
+ return values.slice(1).reduce<[number, number]>((acc, val) => {
173
+ if (val < acc[0]) {
174
+ acc[0] = val;
175
+ }
176
+ if (val > acc[1]) {
177
+ acc[1] = val;
178
+ }
179
+ return acc;
180
+ }, [values[0], values[0]]);
181
+ }, [key, data]);
182
+
183
+ return useCallback(function OpacityScale (record: IRow): number {
184
+ if (!key) {
185
+ return DEFAULT_OPACITY;
186
+ }
187
+ const val = Number(record[key]);
188
+ if (isNaN(val)) {
189
+ return 0;
190
+ }
191
+ const size = (val - domainMin) / (domainMax - domainMin);
192
+ return MIN_OPACITY + size * (MAX_OPACITY - MIN_OPACITY);
193
+ }, [key, domainMin, domainMax, defaultAggregate]);
194
+ };
@@ -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,67 @@
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
+ vegaConfig?: VegaGlobalConfig;
9
+ draggableFieldState: DeepReadonly<DraggableFieldState>;
10
+ visualConfig: DeepReadonly<IVisualConfig>;
11
+ data: IRow[];
12
+ }
13
+
14
+ export interface ILeafletRendererRef {}
15
+
16
+ export const LEAFLET_DEFAULT_WIDTH = 800;
17
+ export const LEAFLET_DEFAULT_HEIGHT = 600;
18
+
19
+ const LeafletRenderer = forwardRef<ILeafletRendererRef, ILeafletRendererProps>(function LeafletRenderer (props, ref) {
20
+ const { draggableFieldState, data, visualConfig, vegaConfig = {} } = props;
21
+ const { latitude: [lat], longitude: [lng], geoId: [geoId], dimensions, measures, size: [size], color: [color], opacity: [opacity], text: [text], details } = draggableFieldState;
22
+ const { defaultAggregated, geoms: [markType], geojson, geoKey = '', scaleIncludeUnmatchedChoropleth = false } = visualConfig;
23
+ const allFields = useMemo(() => [...dimensions, ...measures], [dimensions, measures]);
24
+ const latField = useMemo(() => allFields.find((f) => f.geoRole === 'latitude'), [allFields]);
25
+ const lngField = useMemo(() => allFields.find((f) => f.geoRole === 'longitude'), [allFields]);
26
+ const latitude = useMemo(() => lat ?? latField, [lat, latField]);
27
+ const longitude = useMemo(() => lng ?? lngField, [lng, lngField]);
28
+
29
+ if (markType === 'poi') {
30
+ return (
31
+ <POIRenderer
32
+ data={data}
33
+ allFields={allFields}
34
+ defaultAggregated={defaultAggregated}
35
+ latitude={latitude}
36
+ longitude={longitude}
37
+ color={color}
38
+ opacity={opacity}
39
+ size={size}
40
+ details={details}
41
+ vegaConfig={vegaConfig}
42
+ />
43
+ );
44
+ } else if (markType === 'choropleth') {
45
+ return (
46
+ <ChoroplethRenderer
47
+ data={data}
48
+ allFields={allFields}
49
+ features={geojson}
50
+ geoKey={geoKey}
51
+ defaultAggregated={defaultAggregated}
52
+ geoId={geoId}
53
+ color={color}
54
+ opacity={opacity}
55
+ text={text}
56
+ details={details}
57
+ vegaConfig={vegaConfig}
58
+ scaleIncludeUnmatchedChoropleth={scaleIncludeUnmatchedChoropleth}
59
+ />
60
+ );
61
+ }
62
+
63
+ return null;
64
+ });
65
+
66
+
67
+ 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,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
+ import { useDebounceValueBind } from '../hooks';
3
4
 
4
5
  export default function LimitSetting(props: { value: number; setValue: (v: number) => void }) {
5
6
  const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
7
+ const [innerValue, setInnerValue] = useDebounceValueBind(props.value, v => props.setValue(v));
6
8
 
7
9
  return (
8
10
  <div className=" mt-2">
@@ -10,15 +12,15 @@ export default function LimitSetting(props: { value: number; setValue: (v: numbe
10
12
  className="w-full h-2 bg-blue-100 appearance-none"
11
13
  type="range"
12
14
  name="limit"
13
- value={props.value > 0 ? props.value : 0}
15
+ value={innerValue > 0 ? innerValue : 0}
14
16
  min="1"
15
17
  max="50"
16
- disabled={props.value < 0}
18
+ disabled={innerValue < 0}
17
19
  step="1"
18
20
  onChange={(e) => {
19
21
  const v = parseInt(e.target.value);
20
22
  if (!isNaN(v)) {
21
- props.setValue(v);
23
+ setInnerValue(v);
22
24
  }
23
25
  }}
24
26
  />
@@ -26,12 +28,12 @@ export default function LimitSetting(props: { value: number; setValue: (v: numbe
26
28
  <input
27
29
  type="checkbox"
28
30
  className="h-4 w-4 mr-1 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
29
- checked={props.value > 0}
31
+ checked={innerValue > 0}
30
32
  onChange={(e) => {
31
- props.setValue(e.target.checked ? 30 : -1);
33
+ setInnerValue(e.target.checked ? 30 : -1);
32
34
  }}
33
35
  ></input>
34
- {`${t('limit')}${props.value > 0 ? `: ${props.value}` : ''}`}
36
+ {`${t('limit')}${innerValue > 0 ? `: ${innerValue}` : ''}`}
35
37
  </output>
36
38
  </div>
37
39
  );