@kanaries/graphic-walker 0.2.14 → 0.2.16

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 (128) hide show
  1. package/dist/App.d.ts +5 -2
  2. package/dist/assets/explainer.worker-8428eb12.js.map +1 -1
  3. package/dist/assets/transform.worker-5d54ff09.js.map +1 -0
  4. package/dist/assets/viewQuery.worker-ffefc111.js.map +1 -0
  5. package/dist/components/callout.d.ts +2 -0
  6. package/dist/components/codeExport/index.d.ts +3 -0
  7. package/dist/components/loadingLayer.d.ts +2 -0
  8. package/dist/components/tabs/defaultTab.d.ts +1 -0
  9. package/dist/components/tabs/editableTab.d.ts +1 -2
  10. package/dist/components/toolbar/components.d.ts +4 -1
  11. package/dist/components/toolbar/index.d.ts +2 -0
  12. package/dist/components/toolbar/toolbar-item.d.ts +3 -0
  13. package/dist/components/tooltip.d.ts +2 -0
  14. package/dist/dataSource/dataSelection/config.d.ts +1 -0
  15. package/dist/dataSource/dataSelection/utils.d.ts +2 -0
  16. package/dist/datasets/tmp/test.json +1 -0
  17. package/dist/fields/components.d.ts +0 -1
  18. package/dist/fields/filterField/filterEditDialog.d.ts +1 -1
  19. package/dist/graphic-walker.es.js +23930 -23320
  20. package/dist/graphic-walker.es.js.map +1 -1
  21. package/dist/graphic-walker.umd.js +143 -273
  22. package/dist/graphic-walker.umd.js.map +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/interfaces.d.ts +23 -1
  25. package/dist/lib/execExp.d.ts +8 -0
  26. package/dist/lib/interfaces.d.ts +22 -0
  27. package/dist/lib/op/aggregate.d.ts +3 -0
  28. package/dist/lib/op/bin.d.ts +3 -0
  29. package/dist/lib/op/fold.d.ts +3 -0
  30. package/dist/lib/op/stat.d.ts +8 -0
  31. package/dist/lib/viewQuery.d.ts +5 -0
  32. package/dist/models/visSpecHistory.d.ts +2 -0
  33. package/dist/renderer/index.d.ts +6 -3
  34. package/dist/renderer/specRenderer.d.ts +13 -0
  35. package/dist/services.d.ts +4 -1
  36. package/dist/store/commonStore.d.ts +6 -0
  37. package/dist/store/index.d.ts +3 -2
  38. package/dist/store/visualSpecStore.d.ts +11 -4
  39. package/dist/utils/dataPrep.d.ts +2 -0
  40. package/dist/utils/index.d.ts +3 -5
  41. package/dist/utils/media.d.ts +2 -1
  42. package/dist/utils/save.d.ts +1 -2
  43. package/dist/vis/react-vega.d.ts +4 -23
  44. package/dist/vis/spec/aggregate.d.ts +4 -0
  45. package/dist/vis/spec/encode.d.ts +19 -0
  46. package/dist/vis/spec/field.d.ts +2 -0
  47. package/dist/vis/spec/mark.d.ts +7 -0
  48. package/dist/vis/spec/stack.d.ts +4 -0
  49. package/dist/vis/spec/view.d.ts +67 -0
  50. package/dist/vis/theme.d.ts +36 -20
  51. package/dist/visualSettings/index.d.ts +2 -1
  52. package/dist/workers/transform.d.ts +2 -0
  53. package/package.json +4 -3
  54. package/src/App.tsx +23 -15
  55. package/src/components/callout.tsx +9 -7
  56. package/src/components/clickMenu.tsx +1 -7
  57. package/src/components/codeExport/index.tsx +114 -0
  58. package/src/components/dataTable/index.tsx +10 -10
  59. package/src/components/loadingLayer.tsx +7 -0
  60. package/src/components/modal.tsx +1 -15
  61. package/src/components/sizeSetting.tsx +2 -2
  62. package/src/components/tabs/defaultTab.tsx +4 -2
  63. package/src/components/tabs/editableTab.tsx +75 -40
  64. package/src/components/toolbar/components.tsx +8 -23
  65. package/src/components/toolbar/index.tsx +11 -4
  66. package/src/components/toolbar/toolbar-button.tsx +2 -1
  67. package/src/components/toolbar/toolbar-item.tsx +17 -12
  68. package/src/components/toolbar/toolbar-select-button.tsx +9 -13
  69. package/src/components/toolbar/toolbar-toggle-button.tsx +2 -1
  70. package/src/components/tooltip.tsx +10 -6
  71. package/src/dataSource/dataSelection/config.ts +11 -0
  72. package/src/dataSource/dataSelection/csvData.tsx +72 -40
  73. package/src/dataSource/dataSelection/gwFile.tsx +2 -2
  74. package/src/dataSource/dataSelection/utils.ts +28 -0
  75. package/src/dataSource/index.tsx +2 -3
  76. package/src/dataSource/utils.ts +8 -3
  77. package/src/fields/components.tsx +13 -50
  78. package/src/fields/datasetFields/index.tsx +3 -4
  79. package/src/fields/datasetFields/meaFields.tsx +12 -4
  80. package/src/fields/encodeFields/singleEncodeEditor.tsx +1 -1
  81. package/src/fields/filterField/filterEditDialog.tsx +63 -99
  82. package/src/fields/filterField/slider.tsx +1 -1
  83. package/src/index.css +4 -4
  84. package/src/index.tsx +22 -22
  85. package/src/insightBoard/mainBoard.tsx +9 -2
  86. package/src/interfaces.ts +30 -3
  87. package/src/lib/execExp.ts +147 -0
  88. package/src/lib/interfaces.ts +39 -0
  89. package/src/lib/op/aggregate.ts +49 -0
  90. package/src/lib/op/bin.ts +25 -0
  91. package/src/lib/op/fold.ts +17 -0
  92. package/src/lib/op/stat.ts +46 -0
  93. package/src/lib/viewQuery.ts +23 -0
  94. package/src/locales/en-US.json +8 -3
  95. package/src/locales/i18n.ts +7 -1
  96. package/src/locales/ja-JP.json +197 -0
  97. package/src/locales/zh-CN.json +8 -3
  98. package/src/main.tsx +1 -1
  99. package/src/models/visSpecHistory.ts +14 -0
  100. package/src/renderer/index.tsx +58 -101
  101. package/src/renderer/specRenderer.tsx +119 -0
  102. package/src/segments/segmentNav.tsx +3 -16
  103. package/src/segments/visNav.tsx +17 -6
  104. package/src/services.ts +37 -1
  105. package/src/store/commonStore.ts +14 -9
  106. package/src/store/index.tsx +11 -4
  107. package/src/store/visualSpecStore.ts +89 -50
  108. package/src/utils/dataPrep.ts +24 -0
  109. package/src/utils/index.ts +16 -17
  110. package/src/utils/media.ts +16 -11
  111. package/src/utils/normalization.ts +3 -1
  112. package/src/utils/save.ts +1 -2
  113. package/src/vis/react-vega.tsx +11 -332
  114. package/src/vis/spec/aggregate.ts +13 -0
  115. package/src/vis/spec/encode.ts +69 -0
  116. package/src/vis/spec/field.ts +10 -0
  117. package/src/vis/spec/mark.ts +30 -0
  118. package/src/vis/spec/stack.ts +11 -0
  119. package/src/vis/spec/view.ts +138 -0
  120. package/src/vis/theme.ts +35 -25
  121. package/src/visualSettings/index.tsx +22 -33
  122. package/src/workers/transform.ts +12 -0
  123. package/src/workers/transform.worker.js +13 -0
  124. package/src/workers/viewQuery.worker.js +16 -0
  125. package/dist/components/container.d.ts +0 -2
  126. package/dist/dataSource/pannel.d.ts +0 -5
  127. package/src/components/container.tsx +0 -25
  128. package/src/dataSource/pannel.tsx +0 -71
