function NumericalFilter()

in src/data-table/column-numerical.tsx [190:468]


function NumericalFilter(props) {
  const [css, theme] = useStyletron();
  const locale = React.useContext(LocaleContext);

  const precision = props.options.precision;

  // The state handling of this component could be refactored and cleaned up if we used useReducer.
  const initialState = React.useMemo(() => {
    return (
      props.filterParams || {
        exclude: false,
        excludeKind: 'range',
        comparatorIndex: 0,
        lowerValue: null,
        upperValue: null,
      }
    );
  }, [props.filterParams]);

  const [exclude, setExclude] = React.useState(initialState.exclude);

  // the api of our ButtonGroup forces these numerical indexes...
  // TODO look into allowing semantic names, similar to the radio component. Tricky part would be backwards compat
  const [comparatorIndex, setComparatorIndex] = React.useState(() => {
    switch (initialState.excludeKind) {
      case 'value':
        return 1;
      case 'range':
      default:
        // fallthrough
        return 0;
    }
  });

  // We use the d3 function to get the extent as it's a little more robust to null, -Infinity, etc.
  const [min, max] = React.useMemo(() => extent(props.data), [props.data]);

  const [lv, setLower] = React.useState<number>(() =>
    roundToFixed(initialState.lowerValue || min, precision)
  );
  const [uv, setUpper] = React.useState<number>(() =>
    roundToFixed(initialState.upperValue || max, precision)
  );

  // We keep a separate value for the single select, to give a user the ability to toggle between
  // the range and single values without losing their previous input.
  const [sv, setSingle] = React.useState<number>(() =>
    roundToFixed(initialState.lowerValue || median(props.data), precision)
  );

  // This is the only conditional which we want to use to determine
  // if we are in range or single value mode.
  // Don't derive it via something else, e.g. lowerValue === upperValue, etc.
  const isRange = comparatorIndex === 0;

  const excludeKind = isRange ? 'range' : 'value';

  // while the user is inputting values, we take their input at face value,
  // if we don't do this, a user can't input partial numbers, e.g. "-", or "3."
  const [focused, setFocus] = React.useState(false);
  const [inputValueLower, inputValueUpper] = React.useMemo(() => {
    if (focused) {
      return [isRange ? lv : sv, uv];
    }

    // once the user is done inputting.
    // we validate then format to the given precision
    let l = isRange ? lv : sv;
    // @ts-expect-error todo(ts-migration) TS2322 Type 'string | number | undefined' is not assignable to type 'number'.
    l = validateInput(l) ? l : min;
    let h = validateInput(uv) ? uv : max;

    // @ts-expect-error todo(ts-migration) TS2345 Argument of type 'string | number | undefined' is not assignable to parameter of type 'number'.
    return [roundToFixed(l, precision), roundToFixed(h, precision)];
  }, [isRange, focused, sv, lv, uv, precision]);

  // We have our slider values range from 1 to the bin size, so we have a scale which
  // takes in the data driven range and maps it to values the scale can always handle
  const sliderScale = React.useMemo(
    () =>
      scaleLinear()
        // @ts-expect-error todo(ts-migration) TS2345 Argument of type '(string | undefined)[]' is not assignable to parameter of type 'Iterable<NumberValue>'.
        .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
        .clamp(true),
    [min, max]
  );

  let sliderValue = isRange
    ? [sliderScale(inputValueLower), sliderScale(inputValueUpper)]
    : [sliderScale(inputValueLower)];

  // keep the slider happy by sorting the two values
  if (isRange && sliderValue[0] > sliderValue[1]) {
    sliderValue = [sliderValue[1], sliderValue[0]];
  }

  return (
    <FilterShell
      exclude={exclude}
      onExcludeChange={() => setExclude(!exclude)}
      excludeKind={excludeKind}
      onApply={() => {
        if (isRange) {
          // @ts-expect-error todo(flow->ts)
          const lowerValue = parseFloat(inputValueLower);
          // @ts-expect-error todo(flow->ts)
          const upperValue = parseFloat(inputValueUpper);
          props.setFilter({
            description: `≥ ${lowerValue} and ≤ ${upperValue}`,
            exclude: exclude,
            lowerValue,
            upperValue,
            excludeKind,
          });
        } else {
          // @ts-expect-error todo(flow->ts)
          const value = parseFloat(inputValueLower);
          props.setFilter({
            description: `= ${value}`,
            exclude: exclude,
            lowerValue: inputValueLower,
            upperValue: inputValueLower,
            excludeKind,
          });
        }

        props.close();
      }}
    >
      <ButtonGroup
        size={SIZE.mini}
        mode={MODE.radio}
        selected={comparatorIndex}
        onClick={(_, index) => setComparatorIndex(index)}
        overrides={{
          Root: {
            style: ({ $theme }) => ({ marginBottom: $theme.sizing.scale300 }),
          },
        }}
      >
        <Button
          type="button"
          overrides={{ BaseButton: { style: { width: '100%' } } }}
          aria-label={locale.datatable.numericalFilterRange}
        >
          {locale.datatable.numericalFilterRange}
        </Button>
        <Button
          type="button"
          overrides={{ BaseButton: { style: { width: '100%' } } }}
          aria-label={locale.datatable.numericalFilterSingleValue}
        >
          {locale.datatable.numericalFilterSingleValue}
        </Button>
      </ButtonGroup>

      <Histogram
        data={props.data}
        lower={inputValueLower}
        upper={inputValueUpper}
        isRange={isRange}
        exclude={exclude}
        precision={props.options.precision}
      />

      <div className={css({ display: 'flex', justifyContent: 'space-between' })}>
        <Slider
          // The slider throws errors when switching between single and two values
          // when it tries to read getThumbDistance on a thumb which is not there anymore
          // if we create a new instance these errors are prevented.
          key={isRange.toString()}
          min={1}
          max={MAX_BIN_COUNT}
          value={sliderValue}
          onChange={({ value }) => {
            if (!value) {
              return;
            }
            // we convert back from the slider scale to the actual data's scale
            if (isRange) {
              const [lowerValue, upperValue] = value;
              setLower(sliderScale.invert(lowerValue));
              setUpper(sliderScale.invert(upperValue));
            } else {
              const [singleValue] = value;
              setSingle(sliderScale.invert(singleValue));
            }
          }}
          overrides={{
            InnerThumb: function InnerThumb({ $value, $thumbIndex }) {
              return <React.Fragment>{$value[$thumbIndex]}</React.Fragment>;
            },
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            TickBar: ({ $min, $max }) => null, // we don't want the ticks
            ThumbValue: () => null,
            Root: {
              style: () => ({
                // Aligns the center of the slider handles with the histogram bars
                width: 'calc(100% + 14px)',
                margin: '0 -7px',
              }),
            },
            InnerTrack: {
              style: () => {
                if (!isRange) {
                  return {
                    // For range selection we use the color as is, but when selecting the single value,
                    // we don't want the track standing out, so mute its color
                    background: theme.colors.backgroundSecondary,
                  };
                }
              },
            },
            Thumb: {
              style: () => ({
                // Slider handles are small enough to visually be centered within each histogram bar
                height: '18px',
                width: '18px',
                fontSize: '0px',
              }),
            },
          }}
        />
      </div>
      <div
        className={css({
          display: 'flex',
          marginTop: theme.sizing.scale400,
          // This % gap is visually appealing given the filter box width
          gap: '30%',
          justifyContent: 'space-between',
        })}
      >
        {/* @ts-expect-error todo(ts-migration) TS2769 No overload matches this call. */}
        <Input
          min={min}
          max={max}
          size={INPUT_SIZE.mini}
          overrides={{ Root: { style: { width: '100%' } } }}
          value={inputValueLower}
          onChange={(event) => {
            if (validateInput(event.target.value)) {
              isRange
                ? // @ts-expect-error - we know it is a number by now
                  setLower(event.target.value)
                : // @ts-expect-error - we know it is a number by now
                  setSingle(event.target.value);
            }
          }}
          onFocus={() => setFocus(true)}
          onBlur={() => setFocus(false)}
        />
        {isRange && (
          // @ts-expect-error todo(ts-migration) TS2769 No overload matches this call.
          <Input
            min={min}
            max={max}
            size={INPUT_SIZE.mini}
            overrides={{
              Input: { style: { textAlign: 'right' } },
              Root: { style: { width: '100%' } },
            }}
            value={inputValueUpper}
            onChange={(event) => {
              if (validateInput(event.target.value)) {
                // @ts-expect-error - we know it is a number by now
                setUpper(event.target.value);
              }
            }}
            onFocus={() => setFocus(true)}
            onBlur={() => setFocus(false)}
          />
        )}
      </div>
    </FilterShell>
  );
}