Using D3 and Canvas to Visualize Data on NextJS
Introduction
- In the previous article, I introduced how to use D3 to visualize data as charts. For most chart visualization needs, using D3 to create individual HTML DOM elements is sufficient. However, if you need to build a web application with superior performance to handle complex cases with relatively large datasets, then applying Canvas is a good solution for you
- Canvas is an HTML5 element that allows for extremely high-performance 2D graphic drawing via JavaScript. Unlike SVG, which creates separate DOM elements for each object, Canvas operates as a "blank canvas" where pixels are drawn directly onto a single surface. For instance, in real-world products like Figma, Canva, and TradingView, they all utilize Canvas to handle complex graphical operations.
- Outstanding advantages of Canvas include: superior render speed when processing thousands of objects simultaneously, memory resource savings as it does not increase the size of the DOM tree, and flexible customization for complex animation or interaction effects.
Prerequisites
Please note that this article is a continuation of the previous article on using D3. Please review it as I have mentioned api content for SSE if you need that information.
Detail
The content of this article only needs to focus on using D3 in combination with Canvas, so we only need to create one additional file, app/d3/CanvasCandlestickChart.tsx, with the following content:
'use client'
import * as d3 from 'd3'
import {useEffect, useRef, useState} from 'react'
import type {Candle} from './type'
export default function CanvasCandlestickChart() {
const [data, setData] = useState<Candle[]>([])
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [dimensions, setDimensions] = useState({width: 0, height: 0})
const margin = {top: 20, right: 30, bottom: 30, left: 50}
useEffect(() => {
const eventSource = new EventSource('/api/sse')
eventSource.onmessage = event => {
const newTicks = JSON.parse(event.data)
setData(prev => [...prev, ...newTicks].slice(-500))
}
return () => eventSource.close()
}, [])
useEffect(() => {
if (!containerRef.current) return
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const {width, height} = entry.contentRect
setDimensions({width, height})
}
})
resizeObserver.observe(containerRef.current)
return () => resizeObserver.disconnect()
}, [])
useEffect(() => {
if (!canvasRef.current || data.length === 0 || dimensions.width === 0)
return
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')!
const dpr = window.devicePixelRatio || 1
canvas.width = dimensions.width * dpr
canvas.height = dimensions.height * dpr
ctx.scale(dpr, dpr)
ctx.clearRect(0, 0, dimensions.width, dimensions.height)
const x = d3
.scaleLinear()
.domain([0, data.length])
.range([margin.left, dimensions.width - margin.right])
const y = d3
.scaleLinear()
.domain([d3.min(data, d => d.low)!, d3.max(data, d => d.high)!])
.range([dimensions.height - margin.bottom, margin.top])
const candleWidth = Math.max(
1,
((dimensions.width - margin.left - margin.right) / data.length) * 0.8
)
data.forEach((d, i) => {
const color = d.close > d.open ? '#26a69a' : '#ef5350'
const xPos = x(i)
ctx.strokeStyle = color
ctx.fillStyle = color
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(xPos, y(d.low))
ctx.lineTo(xPos, y(d.high))
ctx.stroke()
const yOpen = y(d.open)
const yClose = y(d.close)
const candleHeight = Math.abs(yOpen - yClose)
ctx.fillRect(
xPos - candleWidth / 2,
Math.min(yOpen, yClose),
candleWidth,
Math.max(candleHeight, 1)
)
})
}, [data, dimensions])
return (
<div className="flex flex-col w-full h-full bg-gray-900 text-white p-4">
<h2 className="text-center mb-2 shrink-0 font-bold">
D3 + Canvas (Responsive)
</h2>
<div
ref={containerRef}
className="flex-1 w-full h-full relative overflow-hidden bg-black border border-gray-700"
>
<canvas
ref={canvasRef}
style={{
width: '100%',
height: '100%',
display: 'block',
}}
/>
</div>
<div className="mt-2 text-[10px] text-gray-500 flex justify-between">
<span>Performance: Canvas is handling {data.length} nodes</span>
<span>
Resolution: {Math.round(dimensions.width)}x
{Math.round(dimensions.height)}
</span>
</div>
</div>
)
}
- The code snippet above builds a real-time candlestick chart (Candlestick Chart) by combining the computational power of D3 and the drawing capabilities of Canvas within NextJS. Key functionalities include:
- Using SSE (Server-Sent Events) to receive continuous data and update the candlestick state.
- Using ResizeObserver to monitor the container's dimensions, ensuring the chart is always responsive to the frame.
- Using D3 to calculate coordinates (scales) for the X and Y axes based on the price data.
- Leveraging the Canvas API to draw candlestick bodies and wicks directly onto the screen, while handling Device Pixel Ratio (DPR) to ensure sharp images on Retina/High-DPI displays.
- In this example, D3 does not play the role of a manipulation tool to draw HTML DOM elements, but rather supports mathematical functions that provide coordinates to draw accurately to every pixel on the canvas. This approach has the advantage of being extremely fast as it does not consume much memory to process, suitable for web applications with high performance requirements that need to render continuously with a large amount of data.
The result will look like this:
Happy coding!
Comments
Post a Comment