@@ -3,16 +3,15 @@ import embed from 'vega-embed';
3
3
  import { Subject, Subscription } from 'rxjs'
4
4
  import * as op from 'rxjs/operators';
5
5
  import type { ScenegraphEvent, View } from 'vega';
6
- import { ISemanticType } from 'visual-insights';
7
6
  import styled from 'styled-components';
8
- import { autoMark } from '../utils/autoMark';
9
- import { COUNT_FIELD_ID } from '../constants';
10
7
 
11
- import { IViewField, IRow, IStackMode } from '../interfaces';
8
+ import { IViewField, IRow, IStackMode, IDarkMode, IThemeKey } from '../interfaces';
12
9
  import { useTranslation } from 'react-i18next';
13
10
  import { getVegaTimeFormatRules } from './temporalFormat';
14
11
  import { builtInThemes } from './theme';
15
12
  import { useCurrentMediaTheme } from '../utils/media';
13
+ import { SingleViewProps, getSingleView } from './spec/view';
14
+ import { NULL_FIELD } from './spec/field';
16
15
 
17
16
  const CanvaContainer = styled.div<{rowSize: number; colSize: number;}>`
18
17
  display: grid;
@@ -49,16 +48,10 @@ interface ReactVegaProps {
49
48
  selectEncoding: SingleViewProps['selectEncoding'];
50
49
  brushEncoding: SingleViewProps['brushEncoding'];
51
50
  /** @default "vega" */
52
- themeKey?: 'vega' | 'g2';
53
- }
54
- const NULL_FIELD: IViewField = {
55
- dragId: '',
56
- fid: '',
57
- name: '',
58
- semanticType: 'quantitative',
59
- analyticType: 'measure',
60
- aggName: 'sum'
51
+ themeKey?: IThemeKey;
52
+ dark?: IDarkMode;
61
53
  }
54
+
62
55
  const click$ = new Subject<ScenegraphEvent>();
63
56
  const selection$ = new Subject<any>();
64
57
  const geomClick$ = selection$.pipe(
@@ -70,9 +63,6 @@ const geomClick$ = selection$.pipe(
70
63
  return false
71
64
  })
72
65
  );
73
- function getFieldType(field: IViewField): 'quantitative' | 'nominal' | 'ordinal' | 'temporal' {
74
- return field.semanticType
75
- }
76
66
 
77
67
  const BRUSH_SIGNAL_NAME = "__gw_brush__";
78
68
  const POINT_SIGNAL_NAME = "__gw_point__";
@@ -84,290 +74,7 @@ interface ParamStoreEntry {
84
74
  data: any;
85
75
  }
86
76
 
87
- interface SingleViewProps {
88
- x: IViewField;
89
- y: IViewField;
90
- color: IViewField;
91
- opacity: IViewField;
92
- size: IViewField;
93
- shape: IViewField,
94
- xOffset: IViewField;
95
- yOffset: IViewField;
96
- row: IViewField;
97
- column: IViewField;
98
- theta: IViewField;
99
- radius: IViewField;
100
- defaultAggregated: boolean;
101
- stack: IStackMode;
102
- geomType: string;
103
- enableCrossFilter: boolean;
104
- asCrossFilterTrigger: boolean;
105
- selectEncoding: 'default' | 'none';
106
- brushEncoding: 'x' | 'y' | 'default' | 'none';
107
- }
108
77
 
109
- function availableChannels (geomType: string): Set<string> {
110
- if (geomType === 'arc') {
111
- return new Set(['opacity', 'color', 'size', 'theta', 'radius'])
112
- }
113
- return new Set(['column', 'opacity', 'color', 'row', 'size', 'x', 'y', 'xOffset', 'yOffset', 'shape'])
114
- }
115
- interface EncodeProps extends Pick<SingleViewProps, 'column' | 'opacity' | 'color' | 'row' | 'size' | 'x' | 'y' | 'xOffset' | 'yOffset' | 'shape' | 'theta' | 'radius'> {
116
- geomType: string;
117
- }
118
- function channelEncode(props: EncodeProps) {
119
- const avcs = availableChannels(props.geomType)
120
- const encoding: {[key: string]: any} = {}
121
- Object.keys(props).filter(c => avcs.has(c)).forEach(c => {
122
- if (props[c] !== NULL_FIELD) {
123
- encoding[c] = {
124
- field: props[c].fid,
125
- title: props[c].name,
126
- type: props[c].semanticType,
127
- }
128
- if (props[c].analyticType !== 'measure') {
129
- // if `aggregate` is set to null,
130
- // do not aggregate this field
131
- encoding[c].aggregate = null;
132
- }
133
- if (props[c].analyticType === 'measure') {
134
- encoding[c].type = 'quantitative'
135
- }
136
- }
137
- })
138
- // FIXME: temporal fix, only for x and y relative order
139
- if (encoding.x && encoding.y) {
140
- if ((props.x.sort && props.x.sort) || (props.y && props.y.sort)) {
141
- if (props.x.sort !== 'none' && (props.y.sort === 'none' || !Boolean(props.y.sort))) {
142
- encoding.x.sort = {
143
- encoding: 'y',
144
- order: props.x.sort
145
- }
146
- } else if (props.y.sort && props.y.sort !== 'none' && (props.x.sort === 'none' || !Boolean(props.x.sort))) {
147
- encoding.y.sort = {
148
- encoding: 'x',
149
- order: props.y.sort
150
- }
151
- }
152
- }
153
- }
154
- return encoding
155
- }
156
- function channelAggregate(encoding: {[key: string]: any}, fields: IViewField[]) {
157
- Object.values(encoding).forEach(c => {
158
- const targetField = fields.find(f => f.fid === c.field && !('aggregate' in c));
159
- if (targetField && targetField.fid === COUNT_FIELD_ID) {
160
- c.field = undefined;
161
- c.aggregate = 'count';
162
- c.title = 'Count'
163
- } else if (targetField && targetField.analyticType === 'measure') {
164
- c.title = `${targetField.aggName}(${targetField.name})`;
165
- c.aggregate = targetField.aggName;
166
- }
167
- })
168
- }
169
- function channelStack(encoding: {[key: string]: any}, stackMode: IStackMode) {
170
- if (stackMode === 'stack') return;
171
- if (encoding.x && encoding.x.type === 'quantitative') {
172
- encoding.x.stack = stackMode === 'none' ? null : 'normalize'
173
- }
174
- if (encoding.y && encoding.y.type === 'quantitative') {
175
- encoding.y.stack = stackMode === 'none' ? null : 'normalize'
176
- }
177
- }
178
- // TODO: xOffset等通道的特性不太稳定,建议后续vega相关特性稳定后,再使用。
179
- // 1. 场景太细,仅仅对对应的坐标轴是nominal(可能由ordinal)时,才可用
180
- // 2. 部分geom type会出现bug,如line,会出现组间的错误连接
181
- // "vega": "^5.22.0",
182
- // "vega-embed": "^6.20.8",
183
- // "vega-lite": "^5.2.0",
184
- function getSingleView(props: SingleViewProps) {
185
- const {
186
- x,
187
- y,
188
- color,
189
- opacity,
190
- size,
191
- shape,
192
- theta,
193
- radius,
194
- row,
195
- column,
196
- xOffset,
197
- yOffset,
198
- defaultAggregated,
199
- stack,
200
- geomType,
201
- selectEncoding,
202
- brushEncoding,
203
- enableCrossFilter,
204
- asCrossFilterTrigger,
205
- } = props
206
- const fields: IViewField[] = [x, y, color, opacity, size, shape, row, column, xOffset, yOffset, theta, radius]
207
- let markType = geomType;
208
- if (geomType === 'auto') {
209
- const types: ISemanticType[] = [];
210
- if (x !== NULL_FIELD) types.push(x.semanticType)//types.push(getFieldType(x));
211
- if (y !== NULL_FIELD) types.push(y.semanticType)//types.push(getFieldType(yField));
212
- markType = autoMark(types);
213
- }
214
-
215
- let encoding = channelEncode({ geomType: markType, x, y, color, opacity, size, shape, row, column, xOffset, yOffset, theta, radius })
216
- if (defaultAggregated) {
217
- channelAggregate(encoding, fields);
218
- }
219
- channelStack(encoding, stack);
220
- if (!enableCrossFilter || brushEncoding === 'none' && selectEncoding === 'none') {
221
- return {
222
- mark: {
223
- type: markType,
224
- opacity: 0.96,
225
- tooltip: true
226
- },
227
- encoding
228
- };
229
- }
230
- const mark = {
231
- type: markType,
232
- opacity: 0.96,
233
- tooltip: true
234
- };
235
-
236
- // TODO:
237
- // 鉴于 Vega 中使用 layer 会导致一些难以覆盖的预期外行为,
238
- // 破坏掉引入交互后视图的正确性,
239
- // 目前不使用 layer 来实现交互(注掉以下代码)。
240
- // 考虑 layer 的目的是 layer + condition 可以用于同时展现“全集”(context)和“选中”两层结构,尤其对于聚合数据有分析帮助;
241
- // 同时,不需要关心作为筛选器的是哪一张图。
242
- // 现在采用临时方案,区别产生筛选的来源,并对其他图仅借助 transform 展现筛选后的数据。
243
- // #[BEGIN bad-layer-interaction]
244
- // const shouldUseMultipleLayers = brushEncoding !== 'none' && Object.values(encoding).some(channel => {
245
- // return typeof channel.aggregate === 'string' && /* 这种 case 对应行数 */ typeof channel.field === 'string';
246
- // });
247
- // if (shouldUseMultipleLayers) {
248
- // if (['column', 'row'].some(key => key in encoding && encoding[key].length > 0)) {
249
- // // 这种情况 Vega 不能处理,是因为 Vega 不支持在 layer 中使用 column / row channel,
250
- // // 会导致渲染出来的视图无法与不使用交互时的结果不变。
251
- // return {
252
- // params: [
253
- // {
254
- // name: BRUSH_SIGNAL_NAME,
255
- // select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
256
- // },
257
- // ],
258
- // mark,
259
- // encoding: {
260
- // ...encoding,
261
- // color: {
262
- // condition: {
263
- // ...encoding.color,
264
- // param: BRUSH_SIGNAL_NAME,
265
- // },
266
- // value: '#888',
267
- // },
268
- // },
269
- // };
270
- // }
271
- // return {
272
- // layer: [
273
- // {
274
- // params: [
275
- // {
276
- // name: BRUSH_SIGNAL_NAME,
277
- // select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
278
- // },
279
- // ],
280
- // mark,
281
- // encoding: {
282
- // ...encoding,
283
- // color: 'color' in encoding ? {
284
- // condition: {
285
- // ...encoding.color,
286
- // test: 'false',
287
- // },
288
- // value: '#888',
289
- // } : {
290
- // value: '#888',
291
- // },
292
- // },
293
- // },
294
- // {
295
- // transform: [{ filter: { param: BRUSH_SIGNAL_NAME }}],
296
- // mark,
297
- // encoding: {
298
- // ...encoding,
299
- // color: encoding.color ?? { value: 'steelblue' },
300
- // },
301
- // },
302
- // ],
303
- // };
304
- // } else if (brushEncoding !== 'none') {
305
- // return {
306
- // params: [
307
- // {
308
- // name: BRUSH_SIGNAL_NAME,
309
- // select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
310
- // },
311
- // ],
312
- // mark,
313
- // encoding: {
314
- // ...encoding,
315
- // color: {
316
- // condition: {
317
- // ...encoding.color,
318
- // param: BRUSH_SIGNAL_NAME,
319
- // },
320
- // value: '#888',
321
- // },
322
- // },
323
- // };
324
- // }
325
- // #[END bad-layer-interaction]
326
-
327
- if (brushEncoding !== 'none') {
328
- return {
329
- transform: asCrossFilterTrigger ? [] : [
330
- { filter: { param: BRUSH_SIGNAL_NAME } }
331
- ],
332
- params: [
333
- // {
334
- // name: BRUSH_SIGNAL_DISPLAY_NAME,
335
- // select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
336
- // on: '__YOU_CANNOT_MODIFY_THIS_SIGNAL__',
337
- // },
338
- {
339
- name: BRUSH_SIGNAL_NAME,
340
- select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
341
- },
342
- ],
343
- mark,
344
- encoding,
345
- };
346
- }
347
-
348
- return {
349
- transform: asCrossFilterTrigger ? [] : [
350
- { filter: { param: POINT_SIGNAL_NAME } }
351
- ],
352
- params: [
353
- {
354
- name: POINT_SIGNAL_NAME,
355
- select: { type: 'point' },
356
- },
357
- ],
358
- mark,
359
- encoding: asCrossFilterTrigger ? {
360
- ...encoding,
361
- color: {
362
- condition: {
363
- ...encoding.color,
364
- param: POINT_SIGNAL_NAME,
365
- },
366
- value: '#888',
367
- },
368
- } : encoding,
369
- };
370
- }
371
78
  const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVega (props, ref) {
372
79
  const {
373
80
  dataSource = [],
@@ -391,12 +98,11 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
391
98
  selectEncoding,
392
99
  brushEncoding,
393
100
  themeKey = 'vega',
101
+ dark = 'media'
394
102
  } = props;
395
- // const container = useRef<HTMLDivElement>(null);
396
- // const containers = useRef<(HTMLDivElement | null)[]>([]);
397
103
  const [viewPlaceholders, setViewPlaceholders] = useState<React.MutableRefObject<HTMLDivElement>[]>([]);
398
104
  const { i18n } = useTranslation();
399
- const mediaTheme = useCurrentMediaTheme();
105
+ const mediaTheme = useCurrentMediaTheme(dark);
400
106
  const themeConfig = builtInThemes[themeKey]?.[mediaTheme];
401
107
  useEffect(() => {
402
108
  const clickSub = geomClick$.subscribe(([values, e]) => {
@@ -491,28 +197,12 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
491
197
  enableCrossFilter: false,
492
198
  asCrossFilterTrigger: false,
493
199
  });
494
- // if (layoutMode === 'fixed') {
495
- // spec.width = 800;
496
- // spec.height = 600;
497
- // }
200
+
498
201
  spec.mark = singleView.mark;
499
202
  if ('encoding' in singleView) {
500
203
  spec.encoding = singleView.encoding;
501
204
  }
502
205
 
503
- // #[BEGIN bad-layer-interaction]
504
- // if ('layer' in singleView) {
505
- // if ('params' in spec) {
506
- // const basicParams = spec['params'];
507
- // delete spec['params'];
508
- // singleView.layer![0].params = [...basicParams, ...singleView.layer![0].params ?? []];
509
- // }
510
- // spec.layer = singleView.layer;
511
- // } else if ('params' in singleView) {
512
- // spec.params.push(...singleView.params!);
513
- // }
514
- // #[END bad-layer-interaction]
515
-
516
206
  if ('params' in singleView) {
517
207
  spec.params.push(...singleView.params!);
518
208
  }
@@ -553,6 +243,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
553
243
  for (let i = 0; i < rowRepeatFields.length; i++) {
554
244
  for (let j = 0; j < colRepeatFields.length; j++, index++) {
555
245
  const sourceId = index;
246
+ const hasLegend = i === 0 && j === colRepeatFields.length - 1;
556
247
  const singleView = getSingleView({
557
248
  x: colRepeatFields[j] || NULL_FIELD,
558
249
  y: rowRepeatFields[i] || NULL_FIELD,
@@ -573,23 +264,11 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
573
264
  brushEncoding,
574
265
  enableCrossFilter: crossFilterTriggerIdx !== -1,
575
266
  asCrossFilterTrigger: crossFilterTriggerIdx === sourceId,
267
+ hideLegend: !hasLegend,
576
268
  });
577
269
  const node = i * colRepeatFields.length + j < viewPlaceholders.length ? viewPlaceholders[i * colRepeatFields.length + j].current : null
578
270
  let commonSpec = { ...spec };
579
271
 
580
- // #[BEGIN bad-layer-interaction]
581
- // if ('layer' in singleView) {
582
- // if ('params' in commonSpec) {
583
- // const { params: basicParams, ...spec } = commonSpec;
584
- // commonSpec = spec;
585
- // singleView.layer![0].params = [...basicParams, ...singleView.layer![0].params ?? []];
586
- // }
587
- // commonSpec.layer = singleView.layer;
588
- // } else if ('params' in singleView) {
589
- // commonSpec.params = [...commonSpec.params, ...singleView.params!];
590
- // }
591
- // #[END bad-layer-interaction]
592
-
593
272
  if ('params' in singleView) {
594
273
  commonSpec.params = [...commonSpec.params, ...singleView.params!];
595
274
  }
@@ -0,0 +1,13 @@
1
+ import { COUNT_FIELD_ID } from '../../constants';
2
+ import { IViewField } from '../../interfaces';
3
+
4
+ export function channelAggregate(encoding: { [key: string]: any }, fields: IViewField[]) {
5
+ Object.values(encoding).forEach((c) => {
6
+ const targetField = fields.find((f) => f.fid === c.field && !('aggregate' in c));
7
+ if (targetField && targetField.fid === COUNT_FIELD_ID) {
8
+ c.title = 'Count';
9
+ } else if (targetField && targetField.analyticType === 'measure') {
10
+ c.title = `${targetField.aggName}(${targetField.name})`;
11
+ }
12
+ });
13
+ }
@@ -0,0 +1,69 @@
1
+ import { IViewField } from '../../interfaces';
2
+ import { NULL_FIELD } from './field';
3
+ export interface IEncodeProps {
4
+ geomType: string;
5
+ x: IViewField;
6
+ y: IViewField;
7
+ color: IViewField;
8
+ opacity: IViewField;
9
+ size: IViewField;
10
+ shape: IViewField;
11
+ xOffset: IViewField;
12
+ yOffset: IViewField;
13
+ row: IViewField;
14
+ column: IViewField;
15
+ theta: IViewField;
16
+ radius: IViewField;
17
+ }
18
+ function availableChannels(geomType: string): Set<string> {
19
+ if (geomType === 'arc') {
20
+ return new Set(['opacity', 'color', 'size', 'theta', 'radius']);
21
+ }
22
+ return new Set(['column', 'opacity', 'color', 'row', 'size', 'x', 'y', 'xOffset', 'yOffset', 'shape']);
23
+ }
24
+ export function channelEncode(props: IEncodeProps) {
25
+ const avcs = availableChannels(props.geomType);
26
+ const encoding: { [key: string]: any } = {};
27
+ Object.keys(props)
28
+ .filter((c) => avcs.has(c))
29
+ .forEach((c) => {
30
+ if (props[c] !== NULL_FIELD) {
31
+ encoding[c] = {
32
+ field: props[c].fid,
33
+ title: props[c].name,
34
+ type: props[c].semanticType,
35
+ };
36
+ if (props[c].analyticType !== 'measure') {
37
+ // if `aggregate` is set to null,
38
+ // do not aggregate this field
39
+ encoding[c].aggregate = null;
40
+ }
41
+ if (props[c].analyticType === 'measure') {
42
+ encoding[c].type = 'quantitative';
43
+ }
44
+ }
45
+ });
46
+ // FIXME: temporal fix, only for x and y relative order
47
+ if (encoding.x) {
48
+ encoding.x.axis = { labelOverlap: true }
49
+ }
50
+ if (encoding && encoding.y) {
51
+ encoding.y.axis = { labelOverlap: true }
52
+ }
53
+ if (encoding.x && encoding.y) {
54
+ if ((props.x.sort && props.x.sort) || (props.y && props.y.sort)) {
55
+ if (props.x.sort !== 'none' && (props.y.sort === 'none' || !Boolean(props.y.sort))) {
56
+ encoding.x.sort = {
57
+ encoding: 'y',
58
+ order: props.x.sort,
59
+ };
60
+ } else if (props.y.sort && props.y.sort !== 'none' && (props.x.sort === 'none' || !Boolean(props.x.sort))) {
61
+ encoding.y.sort = {
62
+ encoding: 'x',
63
+ order: props.y.sort,
64
+ };
65
+ }
66
+ }
67
+ }
68
+ return encoding;
69
+ }
@@ -0,0 +1,10 @@
1
+ import { IViewField } from "../../interfaces";
2
+
3
+ export const NULL_FIELD: IViewField = {
4
+ dragId: '',
5
+ fid: '',
6
+ name: '',
7
+ semanticType: 'quantitative',
8
+ analyticType: 'measure',
9
+ aggName: 'sum',
10
+ };
@@ -0,0 +1,30 @@
1
+ import { ISemanticType } from "visual-insights";
2
+
3
+ /**
4
+ *
5
+ * @param semanticTypeList semanticTypeList.length <= 2,调用时,手动将columns 和 rows的最后一个元素组合传进来
6
+ * @returns geom(mark) type
7
+ */
8
+ export function autoMark(semanticTypeList: ISemanticType[]): string {
9
+ if (semanticTypeList.length < 2) {
10
+ if (semanticTypeList[0] === "temporal") return "tick";
11
+ return "bar";
12
+ }
13
+ const couter: Map<ISemanticType, number> = new Map();
14
+ (["nominal", "ordinal", "quantitative", "temporal"] as ISemanticType[]).forEach((s) => {
15
+ couter.set(s, 0);
16
+ });
17
+ for (let st of semanticTypeList) {
18
+ couter.set(st, couter.get(st)! + 1);
19
+ }
20
+ if (couter.get("nominal") === 1 || couter.get("ordinal") === 1) {
21
+ return "bar";
22
+ }
23
+ if (couter.get("temporal") === 1 && couter.get("quantitative") === 1) {
24
+ return "line";
25
+ }
26
+ if (couter.get("quantitative") === 2) {
27
+ return "point";
28
+ }
29
+ return "point";
30
+ }
@@ -0,0 +1,11 @@
1
+ import { IStackMode } from "../../interfaces";
2
+
3
+ export function channelStack(encoding: { [key: string]: any }, stackMode: IStackMode) {
4
+ if (stackMode === 'stack') return;
5
+ if (encoding.x && encoding.x.type === 'quantitative') {
6
+ encoding.x.stack = stackMode === 'none' ? null : 'normalize';
7
+ }
8
+ if (encoding.y && encoding.y.type === 'quantitative') {
9
+ encoding.y.stack = stackMode === 'none' ? null : 'normalize';
10
+ }
11
+ }
@@ -0,0 +1,138 @@
1
+ import { ISemanticType } from 'visual-insights';
2
+ import { IStackMode, IViewField } from '../../interfaces';
3
+ import { autoMark } from './mark';
4
+ import { NULL_FIELD } from './field';
5
+ import { channelAggregate } from './aggregate';
6
+ import { IEncodeProps, channelEncode } from './encode';
7
+ import { channelStack } from './stack'
8
+
9
+ const BRUSH_SIGNAL_NAME = '__gw_brush__';
10
+ const POINT_SIGNAL_NAME = '__gw_point__';
11
+ export interface SingleViewProps extends IEncodeProps {
12
+
13
+ defaultAggregated: boolean;
14
+ stack: IStackMode;
15
+ enableCrossFilter: boolean;
16
+ asCrossFilterTrigger: boolean;
17
+ selectEncoding: 'default' | 'none';
18
+ brushEncoding: 'x' | 'y' | 'default' | 'none';
19
+ hideLegend?: boolean;
20
+ }
21
+ export function getSingleView(props: SingleViewProps) {
22
+ const {
23
+ x,
24
+ y,
25
+ color,
26
+ opacity,
27
+ size,
28
+ shape,
29
+ theta,
30
+ radius,
31
+ row,
32
+ column,
33
+ xOffset,
34
+ yOffset,
35
+ defaultAggregated,
36
+ stack,
37
+ geomType,
38
+ selectEncoding,
39
+ brushEncoding,
40
+ enableCrossFilter,
41
+ asCrossFilterTrigger,
42
+ hideLegend = false,
43
+ } = props;
44
+ const fields: IViewField[] = [x, y, color, opacity, size, shape, row, column, xOffset, yOffset, theta, radius];
45
+ let markType = geomType;
46
+ let config: any = {};
47
+ if (hideLegend) {
48
+ config.legend = {
49
+ disable: true,
50
+ };
51
+ }
52
+ if (geomType === 'auto') {
53
+ const types: ISemanticType[] = [];
54
+ if (x !== NULL_FIELD) types.push(x.semanticType); //types.push(getFieldType(x));
55
+ if (y !== NULL_FIELD) types.push(y.semanticType); //types.push(getFieldType(yField));
56
+ markType = autoMark(types);
57
+ }
58
+
59
+ let encoding = channelEncode({
60
+ geomType: markType,
61
+ x,
62
+ y,
63
+ color,
64
+ opacity,
65
+ size,
66
+ shape,
67
+ row,
68
+ column,
69
+ xOffset,
70
+ yOffset,
71
+ theta,
72
+ radius,
73
+ });
74
+ if (defaultAggregated) {
75
+ channelAggregate(encoding, fields);
76
+ }
77
+ channelStack(encoding, stack);
78
+ if (!enableCrossFilter || (brushEncoding === 'none' && selectEncoding === 'none')) {
79
+ return {
80
+ config,
81
+ mark: {
82
+ type: markType,
83
+ opacity: 0.96,
84
+ tooltip: true,
85
+ },
86
+ encoding,
87
+ };
88
+ }
89
+ const mark = {
90
+ type: markType,
91
+ opacity: 0.96,
92
+ tooltip: true,
93
+ };
94
+
95
+ if (brushEncoding !== 'none') {
96
+ return {
97
+ config,
98
+ transform: asCrossFilterTrigger ? [] : [{ filter: { param: BRUSH_SIGNAL_NAME } }],
99
+ params: [
100
+ // {
101
+ // name: BRUSH_SIGNAL_DISPLAY_NAME,
102
+ // select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
103
+ // on: '__YOU_CANNOT_MODIFY_THIS_SIGNAL__',
104
+ // },
105
+ {
106
+ name: BRUSH_SIGNAL_NAME,
107
+ select: { type: 'interval', encodings: brushEncoding === 'default' ? undefined : [brushEncoding] },
108
+ },
109
+ ],
110
+ mark,
111
+ encoding,
112
+ };
113
+ }
114
+
115
+ return {
116
+ config,
117
+ transform: asCrossFilterTrigger ? [] : [{ filter: { param: POINT_SIGNAL_NAME } }],
118
+ params: [
119
+ {
120
+ name: POINT_SIGNAL_NAME,
121
+ select: { type: 'point' },
122
+ },
123
+ ],
124
+ mark,
125
+ encoding: asCrossFilterTrigger
126
+ ? {
127
+ ...encoding,
128
+ color: {
129
+ condition: {
130
+ ...encoding.color,
131
+ param: POINT_SIGNAL_NAME,
132
+ },
133
+ value: '#888',
134
+ },
135
+ }
136
+ : encoding,
137
+ };
138
+ }