Building a JSON to Chart Tool with Pure SVG: Bar, Pie, and Line Charts#

You know the feeling: backend returns a wall of JSON numbers and your brain shuts down. With dozens of data points in a table, patterns are hard to spot.

Charts fix this. But pulling in a 50KB library just to visualize one API response feels wasteful. You can render decent charts with raw SVG in under 200 lines. Here’s how JsonKit’s JSON to Chart tool does it.

Data Normalization: From Messy JSON to Clean Data#

The first challenge is handling different JSON formats. Users might paste an array of objects or a flat object — the tool needs to handle both.

interface DataItem {
  label: string
  value: number
  color?: string
}

function parseJson(input: string): DataItem[] {
  const parsed = JSON.parse(input)
  if (Array.isArray(parsed)) {
    return parsed.map((item, index) => ({
      label: item.label || item.name || `Item ${index + 1}`,
      value: Number(item.value || item.count || 0),
      color: item.color
    }))
  }
  // Object format: { "Beijing": 2154, "Shanghai": 2487 }
  return Object.entries(parsed).map(([label, value]) => ({
    label,
    value: Number(value)
  }))
}

Key detail: use Number() instead of parseInt — data could be floats. The fallback fields (labelnamekey) make the parser more forgiving.

Bar Charts: Value-to-Pixel Mapping#

A bar chart is fundamentally about mapping data values to pixel heights:

barHeight = (value / maxValue) * availableHeight

Then distribute bars evenly across the available width:

function renderBars(data: DataItem[], width: number, height: number) {
  const maxValue = Math.max(...data.map(d => d.value))
  const barWidth = 30
  const gap = (width - data.length * barWidth) / (data.length + 1)
  
  return data.map((item, i) => {
    const x = gap + i * (barWidth + gap)
    const barHeight = (item.value / maxValue) * (height - 50)
    const y = height - 30 - barHeight
    return `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" rx="4" fill="${item.color}"/>`
  })
}

One easy gotcha: SVG’s y-axis goes downward, so y = height - 30 - barHeight. The rx="4" adds subtle rounded corners that make a surprising difference.

Pie Charts: SVG Arc Paths#

Pie charts are trickier. SVG has no native pie element — you have to draw each slice with the arc (A) path command:

A rx ry x-axis-rotation large-arc-flag sweep-flag x y

The drawing logic: line from center to start point → arc to end point → line back to center:

function renderPieSlice(
  cx: number, cy: number, r: number,
  startAngle: number, endAngle: number
) {
  const startRad = (startAngle * Math.PI) / 180
  const endRad = (endAngle * Math.PI) / 180
  const x1 = cx + r * Math.cos(startRad)
  const y1 = cy + r * Math.sin(startRad)
  const x2 = cx + r * Math.cos(endRad)
  const y2 = cy + r * Math.sin(endRad)
  const largeArc = (endAngle - startAngle) > 180 ? 1 : 0

  return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`
}

The largeArc flag is a common trap. By default SVG renders the shorter arc. When a slice exceeds 180 degrees, you must set large-arc-flag = 1 explicitly, or you’ll get the wrong shape.

Colors cycle through a predefined 10-color palette, ensuring visual variety without relying on a library:

const defaultColors = [
  '#06b6d4', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981',
  '#3b82f6', '#ef4444', '#84cc16', '#f97316', '#6366f1'
]

Line Charts: Connecting the Dots#

Line charts are the simplest of the three. Calculate (x, y) coordinates for each data point, then connect them with SVG’s polyline:

function renderLine(data: DataItem[], width: number, height: number) {
  const maxValue = Math.max(...data.map(d => d.value))
  const points = data.map((item, i) => {
    const x = 40 + (i * (width - 80)) / (data.length - 1 || 1)
    const y = height - 30 - (item.value / maxValue) * (height - 50)
    return `${x},${y}`
  }).join(' ')

  return `<polyline points="${points}" fill="none" stroke="#06b6d4" stroke-width="2"
    stroke-linecap="round" stroke-linejoin="round" />`
}

stroke-linecap="round" and stroke-linejoin="round" soften the corners. Adding small filled circles at each data point makes the chart much more readable.

SVG Export#

Since the chart is already rendered as SVG, exporting is trivial — serialize it to a string and create a downloadable blob:

function downloadChart() {
  const svg = document.querySelector('.chart-container svg')
  const svgData = new XMLSerializer().serializeToString(svg)
  const blob = new Blob([svgData], { type: 'image/svg+xml' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'chart.svg'
  a.click()
  URL.revokeObjectURL(url)
}

Why Raw SVG?#

No framework. No dependencies. Just a <svg> element and basic math. For small to medium datasets (10–200 points), raw SVG is performant, produces crisp vector graphics, and keeps your bundle tiny.

The full implementation — bar, pie, and line charts with label rendering, percentage tables, and SVG export — comes in under 250 lines. If you have a similar JSON visualization need, try raw SVG before reaching for a charting library.


Related tools: