import React, { useEffect, useState } from 'react'; import { Range, getTrackBackground } from 'react-range'; import classNames from 'classnames'; import './RangeSlider.css'; import { useDebounce } from '../../../utils/hooks'; import { convertToNumber, countDecimals, initSliderScale, initSliderTicks, pow10Fixed, validateValueInRange } from '../utils'; import * as d3 from 'd3'; export const renderTrack = (values: number[], domain: number[], colors: string[]) => { return ({ props, children }) => (
{children}
); }; export const renderThumb = () => { return ({ props, isDragged }) => (
); }; export const renderMark = ( step: number, tickMarks: any, domain: number[], isLogScale?: boolean, inclusiveTickBounds?: boolean ) => { return ({ props, index }) => { /** * Only render the number of ticks specificed in the ticks var (currently 5). * Otherwise, react-range will try to render a tick at each step * which is way too many. */ let tickValue: number; if (isLogScale) { tickValue = Math.pow(10, domain[0] + index * step); } else { tickValue = domain[0] + index * step; } let showTick = false; if (tickMarks.length === 2) { showTick = tickValue === domain[0] || tickValue === domain[1]; } else if (tickMarks.indexOf(tickValue) > -1) { showTick = true; } if (showTick) { return (
= values[0] && tickValue <= values[1] ? '#3273dc' : '#ccc', }} /> {tickValue} {inclusiveTickBounds && tickValue === tickMarks[tickMarks.length - 1] && '+'}
); } else { return null; } }; }; export interface RangeSliderProps { /** * The ID used to identify this component in Dash callbacks */ id?: string; /** * Dash-assigned callback that should be called whenever any of the * properties change */ setProps?: (value: any) => any; /** * Class name(s) to append to the component's default class. */ className?: string; /** * Array with the minimum and maximum possible values. * If using isLogScale, these values will be interpreted as exponents and will be transformed to be `10^x`. */ domain: number[]; /** * Value of the slider. * If using isLogScale, the display value of the slider will be `10^props.value` */ value?: number | string; /** * Number by which the slider handles should move with each step. * Defaults to 1. */ step?: number; /** * Use a logarithmic scale for the slider. * Domain values will be interpreted as exponents and will be transformed to be `10^x`. * So a domain of `[-1, 2]` will yields a range of `[0.01, 100]`. * Note that when using a log scale, the `value` prop will always be the pre-transformed value. */ isLogScale?: boolean; /** * Number of milliseconds that should pass between typing into the slider * number input and the slider handles updating. */ debounce?: number; /** * Number of ticks to show on the slider scale. * Note that D3 will automatically convert this number to a multiple of 1, 2, 5, or 10. * Set to 2 to only include ticks at the min and max bounds of the scale. */ ticks?: number | null; /** * Set to true to display a "+" with the upper bound tick (e.g. "100+"). * Use this to indicate that the upper bound is inclusive (e.g. 100 or more). */ inclusiveTickBounds?: boolean; /** * Function to call when slider values change. */ onChange?: (values: number[]) => void; } /** * Slider input for selecting a single value in a range */ export const RangeSlider: React.FC = ({ domain, value = domain[0], step = 1, isLogScale = false, debounce = isLogScale ? 1000 : 500, ticks = 5, ...otherProps }) => { const props = { domain, value, step, isLogScale, debounce, ticks, ...otherProps }; const [values, setValues] = useState(() => { return [validateValueInRange(convertToNumber(props.value), props.domain[0], props.domain[1])]; }); const [inputValue, setInputValue] = useState(values[0].toString()); const [inputValueToDebounce, setInputValueToDebounce] = useState(inputValue); const debouncedInputValue: string = props.debounce ? useDebounce(inputValueToDebounce, props.debounce) : inputValue; const decimals = countDecimals(props.step); const scale = initSliderScale(props.domain, props.isLogScale); const tickMarks = initSliderTicks(props.ticks, props.domain, scale); const handleSliderChange = (vals: number[]) => { setValues(vals); const newInputValue = props.isLogScale ? pow10Fixed(vals[0]) : vals[0].toString(); setInputValue(newInputValue); }; const handleSliderFinalChange = (vals) => { if (props.setProps) { props.setProps({ value: vals[0] }); } if (props.onChange) { props.onChange( vals.map((val) => { return parseFloat(val.toFixed(decimals)); }) ); } }; const handleInputChange = (e) => { setInputValue(e.target.value); setInputValueToDebounce(e.target.value); }; const validateDebouncedInputValue = () => { let validatedInputValue: number; let validValue: any; /** * If using a log scale, convert the input value back into * an exponent and pass that to the slider. */ if (props.isLogScale) { validatedInputValue = validateValueInRange( parseFloat(debouncedInputValue), Math.pow(10, domain[0]), Math.pow(10, domain[1]) ); validValue = Math.log10(validatedInputValue).toFixed(decimals); } else { validatedInputValue = validateValueInRange( parseFloat(debouncedInputValue), domain[0], domain[1] ); validValue = validatedInputValue; } if (validValue !== values[0]) { setValues([validValue]); setInputValue(validatedInputValue.toString()); handleSliderFinalChange([validValue]); } }; useEffect(() => { validateDebouncedInputValue(); }, [debouncedInputValue]); useEffect(() => { handleSliderChange([ validateValueInRange(convertToNumber(props.value), props.domain[0], props.domain[1]) ]); }, [props.value]); return (
); };