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!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Sitemap

NodeJS Practice Series