Learn Creative Coding (#79) - Data as Creative Material

in StemSocialyesterday

Learn Creative Coding (#79) - Data as Creative Material

cc-banner

We just wrapped an entire arc on 3D -- sixteen episodes of geometry, materials, particles, physics, environments, interaction, and finally VR/AR with WebXR. You can build worlds now. Actual, walkable, interactive worlds. That's a lot of power in your hands.

But here's the thing. Every visual we've created so far came from our imagination. We invented the shapes, chose the colors, decided the positions. Noise functions gave us organic randomness but we still controlled what that randomness looked like. The creative input was always internal -- code expressing ideas we already had.

This episode flips that. Instead of inventing visuals from nothing, we're going to let external data drive the creative decisions. Numbers from the real world -- temperatures, timestamps, measurements, counts, coordinates -- become the raw material for visual output. The data decides the shape. The data picks the color. The data determines the density, the rhythm, the composition. You write the rules for how data maps to visuals, but the data itself is the artist.

This is not data visualization. I want to be clear about that upfront because the distinction matters. Visualization aims for clarity -- bar charts, line graphs, scatter plots. The goal is accurate communication. You look at a chart and understand a trend. Data art aims for something else entirely: emotional resonance, beauty, surprise, provocation. The goal is not "understand this dataset" but "feel something about this dataset." Different goals, different rules, different aesthetics.

Found data vs collected data

There are two kinds of data you can work with, and they feel completely different as creative material.

Found data is stuff that already exists. Weather archives, census records, transit schedules, stock prices, earthquake catalogs, satellite imagery metadata, biodiversity surveys. You didn't create this data -- someone or somthing else did, usually for practical reasons. You're repurposing it. Found-data art is like found-object sculpture: you take something made for one purpose and recontextualize it as art. A spreadsheet of daily rainfall becomes a field of dots whose density shifts with the seasons. A database of flight paths becomes a web of curves connecting continents.

Collected data is stuff you gather yourself. Your daily step counts. A mood journal where you rate each day 1-10. How many cups of coffee you drank. The color of the sky at 7am every morning, recorded as a hex value. Plant growth measurements week by week. This data is personal, intimate, unique to you. Nobody else has it. And that makes the art personal too.

The best reference for collected-data art is the Dear Data project by Giorgia Lupi and Stefanie Posavec. For a year, they each tracked some aspect of their lives every week -- complaints they made, times they laughed, doors they opened -- and mailed each other hand-drawn postcards encoding the data visually. No axes. No labels. Pure visual pattern from personal observation. If you haven't seen it, look it up. It's the most beautiful argument I've seen for why data can be art.

The mapping problem

This is the core creative question of data art: which data dimension maps to which visual property?

Say you have a dataset with three columns: date, temperature, wind speed. You want to turn each row into a visual element. What does temperature control? Position? Color? Size? Opacity? And what does wind speed control? The same options are available. There's no objectively correct answer -- it's a creative choice, and different mappings tell different stories from the same data.

Let me show you what I mean with a simple example. We'll take fake weather data and map it three different ways:

const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// fake weather data: 30 days of temperature and wind
const data = [];
for (let i = 0; i < 30; i++) {
  data.push({
    day: i,
    temp: 15 + Math.sin(i * 0.3) * 10 + (Math.random() - 0.5) * 5,
    wind: 5 + Math.random() * 20
  });
}

Mapping 1: temperature controls vertical position, wind controls circle size.

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);

for (const d of data) {
  const x = (d.day / 29) * 700 + 50;
  const y = 350 - ((d.temp - 5) / 25) * 300;
  const r = d.wind * 0.8;

  ctx.beginPath();
  ctx.arc(x, y, r, 0, Math.PI * 2);
  ctx.fillStyle = `rgba(100, 180, 255, 0.4)`;
  ctx.fill();
}

That reads like a scatter plot. Familiar. The brain can decode temperature and wind independently. Now try a different mapping.

Mapping 2: temperature controls color (cold = blue, hot = red), wind controls opacity.

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);

