"React Hook" 160 lines of code to achieve dynamic and cool visual charts-Ranking

"React Hook" 160 lines of code to achieve dynamic and cool visual charts-Ranking

I saw a post while visiting the community one day:

react-dynamic-charts — A React Library for Visualizing Dynamic Data

This is a library written by a foreign tycoon in the code competition of his company summit:react-dynamic-charts used to create dynamic chart visualization based on dynamic data.

Its design is very flexible, allowing you to control every element and event inside. The method of use is also very simple, and the source code is very refined and worth learning.

But because it provides many APIs, it is not conducive to understanding the source code. So the following implementation is somewhat streamlined:

1. Prepare general utility functions

1. getRandomColor: random color

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color ='#';
  for (let i = 0; i <6; i++) {
    color += letters[Math.floor(Math.random() * 16)]
  }
  return color;
};

2. translateY: fill the Y axis offset

const translateY = (value) => {
  return `translateY(${value}px)`;
}

2. Use useState Hook to declare state variables

We start to write componentsDynamicBarChart

const DynamicBarChart = (props) => {
  const [dataQueue, setDataQueue] = useState([]);
  const [activeItemIdx, setActiveItemIdx] = useState(0);
  const [highestValue, setHighestValue] = useState(0);
  const [currentValues, setCurrentValues] = useState({});
  const [firstRun, setFirstRun] = useState(false);
 //Other codes...
  }

1. Simple understanding of useState:

const [attribute, method of operating attribute] = useState (default value);

2. Variable analysis

  • dataQueue: The original data array of the current operation
  • activeItemIdx: What is the "frame"
  • highestValue: Data value of "Top"
  • currentValues: Data array used for rendering after processing
  • firstRun: First dynamic rendering time

3. Internal operation method and corresponding useEffect

Please eat with notes

//Run dynamically~
function start () {
  if (activeItemIdx> 1) {
    return;
  }
  nextStep(true);
}
//Process the next frame of data
function setNextValues ​​() {
 //When there are no frames (that is, finished), stop rendering
  if (!dataQueue[activeItemIdx]) {
    iterationTimeoutHolder = null;
    return;
  }
 //Data array for each frame
  const roundData = dataQueue[activeItemIdx].values;
  const nextValues ​​= {};
  let highestValue = 0;
 //Process data for final rendering (various styles, colors)
  roundData.map((c) => {
    nextValues[c.id] = {
      ...c,
      color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
    };

    if (Math.abs(c.value)> highestValue) {
      highestValue = Math.abs(c.value);
    }

    return c;
  });

 //Attribute operation, trigger useEffect
  setCurrentValues(nextValues);
  setHighestValue(highestValue);
  setActiveItemIdx(activeItemIdx + 1);
}
//trigger the next step, loop
function nextStep (firstRun = false) {
  setFirstRun(firstRun);
  setNextValues();
}

Corresponding to useEffect

//Take the original data
useEffect(() => {
  setDataQueue(props.data);
}, []);
//trigger dynamic
useEffect(() => {
  start();
}, [dataQueue]);
//Set trigger dynamic interval
useEffect(() => {
  iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
  return () => {
    if (iterationTimeoutHolder) {
      window.clearTimeout(iterationTimeoutHolder);
    }
  };
}, [activeItemIdx]);

useEffect example:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);//Update only when count changes

Why should effectreturn a function?

This is effectan optional clearance mechanisms. Each effectcan return to a clear function. This way, the logic of adding and removing subscriptions can be put together.

4. Organize the data used to render the page

const keys = Object.keys(currentValues);
const {barGapSize, barHeight, showTitle} = props;
const maxValue = highestValue/0.85;
const sortedCurrentValues ​​= keys.sort((a, b) => currentValues[b].value-currentValues[a].value);
const currentItem = dataQueue[activeItemIdx-1] || {};
  • keys: Index of each group of data
  • maxValue: Maximum chart width
  • sortedCurrentValues: Sort each group of data, this item affects dynamic rendering.
  • currentItem: Raw data for each group

5. Start rendering the page...

The general logic is:

  1. According to different Props, the data after cyclic arrangement:sortedCurrentValues
  2. Calculate the width and return the labelbarvalue
  3. According to the calculated height, trigger transform.
