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:

  1. Canvas API - Great performance, handles large datasets well, but complex drawing code
  2. SVG - Vector graphics, scales infinitely, simple DOM operations, ideal for basic charts
  3. 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-500 animates 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 100 with preserveAspectRatio="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) || 0 provides 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