for (const d of data) {
  const x = (d.day / 29) * 700 + 50;
  const y = 200;
  const hue = ((d.temp - 5) / 25) * 240;  // 0=red, 240=blue (inverted)
  const opacity = 0.2 + (d.wind / 25) * 0.7;

  ctx.beginPath();
  ctx.arc(x, y, 12, 0, Math.PI * 2);
  ctx.fillStyle = `hsla(${240 - hue}, 70%, 55%, ${opacity})`;
  ctx.fill();
}

Same data, completely different feeling. The dots sit in a line now -- no positional encoding of temperature. Instead the color gradient tells the temperature story and the opacity variation adds the wind layer. It's less "readable" as a chart but more evocative as a visual. You see the rhythm of warm and cold days as a color wave.

Mapping 3: temperature controls rotation of a line, wind controls line length.

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);
ctx.strokeStyle = 'rgba(200, 220, 255, 0.6)';
ctx.lineWidth = 1.5;

for (const d of data) {
  const x = (d.day / 29) * 700 + 50;
  const y = 200;
  const angle = ((d.temp - 5) / 25) * Math.PI;
  const len = d.wind * 1.5;

  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(angle);
  ctx.beginPath();
  ctx.moveTo(0, -len / 2);
  ctx.lineTo(0, len / 2);
  ctx.stroke();
  ctx.restore();
}

Now it looks like grass blowing in the wind. The temperature rotates each blade and the wind speed stretches it. Same thirty data points, same two dimensions, completely different visual language. The choice of mapping IS the art. The data is just raw material.

Data humanism

Giorgia Lupi wrote a manifesto called "Data Humanism" that I think about a lot. The core idea: data represents human lives, not abstract numbers. Every data point is a person, an event, a moment. A bar in a bar chart showing "500 deaths" flattens 500 individual stories into a rectangle. Data art can push back against that flattening by making each point visible, individual, distinguishable.

This doesn't mean every data art piece needs to be somber or political. It means being aware that data about people carries weight. If you're visualizing earthquake deaths, a playful bouncy animation might be in bad taste. If you're visualizing your own coffee consumption, go wild. The creative tone should match the gravity of the data.

There's also an ethics dimension. Public data isn't automatically ethical to visualize. Health data, income data, identity data -- even anonymized, visualizing it in certain ways can reveal patterns that harm real people. A beautiful map of where HIV patients live is still a map of where HIV patients live. Just because you CAN make it doesn't mean you SHOULD. Think about who sees this and what they could do with it.

Scale changes everything

The number of data points you have fundamentally changes your visual approach.

Small datasets (10-50 points) need individual attention. Each point should be visible, distinguishable, maybe even labeled. You have room to give each data point its own visual identity. Think of it like portraiture -- each element gets a face.

// small dataset: each point gets individual treatment
const smallData = [
  { name: 'Mon', value: 3 },
  { name: 'Tue', value: 7 },
  { name: 'Wed', value: 2 },
  { name: 'Thu', value: 9 },
  { name: 'Fri', value: 5 },
  { name: 'Sat', value: 8 },
  { name: 'Sun', value: 4 }
];

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);

for (let i = 0; i < smallData.length; i++) {
  const d = smallData[i];
  const x = 100 + i * 90;
  const y = 200;
  const size = d.value * 8;

  // unique shape per point
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(d.value * 0.3);

  ctx.beginPath();
  for (let j = 0; j < d.value + 3; j++) {
    const a = (j / (d.value + 3)) * Math.PI * 2;
    const r = size * (0.8 + Math.sin(a * 3) * 0.2);
    if (j === 0) ctx.moveTo(Math.cos(a) * r, Math.sin(a) * r);
    else ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
  }
  ctx.closePath();

  ctx.fillStyle = `hsla(${d.value * 30}, 60%, 50%, 0.6)`;
  ctx.fill();
  ctx.strokeStyle = `hsla(${d.value * 30}, 60%, 70%, 0.8)`;
  ctx.lineWidth = 1;
  ctx.stroke();
  ctx.restore();
}

Large datasets (1000+ points) become textures and fields. Individual points disappear into density. You stop seeing elements and start seeing patterns, gradients, flows. Think of it like landscape painting -- you see the forest, not the trees.

