AashishAIHub's picture
feat: Update all modules with latest UI enhancements and content
13d9acc
raw
history blame
43.6 kB
/**
* Data Visualization Masterclass - Interactive Canvas Visualizations
* Demonstrates core visualization concepts through live examples
*/
(function() {
'use strict';
// Utility functions
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
// Color palette
const COLORS = {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#06b6d4',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
blue: '#3b82f6',
orange: '#f97316',
pink: '#ec4899',
gray: '#6b7280',
dark: '#1f2937',
light: '#f3f4f6'
};
// ==================== ANSCOMBE'S QUARTET ====================
function drawAnscombe() {
const canvas = $('canvas-anscombe');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Anscombe's Quartet data
const datasets = [
{ x: [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5], y: [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68] },
{ x: [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5], y: [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74] },
{ x: [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5], y: [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73] },
{ x: [8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8], y: [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89] }
];
const titles = ['Dataset I', 'Dataset II', 'Dataset III', 'Dataset IV'];
const panelWidth = canvas.width / 2 - 20;
const panelHeight = canvas.height / 2 - 30;
datasets.forEach((data, i) => {
const col = i % 2;
const row = Math.floor(i / 2);
const offsetX = col * (panelWidth + 20) + 40;
const offsetY = row * (panelHeight + 40) + 30;
// Draw axes
ctx.strokeStyle = '#374151';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(offsetX, offsetY + panelHeight - 20);
ctx.lineTo(offsetX + panelWidth - 40, offsetY + panelHeight - 20);
ctx.moveTo(offsetX, offsetY + panelHeight - 20);
ctx.lineTo(offsetX, offsetY);
ctx.stroke();
// Draw title
ctx.fillStyle = COLORS.primary;
ctx.font = 'bold 12px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(titles[i], offsetX + (panelWidth - 40) / 2, offsetY - 8);
// Draw points
const xMin = 2, xMax = 20, yMin = 2, yMax = 14;
data.x.forEach((x, j) => {
const px = offsetX + ((x - xMin) / (xMax - xMin)) * (panelWidth - 50);
const py = offsetY + panelHeight - 25 - ((data.y[j] - yMin) / (yMax - yMin)) * (panelHeight - 40);
ctx.beginPath();
ctx.arc(px, py, 5, 0, Math.PI * 2);
ctx.fillStyle = [COLORS.primary, COLORS.secondary, COLORS.accent, COLORS.success][i];
ctx.fill();
});
});
// Stats text
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('All 4 datasets: Mean X = 9, Mean Y = 7.5, Variance = 11, Correlation = 0.816', 40, canvas.height - 10);
}
// ==================== VISUAL PERCEPTION ====================
let perceptionMode = 'position';
function drawPerception() {
const canvas = $('canvas-perception');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const data = [45, 78, 32, 91, 56, 67, 23, 82, 41, 65];
const categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
if (perceptionMode === 'position') {
// Bar chart (position encoding)
ctx.font = 'bold 14px Inter, sans-serif';
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'center';
ctx.fillText('Position Encoding (Bar Chart) - Most Accurate!', canvas.width / 2, 25);
const barWidth = 50;
const startX = 50;
data.forEach((v, i) => {
const x = startX + i * (barWidth + 15);
const height = v * 2.5;
ctx.fillStyle = COLORS.primary;
ctx.fillRect(x, 280 - height, barWidth, height);
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(categories[i], x + barWidth / 2, 300);
ctx.fillText(v, x + barWidth / 2, 275 - height);
});
} else if (perceptionMode === 'color') {
// Color encoding
ctx.font = 'bold 14px Inter, sans-serif';
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'center';
ctx.fillText('Color Encoding - Good for Categories', canvas.width / 2, 25);
const squareSize = 60;
const startX = 50;
data.forEach((v, i) => {
const x = startX + i * (squareSize + 10);
const hue = (v / 100) * 240; // Blue to red gradient
ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`;
ctx.fillRect(x, 80, squareSize, squareSize);
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(categories[i], x + squareSize / 2, 165);
ctx.fillStyle = 'white';
ctx.fillText(v, x + squareSize / 2, 115);
});
// Legend
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Low', 50, 220);
ctx.fillText('High', 650, 220);
const gradWidth = 600;
for (let i = 0; i < gradWidth; i++) {
const hue = 240 - (i / gradWidth) * 240;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(50 + i, 230, 1, 20);
}
} else if (perceptionMode === 'size') {
// Size encoding (bubble)
ctx.font = 'bold 14px Inter, sans-serif';
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'center';
ctx.fillText('Size Encoding (Bubbles) - Humans Underestimate Area!', canvas.width / 2, 25);
const startX = 60;
data.forEach((v, i) => {
const x = startX + i * 65;
const radius = Math.sqrt(v) * 3.5;
ctx.beginPath();
ctx.arc(x, 150, radius, 0, Math.PI * 2);
ctx.fillStyle = COLORS.accent;
ctx.globalAlpha = 0.7;
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = COLORS.dark;
ctx.font = '10px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(categories[i], x, 230);
ctx.fillText(v, x, 155);
});
}
}
// ==================== GRAMMAR OF GRAPHICS ====================
function drawGrammar() {
const canvas = $('canvas-grammar');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const components = [
{ name: 'Data', icon: '📊', color: COLORS.primary },
{ name: 'Aesthetics', icon: '🎨', color: COLORS.secondary },
{ name: 'Geometry', icon: '◼️', color: COLORS.accent },
{ name: 'Facets', icon: '🔲', color: COLORS.success },
{ name: 'Statistics', icon: '📈', color: COLORS.warning },
{ name: 'Coordinates', icon: '📐', color: COLORS.danger },
{ name: 'Theme', icon: '🎭', color: COLORS.pink }
];
// Draw connected layers
const centerX = canvas.width / 2;
const startY = 60;
const layerHeight = 45;
components.forEach((comp, i) => {
const y = startY + i * layerHeight;
const width = 180 - i * 10;
// Layer rectangle
ctx.fillStyle = comp.color;
ctx.globalAlpha = 0.8;
ctx.beginPath();
ctx.roundRect(centerX - width / 2, y, width, 35, 5);
ctx.fill();
ctx.globalAlpha = 1;
// Text
ctx.fillStyle = 'white';
ctx.font = 'bold 13px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${comp.icon} ${comp.name}`, centerX, y + 22);
// Connector
if (i < components.length - 1) {
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX, y + 35);
ctx.lineTo(centerX, y + layerHeight);
ctx.stroke();
}
});
// Right side - example
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Example in Python:', 450, 60);
const code = [
'ggplot(data=iris,',
' aes(x=sepal_length,',
' y=sepal_width,',
' color=species)) +',
'geom_point(size=3) +',
'facet_wrap(~species) +',
'stat_smooth(method="lm") +',
'coord_fixed() +',
'theme_minimal()'
];
ctx.font = '11px monospace';
ctx.fillStyle = COLORS.gray;
code.forEach((line, i) => {
ctx.fillText(line, 450, 85 + i * 18);
});
}
// ==================== CHOOSING CHARTS ====================
let chartPurpose = 'comparison';
function drawChoosingCharts() {
const canvas = $('canvas-choosing');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 14px Inter, sans-serif';
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'center';
if (chartPurpose === 'comparison') {
ctx.fillText('Comparison: Bar Chart / Grouped Bar / Line', canvas.width / 2, 25);
const data = [
{ name: 'Q1', values: [40, 55, 30] },
{ name: 'Q2', values: [65, 45, 50] },
{ name: 'Q3', values: [55, 70, 45] },
{ name: 'Q4', values: [75, 60, 70] }
];
const colors = [COLORS.primary, COLORS.secondary, COLORS.accent];
const barWidth = 35;
const groupWidth = barWidth * 3 + 30;
const startX = 100;
data.forEach((group, i) => {
const groupX = startX + i * groupWidth;
group.values.forEach((v, j) => {
const x = groupX + j * (barWidth + 5);
const height = v * 3;
ctx.fillStyle = colors[j];
ctx.fillRect(x, 320 - height, barWidth, height);
});
ctx.fillStyle = COLORS.dark;
ctx.font = '12px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(group.name, groupX + groupWidth / 2 - 15, 340);
});
// Legend
['Product A', 'Product B', 'Product C'].forEach((label, i) => {
ctx.fillStyle = colors[i];
ctx.fillRect(580, 60 + i * 25, 15, 15);
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'left';
ctx.fillText(label, 600, 72 + i * 25);
});
} else if (chartPurpose === 'composition') {
ctx.fillText('Composition: Stacked Bar / Pie Chart (use sparingly!)', canvas.width / 2, 25);
// Stacked bar
const data = [
{ name: '2020', values: [30, 25, 45] },
{ name: '2021', values: [35, 30, 35] },
{ name: '2022', values: [25, 40, 35] },
{ name: '2023', values: [40, 35, 25] }
];
const colors = [COLORS.primary, COLORS.secondary, COLORS.accent];
const barWidth = 60;
const startX = 80;
data.forEach((group, i) => {
const x = startX + i * (barWidth + 40);
let y = 320;
group.values.forEach((v, j) => {
const height = v * 2.5;
ctx.fillStyle = colors[j];
ctx.fillRect(x, y - height, barWidth, height);
y -= height;
});
ctx.fillStyle = COLORS.dark;
ctx.font = '12px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(group.name, x + barWidth / 2, 340);
});
// Simple pie
const pieX = 550, pieY = 180, pieR = 80;
const pieData = [0.4, 0.35, 0.25];
let angle = -Math.PI / 2;
pieData.forEach((v, i) => {
ctx.beginPath();
ctx.moveTo(pieX, pieY);
ctx.arc(pieX, pieY, pieR, angle, angle + v * Math.PI * 2);
ctx.fillStyle = colors[i];
ctx.fill();
angle += v * Math.PI * 2;
});
} else if (chartPurpose === 'distribution') {
ctx.fillText('Distribution: Histogram / Box Plot / Violin', canvas.width / 2, 25);
// Simple histogram
const bins = [5, 12, 25, 42, 55, 48, 30, 18, 8, 3];
const barWidth = 50;
const startX = 80;
const maxVal = Math.max(...bins);
bins.forEach((v, i) => {
const x = startX + i * (barWidth + 8);
const height = (v / maxVal) * 200;
ctx.fillStyle = COLORS.primary;
ctx.globalAlpha = 0.8;
ctx.fillRect(x, 300 - height, barWidth, height);
ctx.globalAlpha = 1;
});
// Box plot representation
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('← Histogram shows full distribution', 350, 340);
} else if (chartPurpose === 'relationship') {
ctx.fillText('Relationship: Scatter Plot / Bubble / Heatmap', canvas.width / 2, 25);
// Random scatter with trend
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
ctx.beginPath();
ctx.moveTo(60, 60 + i * 25);
ctx.lineTo(700, 60 + i * 25);
ctx.stroke();
}
const points = [];
for (let i = 0; i < 50; i++) {
const x = 80 + Math.random() * 580;
const baseY = 300 - (x - 80) * 0.35;
const y = baseY + (Math.random() - 0.5) * 80;
points.push({ x, y, size: 4 + Math.random() * 8 });
}
points.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = COLORS.accent;
ctx.globalAlpha = 0.6;
ctx.fill();
ctx.globalAlpha = 1;
});
// Trend line
ctx.strokeStyle = COLORS.danger;
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(80, 300);
ctx.lineTo(660, 80);
ctx.stroke();
ctx.setLineDash([]);
}
}
// ==================== MATPLOTLIB ANATOMY ====================
function drawAnatomy() {
const canvas = $('canvas-anatomy');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw figure border
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 3;
ctx.setLineDash([10, 5]);
ctx.strokeRect(30, 30, canvas.width - 60, canvas.height - 60);
ctx.setLineDash([]);
// Label: Figure
ctx.fillStyle = COLORS.primary;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Figure (plt.figure())', 40, 25);
// Draw axes area
ctx.fillStyle = '#f8fafc';
ctx.fillRect(100, 80, 550, 350);
ctx.strokeStyle = COLORS.secondary;
ctx.lineWidth = 2;
ctx.strokeRect(100, 80, 550, 350);
// Label: Axes
ctx.fillStyle = COLORS.secondary;
ctx.fillText('Axes (ax = fig.add_subplot())', 110, 75);
// Plot area
ctx.fillStyle = 'white';
ctx.fillRect(160, 100, 450, 280);
ctx.strokeStyle = COLORS.accent;
ctx.strokeRect(160, 100, 450, 280);
// Sample line plot
ctx.beginPath();
ctx.moveTo(180, 330);
const points = [[220, 280], [280, 200], [340, 240], [400, 150], [460, 180], [520, 120], [580, 140]];
points.forEach(p => ctx.lineTo(p[0], p[1]));
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 3;
ctx.stroke();
// X-axis
ctx.strokeStyle = COLORS.dark;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(160, 380);
ctx.lineTo(610, 380);
ctx.stroke();
// Y-axis
ctx.beginPath();
ctx.moveTo(160, 100);
ctx.lineTo(160, 380);
ctx.stroke();
// Tick marks
ctx.font = '10px Inter, sans-serif';
ctx.fillStyle = COLORS.gray;
for (let i = 0; i <= 5; i++) {
const x = 160 + i * 90;
ctx.beginPath();
ctx.moveTo(x, 380);
ctx.lineTo(x, 390);
ctx.stroke();
ctx.textAlign = 'center';
ctx.fillText(i * 20, x, 405);
}
for (let i = 0; i <= 4; i++) {
const y = 100 + i * 70;
ctx.beginPath();
ctx.moveTo(150, y);
ctx.lineTo(160, y);
ctx.stroke();
ctx.textAlign = 'right';
ctx.fillText((4 - i) * 25, 145, y + 4);
}
// Labels with arrows
const annotations = [
{ x: 680, y: 100, label: 'Title', target: [400, 85] },
{ x: 680, y: 160, label: 'Y-axis Label', target: [130, 240] },
{ x: 680, y: 220, label: 'Line (Artist)', target: [400, 180] },
{ x: 680, y: 280, label: 'X-axis', target: [400, 395] },
{ x: 680, y: 340, label: 'Tick & Label', target: [250, 405] }
];
ctx.font = '11px Inter, sans-serif';
annotations.forEach(a => {
ctx.fillStyle = COLORS.dark;
ctx.textAlign = 'left';
ctx.fillText(a.label, a.x, a.y);
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(a.x - 5, a.y - 4);
ctx.lineTo(a.target[0], a.target[1]);
ctx.stroke();
ctx.setLineDash([]);
});
// Title
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 16px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Sample Line Plot', 385, 60);
// Axis labels
ctx.font = '12px Inter, sans-serif';
ctx.fillText('X Axis (Time)', 385, 440);
ctx.save();
ctx.translate(50, 240);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Y Axis (Value)', 0, 0);
ctx.restore();
}
// ==================== BASIC PLOTS ====================
let basicPlotType = 'line';
function drawBasicPlots() {
const canvas = $('canvas-basic');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const margin = { top: 50, right: 50, bottom: 50, left: 70 };
const width = canvas.width - margin.left - margin.right;
const height = canvas.height - margin.top - margin.bottom;
// Grid
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const y = margin.top + (i / 5) * height;
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + width, y);
ctx.stroke();
}
if (basicPlotType === 'line') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Line Plot: Time Series Data', canvas.width / 2, 25);
const data = [20, 35, 28, 45, 52, 48, 65, 58, 72, 68, 85, 78];
const xStep = width / (data.length - 1);
ctx.beginPath();
data.forEach((v, i) => {
const x = margin.left + i * xStep;
const y = margin.top + height - (v / 100) * height;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 3;
ctx.stroke();
// Points
data.forEach((v, i) => {
const x = margin.left + i * xStep;
const y = margin.top + height - (v / 100) * height;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = 'white';
ctx.fill();
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 2;
ctx.stroke();
});
} else if (basicPlotType === 'scatter') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Scatter Plot: Two Variables Relationship', canvas.width / 2, 25);
// Generate correlated data
for (let i = 0; i < 60; i++) {
const x = margin.left + Math.random() * width;
const baseY = margin.top + height - ((x - margin.left) / width) * height * 0.7;
const y = baseY + (Math.random() - 0.5) * 100;
ctx.beginPath();
ctx.arc(x, Math.max(margin.top, Math.min(margin.top + height, y)), 6, 0, Math.PI * 2);
ctx.fillStyle = COLORS.accent;
ctx.globalAlpha = 0.7;
ctx.fill();
ctx.globalAlpha = 1;
}
} else if (basicPlotType === 'bar') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Bar Chart: Categorical Comparison', canvas.width / 2, 25);
const data = [75, 52, 88, 45, 92, 67];
const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const barWidth = width / data.length - 20;
data.forEach((v, i) => {
const x = margin.left + i * (barWidth + 20) + 10;
const barHeight = (v / 100) * height;
ctx.fillStyle = COLORS.primary;
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(categories[i], x + barWidth / 2, margin.top + height + 20);
ctx.fillText(v, x + barWidth / 2, margin.top + height - barHeight - 8);
});
} else if (basicPlotType === 'hist') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Histogram: Distribution of Values', canvas.width / 2, 25);
// Normal-ish distribution
const bins = [3, 8, 18, 35, 52, 48, 32, 15, 7, 2];
const barWidth = width / bins.length - 2;
const maxVal = Math.max(...bins);
bins.forEach((v, i) => {
const x = margin.left + i * (barWidth + 2);
const barHeight = (v / maxVal) * height;
ctx.fillStyle = COLORS.secondary;
ctx.globalAlpha = 0.8;
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
ctx.globalAlpha = 1;
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
ctx.strokeRect(x, margin.top + height - barHeight, barWidth, barHeight);
});
// KDE curve
ctx.beginPath();
ctx.strokeStyle = COLORS.danger;
ctx.lineWidth = 2;
bins.forEach((v, i) => {
const x = margin.left + i * (barWidth + 2) + barWidth / 2;
const y = margin.top + height - (v / maxVal) * height;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
} else if (basicPlotType === 'pie') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Pie Chart: Part-to-Whole (Use Sparingly!)', canvas.width / 2, 25);
const data = [35, 25, 20, 12, 8];
const labels = ['Chrome', 'Safari', 'Firefox', 'Edge', 'Others'];
const colors = [COLORS.primary, COLORS.secondary, COLORS.accent, COLORS.success, COLORS.gray];
const centerX = canvas.width / 2 - 100;
const centerY = canvas.height / 2 + 20;
const radius = 120;
let angle = -Math.PI / 2;
data.forEach((v, i) => {
const sliceAngle = (v / 100) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, angle, angle + sliceAngle);
ctx.fillStyle = colors[i];
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
// Label
const labelAngle = angle + sliceAngle / 2;
const labelX = centerX + Math.cos(labelAngle) * (radius + 30);
const labelY = centerY + Math.sin(labelAngle) * (radius + 30);
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${labels[i]} (${v}%)`, labelX, labelY);
angle += sliceAngle;
});
// Warning
ctx.fillStyle = COLORS.warning;
ctx.font = '12px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('⚠️ Bar charts are usually better!', 550, 300);
}
// Y-axis
ctx.strokeStyle = COLORS.dark;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + height);
ctx.lineTo(margin.left + width, margin.top + height);
ctx.stroke();
}
// ==================== SEABORN DISTRIBUTION PLOTS ====================
let distPlotType = 'histplot';
function drawDistributions() {
const canvas = $('canvas-distributions');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const margin = { top: 50, right: 50, bottom: 50, left: 70 };
const width = canvas.width - margin.left - margin.right;
const height = canvas.height - margin.top - margin.bottom;
if (distPlotType === 'histplot') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('sns.histplot(data, kde=True)', canvas.width / 2, 25);
const bins = [2, 5, 12, 22, 38, 45, 42, 35, 20, 10, 5, 2];
const barWidth = width / bins.length - 4;
const maxVal = Math.max(...bins);
bins.forEach((v, i) => {
const x = margin.left + i * (barWidth + 4);
const barHeight = (v / maxVal) * height * 0.9;
ctx.fillStyle = COLORS.primary;
ctx.globalAlpha = 0.6;
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
ctx.globalAlpha = 1;
});
// KDE overlay
ctx.beginPath();
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 3;
bins.forEach((v, i) => {
const x = margin.left + i * (barWidth + 4) + barWidth / 2;
const y = margin.top + height - (v / maxVal) * height * 0.9;
if (i === 0) ctx.moveTo(x, y);
else {
const prevX = margin.left + (i - 1) * (barWidth + 4) + barWidth / 2;
const prevY = margin.top + height - (bins[i - 1] / maxVal) * height * 0.9;
const cpX = (prevX + x) / 2;
ctx.quadraticCurveTo(prevX, prevY, cpX, (prevY + y) / 2);
}
});
ctx.stroke();
} else if (distPlotType === 'kdeplot') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('sns.kdeplot(data, fill=True, multiple="stack")', canvas.width / 2, 25);
// Draw two overlapping KDEs
const kde1 = [5, 15, 35, 55, 70, 60, 40, 20, 8, 3];
const kde2 = [3, 8, 20, 40, 60, 75, 55, 35, 15, 5];
const maxVal = Math.max(...kde1, ...kde2);
const stepWidth = width / (kde1.length - 1);
// Draw KDE 1
ctx.beginPath();
ctx.moveTo(margin.left, margin.top + height);
kde1.forEach((v, i) => {
const x = margin.left + i * stepWidth;
const y = margin.top + height - (v / maxVal) * height * 0.8;
ctx.lineTo(x, y);
});
ctx.lineTo(margin.left + width, margin.top + height);
ctx.fillStyle = COLORS.primary;
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
// Draw KDE 2
ctx.beginPath();
ctx.moveTo(margin.left, margin.top + height);
kde2.forEach((v, i) => {
const x = margin.left + i * stepWidth;
const y = margin.top + height - (v / maxVal) * height * 0.8;
ctx.lineTo(x, y);
});
ctx.lineTo(margin.left + width, margin.top + height);
ctx.fillStyle = COLORS.secondary;
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
// Legend
ctx.fillStyle = COLORS.primary;
ctx.fillRect(620, 60, 20, 12);
ctx.fillStyle = COLORS.secondary;
ctx.fillRect(620, 80, 20, 12);
ctx.fillStyle = COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Group A', 645, 70);
ctx.fillText('Group B', 645, 90);
} else if (distPlotType === 'ecdfplot') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('sns.ecdfplot(data) - Empirical CDF', canvas.width / 2, 25);
// S-curve
ctx.beginPath();
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 3;
for (let i = 0; i <= 100; i++) {
const x = margin.left + (i / 100) * width;
// Sigmoid-like curve
const t = (i - 50) / 15;
const y = margin.top + height - (1 / (1 + Math.exp(-t))) * height;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Labels
ctx.fillStyle = COLORS.gray;
ctx.font = '10px Inter, sans-serif';
ctx.textAlign = 'right';
ctx.fillText('1.0', margin.left - 10, margin.top + 5);
ctx.fillText('0.5', margin.left - 10, margin.top + height / 2);
ctx.fillText('0.0', margin.left - 10, margin.top + height);
} else if (distPlotType === 'rugplot') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('sns.rugplot(data) - Shows Individual Data Points', canvas.width / 2, 25);
// KDE
const kde = [5, 15, 35, 55, 70, 75, 60, 40, 20, 8, 3];
const maxVal = Math.max(...kde);
const stepWidth = width / (kde.length - 1);
ctx.beginPath();
ctx.fillStyle = COLORS.primary;
ctx.globalAlpha = 0.3;
ctx.moveTo(margin.left, margin.top + height - 40);
kde.forEach((v, i) => {
const x = margin.left + i * stepWidth;
const y = margin.top + height - 40 - (v / maxVal) * (height - 60);
ctx.lineTo(x, y);
});
ctx.lineTo(margin.left + width, margin.top + height - 40);
ctx.fill();
ctx.globalAlpha = 1;
// Rug plot (vertical lines at bottom)
ctx.strokeStyle = COLORS.primary;
ctx.lineWidth = 1;
for (let i = 0; i < 80; i++) {
// Cluster around center
const x = margin.left + width / 2 + (Math.random() - 0.5) * width * 0.8 * (1 - Math.abs((Math.random() - 0.5) * 1.5));
ctx.beginPath();
ctx.moveTo(x, margin.top + height - 35);
ctx.lineTo(x, margin.top + height);
ctx.stroke();
}
ctx.fillStyle = COLORS.gray;
ctx.font = '10px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Each line = one data point', canvas.width / 2, margin.top + height + 25);
}
// Axes
ctx.strokeStyle = COLORS.dark;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + height);
ctx.lineTo(margin.left + width, margin.top + height);
ctx.stroke();
}
// ==================== HEATMAPS ====================
let heatmapType = 'basic';
function drawHeatmaps() {
const canvas = $('canvas-heatmaps');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (heatmapType === 'basic' || heatmapType === 'corr') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(heatmapType === 'basic' ? 'sns.heatmap(data, annot=True)' : 'Correlation Matrix (mask upper triangle)', canvas.width / 2, 25);
const size = 6;
const cellSize = 60;
const startX = (canvas.width - size * cellSize) / 2;
const startY = 60;
const labels = ['A', 'B', 'C', 'D', 'E', 'F'];
// Generate correlation matrix
const corr = [];
for (let i = 0; i < size; i++) {
corr[i] = [];
for (let j = 0; j < size; j++) {
if (i === j) corr[i][j] = 1;
else if (j > i) {
corr[i][j] = (Math.random() * 2 - 1).toFixed(2);
if (heatmapType === 'corr') corr[i][j] = null; // mask
} else {
corr[i][j] = corr[j] ? corr[j][i] : (Math.random() * 2 - 1).toFixed(2);
}
}
}
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const x = startX + j * cellSize;
const y = startY + i * cellSize;
const val = corr[i][j];
if (val === null) {
ctx.fillStyle = '#f3f4f6';
} else {
// Colormap: blue (negative) -> white (0) -> red (positive)
const v = parseFloat(val);
if (v < 0) {
const intensity = Math.abs(v);
ctx.fillStyle = `rgb(${66 + (1 - intensity) * 189}, ${102 + (1 - intensity) * 153}, ${241})`;
} else {
const intensity = v;
ctx.fillStyle = `rgb(${239}, ${68 + (1 - intensity) * 187}, ${68 + (1 - intensity) * 187})`;
}
}
ctx.fillRect(x, y, cellSize - 2, cellSize - 2);
if (val !== null) {
ctx.fillStyle = Math.abs(parseFloat(val)) > 0.5 ? 'white' : COLORS.dark;
ctx.font = '11px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(val, x + cellSize / 2 - 1, y + cellSize / 2 + 4);
}
}
}
// Labels
ctx.fillStyle = COLORS.dark;
ctx.font = '12px Inter, sans-serif';
labels.forEach((l, i) => {
ctx.textAlign = 'center';
ctx.fillText(l, startX + i * cellSize + cellSize / 2, startY + size * cellSize + 20);
ctx.textAlign = 'right';
ctx.fillText(l, startX - 10, startY + i * cellSize + cellSize / 2 + 4);
});
// Color bar
const barX = startX + size * cellSize + 30;
const barHeight = size * cellSize;
for (let i = 0; i < barHeight; i++) {
const v = 1 - (i / barHeight) * 2;
if (v < 0) {
const intensity = Math.abs(v);
ctx.fillStyle = `rgb(${66 + (1 - intensity) * 189}, ${102 + (1 - intensity) * 153}, ${241})`;
} else {
const intensity = v;
ctx.fillStyle = `rgb(${239}, ${68 + (1 - intensity) * 187}, ${68 + (1 - intensity) * 187})`;
}
ctx.fillRect(barX, startY + i, 20, 1);
}
ctx.font = '10px Inter, sans-serif';
ctx.textAlign = 'left';
ctx.fillStyle = COLORS.dark;
ctx.fillText('+1.0', barX + 25, startY + 10);
ctx.fillText('0.0', barX + 25, startY + barHeight / 2);
ctx.fillText('-1.0', barX + 25, startY + barHeight - 5);
} else if (heatmapType === 'clustermap') {
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('sns.clustermap(data) - Hierarchically Clustered', canvas.width / 2, 25);
// Draw dendrogram (simplified tree)
ctx.strokeStyle = COLORS.gray;
ctx.lineWidth = 1;
// Top dendrogram
const dendroY = 60;
ctx.beginPath();
ctx.moveTo(200, dendroY + 30);
ctx.lineTo(200, dendroY + 15);
ctx.lineTo(300, dendroY + 15);
ctx.lineTo(300, dendroY + 30);
ctx.moveTo(250, dendroY + 15);
ctx.lineTo(250, dendroY);
ctx.lineTo(450, dendroY);
ctx.lineTo(450, dendroY + 30);
ctx.moveTo(400, dendroY + 30);
ctx.lineTo(400, dendroY + 15);
ctx.lineTo(500, dendroY + 15);
ctx.lineTo(500, dendroY + 30);
ctx.stroke();
// Heatmap body
const startX = 180;
const startY = 100;
const cellSize = 50;
const rows = 5;
const cols = 6;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const x = startX + j * cellSize;
const y = startY + i * cellSize;
// Clustered pattern
let val = Math.random();
if ((i < 2 && j < 3) || (i >= 2 && j >= 3)) val = val * 0.3 + 0.7; // high values in clusters
else val = val * 0.3;
const hue = 240 - val * 240;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, y, cellSize - 2, cellSize - 2);
}
}
// Side dendrogram (simplified)
ctx.beginPath();
ctx.moveTo(startX - 10, startY + 25);
ctx.lineTo(startX - 25, startY + 25);
ctx.lineTo(startX - 25, startY + 75);
ctx.lineTo(startX - 10, startY + 75);
ctx.moveTo(startX - 25, startY + 50);
ctx.lineTo(startX - 40, startY + 50);
ctx.lineTo(startX - 40, startY + 175);
ctx.lineTo(startX - 10, startY + 175);
ctx.stroke();
ctx.fillStyle = COLORS.gray;
ctx.font = '10px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Rows and columns reordered by similarity', canvas.width / 2, canvas.height - 30);
}
}
// ==================== ANIMATIONS ====================
let animationId = null;
let animFrame = 0;
function drawAnimation() {
const canvas = $('canvas-animation');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = COLORS.dark;
ctx.font = 'bold 14px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Gapminder-style Animation: GDP vs Life Expectancy over Time', canvas.width / 2, 25);
const margin = { top: 50, right: 100, bottom: 50, left: 70 };
const width = canvas.width - margin.left - margin.right;
const height = canvas.height - margin.top - margin.bottom;
// Grid
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const y = margin.top + (i / 5) * height;
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(margin.left + width, y);
ctx.stroke();
}
// Countries (bubbles)
const countries = [
{ name: 'USA', color: COLORS.primary, baseX: 0.85, baseY: 0.2, size: 35 },
{ name: 'China', color: COLORS.danger, baseX: 0.4, baseY: 0.25, size: 50 },
{ name: 'India', color: COLORS.warning, baseX: 0.2, baseY: 0.4, size: 45 },
{ name: 'Brazil', color: COLORS.success, baseX: 0.35, baseY: 0.35, size: 25 },
{ name: 'Nigeria', color: COLORS.secondary, baseX: 0.15, baseY: 0.55, size: 28 }
];
// Animate movement
const progress = (animFrame % 100) / 100;
countries.forEach(country => {
// Countries move up and right over time (improving)
const xOffset = progress * 0.3 * Math.sin(progress * Math.PI);
const yOffset = progress * 0.2;
const x = margin.left + (country.baseX + xOffset) * width;
const y = margin.top + (country.baseY - yOffset) * height;
ctx.beginPath();
ctx.arc(x, y, country.size * (1 + progress * 0.3), 0, Math.PI * 2);
ctx.fillStyle = country.color;
ctx.globalAlpha = 0.7;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = country.color;
ctx.font = 'bold 10px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(country.name, x, y + 4);
});
// Year indicator
const year = Math.floor(1960 + progress * 60);
ctx.fillStyle = COLORS.gray;
ctx.font = 'bold 60px Inter, sans-serif';
ctx.textAlign = 'right';
ctx.globalAlpha = 0.3;
ctx.fillText(year, canvas.width - 30, canvas.height - 30);
ctx.globalAlpha = 1;
// Axes
ctx.strokeStyle = COLORS.dark;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, margin.top + height);
ctx.lineTo(margin.left + width, margin.top + height);
ctx.stroke();
ctx.fillStyle = COLORS.dark;
ctx.font = '12px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('GDP per Capita ($)', margin.left + width / 2, canvas.height - 10);
ctx.save();
ctx.translate(20, margin.top + height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Life Expectancy (years)', 0, 0);
ctx.restore();
if (animationId) {
animFrame++;
animationId = requestAnimationFrame(drawAnimation);
}
}
function startAnimation() {
if (!animationId) {
animFrame = 0;
animationId = requestAnimationFrame(drawAnimation);
}
}
function stopAnimation() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// ==================== EVENT LISTENERS ====================
function bindEvents() {
// Perception buttons
$('btn-position')?.addEventListener('click', () => { perceptionMode = 'position'; drawPerception(); });
$('btn-color')?.addEventListener('click', () => { perceptionMode = 'color'; drawPerception(); });
$('btn-size')?.addEventListener('click', () => { perceptionMode = 'size'; drawPerception(); });
// Chart choosing buttons
$('btn-comparison')?.addEventListener('click', () => { chartPurpose = 'comparison'; drawChoosingCharts(); });
$('btn-composition')?.addEventListener('click', () => { chartPurpose = 'composition'; drawChoosingCharts(); });
$('btn-distribution')?.addEventListener('click', () => { chartPurpose = 'distribution'; drawChoosingCharts(); });
$('btn-relationship')?.addEventListener('click', () => { chartPurpose = 'relationship'; drawChoosingCharts(); });
// Basic plot buttons
$('btn-line')?.addEventListener('click', () => { basicPlotType = 'line'; drawBasicPlots(); });
$('btn-scatter')?.addEventListener('click', () => { basicPlotType = 'scatter'; drawBasicPlots(); });
$('btn-bar')?.addEventListener('click', () => { basicPlotType = 'bar'; drawBasicPlots(); });
$('btn-hist')?.addEventListener('click', () => { basicPlotType = 'hist'; drawBasicPlots(); });
$('btn-pie')?.addEventListener('click', () => { basicPlotType = 'pie'; drawBasicPlots(); });
// Distribution buttons
$('btn-histplot')?.addEventListener('click', () => { distPlotType = 'histplot'; drawDistributions(); });
$('btn-kdeplot')?.addEventListener('click', () => { distPlotType = 'kdeplot'; drawDistributions(); });
$('btn-ecdfplot')?.addEventListener('click', () => { distPlotType = 'ecdfplot'; drawDistributions(); });
$('btn-rugplot')?.addEventListener('click', () => { distPlotType = 'rugplot'; drawDistributions(); });
// Heatmap buttons
$('btn-heatmap-basic')?.addEventListener('click', () => { heatmapType = 'basic'; drawHeatmaps(); });
$('btn-corr-matrix')?.addEventListener('click', () => { heatmapType = 'corr'; drawHeatmaps(); });
$('btn-clustermap')?.addEventListener('click', () => { heatmapType = 'clustermap'; drawHeatmaps(); });
// Animation
$('btn-animate')?.addEventListener('click', startAnimation);
$('btn-stop')?.addEventListener('click', stopAnimation);
// Smooth scroll for nav links
$$('.nav__link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href').slice(1);
const target = document.getElementById(targetId);
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
// Update active state
$$('.nav__link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
});
});
}
// ==================== INITIALIZATION ====================
function init() {
bindEvents();
// Draw all initial visualizations
drawAnscombe();
perceptionMode = 'position';
drawPerception();
drawGrammar();
chartPurpose = 'comparison';
drawChoosingCharts();
drawAnatomy();
basicPlotType = 'line';
drawBasicPlots();
distPlotType = 'histplot';
drawDistributions();
heatmapType = 'basic';
drawHeatmaps();
drawAnimation();
}
// Run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();