Home > Mobile >  Custom bar chart with dynamic bars and grid lines using canvas
Custom bar chart with dynamic bars and grid lines using canvas

Time:09-11

I'm trying to create a simple bar chart using html5 canvas and javascript from scratch. I have written this code so far. Yet I have difficulties drawing the bars and grid lines dynamically to match the input data to the canvas size accordingly. As I change the browser size the chart won't update, and the quality is not just good.

const { useState, useRef, useEffect } = React;

function Charts({ data }) {
    const canvas = useRef()
    const bar_color = 'yellowgreen'
    const grid_color = 'grey'

    // I guess these should be 100% but I don't know how to set it
    const width = 100
    const height = 100
  
    useEffect(() => {
        // get the canvas context
        let ctx = canvas.current.getContext('2d')
        let grids = 10
        // process the data
        let n = data.length
        let X_min = 1
        let X_max = n
        let Y_min = minIn(data)
        let Y_max = maxIn(data)
        // draw grids
        for(let i = 0; i < grids; i  ){
            drawLine(ctx, 0, height-i*10, width, height-i*10)
        }
        for(let i = 0; i < n; i  ){
            drawLine(ctx, i*10, height, i*10, 0)
        }
        // draw bars
        data.forEach((datapoint, i) => {
            drawBar(ctx, i*10, height-datapoint, (i 1)*10, height-0)
        })
    }, [data])

    function drawBar(ctx, startX, startY, endX, endY) {
        ctx.save()
        ctx.fillStyle = bar_color
        ctx.fillRect(startX, startY, endX, endY)
        ctx.restore()
    }

    function drawLine(ctx, startX, startY, endX, endY) {
        ctx.save()
        ctx.strokeStyle = grid_color
        ctx.beginPath()
        ctx.moveTo(startX, startY)
        ctx.lineTo(endX, endY)
        ctx.stroke()
        ctx.restore()
    }

    function maxIn(array) {
        return array.length > 0 ? Math.max.apply(Math, array) : 0;
    }

    function minIn(array) {
        return array.length > 0 ? Math.min.apply(Math, array) : 0;
    }

    return (
        <canvas className='chart' ref={canvas} />
    )
};

// input data
let data = [1, -2, 5, 9, -1, 0, 3]

ReactDOM.render(
    <Charts data={data} />, document.getElementById("root")
);
.chart{
    background:#eee;
    width:100%;
    height:100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>

My questions are:

  1. How can I match the canvas with and height of the browser as it may be variable.

  2. How to match the grid lines and bars to both the data and also to size of the canvas.

CodePudding user response:

Alright, For the first part you need a listener on window to handle the changes in the size of the window. Like this:

window.addEventListener("resize", handleResize);

The second part is just implementing a relatively easy algorithm. I suppose because you couldn't figure out the first part you couldn't finish the second.

const { useRef, useEffect } = React;

let data = [1, -2, 5, 9, -1, 0, 3]

function Charts({ data }) {

    // canvas
    const canvas = useRef()
    let ctx = null

    // parameters
    const bar_color = '#68c83c'
    const grid_color = '#ccc'
    const axis_color = '#999'

    // meta data
    let n = data.length
    let [Y_min, Y_max] = [minIn(data), maxIn(data)]
    let Y = Y_max - Y_min

    // ANSWER TO PART 1:
    useEffect(() => {
        // set the canvas context
        ctx = canvas.current.getContext('2d')
        // initialize draw
        handleResize();
        // add listener
        window.addEventListener("resize", handleResize);
        // remove listener when component is unmounted
        return () => window.removeEventListener("resize", handleResize);
    }, [])

    function handleResize() {
        let width = window.innerWidth
        let height = window.innerHeight
        // set canvas size
        ctx.canvas.width = width
        ctx.canvas.height = height

        // ANSWER TO PART 2:
        let bar_width = width / n
        let height_ratio = height / Y
        let zero_line = Y_max * height_ratio
        let grids_h = Y

        // draw grids
        // Horizontal
        for (let i = 0; i < grids_h; i  ) {
            drawLine(0, (height / grids_h) * i, width, (height / grids_h) * i)
        }
        // vertical
        for (let i = 0; i < n; i  ) {
            drawLine(bar_width * i   bar_width / 2, height, bar_width * i   bar_width / 2, 0)
        }
        // y axis at y = 0
        drawLine(0, zero_line, width, zero_line, axis_color)

        // draw bars
        data.forEach((datapoint, i) => {
            // to support negative values
            let y_start = datapoint > 0 ? height_ratio * (Y_max - datapoint) : zero_line
            let bar_height = height_ratio * Math.abs(datapoint)
            drawBar(i * bar_width, y_start, bar_width, bar_height)
        })
    }

    // nice helper functions

    function drawBar(startX, startY, W, H, color = bar_color) {
        ctx.save()
        ctx.fillStyle = color
        ctx.fillRect(startX, startY, W, H) // This works with Width and Height. Not same as lineTo.
        ctx.restore()
    }

    function drawLine(startX, startY, endX, endY, color = grid_color) {
        ctx.save()
        ctx.strokeStyle = color
        ctx.beginPath()
        ctx.moveTo(startX, startY)
        ctx.lineTo(endX, endY)
        ctx.stroke()
        ctx.restore()
    }

    function maxIn(array) {
        return array.length > 0 ? Math.max.apply(Math, array) : 0;
    }

    function minIn(array) {
        return array.length > 0 ? Math.min.apply(Math, array) : 0;
    }

    return ( <canvas className='chart' ref={ canvas } /> )
}

ReactDOM.render( <Charts data={ data } />, document.getElementById("root")
);
body{
    padding:0;
    margin:0
}
.chart{
    background:#eee;
    width:100%;
    height:100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>

Don't forget to compare our codes line by line to see where else I made changes. Cheers.

  • Related