// large dataset: points become texture
const largeCount = 5000;
const largeData = [];
for (let i = 0; i < largeCount; i++) {
  largeData.push({
    x: Math.random(),
    y: Math.random(),
    v: Math.sin(i * 0.01) * 0.5 + Math.random() * 0.5
  });
}

ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);

for (const d of largeData) {
  const px = d.x * 780 + 10;
  const py = d.y * 380 + 10;
  const hue = d.v * 60 + 200;

  ctx.fillStyle = `hsla(${hue}, 50%, 55%, 0.15)`;
  ctx.fillRect(px, py, 2, 2);
}

5000 tiny semi-transparent dots. Individually they're nothing. Together they form a cloud with visible density gradients. Where dots cluster, the color saturates. Where they thin out, the background shows through. The sine wave in the value creates a subtle banding pattern that emerges from the noise. You'd never see this with 20 points.

Temporal data as animation

Data with a time dimension is special because it maps naturally to animation. One frame per data point. The visual evolves as you step through the dataset. Time in the data becomes time on screen.

const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// 365 days of fake temperature data
const yearData = [];
for (let i = 0; i < 365; i++) {
  const season = Math.sin((i / 365) * Math.PI * 2 - Math.PI / 2);
  yearData.push({
    day: i,
    temp: 15 + season * 12 + (Math.random() - 0.5) * 6
  });
}

let currentDay = 0;
const trailLength = 60;

function drawFrame() {
  // semi-transparent black overlay for trail effect
  ctx.fillStyle = 'rgba(10, 10, 26, 0.08)';
  ctx.fillRect(0, 0, 800, 400);

  // draw current day's mark
  const d = yearData[currentDay];
  const angle = (d.day / 365) * Math.PI * 2 - Math.PI / 2;
  const radius = 80 + d.temp * 4;
  const x = 400 + Math.cos(angle) * radius;
  const y = 200 + Math.sin(angle) * radius;

  // color by temperature
  const hue = Math.max(0, Math.min(240, (30 - d.temp) * 8));
  ctx.beginPath();
  ctx.arc(x, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = `hsl(${hue}, 70%, 60%)`;
  ctx.fill();

  currentDay = (currentDay + 1) % 365;
  requestAnimationFrame(drawFrame);
}

drawFrame();

A year of temperature data drawn as a spiral. Each day is a point placed at an angle (day of year) and radius (temperature). The semi-transparent overlay creates a trail so you see recent history fading behind the current point. Cold days pull the spiral inward (smaller radius). Hot days push it outward. Over a full cycle you see the seasonal breathing -- the spiral expands through summer and contracts through winter. The color shifts blue-to-red reinforce the temperature encoding.

The animation adds something a static image can't: anticipation. You watch the spiral grow and you can feel summer approaching as the radius stretches outward. You know winter is coming when it starts pulling back in. The temporal dimension creates narrative. A static scatter plot of this data would show the same pattern but you wouldn't feel it unfolding.

Abstract vs literal

A bar chart of monthly rainfall is literal. The height of each bar encodes the measurement. You read the number, you know the rainfall. Clear, efficient, boring.

Raindrops falling at data-proportional rates is abstract but evocative. January's low rainfall means sparse, slow drops. July's monsoon means a torrent. You don't read a number -- you feel the difference. Less precise, more meaningful.

Creative coding lets you choose any point on this spectrum. Here's rainfall data encoded as particle density:

const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// monthly rainfall in mm (fake data, loosely seasonal)
const rainfall = [42, 38, 51, 64, 78, 92, 110, 105, 85, 68, 53, 45];
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun',
                    'Jul','Aug','Sep','Oct','Nov','Dec'];