<div className="live-chart">
{
<React.Fragment>
  {
    showTitle &&
    <h1>{currentItem.name}</h1>
  }
  <section className="chart">
    <div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
      {
        sortedCurrentValues.map((key, idx) => {
          const currentValueData = currentValues[key];
          const value = currentValueData.value
          let width = Math.abs((value/maxValue * 100));
          let widthStr;
          if (isNaN(width) || !width) {
            widthStr = '1px';
          } else {
            widthStr = `${width}%`;
          }

          return (
            <div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200/1000 }} key={`bar_${key}`}>
              <label>
                {
                  !currentValueData.label
                    ? key
                    : currentValueData.label
                }
              </label>
              <div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color ==='string'? currentValueData.color: `linear-gradient(to right, ${currentValueData.color. join(',')})` }}/>
              <span className="value" style={{ color: typeof currentValueData.color ==='string'? currentValueData.color: currentValueData.color[0] }}>{currentValueData.value}</span>
            </div>
          );
        })
      }
    </div>
  </section>
</React.Fragment>
}
</div>

6. Define general propTypes and defaultProps:

DynamicBarChart.propTypes = {
  showTitle: PropTypes.bool,
  iterationTimeout: PropTypes.number,
  data: PropTypes.array,
  startRunningTimeout: PropTypes.number,
  barHeight: PropTypes.number,
  barGapSize: PropTypes.number,
  baseline: PropTypes.number,
};

DynamicBarChart.defaultProps = {
  showTitle: true,
  iterationTimeout: 200,
  data: [],
  startRunningTimeout: 0,
  barHeight: 50,
  barGapSize: 20,
  baseline: null,
};

export {
  DynamicBarChart
};

7. How to use

import React, {Component} from "react";

import {DynamicBarChart} from "./DynamicBarChart";

import helpers from "./helpers";
import mocks from "./mocks";

import "react-dynamic-charts/dist/index.css";

export default class App extends Component {
  render() {
    return (
      <DynamicBarChart
            barGapSize={10}
            data={helpers.generateData(100, mocks.defaultChart, {
              prefix: "Iteration"
            })}
            iterationTimeout={100}
            showTitle={true}
            startRunningTimeout={2500}
         />
      )
  }
}

1. Batch generate mock data

helpers.js:

function getRandomNumber(min, max) {
  return Math.floor(Math.random() * (max-min + 1) + min);
};

function generateData(iterations = 100, defaultValues ​​= [], namePrefix = {}, maxJump = 100) {
  const arr = [];
  for (let i = 0; i <= iterations; i++) {
    const values ​​= defaultValues.map((v, idx) => {
      if (i === 0 && typeof v.value ==='number') {
        return v;
      }
      return {
        ...v,
        value: i === 0? this.getRandomNumber(1, 1000): arr[i-1].values[idx].value + this.getRandomNumber(0, maxJump)
      }
    });
    arr.push({
      name: `${namePrefix.prefix ||''} ${(namePrefix.initialValue || 0) + i}`,
      values
    });
  }
  return arr;
};

export default {
  getRandomNumber,
  generateData
}

mocks.js:

import helpers from'./helpers';
const defaultChart = [
  {
    id: 1,
    label:'Google',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 2,
    label:'Facebook',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 3,
    label:'Outbrain',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 4,
    label:'Apple',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 5,
    label:'Amazon',
    value: helpers.getRandomNumber(0, 50)
  },
];
export default {
  defaultChart,
}

A beggar version of the dynamic rankings visualization is done well.

8. Complete code

import React, {useState, useEffect} from'react';
import PropTypes from'prop-types';
import'./styles.scss';

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color ='#';
  for (let i = 0; i <6; i++) {
    color += letters[Math.floor(Math.random() * 16)]
  }
  return color;
};

const translateY = (value) => {
  return `translateY(${value}px)`;
}

