Thomas Andrew Hansen
Self Develpoment
Skiing
Tech
Tutorials
React Double Range Slider Component with Rheostat Graph

Are you tired of using boring, ordinary range sliders to filter through your favorite ski resorts? Well, look no further because we have the solution for you: the Double Range Slider Component with Rheostat Graph.

With this state-of-the-art slider, not only can you filter through your resorts by score, but you can also visually track your selected range with a fancy Rheostat graph. And if that wasn't enough, we've even added a little bit of magic to make sure the sliders always have a minimum distance of 8 between them. No more pesky overlapping sliders!

DoubleRangeSliderwithGraph.gif

Importing the component

First, let's start by importing all the necessary libraries and components:

import React, { useState } from 'react';
import { CFormRange, CFormInput, CFormLabel, CCol, CRow } from '@coreui/react';
import PropTypes from 'prop-types';
import RangeRheostatGraph from '../Rheostat/RangeRheostatGraph';

Setting the initial state

Next, we'll use the useState hook to set the initial values for the lower and upper range sliders and their corresponding input fields:

const [lowerValue, setLowerValue] = useState(50);
const [upperValue, setUpperValue] = useState(500);
const [lowerFieldValue, setLowerFieldValue] = useState(50);
const [upperFieldValue, setUpperFieldValue] = useState(500);

Calculating slider positions and width

Then, we have some clever math to calculate the width and position of the range between the sliders:

const widthSpanPercent = 100 / (sliderMax - sliderMin);
const leftPosition = (lowerValue - sliderMin) * widthSpanPercent;
const rightPosition = (upperValue - sliderMin) * widthSpanPercent;
const rangeWidth = (rightPosition + sliderMin) - (leftPosition + sliderMin);

Event handlers

Now comes the fun part: the event handlers! We have two functions to handle the change of each slider and make sure the other slider adjusts accordingly:

function setLowerSlider(targetUpperValue) {
  if ((lowerValue > targetUpperValue - 8) || (targetUpperValue < lowerValue + 8)) {
    if (targetUpperValue >= sliderMax) {
      setLowerValue(parseInt(sliderMax, 10) - 8);
      setLowerFieldValue(parseInt(sliderMax, 10) - 8);
    }
    setLowerValue(Math.max(targetUpperValue - 8, sliderMin));
    setLowerFieldValue(Math.max(targetUpperValue - 8, sliderMin));
    if (targetUpperValue <= sliderMin + 8) {
      setUpperValue(sliderMin + 8);
      setUpperFieldValue(sliderMin + 8);
    }
  }
}

  function setUpperSlider(targetLowerValue) {
    if ((targetLowerValue > upperValue - 8) || (upperValue < targetLowerValue + 8)) {
      if (targetLowerValue < sliderMax - 8) {
        setUpperValue(targetLowerValue + 8);
        setUpperFieldValue(targetLowerValue + 8);
      }
      setUpperValue(Math.min(targetLowerValue + 8, sliderMax));
      setUpperFieldValue(Math.min(targetLowerValue + 8, sliderMax));
      if (targetLowerValue >= sliderMax - 8) {
        setLowerValue(sliderMax - 8);
        setLowerFieldValue(sliderMax - 8);
      }
    }
  }

Event handlers (continued)

We also have a function to limit the input values to within the specified range:

function limitInput(target) {
  if (target > sliderMax) {
    return parseInt(sliderMax, 10);
  } if (target < sliderMin) {
    return parseInt(sliderMin, 10);
  }
  return parseInt(target, 10);
}

Now, let's create the event handlers for when the user changes the values of the range sliders:

function handleLowerRangeSliderChange(target) {
  setLowerValue(limitInput(target));
  setLowerFieldValue(limitInput(target));
  setUpperSlider(parseInt(target, 10));
}

function handleRangeSliderUpperChange(target) {
  setUpperValue(limitInput(target));
  setUpperFieldValue(limitInput(target));
  setLowerSlider(parseInt(target, 10));
}

Rendering the component

Finally, we can render the Double Range Slider Component with Rheostat Graph and pass in the necessary props:

 return (
    <div className={`double-range-slider ${className}`}>
      {useGraph
        && (
        <RangeRheostatGraph
          scoreType={scoreType}
          resortList={resortList}
          leftPosition={leftPosition}
          rightPosition={rightPosition}
          sliderMin={sliderMin}
          sliderMax={sliderMax}
          upperValue={upperValue}
          lowerValue={lowerValue}
          graphTickerQuantity={tickerQuantity}
        />
        )}
      <div className="range-slider-wrap">
        <div className="range-slider-range" style={{ left: `${leftPosition.toString()}%`, width: `${rangeWidth.toString()}%` }} />
      </div>
      <CFormRange
        id="lower"
        className="range-slider range-slider-lower"
        steps={1}
        min={sliderMin}
        max={sliderMax}
        onChange={(e) => handleLowerRangeSliderChange(e.target.value)}
        value={lowerValue}
      />
      <CFormRange
        id="upper"
        className="range-slider range-slider-upper"
        steps={1}
        min={sliderMin}
        max={sliderMax}
        onChange={(e) => handleRangeSliderUpperChange(e.target.value)}
        value={upperValue}
      />
      <CRow className="mt-5 d-flex justify-content-between p-3">
        <CCol xl={5} lg={5} xs={5}>
          <div className="input-wrap">
            <CFormLabel htmlFor="lowerInput" className="w-100 label-inside-input resort-card__small-label">
              Min
            </CFormLabel>
            <CFormInput
              className="lower-input label-inside-input-padding"
              type="number"
              id="lowerInput"
              label="Min"
              placeholder="0"
              min={sliderMin}
              max={sliderMax}
              onBlur={
              (e) => handleLowerRangeSliderChange(e.target.value)
            }
              onChange={(e) => setLowerFieldValue(parseInt(e.target.value, 10))}
              value={lowerFieldValue}
            />
          </div>
        </CCol>
        <CCol xl={2} lg={2} xs={2} className="d-flex justify-content-center align-items-center">
          -
        </CCol>
        <CCol xl={5} lg={5} xs={5}>
          <div className="input-wrap">
            <CFormLabel htmlFor="upperInput" className="w-100 label-inside-input resort-card__small-label">
              Max
            </CFormLabel>
            <CFormInput
              className="upper-input label-inside-input-padding"
              type="number"
              id="upperInput"
              label="Max"
              placeholder="0"
              min={sliderMin}
              max={sliderMax}
              onBlur={
              (e) => handleRangeSliderUpperChange(e.target.value)
            }
              onChange={(e) => setUpperFieldValue(parseInt(e.target.value, 10))}
              value={upperFieldValue}
            />
          </div>
        </CCol>
      </CRow>
    </div>
  );

