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