function drawRainfall() {
  ctx.fillStyle = '#0a0a14';
  ctx.fillRect(0, 0, 800, 400);

  for (let m = 0; m < 12; m++) {
    const x0 = m * (800 / 12);
    const w = 800 / 12;
    const dropCount = Math.floor(rainfall[m] * 0.6);

    for (let d = 0; d < dropCount; d++) {
      const dx = x0 + Math.random() * w;
      const dy = Math.random() * 360 + 20;
      const len = 3 + Math.random() * 8;
      const opacity = 0.15 + Math.random() * 0.25;

      ctx.strokeStyle = `rgba(120, 180, 255, ${opacity})`;
      ctx.lineWidth = 0.8;
      ctx.beginPath();
      ctx.moveTo(dx, dy);
      ctx.lineTo(dx + 0.5, dy + len);
      ctx.stroke();
    }

    // month label
    ctx.fillStyle = 'rgba(200, 200, 220, 0.4)';
    ctx.font = '11px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(monthNames[m], x0 + w / 2, 395);
  }
}

drawRainfall();

Each month gets a column. The number of rain streaks is proportional to the rainfall value. July and August are dense with overlapping streaks. January and February are sparse. You can't read the exact millimeters but you can instantly see the shape of the year -- and it feels like rain, not like a chart. The abstraction adds an emotional layer that literal encoding strips away.

Building a data art piece: personal step data

Allez, let's build something complete. A radial visualization of step count data -- one ring per week, each ring divided into seven segments (one per day), with segment thickness encoded by step count. High-step days make thick arcs. Low-step days make thin ones. The whole thing reads as a circular growth pattern, like a tree ring cross-section where active weeks have thick rings and sedentary weeks have thin ones.

const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 800;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// 12 weeks of fake step data
const weeks = [];
for (let w = 0; w < 12; w++) {
  const week = [];
  for (let d = 0; d < 7; d++) {
    // weekdays higher, weekends variable
    const base = d < 5 ? 6000 : 3000;
    week.push(Math.floor(base + Math.random() * 8000));
  }
  weeks.push(week);
}

ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, 800, 800);

const cx = 400;
const cy = 400;
const minRadius = 60;
const ringGap = 22;

for (let w = 0; w < weeks.length; w++) {
  const innerR = minRadius + w * ringGap;

  for (let d = 0; d < 7; d++) {
    const steps = weeks[w][d];
    const thickness = (steps / 14000) * (ringGap - 2);
    const outerR = innerR + thickness;

    const startAngle = (d / 7) * Math.PI * 2 - Math.PI / 2;
    const endAngle = ((d + 0.92) / 7) * Math.PI * 2 - Math.PI / 2;

    // color by step count: low = cool purple, high = warm gold
    const t = steps / 14000;
    const hue = 280 - t * 240;  // purple to gold
    const lightness = 25 + t * 30;

    ctx.beginPath();
    ctx.arc(cx, cy, outerR, startAngle, endAngle);
    ctx.arc(cx, cy, innerR, endAngle, startAngle, true);
    ctx.closePath();

    ctx.fillStyle = `hsla(${hue}, 55%, ${lightness}%, 0.8)`;
    ctx.fill();

    ctx.strokeStyle = `hsla(${hue}, 55%, ${lightness + 15}%, 0.3)`;
    ctx.lineWidth = 0.5;
    ctx.stroke();
  }
}

// center label
ctx.fillStyle = 'rgba(200, 200, 220, 0.6)';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('12 weeks', cx, cy - 5);
ctx.fillText('of steps', cx, cy + 15);

The result is a radial pattern that looks organic -- like a cross-section of something that grew. The active weeks with high step counts have thick, golden segments. The lazy weeks have thin, purple ones. The seven-day rhythm creates a subtle rotational symmetry (each ring has the same seven-segment structure) but the varying thicknesses break the symmetry enough to keep it interesting.

This is personal data art. Nobody else has your step data. The pattern it produces is uniquely yours. If you actually tracked your steps for three months and fed real numbers into this, the resulting image would be a portrait of your movement habits -- a visual diary entry that encodes something real about your life.

The creative exercise

Here's what I want you to try. Pick something you can track for one week. Anything measurable:

  • Cups of coffee or tea per day
  • Minutes spent outside
  • Number of text messages sent
  • Hours of sleep
  • Mood on a scale of 1 to 10
  • Songs listened to
  • Times you check your phone

