@kanaries/graphic-walker 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +4 -3
- package/dist/assets/transform.worker-5d54ff09.js.map +1 -0
- package/dist/assets/viewQuery.worker-ffefc111.js.map +1 -0
- package/dist/components/codeExport/index.d.ts +3 -0
- package/dist/components/loadingLayer.d.ts +2 -0
- package/dist/components/tabs/defaultTab.d.ts +1 -0
- package/dist/components/tabs/editableTab.d.ts +1 -2
- package/dist/dataSource/dataSelection/config.d.ts +1 -0
- package/dist/dataSource/dataSelection/utils.d.ts +2 -0
- package/dist/dataSource/index.d.ts +0 -1
- package/dist/datasets/tmp/test.json +1 -0
- package/dist/fields/encodeFields/singleEncodeEditor.d.ts +4 -4
- package/dist/graphic-walker.es.js +29228 -33650
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +218 -256
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/interfaces.d.ts +52 -17
- package/dist/lib/execExp.d.ts +8 -0
- package/dist/lib/inferMeta.d.ts +2 -9
- package/dist/lib/insights/explainByChildren.d.ts +16 -0
- package/dist/lib/insights/explainBySelection.d.ts +5 -0
- package/dist/lib/insights/explainValue.d.ts +2 -0
- package/dist/lib/insights/utils.d.ts +11 -0
- package/dist/lib/interfaces.d.ts +23 -0
- package/dist/lib/op/aggregate.d.ts +3 -0
- package/dist/lib/op/bin.d.ts +3 -0
- package/dist/lib/op/fold.d.ts +3 -0
- package/dist/lib/op/stat.d.ts +8 -0
- package/dist/lib/viewQuery.d.ts +4 -0
- package/dist/models/visSpecHistory.d.ts +2 -0
- package/dist/renderer/index.d.ts +8 -7
- package/dist/renderer/specRenderer.d.ts +13 -0
- package/dist/services.d.ts +5 -31
- package/dist/store/commonStore.d.ts +6 -0
- package/dist/store/index.d.ts +3 -2
- package/dist/store/visualSpecStore.d.ts +11 -5
- package/dist/utils/autoMark.d.ts +1 -1
- package/dist/utils/dataPrep.d.ts +3 -2
- package/dist/utils/index.d.ts +3 -5
- package/dist/utils/save.d.ts +1 -2
- package/dist/vis/react-vega.d.ts +2 -22
- package/dist/vis/spec/aggregate.d.ts +4 -0
- package/dist/vis/spec/encode.d.ts +20 -0
- package/dist/vis/spec/field.d.ts +2 -0
- package/dist/vis/spec/mark.d.ts +7 -0
- package/dist/vis/spec/stack.d.ts +4 -0
- package/dist/vis/spec/tooltip.d.ts +4 -0
- package/dist/vis/spec/view.d.ts +73 -0
- package/dist/workers/transform.d.ts +2 -0
- package/package.json +5 -6
- package/src/App.tsx +56 -66
- package/src/components/codeExport/index.tsx +114 -0
- package/src/components/dataTable/index.tsx +10 -10
- package/src/components/loadingLayer.tsx +7 -0
- package/src/components/tabs/defaultTab.tsx +4 -2
- package/src/components/tabs/editableTab.tsx +74 -39
- package/src/dataSource/dataSelection/config.ts +11 -0
- package/src/dataSource/dataSelection/csvData.tsx +71 -39
- package/src/dataSource/dataSelection/gwFile.tsx +2 -2
- package/src/dataSource/dataSelection/utils.ts +28 -0
- package/src/dataSource/index.tsx +0 -17
- package/src/dataSource/utils.ts +8 -3
- package/src/fields/aestheticFields.tsx +1 -2
- package/src/fields/datasetFields/meaFields.tsx +12 -4
- package/src/fields/encodeFields/singleEncodeEditor.tsx +11 -12
- package/src/fields/fieldsContext.tsx +1 -0
- package/src/index.css +4 -4
- package/src/index.tsx +22 -22
- package/src/interfaces.ts +85 -49
- package/src/lib/execExp.ts +147 -0
- package/src/lib/inferMeta.ts +26 -29
- package/src/lib/insights/explainByChildren.ts +50 -0
- package/src/lib/insights/explainBySelection.ts +47 -0
- package/src/lib/insights/explainValue.ts +30 -0
- package/src/lib/insights/utils.ts +21 -0
- package/src/lib/interfaces.ts +33 -0
- package/src/lib/op/aggregate.ts +49 -0
- package/src/lib/op/bin.ts +25 -0
- package/src/lib/op/fold.ts +17 -0
- package/src/lib/op/stat.ts +46 -0
- package/src/lib/viewQuery.ts +22 -0
- package/src/locales/en-US.json +6 -3
- package/src/locales/i18n.ts +0 -1
- package/src/locales/ja-JP.json +4 -2
- package/src/locales/zh-CN.json +6 -3
- package/src/main.tsx +1 -1
- package/src/models/visSpecHistory.ts +14 -0
- package/src/renderer/index.tsx +58 -126
- package/src/renderer/specRenderer.tsx +121 -0
- package/src/segments/segmentNav.tsx +3 -16
- package/src/segments/visNav.tsx +17 -6
- package/src/services.ts +101 -67
- package/src/store/commonStore.ts +14 -9
- package/src/store/index.tsx +11 -4
- package/src/store/visualSpecStore.ts +89 -52
- package/src/utils/autoMark.ts +1 -1
- package/src/utils/dataPrep.ts +25 -2
- package/src/utils/index.ts +16 -17
- package/src/utils/normalization.ts +3 -1
- package/src/utils/save.ts +1 -2
- package/src/vis/react-vega.tsx +9 -340
- package/src/vis/spec/aggregate.ts +13 -0
- package/src/vis/spec/encode.ts +70 -0
- package/src/vis/spec/field.ts +10 -0
- package/src/vis/spec/mark.ts +30 -0
- package/src/vis/spec/stack.ts +11 -0
- package/src/vis/spec/tooltip.ts +16 -0
- package/src/vis/spec/view.ts +136 -0
- package/src/vis/theme.ts +12 -0
- package/src/visualSettings/index.tsx +10 -1
- package/src/workers/transform.ts +12 -0
- package/src/workers/transform.worker.js +13 -0
- package/src/workers/viewQuery.worker.js +16 -0
- package/dist/assets/explainer.worker-8428eb12.js.map +0 -1
- package/dist/dataSource/pannel.d.ts +0 -5
- package/dist/insightBoard/index.d.ts +0 -3
- package/dist/insightBoard/mainBoard.d.ts +0 -11
- package/dist/insightBoard/radioGroupButtons.d.ts +0 -12
- package/dist/insightBoard/selectionSpec.d.ts +0 -13
- package/dist/insightBoard/std2vegaSpec.d.ts +0 -12
- package/dist/insightBoard/utils.d.ts +0 -8
- package/dist/insights.d.ts +0 -61
- package/src/dataSource/pannel.tsx +0 -71
- package/src/insightBoard/index.tsx +0 -31
- package/src/insightBoard/mainBoard.tsx +0 -224
- package/src/insightBoard/radioGroupButtons.tsx +0 -57
- package/src/insightBoard/selectionSpec.ts +0 -113
- package/src/insightBoard/std2vegaSpec.ts +0 -184
- package/src/insightBoard/utils.ts +0 -32
- package/src/insights.ts +0 -408
- package/src/workers/explainer.worker.js +0 -76
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { IExpParamter, IExpression, IField, IRow } from "../interfaces";
|
|
2
|
+
|
|
3
|
+
interface IDataFrame {
|
|
4
|
+
[key: string]: any[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function execExpression (exp: IExpression, dataFrame: IDataFrame, columns: IField[]): IDataFrame {
|
|
8
|
+
const { op, params } = exp;
|
|
9
|
+
const subFrame: IDataFrame = { ...dataFrame };
|
|
10
|
+
const len = dataFrame[Object.keys(dataFrame)[0]].length;
|
|
11
|
+
for (let param of params) {
|
|
12
|
+
switch (param.type) {
|
|
13
|
+
case 'field':
|
|
14
|
+
subFrame[param.value] = dataFrame[param.value];
|
|
15
|
+
break;
|
|
16
|
+
case 'constant':
|
|
17
|
+
subFrame[param.value] = new Array(len).fill(param.value);
|
|
18
|
+
break;
|
|
19
|
+
case 'expression':
|
|
20
|
+
let f = execExpression(param.value, dataFrame, columns);
|
|
21
|
+
Object.keys(f).forEach(key => {
|
|
22
|
+
subFrame[key] = f[key];
|
|
23
|
+
})
|
|
24
|
+
break;
|
|
25
|
+
case 'value':
|
|
26
|
+
default:
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
switch (op) {
|
|
31
|
+
case 'one':
|
|
32
|
+
return one(exp.as, params, subFrame);
|
|
33
|
+
case 'bin':
|
|
34
|
+
return bin(exp.as, params, subFrame);
|
|
35
|
+
case 'log2':
|
|
36
|
+
return log2(exp.as, params, subFrame);
|
|
37
|
+
case 'log10':
|
|
38
|
+
return log10(exp.as, params, subFrame);
|
|
39
|
+
case 'binCount':
|
|
40
|
+
return binCount(exp.as, params, subFrame);
|
|
41
|
+
default:
|
|
42
|
+
return subFrame;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function bin(resKey: string, params: IExpParamter[], data: IDataFrame, binSize: number | undefined = 10): IDataFrame {
|
|
47
|
+
const { value: fieldKey } = params[0];
|
|
48
|
+
const fieldValues = data[fieldKey] as number[];
|
|
49
|
+
let _min = Infinity;
|
|
50
|
+
let _max = -Infinity;
|
|
51
|
+
for (let i = 0; i < fieldValues.length; i++) {
|
|
52
|
+
let val = fieldValues[i];
|
|
53
|
+
if (val > _max) _max = val;
|
|
54
|
+
if (val < _min) _min = val;
|
|
55
|
+
}
|
|
56
|
+
const step = (_max - _min) / binSize;
|
|
57
|
+
const beaStep = Math.max(-Math.round(Math.log10(_max - _min)) + 2, 0)
|
|
58
|
+
const newValues = fieldValues.map((v: number) => {
|
|
59
|
+
let bIndex = Math.floor((v - _min) / step);
|
|
60
|
+
if (bIndex === binSize) bIndex = binSize - 1;
|
|
61
|
+
return Number(((bIndex * step + _min)).toFixed(beaStep))
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
...data,
|
|
65
|
+
[resKey]: newValues,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function binCount(resKey: string, params: IExpParamter[], data: IDataFrame, binSize: number | undefined = 10): IDataFrame {
|
|
70
|
+
const { value: fieldKey } = params[0];
|
|
71
|
+
const fieldValues = data[fieldKey] as number[];
|
|
72
|
+
|
|
73
|
+
const valueWithIndices: {val: number; index: number; orderIndex: number }[] = fieldValues.map((v, i) => ({
|
|
74
|
+
val: v,
|
|
75
|
+
index: i
|
|
76
|
+
})).sort((a, b) => a.val - b.val)
|
|
77
|
+
.map((item, i) => ({
|
|
78
|
+
val: item.val,
|
|
79
|
+
index: item.index,
|
|
80
|
+
orderIndex: i
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const groupSize = valueWithIndices.length / binSize;
|
|
84
|
+
|
|
85
|
+
const newValues = valueWithIndices.map(item => {
|
|
86
|
+
let bIndex = Math.floor(item.orderIndex / groupSize);
|
|
87
|
+
if (bIndex === binSize) bIndex = binSize - 1;
|
|
88
|
+
return bIndex + 1
|
|
89
|
+
})
|
|
90
|
+
return {
|
|
91
|
+
...data,
|
|
92
|
+
[resKey]: newValues,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function log2(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {
|
|
97
|
+
const { value } = params[0];
|
|
98
|
+
const field = data[value];
|
|
99
|
+
const newField = field.map((v: number) => Math.log2(v));
|
|
100
|
+
return {
|
|
101
|
+
...data,
|
|
102
|
+
[resKey]: newField,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function log10(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {
|
|
107
|
+
const { value: fieldKey } = params[0];
|
|
108
|
+
const fieldValues = data[fieldKey];
|
|
109
|
+
const newField = fieldValues.map((v: number) => Math.log10(v));
|
|
110
|
+
return {
|
|
111
|
+
...data,
|
|
112
|
+
[resKey]: newField,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function one(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {
|
|
117
|
+
// const { value: fieldKey } = params[0];
|
|
118
|
+
if (Object.keys(data).length === 0) return data;
|
|
119
|
+
const len = data[Object.keys(data)[0]].length;
|
|
120
|
+
const newField = new Array(len).fill(1);
|
|
121
|
+
return {
|
|
122
|
+
...data,
|
|
123
|
+
[resKey]: newField,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function dataset2DataFrame(dataset: IRow[], columns: IField[]): IDataFrame {
|
|
128
|
+
const dataFrame: IDataFrame = {};
|
|
129
|
+
columns.forEach((col) => {
|
|
130
|
+
dataFrame[col.fid] = dataset.map((row) => row[col.fid]);
|
|
131
|
+
});
|
|
132
|
+
return dataFrame;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function dataframe2Dataset(dataFrame: IDataFrame, columns: IField[]): IRow[] {
|
|
136
|
+
if (columns.length === 0) return [];
|
|
137
|
+
const dataset: IRow[] = [];
|
|
138
|
+
const len = dataFrame[Object.keys(dataFrame)[0]].length;
|
|
139
|
+
for (let i = 0; i < len; i++) {
|
|
140
|
+
const row: IRow = {};
|
|
141
|
+
columns.forEach((col) => {
|
|
142
|
+
row[col.fid] = dataFrame[col.fid][i];
|
|
143
|
+
});
|
|
144
|
+
dataset.push(row);
|
|
145
|
+
}
|
|
146
|
+
return dataset;
|
|
147
|
+
}
|
package/src/lib/inferMeta.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { IAnalyticType, ISemanticType,
|
|
2
|
-
import { IMutField, IRow, IUncertainMutField } from "../interfaces";
|
|
1
|
+
import { IAnalyticType, IMutField, IRow, ISemanticType, IUncertainMutField } from '../interfaces';
|
|
3
2
|
|
|
4
3
|
const COMMON_TIME_FORMAT: RegExp[] = [
|
|
5
4
|
/^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
|
|
@@ -8,13 +7,13 @@ const COMMON_TIME_FORMAT: RegExp[] = [
|
|
|
8
7
|
/^\d{4}\/\d{2}\/\d{2}$/, // YYYY/MM/DD
|
|
9
8
|
/^\d{4}\.\d{2}\.\d{2}$/, // YYYY.MM.DD
|
|
10
9
|
/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/, // YYYY-MM-DD HH:MM:SS
|
|
11
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}
|
|
10
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, // YYYY-MM-DDTHH:MM:SS (ISO-8601)
|
|
12
11
|
];
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* check if this array is a date time array based on some common date format
|
|
16
15
|
* @param data string array
|
|
17
|
-
* @returns
|
|
16
|
+
* @returns
|
|
18
17
|
*/
|
|
19
18
|
export function isDateTimeArray(data: string[]): boolean {
|
|
20
19
|
let isDateTime = true;
|
|
@@ -34,6 +33,19 @@ export function isDateTimeArray(data: string[]): boolean {
|
|
|
34
33
|
return isDateTime;
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
export function isNumericArray(data: any[]): boolean {
|
|
37
|
+
return data.every((item) => {
|
|
38
|
+
// Check if the item is already a number
|
|
39
|
+
if (typeof item === 'number') {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if the item can be converted into a number
|
|
44
|
+
const number = parseFloat(item);
|
|
45
|
+
return !isNaN(number) && isFinite(item);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
function inferAnalyticTypeFromSemanticType(semanticType: ISemanticType): IAnalyticType {
|
|
38
50
|
switch (semanticType) {
|
|
39
51
|
case 'quantitative':
|
|
@@ -43,37 +55,22 @@ function inferAnalyticTypeFromSemanticType(semanticType: ISemanticType): IAnalyt
|
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
/**
|
|
47
|
-
* 这里目前暂时包一层,是为了解耦具体的推断实现。后续这里要调整推断的逻辑。
|
|
48
|
-
* 需要讨论这一层是否和交互层有关,如果没有关系,这一层包裹可以不存在这里,而是在visual-insights中。
|
|
49
|
-
* @param data 原始数据
|
|
50
|
-
* @param fid 字段id
|
|
51
|
-
* @returns semantic type 列表
|
|
52
|
-
*/
|
|
53
58
|
export function inferSemanticType(data: IRow[], fid: string): ISemanticType {
|
|
54
|
-
|
|
59
|
+
const values = data.map((row) => row[fid]);
|
|
60
|
+
|
|
61
|
+
let st: ISemanticType = isNumericArray(values) ? 'quantitative' : 'nominal';
|
|
55
62
|
if (st === 'nominal') {
|
|
56
63
|
if (isDateTimeArray(data.map((row) => `${row[fid]}`))) st = 'temporal';
|
|
57
|
-
} else if (st === 'ordinal') {
|
|
58
|
-
const valueSet: Set<number> = new Set();
|
|
59
|
-
let _max = -Infinity;
|
|
60
|
-
let _min = Infinity;
|
|
61
|
-
for (let v of valueSet) {
|
|
62
|
-
_max = Math.max(_max, v);
|
|
63
|
-
_min = Math.max(_min, v);
|
|
64
|
-
}
|
|
65
|
-
if (_max - _min + 1 !== valueSet.size) {
|
|
66
|
-
st = 'quantitative';
|
|
67
|
-
}
|
|
68
64
|
}
|
|
69
65
|
return st;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
export function inferMeta
|
|
68
|
+
export function inferMeta(props: { dataSource: IRow[]; fields: IUncertainMutField[] }): IMutField[] {
|
|
73
69
|
const { dataSource, fields } = props;
|
|
74
|
-
const finalFieldMetas: IMutField[] = []
|
|
70
|
+
const finalFieldMetas: IMutField[] = [];
|
|
75
71
|
for (let field of fields) {
|
|
76
|
-
let semanticType: ISemanticType =
|
|
72
|
+
let semanticType: ISemanticType =
|
|
73
|
+
field.semanticType === '?' ? inferSemanticType(dataSource, field.fid) : field.semanticType;
|
|
77
74
|
let analyticType: IAnalyticType = inferAnalyticTypeFromSemanticType(semanticType);
|
|
78
75
|
|
|
79
76
|
finalFieldMetas.push({
|
|
@@ -82,7 +79,7 @@ export function inferMeta (props: { dataSource: IRow[]; fields: IUncertainMutFie
|
|
|
82
79
|
name: field.name ? field.name : field.fid,
|
|
83
80
|
analyticType,
|
|
84
81
|
semanticType,
|
|
85
|
-
})
|
|
82
|
+
});
|
|
86
83
|
}
|
|
87
|
-
return finalFieldMetas
|
|
88
|
-
}
|
|
84
|
+
return finalFieldMetas;
|
|
85
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { IMeasure, IRow } from '../../interfaces';
|
|
2
|
+
import { IPredicate, checkChildOutlier, checkMajorFactor, filterByPredicates } from '../../utils';
|
|
3
|
+
import { aggregate } from '../op/aggregate';
|
|
4
|
+
|
|
5
|
+
export function explainByChildren(
|
|
6
|
+
dataSource: IRow[],
|
|
7
|
+
predicates: IPredicate[],
|
|
8
|
+
dimensions: string[],
|
|
9
|
+
measures: IMeasure[]
|
|
10
|
+
) {
|
|
11
|
+
// 1. find most relative dimensions(topK)
|
|
12
|
+
// 2. for each dimension, we check all the dim member in it. find the member whos distribution is most close to current one.
|
|
13
|
+
// here we do not nomorlize all the dim member's distribution, we use the relative distribution instead.
|
|
14
|
+
// 3. the dim member we found can be used to explain current one as major factor.
|
|
15
|
+
// const predicates: IPredicate[] = selection === 'all' ? [] : getPredicates(selection, dimensions, []);
|
|
16
|
+
const viewData = aggregate(dataSource, {
|
|
17
|
+
groupBy: dimensions,
|
|
18
|
+
op: 'aggregate',
|
|
19
|
+
agg: Object.fromEntries(measures.map((mea) => [mea.key, mea.op])),
|
|
20
|
+
});
|
|
21
|
+
const measureIds = measures.map((m) => m.key);
|
|
22
|
+
const parentData = filterByPredicates(viewData, predicates);
|
|
23
|
+
|
|
24
|
+
const majorList: Array<{ key: string; score: number; dimensions: string[]; measures: IMeasure[] }> = [];
|
|
25
|
+
const outlierList: Array<{ key: string; score: number; dimensions: string[]; measures: IMeasure[] }> = [];
|
|
26
|
+
for (let extendDim of dimensions) {
|
|
27
|
+
const data = aggregate(dataSource, {
|
|
28
|
+
groupBy: dimensions.concat(extendDim),
|
|
29
|
+
op: 'aggregate',
|
|
30
|
+
agg: Object.fromEntries(measures.map((mea) => [mea.key, mea.op])),
|
|
31
|
+
});
|
|
32
|
+
let groups: Map<any, IRow[]> = new Map();
|
|
33
|
+
for (let record of data) {
|
|
34
|
+
if (!groups.has(record[extendDim])) {
|
|
35
|
+
groups.set(record[extendDim], []);
|
|
36
|
+
}
|
|
37
|
+
groups.get(record[extendDim])?.push(record);
|
|
38
|
+
}
|
|
39
|
+
const { majorKey, majorSum } = checkMajorFactor(parentData, groups, dimensions, measureIds);
|
|
40
|
+
majorList.push({ key: majorKey, score: majorSum, dimensions: [extendDim], measures });
|
|
41
|
+
const { outlierKey, outlierSum } = checkChildOutlier(parentData, groups, dimensions, measureIds);
|
|
42
|
+
outlierList.push({ key: outlierKey, score: outlierSum, dimensions: [extendDim], measures });
|
|
43
|
+
}
|
|
44
|
+
majorList.sort((a, b) => a.score - b.score);
|
|
45
|
+
outlierList.sort((a, b) => b.score - a.score);
|
|
46
|
+
return {
|
|
47
|
+
majorList,
|
|
48
|
+
outlierList,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IExplainProps, IField } from '../../interfaces';
|
|
2
|
+
import { filterByPredicates } from '../../utils';
|
|
3
|
+
import { compareDistribution, normalizeWithParent } from '../../utils/normalization';
|
|
4
|
+
import { aggregate } from '../op/aggregate';
|
|
5
|
+
import { complementaryFields, groupByAnalyticTypes, meaList2AggProps } from './utils';
|
|
6
|
+
|
|
7
|
+
export function explainBySelection(props: IExplainProps) {
|
|
8
|
+
const { metas, dataSource, viewFields, predicates } = props;
|
|
9
|
+
const { dimensions: dimsInView, measures: measInView } = groupByAnalyticTypes(viewFields);
|
|
10
|
+
const complementaryDimensions = complementaryFields({
|
|
11
|
+
all: metas.filter((f) => f.analyticType === 'dimension'),
|
|
12
|
+
selection: dimsInView,
|
|
13
|
+
});
|
|
14
|
+
const outlierList: Array<{ score: number; viiewFields: IField[] }> = complementaryDimensions.map(extendDim => {
|
|
15
|
+
const overallData = aggregate(dataSource, {
|
|
16
|
+
groupBy: [extendDim.fid],
|
|
17
|
+
op: 'aggregate',
|
|
18
|
+
agg: meaList2AggProps(measInView),
|
|
19
|
+
});
|
|
20
|
+
const viewData = aggregate(dataSource, {
|
|
21
|
+
groupBy: dimsInView.map((f) => f.fid),
|
|
22
|
+
op: 'aggregate',
|
|
23
|
+
agg: meaList2AggProps(measInView),
|
|
24
|
+
});
|
|
25
|
+
const subData = filterByPredicates(viewData, predicates);
|
|
26
|
+
|
|
27
|
+
let outlierNormalization = normalizeWithParent(
|
|
28
|
+
subData,
|
|
29
|
+
overallData,
|
|
30
|
+
measInView.map((mea) => mea.fid),
|
|
31
|
+
false
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
let outlierScore = compareDistribution(
|
|
35
|
+
outlierNormalization.normalizedData,
|
|
36
|
+
outlierNormalization.normalizedParentData,
|
|
37
|
+
[extendDim.fid],
|
|
38
|
+
measInView.map((mea) => mea.fid)
|
|
39
|
+
);
|
|
40
|
+
return {
|
|
41
|
+
viiewFields: measInView.concat(extendDim),
|
|
42
|
+
score: outlierScore,
|
|
43
|
+
}
|
|
44
|
+
}).sort((a, b) => b.score - a.score)
|
|
45
|
+
|
|
46
|
+
return outlierList;
|
|
47
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IAggregator, IExplainProps } from '../../interfaces';
|
|
2
|
+
import { filterByPredicates } from '../../utils';
|
|
3
|
+
import { aggregate } from '../op/aggregate';
|
|
4
|
+
import { complementaryFields, groupByAnalyticTypes } from './utils';
|
|
5
|
+
|
|
6
|
+
export function explainValue(props: IExplainProps): number[] {
|
|
7
|
+
const { viewFields, dataSource, predicates } = props;
|
|
8
|
+
const { dimensions: dimsInView, measures: measInView } = groupByAnalyticTypes(viewFields);
|
|
9
|
+
const viewData = aggregate(dataSource, {
|
|
10
|
+
groupBy: dimsInView.map((f) => f.fid),
|
|
11
|
+
op: 'aggregate',
|
|
12
|
+
agg: Object.fromEntries(measInView.map((mea) => [mea.fid, (mea.aggName ?? 'sum') as IAggregator])),
|
|
13
|
+
});
|
|
14
|
+
const selection = filterByPredicates(viewData, predicates);
|
|
15
|
+
const cmps: number[] = [];
|
|
16
|
+
for (let mea of measInView) {
|
|
17
|
+
const values = viewData.map((r) => r[mea.fid]).sort((a, b) => a - b);
|
|
18
|
+
const selectionValues = selection.map((r) => r[mea.fid]);
|
|
19
|
+
const lowerBoundary: number = values[Math.floor(values.length * 0.15)];
|
|
20
|
+
const higherBoundary: number = values[Math.min(Math.ceil(values.length * 0.85), values.length - 1)];
|
|
21
|
+
if (selectionValues.some((v) => v >= higherBoundary)) {
|
|
22
|
+
cmps.push(1);
|
|
23
|
+
} else if (selectionValues.some((v) => v <= lowerBoundary)) {
|
|
24
|
+
cmps.push(-1);
|
|
25
|
+
} else {
|
|
26
|
+
cmps.push(0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cmps;
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IAggregator, IField } from '../../interfaces';
|
|
2
|
+
import { IAggQuery } from '../interfaces';
|
|
3
|
+
|
|
4
|
+
export function groupByAnalyticTypes(fields: IField[]) {
|
|
5
|
+
const dimensions = fields.filter((f) => f.analyticType === 'dimension');
|
|
6
|
+
const measures = fields.filter((f) => f.analyticType === 'measure');
|
|
7
|
+
return {
|
|
8
|
+
dimensions,
|
|
9
|
+
measures,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function meaList2AggProps(measures: IField[]): IAggQuery['agg'] {
|
|
14
|
+
return Object.fromEntries(measures.map((mea) => [mea.fid, (mea.aggName ?? 'sum') as IAggregator]));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function complementaryFields(props: { selection: IField[]; all: IField[] }): IField[] {
|
|
18
|
+
return props.all
|
|
19
|
+
.filter((f) => f.analyticType === 'dimension')
|
|
20
|
+
.filter((f) => !props.selection.find((vf) => vf.fid === f.fid));
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IAggregator } from "../interfaces";
|
|
2
|
+
|
|
3
|
+
export interface IAggQuery {
|
|
4
|
+
op: 'aggregate';
|
|
5
|
+
groupBy: string[];
|
|
6
|
+
agg: {
|
|
7
|
+
[field: string]: IAggregator;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// interface IFilterQuery {
|
|
12
|
+
// op: 'filter';
|
|
13
|
+
// filter: string;
|
|
14
|
+
// }
|
|
15
|
+
|
|
16
|
+
export interface IFoldQuery {
|
|
17
|
+
op: 'fold';
|
|
18
|
+
foldBy: string[];
|
|
19
|
+
newFoldKeyCol: string;
|
|
20
|
+
newFoldValueCol: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IBinQuery {
|
|
24
|
+
op: 'bin';
|
|
25
|
+
binBy: string;
|
|
26
|
+
newBinCol: string;
|
|
27
|
+
binSize: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export interface IRawQuery {
|
|
32
|
+
op: 'raw';
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { IRow } from "../../interfaces";
|
|
2
|
+
import { IAggQuery } from "../interfaces";
|
|
3
|
+
import { sum, mean, median, stdev, variance, max, min, count } from "./stat";
|
|
4
|
+
|
|
5
|
+
const aggregatorMap = {
|
|
6
|
+
sum,
|
|
7
|
+
mean,
|
|
8
|
+
median,
|
|
9
|
+
stdev,
|
|
10
|
+
variance,
|
|
11
|
+
max,
|
|
12
|
+
min,
|
|
13
|
+
count,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const KEY_JOINER = '___';
|
|
17
|
+
|
|
18
|
+
export function aggregate (data: IRow[], query: IAggQuery): IRow[] {
|
|
19
|
+
const { groupBy, agg } = query;
|
|
20
|
+
const ans: Map<string, IRow> = new Map();
|
|
21
|
+
const groups: Map<string, IRow[]> = new Map();
|
|
22
|
+
for (let row of data) {
|
|
23
|
+
const gk = groupBy.map((k) => row[k]).join(KEY_JOINER);
|
|
24
|
+
|
|
25
|
+
if (!groups.has(gk)) {
|
|
26
|
+
groups.set(gk, []);
|
|
27
|
+
}
|
|
28
|
+
groups.get(gk)?.push(row);
|
|
29
|
+
}
|
|
30
|
+
for (let [gk, subGroup] of groups) {
|
|
31
|
+
if (subGroup.length === 0) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let aggRow: IRow = {};
|
|
35
|
+
for (let k of groupBy) {
|
|
36
|
+
aggRow[k] = subGroup[0][k];
|
|
37
|
+
}
|
|
38
|
+
for (let meaKey in agg) {
|
|
39
|
+
if (aggRow[meaKey] === undefined) {
|
|
40
|
+
aggRow[meaKey] = 0;
|
|
41
|
+
}
|
|
42
|
+
const values: number[] = subGroup.map((r) => r[meaKey]) ?? [];
|
|
43
|
+
const aggregator = aggregatorMap[agg[meaKey]] ?? sum;
|
|
44
|
+
aggRow[meaKey] = aggregator(values);
|
|
45
|
+
}
|
|
46
|
+
ans.set(gk, aggRow);
|
|
47
|
+
}
|
|
48
|
+
return Array.from(ans.values());
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IRow } from "../../interfaces";
|
|
2
|
+
import { IBinQuery } from "../interfaces";
|
|
3
|
+
|
|
4
|
+
export function bin (dataSource: IRow[], query: IBinQuery): IRow[] {
|
|
5
|
+
const { binBy, newBinCol, binSize } = query;
|
|
6
|
+
let _min = Infinity;
|
|
7
|
+
let _max = -Infinity;
|
|
8
|
+
for (let i = 0; i < dataSource.length; i++) {
|
|
9
|
+
let val = dataSource[i][binBy];
|
|
10
|
+
if (val > _max) _max = val;
|
|
11
|
+
if (val < _min) _min = val;
|
|
12
|
+
}
|
|
13
|
+
const step = (_max - _min) / binSize;
|
|
14
|
+
// const beaStep = Math.max(-Math.round(Math.log10(_max - _min)) + 2, 0)
|
|
15
|
+
return dataSource.map((r) => {
|
|
16
|
+
let bIndex = Math.floor((r[binBy] - _min) / step);
|
|
17
|
+
if (bIndex === binSize) bIndex = binSize - 1;
|
|
18
|
+
return {
|
|
19
|
+
...r,
|
|
20
|
+
[newBinCol]: [bIndex * step + _min, (bIndex + 1) * step + _min],
|
|
21
|
+
|
|
22
|
+
// [binFid]: Number(((bIndex * step + _min)).toFixed(beaStep)),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IRow } from "../../interfaces";
|
|
2
|
+
import { IFoldQuery } from "../interfaces";
|
|
3
|
+
|
|
4
|
+
export function fold (data: IRow[], query: IFoldQuery): IRow[] {
|
|
5
|
+
const { foldBy, newFoldKeyCol, newFoldValueCol } = query;
|
|
6
|
+
const ans: IRow[] = [];
|
|
7
|
+
for (let row of data) {
|
|
8
|
+
for (let k of foldBy) {
|
|
9
|
+
const newRow = { ...row };
|
|
10
|
+
newRow[newFoldKeyCol] = k;
|
|
11
|
+
newRow[newFoldValueCol] = row[k];
|
|
12
|
+
delete newRow[k];
|
|
13
|
+
ans.push(newRow);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return ans;
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function mean(nums: number[]): number {
|
|
2
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function sum(nums: number[]): number {
|
|
6
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function median(nums: number[]): number {
|
|
10
|
+
const sorted = nums.sort((a, b) => a - b);
|
|
11
|
+
const mid = Math.floor(sorted.length / 2);
|
|
12
|
+
return sorted.length % 2 === 0 ? (sorted[mid] + sorted[mid - 1]) / 2 : sorted[mid];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function variance(nums: number[]): number {
|
|
16
|
+
const m = mean(nums);
|
|
17
|
+
return mean(nums.map((x) => (x - m) ** 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stdev(nums: number[]): number {
|
|
21
|
+
return Math.sqrt(variance(nums));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function max(nums: number[]): number {
|
|
25
|
+
let ans = -Infinity;
|
|
26
|
+
for (let n of nums) {
|
|
27
|
+
if (n > ans) {
|
|
28
|
+
ans = n;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return ans;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function min(nums: number[]): number {
|
|
35
|
+
let ans = Infinity;
|
|
36
|
+
for (let n of nums) {
|
|
37
|
+
if (n < ans) {
|
|
38
|
+
ans = n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ans;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function count(nums: number[]): number {
|
|
45
|
+
return nums.length;
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IMutField, IRow } from "../interfaces";
|
|
2
|
+
import { aggregate } from "./op/aggregate";
|
|
3
|
+
import { fold } from "./op/fold";
|
|
4
|
+
import { IAggQuery, IBinQuery, IFoldQuery, IRawQuery } from "./interfaces";
|
|
5
|
+
import { bin } from "./op/bin";
|
|
6
|
+
|
|
7
|
+
export type IViewQuery = IAggQuery | IFoldQuery | IBinQuery | IRawQuery;
|
|
8
|
+
|
|
9
|
+
export function queryView (rawData: IRow[], metas: IMutField[], query: IViewQuery) {
|
|
10
|
+
switch (query.op) {
|
|
11
|
+
case 'aggregate':
|
|
12
|
+
return aggregate(rawData, query);
|
|
13
|
+
case 'fold':
|
|
14
|
+
return fold(rawData, query);
|
|
15
|
+
case 'bin':
|
|
16
|
+
return bin(rawData, query);
|
|
17
|
+
case 'raw':
|
|
18
|
+
default:
|
|
19
|
+
return rawData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
package/src/locales/en-US.json
CHANGED
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"shape": "Shape",
|
|
59
59
|
"theta": "Angle",
|
|
60
60
|
"radius": "Radius",
|
|
61
|
-
"filters": "Filters"
|
|
61
|
+
"filters": "Filters",
|
|
62
|
+
"details": "Details"
|
|
62
63
|
},
|
|
63
64
|
"aggregator": {
|
|
64
65
|
"sum": "Sum",
|
|
@@ -117,7 +118,8 @@
|
|
|
117
118
|
"main": {
|
|
118
119
|
"tablist": {
|
|
119
120
|
"new": "+ New",
|
|
120
|
-
"
|
|
121
|
+
"auto_title": "Chart {{idx}}",
|
|
122
|
+
"chart_name": "Chart Name"
|
|
121
123
|
},
|
|
122
124
|
"tabpanel": {
|
|
123
125
|
"menubar": {
|
|
@@ -190,6 +192,7 @@
|
|
|
190
192
|
"prev": "Previous",
|
|
191
193
|
"next": "Next",
|
|
192
194
|
"drop_field": "Drop Field Here",
|
|
193
|
-
"confirm": "Confirm"
|
|
195
|
+
"confirm": "Confirm",
|
|
196
|
+
"cancel": "Cancel"
|
|
194
197
|
}
|
|
195
198
|
}
|
package/src/locales/i18n.ts
CHANGED
package/src/locales/ja-JP.json
CHANGED
|
@@ -117,7 +117,8 @@
|
|
|
117
117
|
"main": {
|
|
118
118
|
"tablist": {
|
|
119
119
|
"new": "+ 新規作成",
|
|
120
|
-
"
|
|
120
|
+
"auto_title": "グラフ {{idx}}",
|
|
121
|
+
"chart_name": "グラフ名"
|
|
121
122
|
},
|
|
122
123
|
"tabpanel": {
|
|
123
124
|
"menubar": {
|
|
@@ -190,6 +191,7 @@
|
|
|
190
191
|
"prev": "前へ",
|
|
191
192
|
"next": "次へ",
|
|
192
193
|
"drop_field": "ここにフィールドをドロップ",
|
|
193
|
-
"confirm": "確認"
|
|
194
|
+
"confirm": "確認",
|
|
195
|
+
"cancel": "キャンセル"
|
|
194
196
|
}
|
|
195
197
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"shape": "形状",
|
|
59
59
|
"theta": "角度",
|
|
60
60
|
"radius": "半径",
|
|
61
|
-
"filters": "筛选器"
|
|
61
|
+
"filters": "筛选器",
|
|
62
|
+
"details": "信息"
|
|
62
63
|
},
|
|
63
64
|
"aggregator": {
|
|
64
65
|
"sum": "求和",
|
|
@@ -117,7 +118,8 @@
|
|
|
117
118
|
"main": {
|
|
118
119
|
"tablist": {
|
|
119
120
|
"new": "+ 新建",
|
|
120
|
-
"
|
|
121
|
+
"auto_title": "图表 {{idx}}",
|
|
122
|
+
"chart_name": "图表名称"
|
|
121
123
|
},
|
|
122
124
|
"tabpanel": {
|
|
123
125
|
"menubar": {
|
|
@@ -190,6 +192,7 @@
|
|
|
190
192
|
"prev": "向前",
|
|
191
193
|
"next": "向后",
|
|
192
194
|
"drop_field": "拖拽字段至此",
|
|
193
|
-
"confirm": "确认"
|
|
195
|
+
"confirm": "确认",
|
|
196
|
+
"cencel": "取消"
|
|
194
197
|
}
|
|
195
198
|
}
|