@kanaries/graphic-walker 0.3.14 → 0.3.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.
- package/dist/assets/sort.worker-4299a6a0.js.map +1 -0
- package/dist/assets/viewQuery.worker-03404216.js.map +1 -1
- package/dist/components/dataTable/index.d.ts +2 -2
- package/dist/components/limitSetting.d.ts +5 -0
- package/dist/dataSource/utils.d.ts +1 -1
- package/dist/graphic-walker.es.js +21095 -20910
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +140 -135
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/interfaces.d.ts +11 -0
- package/dist/lib/inferMeta.d.ts +1 -1
- package/dist/lib/viewQuery.d.ts +2 -2
- package/dist/renderer/hooks.d.ts +2 -0
- package/dist/services.d.ts +4 -3
- package/dist/store/visualSpecStore.d.ts +430 -2
- package/dist/utils/dataPrep.d.ts +2 -2
- package/dist/utils/is-plain-object.d.ts +2 -0
- package/dist/utils/save.d.ts +2 -2
- package/dist/workers/sort.d.ts +2 -0
- package/dist/workers/sort.worker.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/dataTable/index.tsx +154 -74
- package/src/components/dataTable/pagination.tsx +1 -1
- package/src/components/limitSetting.tsx +38 -0
- package/src/dataSource/utils.ts +15 -13
- package/src/hooks/index.ts +10 -0
- package/src/interfaces.ts +16 -0
- package/src/lib/inferMeta.ts +7 -4
- package/src/lib/viewQuery.ts +2 -2
- package/src/locales/en-US.json +1 -0
- package/src/locales/ja-JP.json +1 -0
- package/src/locales/zh-CN.json +1 -0
- package/src/renderer/hooks.ts +37 -7
- package/src/renderer/index.tsx +22 -12
- package/src/renderer/pureRenderer.tsx +7 -11
- package/src/services.ts +27 -5
- package/src/store/visualSpecStore.ts +71 -7
- package/src/utils/dataPrep.ts +21 -28
- package/src/utils/is-plain-object.ts +33 -0
- package/src/utils/save.ts +2 -2
- package/src/visualSettings/index.tsx +24 -5
- package/src/workers/sort.ts +23 -0
- package/src/workers/sort.worker.ts +21 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useMemo, useState } from
|
|
2
|
-
import styled from
|
|
3
|
-
import { IMutField, IRow } from
|
|
4
|
-
import { useTranslation } from
|
|
5
|
-
import Pagination from
|
|
6
|
-
import { ChevronUpDownIcon } from
|
|
7
|
-
import DropdownContext from
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { IMutField, IRow } from '../../interfaces';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import Pagination from './pagination';
|
|
6
|
+
import { ChevronUpDownIcon } from '@heroicons/react/24/outline';
|
|
7
|
+
import DropdownContext from '../dropdownContext';
|
|
8
8
|
|
|
9
9
|
interface DataTableProps {
|
|
10
10
|
size?: number;
|
|
@@ -32,34 +32,83 @@ const Container = styled.div`
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
`;
|
|
35
|
-
const ANALYTIC_TYPE_LIST = [
|
|
36
|
-
const SEMANTIC_TYPE_LIST = [
|
|
35
|
+
const ANALYTIC_TYPE_LIST = ['dimension', 'measure'];
|
|
36
|
+
const SEMANTIC_TYPE_LIST = ['nominal', 'ordinal', 'quantitative', 'temporal'];
|
|
37
37
|
// function getCellType(field: IMutField): 'number' | 'text' {
|
|
38
38
|
// return field.dataType === 'number' || field.dataType === 'integer' ? 'number' : 'text';
|
|
39
39
|
// }
|
|
40
|
-
function getHeaderType(field: IMutField):
|
|
41
|
-
return field.analyticType ===
|
|
40
|
+
function getHeaderType(field: IMutField): 'number' | 'text' {
|
|
41
|
+
return field.analyticType === 'dimension' ? 'text' : 'number';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function getHeaderClassNames(field: IMutField) {
|
|
45
|
-
return field.analyticType ===
|
|
45
|
+
return field.analyticType === 'dimension' ? 'border-t-4 border-blue-400' : 'border-t-4 border-purple-400';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function getSemanticColors(field: IMutField): string {
|
|
49
49
|
switch (field.semanticType) {
|
|
50
|
-
case
|
|
51
|
-
return
|
|
52
|
-
case
|
|
53
|
-
return
|
|
54
|
-
case
|
|
55
|
-
return
|
|
56
|
-
case
|
|
57
|
-
return
|
|
50
|
+
case 'nominal':
|
|
51
|
+
return 'border border-transparent bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-100 dark:border-sky-600';
|
|
52
|
+
case 'ordinal':
|
|
53
|
+
return 'border border-transparent bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-100 dark:border-indigo-600';
|
|
54
|
+
case 'quantitative':
|
|
55
|
+
return 'border border-transparent bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100 dark:border-purple-600';
|
|
56
|
+
case 'temporal':
|
|
57
|
+
return 'border border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 dark:border-yellow-600';
|
|
58
58
|
default:
|
|
59
|
-
return
|
|
59
|
+
return 'border border-transparent bg-gray-400';
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
type wrapMutField = {
|
|
64
|
+
colSpan: number;
|
|
65
|
+
rowSpan: number;
|
|
66
|
+
} & (
|
|
67
|
+
| { type: 'field'; value: IMutField; fIndex: number }
|
|
68
|
+
| {
|
|
69
|
+
type: 'name';
|
|
70
|
+
value: string;
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const getHeaders = (metas: IMutField[]): wrapMutField[][] => {
|
|
75
|
+
const height = metas.map((x) => x.path?.length ?? 1).reduce((a, b) => Math.max(a, b), 0);
|
|
76
|
+
const result: wrapMutField[][] = [...Array(height)].map(() => []);
|
|
77
|
+
let now = 1;
|
|
78
|
+
metas.forEach((x, fIndex) => {
|
|
79
|
+
const path = x.path ?? [x.name ?? x.fid];
|
|
80
|
+
if (path.length > now) {
|
|
81
|
+
for (let i = now - 1; i < path.length - 1; i++) {
|
|
82
|
+
result[i].push({
|
|
83
|
+
colSpan: 0,
|
|
84
|
+
rowSpan: 1,
|
|
85
|
+
type: 'name',
|
|
86
|
+
value: path[i],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
now = path.length;
|
|
91
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
92
|
+
result[i][result[i].length - 1].colSpan++;
|
|
93
|
+
}
|
|
94
|
+
result[path.length - 1].push({
|
|
95
|
+
type: 'field',
|
|
96
|
+
value: x,
|
|
97
|
+
colSpan: 1,
|
|
98
|
+
rowSpan: height - path.length + 1,
|
|
99
|
+
fIndex,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getHeaderKey = (f: wrapMutField) => {
|
|
106
|
+
if (f.type === 'name') {
|
|
107
|
+
return f.value;
|
|
108
|
+
}
|
|
109
|
+
return f.value.name ?? f.value.fid;
|
|
110
|
+
};
|
|
111
|
+
|
|
63
112
|
const DataTable: React.FC<DataTableProps> = (props) => {
|
|
64
113
|
const { size = 10, data, metas, onMetaChange } = props;
|
|
65
114
|
const [pageIndex, setPageIndex] = useState(0);
|
|
@@ -82,6 +131,8 @@ const DataTable: React.FC<DataTableProps> = (props) => {
|
|
|
82
131
|
const from = pageIndex * size;
|
|
83
132
|
const to = Math.min((pageIndex + 1) * size, data.length - 1);
|
|
84
133
|
|
|
134
|
+
const headers = useMemo(() => getHeaders(metas), [metas]);
|
|
135
|
+
|
|
85
136
|
return (
|
|
86
137
|
<Container className="rounded border-gray-200 dark:border-gray-700 border">
|
|
87
138
|
<Pagination
|
|
@@ -97,70 +148,99 @@ const DataTable: React.FC<DataTableProps> = (props) => {
|
|
|
97
148
|
/>
|
|
98
149
|
<table className="min-w-full divide-y">
|
|
99
150
|
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
151
|
+
{headers.map((row) => (
|
|
152
|
+
<tr
|
|
153
|
+
className="divide-x divide-gray-200 dark:divide-gray-700"
|
|
154
|
+
key={`row_${getHeaderKey(row[0])}`}
|
|
155
|
+
>
|
|
156
|
+
{row.map((f) => (
|
|
157
|
+
<th
|
|
158
|
+
colSpan={f.colSpan}
|
|
159
|
+
rowSpan={f.rowSpan}
|
|
160
|
+
key={getHeaderKey(f)}
|
|
161
|
+
className={'align-top'}
|
|
108
162
|
>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
onMetaChange(field.fid, fIndex, {
|
|
115
|
-
analyticType: value as IMutField["analyticType"],
|
|
116
|
-
});
|
|
117
|
-
}}
|
|
163
|
+
{f.type === 'name' && (
|
|
164
|
+
<div
|
|
165
|
+
className={
|
|
166
|
+
'border-t-4 border-yellow-400 whitespace-nowrap py-3.5 text-left text-xs font-semibold text-gray-900 dark:text-gray-50'
|
|
167
|
+
}
|
|
118
168
|
>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</DropdownContext>
|
|
129
|
-
</div>
|
|
130
|
-
<div>
|
|
131
|
-
<DropdownContext
|
|
132
|
-
options={semanticTypeList}
|
|
133
|
-
onSelect={(value) => {
|
|
134
|
-
onMetaChange(field.fid, fIndex, {
|
|
135
|
-
semanticType: value as IMutField["semanticType"],
|
|
136
|
-
});
|
|
137
|
-
}}
|
|
169
|
+
<b className="sticky inset-x-0 w-fit px-6 sm:pl-6">{f.value}</b>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
{f.type === 'field' && (
|
|
173
|
+
<div
|
|
174
|
+
className={
|
|
175
|
+
getHeaderClassNames(f.value) +
|
|
176
|
+
' whitespace-nowrap py-3.5 px-6 text-left text-xs font-semibold text-gray-900 dark:text-gray-50 sm:pl-6'
|
|
177
|
+
}
|
|
138
178
|
>
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
179
|
+
<b>{f.value.basename || f.value.name || f.value.fid}</b>
|
|
180
|
+
<div>
|
|
181
|
+
<DropdownContext
|
|
182
|
+
options={analyticTypeList}
|
|
183
|
+
onSelect={(value) => {
|
|
184
|
+
onMetaChange(f.value.fid, f.fIndex, {
|
|
185
|
+
analyticType: value as IMutField['analyticType'],
|
|
186
|
+
});
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<span
|
|
190
|
+
className={
|
|
191
|
+
'cursor-pointer inline-flex px-2.5 py-0.5 text-xs font-medium mt-1 rounded-full text-xs text-white ' +
|
|
192
|
+
(f.value.analyticType === 'dimension'
|
|
193
|
+
? 'bg-blue-500'
|
|
194
|
+
: 'bg-purple-500')
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
{f.value.analyticType}
|
|
198
|
+
<ChevronUpDownIcon className="ml-2 w-3" />
|
|
199
|
+
</span>
|
|
200
|
+
</DropdownContext>
|
|
201
|
+
</div>
|
|
202
|
+
<div>
|
|
203
|
+
<DropdownContext
|
|
204
|
+
options={semanticTypeList}
|
|
205
|
+
onSelect={(value) => {
|
|
206
|
+
onMetaChange(f.value.fid, f.fIndex, {
|
|
207
|
+
semanticType: value as IMutField['semanticType'],
|
|
208
|
+
});
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<span
|
|
212
|
+
className={
|
|
213
|
+
'cursor-pointer inline-flex px-2.5 py-0.5 text-xs font-medium mt-1 rounded-full text-xs ' +
|
|
214
|
+
getSemanticColors(f.value)
|
|
215
|
+
}
|
|
216
|
+
>
|
|
217
|
+
{f.value.semanticType}
|
|
218
|
+
<ChevronUpDownIcon className="ml-2 w-3" />
|
|
219
|
+
</span>
|
|
220
|
+
</DropdownContext>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</th>
|
|
225
|
+
))}
|
|
226
|
+
</tr>
|
|
227
|
+
))}
|
|
154
228
|
</thead>
|
|
155
229
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-zinc-900">
|
|
156
230
|
{data.slice(from, to + 1).map((row, index) => (
|
|
157
|
-
<tr
|
|
231
|
+
<tr
|
|
232
|
+
className={
|
|
233
|
+
'divide-x divide-gray-200 dark:divide-gray-700 ' +
|
|
234
|
+
(index % 2 ? 'bg-gray-50 dark:bg-gray-900' : '')
|
|
235
|
+
}
|
|
236
|
+
key={index}
|
|
237
|
+
>
|
|
158
238
|
{metas.map((field) => (
|
|
159
239
|
<td
|
|
160
240
|
key={field.fid + index}
|
|
161
241
|
className={
|
|
162
242
|
getHeaderType(field) +
|
|
163
|
-
|
|
243
|
+
' whitespace-nowrap py-2 pl-4 pr-3 text-xs text-gray-500 dark:text-gray-300 sm:pl-6'
|
|
164
244
|
}
|
|
165
245
|
>
|
|
166
246
|
{`${row[field.fid]}`}
|
|
@@ -12,7 +12,7 @@ export default function Pagination(props: PaginationProps) {
|
|
|
12
12
|
const { t } = useTranslation();
|
|
13
13
|
return (
|
|
14
14
|
<nav
|
|
15
|
-
className="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-zinc-900 px-4 py-3 sm:px-6"
|
|
15
|
+
className="sticky inset-x-0 flex items-center justify-between border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-zinc-900 px-4 py-3 sm:px-6"
|
|
16
16
|
aria-label="Pagination"
|
|
17
17
|
>
|
|
18
18
|
<div className="hidden sm:block">
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
export default function LimitSetting(props: { value: number; setValue: (v: number) => void }) {
|
|
5
|
+
const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className=" mt-2">
|
|
9
|
+
<input
|
|
10
|
+
className="w-full h-2 bg-blue-100 appearance-none"
|
|
11
|
+
type="range"
|
|
12
|
+
name="limit"
|
|
13
|
+
value={props.value > 0 ? props.value : 0}
|
|
14
|
+
min="1"
|
|
15
|
+
max="50"
|
|
16
|
+
disabled={props.value < 0}
|
|
17
|
+
step="1"
|
|
18
|
+
onChange={(e) => {
|
|
19
|
+
const v = parseInt(e.target.value);
|
|
20
|
+
if (!isNaN(v)) {
|
|
21
|
+
props.setValue(v);
|
|
22
|
+
}
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
<output className="text-sm ml-1" htmlFor="height">
|
|
26
|
+
<input
|
|
27
|
+
type="checkbox"
|
|
28
|
+
className="h-4 w-4 mr-1 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
29
|
+
checked={props.value > 0}
|
|
30
|
+
onChange={(e) => {
|
|
31
|
+
props.setValue(e.target.checked ? 30 : -1);
|
|
32
|
+
}}
|
|
33
|
+
></input>
|
|
34
|
+
{`${t('limit')}${props.value > 0 ? `: ${props.value}` : ''}`}
|
|
35
|
+
</output>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
package/src/dataSource/utils.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { IRow, IMutField } from
|
|
2
|
-
import { inferMeta } from
|
|
3
|
-
import { guardDataKeys } from
|
|
4
|
-
|
|
1
|
+
import { IRow, IMutField } from '../interfaces';
|
|
2
|
+
import { inferMeta } from '../lib/inferMeta';
|
|
3
|
+
import { flatKeys, guardDataKeys } from '../utils/dataPrep';
|
|
5
4
|
|
|
6
5
|
export function transData(dataSource: IRow[]): {
|
|
7
6
|
dataSource: IRow[];
|
|
@@ -17,31 +16,34 @@ export function transData(dataSource: IRow[]): {
|
|
|
17
16
|
// const rawKeys = Object.keys(sampleRecord);
|
|
18
17
|
// let flatColKeys: string[] = flatNestKeys(sampleRecord);
|
|
19
18
|
|
|
20
|
-
const keys =
|
|
19
|
+
const keys = flatKeys(sampleRecord);
|
|
21
20
|
const metas = inferMeta({
|
|
22
21
|
dataSource,
|
|
23
22
|
fields: keys.map((k) => ({
|
|
24
|
-
fid: k,
|
|
25
|
-
key: k,
|
|
26
|
-
analyticType:
|
|
27
|
-
semanticType:
|
|
23
|
+
fid: k[k.length - 1],
|
|
24
|
+
key: k[k.length - 1],
|
|
25
|
+
analyticType: '?',
|
|
26
|
+
semanticType: '?',
|
|
27
|
+
path: k,
|
|
28
|
+
basename: k[k.length - 1],
|
|
29
|
+
name: k.join('.'),
|
|
28
30
|
})),
|
|
29
|
-
})
|
|
31
|
+
});
|
|
30
32
|
const { safeData, safeMetas } = guardDataKeys(dataSource, metas);
|
|
31
33
|
const finalData: IRow[] = [];
|
|
32
34
|
for (let record of safeData) {
|
|
33
35
|
const newRecord: IRow = {};
|
|
34
36
|
for (let field of safeMetas) {
|
|
35
|
-
if (field.semanticType ===
|
|
37
|
+
if (field.semanticType === 'quantitative') {
|
|
36
38
|
newRecord[field.fid] = Number(record[field.fid]);
|
|
37
39
|
} else {
|
|
38
|
-
newRecord[field.fid] = record[field.fid]
|
|
40
|
+
newRecord[field.fid] = record[field.fid]; //getValueByKeyPath(record, field.fid);// record[field.fid];
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
finalData.push(newRecord);
|
|
42
44
|
}
|
|
43
45
|
return {
|
|
44
46
|
dataSource: finalData,
|
|
45
|
-
fields: safeMetas
|
|
47
|
+
fields: safeMetas,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebounceValue<T>(value: T, timeout = 200): T {
|
|
4
|
+
const [innerValue, setInnerValue] = useState(value);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const handler = setTimeout(() => setInnerValue(value), timeout);
|
|
7
|
+
return () => clearTimeout(handler);
|
|
8
|
+
}, [value]);
|
|
9
|
+
return innerValue;
|
|
10
|
+
}
|
package/src/interfaces.ts
CHANGED
|
@@ -34,18 +34,22 @@ export interface IMutField {
|
|
|
34
34
|
fid: string;
|
|
35
35
|
key?: string;
|
|
36
36
|
name?: string;
|
|
37
|
+
basename?: string;
|
|
37
38
|
disable?: boolean;
|
|
38
39
|
semanticType: ISemanticType;
|
|
39
40
|
analyticType: IAnalyticType;
|
|
41
|
+
path?: string[];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface IUncertainMutField {
|
|
43
45
|
fid: string;
|
|
44
46
|
key?: string;
|
|
45
47
|
name?: string;
|
|
48
|
+
basename?: string;
|
|
46
49
|
disable?: boolean;
|
|
47
50
|
semanticType: ISemanticType | '?';
|
|
48
51
|
analyticType: IAnalyticType | '?';
|
|
52
|
+
path: string[];
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export type IExpParamter =
|
|
@@ -90,6 +94,8 @@ export interface IField {
|
|
|
90
94
|
cmp?: (a: any, b: any) => number;
|
|
91
95
|
computed?: boolean;
|
|
92
96
|
expression?: IExpression;
|
|
97
|
+
basename?: string;
|
|
98
|
+
path?: [],
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
export interface IViewField extends IField {
|
|
@@ -215,6 +221,16 @@ export interface IVisSpec {
|
|
|
215
221
|
readonly config: DeepReadonly<IVisualConfig>;
|
|
216
222
|
}
|
|
217
223
|
|
|
224
|
+
export type SetToArray<T> = (
|
|
225
|
+
T extends object ? (
|
|
226
|
+
T extends Set<infer U> ? Array<U> : { [K in keyof T]: SetToArray<T[K]> }
|
|
227
|
+
) : T
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
export type IVisSpecForExport = SetToArray<IVisSpec>;
|
|
231
|
+
|
|
232
|
+
export type IFilterFieldForExport = SetToArray<IFilterField>;
|
|
233
|
+
|
|
218
234
|
export enum ISegmentKey {
|
|
219
235
|
vis = 'vis',
|
|
220
236
|
data = 'data',
|
package/src/lib/inferMeta.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IAnalyticType, IMutField, IRow, ISemanticType, IUncertainMutField } from '../interfaces';
|
|
2
|
+
import { getValueByKeyPath } from '../utils/dataPrep';
|
|
2
3
|
|
|
3
4
|
const COMMON_TIME_FORMAT: RegExp[] = [
|
|
4
5
|
/^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
|
|
@@ -55,12 +56,12 @@ function inferAnalyticTypeFromSemanticType(semanticType: ISemanticType): IAnalyt
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
export function inferSemanticType(data: IRow[],
|
|
59
|
-
const values = data.map((row) => row
|
|
59
|
+
export function inferSemanticType(data: IRow[], path: string[]): ISemanticType {
|
|
60
|
+
const values = data.map((row) => getValueByKeyPath(row, path));
|
|
60
61
|
|
|
61
62
|
let st: ISemanticType = isNumericArray(values) ? 'quantitative' : 'nominal';
|
|
62
63
|
if (st === 'nominal') {
|
|
63
|
-
if (isDateTimeArray(data.map((row) => `${row
|
|
64
|
+
if (isDateTimeArray(data.map((row) => `${getValueByKeyPath(row, path)}`))) st = 'temporal';
|
|
64
65
|
}
|
|
65
66
|
return st;
|
|
66
67
|
}
|
|
@@ -70,7 +71,7 @@ export function inferMeta(props: { dataSource: IRow[]; fields: IUncertainMutFiel
|
|
|
70
71
|
const finalFieldMetas: IMutField[] = [];
|
|
71
72
|
for (let field of fields) {
|
|
72
73
|
let semanticType: ISemanticType =
|
|
73
|
-
field.semanticType === '?' ? inferSemanticType(dataSource, field.
|
|
74
|
+
field.semanticType === '?' ? inferSemanticType(dataSource, field.path) : field.semanticType;
|
|
74
75
|
let analyticType: IAnalyticType = inferAnalyticTypeFromSemanticType(semanticType);
|
|
75
76
|
|
|
76
77
|
finalFieldMetas.push({
|
|
@@ -79,6 +80,8 @@ export function inferMeta(props: { dataSource: IRow[]; fields: IUncertainMutFiel
|
|
|
79
80
|
name: field.name ? field.name : field.fid,
|
|
80
81
|
analyticType,
|
|
81
82
|
semanticType,
|
|
83
|
+
basename: field.basename || field.name || field.fid,
|
|
84
|
+
path: field.path,
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
return finalFieldMetas;
|
package/src/lib/viewQuery.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IField, IRow } from "../interfaces";
|
|
2
2
|
import { aggregate } from "./op/aggregate";
|
|
3
3
|
import { fold } from "./op/fold";
|
|
4
4
|
import { IAggQuery, IBinQuery, IFoldQuery, IRawQuery } from "./interfaces";
|
|
@@ -6,7 +6,7 @@ import { bin } from "./op/bin";
|
|
|
6
6
|
|
|
7
7
|
export type IViewQuery = IAggQuery | IFoldQuery | IBinQuery | IRawQuery;
|
|
8
8
|
|
|
9
|
-
export function queryView (rawData: IRow[], metas:
|
|
9
|
+
export function queryView (rawData: IRow[], metas: IField[], query: IViewQuery) {
|
|
10
10
|
switch (query.op) {
|
|
11
11
|
case 'aggregate':
|
|
12
12
|
return aggregate(rawData, query);
|
package/src/locales/en-US.json
CHANGED
package/src/locales/ja-JP.json
CHANGED
package/src/locales/zh-CN.json
CHANGED
package/src/renderer/hooks.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { unstable_batchedUpdates } from 'react-dom';
|
|
3
3
|
import type { DeepReadonly, IFilterField, IRow, IViewField } from '../interfaces';
|
|
4
|
-
import { applyFilter, applyViewQuery, transformDataService } from '../services';
|
|
4
|
+
import { applyFilter, applySort, applyViewQuery, transformDataService } from '../services';
|
|
5
5
|
import { getMeaAggKey } from '../utils';
|
|
6
6
|
import { useAppRootContext } from '../components/appRoot';
|
|
7
|
-
|
|
7
|
+
import { useDebounceValue } from '../hooks';
|
|
8
8
|
|
|
9
9
|
interface UseRendererProps {
|
|
10
10
|
data: IRow[];
|
|
@@ -13,6 +13,8 @@ interface UseRendererProps {
|
|
|
13
13
|
viewMeasures: IViewField[];
|
|
14
14
|
filters: readonly DeepReadonly<IFilterField>[];
|
|
15
15
|
defaultAggregated: boolean;
|
|
16
|
+
sort: 'none' | 'ascending' | 'descending';
|
|
17
|
+
limit: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
interface UseRendererResult {
|
|
@@ -21,10 +23,21 @@ interface UseRendererResult {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
24
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
data,
|
|
28
|
+
allFields,
|
|
29
|
+
viewDimensions,
|
|
30
|
+
viewMeasures,
|
|
31
|
+
filters,
|
|
32
|
+
defaultAggregated,
|
|
33
|
+
sort,
|
|
34
|
+
limit: storeLimit,
|
|
35
|
+
} = props;
|
|
25
36
|
const [computing, setComputing] = useState(false);
|
|
26
37
|
const taskIdRef = useRef(0);
|
|
27
38
|
|
|
39
|
+
const limit = useDebounceValue(storeLimit);
|
|
40
|
+
|
|
28
41
|
const [viewData, setViewData] = useState<IRow[]>([]);
|
|
29
42
|
|
|
30
43
|
const appRef = useAppRootContext();
|
|
@@ -50,10 +63,26 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
50
63
|
return applyViewQuery(d, dims.concat(meas), {
|
|
51
64
|
op: defaultAggregated ? 'aggregate' : 'raw',
|
|
52
65
|
groupBy: dims.map((f) => f.fid),
|
|
53
|
-
measures: meas.map((f) => ({
|
|
66
|
+
measures: meas.map((f) => ({
|
|
67
|
+
field: f.fid,
|
|
68
|
+
agg: f.aggName as any,
|
|
69
|
+
asFieldKey: getMeaAggKey(f.fid, f.aggName!),
|
|
70
|
+
})),
|
|
54
71
|
});
|
|
55
72
|
})
|
|
56
|
-
.then(data => {
|
|
73
|
+
.then((data) => {
|
|
74
|
+
if (limit > 0 && sort !== 'none' && viewMeasures.length > 0) {
|
|
75
|
+
return applySort(data, viewMeasures, sort);
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
})
|
|
79
|
+
.then((data) => {
|
|
80
|
+
if (limit > 0) {
|
|
81
|
+
return data.slice(0, limit);
|
|
82
|
+
}
|
|
83
|
+
return data;
|
|
84
|
+
})
|
|
85
|
+
.then((data) => {
|
|
57
86
|
if (taskId !== taskIdRef.current) {
|
|
58
87
|
return;
|
|
59
88
|
}
|
|
@@ -62,7 +91,8 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
62
91
|
setComputing(false);
|
|
63
92
|
setViewData(data);
|
|
64
93
|
});
|
|
65
|
-
})
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
66
96
|
if (taskId !== taskIdRef.current) {
|
|
67
97
|
return;
|
|
68
98
|
}
|
|
@@ -76,7 +106,7 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
|
|
|
76
106
|
return () => {
|
|
77
107
|
taskIdRef.current++;
|
|
78
108
|
};
|
|
79
|
-
}, [data, filters, viewDimensions, viewMeasures, defaultAggregated]);
|
|
109
|
+
}, [data, filters, viewDimensions, viewMeasures, defaultAggregated, limit]);
|
|
80
110
|
|
|
81
111
|
return useMemo(() => {
|
|
82
112
|
return {
|