const DynamicBarChart = (props) => {
  const [dataQueue, setDataQueue] = useState([]);
  const [activeItemIdx, setActiveItemIdx] = useState(0);
  const [highestValue, setHighestValue] = useState(0);
  const [currentValues, setCurrentValues] = useState({});
  const [firstRun, setFirstRun] = useState(false);
  let iterationTimeoutHolder = null;

  function start () {
    if (activeItemIdx> 1) {
      return;
    }
    nextStep(true);
  }

  function setNextValues ​​() {
    if (!dataQueue[activeItemIdx]) {
      iterationTimeoutHolder = null;
      return;
    }

    const roundData = dataQueue[activeItemIdx].values;
    const nextValues ​​= {};
    let highestValue = 0;
    roundData.map((c) => {
      nextValues[c.id] = {
        ...c,
        color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
      };

      if (Math.abs(c.value)> highestValue) {
        highestValue = Math.abs(c.value);
      }

      return c;
    });
    console.table(highestValue);

    setCurrentValues(nextValues);
    setHighestValue(highestValue);
    setActiveItemIdx(activeItemIdx + 1);
  }

  function nextStep (firstRun = false) {
    setFirstRun(firstRun);
    setNextValues();
  }

  useEffect(() => {
    setDataQueue(props.data);
  }, []);

  useEffect(() => {
    start();
  }, [dataQueue]);

  useEffect(() => {
    iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
    return () => {
      if (iterationTimeoutHolder) {
        window.clearTimeout(iterationTimeoutHolder);
      }
    };
  }, [activeItemIdx]);

  const keys = Object.keys(currentValues);
  const {barGapSize, barHeight, showTitle, data} = props;
  console.table('data', data);
  const maxValue = highestValue/0.85;
  const sortedCurrentValues ​​= keys.sort((a, b) => currentValues[b].value-currentValues[a].value);
  const currentItem = dataQueue[activeItemIdx-1] || {};

  return (
    <div className="live-chart">
      {
        <React.Fragment>
          {
            showTitle &&
            <h1>{currentItem.name}</h1>
          }
          <section className="chart">
            <div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
              {
                sortedCurrentValues.map((key, idx) => {
                  const currentValueData = currentValues[key];
                  const value = currentValueData.value
                  let width = Math.abs((value/maxValue * 100));
                  let widthStr;
                  if (isNaN(width) || !width) {
                    widthStr = '1px';
                  } else {
                    widthStr = `${width}%`;
                  }

                  return (
                    <div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200/1000 }} key={`bar_${key}`}>
                      <label>
                        {
                          !currentValueData.label
                            ? key
                            : currentValueData.label
                        }
                      </label>
                      <div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color ==='string'? currentValueData.color: `linear-gradient(to right, ${currentValueData.color. join(',')})` }}/>
                      <span className="value" style={{ color: typeof currentValueData.color ==='string'? currentValueData.color: currentValueData.color[0] }}>{currentValueData.value}</span>
                    </div>
                  );
                })
              }
            </div>
          </section>
        </React.Fragment>
      }
    </div>
  );
};

DynamicBarChart.propTypes = {
  showTitle: PropTypes.bool,
  iterationTimeout: PropTypes.number,
  data: PropTypes.array,
  startRunningTimeout: PropTypes.number,
  barHeight: PropTypes.number,
  barGapSize: PropTypes.number,
  baseline: PropTypes.number,
};

DynamicBarChart.defaultProps = {
  showTitle: true,
  iterationTimeout: 200,
  data: [],
  startRunningTimeout: 0,
  barHeight: 50,
  barGapSize: 20,
  baseline: null,
};

export {
  DynamicBarChart
};

styles.scss

.live-chart {
  width: 100%;
  padding: 20px;
  box-sizing: border-box;
  position: relative;
  text-align: center;
  h1 {
    font-weight: 700;
    font-size: 60px;
    text-transform: uppercase;
    text-align: center;
    padding: 20px 10px;
    margin: 0;
  }

  .chart { 
    position: relative;
    margin: 20px auto;
  }

  .chart-bars {
    position: relative;
    width: 100%;
  }

  .bar-wrapper {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(0);
    transition: transform 0.5s linear;
    padding-left: 200px;
    box-sizing: border-box;
    width: 100%;
    justify-content: flex-start;

    label {
      position: absolute;
      height: 100%;
      width: 200px;
      left: 0;
      padding: 0 10px;
      box-sizing: border-box;
      text-align: right;
      top: 50%;
      transform: translateY(-50%);
      font-size: 16px;
      font-weight: 700;
      display: flex;
      justify-content: flex-end;
      align-items: center;
    }

    .value {
      font-size: 16px;
      font-weight: 700;
      margin-left: 10px;
    }

    .bar {
      width: 0%;
      transition: width 0.5s linear;
    }
  }
}

Original project address: react-dynamic-charts: https://dsternlicht.github.io/react-dynamic-charts/

Conclusion

I have always been interested in realizing the visualization of dynamic rankings, but most of them are based on D3or echartsrealized. And this library not only breaks away from the graphics library, but also uses React 16new features. It also made me thoroughly understand the React Hookmagical effect.

❤️ After watching three things

If you think this content is quite inspiring for you, I would like to invite you to do three small favors for me:

  • Click "Watching" so that more people can see this content (If you like to watch it or not, it's all hooligans-_-)
  • Pay attention to the public account "Front-end adviser", and share original & excellent technical articles from time to time.
  • Add WeChat: huab119, reply: add group. Join the front-end adviser's public account exchange group. Those who are too lazy to cloneproject can reply " visualization " in the background of the official account, directly take the core code and drag it into the project.
Reference: https://cloud.tencent.com/developer/article/1489285 "React Hook" 160 lines of code to achieve dynamic and cool visualization charts-Ranking-Cloud + Community-Tencent Cloud