@kanaries/graphic-walker 0.2.3 → 0.2.5

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 (77) hide show
  1. package/dist/fields/datasetFields/index.d.ts +3 -3
  2. package/dist/graphic-walker.es.js +7167 -7152
  3. package/dist/graphic-walker.es.js.map +1 -1
  4. package/dist/graphic-walker.umd.js +96 -96
  5. package/dist/graphic-walker.umd.js.map +1 -1
  6. package/dist/store/index.d.ts +11 -1
  7. package/package.json +5 -3
  8. package/src/App.tsx +140 -0
  9. package/src/assets/kanaries.ico +0 -0
  10. package/src/components/clickMenu.tsx +29 -0
  11. package/src/components/container.tsx +16 -0
  12. package/src/components/dataTypeIcon.tsx +20 -0
  13. package/src/components/liteForm.tsx +16 -0
  14. package/src/components/modal.tsx +85 -0
  15. package/src/components/sizeSetting.tsx +95 -0
  16. package/src/components/tabs/pureTab.tsx +70 -0
  17. package/src/config.ts +57 -0
  18. package/src/constants.ts +1 -0
  19. package/src/dataSource/config.ts +62 -0
  20. package/src/dataSource/dataSelection/csvData.tsx +77 -0
  21. package/src/dataSource/dataSelection/gwFile.tsx +38 -0
  22. package/src/dataSource/dataSelection/index.tsx +57 -0
  23. package/src/dataSource/dataSelection/publicData.tsx +57 -0
  24. package/src/dataSource/index.tsx +78 -0
  25. package/src/dataSource/pannel.tsx +71 -0
  26. package/src/dataSource/table.tsx +125 -0
  27. package/src/dataSource/utils.ts +47 -0
  28. package/src/fields/aestheticFields.tsx +23 -0
  29. package/src/fields/components.tsx +159 -0
  30. package/src/fields/datasetFields/dimFields.tsx +45 -0
  31. package/src/fields/datasetFields/fieldPill.tsx +10 -0
  32. package/src/fields/datasetFields/index.tsx +28 -0
  33. package/src/fields/datasetFields/meaFields.tsx +58 -0
  34. package/src/fields/fieldsContext.tsx +59 -0
  35. package/src/fields/filterField/filterEditDialog.tsx +143 -0
  36. package/src/fields/filterField/filterPill.tsx +113 -0
  37. package/src/fields/filterField/index.tsx +61 -0
  38. package/src/fields/filterField/slider.tsx +236 -0
  39. package/src/fields/filterField/tabs.tsx +421 -0
  40. package/src/fields/obComponents/obFContainer.tsx +40 -0
  41. package/src/fields/obComponents/obPill.tsx +48 -0
  42. package/src/fields/posFields/index.tsx +33 -0
  43. package/src/fields/select.tsx +31 -0
  44. package/src/fields/utils.ts +31 -0
  45. package/src/index.css +13 -0
  46. package/src/index.tsx +12 -0
  47. package/src/insightBoard/index.tsx +30 -0
  48. package/src/insightBoard/mainBoard.tsx +203 -0
  49. package/src/insightBoard/radioGroupButtons.tsx +50 -0
  50. package/src/insightBoard/selectionSpec.ts +113 -0
  51. package/src/insightBoard/std2vegaSpec.ts +184 -0
  52. package/src/insightBoard/utils.ts +32 -0
  53. package/src/insights.ts +408 -0
  54. package/src/interfaces.ts +154 -0
  55. package/src/locales/en-US.json +140 -0
  56. package/src/locales/i18n.ts +50 -0
  57. package/src/locales/zh-CN.json +140 -0
  58. package/src/main.tsx +10 -0
  59. package/src/models/visSpecHistory.ts +129 -0
  60. package/src/renderer/index.tsx +104 -0
  61. package/src/segments/visNav.tsx +48 -0
  62. package/src/services.ts +139 -0
  63. package/src/store/commonStore.ts +158 -0
  64. package/src/store/index.tsx +53 -0
  65. package/src/store/visualSpecStore.ts +586 -0
  66. package/src/utils/autoMark.ts +34 -0
  67. package/src/utils/index.ts +251 -0
  68. package/src/utils/normalization.ts +158 -0
  69. package/src/utils/save.ts +46 -0
  70. package/src/vis/future-react-vega.tsx +193 -0
  71. package/src/vis/gen-vega.tsx +52 -0
  72. package/src/vis/react-vega.tsx +398 -0
  73. package/src/visualSettings/index.tsx +252 -0
  74. package/src/visualSettings/menubar.tsx +109 -0
  75. package/src/vite-env.d.ts +1 -0
  76. package/src/workers/explainer.worker.js +78 -0
  77. package/src/workers/filter.worker.js +70 -0
