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:
-
How can I match the canvas with and height of the browser as it may be variable.
-
How to match the grid lines and bars to both the data and also to size of the canvas.
>Solution :
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.