Seven days, one measurement per day. Seven numbers. Then write a visual encoding for those seven numbers. Not a bar chart -- something that reflects the nature of what you measured. Coffee cups could be brown rings, growing darker with more caffeine. Sleep hours could be horizontal bands of deep blue, longer bands for longer nights. Mood could be the radius of a circle, small for bad days, large for good ones, the seven circles overlapping in a cluster.

No axes. No labels. No legend. Pure visual pattern from personal data. The meaning is in the mapping, and only you know the key.

Here's a starter template for the coffee version to get you going:

const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

// your actual data goes here
const coffees = [3, 2, 4, 1, 3, 5, 2];  // Mon-Sun

ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, 400, 400);

const cx = 200;
const cy = 200;

for (let i = 0; i < coffees.length; i++) {
  const angle = (i / 7) * Math.PI * 2 - Math.PI / 2;
  const cups = coffees[i];

  for (let c = 0; c < cups; c++) {
    const dist = 40 + c * 28;
    const x = cx + Math.cos(angle) * dist;
    const y = cy + Math.sin(angle) * dist;
    const darkness = 0.3 + (c / 5) * 0.4;

    ctx.beginPath();
    ctx.arc(x, y, 10, 0, Math.PI * 2);
    ctx.fillStyle = `hsla(25, 65%, ${20 + darkness * 30}%, 0.7)`;
    ctx.fill();
  }
}

Each day is a direction (Monday at the top, going clockwise). Each cup of coffee is a brown circle radiating outward along that ray. More cups = more circles = a longer arm. Heavy coffee days have long spokes, light days have short ones. The color deepens with each additional cup because thats how caffine accumulation feels. :-)

What's ahead

We've been making visuals from imagination for 78 episodes. Now we're making visuals from the world. Data as creative material means your art reflects something real -- temperatures, movements, quantities, rhythms from actual life. The mapping from data to visuals is where the creativity lives: same data, different mapping, different art.

But so far we've been using fake data arrays we typed into our code. Real data lives elsewhere -- on servers, behind URLs, in files with specific formats. Next up we need to learn how to actually fetch data from the outside world and bring it into our canvas. The browser has powerful tools for pulling in external information, and once you can access real datasets, everything changes.

't Komt erop neer...

  • Data art is not data visualization. Visualization aims for clarity and accurate communication (bar charts, line graphs). Data art aims for emotional resonance, beauty, or provocation. Different goals, different rules, different aesthetics. Creative coding gives you the freedom to prioritize feeling over precision
  • Found data (weather archives, census records, transit data) is data that already exists, repurposed as creative material -- like found-object sculpture. Collected data (step counts, mood journals, personal measurements) is data you gather yourself, making the art personal and unique. The Dear Data project by Lupi and Posavec is the best reference for collected-data art
  • The mapping problem is the core creative question: which data dimension maps to which visual property? Temperature to position, color, size, rotation -- each mapping tells a different story from the same dataset. The choice of mapping IS the art. Three examples from the same weather data produce completely different visual languages
  • Scale changes your approach. Small datasets (10-50 points) get individual treatment -- each element visible and distinguishable. Large datasets (1000+ points) become textures and density fields. You stop seeing individual elements and start seeing emergent patterns. The visual approach must match the data volume
  • Temporal data maps naturally to animation. One frame per data point creates narrative -- you feel changes unfolding rather than reading them statically. The spiral temperature example shows seasonal breathing that a static chart can't convey
  • Abstract vs literal is a spectrum. A bar chart of rainfall is literal and precise. Particle-density rain streaks are abstract and evocative. Creative coding lets you choose any point on this spectrum. Less precision often means more emotional impact
  • The radial step-count visualization encodes 12 weeks of daily steps as concentric rings -- thick golden arcs for active days, thin purple ones for sedentary days. The result looks organic, like tree rings, and is uniquely personal to whoever's data it encodes
  • Data about people carries ethical weight. Public data isn't automatically ethical to visualize. Health data, income data, identity data -- even anonymized -- can reveal patterns that harm real people. The creative tone should match the gravity of the data

Sallukes! Thanks for reading.

X

@femdev