From 88812c6b235bfe3261f0099e9d71bc37ff436f23 Mon Sep 17 00:00:00 2001 From: AdzicMilos Date: Fri, 9 Feb 2024 12:42:52 +0100 Subject: [PATCH] feat: add range slider with custom scale --- .../index.stories.mdx | 31 +++- .../rangeFilterInputWithSlider/index.tsx | 41 +++-- .../rangeSlider/RangeSliderWithChart.tsx | 120 ++------------- .../rangeSlider/RangeSliderWithScale.tsx | 142 ++++++++++++++++++ 4 files changed, 214 insertions(+), 120 deletions(-) create mode 100644 src/components/rangeSlider/RangeSliderWithScale.tsx diff --git a/src/components/rangeFilterInputWithSlider/index.stories.mdx b/src/components/rangeFilterInputWithSlider/index.stories.mdx index c2fc29bdf..4247bdbff 100644 --- a/src/components/rangeFilterInputWithSlider/index.stories.mdx +++ b/src/components/rangeFilterInputWithSlider/index.stories.mdx @@ -45,11 +45,11 @@ return ( ); }; -## Overview +## With histograms + +## With custom range slider scale + + + + {Template.bind({})} + + diff --git a/src/components/rangeFilterInputWithSlider/index.tsx b/src/components/rangeFilterInputWithSlider/index.tsx index 5b126d302..53a433c19 100644 --- a/src/components/rangeFilterInputWithSlider/index.tsx +++ b/src/components/rangeFilterInputWithSlider/index.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; +import RangeSliderWithScale from '../rangeSlider/RangeSliderWithScale'; import RangeSliderWithChart, { Facet, NumericMinMaxValue, @@ -21,8 +22,16 @@ type ChangeRangeInputWithSliderCallback = { changeType?: 'inputfield' | 'slider'; } & ChangeCallback; +type RangeSliderProps = { + facets?: Array; + rangeSliderScale?: Array; + chartHeight?: string; +} & ( + | { facets: Array; chartHeight?: string; rangeSliderScale?: never } + | { rangeSliderScale: Array; facets?: never; chartHeight?: never } +); + export type Props = { - facets: Array; from: RangeFilterInputField; onChange: ( event: ChangeRangeInputWithSliderCallback, @@ -32,13 +41,14 @@ export type Props = { ) => void; to: RangeFilterInputField; unit?: string; - chartHeight?: string; -} & PickedNumberInputProps; +} & RangeSliderProps & + PickedNumberInputProps; function RangeFilterInputWithSlider< NameFrom extends string, NameTo extends string, >({ + rangeSliderScale, facets, unit, onChange, @@ -107,14 +117,23 @@ function RangeFilterInputWithSlider< return ( - + {facets ? ( + + ) : null} + {rangeSliderScale ? ( + + ) : null} = ({ onSliderRelease, chartHeight = '3xl', }) => { - const [startRange, setStartRange] = useState(null); - const sortedFacetsByFromKey = facets.sort((a, b) => a.from - b.from); - const scale = sortedFacetsByFromKey.map(({ from }) => from); - const toIndex = (value: number) => { - const closestValue = scale.reduce((prev, curr) => { - return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; - }, 0); - - return scale.indexOf(closestValue); - }; - - const toValue = (index: number) => { - if (index === scale.length) { - return null; - } - - return scale[index]; - }; - - const toMinMax = ( - minIndex: number, - maxIndex: number, - previousSelection: NumericMinMaxValue, - ): NumericMinMaxValue => ({ - min: minIndex ? toValue(minIndex) : null, - max: maxIndex ? toValue(maxIndex) : previousSelection.max, - }); - - const toRange = ({ min, max }: NumericMinMaxValue) => { - const maxValue = max ? toIndex(max) : scale.length; - const minValue = min ? toIndex(min) : 0; - - const range: number[] = [minValue, maxValue]; - const sortedRange = [...range].sort((a, b) => a - b); - - return JSON.stringify(range) === JSON.stringify(sortedRange) - ? range - : [minValue, minValue]; - }; - - const getChangedThumb = ( - initial: number[], - current: number[], - ): 'max' | 'min' | null => { - const [initialMinIndex, initialMaxIndex] = initial; - const [currentMinIndex, currentMaxIndex] = current; - if ( - initialMinIndex === currentMinIndex && - initialMaxIndex === currentMaxIndex - ) { - return null; - } - - return initialMinIndex !== currentMinIndex ? 'min' : 'max'; - }; - - const getChangeEvent = ([ - newMinIndex, - newMaxIndex, - ]: number[]): ChangeCallback | null => { - const changedThumb = getChangedThumb( - startRange ? startRange : toRange(selection), - [newMinIndex, newMaxIndex], - ); - - if (!changedThumb) return null; - - return { - touched: changedThumb, - value: toMinMax(newMinIndex, newMaxIndex, selection), - }; - }; - - const handleChange = ( - newValues: number[], - callback: (event: ChangeCallback) => void, - ) => { - const changeEvent = getChangeEvent(newValues); - if (changeEvent) { - callback(changeEvent); - } - }; - return ( - <> - - - - handleChange(newValues, onSliderChange)} - onChangeEnd={(newValues) => { - const callback = (event: ChangeCallback) => { - onSliderRelease(event); - setStartRange(newValues); - }; - handleChange(newValues, callback); - }} - onChangeStart={setStartRange} - value={toRange(selection)} - /> - + ) => ( + + + + )} + /> ); }; diff --git a/src/components/rangeSlider/RangeSliderWithScale.tsx b/src/components/rangeSlider/RangeSliderWithScale.tsx new file mode 100644 index 000000000..f511bbd62 --- /dev/null +++ b/src/components/rangeSlider/RangeSliderWithScale.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; + +import RangeSlider from './'; + +export type NumericMinMaxValue = { + min: number | null | undefined; + max: number | null | undefined; +}; + +export type ChangeCallback = { + touched: 'min' | 'max'; + value: NumericMinMaxValue; +}; + +export type Facet = { + from: number; + to?: number | null; + value: number; +}; + +interface RangeSliderWithScaleProps { + scale: Array; + selection: NumericMinMaxValue; + onSliderChange: (event: ChangeCallback) => void; + onSliderRelease: (event: ChangeCallback) => void; + renderChart?: (range: number[]) => React.ReactNode; +} + +const RangeSliderWithScale: React.FC = ({ + scale, + selection, + onSliderChange, + onSliderRelease, + renderChart, +}) => { + const [startRange, setStartRange] = useState(null); + const sortedScale = scale.sort((a, b) => a - b); + + const toIndex = (value: number) => { + const closestValue = sortedScale.reduce((prev, curr) => { + return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; + }, 0); + + return sortedScale.indexOf(closestValue); + }; + + const toValue = (index: number) => { + if (index === sortedScale.length) { + return null; + } + + return sortedScale[index]; + }; + + const toMinMax = ( + minIndex: number, + maxIndex: number, + previousSelection: NumericMinMaxValue, + ): NumericMinMaxValue => ({ + min: minIndex ? toValue(minIndex) : null, + max: maxIndex ? toValue(maxIndex) : previousSelection.max, + }); + + const toRange = ({ min, max }: NumericMinMaxValue) => { + const maxValue = max ? toIndex(max) : sortedScale.length; + const minValue = min ? toIndex(min) : 0; + + const range: number[] = [minValue, maxValue]; + const sortedRange = [...range].sort((a, b) => a - b); + + return JSON.stringify(range) === JSON.stringify(sortedRange) + ? range + : [minValue, minValue]; + }; + + const getChangedThumb = ( + initial: number[], + current: number[], + ): 'max' | 'min' | null => { + const [initialMinIndex, initialMaxIndex] = initial; + const [currentMinIndex, currentMaxIndex] = current; + if ( + initialMinIndex === currentMinIndex && + initialMaxIndex === currentMaxIndex + ) { + return null; + } + + return initialMinIndex !== currentMinIndex ? 'min' : 'max'; + }; + + const getChangeEvent = ([ + newMinIndex, + newMaxIndex, + ]: number[]): ChangeCallback | null => { + const changedThumb = getChangedThumb(startRange || toRange(selection), [ + newMinIndex, + newMaxIndex, + ]); + + if (!changedThumb) return null; + + return { + touched: changedThumb, + value: toMinMax(newMinIndex, newMaxIndex, selection), + }; + }; + + const handleChange = ( + newValues: number[], + callback: (event: ChangeCallback) => void, + ) => { + const changeEvent = getChangeEvent(newValues); + if (changeEvent) { + callback(changeEvent); + } + }; + + return ( + <> + {renderChart ? renderChart(toRange(selection)) : null} + handleChange(newValues, onSliderChange)} + onChangeEnd={(newValues) => { + const callback = (event: ChangeCallback) => { + onSliderRelease(event); + setStartRange(newValues); + }; + handleChange(newValues, callback); + }} + onChangeStart={setStartRange} + value={toRange(selection)} + /> + + ); +}; + +export default RangeSliderWithScale;