@@ -0,0 +1,251 @@
1
+ import i18next from 'i18next';
2
+ import { COUNT_FIELD_ID } from '../constants';
3
+ import { IRow, Filters, IMutField } from '../interfaces';
4
+ interface NRReturns {
5
+ normalizedData: IRow[];
6
+ maxMeasures:IRow;
7
+ minMeasures:IRow;
8
+ totalMeasures:IRow
9
+ }
10
+ function normalizeRecords(dataSource: IRow[], measures: string[]): NRReturns {
11
+ const maxMeasures: IRow = {};
12
+ const minMeasures: IRow = {};
13
+ const totalMeasures: IRow = {};
14
+ measures.forEach(mea => {
15
+ maxMeasures[mea] = -Infinity;
16
+ minMeasures[mea] = Infinity;
17
+ totalMeasures[mea] = 0;
18
+ })
19
+ dataSource.forEach(record => {
20
+ measures.forEach(mea => {
21
+ maxMeasures[mea] = Math.max(record[mea], maxMeasures[mea])
22
+ minMeasures[mea] = Math.min(record[mea], minMeasures[mea])
23
+ })
24
+ })
25
+ const newData: IRow[] = [];
26
+ dataSource.forEach(record => {
27
+ const norRecord: IRow = { ... record };
28
+ measures.forEach(mea => {
29
+ // norRecord[mea] = norRecord[mea] - minMeasures[mea]
30
+ totalMeasures[mea] += Math.abs(norRecord[mea]);
31
+ })
32
+ newData.push(norRecord)
33
+ })
34
+ newData.forEach(record => {
35
+ measures.forEach(mea => {
36
+ record[mea] /= totalMeasures[mea];
37
+ })
38
+ })
39
+ return {
40
+ normalizedData: newData,
41
+ maxMeasures,
42
+ minMeasures,
43
+ totalMeasures
44
+ }
45
+ }
46
+
47
+ function normalize2PositiveRecords(dataSource: IRow[], measures: string[]): NRReturns {
48
+ const maxMeasures: IRow = {};
49
+ const minMeasures: IRow = {};
50
+ const totalMeasures: IRow = {};
51
+ measures.forEach((mea) => {
52
+ maxMeasures[mea] = -Infinity;
53
+ minMeasures[mea] = Infinity;
54
+ totalMeasures[mea] = 0;
55
+ });
56
+ dataSource.forEach((record) => {
57
+ measures.forEach((mea) => {
58
+ maxMeasures[mea] = Math.max(record[mea], maxMeasures[mea]);
59
+ minMeasures[mea] = Math.min(record[mea], minMeasures[mea]);
60
+ });
61
+ });
62
+ const newData: IRow[] = [];
63
+ dataSource.forEach((record) => {
64
+ const norRecord: IRow = { ...record };
65
+ measures.forEach((mea) => {
66
+ norRecord[mea] = norRecord[mea] - minMeasures[mea]
67
+ totalMeasures[mea] += norRecord[mea];
68
+ });
69
+ newData.push(norRecord);
70
+ });
71
+ newData.forEach((record) => {
72
+ measures.forEach((mea) => {
73
+ record[mea] /= totalMeasures[mea];
74
+ // if (isNaN(record[mea])) {
75
+ // record[mea] = 1
76
+ // }
77
+ });
78
+ });
79
+ return {
80
+ normalizedData: newData,
81
+ maxMeasures,
82
+ minMeasures,
83
+ totalMeasures,
84
+ };
85
+ }
86
+
87
+ export function checkMajorFactor(data: IRow[], childrenData: Map<any, IRow[]>, dimensions: string[], measures: string[]): { majorKey: string; majorSum: number } {
88
+ const { normalizedData, maxMeasures, minMeasures, totalMeasures } = normalizeRecords(data, measures);
89
+ let majorSum = Infinity;
90
+ let majorKey = '';
91
+ for (let [key, childData] of childrenData) {
92
+ let sum = 0;
93
+ for (let record of normalizedData ) {
94
+ let target = childData.find(childRecord => {
95
+ return dimensions.every(dim => record[dim] === childRecord[dim])
96
+ })
97
+ if (target) {
98
+ measures.forEach(mea => {
99
+ let targetValue = (typeof target![mea] === 'number' && !isNaN(target![mea])) ? target![mea] : 0;
100
+ targetValue = (targetValue) / totalMeasures[mea]
101
+ sum += Math.abs(record[mea] - targetValue)
102
+ })
103
+ } else {
104
+ measures.forEach(mea => {
105
+ sum += Math.abs(record[mea]);
106
+ })
107
+ }
108
+ }
109
+ if (sum < majorSum) {
110
+ majorSum = sum;
111
+ majorKey = key;
112
+ }
113
+ }
114
+ majorSum /= (measures.length * 2);
115
+ return { majorKey, majorSum };
116
+ }
117
+
118
+ export function checkChildOutlier(data: IRow[], childrenData: Map<any, IRow[]>, dimensions: string[], measures: string[]): { outlierKey: string; outlierSum: number } {
119
+ // const { normalizedData, maxMeasures, minMeasures, totalMeasures } = normalize2PositiveRecords(data, measures);
120
+ const { normalizedData, maxMeasures, minMeasures, totalMeasures } = normalizeRecords(data, measures);
121
+ let outlierSum = -Infinity;
122
+ let outlierKey = '';
123
+ for (let [key, childData] of childrenData) {
124
+ // const { normalizedData: normalizedChildData } = normalize2PositiveRecords(childData, measures);
125
+ const { normalizedData: normalizedChildData } = normalizeRecords(childData, measures);
126
+ let sum = 0;
127
+ for (let record of normalizedData ) {
128
+ let target = normalizedChildData.find(childRecord => {
129
+ return dimensions.every(dim => record[dim] === childRecord[dim])
130
+ })
131
+ if (target) {
132
+ measures.forEach(mea => {
133
+ let targetValue = (typeof target![mea] === 'number' && !isNaN(target![mea])) ? target![mea] : 0;
134
+ sum += Math.abs(record[mea] - targetValue)
135
+ })
136
+ } else {
137
+ measures.forEach(mea => {
138
+ sum += Math.abs(record[mea]);
139
+ })
140
+ }
141
+ }
142
+ if (sum > outlierSum) {
143
+ outlierSum = sum;
144
+ outlierKey = key;
145
+ }
146
+ }
147
+ outlierSum /= (measures.length * 2);
148
+ return { outlierKey, outlierSum };
149
+ }
150
+ export interface IPredicate {
151
+ key: string;
152
+ type: 'discrete' | 'continuous';
153
+ range: Set<any> | [number, number];
154
+ }
155
+ export function getPredicates(selection: IRow[], dimensions: string[], measures: string[]): IPredicate[] {
156
+ const predicates: IPredicate[] = [];
157
+ dimensions.forEach(dim => {
158
+ predicates.push({
159
+ key: dim,
160
+ type: 'discrete',
161
+ range: new Set()
162
+ })
163
+ })
164
+ measures.forEach(mea => {
165
+ predicates.push({
166
+ key: mea,
167
+ type: 'continuous',
168
+ range: [Infinity, -Infinity]
169
+ })
170
+ })
171
+ selection.forEach(record => {
172
+ dimensions.forEach((dim, index) => {
173
+ (predicates[index].range as Set<any>).add(record[dim])
174
+ })
175
+ measures.forEach((mea, index) => {
176
+ (predicates[index].range as [number, number])[0] = Math.min(
177
+ (predicates[index].range as [number, number])[0],
178
+ record[mea]
179
+ );
180
+ (predicates[index].range as [number, number])[1] = Math.max(
181
+ (predicates[index].range as [number, number])[1],
182
+ record[mea]
183
+ );
184
+ })
185
+ })
186
+ return predicates;
187
+ }
188
+
189
+ export function getPredicatesFromVegaSignals(signals: Filters, dimensions: string[], measures: string[]): IPredicate[] {
190
+ const predicates: IPredicate[] = [];
191
+ dimensions.forEach(dim => {
192
+ predicates.push({
193
+ type: 'discrete',
194
+ range: new Set(signals[dim]),
195
+ key: dim
196
+ });
197
+ });
198
+ return predicates;
199
+ }
200
+
201
+ export function filterByPredicates(data: IRow[], predicates: IPredicate[]): IRow[] {
202
+ const filterData = data.filter((record) => {
203
+ return predicates.every((pre) => {
204
+ if (pre.type === 'continuous') {
205
+ return (
206
+ record[pre.key] >= (pre.range as [number, number])[0] &&
207
+ record[pre.key] <= (pre.range as [number, number])[1]
208
+ );
209
+ } else {
210
+ return (pre.range as Set<any>).has(record[pre.key]);
211
+ }
212
+ });
213
+ });
214
+ return filterData;
215
+ }
216
+
217
+ export function applyFilters(dataSource: IRow[], filters: Filters): IRow[] {
218
+ let filterKeys = Object.keys(filters);
219
+ return dataSource.filter((record) => {
220
+ let keep = true;
221
+ for (let filterKey of filterKeys) {
222
+ if (filters[filterKey].length > 0) {
223
+ if (!filters[filterKey].includes(record[filterKey])) {
224
+ keep = false;
225
+ break;
226
+ }
227
+ }
228
+ }
229
+ return keep;
230
+ });
231
+ }
232
+
233
+ export function extendCountField (dataSource: IRow[], fields: IMutField[]): {
234
+ dataSource: IRow[];
235
+ fields: IMutField[];
236
+ } {
237
+ const nextData = dataSource.map(r => ({
238
+ ...r,
239
+ [COUNT_FIELD_ID]: 1
240
+ }))
241
+ const nextFields = fields.concat({
242
+ fid: COUNT_FIELD_ID,
243
+ name: i18next.t('constant.row_count'),
244
+ analyticType: 'measure',
245
+ semanticType: 'quantitative'
246
+ })
247
+ return {
248
+ dataSource: nextData,
249
+ fields: nextFields
250
+ }
251
+ }
@@ -0,0 +1,158 @@
1
+ import { IRow } from '../interfaces';
2
+
3
+ export function normalizeWithParent(
4
+ data: IRow[],
5
+ parentData: IRow[],
6
+ measures: string[],
7
+ syncScale: boolean
8
+ ): {
9
+ normalizedData: IRow[];
10
+ normalizedParentData: IRow[];
11
+ } {
12
+ const totalMeasuresOfParent: IRow = {};
13
+ const totalMeasures: IRow = {};
14
+ measures.forEach(mea => {
15
+ totalMeasuresOfParent[mea] = 0;
16
+ totalMeasures[mea] = 0;
17
+ })
18
+ parentData.forEach(record => {
19
+ measures.forEach(mea => {
20
+ totalMeasuresOfParent[mea] += Math.abs(record[mea])
21
+ })
22
+ })
23
+ data.forEach(record => {
24
+ measures.forEach(mea => {
25
+ totalMeasures[mea] += Math.abs(record[mea]);
26
+ })
27
+ })
28
+ const normalizedParentData: IRow[] = [];
29
+ parentData.forEach(record => {
30
+ const newRecord = { ...record };
31
+ measures.forEach(mea => {
32
+ newRecord[mea] /= totalMeasuresOfParent[mea];
33
+ })
34
+ normalizedParentData.push(newRecord);
35
+ })
36
+ const normalizedData: IRow[] = [];
37
+ data.forEach(record => {
38
+ const newRecord = { ...record };
39
+ measures.forEach(mea => {
40
+ if (syncScale) {
41
+ newRecord[mea] /= totalMeasuresOfParent[mea];
42
+ } else {
43
+ newRecord[mea] /= totalMeasures[mea]
44
+ }
45
+ })
46
+ normalizedData.push(newRecord);
47
+ })
48
+ return {
49
+ normalizedData,
50
+ normalizedParentData
51
+ };
52
+ }
53
+
54
+ export function compareDistribution (distribution1: IRow[], distribution2: IRow[], dimensions: string[], measures: string[]): number {
55
+ let score = 0;
56
+ let count = 0;
57
+ const tagsForD2: boolean[] = distribution2.map(() => false);
58
+ for (let record of distribution1) {
59
+ let targetRecordIndex = distribution2.findIndex((r, i) => {
60
+ return !tagsForD2[i] && dimensions.every(dim => r[dim] === record[dim])
61
+ })
62
+ if (targetRecordIndex > -1) {
63
+ tagsForD2[targetRecordIndex] = true;
64
+ const targetRecord = distribution2[targetRecordIndex];
65
+ for (let mea of measures) {
66
+ // score += Math.abs(targetRecord[mea] - record[mea]);
67
+ // if (targetRecord[mea] === 0 || record[mea] === 0) continue;
68
+ // score += Math.max(targetRecord[mea], record[mea]) / Math.min(targetRecord[mea], record[mea]);
69
+
70
+ score = Math.max(
71
+ score,
72
+ Math.max(targetRecord[mea], record[mea]) /
73
+ Math.min(targetRecord[mea], record[mea])
74
+ );
75
+ count++;
76
+ }
77
+ } else {
78
+ for (let mea of measures) {
79
+ score = Math.max(score, record[mea])
80
+ // score += Math.abs(record[mea])
81
+ count++;
82
+ }
83
+ }
84
+ }
85
+ for (let i = 0; i < distribution2.length; i++) {
86
+ if (!tagsForD2[i]) {
87
+ tagsForD2[i] = true;
88
+ for (let mea of measures) {
89
+ // score += Math.abs(distribution2[i][mea])
90
+ score = Math.max(score, distribution2[i][mea]);
91
+ count++;
92
+ }
93
+ }
94
+ }
95
+ return score;
96
+ }
97
+
98
+ export function normalizeByMeasures (dataSource: IRow[], measures: string[]) {
99
+ let sums: Map<string, number> = new Map();
100
+
101
+ measures.forEach(mea => {
102
+ sums.set(mea, 0);
103
+ })
104
+
105
+ dataSource.forEach(record => {
106
+ measures.forEach(mea => {
107
+ sums.set(mea, sums.get(mea)! + Math.abs(record[mea]));
108
+ })
109
+ })
110
+
111
+ const ans: IRow[] = [];
112
+ dataSource.forEach(record => {
113
+ const norRecord: IRow = { ...record };
114
+ measures.forEach(mea => {
115
+ norRecord[mea] /= sums.get(mea)!;
116
+ })
117
+ ans.push(norRecord);
118
+ });
119
+ return ans;
120
+ }
121
+
122
+ export function getDistributionDifference(dataSource: IRow[], dimensions: string[], measure1: string, measure2: string): number {
123
+ let score = 0;
124
+ for (let record of dataSource) {
125
+ // score += Math.abs(record[measure1] - record[measure2])
126
+ if (record[measure1] === 0 || record[measure2] === 0) continue;
127
+ score += Math.max(record[measure1], record[measure2]) / Math.min(record[measure1], record[measure2]);
128
+ }
129
+ return score;
130
+ }
131
+
132
+ export function makeBinField (dataSource: IRow[], fid: string, binFid: string, binSize: number | undefined = 10) {
133
+ let _min = Infinity;
134
+ let _max = -Infinity;
135
+ for (let i = 0; i < dataSource.length; i++) {
136
+ let val = dataSource[i][fid];
137
+ if (val > _max) _max = val;
138
+ if (val < _min) _min = val;
139
+ }
140
+ const step = (_max - _min) / binSize;
141
+ return dataSource.map(r => {
142
+ let bIndex = Math.floor((r[fid] - _min) / step);
143
+ if (bIndex === binSize) bIndex = binSize - 1;
144
+ return {
145
+ ...r,
146
+ [binFid]: bIndex * step + _min
147
+ }
148
+ })
149
+ }
150
+
151
+ export function makeLogField (dataSource: IRow[], fid: string, logFid: string) {
152
+ return dataSource.map(r => {
153
+ return {
154
+ ...r,
155
+ [logFid]: (typeof r[fid] === 'number' && r[fid] > 0) ? Math.log10(r[fid]) : null
156
+ }
157
+ })
158
+ }
@@ -0,0 +1,46 @@
1
+ import { IDataSet, IDataSource, IVisSpec } from "../interfaces";
2
+ import { VisSpecWithHistory } from "../models/visSpecHistory";
3
+
4
+
5
+ export function dumpsGWPureSpec (list: VisSpecWithHistory[]): IVisSpec[] {
6
+ return list.map(l => l.exportGW())
7
+ }
8
+
9
+ export function parseGWPureSpec (list: IVisSpec[]): VisSpecWithHistory[] {
10
+ return list.map(l => new VisSpecWithHistory(l))
11
+ }
12
+
13
+ interface IStoInfo {
14
+ datasets: IDataSet[];
15
+ specList: IVisSpec[];
16
+ dataSources: IDataSource[]
17
+ }
18
+
19
+ export function stringifyGWContent (info: IStoInfo) {
20
+ return JSON.stringify(info)
21
+ }
22
+
23
+ export function parseGWContent (raw: string): IStoInfo {
24
+ return JSON.parse(raw)
25
+ }
26
+ // JSON.stringify
27
+
28
+ export function download(data: string, filename: string, type: string) {
29
+ var file = new Blob([data], {type: type});
30
+ // @ts-ignore
31
+ if (window.navigator.msSaveOrOpenBlob) // IE10+
32
+ // @ts-ignore
33
+ window.navigator.msSaveOrOpenBlob(file, filename);
34
+ else { // Others
35
+ var a = document.createElement("a"),
36
+ url = URL.createObjectURL(file);
37
+ a.href = url;
38
+ a.download = filename;
39
+ document.body.appendChild(a);
40
+ a.click();
41
+ setTimeout(function() {
42
+ document.body.removeChild(a);
43
+ window.URL.revokeObjectURL(url);
44
+ }, 0);
45
+ }
46
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * TODO: This file will be used when vega-lite facets bug is fixed.
3
+ * https://github.com/vega/vega-lite/issues/4680
4
+ */
5
+ import React, { useEffect, useRef } from 'react';
6
+ import embed from 'vega-embed';
7
+ import { Subject } from 'rxjs'
8
+ import * as op from 'rxjs/operators';
9
+ import { ScenegraphEvent } from 'vega';
10
+ import { IField, IRow } from '../interfaces';
11
+
12
+ const SELECTION_NAME = 'geom';
13
+ interface ReactVegaProps {
14
+ rows: IField[];
15
+ columns: IField[];
16
+ dataSource: IRow[];
17
+ defaultAggregate?: boolean;
18
+ geomType: string;
19
+ color?: IField;
20
+ opacity?: IField;
21
+ size?: IField;
22
+ onGeomClick?: (values: any, e: any) => void
23
+ }
24
+ const NULL_FIELD: IField = {
25
+ fid: '',
26
+ name: '',
27
+ semanticType: 'quantitative',
28
+ analyticType: 'measure',
29
+ aggName: 'sum'
30
+ }
31
+ const click$ = new Subject<ScenegraphEvent>();
32
+ const selection$ = new Subject<any>();
33
+ const geomClick$ = selection$.pipe(
34
+ op.withLatestFrom(click$),
35
+ op.filter(([values, _]) => {
36
+ if (Object.keys(values).length > 0) {
37
+ return true
38
+ }
39
+ return false
40
+ })
41
+ );
42
+ function getFieldType(field: IField): 'quantitative' | 'nominal' | 'ordinal' | 'temporal' {
43
+ if (field.analyticType === 'measure') return 'quantitative';
44
+ return 'nominal';
45
+ }
46
+
47
+ function getSingleView(xField: IField, yField: IField, color: IField, opacity: IField, size: IField, row: IField, col: IField, defaultAggregated: boolean, geomType: string) {
48
+ return {
49
+ mark: geomType,
50
+ encoding: {
51
+ x: {
52
+ field: xField.fid,
53
+ type: getFieldType(xField),
54
+ aggregate:
55
+ xField.analyticType === 'measure' &&
56
+ defaultAggregated &&
57
+ (xField.aggName as any),
58
+ },
59
+ y: {
60
+ field: yField.fid,
61
+ type: getFieldType(yField),
62
+ aggregate:
63
+ yField.analyticType === 'measure' &&
64
+ defaultAggregated &&
65
+ (yField.aggName as any),
66
+ },
67
+ row: row !== NULL_FIELD ? {
68
+ field: row.fid,
69
+ type: getFieldType(row),
70
+ } : undefined,
71
+ column: col !== NULL_FIELD ? {
72
+ field: col.fid,
73
+ type: getFieldType(col),
74
+ } : undefined,
75
+ color: color !== NULL_FIELD ? {
76
+ field: color.fid,
77
+ type: getFieldType(color)
78
+ } : undefined,
79
+ opacity: opacity !== NULL_FIELD ? {
80
+ field: opacity.fid,
81
+ type: getFieldType(opacity)
82
+ } : undefined,
83
+ size: size !== NULL_FIELD ? {
84
+ field: size.fid,
85
+ type: getFieldType(size)
86
+ } : undefined
87
+ }
88
+ };
89
+ }
90
+ const ReactVega: React.FC<ReactVegaProps> = props => {
91
+ const {
92
+ dataSource = [],
93
+ rows = [],
94
+ columns = [],
95
+ defaultAggregate = true,
96
+ geomType,
97
+ color,
98
+ opacity,
99
+ size,
100
+ onGeomClick
101
+ } = props;
102
+ const container = useRef<HTMLDivElement>(null);
103
+ useEffect(() => {
104
+ const clickSub = geomClick$.subscribe(([values, e]) => {
105
+ if (onGeomClick) {
106
+ onGeomClick(values, e);
107
+ }
108
+ })
109
+ return () => {
110
+ clickSub.unsubscribe();
111
+ }
112
+ }, []);
113
+ useEffect(() => {
114
+ if (container.current) {
115
+ const rowDims = rows.filter(f => f.analyticType === 'dimension');
116
+ const colDims = columns.filter(f => f.analyticType === 'dimension');
117
+ const rowMeas = rows.filter(f => f.analyticType === 'measure');
118
+ const colMeas = columns.filter(f => f.analyticType === 'measure');
119
+
120
+ const yField = rows.length > 0 ? rows[rows.length - 1] : NULL_FIELD;
121
+ const xField = columns.length > 0 ? columns[columns.length - 1] : NULL_FIELD;
122
+
123
+ const rowFacetFields = rowDims.slice(0, -1);
124
+ const colFacetFields = colDims.slice(0, -1);
125
+ const rowFacetField = rowFacetFields.length > 0 ? rowFacetFields[rowFacetFields.length - 1] : NULL_FIELD;
126
+ const colFacetField = colFacetFields.length > 0 ? colFacetFields[colFacetFields.length - 1] : NULL_FIELD;
127
+
128
+ const rowRepeatFields = rowMeas.length === 0 ? rowDims.slice(-1) : rowMeas;//rowMeas.slice(0, -1);
129
+ const colRepeatFields = colMeas.length === 0 ? colDims.slice(-1) : colMeas;//colMeas.slice(0, -1);
130
+
131
+ const rowRepeatField = rowRepeatFields.length > 0 ? rowRepeatFields[rowRepeatFields.length - 1] : NULL_FIELD;
132
+ const colRepeatField = colRepeatFields.length > 0 ? colRepeatFields[colRepeatFields.length - 1] : NULL_FIELD;
133
+
134
+ const dimensions = [...rows, ...columns, color, opacity, size].filter(f => Boolean(f)).map(f => (f as IField).fid)
135
+
136
+ const spec: any = {
137
+ data: {
138
+ values: dataSource,
139
+ },
140
+ selection: {
141
+ [SELECTION_NAME]: {
142
+ type: 'single',
143
+ fields: dimensions
144
+ }
145
+ }
146
+ };
147
+ if (false) {
148
+ // const singleView = getSingleView(
149
+ // xField,
150
+ // yField,
151
+ // color ? color : NULL_FIELD,
152
+ // opacity ? opacity : NULL_FIELD,
153
+ // size ? size : NULL_FIELD,
154
+ // rowFacetField,
155
+ // colFacetField,
156
+ // defaultAggregate,
157
+ // geomType
158
+ // );
159
+ // spec.mark = singleView.mark;
160
+ // spec.encoding = singleView.encoding;
161
+ } else {
162
+ spec.concat = [];
163
+ for (let i = 0; i < rowRepeatFields.length; i++) {
164
+ for (let j = 0; j < colRepeatFields.length; j++) {
165
+ const singleView = getSingleView(
166
+ colRepeatFields[j] || NULL_FIELD,
167
+ rowRepeatFields[i] || NULL_FIELD,
168
+ color ? color : NULL_FIELD,
169
+ opacity ? opacity : NULL_FIELD,
170
+ size ? size : NULL_FIELD,
171
+ rowFacetField,
172
+ colFacetField,
173
+ defaultAggregate,
174
+ geomType
175
+ );
176
+ spec.concat.push(singleView)
177
+ }
178
+ }
179
+ }
180
+ embed(container.current, spec, { mode: 'vega-lite', actions: false }).then(res => {
181
+ res.view.addEventListener('click', (e) => {
182
+ click$.next(e);
183
+ })
184
+ res.view.addSignalListener(SELECTION_NAME, (name: any, values: any) => {
185
+ selection$.next(values);
186
+ });
187
+ });
188
+ }
189
+ }, [dataSource, rows, columns, defaultAggregate, geomType, color, opacity, size]);
190
+ return <div ref={container}></div>
191
+ }
192
+
193
+ export default ReactVega;
@@ -0,0 +1,52 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+ import embed, { vega, Result } from 'vega-embed';
3
+ import { Spec } from 'vega';
4
+
5
+ interface GenVegaProps {
6
+ dataSource: any[];
7
+ spec: Spec;
8
+ signalHandler?: {
9
+ [key: string]: (name: any, value: any) => void;
10
+ };
11
+ }
12
+ const GenVega: React.FC<GenVegaProps> = (props) => {
13
+ const { spec, dataSource, signalHandler = {} } = props;
14
+ const container = useRef<HTMLDivElement>(null);
15
+ const [view, setView] = useState<Result['view']>();
16
+ useEffect(() => {
17
+ if (container.current) {
18
+ embed(container.current, spec).then((res) => {
19
+ setView(res.view);
20
+ });
21
+ }
22
+ }, [spec]);
23
+ useEffect(() => {
24
+ if (view && signalHandler) {
25
+ for (let key in signalHandler) {
26
+ view.addSignalListener('sl', signalHandler[key]);
27
+ }
28
+ }
29
+ return () => {
30
+ if (view && signalHandler) {
31
+ for (let key in signalHandler) {
32
+ view.removeSignalListener('sl', signalHandler[key]);
33
+ }
34
+ }
35
+ };
36
+ }, [view, signalHandler]);
37
+ useEffect(() => {
38
+ view &&
39
+ view.change(
40
+ 'dataSource',
41
+ vega
42
+ .changeset()
43
+ .remove(() => true)
44
+ .insert(dataSource)
45
+ );
46
+ view && view.resize();
47
+ view && view.runAsync();
48
+ }, [view, dataSource]);
49
+ return <div ref={container} />;
50
+ };
51
+
52
+ export default GenVega;