@kanaries/graphic-walker 0.2.7 → 0.2.8
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/graphic-walker.es.js +14878 -14717
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +111 -111
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/interfaces.d.ts +10 -1
- package/dist/store/visualSpecStore.d.ts +1 -0
- package/dist/utils/save.d.ts +3 -1
- package/dist/vis/react-vega.d.ts +23 -0
- package/package.json +1 -1
- package/src/interfaces.ts +19 -1
- package/src/locales/en-US.json +12 -0
- package/src/locales/zh-CN.json +12 -0
- package/src/renderer/index.tsx +14 -8
- package/src/store/visualSpecStore.ts +28 -3
- package/src/utils/save.ts +4 -1
- package/src/vis/react-vega.tsx +303 -20
- package/src/visualSettings/index.tsx +64 -2
package/src/vis/react-vega.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo } from 'react';
|
|
1
|
+
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
|
2
2
|
import embed from 'vega-embed';
|
|
3
|
-
import { Subject } from 'rxjs'
|
|
3
|
+
import { Subject, Subscription } from 'rxjs'
|
|
4
4
|
import * as op from 'rxjs/operators';
|
|
5
5
|
import { ScenegraphEvent } from 'vega';
|
|
6
6
|
import { ISemanticType } from 'visual-insights';
|
|
@@ -36,6 +36,8 @@ interface ReactVegaProps {
|
|
|
36
36
|
width: number;
|
|
37
37
|
height: number;
|
|
38
38
|
onGeomClick?: (values: any, e: any) => void
|
|
39
|
+
selectEncoding: SingleViewProps['selectEncoding'];
|
|
40
|
+
brushEncoding: SingleViewProps['brushEncoding'];
|
|
39
41
|
}
|
|
40
42
|
const NULL_FIELD: IViewField = {
|
|
41
43
|
dragId: '',
|
|
@@ -59,6 +61,17 @@ const geomClick$ = selection$.pipe(
|
|
|
59
61
|
function getFieldType(field: IViewField): 'quantitative' | 'nominal' | 'ordinal' | 'temporal' {
|
|
60
62
|
return field.semanticType
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
const BRUSH_SIGNAL_NAME = "__gw_brush__";
|
|
66
|
+
const POINT_SIGNAL_NAME = "__gw_point__";
|
|
67
|
+
|
|
68
|
+
interface ParamStoreEntry {
|
|
69
|
+
signal: typeof BRUSH_SIGNAL_NAME | typeof POINT_SIGNAL_NAME;
|
|
70
|
+
/** 这个标记用于防止循环 */
|
|
71
|
+
source: number;
|
|
72
|
+
data: any;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
interface SingleViewProps {
|
|
63
76
|
x: IViewField;
|
|
64
77
|
y: IViewField;
|
|
@@ -75,6 +88,10 @@ interface SingleViewProps {
|
|
|
75
88
|
defaultAggregated: boolean;
|
|
76
89
|
stack: IStackMode;
|
|
77
90
|
geomType: string;
|
|
91
|
+
enableCrossFilter: boolean;
|
|
92
|
+
asCrossFilterTrigger: boolean;
|
|
93
|
+
selectEncoding: 'default' | 'none';
|
|
94
|
+
brushEncoding: 'x' | 'y' | 'default' | 'none';
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
function availableChannels (geomType: string): Set<string> {
|
|
@@ -163,7 +180,11 @@ function getSingleView(props: SingleViewProps) {
|
|
|
163
180
|
yOffset,
|
|
164
181
|
defaultAggregated,
|
|
165
182
|
stack,
|
|
166
|
-
geomType
|
|
183
|
+
geomType,
|
|
184
|
+
selectEncoding,
|
|
185
|
+
brushEncoding,
|
|
186
|
+
enableCrossFilter,
|
|
187
|
+
asCrossFilterTrigger,
|
|
167
188
|
} = props
|
|
168
189
|
const fields: IViewField[] = [x, y, color, opacity, size, shape, row, column, xOffset, yOffset, theta, radius]
|
|
169
190
|
let markType = geomType;
|
|
@@ -179,15 +200,156 @@ function getSingleView(props: SingleViewProps) {
|
|
|
179
200
|
channelAggregate(encoding, fields);
|
|
180
201
|
}
|
|
181
202
|
channelStack(encoding, stack);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
if (!enableCrossFilter || brushEncoding === 'none' && selectEncoding === 'none') {
|
|
204
|
+
return {
|
|
205
|
+
mark: {
|
|
206
|
+
type: markType,
|
|
207
|
+
opacity: 0.96,
|
|
208
|
+
tooltip: true
|
|
209
|
+
},
|
|
210
|
+
encoding
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const mark = {
|
|
214
|
+
type: markType,
|
|
215
|
+
opacity: 0.96,
|
|
216
|
+
tooltip: true
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// TODO:
|
|
220
|
+
// 鉴于 Vega 中使用 layer 会导致一些难以覆盖的预期外行为,
|
|
221
|
+
// 破坏掉引入交互后视图的正确性,
|
|
222
|
+
// 目前不使用 layer 来实现交互(注掉以下代码)。
|
|
223
|
+
// 考虑 layer 的目的是 layer + condition 可以用于同时展现“全集”(context)和“选中”两层结构,尤其对于聚合数据有分析帮助;
|
|
224
|
+
// 同时,不需要关心作为筛选器的是哪一张图。
|
|
225
|
+
// 现在采用临时方案,区别产生筛选的来源,并对其他图仅借助 transform 展现筛选后的数据。
|
|
226
|
+
// #[BEGIN bad-layer-interaction]
|
|
227
|
+
// const shouldUseMultipleLayers = brushEncoding !== 'none' && Object.values(encoding).some(channel => {
|
|
228
|
+
// return typeof channel.aggregate === 'string' && /* 这种 case 对应行数 */ typeof channel.field === 'string';
|
|
229
|
+
// });
|
|
230
|
+
// if (shouldUseMultipleLayers) {
|
|
231
|
+
// if (['column', 'row'].some(key => key in encoding && encoding[key].length > 0)) {
|
|
232
|
+
// // 这种情况 Vega 不能处理,是因为 Vega 不支持在 layer 中使用 column / row channel,
|
|
233
|
+
// // 会导致渲染出来的视图无法与不使用交互时的结果不变。
|
|
234
|
+
// return {
|
|
235
|
+
// params: [
|
|
236
|
+
// {
|
|
237
|
+
// name: BRUSH_SIGNAL_NAME,
|
|
238
|
+
// select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
|
|
239
|
+
// },
|
|
240
|
+
// ],
|
|
241
|
+
// mark,
|
|
242
|
+
// encoding: {
|
|
243
|
+
// ...encoding,
|
|
244
|
+
// color: {
|
|
245
|
+
// condition: {
|
|
246
|
+
// ...encoding.color,
|
|
247
|
+
// param: BRUSH_SIGNAL_NAME,
|
|
248
|
+
// },
|
|
249
|
+
// value: '#888',
|
|
250
|
+
// },
|
|
251
|
+
// },
|
|
252
|
+
// };
|
|
253
|
+
// }
|
|
254
|
+
// return {
|
|
255
|
+
// layer: [
|
|
256
|
+
// {
|
|
257
|
+
// params: [
|
|
258
|
+
// {
|
|
259
|
+
// name: BRUSH_SIGNAL_NAME,
|
|
260
|
+
// select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
|
|
261
|
+
// },
|
|
262
|
+
// ],
|
|
263
|
+
// mark,
|
|
264
|
+
// encoding: {
|
|
265
|
+
// ...encoding,
|
|
266
|
+
// color: 'color' in encoding ? {
|
|
267
|
+
// condition: {
|
|
268
|
+
// ...encoding.color,
|
|
269
|
+
// test: 'false',
|
|
270
|
+
// },
|
|
271
|
+
// value: '#888',
|
|
272
|
+
// } : {
|
|
273
|
+
// value: '#888',
|
|
274
|
+
// },
|
|
275
|
+
// },
|
|
276
|
+
// },
|
|
277
|
+
// {
|
|
278
|
+
// transform: [{ filter: { param: BRUSH_SIGNAL_NAME }}],
|
|
279
|
+
// mark,
|
|
280
|
+
// encoding: {
|
|
281
|
+
// ...encoding,
|
|
282
|
+
// color: encoding.color ?? { value: 'steelblue' },
|
|
283
|
+
// },
|
|
284
|
+
// },
|
|
285
|
+
// ],
|
|
286
|
+
// };
|
|
287
|
+
// } else if (brushEncoding !== 'none') {
|
|
288
|
+
// return {
|
|
289
|
+
// params: [
|
|
290
|
+
// {
|
|
291
|
+
// name: BRUSH_SIGNAL_NAME,
|
|
292
|
+
// select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
|
|
293
|
+
// },
|
|
294
|
+
// ],
|
|
295
|
+
// mark,
|
|
296
|
+
// encoding: {
|
|
297
|
+
// ...encoding,
|
|
298
|
+
// color: {
|
|
299
|
+
// condition: {
|
|
300
|
+
// ...encoding.color,
|
|
301
|
+
// param: BRUSH_SIGNAL_NAME,
|
|
302
|
+
// },
|
|
303
|
+
// value: '#888',
|
|
304
|
+
// },
|
|
305
|
+
// },
|
|
306
|
+
// };
|
|
307
|
+
// }
|
|
308
|
+
// #[END bad-layer-interaction]
|
|
309
|
+
|
|
310
|
+
if (brushEncoding !== 'none') {
|
|
311
|
+
return {
|
|
312
|
+
transform: asCrossFilterTrigger ? [] : [
|
|
313
|
+
{ filter: { param: BRUSH_SIGNAL_NAME } }
|
|
314
|
+
],
|
|
315
|
+
params: [
|
|
316
|
+
// {
|
|
317
|
+
// name: BRUSH_SIGNAL_DISPLAY_NAME,
|
|
318
|
+
// select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
|
|
319
|
+
// on: '__YOU_CANNOT_MODIFY_THIS_SIGNAL__',
|
|
320
|
+
// },
|
|
321
|
+
{
|
|
322
|
+
name: BRUSH_SIGNAL_NAME,
|
|
323
|
+
select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
mark,
|
|
327
|
+
encoding,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
transform: asCrossFilterTrigger ? [] : [
|
|
333
|
+
{ filter: { param: POINT_SIGNAL_NAME } }
|
|
334
|
+
],
|
|
335
|
+
params: [
|
|
336
|
+
{
|
|
337
|
+
name: POINT_SIGNAL_NAME,
|
|
338
|
+
select: { type: 'point' },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
mark,
|
|
342
|
+
encoding: asCrossFilterTrigger ? {
|
|
343
|
+
...encoding,
|
|
344
|
+
color: {
|
|
345
|
+
condition: {
|
|
346
|
+
...encoding.color,
|
|
347
|
+
param: POINT_SIGNAL_NAME,
|
|
348
|
+
},
|
|
349
|
+
value: '#888',
|
|
350
|
+
},
|
|
351
|
+
} : encoding,
|
|
189
352
|
};
|
|
190
|
-
return spec;
|
|
191
353
|
}
|
|
192
354
|
const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
193
355
|
const {
|
|
@@ -208,7 +370,9 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
208
370
|
interactiveScale,
|
|
209
371
|
layoutMode,
|
|
210
372
|
width,
|
|
211
|
-
height
|
|
373
|
+
height,
|
|
374
|
+
selectEncoding,
|
|
375
|
+
brushEncoding,
|
|
212
376
|
} = props;
|
|
213
377
|
// const container = useRef<HTMLDivElement>(null);
|
|
214
378
|
// const containers = useRef<(HTMLDivElement | null)[]>([]);
|
|
@@ -222,7 +386,7 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
222
386
|
return () => {
|
|
223
387
|
clickSub.unsubscribe();
|
|
224
388
|
}
|
|
225
|
-
}, []);
|
|
389
|
+
}, [onGeomClick]);
|
|
226
390
|
const rowDims = useMemo(() => rows.filter(f => f.analyticType === 'dimension'), [rows]);
|
|
227
391
|
const colDims = useMemo(() => columns.filter(f => f.analyticType === 'dimension'), [columns]);
|
|
228
392
|
const rowMeas = useMemo(() => rows.filter(f => f.analyticType === 'measure'), [rows]);
|
|
@@ -233,8 +397,10 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
233
397
|
const colRepeatFields = useMemo(() => colMeas.length === 0 ? colDims.slice(-1) : colMeas, [rowDims, rowMeas]);//colMeas.slice(0, -1);
|
|
234
398
|
const allFieldIds = useMemo(() => [...rows, ...columns, color, opacity, size].filter(f => Boolean(f)).map(f => (f as IViewField).fid), [rows, columns, color, opacity, size]);
|
|
235
399
|
|
|
400
|
+
const [crossFilterTriggerIdx, setCrossFilterTriggerIdx] = useState(-1);
|
|
236
401
|
|
|
237
402
|
useEffect(() => {
|
|
403
|
+
setCrossFilterTriggerIdx(-1);
|
|
238
404
|
setViewPlaceholders(views => {
|
|
239
405
|
const viewNum = Math.max(1, rowRepeatFields.length * colRepeatFields.length)
|
|
240
406
|
const nextViews = new Array(viewNum).fill(null).map((v, i) => views[i] || React.createRef())
|
|
@@ -295,14 +461,38 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
295
461
|
yOffset: NULL_FIELD,
|
|
296
462
|
defaultAggregated: defaultAggregate,
|
|
297
463
|
stack,
|
|
298
|
-
geomType
|
|
464
|
+
geomType,
|
|
465
|
+
selectEncoding,
|
|
466
|
+
brushEncoding,
|
|
467
|
+
enableCrossFilter: false,
|
|
468
|
+
asCrossFilterTrigger: false,
|
|
299
469
|
});
|
|
300
470
|
// if (layoutMode === 'fixed') {
|
|
301
471
|
// spec.width = 800;
|
|
302
472
|
// spec.height = 600;
|
|
303
473
|
// }
|
|
304
474
|
spec.mark = singleView.mark;
|
|
305
|
-
|
|
475
|
+
if ('encoding' in singleView) {
|
|
476
|
+
spec.encoding = singleView.encoding;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// #[BEGIN bad-layer-interaction]
|
|
480
|
+
// if ('layer' in singleView) {
|
|
481
|
+
// if ('params' in spec) {
|
|
482
|
+
// const basicParams = spec['params'];
|
|
483
|
+
// delete spec['params'];
|
|
484
|
+
// singleView.layer![0].params = [...basicParams, ...singleView.layer![0].params ?? []];
|
|
485
|
+
// }
|
|
486
|
+
// spec.layer = singleView.layer;
|
|
487
|
+
// } else if ('params' in singleView) {
|
|
488
|
+
// spec.params.push(...singleView.params!);
|
|
489
|
+
// }
|
|
490
|
+
// #[END bad-layer-interaction]
|
|
491
|
+
|
|
492
|
+
if ('params' in singleView) {
|
|
493
|
+
spec.params.push(...singleView.params!);
|
|
494
|
+
}
|
|
495
|
+
// console.log(JSON.stringify(spec, undefined, 2));
|
|
306
496
|
if (viewPlaceholders.length > 0 && viewPlaceholders[0].current) {
|
|
307
497
|
embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions }).then(res => {
|
|
308
498
|
try {
|
|
@@ -323,8 +513,22 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
323
513
|
spec.height = Math.floor(height / rowRepeatFields.length) - 5;
|
|
324
514
|
spec.autosize = 'fit'
|
|
325
515
|
}
|
|
516
|
+
const combinedParamStore$ = new Subject<ParamStoreEntry>();
|
|
517
|
+
const throttledParamStore$ = combinedParamStore$.pipe(
|
|
518
|
+
op.throttleTime(
|
|
519
|
+
dataSource.length / 64 * rowRepeatFields.length * colRepeatFields.length,
|
|
520
|
+
undefined,
|
|
521
|
+
{ leading: false, trailing: true }
|
|
522
|
+
)
|
|
523
|
+
);
|
|
524
|
+
const subscriptions: Subscription[] = [];
|
|
525
|
+
const subscribe = (cb: (entry: ParamStoreEntry) => void) => {
|
|
526
|
+
subscriptions.push(throttledParamStore$.subscribe(cb));
|
|
527
|
+
};
|
|
528
|
+
let index = 0;
|
|
326
529
|
for (let i = 0; i < rowRepeatFields.length; i++) {
|
|
327
|
-
for (let j = 0; j < colRepeatFields.length; j++) {
|
|
530
|
+
for (let j = 0; j < colRepeatFields.length; j++, index++) {
|
|
531
|
+
const sourceId = index;
|
|
328
532
|
const singleView = getSingleView({
|
|
329
533
|
x: colRepeatFields[j] || NULL_FIELD,
|
|
330
534
|
y: rowRepeatFields[i] || NULL_FIELD,
|
|
@@ -340,13 +544,87 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
340
544
|
yOffset: NULL_FIELD,
|
|
341
545
|
defaultAggregated: defaultAggregate,
|
|
342
546
|
stack,
|
|
343
|
-
geomType
|
|
547
|
+
geomType,
|
|
548
|
+
selectEncoding,
|
|
549
|
+
brushEncoding,
|
|
550
|
+
enableCrossFilter: crossFilterTriggerIdx !== -1,
|
|
551
|
+
asCrossFilterTrigger: crossFilterTriggerIdx === sourceId,
|
|
344
552
|
});
|
|
345
553
|
const node = i * colRepeatFields.length + j < viewPlaceholders.length ? viewPlaceholders[i * colRepeatFields.length + j].current : null
|
|
346
|
-
|
|
554
|
+
let commonSpec = { ...spec };
|
|
555
|
+
|
|
556
|
+
// #[BEGIN bad-layer-interaction]
|
|
557
|
+
// if ('layer' in singleView) {
|
|
558
|
+
// if ('params' in commonSpec) {
|
|
559
|
+
// const { params: basicParams, ...spec } = commonSpec;
|
|
560
|
+
// commonSpec = spec;
|
|
561
|
+
// singleView.layer![0].params = [...basicParams, ...singleView.layer![0].params ?? []];
|
|
562
|
+
// }
|
|
563
|
+
// commonSpec.layer = singleView.layer;
|
|
564
|
+
// } else if ('params' in singleView) {
|
|
565
|
+
// commonSpec.params = [...commonSpec.params, ...singleView.params!];
|
|
566
|
+
// }
|
|
567
|
+
// #[END bad-layer-interaction]
|
|
568
|
+
|
|
569
|
+
if ('params' in singleView) {
|
|
570
|
+
commonSpec.params = [...commonSpec.params, ...singleView.params!];
|
|
571
|
+
}
|
|
572
|
+
const ans = { ...commonSpec, ...singleView }
|
|
573
|
+
if ('params' in commonSpec) {
|
|
574
|
+
ans.params = commonSpec.params;
|
|
575
|
+
}
|
|
576
|
+
// console.log(JSON.stringify(ans, undefined, 2));
|
|
347
577
|
if (node) {
|
|
348
578
|
embed(node, ans, { mode: 'vega-lite', actions: showActions }).then(res => {
|
|
579
|
+
// 这种 case 下,我们来考虑联动的 params
|
|
580
|
+
// vega 使用 Data 来维护 params 的状态,只需要打通这些状态就可以实现联动
|
|
581
|
+
const paramStores = (res.vgSpec.data?.map(d => d.name) ?? []).filter(
|
|
582
|
+
name => [BRUSH_SIGNAL_NAME, POINT_SIGNAL_NAME].map(p => `${p}_store`).includes(name)
|
|
583
|
+
).map(name => name.replace(/_store$/, ''));
|
|
349
584
|
try {
|
|
585
|
+
for (const param of paramStores) {
|
|
586
|
+
let noBroadcasting = false;
|
|
587
|
+
// 发出
|
|
588
|
+
res.view.addSignalListener(param, name => {
|
|
589
|
+
if (noBroadcasting) {
|
|
590
|
+
noBroadcasting = false;
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if ([BRUSH_SIGNAL_NAME, POINT_SIGNAL_NAME].includes(name)) {
|
|
594
|
+
const data = res.view.getState().data?.[`${name}_store`];
|
|
595
|
+
if (!data || (Array.isArray(data) && data.length === 0)) {
|
|
596
|
+
setCrossFilterTriggerIdx(-1);
|
|
597
|
+
}
|
|
598
|
+
combinedParamStore$.next({
|
|
599
|
+
signal: name as typeof BRUSH_SIGNAL_NAME | typeof POINT_SIGNAL_NAME,
|
|
600
|
+
source: sourceId,
|
|
601
|
+
data: data ?? null,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
// 订阅
|
|
606
|
+
subscribe(entry => {
|
|
607
|
+
if (entry.source === sourceId || !entry.data) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// 防止被动更新触发广播
|
|
611
|
+
noBroadcasting = true;
|
|
612
|
+
res.view.setState({
|
|
613
|
+
data: {
|
|
614
|
+
[`${entry.signal}_store`]: entry.data,
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.warn('Crossing filter failed', error);
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
res.view.addEventListener('mouseover', () => {
|
|
624
|
+
if (sourceId !== crossFilterTriggerIdx) {
|
|
625
|
+
setCrossFilterTriggerIdx(sourceId);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
350
628
|
res.view.addEventListener('click', (e) => {
|
|
351
629
|
click$.next(e);
|
|
352
630
|
})
|
|
@@ -360,8 +638,10 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
360
638
|
}
|
|
361
639
|
}
|
|
362
640
|
}
|
|
641
|
+
return () => {
|
|
642
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
643
|
+
};
|
|
363
644
|
}
|
|
364
|
-
|
|
365
645
|
}, [
|
|
366
646
|
dataSource,
|
|
367
647
|
allFieldIds,
|
|
@@ -384,7 +664,10 @@ const ReactVega: React.FC<ReactVegaProps> = props => {
|
|
|
384
664
|
interactiveScale,
|
|
385
665
|
layoutMode,
|
|
386
666
|
width,
|
|
387
|
-
height
|
|
667
|
+
height,
|
|
668
|
+
selectEncoding,
|
|
669
|
+
brushEncoding,
|
|
670
|
+
crossFilterTriggerIdx,
|
|
388
671
|
]);
|
|
389
672
|
|
|
390
673
|
return <CanvaContainer rowSize={Math.max(rowRepeatFields.length, 1)} colSize={Math.max(colRepeatFields.length, 1)}>
|
|
@@ -6,9 +6,9 @@ import { ArrowPathIcon } from '@heroicons/react/24/solid';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { LiteForm } from '../components/liteForm';
|
|
8
8
|
import SizeSetting from '../components/sizeSetting';
|
|
9
|
-
import {
|
|
9
|
+
import { GEMO_TYPES, STACK_MODE, CHART_LAYOUT_TYPE } from '../config';
|
|
10
10
|
import { useGlobalStore } from '../store';
|
|
11
|
-
import { IStackMode } from '../interfaces';
|
|
11
|
+
import { IStackMode, EXPLORATION_TYPES, IBrushDirection, BRUSH_DIRECTIONS } from '../interfaces';
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
export const LiteContainer = styled.div`
|
|
@@ -226,6 +226,68 @@ const VisualSettings: React.FC = () => {
|
|
|
226
226
|
{t('size')}
|
|
227
227
|
</label>
|
|
228
228
|
</div>
|
|
229
|
+
<div className="item">
|
|
230
|
+
<label
|
|
231
|
+
id="dropdown:exploration_mode:label"
|
|
232
|
+
htmlFor="dropdown:exploration_mode"
|
|
233
|
+
>
|
|
234
|
+
{tGlobal(`constant.exploration_mode.__enum__`)}
|
|
235
|
+
</label>
|
|
236
|
+
<select
|
|
237
|
+
className="border border-gray-500 rounded-sm text-xs pt-0.5 pb-0.5 pl-2 pr-2 cursor-pointer"
|
|
238
|
+
id="dropdown:exploration_mode"
|
|
239
|
+
aria-describedby="dropdown:exploration_mode:label"
|
|
240
|
+
value={visualConfig.exploration.mode}
|
|
241
|
+
onChange={e => {
|
|
242
|
+
vizStore.setExploration({
|
|
243
|
+
mode: e.target.value as (typeof EXPLORATION_TYPES)[number]
|
|
244
|
+
});
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
{EXPLORATION_TYPES.map(g => (
|
|
248
|
+
<option
|
|
249
|
+
key={g}
|
|
250
|
+
value={g}
|
|
251
|
+
className="cursor-pointer"
|
|
252
|
+
aria-selected={visualConfig.exploration.mode === g}
|
|
253
|
+
>
|
|
254
|
+
{tGlobal(`constant.exploration_mode.${g}`)}
|
|
255
|
+
</option>
|
|
256
|
+
))}
|
|
257
|
+
</select>
|
|
258
|
+
</div>
|
|
259
|
+
<div className="item" style={{ opacity: visualConfig.exploration.mode !== 'brush' ? 0.3 : undefined }}>
|
|
260
|
+
<label
|
|
261
|
+
id="dropdown:brush_mode:label"
|
|
262
|
+
htmlFor="dropdown:brush_mode"
|
|
263
|
+
>
|
|
264
|
+
{tGlobal(`constant.brush_mode.__enum__`)}
|
|
265
|
+
</label>
|
|
266
|
+
<select
|
|
267
|
+
className="border border-gray-500 rounded-sm text-xs pt-0.5 pb-0.5 pl-2 pr-2 cursor-pointer"
|
|
268
|
+
id="dropdown:brush_mode"
|
|
269
|
+
aria-describedby="dropdown:brush_mode:label"
|
|
270
|
+
disabled={visualConfig.exploration.mode !== 'brush'}
|
|
271
|
+
aria-disabled={visualConfig.exploration.mode !== 'brush'}
|
|
272
|
+
value={visualConfig.exploration.brushDirection}
|
|
273
|
+
onChange={e => {
|
|
274
|
+
vizStore.setExploration({
|
|
275
|
+
brushDirection: e.target.value as IBrushDirection
|
|
276
|
+
});
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{BRUSH_DIRECTIONS.map(g => (
|
|
280
|
+
<option
|
|
281
|
+
key={g}
|
|
282
|
+
value={g}
|
|
283
|
+
className="cursor-pointer"
|
|
284
|
+
aria-selected={visualConfig.exploration.brushDirection === g}
|
|
285
|
+
>
|
|
286
|
+
{tGlobal(`constant.brush_mode.${g}`)}
|
|
287
|
+
</option>
|
|
288
|
+
))}
|
|
289
|
+
</select>
|
|
290
|
+
</div>
|
|
229
291
|
<div className="item">
|
|
230
292
|
<input
|
|
231
293
|
type="checkbox"
|