Building the Rheostat Graph

The Rheostat graph is an optional feature of the Double Range Slider Component that allows you to visually track the selected range of values. Here's how to build it:

Importing dependencies

First, let's import all the necessary libraries and components:

import React from 'react';
import PropTypes from 'prop-types';
import { CSvg, CRect, CText } from '@coreui/react';

Rheostat Graph Props

Here are the props that can be passed to the Double Range Slider Component with Rheostat Graph:

  • resortList: an array of objects representing the ski resorts to be displayed in the Rheostat graph
  • sliderMin: the minimum value for the range sliders
  • sliderMax: the maximum value for the range sliders
  • scoreType: the property of the resort objects to use for the Rheostat graph
  • useGraph: a boolean value to toggle the display of the Rheostat graph
  • tickerQuantity: the number of tick marks to display on the Rheostat graph
  • className: a string value to specify a custom class for the component's outer div

Calculating tick mark positions

Then, we'll need to calculate the positions of the tick marks on the Rheostat graph:

 resortList.forEach((resort) => {
    let avg = 0;
    resort.ratings.filter((rating) => rating.title === scoreType).forEach((score) => {
      avg += score.value;
    });
    avg /= resort.ratings.filter((rating) => rating.title === scoreType).length;
    resortAverages.push(avg);
  });

  // Get maxResortInSpan:
  // Finds the most amount of resorts in a single ticker, so we can utilize full height.
  tickerQuantity.forEach((tick) => {
    const spanLow = spanPercent * tick;
    const spanHigh = spanPercent * tick + spanPercent;

    if (resortAverages.some((el) => el >= spanLow && el < spanHigh)) {
      let resortQty = 0;

      resortAverages.forEach((avg) => {
        if (avg >= spanLow && avg <= spanHigh) {
          resortQty += 1;
        }
        if (maxResortInSpan < resortQty) {
          setMaxResortInSpan(resortQty);
        }
      });
    }
  });

  // Set initial tickers
  tickerQuantity.forEach((tick) => {
    const spanLow = spanPercent * tick;
    const spanHigh = spanPercent * tick + spanPercent;
    let resortQty = 0;

    if (resortAverages.some((el) => el >= spanLow && el < spanHigh)) {
      resortQty = 0;

      resortAverages.forEach((avg) => {
        if (avg >= spanLow && avg <= spanHigh) {
          resortQty += 1;
        }
      });
    }

    height = (100 / maxResortInSpan) * resortQty;

    tickers.push({
      key: tick,
      height: height > 1 ? height : 5,
      backgroundColor: 'light',
    });
  });

  // Change color of graph ticks based on the sliders input.
  if (upperValue < sliderMax) {
    const tickersArray = tickers;

    tickers.forEach((tick) => {
      const tickObj = tick;
      if ((tick.key * spanPercent) > rightPosition) {
        tickObj.backgroundColor = 'dark';
      } else {
        tickObj.backgroundColor = 'light';
      }
      if (tick.key !== tickObj.key) {
        tickersArray.push(tickObj);
      }
    });
    tickers = tickersArray;
  }
  if (lowerValue > sliderMin) {
    const tickersArray = tickers;

    tickers.forEach((tick) => {
      const tickObj = tick;
      if ((tick.key * spanPercent) < leftPosition) {
        tickObj.backgroundColor = 'dark';
      } else if ((tick.key * spanPercent) < rightPosition) {
        tickObj.backgroundColor = 'light';
      }
      if (tick.key !== tickObj.key) {
        tickersArray.push(tickObj);
      }
    });
    tickers = tickersArray;
  }

Rendering the graph

Now, let's finish rendering the rheostat component

  return (
    <div className="range-rheostat-graph">
      <div className="range-rheostat-graph__ticker-wrap d-flex justify-content-around">
        {tickers && tickers.map((ticker) => (
          <RheostatTicker
            key={ticker.key}
            value={ticker.height}
            backgroundColor={ticker.backgroundColor}
          />
        ))}
      </div>
    </div>
  );

That's it! You now know how to build a Double Range Slider Component with Rheostat Graph. This interactive and visually appealing component allows you to easily filter through a list of anything by your type and track your selected range with a fancy Rheostat graph. With just a few lines of code, you can add this impressive feature to your own projects and wow your users. Happy coding!