@kanaries/graphic-walker 0.3.12 → 0.3.14
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/transform.worker-a12fb3d8.js.map +1 -0
- package/dist/components/timeoutImg.d.ts +5 -0
- package/dist/fields/filterField/slider.d.ts +0 -1
- package/dist/graphic-walker.es.js +15327 -15103
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +243 -110
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/store/index.d.ts +3 -8
- package/package.json +2 -1
- package/src/assets/kanaries.png +0 -0
- package/src/components/modal.tsx +9 -9
- package/src/components/timeoutImg.tsx +29 -0
- package/src/components/toolbar/components.tsx +1 -0
- package/src/fields/filterField/filterEditDialog.tsx +33 -8
- package/src/fields/filterField/slider.tsx +127 -85
- package/src/fields/filterField/tabs.tsx +352 -186
- package/src/lib/execExp.ts +1 -1
- package/src/locales/en-US.json +10 -3
- package/src/locales/ja-JP.json +10 -3
- package/src/locales/zh-CN.json +10 -3
- package/src/store/index.tsx +46 -45
- package/src/visualSettings/index.tsx +296 -70
- package/dist/assets/transform.worker-90e4f506.js.map +0 -1
|
@@ -1,107 +1,131 @@
|
|
|
1
1
|
import { observer } from 'mobx-react-lite';
|
|
2
|
-
import React 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
|
-
import PureTabs from '../../components/tabs/defaultTab';
|
|
9
8
|
import Slider from './slider';
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
ChevronDownIcon,
|
|
11
|
+
ChevronUpIcon,
|
|
12
|
+
} from '@heroicons/react/24/outline';
|
|
11
13
|
|
|
12
14
|
export type RuleFormProps = {
|
|
13
15
|
field: IFilterField;
|
|
14
16
|
onChange: (rule: IFilterRule) => void;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
const Container = styled.div
|
|
18
|
-
|
|
19
|
-
marginInline: '2em',
|
|
19
|
+
const Container = styled.div`
|
|
20
|
+
margin-block: 1em;
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
display:
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
> .btn-grp {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: row;
|
|
25
|
+
margin-block: 1em;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
> * {
|
|
28
|
+
margin-inline-start: 0.6em;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
&:first-child: {
|
|
31
|
+
margin-inline-start: 0;
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export const Button = styled.button
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
color:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const Button = styled.button`
|
|
38
|
+
:hover: {
|
|
39
|
+
background-color: rgba(243, 244, 246, 0.5);
|
|
40
|
+
};
|
|
41
|
+
color: rgb(55, 65, 81);
|
|
42
|
+
border: 1px solid rgb(226 232 240);
|
|
43
|
+
border-radius: 0.5em;
|
|
44
|
+
padding-block: 0.4em;
|
|
45
|
+
padding-inline: 1em;
|
|
46
|
+
user-select: none;
|
|
47
|
+
font-weight: bold;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const Table = styled.div`
|
|
52
|
+
display: grid;
|
|
53
|
+
grid-template-columns: 4em auto max-content;
|
|
54
|
+
max-height: 30vh;
|
|
55
|
+
overflow-y: scroll;
|
|
56
|
+
|
|
57
|
+
& > * {
|
|
58
|
+
padding-block: 0.6em;
|
|
59
|
+
padding-inline: 0.2em;
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
text-overflow: ellipsis;
|
|
63
|
+
user-select: none;
|
|
64
|
+
border-bottom: 0.8px solid rgb(226 232 240);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
& > input,
|
|
68
|
+
& > *[for] {
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const TabsContainer = styled.div`
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
align-items: stretch;
|
|
77
|
+
justify-content: stretch;
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const CalendarInputContainer = styled.div`
|
|
81
|
+
display: flex;
|
|
82
|
+
padding-block: 1em;
|
|
83
|
+
width: 100%;
|
|
84
|
+
|
|
85
|
+
> .calendar-input {
|
|
86
|
+
width: 100%;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
> .calendar-input:first-child {
|
|
90
|
+
margin-right: 0.5em;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
> .calendar-input:last-child {
|
|
94
|
+
margin-left: 0.5em;
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const TabPanel = styled.div``;
|
|
99
|
+
|
|
100
|
+
const TabItem = styled.div``;
|
|
101
|
+
|
|
102
|
+
const StatusCheckbox: React.FC<{ currentNum: number; totalNum: number; onChange: () => void }> = props => {
|
|
103
|
+
const { currentNum, totalNum, onChange } = props;
|
|
104
|
+
const checkboxRef = useRef(null);
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const TabHeader = styled.label({
|
|
82
|
-
outline: 'none',
|
|
83
|
-
userSelect: 'none',
|
|
84
|
-
paddingBlock: '0.4em',
|
|
85
|
-
paddingInline: '1em 2em',
|
|
86
|
-
borderWidth: '1px',
|
|
87
|
-
borderRadius: '4px 4px 0 0',
|
|
88
|
-
position: 'relative',
|
|
89
|
-
|
|
90
|
-
'&[aria-selected]': {
|
|
91
|
-
borderBottomColor: '#0000',
|
|
92
|
-
zIndex: 15,
|
|
93
|
-
},
|
|
94
|
-
'&[aria-selected=false]': {
|
|
95
|
-
backgroundColor: '#f8f8f8',
|
|
96
|
-
borderBottomColor: '#e2e2e2',
|
|
97
|
-
cursor: 'pointer',
|
|
98
|
-
zIndex: 14,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const TabPanel = styled.div({});
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
if (!checkboxRef.current) return;
|
|
108
|
+
const checkboxRefDOM = (checkboxRef.current as HTMLInputElement)
|
|
109
|
+
if (currentNum === totalNum) {
|
|
110
|
+
checkboxRefDOM.checked = true;
|
|
111
|
+
checkboxRefDOM.indeterminate = false;
|
|
112
|
+
} else if (currentNum < totalNum && currentNum > 0) {
|
|
113
|
+
checkboxRefDOM.indeterminate = true;
|
|
114
|
+
} else if (currentNum === 0) {
|
|
115
|
+
checkboxRefDOM.checked = false;
|
|
116
|
+
checkboxRefDOM.indeterminate = false;
|
|
117
|
+
}
|
|
118
|
+
}, [currentNum, totalNum])
|
|
103
119
|
|
|
104
|
-
|
|
120
|
+
return (
|
|
121
|
+
<input
|
|
122
|
+
type="checkbox"
|
|
123
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
124
|
+
ref={checkboxRef}
|
|
125
|
+
onChange={() => onChange()}
|
|
126
|
+
/>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
105
129
|
|
|
106
130
|
export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
107
131
|
active,
|
|
@@ -111,17 +135,43 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
111
135
|
const { commonStore } = useGlobalStore();
|
|
112
136
|
const { currentDataset: { dataSource } } = commonStore;
|
|
113
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
|
+
|
|
114
147
|
const count = React.useMemo(() => {
|
|
115
148
|
return dataSource.reduce<Map<string | number, number>>((tmp, d) => {
|
|
116
149
|
const val = d[field.fid];
|
|
117
150
|
|
|
118
151
|
tmp.set(val, (tmp.get(val) ?? 0) + 1);
|
|
119
|
-
|
|
152
|
+
|
|
120
153
|
return tmp;
|
|
121
154
|
}, new Map<string | number, number>());
|
|
122
155
|
}, [dataSource, field]);
|
|
123
156
|
|
|
124
|
-
const
|
|
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
|
+
|
|
174
|
+
const { t } = useTranslation('translation');
|
|
125
175
|
|
|
126
176
|
React.useEffect(() => {
|
|
127
177
|
if (active && field.rule?.type !== 'one of') {
|
|
@@ -132,51 +182,123 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
132
182
|
}
|
|
133
183
|
}, [active, onChange, field, count]);
|
|
134
184
|
|
|
185
|
+
const handleToggleFullOrEmptySet = () => {
|
|
186
|
+
if (!field.rule || field.rule.type !== 'one of') return;
|
|
187
|
+
const curSet = field.rule.value;
|
|
188
|
+
onChange({
|
|
189
|
+
type: 'one of',
|
|
190
|
+
value: new Set<number | string>(
|
|
191
|
+
curSet.size === count.size
|
|
192
|
+
? []
|
|
193
|
+
: count.keys()
|
|
194
|
+
),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const handleToggleReverseSet = () => {
|
|
198
|
+
if (!field.rule || field.rule.type !== 'one of') return;
|
|
199
|
+
const curSet = field.rule.value;
|
|
200
|
+
onChange({
|
|
201
|
+
type: 'one of',
|
|
202
|
+
value: new Set<number | string>(
|
|
203
|
+
[...count.keys()].filter(key => !curSet.has(key))
|
|
204
|
+
),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const handleSelectValue = (value, checked) => {
|
|
208
|
+
if (!field.rule || field.rule?.type !== 'one of') return;
|
|
209
|
+
const rule: IFilterRule = {
|
|
210
|
+
type: 'one of',
|
|
211
|
+
value: new Set(field.rule.value)
|
|
212
|
+
};
|
|
213
|
+
if (checked) {
|
|
214
|
+
rule.value.add(value);
|
|
215
|
+
} else {
|
|
216
|
+
rule.value.delete(value);
|
|
217
|
+
}
|
|
218
|
+
onChange(rule);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const selectedValueSum = useMemo(() => {
|
|
222
|
+
if (!field.rule) return 0;
|
|
223
|
+
return [...field.rule.value].reduce<number>((sum, key) => {
|
|
224
|
+
const s = dataSource.filter(which => which[field.fid] === key).length;
|
|
225
|
+
return sum + s;
|
|
226
|
+
}, 0)
|
|
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
|
+
}
|
|
243
|
+
|
|
135
244
|
return field.rule?.type === 'one of' ? (
|
|
136
245
|
<Container>
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
{
|
|
246
|
+
<div>{t('constant.filter_type.one_of')}</div>
|
|
247
|
+
<div className="text-gray-500 dark:text-gray-300">{t('constant.filter_type.one_of_desc')}</div>
|
|
248
|
+
<div className="btn-grp">
|
|
249
|
+
<Button
|
|
250
|
+
className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
251
|
+
onClick={() => handleToggleFullOrEmptySet()}
|
|
252
|
+
>
|
|
253
|
+
{
|
|
254
|
+
field.rule.value.size === count.size
|
|
255
|
+
? t('filters.btn.unselect_all')
|
|
256
|
+
: t('filters.btn.select_all')
|
|
257
|
+
}
|
|
258
|
+
</Button>
|
|
259
|
+
<Button
|
|
260
|
+
className="dark:bg-zinc-900 dark:text-gray-200 dark:hover:bg-gray-800"
|
|
261
|
+
onClick={() => handleToggleReverseSet()}
|
|
262
|
+
>
|
|
263
|
+
{t('filters.btn.reverse')}
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
<Table className="bg-slate-50 dark:bg-gray-800">
|
|
267
|
+
<div className="flex justify-center items-center">
|
|
268
|
+
<StatusCheckbox
|
|
269
|
+
currentNum={field.rule.value.size}
|
|
270
|
+
totalNum={count.size}
|
|
271
|
+
onChange={handleToggleFullOrEmptySet}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
<label className="header text-gray-500 dark:text-gray-300 flex items-center">
|
|
275
|
+
{t('filters.header.value')}
|
|
276
|
+
<SortButton currentKey="value" />
|
|
143
277
|
</label>
|
|
144
|
-
<label className="header">
|
|
145
|
-
{t('header.count')}
|
|
278
|
+
<label className="header text-gray-500 dark:text-gray-300 flex items-center">
|
|
279
|
+
{t('filters.header.count')}
|
|
280
|
+
<SortButton currentKey="count" />
|
|
146
281
|
</label>
|
|
147
282
|
</Table>
|
|
283
|
+
{/* <hr /> */}
|
|
148
284
|
<Table>
|
|
149
285
|
{
|
|
150
|
-
|
|
286
|
+
sortedList.map(([value, count], idx) => {
|
|
151
287
|
const id = `rule_checkbox_${idx}`;
|
|
152
288
|
|
|
153
289
|
return (
|
|
154
290
|
<React.Fragment key={idx}>
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const rule: IFilterRule = {
|
|
167
|
-
type: 'one of',
|
|
168
|
-
value: new Set(field.rule.value)
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
if (checked) {
|
|
172
|
-
rule.value.add(value);
|
|
173
|
-
} else {
|
|
174
|
-
rule.value.delete(value);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
onChange(rule);
|
|
178
|
-
}}
|
|
179
|
-
/>
|
|
291
|
+
<div className="flex justify-center items-center">
|
|
292
|
+
<input
|
|
293
|
+
type="checkbox"
|
|
294
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
295
|
+
checked={field.rule?.type === 'one of' && field.rule.value.has(value)}
|
|
296
|
+
id={id}
|
|
297
|
+
aria-describedby={`${id}_label`}
|
|
298
|
+
title={String(value)}
|
|
299
|
+
onChange={({ target: { checked } }) => handleSelectValue(value, checked)}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
180
302
|
<label
|
|
181
303
|
id={`${id}_label`}
|
|
182
304
|
htmlFor={id}
|
|
@@ -197,60 +319,45 @@ export const FilterOneOfRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
197
319
|
<Table className="text-gray-600">
|
|
198
320
|
<label></label>
|
|
199
321
|
<label>
|
|
200
|
-
{t('selected_keys', { count: field.rule.value.size })}
|
|
322
|
+
{t('filters.selected_keys', { count: field.rule.value.size })}
|
|
201
323
|
</label>
|
|
202
324
|
<label>
|
|
203
|
-
{
|
|
204
|
-
const s = dataSource.filter(which => which[field.fid] === key).length;
|
|
205
|
-
|
|
206
|
-
return sum + s;
|
|
207
|
-
}, 0)}
|
|
325
|
+
{selectedValueSum}
|
|
208
326
|
</label>
|
|
209
327
|
</Table>
|
|
210
|
-
<div className="btn-grp">
|
|
211
|
-
<Button
|
|
212
|
-
onClick={() => {
|
|
213
|
-
if (field.rule?.type === 'one of') {
|
|
214
|
-
const curSet = field.rule.value;
|
|
215
|
-
|
|
216
|
-
onChange({
|
|
217
|
-
type: 'one of',
|
|
218
|
-
value: new Set<number | string>(
|
|
219
|
-
curSet.size === count.size
|
|
220
|
-
? []
|
|
221
|
-
: count.keys()
|
|
222
|
-
),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
|
-
>
|
|
227
|
-
{
|
|
228
|
-
field.rule.value.size === count.size
|
|
229
|
-
? t('btn.unselect_all')
|
|
230
|
-
: t('btn.select_all')
|
|
231
|
-
}
|
|
232
|
-
</Button>
|
|
233
|
-
<Button
|
|
234
|
-
onClick={() => {
|
|
235
|
-
if (field.rule?.type === 'one of') {
|
|
236
|
-
const curSet = field.rule.value;
|
|
237
|
-
|
|
238
|
-
onChange({
|
|
239
|
-
type: 'one of',
|
|
240
|
-
value: new Set<number | string>(
|
|
241
|
-
[...count.keys()].filter(key => !curSet.has(key))
|
|
242
|
-
),
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}}
|
|
246
|
-
>
|
|
247
|
-
{t('btn.reverse')}
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
328
|
</Container>
|
|
251
329
|
) : null;
|
|
252
330
|
});
|
|
253
331
|
|
|
332
|
+
interface CalendarInputProps {
|
|
333
|
+
min: number;
|
|
334
|
+
max: number;
|
|
335
|
+
value: number;
|
|
336
|
+
onChange: (value: number) => void;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const CalendarInput: React.FC<CalendarInputProps> = props => {
|
|
340
|
+
const { min, max, value, onChange } = props;
|
|
341
|
+
const dateStringFormatter = (timestamp: number) => {
|
|
342
|
+
return new Date(timestamp).toISOString().slice(0, 19);
|
|
343
|
+
}
|
|
344
|
+
const handleSubmitDate = (value) => {
|
|
345
|
+
if (new Date(value).getTime() <= max && new Date(value).getTime() >= min) {
|
|
346
|
+
onChange(new Date(value).getTime())
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return (
|
|
350
|
+
<input
|
|
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"
|
|
353
|
+
min={dateStringFormatter(min)}
|
|
354
|
+
max={dateStringFormatter(max)}
|
|
355
|
+
defaultValue={dateStringFormatter(value)}
|
|
356
|
+
onChange={(e) => handleSubmitDate(e.target.value)}
|
|
357
|
+
/>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
254
361
|
export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean }> = observer(({
|
|
255
362
|
active,
|
|
256
363
|
field,
|
|
@@ -259,6 +366,8 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
259
366
|
const { commonStore } = useGlobalStore();
|
|
260
367
|
const { currentDataset: { dataSource } } = commonStore;
|
|
261
368
|
|
|
369
|
+
const { t } = useTranslation('translation');
|
|
370
|
+
|
|
262
371
|
const sorted = React.useMemo(() => {
|
|
263
372
|
return dataSource.reduce<number[]>((list, d) => {
|
|
264
373
|
try {
|
|
@@ -266,7 +375,7 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
266
375
|
|
|
267
376
|
list.push(time);
|
|
268
377
|
} catch (error) {
|
|
269
|
-
|
|
378
|
+
|
|
270
379
|
}
|
|
271
380
|
return list;
|
|
272
381
|
}, []).sort((a, b) => a - b);
|
|
@@ -293,14 +402,29 @@ export const FilterTemporalRangeRule: React.FC<RuleFormProps & { active: boolean
|
|
|
293
402
|
}, []);
|
|
294
403
|
|
|
295
404
|
return field.rule?.type === 'temporal range' ? (
|
|
296
|
-
<Container>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
405
|
+
<Container className="overflow-visible">
|
|
406
|
+
<div>{t('constant.filter_type.temporal_range')}</div>
|
|
407
|
+
<div className="text-gray-500">{t('constant.filter_type.temporal_range_desc')}</div>
|
|
408
|
+
<CalendarInputContainer>
|
|
409
|
+
<div className="calendar-input">
|
|
410
|
+
<div className="my-1">{t('filters.range.start_value')}</div>
|
|
411
|
+
<CalendarInput
|
|
412
|
+
min={min}
|
|
413
|
+
max={field.rule.value[1]}
|
|
414
|
+
value={field.rule.value[0]}
|
|
415
|
+
onChange={(value) => handleChange([value, field.rule?.value[1]])}
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
<div className="calendar-input">
|
|
419
|
+
<div className="my-1">{t('filters.range.end_value')}</div>
|
|
420
|
+
<CalendarInput
|
|
421
|
+
min={field.rule.value[0]}
|
|
422
|
+
max={max}
|
|
423
|
+
value={field.rule.value[1]}
|
|
424
|
+
onChange={(value) => handleChange([field.rule?.value[0], value])}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
</CalendarInputContainer>
|
|
304
428
|
</Container>
|
|
305
429
|
) : null;
|
|
306
430
|
});
|
|
@@ -313,6 +437,8 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
313
437
|
const { commonStore } = useGlobalStore();
|
|
314
438
|
const { currentDataset: { dataSource } } = commonStore;
|
|
315
439
|
|
|
440
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
441
|
+
|
|
316
442
|
const sorted = React.useMemo(() => {
|
|
317
443
|
return dataSource.map(d => d[field.fid]).sort((a, b) => a - b);
|
|
318
444
|
}, [dataSource, field]);
|
|
@@ -339,6 +465,8 @@ export const FilterRangeRule: React.FC<RuleFormProps & { active: boolean }> = ob
|
|
|
339
465
|
|
|
340
466
|
return field.rule?.type === 'range' ? (
|
|
341
467
|
<Container>
|
|
468
|
+
<div>{t('range')}</div>
|
|
469
|
+
<div className="text-gray-500">{t('range_desc')}</div>
|
|
342
470
|
<Slider
|
|
343
471
|
min={min}
|
|
344
472
|
max={max}
|
|
@@ -355,6 +483,21 @@ const filterTabs: Record<IFilterRule['type'], React.FC<RuleFormProps & { active:
|
|
|
355
483
|
'temporal range': FilterTemporalRangeRule,
|
|
356
484
|
};
|
|
357
485
|
|
|
486
|
+
const tabOptionDict = {
|
|
487
|
+
"one of": {
|
|
488
|
+
key: "one_of",
|
|
489
|
+
descKey: "one_of_desc"
|
|
490
|
+
},
|
|
491
|
+
"range": {
|
|
492
|
+
key: "range",
|
|
493
|
+
descKey: "range_desc"
|
|
494
|
+
},
|
|
495
|
+
"temporal range": {
|
|
496
|
+
key: "temporal_range",
|
|
497
|
+
descKey: "temporal_range_desc"
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
358
501
|
export interface TabsProps extends RuleFormProps {
|
|
359
502
|
tabs: IFilterRule['type'][];
|
|
360
503
|
}
|
|
@@ -366,19 +509,42 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
366
509
|
const { t } = useTranslation('translation', { keyPrefix: 'constant.filter_type' });
|
|
367
510
|
|
|
368
511
|
const [which, setWhich] = React.useState(field.rule?.type ?? tabs[0]!);
|
|
512
|
+
React.useEffect(() => {
|
|
513
|
+
if (!tabs.includes(which)) setWhich(tabs[0]);
|
|
514
|
+
}, [tabs])
|
|
369
515
|
|
|
370
516
|
return (
|
|
371
517
|
<TabsContainer>
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
518
|
+
<div>
|
|
519
|
+
{
|
|
520
|
+
tabs.map((option) => {
|
|
521
|
+
return (
|
|
522
|
+
<div className="flex my-2" key={option}>
|
|
523
|
+
<div className="align-top">
|
|
524
|
+
<input
|
|
525
|
+
type="radio"
|
|
526
|
+
className="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
|
527
|
+
id={option}
|
|
528
|
+
checked={option === which}
|
|
529
|
+
onChange={e => setWhich((e.target as HTMLInputElement).value as typeof which)}
|
|
530
|
+
name="filter_type"
|
|
531
|
+
value={option}
|
|
532
|
+
/>
|
|
533
|
+
</div>
|
|
534
|
+
<div className="ml-3">
|
|
535
|
+
<label htmlFor={option}>
|
|
536
|
+
{t(tabOptionDict[option].key)}
|
|
537
|
+
</label>
|
|
538
|
+
<div className="text-gray-500">
|
|
539
|
+
{t(tabOptionDict[option].descKey)}
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
</div>
|
|
547
|
+
<hr className="my-0.5" />
|
|
382
548
|
<TabPanel>
|
|
383
549
|
{
|
|
384
550
|
tabs.map((tab, i) => {
|
|
@@ -387,8 +553,8 @@ const Tabs: React.FC<TabsProps> = observer(({ field, onChange, tabs }) => {
|
|
|
387
553
|
return draggableFieldState === null ? null : (
|
|
388
554
|
<TabItem
|
|
389
555
|
key={i}
|
|
390
|
-
id={`filter-panel-${tab
|
|
391
|
-
aria-labelledby={`filter-tab-${tab
|
|
556
|
+
id={`filter-panel-${tabOptionDict[tab]}`}
|
|
557
|
+
aria-labelledby={`filter-tab-${tabOptionDict[tab]}`}
|
|
392
558
|
role="tabpanel"
|
|
393
559
|
hidden={which !== tab}
|
|
394
560
|
tabIndex={0}
|
package/src/lib/execExp.ts
CHANGED
|
@@ -82,7 +82,7 @@ function binCount(resKey: string, params: IExpParamter[], data: IDataFrame, binS
|
|
|
82
82
|
|
|
83
83
|
const groupSize = valueWithIndices.length / binSize;
|
|
84
84
|
|
|
85
|
-
const newValues = valueWithIndices.map(item => {
|
|
85
|
+
const newValues = valueWithIndices.sort((a, b) => a.index - b.index).map(item => {
|
|
86
86
|
let bIndex = Math.floor(item.orderIndex / groupSize);
|
|
87
87
|
if (bIndex === binSize) bIndex = binSize - 1;
|
|
88
88
|
return bIndex + 1
|