Last episode we worked with real-time data streams -- WebSockets, Server-Sent Events, rolling buffers, smoothing, spike detection, connection health indicators, batching fast data, and the accumulation technique that turns a canvas into a long-exposure data photograph. Everything was alive and flowing. But here's the thing: all the data sources we've used throughout this arc -- geographic coordinates, timestamps, network connections, text corpora, live price feeds -- came from outside. External data about external things. The most intimate data source hasn't been touched yet: you.
Your step count. Your sleep patterns. Your screen time. What music you listened to this week. How many coffees you drank. When you felt happy, anxious, bored, focused. The data of a lived life. Personal data art turns the lens inward. Instead of visualizing earthquakes or stock tickers, you visualize yourself -- your habits, your rhythms, the patterns you can't see while you're living them. And the result is the most honest kind of data art there is.
This episode is about the quantified self as creative material. We'll look at the Dear Data project (the defining work in this space), explore manual vs automatic data collection, design personal visual languages, build week-long and year-long data portraits, compare your data against someone else's, and wrestle with the privacy question that sits at the heart of it all. We used mapping techniques from episode 82, temporal layouts from episode 84, and text analysis from episode 86 -- all of those apply here, but with data that's uniquely yours.
In 2014, Giorgia Lupi and Stefanie Posavec started a year-long project. Each week, they picked a theme -- complaints, phone usage, desires, laughter, doors, jealousy -- tracked data about that theme for seven days, then drew a hand-made visualization on a postcard and mailed it across the Atlantic. Lupi in New York, Posavec in London. Fifty-two weeks, fifty-two postcards each.
Dear Data is the reference point for personal data art. Not because the visualizations are technically complex (they're hand-drawn, sometimes messy), but because the project demonstrated something important: personal data art is a practice, not a product. The act of tracking changes how you pay attention. You notice things you normally filter out. How many times did you complain today? You don't know until you start counting. And once you start counting, you become aware of the pattern, and the pattern changes because you're watching it.
The postcards themselves are beautiful -- custom visual encodings invented fresh each week. One week, complaints are represented as colored spirals where the coil tightness indicates intensity. Another week, phone pickups are dots arranged in a timeline where color indicates the reason (boredom, notification, intentional). There's no standard chart type. Every week invents its own visual language. The legend IS part of the artwork.
We can't mail postcards in code, but we can build the same practice digitally. Track, encode, visualize, reflect.
Personal data comes from two sources, and they feel completely different.
Automatic data is effortless. Your phone tracks screen time without asking. A fitness tracker logs steps, heart rate, sleep duration. Spotify wraps your listening history into neat JSON. Bank transactions record every purchase with timestamps and amounts. The data exists whether you think about it or not. It's comprehensive, precise, and emotionally flat. You didn't choose to record it -- a machine did.
Manual data requires discipline. You decide to track your mood on a 1-10 scale, five times a day. You tally how many times you said "sorry" in conversations. You note what you ate, how it made you feel, what the weather was like. Manual data is incomplete, subjective, biased by what you remember to write down. But it's rich. The act of recording is itself a creative act -- you decide what to measure, how to categorize it, what counts.
For personal data art, manual data is usually more interesting. Not because it's more accurate (it's not), but because the measuring process involves decisions that encode your perspective. When you rate your mood as a 6, that's your interpretation. When a fitness tracker says you slept 7.2 hours, that's just a number. The subjectivity is the point.
// structure for manual daily tracking
const dailyEntry = {
date: '2025-03-15',
mood: 7, // 1-10 scale, your subjective rating
energy: 5, // 1-10
coffees: 3,
outsideMinutes: 45,
screenHours: 6.5,
bestMoment: 'walk in the park',
worstMoment: 'stuck in traffic',
weather: 'cloudy',
sleptWell: true,
exercised: false,
socialInteractions: 4 // rough count of meaningful conversations
};
// a week of manual data
const weekData = [
{ date: '2025-03-10', mood: 6, energy: 4, coffees: 4, outsideMinutes: 20, screenHours: 8 },
{ date: '2025-03-11', mood: 7, energy: 6, coffees: 3, outsideMinutes: 60, screenHours: 5 },
{ date: '2025-03-12', mood: 5, energy: 3, coffees: 5, outsideMinutes: 10, screenHours: 9 },
{ date: '2025-03-13', mood: 8, energy: 7, coffees: 2, outsideMinutes: 90, screenHours: 4 },
{ date: '2025-03-14', mood: 6, energy: 5, coffees: 3, outsideMinutes: 30, screenHours: 7 },
{ date: '2025-03-15', mood: 7, energy: 5, coffees: 3, outsideMinutes: 45, screenHours: 6.5 },
{ date: '2025-03-16', mood: 9, energy: 8, coffees: 1, outsideMinutes: 120, screenHours: 3 }
];
Look at that week. Wednesday was rough -- mood 5, energy 3, five coffees, barely went outside, nine hours of screen time. Sunday was great -- mood 9, one coffee, two hours outside. You can already see a story forming. The data is telling you something about the relationship between outdoor time and mood that you probably sensed but never quantified. That's the power of tracking.
Standard charts -- bar charts, line charts, pie charts -- work fine for analytical dashboards. But personal data art isn't a dashboard. It's a portrait. And portraits deserve their own visual vocabulary.
The Dear Data approach: invent symbols for YOUR data. What does a circle mean in your system? A line? A color? There are no rules. The visual encoding is part of the creative output. Your legend is your artistic signature.
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const weekData = [
{ mood: 6, energy: 4, coffees: 4, outside: 20, screen: 8 },
{ mood: 7, energy: 6, coffees: 3, outside: 60, screen: 5 },
{ mood: 5, energy: 3, coffees: 5, outside: 10, screen: 9 },
{ mood: 8, energy: 7, coffees: 2, outside: 90, screen: 4 },
{ mood: 6, energy: 5, coffees: 3, outside: 30, screen: 7 },
{ mood: 7, energy: 5, coffees: 3, outside: 45, screen: 6.5 },
{ mood: 9, energy: 8, coffees: 1, outside: 120, screen: 3 }
];
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 600);
for (let i = 0; i < weekData.length; i++) {
const d = weekData[i];
const cx = 70 + i * 100;
const cy = 300;
// mood = circle size (bigger = happier)
const moodRadius = d.mood * 4;
// energy = vertical position (higher = more energy)
const yOffset = (d.energy - 5) * 20;
// outside minutes = ring thickness
const ringWidth = d.outside / 30;
// coffees = small dots orbiting the circle
const coffeeAngleStep = (Math.PI * 2) / Math.max(d.coffees, 1);
// screen time = opacity of a background rectangle (more screen = darker overlay)
const screenAlpha = d.screen / 12;
// background: screen time glow
ctx.fillStyle = `rgba(60, 30, 80, ${screenAlpha * 0.3})`;
ctx.fillRect(cx - 40, cy - yOffset - 60, 80, 120);
// outer ring: outside time
ctx.beginPath();
ctx.arc(cx, cy - yOffset, moodRadius + ringWidth + 2, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(80, 200, 140, ${0.3 + ringWidth * 0.05})`;
ctx.lineWidth = ringWidth;
ctx.stroke();
// main circle: mood
const moodHue = 200 + (d.mood - 1) * 15;
ctx.beginPath();
ctx.arc(cx, cy - yOffset, moodRadius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${moodHue}, 55%, 50%, 0.6)`;
ctx.fill();
// coffee dots
for (let c = 0; c < d.coffees; c++) {
const angle = coffeeAngleStep * c - Math.PI / 2;
const orbitR = moodRadius + ringWidth + 10;
const dotX = cx + Math.cos(angle) * orbitR;
const dotY = (cy - yOffset) + Math.sin(angle) * orbitR;
ctx.beginPath();
ctx.arc(dotX, dotY, 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(180, 120, 60, 0.7)';
ctx.fill();
}
}
// day labels
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(140, 150, 170, 0.4)';
for (let i = 0; i < 7; i++) {
ctx.fillText(days[i], 70 + i * 100, 420);
}
Seven days, seven glyphs. Each one encodes five data dimensions simultaneously: mood (circle size), energy (vertical position), outside time (ring thickness), coffees (orbiting dots), screen time (background darkness). Wednesday is small, low, ringed by coffee dots, sitting in a dark rectangle. Sunday is big, high, wrapped in a thick green ring, one tiny coffee dot, no screen glow. The visual language is entirely custom -- nobody else uses this encoding. That's the point. It's YOUR visual system for YOUR data.
See where this is going? :-) The encoding decisions are creative decisions. You could just as easily map mood to color and energy to size. Or coffees to height and screen time to circle count. Each mapping reveals different relationships. The one above emphasizes the mood-energy-outside triangle because those three felt most connected in the data.
A week is the right scope for a first personal data piece. Seven data points is small enough to track reliably (you won't forget) and large enough to show a pattern (weekday vs weekend rhythms emerge clearly). The constraint forces focus: you can't track everything, so you pick the two or three metrics that matter most to you right now.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// one week: mood and hours of sleep
const week = [
{ day: 'Mon', mood: 5, sleep: 6.5 },
{ day: 'Tue', mood: 6, sleep: 7.0 },
{ day: 'Wed', mood: 4, sleep: 5.5 },
{ day: 'Thu', mood: 7, sleep: 7.5 },
{ day: 'Fri', mood: 7, sleep: 7.0 },
{ day: 'Sat', mood: 8, sleep: 8.5 },
{ day: 'Sun', mood: 9, sleep: 9.0 }
];
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 400);
for (let i = 0; i < week.length; i++) {
const d = week[i];
const x = 80 + i * 110;
// sleep as a vertical bar (taller = more sleep)
const sleepH = d.sleep * 25;
const sleepY = 350 - sleepH;
// color from mood: cold blue (low) to warm amber (high)
const moodNorm = (d.mood - 1) / 9;
const hue = moodNorm * 40 + (1 - moodNorm) * 220;
const light = 25 + moodNorm * 20;
ctx.fillStyle = `hsl(${hue}, 55%, ${light}%)`;
ctx.fillRect(x - 20, sleepY, 40, sleepH);
// mood as a dot on the bar
const moodY = 350 - (d.mood / 10) * 300;
ctx.beginPath();
ctx.arc(x, moodY, 6, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${hue}, 65%, ${light + 15}%)`;
ctx.fill();
// connecting line between mood dots
if (i > 0) {
const prevX = 80 + (i - 1) * 110;
const prevMoodY = 350 - (week[i - 1].mood / 10) * 300;
ctx.beginPath();
ctx.moveTo(prevX, prevMoodY);
ctx.lineTo(x, moodY);
ctx.strokeStyle = 'rgba(180, 160, 200, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
}
// day label
ctx.fillStyle = 'rgba(140, 150, 170, 0.5)';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText(d.day, x, 375);
}
Sleep as bar height. Mood as dot position and color. The connecting line between mood dots shows the emotional trajectory across the week. Wednesday -- short bar, cold blue, dot sitting low. Sunday -- tall bar, warm amber, dot riding high. The correlation is right there in front of you: more sleep, better mood. You knew that intellectually. But seeing it in your own data, drawn from your own week, hits differently.
This is the simplest version. No axes, no labels beyond the day names. Not a chart -- a portrait. The visual weight and color tell the story. Someone looking at this doesn't need a legend to feel the difference between Wednesday and Sunday.
A week shows a rhythm. A year shows a life. Three hundred sixty-five data points arranged in a grid, a spiral, or a timeline reveal patterns you can't perceive day-by-day: seasonal shifts, weekly cycles, gradual trends, sudden changes. The calendar heatmap layout from episode 84 works perfectly here.
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// generate a year of simulated mood data
// (in real use, this comes from your actual tracking)
const yearMood = [];
for (let d = 0; d < 365; d++) {
// base mood with seasonal variation (happier in summer)
const seasonalBase = 6 + Math.sin((d / 365) * Math.PI * 2 - Math.PI / 2) * 1.5;
// weekly cycle (weekends slightly better)
const dayOfWeek = d % 7;
const weekendBoost = (dayOfWeek >= 5) ? 0.8 : 0;
// random daily variation
const noise = (Math.random() - 0.5) * 3;
// occasional bad days
const badDay = Math.random() < 0.05 ? -3 : 0;
const mood = Math.max(1, Math.min(10, seasonalBase + weekendBoost + noise + badDay));
yearMood.push(mood);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 600);
// calendar grid: 52 columns (weeks), 7 rows (days)
const cellW = 12;
const cellH = 12;
const offsetX = 60;
const offsetY = 80;
for (let d = 0; d < 365; d++) {
const week = Math.floor(d / 7);
const dayOfWeek = d % 7;
const x = offsetX + week * (cellW + 2);
const y = offsetY + dayOfWeek * (cellH + 2);
const mood = yearMood[d];
const norm = (mood - 1) / 9;
const hue = norm * 40 + (1 - norm) * 240;
const lightness = 15 + norm * 30;
ctx.fillStyle = `hsl(${hue}, 55%, ${lightness}%)`;
ctx.fillRect(x, y, cellW, cellH);
}
// month labels (approximate)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
ctx.font = '9px monospace';
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(120, 130, 150, 0.4)';
for (let m = 0; m < 12; m++) {
const weekStart = Math.floor((m / 12) * 52);
const x = offsetX + weekStart * (cellW + 2) + cellW * 2;
ctx.fillText(months[m], x, offsetY - 10);
}
// day labels
const dayLabels = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
ctx.textAlign = 'right';
for (let d = 0; d < 7; d++) {
const y = offsetY + d * (cellH + 2) + cellH / 2 + 3;
ctx.fillText(dayLabels[d], offsetX - 8, y);
}
A year of mood, one cell per day. The seasonal arc is visible -- the left side (winter) runs cooler, the right side (summer) warmer. Weekend rows are slightly brighter than weekday rows. Those scattered dark cells? Bad days. They're random in the simulated data, but in real data they'd correspond to actual events -- illness, conflict, stress, loss. The pattern of bad days across a year is its own story. Do they cluster? Do they follow a trigger? Are they seasonal?
The calendar layout is just one option. A spiral (365 points wrapping outward from center) emphasizes cycles. A simple horizontal timeline emphasizes trend. A circular layout (days as degrees around a circle, 0 degrees = January 1, 360 = December 31) emphasizes yearly rhythm. Same data, different shapes, different questions answered. We covered these temporal layouts in episode 84 -- all applicable here.
One of the most powerful things you can do with personal data is compare yourself across time. Your mood this March vs last March. Your sleep pattern during a project deadline vs during vacation. Your coffee consumption before and after you started exercising. The comparison reveals change in a way that memory can't -- memory smooths over details, but data doesn't.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 500;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// two weeks: before and after starting a morning walk habit
const before = [
{ mood: 5, sleep: 6.0, energy: 4 },
{ mood: 6, sleep: 6.5, energy: 5 },
{ mood: 4, sleep: 5.5, energy: 3 },
{ mood: 5, sleep: 6.0, energy: 4 },
{ mood: 6, sleep: 7.0, energy: 5 },
{ mood: 7, sleep: 7.5, energy: 6 },
{ mood: 6, sleep: 7.0, energy: 5 }
];
const after = [
{ mood: 7, sleep: 7.0, energy: 6 },
{ mood: 7, sleep: 7.5, energy: 7 },
{ mood: 6, sleep: 7.0, energy: 6 },
{ mood: 8, sleep: 7.5, energy: 7 },
{ mood: 8, sleep: 8.0, energy: 8 },
{ mood: 9, sleep: 8.0, energy: 8 },
{ mood: 8, sleep: 7.5, energy: 7 }
];
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 500);
function drawWeek(data, startX, startY, label, hueBase) {
ctx.fillStyle = 'rgba(160, 170, 190, 0.4)';
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.fillText(label, startX + 180, startY - 15);
for (let i = 0; i < data.length; i++) {
const d = data[i];
const x = startX + i * 55;
// mood as main circle
const radius = d.mood * 3;
const hue = hueBase + (d.mood - 1) * 8;
// energy as y position
const y = startY + 80 - d.energy * 10;
// sleep as ring
const ringWidth = (d.sleep - 4) * 1.5;
// ring
ctx.beginPath();
ctx.arc(x, y, radius + ringWidth + 2, 0, Math.PI * 2);
ctx.strokeStyle = `hsla(${hue + 30}, 40%, 40%, 0.3)`;
ctx.lineWidth = ringWidth;
ctx.stroke();
// main circle
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue}, 55%, 50%, 0.6)`;
ctx.fill();
}
}
drawWeek(before, 40, 100, 'Before (no morning walk)', 210);
drawWeek(after, 40, 320, 'After (daily morning walk)', 140);
// day labels
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
ctx.font = '9px monospace';
ctx.fillStyle = 'rgba(120, 130, 150, 0.3)';
ctx.textAlign = 'center';
for (let i = 0; i < 7; i++) {
ctx.fillText(days[i], 40 + i * 55, 250);
ctx.fillText(days[i], 40 + i * 55, 470);
}
The "before" row sits low, in cool tones, small circles, thin rings. The "after" row floats higher, in warmer greens, bigger circles, thicker rings. The visual differnce is immediate. You don't need to read numbers. The before-after contrast tells the story in shape and color.
This is where personal data art becomes genuinely useful beyond aesthetics. Seeing the impact of a habit change in your own data, in your own visual system, is more convincing than any generic health article. You built this. You tracked this. The data is yours. The conclusion is inescapable because it came from you.
Here's the thing about personal data art that makes it different from every other data visualization: the data is you. Publishing a bar chart of your sleep patterns is trivially a privacy act. But publishing a visualization of your mood scores, your anxious days, your relationship patterns? That's vulnerability. And the tension between revealing and concealing is part of the art.
You control the abstraction level. Show the raw numbers and the viewer knows everything. Show only the visual pattern -- shapes and colors without labels, no axis, no legend -- and the viewer sees beauty without knowing what it means. You know. They don't. The gap between what you know and what they know is the artistic space.
const canvas = document.createElement('canvas');
canvas.width = 700;
canvas.height = 700;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 30 days of "something" -- the viewer doesn't know what
const data = [
4, 6, 7, 3, 5, 8, 7, 6, 2, 5,
6, 7, 8, 4, 3, 6, 7, 8, 9, 5,
4, 6, 7, 8, 3, 5, 7, 8, 9, 7
];
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 700, 700);
const cx = 350;
const cy = 350;
for (let i = 0; i < data.length; i++) {
const angle = (i / 30) * Math.PI * 2 - Math.PI / 2;
const nextAngle = ((i + 1) / 30) * Math.PI * 2 - Math.PI / 2;
const value = data[i];
const innerR = 80;
const outerR = innerR + value * 25;
// petal shape
ctx.beginPath();
ctx.moveTo(
cx + Math.cos(angle) * innerR,
cy + Math.sin(angle) * innerR
);
ctx.quadraticCurveTo(
cx + Math.cos((angle + nextAngle) / 2) * outerR,
cy + Math.sin((angle + nextAngle) / 2) * outerR,
cx + Math.cos(nextAngle) * innerR,
cy + Math.sin(nextAngle) * innerR
);
const hue = 200 + value * 15;
const alpha = 0.15 + (value / 10) * 0.3;
ctx.fillStyle = `hsla(${hue}, 50%, 45%, ${alpha})`;
ctx.fill();
// thin line to outer edge
ctx.beginPath();
ctx.moveTo(cx, cy);
const midAngle = (angle + nextAngle) / 2;
ctx.lineTo(
cx + Math.cos(midAngle) * outerR,
cy + Math.sin(midAngle) * outerR
);
ctx.strokeStyle = `hsla(${hue}, 40%, 50%, ${alpha * 0.4})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
// center circle
ctx.beginPath();
ctx.arc(cx, cy, 78, 0, Math.PI * 2);
ctx.fillStyle = '#0a0a1a';
ctx.fill();
ctx.beginPath();
ctx.arc(cx, cy, 78, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(100, 120, 160, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
Thirty petals radiating from center. Each petal's length encodes... something. The viewer sees a flower-like form, some petals longer than others, colors shifting between cool and warm. It's beautiful. It's data. But what data? Only you know. Maybe it's your anxiety level. Maybe it's how many minutes you meditated. Maybe it's how many times you checked your phone. The abstraction protects you while the pattern speaks.
This is the creative space that personal data art opens up. You can share the shape of your experience without sharing the experience itself. The medium IS the privacy control.
The most powerful personal data art isn't a one-off project. It's a practice. Daily collection, weekly visualization. Over months, you build a visual autobiography. The discipline of looking at your own patterns changes how you live -- not because the visualization tells you what to do, but because the act of measurement makes invisible things visible.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 500;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 4 weeks of daily data (28 days)
// each day: mood (1-10), primary activity, social level (1-5)
const diary = [];
const activities = ['work', 'creative', 'social', 'rest', 'exercise'];
const activityColors = {
work: { h: 220, s: 40, l: 35 },
creative: { h: 280, s: 50, l: 45 },
social: { h: 40, s: 55, l: 45 },
rest: { h: 160, s: 35, l: 30 },
exercise: { h: 100, s: 50, l: 40 }
};
for (let d = 0; d < 28; d++) {
const dayOfWeek = d % 7;
const isWeekend = dayOfWeek >= 5;
const activity = isWeekend
? activities[1 + Math.floor(Math.random() * 4)]
: (Math.random() < 0.7 ? 'work' : activities[Math.floor(Math.random() * 5)]);
diary.push({
mood: 4 + Math.floor(Math.random() * 6),
activity: activity,
social: isWeekend ? 3 + Math.floor(Math.random() * 3) : 1 + Math.floor(Math.random() * 3)
});
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 500);
// draw as a grid: 4 rows (weeks) x 7 columns (days)
const cellW = 100;
const cellH = 100;
const padX = 55;
const padY = 50;
for (let d = 0; d < 28; d++) {
const col = d % 7;
const row = Math.floor(d / 7);
const entry = diary[d];
const x = padX + col * (cellW + 8);
const y = padY + row * (cellH + 8);
const ac = activityColors[entry.activity];
// cell background tinted by activity
ctx.fillStyle = `hsla(${ac.h}, ${ac.s}%, ${ac.l}%, 0.15)`;
ctx.fillRect(x, y, cellW, cellH);
// mood as central circle
const radius = entry.mood * 3.5;
ctx.beginPath();
ctx.arc(x + cellW / 2, y + cellH / 2, radius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${ac.h}, ${ac.s + 10}%, ${ac.l + 15}%, 0.5)`;
ctx.fill();
// social level as dots around the circle
for (let s = 0; s < entry.social; s++) {
const sa = (s / entry.social) * Math.PI * 2 - Math.PI / 2;
const sx = x + cellW / 2 + Math.cos(sa) * (radius + 8);
const sy = y + cellH / 2 + Math.sin(sa) * (radius + 8);
ctx.beginPath();
ctx.arc(sx, sy, 2.5, 0, Math.PI * 2);
ctx.fillStyle = `hsla(40, 50%, 60%, 0.5)`;
ctx.fill();
}
}
// column headers
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
ctx.font = '9px monospace';
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(130, 140, 160, 0.4)';
for (let c = 0; c < 7; c++) {
ctx.fillText(dayNames[c], padX + c * (cellW + 8) + cellW / 2, padY - 8);
}
// row labels
ctx.textAlign = 'right';
for (let r = 0; r < 4; r++) {
ctx.fillText('W' + (r + 1), padX - 10, padY + r * (cellH + 8) + cellH / 2 + 3);
}
Four weeks as a 4x7 grid. Each cell is a day. Background color from the primary activity -- blue for work, purple for creative, warm amber for social, green for rest. Circle size from mood. Satellite dots from social interaction count. The weekday/weekend pattern is obvious: weekdays are mostly blue (work), weekends shift to varied colors. High-social days (more orbiting dots) tend to have larger circles (higher mood). Creative days have a distinctive purple tint.
Over months of these weekly grids, you'd see trends emerge. Gradual shifts in your activity distribution. Seasonal mood patterns. The week where everything went wrong shows up as a row of small, cool circles. The week of the vacation is warm, large, busy with social dots. Your life, week by week, in shapes and colors.
Even your own data can reveal information about others. Your location history shows where you went -- and who you were with. Your text message count implies someone on the other end. Your mood dropping after a conversation implicates the person you talked to. The ethics of personal data art are surprisingly complex for data that's ostensibly just about you.
The Dear Data project handled this by abstracting. When tracking "times I said thank you," the person being thanked wasn't identified. When tracking "desires," the specifics were encoded into shapes that only the creator could decode. The visualization shared the pattern, not the content.
For your own practice: before you publish or share a personal data visualization, think about what it implies about the people in your life. A mood chart that dips every time you visit your parents tells a story about your relationship with your parents, not just about your mood. An exercise tracker that stops for three months might coincide with a breakup. Data is never just about the individual -- it always sits in a web of relationships.
This isn't a reason not to do personal data art. It's a reason to be thoughtful about what you share and how. Abstraction is your tool. Encode, don't expose.
The real insight in personal data isn't any single metric -- it's the relationship between metrics. Does more sleep predict better mood? Does screen time correlate with energy? A scatter plot of one metric against another makes correlations visible instantly.
const canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 30 days of sleep vs mood
const days = [];
for (let i = 0; i < 30; i++) {
const sleep = 5 + Math.random() * 4;
// mood correlates loosely with sleep + noise
const mood = Math.min(10, Math.max(1, sleep * 0.8 + (Math.random() - 0.5) * 3));
days.push({ sleep, mood });
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 600);
// axes
ctx.strokeStyle = 'rgba(60, 70, 90, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(60, 540);
ctx.lineTo(560, 540);
ctx.moveTo(60, 540);
ctx.lineTo(60, 40);
ctx.stroke();
// axis labels
ctx.fillStyle = 'rgba(120, 130, 150, 0.4)';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('hours of sleep', 310, 580);
ctx.save();
ctx.translate(20, 290);
ctx.rotate(-Math.PI / 2);
ctx.fillText('mood (1-10)', 0, 0);
ctx.restore();
// plot points
for (const d of days) {
const x = 60 + ((d.sleep - 4) / 6) * 500;
const y = 540 - ((d.mood - 1) / 9) * 500;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
const moodNorm = (d.mood - 1) / 9;
const hue = moodNorm * 40 + (1 - moodNorm) * 220;
ctx.fillStyle = `hsla(${hue}, 55%, 50%, 0.5)`;
ctx.fill();
}
The dots drift upward and to the right -- more sleep, higher mood. The correlation isn't perfect (human data never is), but the trend is visible. The scatter is part of the truth too: some days you slept well and still felt lousy. Some days you were short on sleep but your mood was great. Life is noisy. The scatter plot shows both the trend and the noise, which is more honest than a clean trend line that pretends the noise doesn't exist.
You can build this for any pair of metrics from your tracking data. Screen time vs energy. Coffees vs sleep quality. Outside minutes vs mood. Each scatter tells you something about how your daily variables interconnect. Over a month of data, patterns emerge that your gut feeling always suspected but couldn't prove.
Personal data art has a tradition of physicality that most data visualization doesn't. Dear Data was postcards. Other artists have turned personal data into woven textiles, laser-cut discs, 3D-printed sculptures, hand-drawn journals. The physicality adds intimacy -- you can touch it, hang it on a wall, give it as a gift.
We work digitally in this series, but you can bridge the gap. The simplest way: export your canvas and print it.
// export canvas to downloadable PNG
function exportCanvas(canvas, filename) {
const link = document.createElement('a');
link.download = filename || 'data-portrait.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
// export as SVG (for laser cutting, high-res printing)
function exportSVG(width, height, drawFn) {
let svg = ``;
svg += ``;
// drawFn returns an array of SVG elements as strings
const elements = drawFn();
for (const el of elements) {
svg += el;
}
svg += '';
const blob = new Blob([svg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'data-portrait.svg';
link.href = url;
link.click();
URL.revokeObjectURL(url);
}
// usage: after drawing to canvas
// exportCanvas(canvas, 'week-portrait-mar-10.png');
PNG for quick prints. SVG for scalable output -- laser cutters, plotters, and high-resolution printers all work with vector formats. Episode 29 covered SVG generation in depth; the same techniques apply here. Your weekly data portrait, printed on good paper, framed on a wall, is genuinely a beautiful object. And it means something that a random abstract poster doesn't, because the shapes encode your actual lived experience.
The most memorable Dear Data postcards aren't the cleanest ones. They're the ones where the pen slipped, where the spacing is uneven, where a color ran out and they switched mid-drawing. The imperfection is evidence of a human hand. In personal data art, that evidence matters more than precision.
Allez, time to build a complete personal data portrait. We'll combine mood, sleep, coffees, outside time, and screen time into a single weekly visualization that uses custom encodings, a circular layout, and layered visual channels. This is the kind of piece you could generate every Sunday evening from your week's tracking data.
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 800;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const week = [
{ day: 'Mon', mood: 5, sleep: 6.0, coffees: 4, outside: 15, screen: 8.5 },
{ day: 'Tue', mood: 6, sleep: 7.0, coffees: 3, outside: 30, screen: 7.0 },
{ day: 'Wed', mood: 4, sleep: 5.0, coffees: 5, outside: 10, screen: 9.0 },
{ day: 'Thu', mood: 7, sleep: 7.5, coffees: 2, outside: 60, screen: 5.5 },
{ day: 'Fri', mood: 6, sleep: 6.5, coffees: 3, outside: 25, screen: 7.5 },
{ day: 'Sat', mood: 8, sleep: 8.0, coffees: 2, outside: 90, screen: 4.0 },
{ day: 'Sun', mood: 9, sleep: 9.0, coffees: 1, outside: 120, screen: 2.5 }
];
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 800);
const centerX = 400;
const centerY = 400;
for (let i = 0; i < 7; i++) {
const d = week[i];
const angle = (i / 7) * Math.PI * 2 - Math.PI / 2;
const nextAngle = ((i + 1) / 7) * Math.PI * 2 - Math.PI / 2;
const midAngle = (angle + nextAngle) / 2;
// base radius from mood
const baseR = 100 + d.mood * 22;
// sleep extends the outer reach
const sleepR = baseR + (d.sleep - 4) * 12;
// draw the day's sector
// outer arc: sleep
ctx.beginPath();
ctx.moveTo(
centerX + Math.cos(angle) * 90,
centerY + Math.sin(angle) * 90
);
ctx.lineTo(
centerX + Math.cos(angle) * sleepR,
centerY + Math.sin(angle) * sleepR
);
ctx.arc(centerX, centerY, sleepR, angle, nextAngle);
ctx.lineTo(
centerX + Math.cos(nextAngle) * 90,
centerY + Math.sin(nextAngle) * 90
);
ctx.arc(centerX, centerY, 90, nextAngle, angle, true);
ctx.closePath();
// color from mood (cold to warm)
const moodNorm = (d.mood - 1) / 9;
const hue = moodNorm * 40 + (1 - moodNorm) * 230;
const sat = 45 + moodNorm * 15;
const light = 20 + moodNorm * 20;
ctx.fillStyle = `hsla(${hue}, ${sat}%, ${light}%, 0.4)`;
ctx.fill();
ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${light + 10}%, 0.3)`;
ctx.lineWidth = 1;
ctx.stroke();
// outside time as a green arc at the outer edge
const outsideWidth = d.outside / 20;
if (outsideWidth > 0.5) {
ctx.beginPath();
ctx.arc(centerX, centerY, sleepR + 5, angle + 0.02, nextAngle - 0.02);
ctx.strokeStyle = `rgba(80, 200, 140, ${0.2 + outsideWidth * 0.04})`;
ctx.lineWidth = outsideWidth;
ctx.stroke();
}
// coffee dots along the inner edge
for (let c = 0; c < d.coffees; c++) {
const coffeeAngle = angle + (c + 0.5) / d.coffees * (nextAngle - angle);
const coffeeR = 80;
const cx2 = centerX + Math.cos(coffeeAngle) * coffeeR;
const cy2 = centerY + Math.sin(coffeeAngle) * coffeeR;
ctx.beginPath();
ctx.arc(cx2, cy2, 2.5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(180, 120, 60, 0.6)';
ctx.fill();
}
// screen time as inner shadow (more screen = darker inner area)
const screenAlpha = (d.screen / 10) * 0.15;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, 90, angle, nextAngle);
ctx.closePath();
ctx.fillStyle = `rgba(80, 40, 120, ${screenAlpha})`;
ctx.fill();
// day label
const labelR = sleepR + 20;
const labelX = centerX + Math.cos(midAngle) * labelR;
const labelY = centerY + Math.sin(midAngle) * labelR;
ctx.fillStyle = 'rgba(140, 150, 170, 0.4)';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText(d.day, labelX, labelY + 3);
}
// center label
ctx.fillStyle = 'rgba(100, 110, 130, 0.3)';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('WEEK', centerX, centerY - 5);
ctx.fillText('Mar 10-16', centerX, centerY + 8);
A radial portrait. Seven sectors, one per day. Sector size is uniform (each day gets a seventh of the circle). Sector reach encodes two things: the base radius from mood, the outer extension from sleep. Color temperature shifts from cold (bad days) to warm (good days). Green arcs at the outer edge mark time spent outside -- thick green bands on Saturday and Sunday, barely visible on Wednesday. Coffee dots cluster along the inner rim. The center darkens with screen time -- Wednesday's core is the darkest (9 hours), Sunday's is almost clear (2.5 hours).
The whole week in a single shape. Wednesday's sector is short, cold, dark at the core, bristling with coffee dots, no green. Sunday's sector reaches outward, warm amber, wrapped in green, one tiny coffee dot, light core. The visual contrast between the worst and best days of the week is dramatic -- and all from data you tracked yourself.
That's the promise of personal data art. It turns the invisible rhythms of your life into visible patterns. Not to optimize (that's the quantified-self productivity mindset, which is fine but not what we're doing here). To see. To create something beautiful from the raw material of being alive. The data is yours. The visualization is yours. The meaning is yours.
The data art arc has taken us through geographic data (episode 83), temporal data (84), networks (85), text (86), real-time streams (87), and now personal data (this episode). Each data type needed its own visual language, its own encoding strategies, its own set of creative decisions. And every technique from the earlier episodes -- the map() function, log scaling, calendar layouts, force-directed graphs, color mapping -- showed up again in new contexts. Data art is accumulative: the more tools you have, the richer your vocabulary, the more you can say with a single canvas.
Sallukes! Thanks for reading.
X