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.
@@ -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 {COLUMNS, NUMERICAL_FORMATS, NUMERICAL_OPERATIONS} from './constants.js';
19
- import FilterShell from './filter-shell.js';
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
- comparisons: Array<{|
44
- value: number,
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
- function filterParamsToInitialState(filterParams) {
90
- if (filterParams) {
91
- if (filterParams.comparisons.length > 1) {
92
- if (
93
- filterParams.comparisons[0].operation === NUMERICAL_OPERATIONS.LT &&
94
- filterParams.comparisons[1].operation === NUMERICAL_OPERATIONS.GT
95
- ) {
96
- return {
97
- exclude: !filterParams.exclude,
98
- comparatorIndex: 0,
99
- operatorIndex: 4,
100
- right: filterParams.comparisons[1].value.toString(),
101
- left: filterParams.comparisons[0].value.toString(),
102
- };
103
- }
104
- } else {
105
- const comparison = filterParams.comparisons[0];
106
- if (comparison.operation === NUMERICAL_OPERATIONS.LT) {
107
- return {
108
- exclude: filterParams.exclude,
109
- comparatorIndex: 0,
110
- operatorIndex: 0,
111
- left: '',
112
- right: comparison.value.toString(),
113
- };
114
- } else if (comparison.operation === NUMERICAL_OPERATIONS.GT) {
115
- return {
116
- exclude: filterParams.exclude,
117
- comparatorIndex: 0,
118
- operatorIndex: 1,
119
- left: comparison.value.toString(),
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
- exclude: false,
152
- comparatorIndex: 0,
153
- operatorIndex: 0,
154
- left: '',
155
- right: '',
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 initialState = filterParamsToInitialState(props.filterParams);
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
- const [comparatorIndex, setComparatorIndex] = React.useState(
166
- initialState.comparatorIndex,
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 [operatorIndex, setOperatorIndex] = React.useState(
169
- initialState.operatorIndex,
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
- React.useEffect(() => {
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
- React.useEffect(() => {
214
- switch (operatorIndex) {
215
- case 4:
216
- default:
217
- break;
218
- case 1:
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
- }, [operatorIndex]);
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
- switch (operatorIndex) {
236
- case 0: {
237
- const value = parseFloat(right);
238
- const operation = NUMERICAL_OPERATIONS.LT;
239
- props.setFilter({
240
- comparisons: [{value, operation}],
241
- description: `< ${value}`,
242
- exclude,
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(left);
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.compact}
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
- {isRange && (
339
- <ButtonGroup
340
- size={SIZE.compact}
341
- mode={MODE.radio}
342
- selected={operatorIndex}
343
- onClick={(_, index) => setOperatorIndex(index)}
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: ({$theme}) => ({marginBottom: $theme.sizing.scale500}),
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
- <Button
351
- type="button"
352
- overrides={{BaseButton: {style: {width: '100%'}}}}
353
- >
354
- &#60;
355
- </Button>
356
- <Button
357
- type="button"
358
- overrides={{BaseButton: {style: {width: '100%'}}}}
359
- >
360
- &#62;
361
- </Button>
362
- <Button
363
- type="button"
364
- overrides={{BaseButton: {style: {width: '100%'}}}}
365
- >
366
- &#8804;
367
- </Button>
368
- <Button
369
- type="button"
370
- overrides={{BaseButton: {style: {width: '100%'}}}}
371
- >
372
- &#8805;
373
- </Button>
374
- <Button
375
- type="button"
376
- overrides={{BaseButton: {style: {width: '100%'}}}}
377
- >
378
- &#61;
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
- size={INPUT_SIZE.compact}
398
- overrides={{Root: {style: {width: isRange ? '152px' : '100%'}}}}
399
- disabled={leftDisabled}
400
- inputRef={leftInputRef}
401
- value={left}
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
- setLeft(event.target.value);
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
- size={INPUT_SIZE.compact}
412
- overrides={{Root: {style: {width: '152px'}}}}
413
- disabled={rightDisabled}
414
- inputRef={rightInputRef}
415
- value={right}
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
- setRight(event.target.value);
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 included = params.comparisons.some(c => {
484
- const left = roundToFixed(data, normalizedOptions.precision);
485
- const right = roundToFixed(c.value, normalizedOptions.precision);
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
  },