@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.
- package/dist/fields/datasetFields/index.d.ts +3 -3
- package/dist/graphic-walker.es.js +7167 -7152
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +96 -96
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/store/index.d.ts +11 -1
- package/package.json +5 -3
- package/src/App.tsx +140 -0
- package/src/assets/kanaries.ico +0 -0
- package/src/components/clickMenu.tsx +29 -0
- package/src/components/container.tsx +16 -0
- package/src/components/dataTypeIcon.tsx +20 -0
- package/src/components/liteForm.tsx +16 -0
- package/src/components/modal.tsx +85 -0
- package/src/components/sizeSetting.tsx +95 -0
- package/src/components/tabs/pureTab.tsx +70 -0
- package/src/config.ts +57 -0
- package/src/constants.ts +1 -0
- package/src/dataSource/config.ts +62 -0
- package/src/dataSource/dataSelection/csvData.tsx +77 -0
- package/src/dataSource/dataSelection/gwFile.tsx +38 -0
- package/src/dataSource/dataSelection/index.tsx +57 -0
- package/src/dataSource/dataSelection/publicData.tsx +57 -0
- package/src/dataSource/index.tsx +78 -0
- package/src/dataSource/pannel.tsx +71 -0
- package/src/dataSource/table.tsx +125 -0
- package/src/dataSource/utils.ts +47 -0
- package/src/fields/aestheticFields.tsx +23 -0
- package/src/fields/components.tsx +159 -0
- package/src/fields/datasetFields/dimFields.tsx +45 -0
- package/src/fields/datasetFields/fieldPill.tsx +10 -0
- package/src/fields/datasetFields/index.tsx +28 -0
- package/src/fields/datasetFields/meaFields.tsx +58 -0
- package/src/fields/fieldsContext.tsx +59 -0
- package/src/fields/filterField/filterEditDialog.tsx +143 -0
- package/src/fields/filterField/filterPill.tsx +113 -0
- package/src/fields/filterField/index.tsx +61 -0
- package/src/fields/filterField/slider.tsx +236 -0
- package/src/fields/filterField/tabs.tsx +421 -0
- package/src/fields/obComponents/obFContainer.tsx +40 -0
- package/src/fields/obComponents/obPill.tsx +48 -0
- package/src/fields/posFields/index.tsx +33 -0
- package/src/fields/select.tsx +31 -0
- package/src/fields/utils.ts +31 -0
- package/src/index.css +13 -0
- package/src/index.tsx +12 -0
- package/src/insightBoard/index.tsx +30 -0
- package/src/insightBoard/mainBoard.tsx +203 -0
- package/src/insightBoard/radioGroupButtons.tsx +50 -0
- package/src/insightBoard/selectionSpec.ts +113 -0
- package/src/insightBoard/std2vegaSpec.ts +184 -0
- package/src/insightBoard/utils.ts +32 -0
- package/src/insights.ts +408 -0
- package/src/interfaces.ts +154 -0
- package/src/locales/en-US.json +140 -0
- package/src/locales/i18n.ts +50 -0
- package/src/locales/zh-CN.json +140 -0
- package/src/main.tsx +10 -0
- package/src/models/visSpecHistory.ts +129 -0
- package/src/renderer/index.tsx +104 -0
- package/src/segments/visNav.tsx +48 -0
- package/src/services.ts +139 -0
- package/src/store/commonStore.ts +158 -0
- package/src/store/index.tsx +53 -0
- package/src/store/visualSpecStore.ts +586 -0
- package/src/utils/autoMark.ts +34 -0
- package/src/utils/index.ts +251 -0
- package/src/utils/normalization.ts +158 -0
- package/src/utils/save.ts +46 -0
- package/src/vis/future-react-vega.tsx +193 -0
- package/src/vis/gen-vega.tsx +52 -0
- package/src/vis/react-vega.tsx +398 -0
- package/src/visualSettings/index.tsx +252 -0
- package/src/visualSettings/menubar.tsx +109 -0
- package/src/vite-env.d.ts +1 -0
- package/src/workers/explainer.worker.js +78 -0
- 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;
|