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.
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.