baseui 10.9.2 → 10.10.0
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/data-table/column-numerical.js +320 -362
- package/data-table/column-numerical.js.flow +285 -287
- package/data-table/constants.js +17 -11
- package/data-table/constants.js.flow +11 -8
- package/data-table/data-table.js +53 -50
- package/data-table/data-table.js.flow +18 -13
- package/data-table/filter-shell.js +27 -4
- package/data-table/filter-shell.js.flow +33 -9
- package/data-table/locale.js +4 -2
- package/data-table/locale.js.flow +6 -2
- package/data-table/measure-column-widths.js +83 -119
- package/data-table/measure-column-widths.js.flow +87 -107
- package/es/data-table/column-numerical.js +252 -320
- package/es/data-table/constants.js +12 -8
- package/es/data-table/data-table.js +18 -16
- package/es/data-table/filter-shell.js +26 -4
- package/es/data-table/locale.js +4 -2
- package/es/data-table/measure-column-widths.js +75 -84
- package/es/timezonepicker/timezone-picker.js +1 -1
- package/esm/data-table/column-numerical.js +317 -360
- package/esm/data-table/constants.js +12 -8
- package/esm/data-table/data-table.js +53 -50
- package/esm/data-table/filter-shell.js +26 -4
- package/esm/data-table/locale.js +4 -2
- package/esm/data-table/measure-column-widths.js +83 -119
- package/esm/timezonepicker/timezone-picker.js +1 -1
- package/package.json +2 -1
- package/timezonepicker/timezone-picker.js +1 -1
- package/timezonepicker/timezone-picker.js.flow +1 -1
|
@@ -12,26 +12,25 @@ import {Button, SIZE} from '../button/index.js';
|
|
|
12
12
|
import {ButtonGroup, MODE} from '../button-group/index.js';
|
|
13
13
|
import {Input, SIZE as INPUT_SIZE} from '../input/index.js';
|
|
14
14
|
import {useStyletron} from '../styles/index.js';
|
|
15
|
-
import {ParagraphXSmall} from '../typography/index.js';
|
|
16
15
|
|
|
17
16
|
import Column from './column.js';
|
|
18
|
-
import {
|
|
19
|
-
|
|
17
|
+
import {
|
|
18
|
+
COLUMNS,
|
|
19
|
+
NUMERICAL_FORMATS,
|
|
20
|
+
MAX_BIN_COUNT,
|
|
21
|
+
HISTOGRAM_SIZE,
|
|
22
|
+
} from './constants.js';
|
|
23
|
+
import FilterShell, {type ExcludeKind} from './filter-shell.js';
|
|
20
24
|
import type {ColumnT, SharedColumnOptionsT} from './types.js';
|
|
21
25
|
import {LocaleContext} from '../locale/index.js';
|
|
26
|
+
import {bin, max as maxFunc, extent, scaleLinear, median, bisector} from 'd3';
|
|
27
|
+
import {Slider} from '../slider/index.js';
|
|
22
28
|
|
|
23
29
|
type NumericalFormats =
|
|
24
30
|
| typeof NUMERICAL_FORMATS.DEFAULT
|
|
25
31
|
| typeof NUMERICAL_FORMATS.ACCOUNTING
|
|
26
32
|
| typeof NUMERICAL_FORMATS.PERCENTAGE;
|
|
27
33
|
|
|
28
|
-
type NumericalOperations =
|
|
29
|
-
| typeof NUMERICAL_OPERATIONS.EQ
|
|
30
|
-
| typeof NUMERICAL_OPERATIONS.GT
|
|
31
|
-
| typeof NUMERICAL_OPERATIONS.GTE
|
|
32
|
-
| typeof NUMERICAL_OPERATIONS.LT
|
|
33
|
-
| typeof NUMERICAL_OPERATIONS.LTE;
|
|
34
|
-
|
|
35
34
|
type OptionsT = {|
|
|
36
35
|
...SharedColumnOptionsT<number>,
|
|
37
36
|
format?: NumericalFormats | ((value: number) => string),
|
|
@@ -40,12 +39,11 @@ type OptionsT = {|
|
|
|
40
39
|
|};
|
|
41
40
|
|
|
42
41
|
type FilterParametersT = {|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
operation: NumericalOperations,
|
|
46
|
-
|}>,
|
|
42
|
+
lowerValue: number,
|
|
43
|
+
upperValue: number,
|
|
47
44
|
description: string,
|
|
48
45
|
exclude: boolean,
|
|
46
|
+
excludeKind: ExcludeKind,
|
|
49
47
|
|};
|
|
50
48
|
|
|
51
49
|
type NumericalColumnT = ColumnT<number, FilterParametersT>;
|
|
@@ -86,224 +84,208 @@ function validateInput(input) {
|
|
|
86
84
|
return Boolean(parseFloat(input)) || input === '' || input === '-';
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
right: '',
|
|
121
|
-
};
|
|
122
|
-
} else if (comparison.operation === NUMERICAL_OPERATIONS.LTE) {
|
|
123
|
-
return {
|
|
124
|
-
exclude: filterParams.exclude,
|
|
125
|
-
comparatorIndex: 0,
|
|
126
|
-
operatorIndex: 2,
|
|
127
|
-
left: '',
|
|
128
|
-
right: comparison.value.toString(),
|
|
129
|
-
};
|
|
130
|
-
} else if (comparison.operation === NUMERICAL_OPERATIONS.GTE) {
|
|
131
|
-
return {
|
|
132
|
-
exclude: filterParams.exclude,
|
|
133
|
-
comparatorIndex: 0,
|
|
134
|
-
operatorIndex: 3,
|
|
135
|
-
left: comparison.value.toString(),
|
|
136
|
-
right: '',
|
|
137
|
-
};
|
|
138
|
-
} else if (comparison.operation === NUMERICAL_OPERATIONS.EQ) {
|
|
139
|
-
return {
|
|
140
|
-
exclude: filterParams.exclude,
|
|
141
|
-
comparatorIndex: 1,
|
|
142
|
-
operatorIndex: 0,
|
|
143
|
-
left: comparison.value.toString(),
|
|
144
|
-
right: '',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
87
|
+
const bisect = bisector(d => d.x0);
|
|
88
|
+
|
|
89
|
+
const Histogram = React.memo(function Histogram({
|
|
90
|
+
data,
|
|
91
|
+
lower,
|
|
92
|
+
upper,
|
|
93
|
+
isRange,
|
|
94
|
+
exclude,
|
|
95
|
+
precision,
|
|
96
|
+
}) {
|
|
97
|
+
const [css, theme] = useStyletron();
|
|
98
|
+
|
|
99
|
+
const {bins, xScale, yScale} = React.useMemo(() => {
|
|
100
|
+
const bins = bin().thresholds(Math.min(data.length, MAX_BIN_COUNT))(data);
|
|
101
|
+
|
|
102
|
+
const xScale = scaleLinear()
|
|
103
|
+
.domain([bins[0].x0, bins[bins.length - 1].x1])
|
|
104
|
+
.range([0, HISTOGRAM_SIZE.width])
|
|
105
|
+
.clamp(true);
|
|
106
|
+
|
|
107
|
+
const yScale = scaleLinear()
|
|
108
|
+
.domain([0, maxFunc(bins, d => d.length)])
|
|
109
|
+
.nice()
|
|
110
|
+
.range([HISTOGRAM_SIZE.height, 0]);
|
|
111
|
+
return {bins, xScale, yScale};
|
|
112
|
+
}, [data]);
|
|
113
|
+
|
|
114
|
+
// We need to find the index of bar which is nearest to the given single value
|
|
115
|
+
const singleIndexNearest = React.useMemo(() => {
|
|
116
|
+
if (isRange) {
|
|
117
|
+
return null;
|
|
147
118
|
}
|
|
148
|
-
|
|
119
|
+
return bisect.center(bins, lower);
|
|
120
|
+
}, [isRange, data, lower, upper]);
|
|
149
121
|
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
className={css({
|
|
125
|
+
display: 'flex',
|
|
126
|
+
marginTop: theme.sizing.scale600,
|
|
127
|
+
marginLeft: theme.sizing.scale200,
|
|
128
|
+
marginRight: 0,
|
|
129
|
+
marginBottom: theme.sizing.scale400,
|
|
130
|
+
justifyContent: 'space-between',
|
|
131
|
+
overflow: 'visible',
|
|
132
|
+
})}
|
|
133
|
+
>
|
|
134
|
+
<svg {...HISTOGRAM_SIZE}>
|
|
135
|
+
{bins.map((d, index) => {
|
|
136
|
+
const x = xScale(d.x0) + 1;
|
|
137
|
+
const y = yScale(d.length);
|
|
138
|
+
const width = Math.max(0, xScale(d.x1) - xScale(d.x0) - 1);
|
|
139
|
+
const height = yScale(0) - yScale(d.length);
|
|
140
|
+
|
|
141
|
+
let included;
|
|
142
|
+
if (singleIndexNearest != null) {
|
|
143
|
+
included = index === singleIndexNearest;
|
|
144
|
+
} else {
|
|
145
|
+
const withinLower = d.x1 > lower;
|
|
146
|
+
const withinUpper = d.x0 <= upper;
|
|
147
|
+
included = withinLower && withinUpper;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (exclude) {
|
|
151
|
+
included = !included;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<rect
|
|
156
|
+
key={`bar-${index}`}
|
|
157
|
+
fill={included ? theme.colors.primary : theme.colors.mono400}
|
|
158
|
+
x={x}
|
|
159
|
+
y={y}
|
|
160
|
+
width={width}
|
|
161
|
+
height={height}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</svg>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
});
|
|
158
169
|
|
|
159
170
|
function NumericalFilter(props) {
|
|
160
171
|
const [css, theme] = useStyletron();
|
|
161
172
|
const locale = React.useContext(LocaleContext);
|
|
162
173
|
|
|
163
|
-
const
|
|
174
|
+
const precision = props.options.precision;
|
|
175
|
+
|
|
176
|
+
// The state handling of this component could be refactored and cleaned up if we used useReducer.
|
|
177
|
+
const initialState = React.useMemo(() => {
|
|
178
|
+
return (
|
|
179
|
+
props.filterParams || {
|
|
180
|
+
exclude: false,
|
|
181
|
+
excludeKind: 'range',
|
|
182
|
+
comparatorIndex: 0,
|
|
183
|
+
lowerValue: null,
|
|
184
|
+
upperValue: null,
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}, [props.filterParams]);
|
|
188
|
+
|
|
164
189
|
const [exclude, setExclude] = React.useState(initialState.exclude);
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
// the api of our ButtonGroup forces these numerical indexes...
|
|
192
|
+
// TODO look into allowing semantic names, similar to the radio component. Tricky part would be backwards compat
|
|
193
|
+
const [comparatorIndex, setComparatorIndex] = React.useState(() => {
|
|
194
|
+
switch (initialState.excludeKind) {
|
|
195
|
+
case 'value':
|
|
196
|
+
return 1;
|
|
197
|
+
case 'range':
|
|
198
|
+
default:
|
|
199
|
+
// fallthrough
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// We use the d3 function to get the extent as it's a little more robust to null, -Infinity, etc.
|
|
205
|
+
const [min, max] = React.useMemo(() => extent(props.data), [props.data]);
|
|
206
|
+
|
|
207
|
+
const [lv, setLower] = React.useState<number>(() =>
|
|
208
|
+
roundToFixed(initialState.lowerValue || min, precision),
|
|
167
209
|
);
|
|
168
|
-
const [
|
|
169
|
-
initialState.
|
|
210
|
+
const [uv, setUpper] = React.useState<number>(() =>
|
|
211
|
+
roundToFixed(initialState.upperValue || max, precision),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// We keep a separate value for the single select, to give a user the ability to toggle between
|
|
215
|
+
// the range and single values without losing their previous input.
|
|
216
|
+
const [sv, setSingle] = React.useState<number>(() =>
|
|
217
|
+
roundToFixed(initialState.lowerValue || median(props.data), precision),
|
|
170
218
|
);
|
|
171
|
-
const [left, setLeft] = React.useState(initialState.left);
|
|
172
|
-
const [right, setRight] = React.useState(initialState.right);
|
|
173
219
|
|
|
220
|
+
// This is the only conditional which we want to use to determine
|
|
221
|
+
// if we are in range or single value mode.
|
|
222
|
+
// Don't derive it via something else, e.g. lowerValue === upperValue, etc.
|
|
174
223
|
const isRange = comparatorIndex === 0;
|
|
175
|
-
const min = React.useMemo(() => Math.min(...props.data), [props.data]);
|
|
176
|
-
const max = React.useMemo(() => Math.max(...props.data), [props.data]);
|
|
177
224
|
|
|
178
|
-
|
|
179
|
-
if (!left) {
|
|
180
|
-
setLeft(min.toString());
|
|
181
|
-
}
|
|
182
|
-
if (!right) {
|
|
183
|
-
setRight(max.toString());
|
|
184
|
-
}
|
|
185
|
-
}, []);
|
|
186
|
-
|
|
187
|
-
const [leftDisabled, rightDisabled] = React.useMemo(() => {
|
|
188
|
-
if (!isRange) return [false, false];
|
|
189
|
-
switch (operatorIndex) {
|
|
190
|
-
case 4:
|
|
191
|
-
return [false, false];
|
|
192
|
-
case 0:
|
|
193
|
-
case 2:
|
|
194
|
-
return [true, false];
|
|
195
|
-
case 1:
|
|
196
|
-
case 3:
|
|
197
|
-
return [false, true];
|
|
198
|
-
default:
|
|
199
|
-
return [true, true];
|
|
200
|
-
}
|
|
201
|
-
}, [operatorIndex, isRange]);
|
|
202
|
-
|
|
203
|
-
const leftInputRef = React.useRef(null);
|
|
204
|
-
const rightInputRef = React.useRef(null);
|
|
205
|
-
React.useEffect(() => {
|
|
206
|
-
if (!leftDisabled && leftInputRef.current) {
|
|
207
|
-
leftInputRef.current.focus({preventScroll: true});
|
|
208
|
-
} else if (!rightDisabled && rightInputRef.current) {
|
|
209
|
-
rightInputRef.current.focus({preventScroll: true});
|
|
210
|
-
}
|
|
211
|
-
}, [leftDisabled, rightDisabled, comparatorIndex]);
|
|
225
|
+
const excludeKind = isRange ? 'range' : 'value';
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
case 3:
|
|
220
|
-
setRight(max.toString());
|
|
221
|
-
break;
|
|
222
|
-
case 0:
|
|
223
|
-
case 2:
|
|
224
|
-
setLeft(min.toString());
|
|
225
|
-
break;
|
|
227
|
+
// while the user is inputting values, we take their input at face value,
|
|
228
|
+
// if we don't do this, a user can't input partial numbers, e.g. "-", or "3."
|
|
229
|
+
const [focused, setFocus] = React.useState(false);
|
|
230
|
+
const [inputValueLower, inputValueUpper] = React.useMemo(() => {
|
|
231
|
+
if (focused) {
|
|
232
|
+
return [isRange ? lv : sv, uv];
|
|
226
233
|
}
|
|
227
|
-
|
|
234
|
+
|
|
235
|
+
// once the user is done inputting.
|
|
236
|
+
// we validate then format to the given precision
|
|
237
|
+
let l = isRange ? lv : sv;
|
|
238
|
+
l = validateInput(l) ? l : min;
|
|
239
|
+
let h = validateInput(uv) ? uv : max;
|
|
240
|
+
|
|
241
|
+
return [roundToFixed(l, precision), roundToFixed(h, precision)];
|
|
242
|
+
}, [isRange, focused, sv, lv, uv, precision]);
|
|
243
|
+
|
|
244
|
+
// We have our slider values range from 1 to the bin size, so we have a scale which
|
|
245
|
+
// takes in the data driven range and maps it to values the scale can always handle
|
|
246
|
+
const sliderScale = React.useMemo(
|
|
247
|
+
() =>
|
|
248
|
+
scaleLinear()
|
|
249
|
+
.domain([min, max])
|
|
250
|
+
.rangeRound([1, MAX_BIN_COUNT])
|
|
251
|
+
// We clamp the values within our min and max even if a user enters a huge number
|
|
252
|
+
.clamp(true),
|
|
253
|
+
[min, max],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
let sliderValue = isRange
|
|
257
|
+
? [sliderScale(inputValueLower), sliderScale(inputValueUpper)]
|
|
258
|
+
: [sliderScale(inputValueLower)];
|
|
259
|
+
|
|
260
|
+
// keep the slider happy by sorting the two values
|
|
261
|
+
if (isRange && sliderValue[0] > sliderValue[1]) {
|
|
262
|
+
sliderValue = [sliderValue[1], sliderValue[0]];
|
|
263
|
+
}
|
|
228
264
|
|
|
229
265
|
return (
|
|
230
266
|
<FilterShell
|
|
231
267
|
exclude={exclude}
|
|
232
268
|
onExcludeChange={() => setExclude(!exclude)}
|
|
269
|
+
excludeKind={excludeKind}
|
|
233
270
|
onApply={() => {
|
|
234
271
|
if (isRange) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
case 1: {
|
|
247
|
-
const value = parseFloat(left);
|
|
248
|
-
const operation = NUMERICAL_OPERATIONS.GT;
|
|
249
|
-
props.setFilter({
|
|
250
|
-
comparisons: [{value, operation}],
|
|
251
|
-
description: `> ${value}`,
|
|
252
|
-
exclude,
|
|
253
|
-
});
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
case 2: {
|
|
257
|
-
const value = parseFloat(right);
|
|
258
|
-
const operation = NUMERICAL_OPERATIONS.LTE;
|
|
259
|
-
props.setFilter({
|
|
260
|
-
comparisons: [{value, operation}],
|
|
261
|
-
description: `≤ ${value}`,
|
|
262
|
-
exclude,
|
|
263
|
-
});
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
case 3: {
|
|
267
|
-
const value = parseFloat(left);
|
|
268
|
-
const operation = NUMERICAL_OPERATIONS.GTE;
|
|
269
|
-
props.setFilter({
|
|
270
|
-
comparisons: [{value, operation}],
|
|
271
|
-
description: `≥ ${value}`,
|
|
272
|
-
exclude,
|
|
273
|
-
});
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
case 4: {
|
|
277
|
-
// 'between' case is interesting since if we want less than 10 plus greater than 5
|
|
278
|
-
// comparators, the filter will include _all_ numbers.
|
|
279
|
-
const leftValue = parseFloat(left);
|
|
280
|
-
const rightValue = parseFloat(right);
|
|
281
|
-
props.setFilter({
|
|
282
|
-
comparisons: [
|
|
283
|
-
{
|
|
284
|
-
value: leftValue,
|
|
285
|
-
operation: NUMERICAL_OPERATIONS.LT,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
value: rightValue,
|
|
289
|
-
operation: NUMERICAL_OPERATIONS.GT,
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
description: `≥ ${leftValue} & ≤ ${rightValue}`,
|
|
293
|
-
exclude: !exclude,
|
|
294
|
-
});
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
default:
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
272
|
+
const lowerValue = parseFloat(inputValueLower);
|
|
273
|
+
const upperValue = parseFloat(inputValueUpper);
|
|
274
|
+
props.setFilter({
|
|
275
|
+
description: `≥ ${lowerValue} and ≤ ${upperValue}`,
|
|
276
|
+
exclude: exclude,
|
|
277
|
+
lowerValue,
|
|
278
|
+
upperValue,
|
|
279
|
+
excludeKind,
|
|
280
|
+
});
|
|
300
281
|
} else {
|
|
301
|
-
const value = parseFloat(
|
|
302
|
-
const operation = NUMERICAL_OPERATIONS.EQ;
|
|
282
|
+
const value = parseFloat(inputValueLower);
|
|
303
283
|
props.setFilter({
|
|
304
|
-
comparisons: [{value, operation}],
|
|
305
284
|
description: `= ${value}`,
|
|
306
|
-
exclude,
|
|
285
|
+
exclude: exclude,
|
|
286
|
+
lowerValue: inputValueLower,
|
|
287
|
+
upperValue: inputValueLower,
|
|
288
|
+
excludeKind,
|
|
307
289
|
});
|
|
308
290
|
}
|
|
309
291
|
|
|
@@ -311,7 +293,7 @@ function NumericalFilter(props) {
|
|
|
311
293
|
}}
|
|
312
294
|
>
|
|
313
295
|
<ButtonGroup
|
|
314
|
-
size={SIZE.
|
|
296
|
+
size={SIZE.mini}
|
|
315
297
|
mode={MODE.radio}
|
|
316
298
|
selected={comparatorIndex}
|
|
317
299
|
onClick={(_, index) => setComparatorIndex(index)}
|
|
@@ -324,100 +306,131 @@ function NumericalFilter(props) {
|
|
|
324
306
|
<Button
|
|
325
307
|
type="button"
|
|
326
308
|
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
309
|
+
aria-label={locale.datatable.numericalFilterRange}
|
|
327
310
|
>
|
|
328
311
|
{locale.datatable.numericalFilterRange}
|
|
329
312
|
</Button>
|
|
330
313
|
<Button
|
|
331
314
|
type="button"
|
|
332
315
|
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
316
|
+
aria-label={locale.datatable.numericalFilterSingleValue}
|
|
333
317
|
>
|
|
334
318
|
{locale.datatable.numericalFilterSingleValue}
|
|
335
319
|
</Button>
|
|
336
320
|
</ButtonGroup>
|
|
337
321
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
322
|
+
<Histogram
|
|
323
|
+
data={props.data}
|
|
324
|
+
lower={inputValueLower}
|
|
325
|
+
upper={inputValueUpper}
|
|
326
|
+
isRange={isRange}
|
|
327
|
+
exclude={exclude}
|
|
328
|
+
precision={props.options.precision}
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
<div className={css({display: 'flex', justifyContent: 'space-between'})}>
|
|
332
|
+
<Slider
|
|
333
|
+
// The slider throws errors when switching between single and two values
|
|
334
|
+
// when it tries to read getThumbDistance on a thumb which is not there anymore
|
|
335
|
+
// if we create a new instance these errors are prevented.
|
|
336
|
+
key={isRange.toString()}
|
|
337
|
+
min={1}
|
|
338
|
+
max={MAX_BIN_COUNT}
|
|
339
|
+
value={sliderValue}
|
|
340
|
+
onChange={({value}) => {
|
|
341
|
+
if (!value) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// we convert back from the slider scale to the actual data's scale
|
|
345
|
+
if (isRange) {
|
|
346
|
+
const [lowerValue, upperValue] = value;
|
|
347
|
+
setLower(sliderScale.invert(lowerValue));
|
|
348
|
+
setUpper(sliderScale.invert(upperValue));
|
|
349
|
+
} else {
|
|
350
|
+
const [singleValue] = value;
|
|
351
|
+
setSingle(sliderScale.invert(singleValue));
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
344
354
|
overrides={{
|
|
355
|
+
InnerThumb: function InnerThumb({$value, $thumbIndex}) {
|
|
356
|
+
return <React.Fragment>{$value[$thumbIndex]}</React.Fragment>;
|
|
357
|
+
},
|
|
358
|
+
TickBar: ({$min, $max}) => null, // we don't want the ticks
|
|
359
|
+
ThumbValue: () => null,
|
|
345
360
|
Root: {
|
|
346
|
-
style: (
|
|
361
|
+
style: () => ({
|
|
362
|
+
// Aligns the center of the slider handles with the histogram bars
|
|
363
|
+
width: 'calc(100% + 14px)',
|
|
364
|
+
margin: '0 -7px',
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
InnerTrack: {
|
|
368
|
+
style: ({$theme}) => {
|
|
369
|
+
if (!isRange) {
|
|
370
|
+
return {
|
|
371
|
+
// For range selection we use the color as is, but when selecting the single value,
|
|
372
|
+
// we don't want the track standing out, so mute its color
|
|
373
|
+
background: theme.colors.mono400,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
Thumb: {
|
|
379
|
+
style: () => ({
|
|
380
|
+
// Slider handles are small enough to visually be centered within each histogram bar
|
|
381
|
+
height: '18px',
|
|
382
|
+
width: '18px',
|
|
383
|
+
fontSize: '0px',
|
|
384
|
+
}),
|
|
347
385
|
},
|
|
348
386
|
}}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
type="button"
|
|
352
|
-
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
353
|
-
>
|
|
354
|
-
<
|
|
355
|
-
</Button>
|
|
356
|
-
<Button
|
|
357
|
-
type="button"
|
|
358
|
-
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
359
|
-
>
|
|
360
|
-
>
|
|
361
|
-
</Button>
|
|
362
|
-
<Button
|
|
363
|
-
type="button"
|
|
364
|
-
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
365
|
-
>
|
|
366
|
-
≤
|
|
367
|
-
</Button>
|
|
368
|
-
<Button
|
|
369
|
-
type="button"
|
|
370
|
-
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
371
|
-
>
|
|
372
|
-
≥
|
|
373
|
-
</Button>
|
|
374
|
-
<Button
|
|
375
|
-
type="button"
|
|
376
|
-
overrides={{BaseButton: {style: {width: '100%'}}}}
|
|
377
|
-
>
|
|
378
|
-
=
|
|
379
|
-
</Button>
|
|
380
|
-
</ButtonGroup>
|
|
381
|
-
)}
|
|
382
|
-
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
383
389
|
<div
|
|
384
390
|
className={css({
|
|
385
391
|
display: 'flex',
|
|
392
|
+
marginTop: theme.sizing.scale400,
|
|
393
|
+
// This % gap is visually appealing given the filter box width
|
|
394
|
+
gap: '30%',
|
|
386
395
|
justifyContent: 'space-between',
|
|
387
|
-
marginLeft: theme.sizing.scale300,
|
|
388
|
-
marginRight: theme.sizing.scale300,
|
|
389
396
|
})}
|
|
390
397
|
>
|
|
391
|
-
<ParagraphXSmall>{format(min, props.options)}</ParagraphXSmall>{' '}
|
|
392
|
-
<ParagraphXSmall>{format(max, props.options)}</ParagraphXSmall>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
<div className={css({display: 'flex', justifyContent: 'space-between'})}>
|
|
396
398
|
<Input
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
value={
|
|
399
|
+
min={min}
|
|
400
|
+
max={max}
|
|
401
|
+
size={INPUT_SIZE.mini}
|
|
402
|
+
overrides={{Root: {style: {width: '100%'}}}}
|
|
403
|
+
value={inputValueLower}
|
|
402
404
|
onChange={event => {
|
|
403
405
|
if (validateInput(event.target.value)) {
|
|
404
|
-
|
|
406
|
+
isRange
|
|
407
|
+
? // $FlowFixMe - we know it is a number by now
|
|
408
|
+
setLower(event.target.value)
|
|
409
|
+
: // $FlowFixMe - we know it is a number by now
|
|
410
|
+
setSingle(event.target.value);
|
|
405
411
|
}
|
|
406
412
|
}}
|
|
413
|
+
onFocus={() => setFocus(true)}
|
|
414
|
+
onBlur={() => setFocus(false)}
|
|
407
415
|
/>
|
|
408
|
-
|
|
409
416
|
{isRange && (
|
|
410
417
|
<Input
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
min={min}
|
|
419
|
+
max={max}
|
|
420
|
+
size={INPUT_SIZE.mini}
|
|
421
|
+
overrides={{
|
|
422
|
+
Input: {style: {textAlign: 'right'}},
|
|
423
|
+
Root: {style: {width: '100%'}},
|
|
424
|
+
}}
|
|
425
|
+
value={inputValueUpper}
|
|
416
426
|
onChange={event => {
|
|
417
427
|
if (validateInput(event.target.value)) {
|
|
418
|
-
|
|
428
|
+
// $FlowFixMe - we know it is a number by now
|
|
429
|
+
setUpper(event.target.value);
|
|
419
430
|
}
|
|
420
431
|
}}
|
|
432
|
+
onFocus={() => setFocus(true)}
|
|
433
|
+
onBlur={() => setFocus(false)}
|
|
421
434
|
/>
|
|
422
435
|
)}
|
|
423
436
|
</div>
|
|
@@ -480,24 +493,9 @@ function NumericalColumn(options: OptionsT): NumericalColumnT {
|
|
|
480
493
|
kind: COLUMNS.NUMERICAL,
|
|
481
494
|
buildFilter: function(params) {
|
|
482
495
|
return function(data) {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
switch (c.operation) {
|
|
487
|
-
case NUMERICAL_OPERATIONS.EQ:
|
|
488
|
-
return left === right;
|
|
489
|
-
case NUMERICAL_OPERATIONS.GT:
|
|
490
|
-
return left > right;
|
|
491
|
-
case NUMERICAL_OPERATIONS.GTE:
|
|
492
|
-
return left >= right;
|
|
493
|
-
case NUMERICAL_OPERATIONS.LT:
|
|
494
|
-
return left < right;
|
|
495
|
-
case NUMERICAL_OPERATIONS.LTE:
|
|
496
|
-
return left <= right;
|
|
497
|
-
default:
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
});
|
|
496
|
+
const value = roundToFixed(data, normalizedOptions.precision);
|
|
497
|
+
const included =
|
|
498
|
+
value >= params.lowerValue && value <= params.upperValue;
|
|
501
499
|
return params.exclude ? !included : included;
|
|
502
500
|
};
|
|
503
501
|
},
|