p5.js Intermediate Article 6

Data Visualisation

Load JSON and CSV data, build bar charts, scatter plots, and network graphs, and animate data transitions.

⏱ 22 min read data JSON CSV charts visualisation

Loading data

p5.js provides loadJSON() and loadTable() for the most common data formats. Always load in preload():

let data;

function preload() {
  data = loadJSON('data.json');
  // or:
  data = loadTable('data.csv', 'csv', 'header');
}

JSON structure

{
  "cities": [
    { "name": "London",  "population": 9648110, "lat": 51.5, "lon": -0.12 },
    { "name": "Paris",   "population": 2161000, "lat": 48.86, "lon": 2.35 },
    { "name": "Berlin",  "population": 3664088, "lat": 52.52, "lon": 13.40 }
  ]
}
function draw() {
  let cities = data.cities;
  for (let city of cities) {
    print(city.name, city.population);
  }
}

CSV with loadTable()

// data.csv:
// name,value,category
// Alpha,42,A
// Beta,17,B

let table;

function preload() {
  table = loadTable('data.csv', 'csv', 'header');
}

function draw() {
  for (let row of table.rows) {
    let name  = row.getString('name');
    let value = row.getNum('value');
    print(name, value);
  }
}

Bar chart

let data;
let animProgress = 0;

function preload() {
  data = loadJSON('monthly.json');
}

function setup() {
  createCanvas(700, 450);
}

function draw() {
  background(20);
  animProgress = min(animProgress + 0.03, 1);

  let values   = data.months;
  let maxVal   = max(values.map(d => d.value));
  let barW     = (width - 100) / values.length;
  let chartH   = height - 80;
  let eased    = easeOutCubic(animProgress);

  for (let i = 0; i < values.length; i++) {
    let d    = values[i];
    let barH = map(d.value, 0, maxVal, 0, chartH) * eased;
    let x    = 50 + i * barW;
    let y    = height - 40 - barH;

    // Bar
    colorMode(HSB, 360, 100, 100);
    fill(map(i, 0, values.length, 200, 280), 70, 90);
    noStroke();
    rect(x + 2, y, barW - 4, barH, 3, 3, 0, 0);

    // Label
    colorMode(RGB);
    fill(200);
    textSize(11);
    textAlign(CENTER);
    text(d.label, x + barW / 2, height - 22);

    // Value
    if (animProgress > 0.9) {
      fill(255);
      textSize(10);
      text(d.value, x + barW / 2, y - 4);
    }
  }
}

function easeOutCubic(t) {
  return 1 - pow(1 - t, 3);
}

Scatter plot

function drawScatterPlot(records, xKey, yKey) {
  let xs = records.map(r => r[xKey]);
  let ys = records.map(r => r[yKey]);

  let pad  = 60;
  let xMin = min(xs), xMax = max(xs);
  let yMin = min(ys), yMax = max(ys);

  // Axes
  stroke(80);
  strokeWeight(1);
  line(pad, pad, pad, height - pad);           // y axis
  line(pad, height - pad, width - pad, height - pad);  // x axis

  // Points
  noStroke();
  for (let r of records) {
    let x = map(r[xKey], xMin, xMax, pad, width - pad);
    let y = map(r[yKey], yMin, yMax, height - pad, pad);

    let hovering = dist(mouseX, mouseY, x, y) < 8;
    fill(hovering ? color(255, 200, 50) : color(100, 180, 255, 180));
    circle(x, y, hovering ? 14 : 8);

    if (hovering) {
      fill(255);
      textSize(11);
      textAlign(LEFT);
      text(`${r.name}\n${xKey}: ${r[xKey]}\n${yKey}: ${r[yKey]}`, x + 10, y - 10);
    }
  }
}

Animated transitions

When data updates, lerp between old and new values:

let displayed = [];   // current display values (lerped)
let target    = [];   // target values from data

function updateData(newValues) {
  target = newValues;
  if (displayed.length === 0) displayed = [...newValues];
}

function draw() {
  // Lerp displayed values toward target
  for (let i = 0; i < target.length; i++) {
    displayed[i] = lerp(displayed[i] || 0, target[i], 0.08);
  }
  // Draw using displayed values
}

Network graph (force-directed)

let nodes = [], edges = [];

function initGraph(graphData) {
  nodes = graphData.nodes.map(n => ({
    ...n,
    x: random(100, width - 100),
    y: random(100, height - 100),
    vx: 0, vy: 0
  }));
  edges = graphData.edges;
}

function layoutStep() {
  // Repulsion between all node pairs
  for (let i = 0; i < nodes.length; i++) {
    for (let j = i + 1; j < nodes.length; j++) {
      let dx   = nodes[j].x - nodes[i].x;
      let dy   = nodes[j].y - nodes[i].y;
      let dist = Math.sqrt(dx * dx + dy * dy) || 1;
      let f    = 3000 / (dist * dist);
      nodes[i].vx -= dx / dist * f;
      nodes[i].vy -= dy / dist * f;
      nodes[j].vx += dx / dist * f;
      nodes[j].vy += dy / dist * f;
    }
  }

  // Spring attraction along edges
  for (let e of edges) {
    let a = nodes[e.source], b = nodes[e.target];
    let dx = b.x - a.x, dy = b.y - a.y;
    let d  = Math.sqrt(dx * dx + dy * dy) || 1;
    let f  = (d - 100) * 0.01;
    a.vx += dx / d * f; a.vy += dy / d * f;
    b.vx -= dx / d * f; b.vy -= dy / d * f;
  }

  // Apply velocity with damping
  for (let n of nodes) {
    n.x = constrain(n.x + n.vx, 20, width - 20);
    n.y = constrain(n.y + n.vy, 20, height - 20);
    n.vx *= 0.85;
    n.vy *= 0.85;
  }
}

Key takeaways

  • loadJSON() and loadTable() must be called in preload()
  • Map data values to visual properties with map(value, dataMin, dataMax, visualMin, visualMax)
  • Animated entrances (lerp toward target) make visualisations feel polished
  • Hover detection (dist() from mouse) adds interactivity with minimal code
  • Force-directed layouts for graphs: repel all nodes, attract connected pairs
  • Compute min() and max() of your data before drawing to set the scale correctly