Building a Chart Generator from Scratch: SVG Rendering and Data Visualization Core Techniques
Building a Chart Generator from Scratch: SVG Rendering and Data Visualization Core Techniques#
I was building a dashboard recently and needed to quickly generate several common chart types. Chart.js is great, but sometimes you just need a simple visualization without adding hundreds of KB to your bundle. So I built a pure frontend chart generator and documented the core techniques.
Three Paths to Chart Rendering#
There are three main approaches to building charts:
- Canvas API - Great performance, handles large datasets well, but complex drawing code
- SVG - Vector graphics, scales infinitely, simple DOM operations, ideal for basic charts
- CSS + div - Bar charts can be pure CSS, simple and effective
I chose a hybrid approach: bar charts with CSS + div, line and pie charts with SVG.
Bar Chart: The Art of CSS Width Percentages#
Bar charts are the simplest. The core is calculating percentage widths:
const maxValue = Math.max(...data.map(d => d.value))
// Each bar
<div
className="h-full transition-all duration-500"
style={{
width: `${(item.value / maxValue) * 100}%`,
backgroundColor: colors[i % colors.length]
}}
/>
Key details:
- Normalization: Divide all values by max to get 0-1 ratio
- Color cycling:
colors[i % colors.length]ensures we don’t run out - Smooth transitions:
transition-all duration-500animates width changes
This approach renders fast with minimal code. But for stacked or grouped bar charts, you’d need Canvas or SVG.
Line Chart: SVG Polyline Coordinate Calculation#
Line charts use SVG’s <polyline> element. The core is converting data points to coordinates:
const points = data.map((d, i) => {
const x = (i / (data.length - 1)) * 100 // Evenly distributed X
const y = 100 - (d.value / maxValue) * 100 // Y from bottom to top
return `${x},${y}`
}).join(' ')
// SVG rendering
<svg viewBox="0 0 100 100" className="w-full h-64">
<polyline
fill="none"
stroke="#06b6d4"
strokeWidth="0.5"
points={points}
/>
{data.map((d, i) => {
const x = (i / (data.length - 1)) * 100
const y = 100 - (d.value / maxValue) * 100
return (
<g key={i}>
<circle cx={x} cy={y} r="1.5" fill="#06b6d4" />
<text x={x} y={y - 5} fontSize="4">{d.value}</text>
</g>
)
})}
</svg>
Coordinate calculation logic:
- X coordinate:
i / (data.length - 1) * 100, evenly distribute points across 0-100 range - Y coordinate:
100 - (d.value / maxValue) * 100, note SVG Y-axis goes top to bottom, so we invert - viewBox: Using
0 0 100 100withpreserveAspectRatio="none"for responsive scaling
Pie Chart: The Math Behind SVG Arc Paths#
Pie charts are the most complex, requiring arc calculation for each slice:
const total = data.reduce((sum, d) => sum + d.value, 0)
let currentAngle = 0
data.map((d, i) => {
const angle = (d.value / total) * 360 // Slice angle
const startAngle = currentAngle
currentAngle += angle
const endAngle = currentAngle
// Start and end coordinates
const x1 = 50 + 40 * Math.cos((startAngle * Math.PI) / 180)
const y1 = 50 + 40 * Math.sin((startAngle * Math.PI) / 180)
const x2 = 50 + 40 * Math.cos((endAngle * Math.PI) / 180)
const y2 = 50 + 40 * Math.sin((endAngle * Math.PI) / 180)
const largeArc = angle > 180 ? 1 : 0
return (
<path
d={`M 50 50 L ${x1} ${y1} A 40 40 0 ${largeArc} 1 ${x2} ${y2} Z`}
fill={colors[i % colors.length]}
/>
)
})
SVG arc path syntax: M x0 y0 L x1 y1 A rx ry x-axis-rotation large-arc-flag sweep-flag x2 y2 Z
- M 50 50: Move to center
- L x1 y1: Line to arc start point
- A 40 40: Elliptical arc, rx=ry=40 makes it circular
- large-arc-flag: 1 if angle > 180°, otherwise 0
- sweep-flag: 1 for clockwise
- Z: Close path
The formulas x = cx + r * cos(θ) and y = cy + r * sin(θ) convert angles to coordinates.
Data Parsing: Simple CSV Format Handling#
Input uses a simple CSV format:
const data = dataInput.split("\n")
.filter(line => line.trim())
.map(line => {
const [label, value] = line.split(",")
return {
label: label?.trim() || "",
value: parseFloat(value) || 0
}
})
More intuitive than JSON - users can paste directly from Excel. Handle edge cases:
- Filter empty lines:
.filter(line => line.trim()) - Parse failures:
parseFloat(value) || 0provides default - Extra whitespace:
.trim()cleans up
Performance: useMemo for Caching#
Chart calculations depend on data. Cache with useMemo:
const data = useMemo(() => {
return parseData(dataInput)
}, [dataInput])
const maxValue = useMemo(() => {
return Math.max(...data.map(d => d.value))
}, [data])
Only recalculate when dataInput changes, avoiding redundant computation on each render.
Code Generation: Let Users Take the Data#
Another feature: generate Chart.js code:
const generateCode = () => {
const chartData = data.map(d => ({ name: d.label, value: d.value }))
return `const data = ${JSON.stringify(chartData, null, 2)};
new Chart(ctx, {
type: '${chartType}',
data: {
labels: data.map(d => d.name),
datasets: [{
label: '${title}',
data: data.map(d => d.value),
backgroundColor: ${JSON.stringify(colors.slice(0, data.length))}
}]
}
});`
}
Users can copy-paste directly into their projects, making the tool more practical.
Edge Cases#
1. Single Data Point#
Line chart with one point causes division by zero:
const x = (i / (data.length - 1 || 1)) * 100
Add || 1 fallback.
2. All Zeros#
Math.max(...[0, 0, 0]) returns 0, causing division by zero:
const safeMax = maxValue || 1
3. Not Enough Colors#
7 preset colors, use modulo for more: colors[i % colors.length]
The Result#
Based on these ideas, I built: Chart Generator
Features:
- Three chart types: bar, line, pie
- Real-time preview
- Chart.js code generation for easy integration
Under 200 lines of core code, but covers SVG rendering, data visualization, and performance optimization. For simple data display scenarios, it’s sufficient.
Related: JSON to Chart | CSV Viewer