@kanaries/graphic-walker 0.3.13 → 0.3.15
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/viewQuery.worker-03404216.js.map +1 -1
- package/dist/components/dataTable/index.d.ts +2 -2
- package/dist/components/timeoutImg.d.ts +5 -0
- package/dist/dataSource/utils.d.ts +1 -1
- package/dist/graphic-walker.es.js +24450 -24270
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +142 -136
- package/dist/graphic-walker.umd.js.map +1 -1
- 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/services.d.ts +3 -3
- package/dist/store/index.d.ts +3 -8
- package/dist/store/visualSpecStore.d.ts +428 -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/package.json +1 -1
- package/src/assets/kanaries.png +0 -0
- package/src/components/dataTable/index.tsx +154 -74
- package/src/components/dataTable/pagination.tsx +1 -1
- package/src/components/timeoutImg.tsx +29 -0
- package/src/components/toolbar/components.tsx +1 -0
- package/src/dataSource/utils.ts +15 -13
- package/src/fields/filterField/slider.tsx +1 -1
- package/src/fields/filterField/tabs.tsx +67 -18
- package/src/interfaces.ts +16 -0
- package/src/lib/inferMeta.ts +7 -4
- package/src/lib/viewQuery.ts +2 -2
- package/src/services.ts +3 -3
- package/src/store/index.tsx +46 -45
- package/src/store/visualSpecStore.ts +57 -5
- 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 +21 -27
- package/src/assets/kanaries-logo.svg +0 -1
|
@@ -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,29 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export const ImageWithFallback = (
|
|
4
|
+
props: React.ImgHTMLAttributes<HTMLImageElement> & { fallbackSrc: string; timeout: number }
|
|
5
|
+
) => {
|
|
6
|
+
const { src, fallbackSrc, timeout, ...rest } = props;
|
|
7
|
+
const [failed, setFailed] = useState(false);
|
|
8
|
+
const imgLoadedOnInitSrc = useRef(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const timer = setTimeout(() => {
|
|
12
|
+
if (!imgLoadedOnInitSrc.current) setFailed(true);
|
|
13
|
+
}, timeout);
|
|
14
|
+
return () => clearTimeout(timer);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<img
|
|
19
|
+
{...rest}
|
|
20
|
+
src={failed ? src : fallbackSrc}
|
|
21
|
+
onError={() => {
|
|
22
|
+
setFailed(true);
|
|
23
|
+
}}
|
|
24
|
+
onLoad={() => {
|
|
25
|
+
imgLoadedOnInitSrc.current = true;
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -74,6 +74,7 @@ export const ToolbarItemContainerElement = styled.div<{ split: boolean; dark: bo
|
|
|
74
74
|
outline: none;
|
|
75
75
|
width: ${({ split }) => split ? 'calc(var(--height) + 10px)' : 'var(--height)'};
|
|
76
76
|
height: var(--height);
|
|
77
|
+
align-items: center;
|
|
77
78
|
overflow: hidden;
|
|
78
79
|
color: ${({ dark }) => dark ? 'var(--dark-mode-color)' : 'var(--color)'};
|
|
79
80
|
position: relative;
|
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
|
}
|
|
@@ -100,7 +100,7 @@ const ValueInput: React.FC<ValueInputProps> = props => {
|
|
|
100
100
|
type="number"
|
|
101
101
|
min={min}
|
|
102
102
|
max={max}
|
|
103
|
-
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
103
|
+
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 dark:bg-zinc-900 dark:border-gray-700 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
104
104
|
value={value}
|
|
105
105
|
onChange={(e) => handleSubmitValue(Number(e.target.value))}
|
|
106
106
|
/>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React, { useMemo, useRef } from 'react';
|
|
2
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
|
|
6
6
|
import type { IFilterField, IFilterRule } from '../../interfaces';
|
|
7
7
|
import { useGlobalStore } from '../../store';
|
|
8
8
|
import Slider from './slider';
|
|
9
|
+
import {
|
|
10
|
+
ChevronDownIcon,
|
|
11
|
+
ChevronUpIcon,
|
|
12
|
+
} from '@heroicons/react/24/outline';
|
|
9
13
|
|
|
10
14
|
export type RuleFormProps = {
|
|
11
15
|
field: IFilterField;
|
|
@@ -95,7 +99,7 @@ const TabPanel = styled.div``;
|
|
|
95
99
|
|
|
96
100
|
const TabItem = styled.div``;
|
|
97
101
|
|
|
98
|
-
const StatusCheckbox: React.FC<{currentNum: number; totalNum: number; onChange: () => void}> = props => {
|
|
102
|
+
const StatusCheckbox: React.FC<{ currentNum: number; totalNum: number; onChange: () => void }> = props => {
|
|
99
103
|
const { currentNum, totalNum, onChange } = props;
|
|
100
104
|
const checkboxRef = useRef(null);
|
|
101
105
|
|
|
@@ -131,16 +135,42 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
131
135
|
const { commonStore } = useGlobalStore();
|
|
132
136
|
const { currentDataset: { dataSource } } = commonStore;
|
|
133
137
|
|
|
138
|
+
interface SortConfig {
|
|
139
|
+
key: 'value' | 'count';
|
|
140
|
+
ascending: boolean;
|
|
141
|
+
}
|
|
142
|
+
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
|
143
|
+
key: "count",
|
|
144
|
+
ascending: true
|
|
145
|
+
});
|
|
146
|
+
|
|
134
147
|
const count = React.useMemo(() => {
|
|
135
148
|
return dataSource.reduce<Map<string | number, number>>((tmp, d) => {
|
|
136
149
|
const val = d[field.fid];
|
|
137
150
|
|
|
138
151
|
tmp.set(val, (tmp.get(val) ?? 0) + 1);
|
|
139
|
-
|
|
152
|
+
|
|
140
153
|
return tmp;
|
|
141
154
|
}, new Map<string | number, number>());
|
|
142
155
|
}, [dataSource, field]);
|
|
143
156
|
|
|
157
|
+
const sortedList = useMemo(() => {
|
|
158
|
+
const entries = Array.from(count.entries());
|
|
159
|
+
const compare = (a: [string | number, number], b: [string | number, number]) => {
|
|
160
|
+
if (sortConfig.key === 'count') {
|
|
161
|
+
return a[1] - b[1];
|
|
162
|
+
} else {
|
|
163
|
+
if (typeof a[0] === 'number' && typeof b[0] === 'number') {
|
|
164
|
+
return a[0] - b[0];
|
|
165
|
+
} else {
|
|
166
|
+
return String(a[0]).localeCompare(String(b[0]))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
entries.sort(sortConfig.ascending ? compare : (a, b) => -compare(a, b));
|
|
171
|
+
return entries;
|
|
172
|
+
}, [count, sortConfig]);
|
|
173
|
+
|
|
144
174
|
const { t } = useTranslation('translation');
|
|
145
175
|
|
|
146
176
|
React.useEffect(() => {
|
|
@@ -172,7 +202,7 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
172
202
|
value: new Set<number | string>(
|
|
173
203
|
[...count.keys()].filter(key => !curSet.has(key))
|
|
174
204
|
),
|
|
175
|
-
});
|
|
205
|
+
});
|
|
176
206
|
}
|
|
177
207
|
const handleSelectValue = (value, checked) => {
|
|
178
208
|
if (!field.rule || field.rule?.type !== 'one of') return;
|
|
@@ -187,21 +217,37 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
187
217
|
}
|
|
188
218
|
onChange(rule);
|
|
189
219
|
}
|
|
190
|
-
|
|
220
|
+
|
|
191
221
|
const selectedValueSum = useMemo(() => {
|
|
192
222
|
if (!field.rule) return 0;
|
|
193
223
|
return [...field.rule.value].reduce<number>((sum, key) => {
|
|
194
224
|
const s = dataSource.filter(which => which[field.fid] === key).length;
|
|
195
225
|
return sum + s;
|
|
196
226
|
}, 0)
|
|
197
|
-
}, [field.rule?.value])
|
|
227
|
+
}, [field.rule?.value]);
|
|
228
|
+
|
|
229
|
+
const SortButton: React.FC<{ currentKey: SortConfig["key"] }> = ({ currentKey }) => {
|
|
230
|
+
const isCurrentKey = sortConfig.key === currentKey;
|
|
231
|
+
return (
|
|
232
|
+
<span
|
|
233
|
+
className={`ml-2 flex-none rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer ${isCurrentKey ? "text-indigo-600" : "text-gray-500"}`}
|
|
234
|
+
onClick={() => setSortConfig({ key: currentKey, ascending: (isCurrentKey ? !sortConfig.ascending : true) })}
|
|
235
|
+
>
|
|
236
|
+
{isCurrentKey && !sortConfig.ascending
|
|
237
|
+
? <ChevronDownIcon className="h-4 w-4" />
|
|
238
|
+
: <ChevronUpIcon className="h-4 w-4" />
|
|
239
|
+
}
|
|
240
|
+
</span>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
198
243
|
|
|
199
244
|
return field.rule?.type === 'one of' ? (
|
|
200
245
|
<Container>
|
|
201
246
|
<div>{t('constant.filter_type.one_of')}</div>
|
|
202
|
-
<div className="text-gray-500">{t('constant.filter_type.one_of_desc')}</div>
|
|
247
|
+
<div className="text-gray-500 dark:text-gray-300">{t('constant.filter_type.one_of_desc')}</div>
|
|
203
248
|
<div className="btn-grp">
|
|
204
249
|
<Button
|
|
250
|
+
className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
205
251
|
onClick={() => handleToggleFullOrEmptySet()}
|
|
206
252
|
>
|
|
207
253
|
{
|
|
@@ -211,12 +257,13 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
211
257
|
}
|
|
212
258
|
</Button>
|
|
213
259
|
<Button
|
|
260
|
+
className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
214
261
|
onClick={() => handleToggleReverseSet()}
|
|
215
262
|
>
|
|
216
263
|
{t('filters.btn.reverse')}
|
|
217
264
|
</Button>
|
|
218
265
|
</div>
|
|
219
|
-
<Table className="bg-slate-50">
|
|
266
|
+
<Table className="bg-slate-50 dark:bg-gray-800">
|
|
220
267
|
<div className="flex justify-center items-center">
|
|
221
268
|
<StatusCheckbox
|
|
222
269
|
currentNum={field.rule.value.size}
|
|
@@ -224,17 +271,19 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
224
271
|
onChange={handleToggleFullOrEmptySet}
|
|
225
272
|
/>
|
|
226
273
|
</div>
|
|
227
|
-
<label className="header text-gray-500">
|
|
274
|
+
<label className="header text-gray-500 dark:text-gray-300 flex items-center">
|
|
228
275
|
{t('filters.header.value')}
|
|
276
|
+
<SortButton currentKey="value" />
|
|
229
277
|
</label>
|
|
230
|
-
<label className="header text-gray-500">
|
|
278
|
+
<label className="header text-gray-500 dark:text-gray-300 flex items-center">
|
|
231
279
|
{t('filters.header.count')}
|
|
280
|
+
<SortButton currentKey="count" />
|
|
232
281
|
</label>
|
|
233
282
|
</Table>
|
|
234
283
|
{/* <hr /> */}
|
|
235
284
|
<Table>
|
|
236
285
|
{
|
|
237
|
-
|
|
286
|
+
sortedList.map(([value, count], idx) => {
|
|
238
287
|
const id = `rule_checkbox_${idx}`;
|
|
239
288
|
|
|
240
289
|
return (
|
|
@@ -299,8 +348,8 @@ const CalendarInput: React.FC<CalendarInputProps> = props => {
|
|
|
299
348
|
}
|
|
300
349
|
return (
|
|
301
350
|
<input
|
|
302
|
-
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
303
|
-
type="datetime-local"
|
|
351
|
+
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 dark:bg-zinc-900 dark:border-gray-700 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
352
|
+
type="datetime-local"
|
|
304
353
|
min={dateStringFormatter(min)}
|
|
305
354
|
max={dateStringFormatter(max)}
|
|
306
355
|
defaultValue={dateStringFormatter(value)}
|
|
@@ -326,7 +375,7 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
326
375
|
|
|
327
376
|
list.push(time);
|
|
328
377
|
} catch (error) {
|
|
329
|
-
|
|
378
|
+
|
|
330
379
|
}
|
|
331
380
|
return list;
|
|
332
381
|
}, []).sort((a, b) => a - b);
|
|
@@ -463,7 +512,7 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
463
512
|
React.useEffect(() => {
|
|
464
513
|
if (!tabs.includes(which)) setWhich(tabs[0]);
|
|
465
514
|
}, [tabs])
|
|
466
|
-
|
|
515
|
+
|
|
467
516
|
return (
|
|
468
517
|
<TabsContainer>
|
|
469
518
|
<div>
|
|
@@ -472,13 +521,13 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
472
521
|
return (
|
|
473
522
|
<div className="flex my-2" key={option}>
|
|
474
523
|
<div className="align-top">
|
|
475
|
-
<input
|
|
524
|
+
<input
|
|
476
525
|
type="radio"
|
|
477
526
|
className="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
478
527
|
id={option}
|
|
479
528
|
checked={option === which}
|
|
480
529
|
onChange={e => setWhich((e.target as HTMLInputElement).value as typeof which)}
|
|
481
|
-
name="filter_type"
|
|
530
|
+
name="filter_type"
|
|
482
531
|
value={option}
|
|
483
532
|
/>
|
|
484
533
|
</div>
|
|
@@ -495,7 +544,7 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
495
544
|
})
|
|
496
545
|
}
|
|
497
546
|
</div>
|
|
498
|
-
<hr className="my-0.5"/>
|
|
547
|
+
<hr className="my-0.5" />
|
|
499
548
|
<TabPanel>
|
|
500
549
|
{
|
|
501
550
|
tabs.map((tab, i) => {
|
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',
|