@kanaries/graphic-walker 0.3.16 → 0.4.0
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/dist/App.d.ts +9 -2
- package/dist/assets/filter.worker-f09fcd6f.js.map +1 -1
- package/dist/assets/sort.worker-f77540ac.js.map +1 -0
- package/dist/assets/transform.worker-bae8e910.js.map +1 -0
- package/dist/assets/{viewQuery.worker-03404216.js.map → viewQuery.worker-bdb6477c.js.map} +1 -1
- package/dist/components/askViz/index.d.ts +6 -0
- package/dist/components/askViz/schemaTransform.d.ts +2 -0
- package/dist/components/dataTable/index.d.ts +8 -5
- package/dist/components/pivotTable/store.d.ts +0 -2
- package/dist/components/spinner.d.ts +2 -0
- package/dist/computation/clientComputation.d.ts +3 -0
- package/dist/computation/serverComputation.d.ts +8 -0
- package/dist/config.d.ts +3 -1
- package/dist/fields/filterField/tabs.d.ts +2 -1
- package/dist/graphic-walker.es.js +22226 -21650
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +137 -137
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/interfaces.d.ts +93 -4
- package/dist/lib/execExp.d.ts +4 -4
- package/dist/lib/interfaces.d.ts +1 -0
- package/dist/lib/viewQuery.d.ts +2 -2
- package/dist/renderer/hooks.d.ts +8 -4
- package/dist/renderer/index.d.ts +2 -1
- package/dist/renderer/pureRenderer.d.ts +17 -1
- package/dist/renderer/specRenderer.d.ts +1 -0
- package/dist/services.d.ts +8 -5
- package/dist/store/commonStore.d.ts +2 -2
- package/dist/store/visualSpecStore.d.ts +58 -42
- package/dist/utils/save.d.ts +10 -2
- package/dist/utils/workflow.d.ts +3 -0
- package/dist/vis/react-vega.d.ts +3 -1
- package/dist/workers/sort.d.ts +2 -2
- package/dist/workers/transform.d.ts +5 -2
- package/package.json +2 -2
- package/src/App.tsx +46 -7
- package/src/components/askViz/index.tsx +92 -0
- package/src/components/askViz/schemaTransform.ts +38 -0
- package/src/components/dataTable/index.tsx +51 -11
- package/src/components/pivotTable/index.tsx +0 -1
- package/src/components/pivotTable/store.tsx +0 -16
- package/src/components/spinner.tsx +14 -0
- package/src/components/toggle.tsx +2 -2
- package/src/components/visualConfig/index.tsx +78 -8
- package/src/computation/clientComputation.ts +55 -0
- package/src/computation/serverComputation.ts +153 -0
- package/src/config.ts +15 -2
- package/src/dataSource/datasetConfig/index.tsx +38 -6
- package/src/dataSource/table.tsx +11 -2
- package/src/fields/filterField/filterEditDialog.tsx +11 -10
- package/src/fields/filterField/tabs.tsx +178 -77
- package/src/index.tsx +2 -0
- package/src/interfaces.ts +108 -5
- package/src/lib/execExp.ts +20 -11
- package/src/lib/interfaces.ts +1 -0
- package/src/lib/op/aggregate.ts +1 -1
- package/src/lib/viewQuery.ts +2 -2
- package/src/locales/en-US.json +11 -2
- package/src/locales/ja-JP.json +11 -2
- package/src/locales/zh-CN.json +11 -2
- package/src/main.tsx +1 -1
- package/src/renderer/hooks.ts +100 -66
- package/src/renderer/index.tsx +10 -6
- package/src/renderer/pureRenderer.tsx +40 -14
- package/src/renderer/specRenderer.tsx +24 -7
- package/src/services.ts +7 -8
- package/src/store/commonStore.ts +7 -7
- package/src/store/visualSpecStore.ts +287 -192
- package/src/utils/save.ts +81 -3
- package/src/utils/workflow.ts +148 -0
- package/src/vis/react-vega.tsx +21 -6
- package/src/vis/spec/aggregate.ts +3 -2
- package/src/vis/spec/stack.ts +7 -6
- package/src/visualSettings/index.tsx +2 -3
- package/src/workers/filter.worker.js +1 -1
- package/src/workers/sort.ts +3 -4
- package/src/workers/sort.worker.ts +2 -2
- package/src/workers/transform.ts +7 -8
- package/src/workers/transform.worker.js +2 -2
- package/src/workers/viewQuery.worker.js +2 -2
- package/dist/assets/sort.worker-4299a6a0.js.map +0 -1
- package/dist/assets/transform.worker-a12fb3d8.js.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
|
|
6
|
-
import type { IFilterField, IFilterRule } from '../../interfaces';
|
|
6
|
+
import type { IFilterField, IFilterRule, IRow, DataSet, IFieldStats, IField, IViewField } from '../../interfaces';
|
|
7
7
|
import { useGlobalStore } from '../../store';
|
|
8
|
+
import LoadingLayer from '../../components/loadingLayer';
|
|
9
|
+
import { useComputationFunc, useRenderer } from '../../renderer/hooks';
|
|
10
|
+
import { fieldStatServer } from '../../computation/serverComputation';
|
|
8
11
|
import Slider from './slider';
|
|
9
12
|
import {
|
|
10
13
|
ChevronDownIcon,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
} from '@heroicons/react/24/outline';
|
|
13
16
|
|
|
14
17
|
export type RuleFormProps = {
|
|
18
|
+
dataset: DataSet;
|
|
15
19
|
field: IFilterField;
|
|
16
20
|
onChange: (rule: IFilterRule) => void;
|
|
17
21
|
};
|
|
@@ -27,15 +31,15 @@ const Container = styled.div`
|
|
|
27
31
|
> * {
|
|
28
32
|
margin-inline-start: 0.6em;
|
|
29
33
|
|
|
30
|
-
&:first-child
|
|
34
|
+
&:first-child {
|
|
31
35
|
margin-inline-start: 0;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
35
39
|
`;
|
|
36
40
|
|
|
37
41
|
export const Button = styled.button`
|
|
38
|
-
:hover
|
|
42
|
+
:hover {
|
|
39
43
|
background-color: rgba(243, 244, 246, 0.5);
|
|
40
44
|
};
|
|
41
45
|
color: rgb(55, 65, 81);
|
|
@@ -127,13 +131,75 @@ const StatusCheckbox: React.FC<{ currentNum: number; totalNum: number; onChange:
|
|
|
127
131
|
)
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
type FieldDistributionEntry = IFieldStats['values'][number];
|
|
135
|
+
|
|
136
|
+
const defaultValueComparator = (a: any, b: any) => {
|
|
137
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
138
|
+
return a - b;
|
|
139
|
+
} else {
|
|
140
|
+
return String(a).localeCompare(String(b))
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const countCmp = (a: FieldDistributionEntry, b: FieldDistributionEntry) => {
|
|
145
|
+
return a.count - b.count;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const useFieldStats = (
|
|
149
|
+
field: IField,
|
|
150
|
+
attributes: { values: boolean; range: boolean },
|
|
151
|
+
sortBy: 'value' | 'value_dsc' | 'count' | 'count_dsc' | 'none'
|
|
152
|
+
): IFieldStats | null => {
|
|
153
|
+
const { values, range } = attributes;
|
|
154
|
+
const { fid, cmp = defaultValueComparator } = field;
|
|
155
|
+
const valueCmp = React.useCallback<typeof countCmp>((a, b) => {
|
|
156
|
+
return cmp(a.value, b.value);
|
|
157
|
+
}, [cmp]);
|
|
158
|
+
const comparator = sortBy === "none" ? null : sortBy.startsWith("value") ? valueCmp : countCmp;
|
|
159
|
+
const sortMulti = sortBy.endsWith("dsc") ? -1 : 1;
|
|
160
|
+
const [loading, setLoading] = React.useState(true);
|
|
161
|
+
const [stats, setStats] = React.useState<IFieldStats | null>(null);
|
|
162
|
+
const computationFunction = useComputationFunc();
|
|
163
|
+
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
setLoading(true);
|
|
166
|
+
let isCancelled = false;
|
|
167
|
+
fieldStatServer(computationFunction, fid, { values, range }).then(stats => {
|
|
168
|
+
if (isCancelled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setStats(stats);
|
|
172
|
+
setLoading(false);
|
|
173
|
+
}).catch(reason => {
|
|
174
|
+
console.warn(reason);
|
|
175
|
+
if (isCancelled) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
setStats(null);
|
|
179
|
+
setLoading(false);
|
|
180
|
+
});
|
|
181
|
+
return () => {
|
|
182
|
+
isCancelled = true;
|
|
183
|
+
};
|
|
184
|
+
}, [fid, computationFunction, values, range]);
|
|
185
|
+
|
|
186
|
+
const sortedStats = React.useMemo<typeof stats>(() => {
|
|
187
|
+
if (!stats || !comparator) {
|
|
188
|
+
return stats;
|
|
189
|
+
}
|
|
190
|
+
const copy = { ...stats };
|
|
191
|
+
copy.values = copy.values.slice().sort((a,b) => sortMulti * comparator(a,b));
|
|
192
|
+
return copy;
|
|
193
|
+
}, [stats, comparator, sortMulti]);
|
|
194
|
+
|
|
195
|
+
return loading ? null : sortedStats;
|
|
196
|
+
};
|
|
197
|
+
|
|
130
198
|
export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
131
199
|
active,
|
|
132
200
|
field,
|
|
133
201
|
onChange,
|
|
134
202
|
}) => {
|
|
135
|
-
const { commonStore } = useGlobalStore();
|
|
136
|
-
const { currentDataset: { dataSource } } = commonStore;
|
|
137
203
|
|
|
138
204
|
interface SortConfig {
|
|
139
205
|
key: 'value' | 'count';
|
|
@@ -144,67 +210,41 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
144
210
|
ascending: true
|
|
145
211
|
});
|
|
146
212
|
|
|
147
|
-
const count = React.useMemo(() => {
|
|
148
|
-
return dataSource.reduce<Map<string | number, number>>((tmp, d) => {
|
|
149
|
-
const val = d[field.fid];
|
|
150
|
-
|
|
151
|
-
tmp.set(val, (tmp.get(val) ?? 0) + 1);
|
|
152
|
-
|
|
153
|
-
return tmp;
|
|
154
|
-
}, new Map<string | number, number>());
|
|
155
|
-
}, [dataSource, field]);
|
|
156
|
-
|
|
157
|
-
const sortedList = useMemo(() => {
|
|
158
|
-
const entries = Array.from(count.entries());
|
|
159
|
-
const compare = (a: [string | number, number], b: [string | number, number]) => {
|
|
160
|
-
if (sortConfig.key === 'count') {
|
|
161
|
-
return a[1] - b[1];
|
|
162
|
-
} else {
|
|
163
|
-
if (typeof a[0] === 'number' && typeof b[0] === 'number') {
|
|
164
|
-
return a[0] - b[0];
|
|
165
|
-
} else {
|
|
166
|
-
return String(a[0]).localeCompare(String(b[0]))
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
entries.sort(sortConfig.ascending ? compare : (a, b) => -compare(a, b));
|
|
171
|
-
return entries;
|
|
172
|
-
}, [count, sortConfig]);
|
|
173
|
-
|
|
174
213
|
const { t } = useTranslation('translation');
|
|
175
214
|
|
|
215
|
+
const stats = useFieldStats(field, { values: true, range: false }, `${sortConfig.key}${sortConfig.ascending ? '': '_dsc'}`);
|
|
216
|
+
const count = stats?.values;
|
|
217
|
+
|
|
176
218
|
React.useEffect(() => {
|
|
177
|
-
if (active && field.rule?.type !== 'one of') {
|
|
219
|
+
if (count && active && field.rule?.type !== 'one of') {
|
|
178
220
|
onChange({
|
|
179
221
|
type: 'one of',
|
|
180
|
-
value: new Set<string | number>(count.
|
|
222
|
+
value: new Set<string | number>(count.map(item => item.value)),
|
|
181
223
|
});
|
|
182
224
|
}
|
|
183
225
|
}, [active, onChange, field, count]);
|
|
184
226
|
|
|
185
227
|
const handleToggleFullOrEmptySet = () => {
|
|
186
|
-
if (!field.rule || field.rule.type !== 'one of') return;
|
|
228
|
+
if (!field.rule || field.rule.type !== 'one of' || !count) return;
|
|
187
229
|
const curSet = field.rule.value;
|
|
188
230
|
onChange({
|
|
189
231
|
type: 'one of',
|
|
190
232
|
value: new Set<number | string>(
|
|
191
|
-
curSet.size === count.
|
|
192
|
-
? []
|
|
193
|
-
: count.keys()
|
|
233
|
+
curSet.size === count.length ? [] : count.map(c => c.value)
|
|
194
234
|
),
|
|
195
235
|
});
|
|
196
236
|
}
|
|
197
237
|
const handleToggleReverseSet = () => {
|
|
198
|
-
if (!field.rule || field.rule.type !== 'one of') return;
|
|
238
|
+
if (!field.rule || field.rule.type !== 'one of' || !count) return;
|
|
199
239
|
const curSet = field.rule.value;
|
|
200
240
|
onChange({
|
|
201
241
|
type: 'one of',
|
|
202
242
|
value: new Set<number | string>(
|
|
203
|
-
|
|
243
|
+
count.map(c => c.value).filter(key => !curSet.has(key))
|
|
204
244
|
),
|
|
205
245
|
});
|
|
206
246
|
}
|
|
207
|
-
const handleSelectValue = (value, checked) => {
|
|
247
|
+
const handleSelectValue = (value: any, checked: boolean) => {
|
|
208
248
|
if (!field.rule || field.rule?.type !== 'one of') return;
|
|
209
249
|
const rule: IFilterRule = {
|
|
210
250
|
type: 'one of',
|
|
@@ -219,12 +259,20 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
219
259
|
}
|
|
220
260
|
|
|
221
261
|
const selectedValueSum = useMemo(() => {
|
|
222
|
-
if (!field.rule) return 0;
|
|
262
|
+
if (!field.rule?.value || !count) return 0;
|
|
223
263
|
return [...field.rule.value].reduce<number>((sum, key) => {
|
|
224
|
-
const s =
|
|
264
|
+
const s = count.find(c => c.value === key)?.count || 0;
|
|
225
265
|
return sum + s;
|
|
226
266
|
}, 0)
|
|
227
|
-
}, [field.rule?.value]);
|
|
267
|
+
}, [field.rule?.value, count, field.fid]);
|
|
268
|
+
|
|
269
|
+
if (!stats) {
|
|
270
|
+
return (
|
|
271
|
+
<div className="h-24 w-full relative">
|
|
272
|
+
<LoadingLayer />
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
228
276
|
|
|
229
277
|
const SortButton: React.FC<{ currentKey: SortConfig["key"] }> = ({ currentKey }) => {
|
|
230
278
|
const isCurrentKey = sortConfig.key === currentKey;
|
|
@@ -249,9 +297,10 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
249
297
|
<Button
|
|
250
298
|
className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
251
299
|
onClick={() => handleToggleFullOrEmptySet()}
|
|
300
|
+
disabled={!count}
|
|
252
301
|
>
|
|
253
302
|
{
|
|
254
|
-
field.rule.value.size === count
|
|
303
|
+
field.rule.value.size === count?.length
|
|
255
304
|
? t('filters.btn.unselect_all')
|
|
256
305
|
: t('filters.btn.select_all')
|
|
257
306
|
}
|
|
@@ -267,7 +316,7 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
267
316
|
<div className="flex justify-center items-center">
|
|
268
317
|
<StatusCheckbox
|
|
269
318
|
currentNum={field.rule.value.size}
|
|
270
|
-
totalNum={count
|
|
319
|
+
totalNum={count?.length ?? 0}
|
|
271
320
|
onChange={handleToggleFullOrEmptySet}
|
|
272
321
|
/>
|
|
273
322
|
</div>
|
|
@@ -283,7 +332,7 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
283
332
|
{/* <hr /> */}
|
|
284
333
|
<Table>
|
|
285
334
|
{
|
|
286
|
-
|
|
335
|
+
count?.map(({ value, count }, idx) => {
|
|
287
336
|
const id = `rule_checkbox_${idx}`;
|
|
288
337
|
|
|
289
338
|
return (
|
|
@@ -339,9 +388,11 @@ interface CalendarInputProps {
|
|
|
339
388
|
const CalendarInput: React.FC<CalendarInputProps> = props => {
|
|
340
389
|
const { min, max, value, onChange } = props;
|
|
341
390
|
const dateStringFormatter = (timestamp: number) => {
|
|
342
|
-
|
|
391
|
+
const date = new Date(timestamp);
|
|
392
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
393
|
+
return date.toISOString().slice(0, 19);
|
|
343
394
|
}
|
|
344
|
-
const handleSubmitDate = (value) => {
|
|
395
|
+
const handleSubmitDate = (value: string) => {
|
|
345
396
|
if (new Date(value).getTime() <= max && new Date(value).getTime() >= min) {
|
|
346
397
|
onChange(new Date(value).getTime())
|
|
347
398
|
}
|
|
@@ -358,18 +409,48 @@ const CalendarInput: React.FC<CalendarInputProps> = props => {
|
|
|
358
409
|
)
|
|
359
410
|
}
|
|
360
411
|
|
|
412
|
+
const emptyFilters = [] as const;
|
|
413
|
+
|
|
361
414
|
export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
415
|
+
dataset,
|
|
362
416
|
active,
|
|
363
417
|
field,
|
|
364
418
|
onChange,
|
|
365
419
|
}) => {
|
|
366
|
-
const {
|
|
367
|
-
const { currentDataset: { dataSource } } = commonStore;
|
|
420
|
+
const { rawFields } = dataset;
|
|
368
421
|
|
|
369
422
|
const { t } = useTranslation('translation');
|
|
370
423
|
|
|
424
|
+
const fields = useMemo(() => {
|
|
425
|
+
return rawFields.map<Omit<IViewField, 'dragId'>>(f => ({
|
|
426
|
+
...f,
|
|
427
|
+
name: f.name || f.fid,
|
|
428
|
+
}));
|
|
429
|
+
}, [rawFields]);
|
|
430
|
+
|
|
431
|
+
const viewDimensions = useMemo(() => {
|
|
432
|
+
return field.analyticType === 'dimension' ? [field] : [];
|
|
433
|
+
}, [field]);
|
|
434
|
+
|
|
435
|
+
const viewMeasures = useMemo(() => {
|
|
436
|
+
return field.analyticType === 'measure' ? [field] : [];
|
|
437
|
+
}, [field]);
|
|
438
|
+
|
|
439
|
+
const computationFunction = useComputationFunc();
|
|
440
|
+
|
|
441
|
+
const { viewData, loading } = useRenderer({
|
|
442
|
+
allFields: fields,
|
|
443
|
+
viewDimensions,
|
|
444
|
+
viewMeasures,
|
|
445
|
+
filters: emptyFilters,
|
|
446
|
+
defaultAggregated: false,
|
|
447
|
+
computationFunction,
|
|
448
|
+
limit: 1000,
|
|
449
|
+
sort: 'none',
|
|
450
|
+
});
|
|
451
|
+
|
|
371
452
|
const sorted = React.useMemo(() => {
|
|
372
|
-
return
|
|
453
|
+
return viewData.reduce<number[]>((list, d) => {
|
|
373
454
|
try {
|
|
374
455
|
const time = new Date(d[field.fid]).getTime();
|
|
375
456
|
|
|
@@ -379,20 +460,30 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
379
460
|
}
|
|
380
461
|
return list;
|
|
381
462
|
}, []).sort((a, b) => a - b);
|
|
382
|
-
}, [
|
|
463
|
+
}, [viewData, field.fid]);
|
|
383
464
|
|
|
384
|
-
const [min, max] = React.useMemo(() => {
|
|
385
|
-
|
|
465
|
+
const [min, max, loaded] = React.useMemo<[min: number, max: number, loaded: boolean]>(() => {
|
|
466
|
+
if (!sorted.length) return [0, 0, false];
|
|
467
|
+
return [sorted[0] ?? 0, Math.max(sorted[sorted.length - 1] ?? 0, sorted[0] ?? 0), true];
|
|
386
468
|
}, [sorted]);
|
|
387
469
|
|
|
388
470
|
React.useEffect(() => {
|
|
389
471
|
if (active && field.rule?.type !== 'temporal range') {
|
|
390
472
|
onChange({
|
|
391
473
|
type: 'temporal range',
|
|
392
|
-
value: [
|
|
474
|
+
value: [min, max],
|
|
393
475
|
});
|
|
394
476
|
}
|
|
395
|
-
}, [onChange, field,
|
|
477
|
+
}, [onChange, field, min, max, active]);
|
|
478
|
+
|
|
479
|
+
React.useEffect(() => {
|
|
480
|
+
if (active && loaded && field.rule?.type === 'temporal range' && field.rule.value[0] !== min && field.rule.value[1] !== max) {
|
|
481
|
+
onChange({
|
|
482
|
+
type: 'temporal range',
|
|
483
|
+
value: [min, max],
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}, [field.rule, min, max, active, loaded]);
|
|
396
487
|
|
|
397
488
|
const handleChange = React.useCallback((value: readonly [number, number]) => {
|
|
398
489
|
onChange({
|
|
@@ -401,6 +492,14 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
401
492
|
});
|
|
402
493
|
}, []);
|
|
403
494
|
|
|
495
|
+
if (loading) {
|
|
496
|
+
return (
|
|
497
|
+
<div className="h-24 w-full relative">
|
|
498
|
+
<LoadingLayer />
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
404
503
|
return field.rule?.type === 'temporal range' ? (
|
|
405
504
|
<Container className="overflow-visible">
|
|
406
505
|
<div>{t('constant.filter_type.temporal_range')}</div>
|
|
@@ -434,27 +533,19 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
434
533
|
field,
|
|
435
534
|
onChange,
|
|
436
535
|
}) => {
|
|
437
|
-
const { commonStore } = useGlobalStore();
|
|
438
|
-
const { currentDataset: { dataSource } } = commonStore;
|
|
439
|
-
|
|
440
536
|
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
441
537
|
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
}, [dataSource, field]);
|
|
445
|
-
|
|
446
|
-
const [min, max] = React.useMemo(() => {
|
|
447
|
-
return [sorted[0] ?? 0, Math.max(sorted[sorted.length - 1] ?? 0, sorted[0] ?? 0)];
|
|
448
|
-
}, [sorted]);
|
|
538
|
+
const stats = useFieldStats(field, { values: false, range: true }, 'none');
|
|
539
|
+
const range = stats?.range;
|
|
449
540
|
|
|
450
541
|
React.useEffect(() => {
|
|
451
|
-
if (active && field.rule?.type !== 'range') {
|
|
542
|
+
if (range && active && field.rule?.type !== 'range') {
|
|
452
543
|
onChange({
|
|
453
544
|
type: 'range',
|
|
454
|
-
value:
|
|
545
|
+
value: range,
|
|
455
546
|
});
|
|
456
547
|
}
|
|
457
|
-
}, [onChange, field,
|
|
548
|
+
}, [onChange, field, range, active]);
|
|
458
549
|
|
|
459
550
|
const handleChange = React.useCallback((value: readonly [number, number]) => {
|
|
460
551
|
onChange({
|
|
@@ -463,13 +554,21 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
463
554
|
});
|
|
464
555
|
}, []);
|
|
465
556
|
|
|
557
|
+
if (!range) {
|
|
558
|
+
return (
|
|
559
|
+
<div className="h-24 w-full relative">
|
|
560
|
+
<LoadingLayer />
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
466
565
|
return field.rule?.type === 'range' ? (
|
|
467
566
|
<Container>
|
|
468
567
|
<div>{t('range')}</div>
|
|
469
568
|
<div className="text-gray-500">{t('range_desc')}</div>
|
|
470
569
|
<Slider
|
|
471
|
-
min={
|
|
472
|
-
max={
|
|
570
|
+
min={range[0]}
|
|
571
|
+
max={range[1]}
|
|
473
572
|
value={field.rule.value}
|
|
474
573
|
onChange={handleChange}
|
|
475
574
|
/>
|
|
@@ -503,8 +602,9 @@ export interface TabsProps extends RuleFormProps {
|
|
|
503
602
|
}
|
|
504
603
|
|
|
505
604
|
const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
506
|
-
const { vizStore } = useGlobalStore();
|
|
605
|
+
const { vizStore, commonStore } = useGlobalStore();
|
|
507
606
|
const { draggableFieldState } = vizStore;
|
|
607
|
+
const { currentDataset } = commonStore;
|
|
508
608
|
|
|
509
609
|
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
510
610
|
|
|
@@ -563,6 +663,7 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
563
663
|
field={field}
|
|
564
664
|
onChange={onChange}
|
|
565
665
|
active={which === tab}
|
|
666
|
+
dataset={currentDataset}
|
|
566
667
|
/>
|
|
567
668
|
</TabItem>
|
|
568
669
|
);
|
package/src/index.tsx
CHANGED
|
@@ -38,3 +38,5 @@ export const GraphicWalker = observer(forwardRef<IGWHandler, IGWProps>((props, r
|
|
|
38
38
|
export { default as PureRenderer } from './renderer/pureRenderer';
|
|
39
39
|
export { embedGraphicWalker } from './vanilla';
|
|
40
40
|
export type { IGWProps };
|
|
41
|
+
export { ISegmentKey } from './interfaces';
|
|
42
|
+
export { resolveSpecFromStoInfo } from './utils/save';
|
package/src/interfaces.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {Config as VgConfig, View} from 'vega';
|
|
2
2
|
import {Config as VlConfig} from 'vega-lite';
|
|
3
|
+
import type {IViewQuery} from "./lib/viewQuery";
|
|
3
4
|
|
|
4
5
|
export type DeepReadonly<T extends Record<keyof any, any>> = {
|
|
5
6
|
readonly [K in keyof T]: T[K] extends Record<keyof any, any> ? DeepReadonly<T[K]> : T[K];
|
|
@@ -52,6 +53,15 @@ export interface IUncertainMutField {
|
|
|
52
53
|
path: string[];
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export interface IDatasetStats {
|
|
57
|
+
rowCount: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IFieldStats {
|
|
61
|
+
values: { value: number | string; count: number }[];
|
|
62
|
+
range: [number, number];
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
export type IExpParamter =
|
|
56
66
|
| {
|
|
57
67
|
type: 'field';
|
|
@@ -95,12 +105,12 @@ export interface IField {
|
|
|
95
105
|
computed?: boolean;
|
|
96
106
|
expression?: IExpression;
|
|
97
107
|
basename?: string;
|
|
98
|
-
path?: [],
|
|
108
|
+
path?: string[],
|
|
99
109
|
}
|
|
100
|
-
|
|
110
|
+
export type ISortMode = 'none' | 'ascending' | 'descending';
|
|
101
111
|
export interface IViewField extends IField {
|
|
102
112
|
dragId: string;
|
|
103
|
-
sort?:
|
|
113
|
+
sort?: ISortMode;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
export interface DataSet {
|
|
@@ -157,6 +167,11 @@ export interface IFilterField extends IViewField {
|
|
|
157
167
|
rule: IFilterRule | null;
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
export interface IFilterFiledSimple {
|
|
171
|
+
fid: string;
|
|
172
|
+
rule: IFilterRule | null;
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
export interface DraggableFieldState {
|
|
161
176
|
dimensions: IViewField[];
|
|
162
177
|
measures: IViewField[];
|
|
@@ -192,7 +207,7 @@ export type IFilterRule =
|
|
|
192
207
|
value: Set<string | number>;
|
|
193
208
|
};
|
|
194
209
|
|
|
195
|
-
export type IStackMode = 'none' | 'stack' | 'normalize';
|
|
210
|
+
export type IStackMode = 'none' | 'stack' | 'normalize' | 'zero' | 'center';
|
|
196
211
|
|
|
197
212
|
export interface IVisualConfig {
|
|
198
213
|
defaultAggregated: boolean;
|
|
@@ -200,18 +215,28 @@ export interface IVisualConfig {
|
|
|
200
215
|
stack: IStackMode;
|
|
201
216
|
showActions: boolean;
|
|
202
217
|
interactiveScale: boolean;
|
|
203
|
-
sorted:
|
|
218
|
+
sorted: ISortMode;
|
|
204
219
|
zeroScale: boolean;
|
|
220
|
+
background?: string;
|
|
205
221
|
format: {
|
|
206
222
|
numberFormat?: string;
|
|
207
223
|
timeFormat?: string;
|
|
208
224
|
normalizedNumberFormat?: string;
|
|
209
225
|
};
|
|
226
|
+
resolve: {
|
|
227
|
+
x?: boolean;
|
|
228
|
+
y?: boolean;
|
|
229
|
+
color?: boolean;
|
|
230
|
+
opacity?: boolean;
|
|
231
|
+
shape?: boolean;
|
|
232
|
+
size?: boolean;
|
|
233
|
+
};
|
|
210
234
|
size: {
|
|
211
235
|
mode: 'auto' | 'fixed';
|
|
212
236
|
width: number;
|
|
213
237
|
height: number;
|
|
214
238
|
};
|
|
239
|
+
limit: number;
|
|
215
240
|
}
|
|
216
241
|
|
|
217
242
|
export interface IVisSpec {
|
|
@@ -238,6 +263,7 @@ export enum ISegmentKey {
|
|
|
238
263
|
|
|
239
264
|
export type IThemeKey = 'vega' | 'g2';
|
|
240
265
|
export type IDarkMode = 'media' | 'light' | 'dark';
|
|
266
|
+
export type IComputationFunction = (payload: IDataQueryPayload) => Promise<IRow[]>;
|
|
241
267
|
|
|
242
268
|
export type VegaGlobalConfig = VgConfig | VlConfig;
|
|
243
269
|
|
|
@@ -345,3 +371,80 @@ export interface IGWHandler {
|
|
|
345
371
|
export interface IGWHandlerInsider extends IGWHandler {
|
|
346
372
|
updateRenderStatus: (renderStatus: IRenderStatus) => void;
|
|
347
373
|
}
|
|
374
|
+
|
|
375
|
+
export interface IVisField {
|
|
376
|
+
key: string;
|
|
377
|
+
type: ISemanticType;
|
|
378
|
+
name?: string;
|
|
379
|
+
description?: string;
|
|
380
|
+
format?: string;
|
|
381
|
+
expression?: IExpression;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export type IVisFieldComputation = {
|
|
385
|
+
field: IVisField['key'];
|
|
386
|
+
expression: NonNullable<IVisField['expression']>;
|
|
387
|
+
name: NonNullable<IVisField['name']>;
|
|
388
|
+
type: IVisField['type'];
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export interface IVisFilter {
|
|
392
|
+
fid: string;
|
|
393
|
+
rule: SetToArray<IFilterRule>;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export interface IFilterWorkflowStep {
|
|
397
|
+
type: 'filter';
|
|
398
|
+
filters: IVisFilter[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface ITransformWorkflowStep {
|
|
402
|
+
type: 'transform';
|
|
403
|
+
transform: {
|
|
404
|
+
key: IVisFieldComputation['field'];
|
|
405
|
+
expression: IVisFieldComputation['expression'];
|
|
406
|
+
}[];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface IViewWorkflowStep {
|
|
410
|
+
type: 'view';
|
|
411
|
+
query: IViewQuery[];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface ISortWorkflowStep {
|
|
415
|
+
type: 'sort';
|
|
416
|
+
sort: 'ascending' | 'descending';
|
|
417
|
+
by: string[];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export type IDataQueryWorkflowStep = IFilterWorkflowStep | ITransformWorkflowStep | IViewWorkflowStep | ISortWorkflowStep;
|
|
421
|
+
|
|
422
|
+
export interface IDataQueryPayload {
|
|
423
|
+
workflow: IDataQueryWorkflowStep[];
|
|
424
|
+
limit?: number;
|
|
425
|
+
offset?: number;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface ILoadDataPayload {
|
|
429
|
+
pageSize: number;
|
|
430
|
+
pageIndex: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface IGWDatasetStat {
|
|
434
|
+
count: number;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export type IResponse<T> = (
|
|
438
|
+
| {
|
|
439
|
+
success: true;
|
|
440
|
+
data: T;
|
|
441
|
+
}
|
|
442
|
+
| {
|
|
443
|
+
success: false;
|
|
444
|
+
message: string;
|
|
445
|
+
error?: {
|
|
446
|
+
code: `ERR_${Uppercase<string>}`;
|
|
447
|
+
options?: Record<string, string>;
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
);
|
package/src/lib/execExp.ts
CHANGED
|
@@ -4,7 +4,7 @@ interface IDataFrame {
|
|
|
4
4
|
[key: string]: any[];
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function execExpression (exp: IExpression, dataFrame: IDataFrame
|
|
7
|
+
export function execExpression (exp: IExpression, dataFrame: IDataFrame): IDataFrame {
|
|
8
8
|
const { op, params } = exp;
|
|
9
9
|
const subFrame: IDataFrame = { ...dataFrame };
|
|
10
10
|
const len = dataFrame[Object.keys(dataFrame)[0]].length;
|
|
@@ -17,7 +17,7 @@ export function execExpression (exp: IExpression, dataFrame: IDataFrame, columns
|
|
|
17
17
|
subFrame[param.value] = new Array(len).fill(param.value);
|
|
18
18
|
break;
|
|
19
19
|
case 'expression':
|
|
20
|
-
let f = execExpression(param.value, dataFrame
|
|
20
|
+
let f = execExpression(param.value, dataFrame);
|
|
21
21
|
Object.keys(f).forEach(key => {
|
|
22
22
|
subFrame[key] = f[key];
|
|
23
23
|
})
|
|
@@ -54,11 +54,18 @@ function bin(resKey: string, params: IExpParamter[], data: IDataFrame, binSize:
|
|
|
54
54
|
if (val < _min) _min = val;
|
|
55
55
|
}
|
|
56
56
|
const step = (_max - _min) / binSize;
|
|
57
|
-
|
|
57
|
+
// prevent (_max - _min) to be 0
|
|
58
|
+
const safeWidth = Math.min(Number.MAX_SAFE_INTEGER, Math.max(_max - _min, Number.MIN_VALUE));
|
|
59
|
+
const beaStep = Math.max(-Math.round(Math.log10(safeWidth)) + 2, 0)
|
|
60
|
+
// toFix() accepts 0-100
|
|
61
|
+
const safeBeaStep = Math.min(100, Math.max(0, Math.max(Number.isFinite(beaStep) ? beaStep : 0, 0)));
|
|
58
62
|
const newValues = fieldValues.map((v: number) => {
|
|
59
63
|
let bIndex = Math.floor((v - _min) / step);
|
|
60
64
|
if (bIndex === binSize) bIndex = binSize - 1;
|
|
61
|
-
|
|
65
|
+
if (Number.isNaN(bIndex)) {
|
|
66
|
+
bIndex = 0;
|
|
67
|
+
}
|
|
68
|
+
return Number(((bIndex * step + _min)).toFixed(safeBeaStep))
|
|
62
69
|
});
|
|
63
70
|
return {
|
|
64
71
|
...data,
|
|
@@ -124,22 +131,24 @@ function one(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFra
|
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
export function dataset2DataFrame(dataset: IRow[]
|
|
134
|
+
export function dataset2DataFrame(dataset: IRow[]): IDataFrame {
|
|
128
135
|
const dataFrame: IDataFrame = {};
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
if (dataset.length === 0) return dataFrame;
|
|
137
|
+
Object.keys(dataset[0]).forEach((k) => {
|
|
138
|
+
dataFrame[k] = dataset.map((row) => row[k]);
|
|
131
139
|
});
|
|
132
140
|
return dataFrame;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
export function dataframe2Dataset(dataFrame: IDataFrame
|
|
136
|
-
|
|
143
|
+
export function dataframe2Dataset(dataFrame: IDataFrame): IRow[] {
|
|
144
|
+
const cols = Object.keys(dataFrame);
|
|
145
|
+
if (cols.length === 0) return [];
|
|
137
146
|
const dataset: IRow[] = [];
|
|
138
147
|
const len = dataFrame[Object.keys(dataFrame)[0]].length;
|
|
139
148
|
for (let i = 0; i < len; i++) {
|
|
140
149
|
const row: IRow = {};
|
|
141
|
-
|
|
142
|
-
row[
|
|
150
|
+
cols.forEach((k) => {
|
|
151
|
+
row[k] = dataFrame[k][i];
|
|
143
152
|
});
|
|
144
153
|
dataset.push(row);
|
|
145
154
|
}
|