@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.
@@ -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
- marginBlock: '1em',
19
- marginInline: '2em',
19
+ const Container = styled.div`
20
+ margin-block: 1em;
20
21
 
21
- '> .btn-grp': {
22
- display: 'flex',
23
- flexDirection: 'row',
24
- marginBlock: '0.4em 0.6em',
22
+ > .btn-grp {
23
+ display: flex;
24
+ flex-direction: row;
25
+ margin-block: 1em;
25
26
 
26
- '> *': {
27
- marginInlineStart: '0.6em',
27
+ > * {
28
+ margin-inline-start: 0.6em;
28
29
 
29
- '&:first-child': {
30
- marginInlineStart: 0,
30
+ &:first-child: {
31
+ margin-inline-start: 0;
31
32
  },
32
33
  },
33
34
  },
34
- });
35
-
36
- export const Button = styled.button({
37
- '&:hover': {
38
- backgroundColor: 'rgba(243, 244, 246, 0.5)',
39
- },
40
- color: 'rgb(55, 65, 81)',
41
- boxShadow: '1px 1px 2px #0002, inset 2px 2px 4px #0001',
42
- paddingBlock: '0.2em',
43
- paddingInline: '0.5em',
44
- userSelect: 'none',
45
- cursor: 'pointer',
46
- });
47
-
48
- const Table = styled.div({
49
- display: 'grid',
50
- gridTemplateColumns: '4em auto max-content',
51
- maxHeight: '30vh',
52
- overflowY: 'scroll',
53
- '& > *': {
54
- marginBlock: '2px',
55
- paddingInline: '4px',
56
- whiteSpace: 'nowrap',
57
- overflow: 'hidden',
58
- textOverflow: 'ellipsis',
59
- userSelect: 'none',
60
- },
61
- '& > input, & > *[for]': {
62
- cursor: 'pointer',
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
- const TabsContainer = styled.div({
67
- display: 'flex',
68
- flexDirection: 'column',
69
- alignItems: 'stretch',
70
- justifyContent: 'stretch',
71
- });
72
-
73
- const TabList = styled.div({
74
- display: 'flex',
75
- flexDirection: 'row',
76
- alignItems: 'stretch',
77
- justifyContent: 'flex-start',
78
- overflow: 'hidden',
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
- const TabItem = styled.div({});
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 { t } = useTranslation('translation', { keyPrefix: 'filters' });
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
- <Table>
138
- <label className="header">
139
- {t('header.visibility')}
140
- </label>
141
- <label className="header">
142
- {t('header.value')}
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
- [...count.entries()].map(([value, count], idx) => {
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
- <input
156
- type="checkbox"
157
- checked={field.rule?.type === 'one of' && field.rule.value.has(value)}
158
- id={id}
159
- aria-describedby={`${id}_label`}
160
- title={String(value)}
161
- onChange={({ target: { checked } }) => {
162
- if (field.rule?.type !== 'one of') {
163
- return;
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
- {[...field.rule.value].reduce<number>((sum, key) => {
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
- <Slider
298
- min={min}
299
- max={max}
300
- value={field.rule.value}
301
- onChange={handleChange}
302
- isDateTime
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
- <PureTabs
373
- selectedKey={which}
374
- tabs={tabs.map(tab => ({
375
- key: tab,
376
- label: t(tab.replaceAll(/ /g, '_')),
377
- }))}
378
- onSelected={sk => {
379
- setWhich(sk as typeof which);
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.replaceAll(/ /g, '_')}`}
391
- aria-labelledby={`filter-tab-${tab.replaceAll(/ /g, '_')}`}
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}
@@ -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