Last episode we went geographic -- projections, GeoJSON boundaries, choropleths, earthquake scatter, flight routes, GPS traces. Geography adds a sense of place to data art that nothing else can match. The Ring of Fire appearing from nothing but coordinate dots. Your daily commute emerging as a bright thread through faint weekend wanderings. Geography grounds data in the physical world.
But there's another dimension that shows up in almost every dataset, and it's arguably even more fundamental than space: time. Every server log has timestamps. Every weather dataset has dates. Every stock ticker, every heartbeat monitor, every sleep tracker -- time is the axis that most data rides on. And how you visualize time on a flat canvas is a genuinely creative decision. A timeline? A spiral? A calendar grid? A clock? Each one reveals different patterns and hides others. The representation IS the question you're asking the data.
This episode is about temporal visualization. We'll build timelines, spirals, calendar heatmaps, clock diagrams, and animated playback -- all the ways you can represent the flow of time in creative code. We touched on time briefly back in episode 13 when we used trigonometry for circular motion, and again in episode 16 with easing functions for smooth animation over time. Now time itself becomes the data we're visualizing, not just the medium our animations run in.
The most natural temporal visualization is a line. Time flows left to right (in most cultures), values go up and down. It's what we're all used to from stock charts, weather graphs, fitness apps. Simple, effective, and boring if you stop there -- but let's build the foundation.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 300;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 365 days of fake temperature data
const temps = [];
for (let d = 0; d < 365; d++) {
const seasonal = Math.sin((d / 365) * Math.PI * 2 - Math.PI / 2) * 12;
const daily = (Math.random() - 0.5) * 8;
temps.push(10 + seasonal + daily);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 300);
ctx.beginPath();
for (let i = 0; i < temps.length; i++) {
const x = (i / 364) * 860 + 20;
const y = 280 - ((temps[i] + 5) / 35) * 260;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = 'rgba(120, 180, 255, 0.6)';
ctx.lineWidth = 1.2;
ctx.stroke();
A year of temperature data. The seasonal wave is clear -- winter dip on the left, summer peak in the middle, back down on the right. The daily noise adds texture. It looks like a seismograph or an EKG -- the classic time series shape. Nothing surprising here, but it's the baseline everything else builds on.
The problem with straight timelines: they get long. A decade of daily data is 3,650 points. A century is 36,500. At some point the canvas isn't wide enough and you're either scrolling horizontally (bad for art) or compressing so hard that individual days become sub-pixel (bad for detail). That's when you need different layouts.
Here's where it gets interesting. Instead of spreading time in a straight line, wrap it around a spiral. Each revolution represents one cycle -- a year, a month, a week. Values that repeat at the same phase of the cycle line up radially. Seasonal patterns become visible as shapes in the spiral, because data from the same month in different years sits at the same angle.
const canvas = document.createElement('canvas');
canvas.width = 700;
canvas.height = 700;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 3 years of daily data
const days = 365 * 3;
const values = [];
for (let d = 0; d < days; d++) {
const yearPhase = (d % 365) / 365;
const seasonal = Math.sin(yearPhase * Math.PI * 2 - Math.PI / 2) * 0.4;
const noise = (Math.random() - 0.5) * 0.3;
values.push(0.5 + seasonal + noise);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 700, 700);
const cx = 350;
const cy = 350;
const startRadius = 40;
const endRadius = 310;
for (let i = 0; i < values.length; i++) {
const t = i / days;
const angle = (i / 365) * Math.PI * 2 - Math.PI / 2;
const radius = startRadius + t * (endRadius - startRadius);
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
// color from value: cold blue to warm orange
const hue = values[i] * 40 + (1 - values[i]) * 220;
const lightness = 25 + values[i] * 35;
ctx.beginPath();
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${hue}, 55%, ${lightness}%)`;
ctx.fill();
}
Three years spiraling outward from the center. Each revolution is one year. Summer sits at the bottom (warm orange dots), winter at the top (cool blue dots). Because the spiral grows outward, you can compare year-over-year patterns by looking radially -- "was this January warmer than last January?" becomes a visual comparison at the same angle, just at different radii.
The spiral layout takes the same data as the straight timeline and asks a completely different question. The timeline asks "what happened over time?" The spiral asks "what patterns repeat?" Same data, different structure, different insight. That's the whole point of this episode -- the temporal layout is a creative choice, not a given.
You've seen this one. Github's contribution graph is a calendar heatmap -- one cell per day, arranged in columns of 7 (one week), colored by activity. Mondays in one row, Tuesdays in the next, weeks flowing left to right. It reveals two kinds of patterns simultaneously: weekly rhythms (which days are active) and seasonal trends (which months are busy).
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 160;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// one year of "activity" data (like commits, or steps, or whatever)
const yearData = [];
for (let d = 0; d < 365; d++) {
const dayOfWeek = d % 7;
const weekdayBoost = dayOfWeek < 5 ? 0.3 : 0;
const seasonalBoost = Math.sin((d / 365) * Math.PI * 2) * 0.2;
const random = Math.random() * 0.5;
const skipDay = Math.random() < 0.15 ? 0 : 1;
yearData.push(Math.max(0, (weekdayBoost + seasonalBoost + random) * skipDay));
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 160);
const cellSize = 12;
const gap = 2;
for (let d = 0; d < 365; d++) {
const week = Math.floor(d / 7);
const dayOfWeek = d % 7;
const x = 20 + week * (cellSize + gap);
const y = 15 + dayOfWeek * (cellSize + gap);
const v = yearData[d];
const maxVal = 1.0;
if (v === 0) {
ctx.fillStyle = 'rgba(30, 35, 45, 0.6)';
} else {
const t = Math.min(v / maxVal, 1.0);
const lightness = 15 + t * 40;
ctx.fillStyle = `hsl(130, 50%, ${lightness}%)`;
}
ctx.fillRect(x, y, cellSize, cellSize);
}
The grid is 7 rows tall (days of the week) and ~52 columns wide (weeks of the year). Empty days are dark, active days glow green. You can immediately see: weekdays are brighter than weekends (the top 5 rows glow more than the bottom 2). And there's a seasonal wave -- the middle section is brighter because of the sinusoidal boost we added. Two temporal patterns, both visible at a glance, in a compact visual.
The beauty of the calendar heatmap is density. 365 data points in a space roughly 800x160 pixels. Every day is visible individually, and the overall pattern is visible too. Compare that to a timeline of 365 points -- the individual days blur together. The grid layout trades temporal continuity (days aren't connected by a line) for spatial efficiency (every day gets its own cell).
For data that has a daily cycle -- when do you wake up, when do events happen, when does traffic peak -- a clock layout is natural. 24 hours around a circle. Midnight at the top, noon at the bottom (or wherever you want it). Plot events at their hour angle. The clustering pattern shows your daily rhythm.
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 200 events with timestamps (hour of day)
const events = [];
for (let i = 0; i < 200; i++) {
const r = Math.random();
let hour;
if (r < 0.3) {
// morning cluster: 7-10
hour = 7 + Math.random() * 3 + (Math.random() - 0.5) * 0.5;
} else if (r < 0.6) {
// afternoon cluster: 13-17
hour = 13 + Math.random() * 4 + (Math.random() - 0.5) * 0.5;
} else if (r < 0.8) {
// evening cluster: 19-22
hour = 19 + Math.random() * 3;
} else {
// scattered throughout
hour = Math.random() * 24;
}
events.push(hour % 24);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 500, 500);
const cx = 250;
const cy = 250;
// draw hour marks
for (let h = 0; h < 24; h++) {
const angle = (h / 24) * Math.PI * 2 - Math.PI / 2;
const inner = 180;
const outer = 195;
const x1 = cx + Math.cos(angle) * inner;
const y1 = cy + Math.sin(angle) * inner;
const x2 = cx + Math.cos(angle) * outer;
const y2 = cy + Math.sin(angle) * outer;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = 'rgba(80, 90, 110, 0.5)';
ctx.lineWidth = h % 6 === 0 ? 2 : 1;
ctx.stroke();
}
// plot events
for (const hour of events) {
const angle = (hour / 24) * Math.PI * 2 - Math.PI / 2;
// randomize radius slightly so dots don't stack exactly
const r = 60 + Math.random() * 110;
const x = cx + Math.cos(angle) * r;
const y = cy + Math.sin(angle) * r;
ctx.beginPath();
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(100, 200, 180, 0.35)';
ctx.fill();
}
Events cluster visibly: a dense arc in the morning (7-10), another in the afternoon (13-17), another in the evening (19-22). The dead of night is nearly empty. The radius jitter spreads dots out so overlapping events are visible instead of stacking on top of each other. This is basicaly a polar scatter plot where the angle is the hour and the radius is random noise for readability.
The clock layout works because our brains already associate circular time with daily cycles. We grew up reading clocks. The "afternoon" being in a certain sector feels natural. A timeline showing the same events as a horizontal strip wouldn't carry that intuitive sense of daily rhythm.
Edward Tufte's favorite technique: make the same chart many times, once per time period. Same scale, same layout, different data. The repetition makes comparison effortless.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 4 years of monthly data (48 months)
const months = [];
for (let m = 0; m < 48; m++) {
const yearPhase = (m % 12) / 12;
const seasonal = Math.sin(yearPhase * Math.PI * 2 - Math.PI / 2) * 30;
const trend = m * 0.3;
const noise = (Math.random() - 0.5) * 15;
months.push(50 + seasonal + trend + noise);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 400);
// 4 small charts, one per year
const chartW = 180;
const chartH = 140;
for (let year = 0; year < 4; year++) {
const ox = 40 + year * 210;
const oy = 60;
// year label
ctx.fillStyle = 'rgba(180, 180, 200, 0.5)';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText(`Year ${year + 1}`, ox + chartW / 2, oy - 12);
// chart background
ctx.fillStyle = 'rgba(20, 25, 35, 0.8)';
ctx.fillRect(ox, oy, chartW, chartH);
// data for this year
const yearData = months.slice(year * 12, year * 12 + 12);
// bars
const barW = chartW / 12 - 2;
for (let m = 0; m < 12; m++) {
const value = yearData[m];
const barH = (value / 100) * chartH;
const x = ox + m * (barW + 2) + 1;
const y = oy + chartH - barH;
const hue = (m / 12) * 360;
ctx.fillStyle = `hsla(${hue}, 45%, 45%, 0.7)`;
ctx.fillRect(x, y, barW, barH);
}
}
Four years, side by side. Same vertical scale, same month-coloring. You can instantly see: the seasonal pattern repeats (summer bars tall, winter bars short), and there's an upward trend (Year 4 bars are taller overall than Year 1). Neither pattern would be as obvious in a single combined chart. The small multiples layout makes both patterns leap out because your eye naturally scans across and compares corresponding positions.
Small multiples work for any time unit. Twelve small charts for months of the year. Seven for days of the week. Twenty-four for hours of the day. The grid structure turns temporal comparison into spatial comparison, and spatial comparison is something your visual cortex is spectacularly good at.
A streamgraph is a stacked area chart with a twist -- instead of stacking from a flat baseline, the layers flow around a center axis. Each layer represents a category, and its thickness at any point represents that category's value at that time. The result looks organic, like a river splitting into tributaries, or geological strata.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 5 categories over 60 time steps
const categories = 5;
const steps = 60;
const data = [];
for (let c = 0; c < categories; c++) {
const series = [];
let val = 20 + Math.random() * 30;
for (let t = 0; t < steps; t++) {
val += (Math.random() - 0.5) * 10;
val = Math.max(5, Math.min(60, val));
series.push(val);
}
data.push(series);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 400);
const hues = [0, 60, 140, 210, 300];
// compute stacked values centered around the middle
for (let t = 0; t < steps; t++) {
const total = data.reduce((sum, series) => sum + series[t], 0);
const x = (t / (steps - 1)) * 860 + 20;
let yOffset = 200 - total / 2;
for (let c = 0; c < categories; c++) {
const h = data[c][t];
const y = yOffset;
// draw a vertical slice of this category
ctx.fillStyle = `hsla(${hues[c]}, 50%, 45%, 0.7)`;
ctx.fillRect(x - 1, y, 16, h);
yOffset += h;
}
}
Five colored streams flowing across the canvas. Each stream's thickness at any x-position tells you that category's value at that time. When one stream thickens, the others shift to make room. The total height at any point is the sum of all categories. You can see relative proportions changing over time -- which category dominates, when one shrinks and another grows.
Streamgraphs are visually beautiful but hard to read precisely. The shifting baseline means you can't easily compare exact values. They're better for showing overall flow and dominance patterns than for precise quantitative comparison. For creative coding that's usually fine -- we care about the feel of the data more than exact numbers.
Instead of showing all time at once, play it. Each frame adds the next data point. The viewer watches the pattern emerge over time, like watching a drawing being made. Hans Rosling popularized this with Gapminder -- a scatter plot that animates through decades, and you watch countries move across the chart as their statistics change.
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 200 data points arriving over time
const allPoints = [];
for (let i = 0; i < 200; i++) {
const t = i / 200;
allPoints.push({
x: 50 + t * 700 + (Math.random() - 0.5) * 40,
y: 200 + Math.sin(t * Math.PI * 4) * 100 + (Math.random() - 0.5) * 50,
time: i
});
}
let currentIdx = 0;
function drawFrame() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 400);
// draw all points up to current time
for (let i = 0; i <= currentIdx; i++) {
const p = allPoints[i];
const age = currentIdx - i;
const fade = Math.max(0.15, 1.0 - age / 80);
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
ctx.fillStyle = `rgba(140, 200, 255, ${fade})`;
ctx.fill();
}
// newest point gets a highlight
if (currentIdx < allPoints.length) {
const newest = allPoints[currentIdx];
ctx.beginPath();
ctx.arc(newest.x, newest.y, 7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 200, 100, 0.8)';
ctx.fill();
}
currentIdx++;
if (currentIdx >= allPoints.length) currentIdx = 0;
requestAnimationFrame(drawFrame);
}
drawFrame();
Points appear one by one, newest highlighted in gold, older ones fading. You watch the sine wave emerge from individual dots. The temporal sequence becomes a narrative -- "first this happened, then that." A static chart of all 200 points is a cloud. The animation reveals the order in which the cloud formed. That ordering often carries meaning that the final shape doesn't: the trajectory matters, not just the destination.
We covered animation basics way back in episode 3 and the requestAnimationFrame loop in episodes 16-17. The difference here is that the animation isn't decorative -- it's carrying data. The timing of each frame corresponds to a data timestamp. The animation IS the temporal visualization.
This one is my favourite. Take a year of hourly data. Map hour-of-day to the x-axis (0-23) and day-of-year to the y-axis (0-364). Each cell is one hour of one day. Color by activity value. The result is a dense 24x365 heatmap -- a "fingerprint" of your year where every temporal pattern is visible simultaneously.
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 700;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// generate hourly activity data for a year
// weekday mornings and afternoons are active, nights and weekends are quiet
const hourlyData = [];
for (let day = 0; day < 365; day++) {
const isWeekend = (day % 7) >= 5;
const row = [];
for (let hour = 0; hour < 24; hour++) {
let activity = 0.05;
if (!isWeekend) {
// morning commute
if (hour >= 7 && hour <= 9) activity = 0.5 + Math.random() * 0.4;
// work hours
else if (hour >= 10 && hour <= 17) activity = 0.3 + Math.random() * 0.3;
// evening
else if (hour >= 18 && hour <= 22) activity = 0.2 + Math.random() * 0.3;
} else {
// weekend: late mornings, sporadic afternoons
if (hour >= 10 && hour <= 14) activity = 0.2 + Math.random() * 0.3;
if (hour >= 16 && hour <= 21) activity = 0.15 + Math.random() * 0.25;
}
// summer vacation: two weeks of low activity
if (day >= 200 && day <= 214) activity *= 0.2;
row.push(activity);
}
hourlyData.push(row);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 500, 700);
const cellW = 18;
const cellH = 1.8;
const offsetX = 30;
const offsetY = 15;
for (let day = 0; day < 365; day++) {
for (let hour = 0; hour < 24; hour++) {
const v = hourlyData[day][hour];
const x = offsetX + hour * cellW;
const y = offsetY + day * cellH;
const hue = 200 + v * 40;
const lightness = 5 + v * 50;
ctx.fillStyle = `hsl(${hue}, 50%, ${lightness}%)`;
ctx.fillRect(x, y, cellW - 1, cellH);
}
}
The fingerprint reveals everything at once. Horizontal bands of brightness during work hours (7-17) on weekdays. Darker bands on weekends that repeat every 7 rows. A dark horizontal stripe around day 200-214 -- the summer vacation. The morning commute shows as a distinct bright column at hours 7-9. Evening activity fades gradually from hour 18 to 22.
365 days times 24 hours is 8,760 data points in one image. Every hour of the year, visible simultaneously. No scrolling, no animation, no interaction needed. This is the kind of dense temporal visualization that's impossible to achieve with a timeline. The 2D layout turns time into a spatial pattern, and your visual cortex can process the entire year in a single glance. :-)
This is where we connect back to last episode. Geographic data with timestamps -- earthquake histories, pandemic spread over months, flight patterns at different hours -- gets a whole new dimension when you combine spatial and temporal visualization.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 450;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
function project(lon, lat) {
return {
x: ((lon + 180) / 360) * 900,
y: ((90 - lat) / 180) * 450
};
}
// events with location AND time
const events = [];
for (let i = 0; i < 300; i++) {
const angle = Math.random() * Math.PI * 2;
const lon = 160 * Math.cos(angle) + (Math.random() - 0.5) * 25;
const lat = 15 * Math.sin(angle) + (Math.random() - 0.5) * 20;
events.push({
pos: project(lon, lat),
month: Math.floor(Math.random() * 12),
intensity: 0.3 + Math.random() * 0.7
});
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 900, 450);
// color by month: January blue, July red, cycling through the year
for (const ev of events) {
const monthHue = (ev.month / 12) * 360;
const r = 2 + ev.intensity * 5;
ctx.beginPath();
ctx.arc(ev.pos.x, ev.pos.y, r, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${monthHue}, 55%, 50%, ${ev.intensity * 0.6})`;
ctx.fill();
}
Each event dot is colored by its month. Early-year events are blue, mid-year events are green-yellow, late-year events are red-purple. If certain regions have more events in certain months, you'll see color clustering in those areas. The geographic and temporal dimensions are encoded simultaneously -- position for location, hue for time. This is the multi-channel mapping we built in episode 82, applied to spatio-temporal data.
Allez, time to build something personal. A week of hourly data -- what were you doing each hour? -- visualized as a radial diagram. Seven concentric rings (one per day), each ring divided into 24 segments (one per hour). Color and brightness encode activity level.
const canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// simulate a week of hourly activity
const weekData = [];
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
for (let day = 0; day < 7; day++) {
const isWeekend = day >= 5;
const hours = [];
for (let h = 0; h < 24; h++) {
let val = 0.05;
if (!isWeekend) {
if (h >= 7 && h <= 8) val = 0.6 + Math.random() * 0.3; // commute
if (h >= 9 && h <= 12) val = 0.7 + Math.random() * 0.2; // work morning
if (h >= 13 && h <= 17) val = 0.5 + Math.random() * 0.3; // work afternoon
if (h >= 18 && h <= 21) val = 0.3 + Math.random() * 0.2; // evening
} else {
if (h >= 10 && h <= 12) val = 0.3 + Math.random() * 0.2;
if (h >= 14 && h <= 18) val = 0.2 + Math.random() * 0.3;
if (h >= 19 && h <= 23) val = 0.3 + Math.random() * 0.3;
}
hours.push(val);
}
weekData.push(hours);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 600);
const cx = 300;
const cy = 300;
const minR = 60;
const maxR = 270;
const ringWidth = (maxR - minR) / 7;
for (let day = 0; day < 7; day++) {
const innerR = minR + day * ringWidth;
const outerR = innerR + ringWidth - 2;
for (let h = 0; h < 24; h++) {
const startAngle = (h / 24) * Math.PI * 2 - Math.PI / 2;
const endAngle = ((h + 1) / 24) * Math.PI * 2 - Math.PI / 2;
const val = weekData[day][h];
const hue = 200 - val * 180;
const lightness = 8 + val * 45;
ctx.beginPath();
ctx.arc(cx, cy, outerR, startAngle, endAngle);
ctx.arc(cx, cy, innerR, endAngle, startAngle, true);
ctx.closePath();
ctx.fillStyle = `hsl(${hue}, 50%, ${lightness}%)`;
ctx.fill();
}
}
// day labels
ctx.fillStyle = 'rgba(180, 180, 200, 0.6)';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
for (let day = 0; day < 7; day++) {
const r = minR + day * ringWidth + ringWidth / 2;
ctx.fillText(dayNames[day], cx - r - 5, cy + 4);
}
Seven rings, 24 segments each, 168 cells total. Your entire week in one circle. The inner ring is Monday, the outer ring is Sunday. Each segment covers one hour, starting from midnight at the top (12 o'clock position). Bright warm colors for active hours, dark cool colors for quiet hours.
The pattern is immediately readable: weekdays have a consistent bright band from 7am to 5pm (the work arc), weekends have a later, shorter, and more scattered pattern. The late-night hours are uniformly dark across all seven days. And if you had real data -- screen time, step counts, heart rate, whatever your phone tracks -- the patterns would be your actual rhythms. Something you live every day but rarely see laid out visually.
One last thing worth thinking about. Our perception of time isn't linear. Last week feels closer than "7 days ago" suggests. Last year feels more distant than "365 days ago" should. Our internal time scale is roughly logarithmic -- recent events are stretched, distant events are compressed.
For personal data art -- visualizing your own life, your own habits -- consider using a log scale on the time axis. Recent days get more space, older days get less. This matches how the data feels to you, not how it measures objectively. A linear timeline treats January 1st and December 31st as equidistant from July 1st. A log timeline from today backward gives yesterday 10 pixels and last month 10 pixels -- because that's roughly how they feel in your memory.
This connects to the non-linear mapping we covered in episode 82 with easing curves and log normalization. The same principle applies: when the raw data distribution doesn't match human perception, a non-linear mapping closes the gap. For time, that gap is between clock-time and felt-time.
We've got geography and time covered now -- two of the most common data dimensions in the real world. But data isn't always about places and dates. Relationships between entities -- who knows who, what links to what, how systems connect -- that's a whole different category. Nodes and edges, clusters and bridges, networks of all kinds. The visual language for connections is different from anything we've built so far. We're heading there.
Sallukes! Thanks for reading.
X