/** * 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 RELATIONSHIP PLOTS ==================== let relPlotType = 'scatter'; function drawRelationships() { const canvas = $('canvas-relationships'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const margin = { top: 50, right: 150, bottom: 50, left: 70 }; const width = canvas.width - margin.left - margin.right; const height = canvas.height - margin.top - margin.bottom; // Generate correlated data const n = 60; const data = []; for (let i = 0; i < n; i++) { const x = margin.left + (i / n) * width; const group = i % 2 === 0 ? 'A' : 'B'; const noise = (Math.random() - 0.5) * 50; const slope = group === 'A' ? 0.5 : 0.8; const intercept = group === 'A' ? 20 : -30; const yRaw = (i / n) * 100 * slope + intercept + noise; // Map to canvas Y const y = margin.top + height - ((yRaw + 50) / 150) * height; data.push({ x, y, group }); } if (relPlotType === 'scatter') { ctx.fillStyle = COLORS.dark; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('sns.scatterplot(..., hue="group")', canvas.width / 2, 25); data.forEach(d => { ctx.beginPath(); ctx.arc(d.x, d.y, 6, 0, Math.PI * 2); ctx.fillStyle = d.group === 'A' ? COLORS.primary : COLORS.secondary; ctx.globalAlpha = 0.7; ctx.fill(); ctx.strokeStyle = 'white'; ctx.globalAlpha = 1; ctx.stroke(); }); // Legend ctx.fillStyle = COLORS.primary; ctx.fillRect(canvas.width - 120, 60, 15, 15); ctx.fillStyle = COLORS.secondary; ctx.fillRect(canvas.width - 120, 85, 15, 15); ctx.fillStyle = COLORS.dark; ctx.font = '12px Inter, sans-serif'; ctx.textAlign = 'left'; ctx.fillText('Group A', canvas.width - 95, 72); ctx.fillText('Group B', canvas.width - 95, 97); } else if (relPlotType === 'regplot') { ctx.fillStyle = COLORS.dark; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('sns.regplot() - Scatter + Regression Line + 95% CI', canvas.width / 2, 25); // Draw confidence interval band (approximated) ctx.beginPath(); ctx.moveTo(margin.left, margin.top + height - 30); ctx.lineTo(margin.left + width, margin.top + 20); ctx.lineTo(margin.left + width, margin.top + 80); ctx.lineTo(margin.left, margin.top + height - (-30)); ctx.fillStyle = COLORS.primary; ctx.globalAlpha = 0.2; ctx.fill(); ctx.globalAlpha = 1; // Draw regression line ctx.beginPath(); ctx.moveTo(margin.left, margin.top + height - 10); ctx.lineTo(margin.left + width, margin.top + 50); ctx.strokeStyle = COLORS.primary; ctx.lineWidth = 3; ctx.stroke(); // Draw points data.forEach(d => { ctx.beginPath(); ctx.arc(d.x, d.y, 5, 0, Math.PI * 2); ctx.fillStyle = COLORS.dark; ctx.globalAlpha = 0.5; ctx.fill(); ctx.globalAlpha = 1; }); } else if (relPlotType === 'residplot') { ctx.fillStyle = COLORS.dark; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('sns.residplot() - Plotting Regression Residuals', canvas.width / 2, 25); // Zero line const midY = margin.top + height / 2; ctx.beginPath(); ctx.moveTo(margin.left, midY); ctx.lineTo(margin.left + width, midY); ctx.strokeStyle = COLORS.danger; ctx.setLineDash([5, 5]); ctx.stroke(); ctx.setLineDash([]); // Residuals data.forEach(d => { // Calculate distance from an imaginary regression line const expectedY = margin.top + height - 10 - ((d.x - margin.left) / width) * (height - 60); const residual = d.y - expectedY; ctx.beginPath(); ctx.arc(d.x, midY + residual, 5, 0, Math.PI * 2); ctx.fillStyle = COLORS.blue; ctx.globalAlpha = 0.6; ctx.fill(); ctx.globalAlpha = 1; // Stem ctx.beginPath(); ctx.moveTo(d.x, midY); ctx.lineTo(d.x, midY + residual); ctx.strokeStyle = COLORS.blue; ctx.globalAlpha = 0.3; ctx.stroke(); ctx.globalAlpha = 1; }); } else if (relPlotType === 'pairplot') { ctx.fillStyle = COLORS.dark; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('sns.pairplot() - Pairwise relationships', canvas.width / 2, 25); const gridSize = 3; const cellW = width / gridSize; const cellH = height / gridSize; ctx.lineWidth = 1; for (let r = 0; r < gridSize; r++) { for (let c = 0; c < gridSize; c++) { const cx = margin.left + c * cellW; const cy = margin.top + r * cellH; ctx.strokeStyle = COLORS.gray; ctx.strokeRect(cx, cy, cellW, cellH); // Diagonal: KDE if (r === c) { ctx.beginPath(); ctx.moveTo(cx, cy + cellH); for (let i = 0; i <= cellW; i += 5) { const h = Math.sin((i / cellW) * Math.PI) * (cellH * 0.8) + (Math.random() * 10 - 5); ctx.lineTo(cx + i, cy + cellH - Math.max(0, h)); } ctx.lineTo(cx + cellW, cy + cellH); ctx.fillStyle = COLORS.success; ctx.globalAlpha = 0.3; ctx.fill(); ctx.globalAlpha = 1; } // Off-diagonal: Scatter else { for (let i = 0; i < 20; i++) { ctx.beginPath(); ctx.arc(cx + 5 + Math.random() * (cellW - 10), cy + 5 + Math.random() * (cellH - 10), 3, 0, Math.PI * 2); ctx.fillStyle = COLORS.dark; ctx.globalAlpha = 0.4; ctx.fill(); 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(); } // ==================== 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; } } // ==================== GEOSPATIAL ==================== let geoType = 'choropleth'; function drawGeo() { const canvas = $('canvas-geo'); 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'; if (geoType === 'choropleth') { ctx.fillText('Choropleth Map: Values mapped to regions', canvas.width / 2, 25); // Draw grid lines (longitude/latitude) ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = 1; for (let i = 0; i <= 8; i++) { ctx.beginPath(); ctx.moveTo(i * 100, 50); ctx.lineTo(i * 100, 450); ctx.stroke(); } for (let i = 0; i <= 4; i++) { ctx.beginPath(); ctx.moveTo(0, 50 + i * 100); ctx.lineTo(800, 50 + i * 100); ctx.stroke(); } // Draw stylized map regions const regions = [ { path: [[100, 100], [300, 80], [350, 200], [150, 250]], val: 0.8 }, { path: [[350, 200], [500, 150], [600, 300], [400, 350]], val: 0.4 }, { path: [[150, 250], [400, 350], [300, 450], [100, 400]], val: 0.2 }, { path: [[500, 150], [700, 100], [750, 250], [600, 300]], val: 0.9 }, { path: [[400, 350], [600, 300], [650, 450], [450, 480]], val: 0.6 } ]; regions.forEach(r => { ctx.beginPath(); ctx.moveTo(r.path[0][0], r.path[0][1]); for (let i = 1; i < r.path.length; i++) { ctx.lineTo(r.path[i][0], r.path[i][1]); } ctx.closePath(); // Color mapping const hue = 240 - r.val * 240; ctx.fillStyle = `hsla(${hue}, 70%, 50%, 0.7)`; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); }); } else if (geoType === 'scatter') { ctx.fillText('Scatter on Map: Point data over geography', canvas.width / 2, 25); // Draw basic continent outlines ctx.strokeStyle = COLORS.gray; ctx.lineWidth = 2; ctx.beginPath(); // Rough Americas ctx.moveTo(200, 100); ctx.quadraticCurveTo(100, 200, 250, 250); ctx.lineTo(300, 400); ctx.lineTo(350, 380); ctx.lineTo(250, 200); ctx.lineTo(350, 80); ctx.closePath(); // Rough Afro-Eurasia ctx.moveTo(400, 150); ctx.quadraticCurveTo(500, 50, 700, 100); ctx.lineTo(750, 250); ctx.lineTo(600, 300); ctx.lineTo(450, 400); ctx.lineTo(380, 250); ctx.closePath(); ctx.stroke(); ctx.fillStyle = '#f3f4f6'; ctx.fill(); // Scatter points for (let i = 0; i < 40; i++) { const x = 150 + Math.random() * 600; const y = 80 + Math.random() * 350; ctx.beginPath(); ctx.arc(x, y, 3 + Math.random() * 8, 0, Math.PI * 2); ctx.fillStyle = COLORS.primary; ctx.globalAlpha = 0.6; ctx.fill(); ctx.globalAlpha = 1; } } else if (geoType === 'heatmap') { ctx.fillText('Density Heatmap: Clustering over geography', canvas.width / 2, 25); // Base map ctx.fillStyle = '#1f2937'; ctx.fillRect(50, 50, 700, 400); // Gradient density clusters const clusters = [ { x: 250, y: 150, r: 100, intensity: 1 }, { x: 550, y: 200, r: 150, intensity: 0.8 }, { x: 300, y: 350, r: 120, intensity: 0.9 }, { x: 650, y: 350, r: 80, intensity: 0.6 } ]; clusters.forEach(c => { const grad = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, c.r); grad.addColorStop(0, `rgba(239, 68, 68, ${c.intensity})`); // red grad.addColorStop(0.3, `rgba(245, 158, 11, ${c.intensity * 0.7})`); // orange grad.addColorStop(0.6, `rgba(16, 185, 129, ${c.intensity * 0.4})`); // green grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2); ctx.fill(); }); } } // ==================== 3D PLOTS ==================== let plot3DType = 'scatter'; function draw3D() { const canvas = $('canvas-3d'); 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'; const cx = canvas.width / 2; const cy = canvas.height / 2 + 20; // Draw isometric axes ctx.strokeStyle = COLORS.gray; ctx.lineWidth = 2; ctx.beginPath(); // Z axis (up) ctx.moveTo(cx, cy); ctx.lineTo(cx, cy - 150); // X axis (down-left) ctx.moveTo(cx, cy); ctx.lineTo(cx - 150, cy + 86); // Y axis (down-right) ctx.moveTo(cx, cy); ctx.lineTo(cx + 150, cy + 86); ctx.stroke(); ctx.fillStyle = COLORS.gray; ctx.font = '12px Inter, sans-serif'; ctx.fillText('Z', cx, cy - 160); ctx.fillText('X', cx - 160, cy + 96); ctx.fillText('Y', cx + 160, cy + 96); // Helper for isometric projection const isoMap = (x, y, z) => { // 30 degree isometric projection const cos30 = Math.cos(Math.PI / 6); const sin30 = Math.sin(Math.PI / 6); return { px: cx + (y - x) * cos30, py: cy + (x + y) * sin30 - z }; }; if (plot3DType === 'scatter') { ctx.fillText('3D Scatter Plot', canvas.width / 2, 25); for (let i = 0; i < 80; i++) { // Random 3D coords [0, 100] const x = Math.random() * 100; const y = Math.random() * 100; const z = (x + y) / 2 + (Math.random() - 0.5) * 40; // correlated Z const pos = isoMap(x, y, z); ctx.beginPath(); ctx.arc(pos.px, pos.py, 4, 0, Math.PI * 2); const hue = (z / 120) * 240; ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 1; ctx.stroke(); } } else if (plot3DType === 'surface' || plot3DType === 'wireframe') { ctx.fillText(`3D ${plot3DType === 'surface' ? 'Surface' : 'Wireframe'} Plot (sin(R)/R)`, canvas.width / 2, 25); const resolution = 15; const step = 120 / resolution; const grid = []; for (let x = -60; x <= 60; x += step) { const row = []; for (let y = -60; y <= 60; y += step) { const r = Math.sqrt(x * x + y * y); const z = r === 0 ? 80 : Math.sin(r * 0.15) / (r * 0.15) * 80; row.push({ x: x + 60, y: y + 60, z: z + 40 }); } grid.push(row); } ctx.lineWidth = 1; // Draw polygons from back to front (rough Painter's Algorithm) // Since it's symmetric, a simple loop works decently for (let i = 0; i < grid.length - 1; i++) { for (let j = 0; j < grid[i].length - 1; j++) { const p1 = isoMap(grid[i][j].x, grid[i][j].y, grid[i][j].z); const p2 = isoMap(grid[i + 1][j].x, grid[i + 1][j].y, grid[i + 1][j].z); const p3 = isoMap(grid[i + 1][j + 1].x, grid[i + 1][j + 1].y, grid[i + 1][j + 1].z); const p4 = isoMap(grid[i][j + 1].x, grid[i][j + 1].y, grid[i][j + 1].z); ctx.beginPath(); ctx.moveTo(p1.px, p1.py); ctx.lineTo(p2.px, p2.py); ctx.lineTo(p3.px, p3.py); ctx.lineTo(p4.px, p4.py); ctx.closePath(); const avgZ = (grid[i][j].z + grid[i + 1][j].z + grid[i + 1][j + 1].z + grid[i][j + 1].z) / 4; const hue = (avgZ / 120) * 240 + 60; // offset hue if (plot3DType === 'surface') { ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`; ctx.fill(); ctx.strokeStyle = `hsl(${240 - hue}, 70%, 30%)`; } else { ctx.strokeStyle = COLORS.primary; } ctx.stroke(); } } } } // ==================== STORYTELLING ==================== function drawStorytelling() { const canvas = $('canvas-storytelling'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // Title: Big, bold conclusion ctx.fillStyle = COLORS.dark; ctx.font = 'bold 22px Inter, sans-serif'; ctx.textAlign = 'left'; ctx.fillText('Product B exceeded sales targets by 45% in Q4', 50, 40); // Subtitle / context ctx.fillStyle = COLORS.gray; ctx.font = '14px Inter, sans-serif'; ctx.fillText('Driven by the new marketing campaign launched in September.', 50, 65); // Plot Area const margin = { left: 50, right: 150, top: 120, bottom: 50 }; const width = canvas.width - margin.left - margin.right; const height = canvas.height - margin.top - margin.bottom; // Remove harsh gridlines, only light Y grid ctx.strokeStyle = '#f3f4f6'; ctx.lineWidth = 1; for (let i = 0; i <= 4; i++) { const y = margin.top + (i / 4) * height; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(margin.left + width, y); ctx.stroke(); } // Data Series A (Context - Greyed out) const dataA = [120, 130, 125, 140]; const dataB = [90, 100, 110, 205]; // Massive spike const target = 140; const quarters = ['Q1', 'Q2', 'Q3', 'Q4']; const xStep = width / 3; // Draw Target Line const targetY = margin.top + height - (target / 250) * height; ctx.beginPath(); ctx.moveTo(margin.left, targetY); ctx.lineTo(margin.left + width, targetY); ctx.strokeStyle = COLORS.warning; ctx.setLineDash([5, 5]); ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = COLORS.warning; ctx.fillText('Q4 Target (140)', margin.left + width + 10, targetY + 5); // Draw Line A (Less important) ctx.beginPath(); dataA.forEach((v, i) => { const x = margin.left + i * xStep; const y = margin.top + height - (v / 250) * height; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.strokeStyle = '#cbd5e1'; // light slate ctx.lineWidth = 3; ctx.stroke(); ctx.fillStyle = '#94a3b8'; ctx.fillText('Product A', margin.left + width + 10, margin.top + height - (dataA[3] / 250) * height + 5); // Draw Line B (The Hero) ctx.beginPath(); dataB.forEach((v, i) => { const x = margin.left + i * xStep; const y = margin.top + height - (v / 250) * height; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.strokeStyle = COLORS.primary; ctx.lineWidth = 5; ctx.stroke(); // Highlight points for B dataB.forEach((v, i) => { const x = margin.left + i * xStep; const y = margin.top + height - (v / 250) * height; ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fillStyle = i === 3 ? COLORS.success : COLORS.primary; // Make final point stand out more ctx.fill(); // Data labels directly on line ctx.fillStyle = i === 3 ? COLORS.success : COLORS.dark; ctx.font = i === 3 ? 'bold 16px Inter, sans-serif' : '12px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(v, x, y - 15); }); // Label Hero line ctx.fillStyle = COLORS.primary; ctx.textAlign = 'left'; ctx.font = 'bold 14px Inter, sans-serif'; ctx.fillText('Product B', margin.left + width + 10, margin.top + height - (dataB[3] / 250) * height + 5); // X Axis ctx.strokeStyle = COLORS.dark; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(margin.left, margin.top + height); ctx.lineTo(margin.left + width, margin.top + height); ctx.stroke(); ctx.fillStyle = COLORS.dark; ctx.textAlign = 'center'; quarters.forEach((q, i) => { ctx.fillText(q, margin.left + i * xStep, margin.top + height + 20); }); // Storytelling annotation ctx.beginPath(); const finalX = margin.left + 3 * xStep; const finalY = margin.top + height - (dataB[3] / 250) * height; ctx.moveTo(finalX - 60, finalY + 40); ctx.lineTo(finalX - 10, finalY + 10); ctx.strokeStyle = COLORS.success; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = COLORS.success; ctx.textAlign = 'right'; ctx.fillText('+45% over Target!', finalX - 65, finalY + 45); } // ==================== SUBPLOTS & LAYOUTS ==================== let subplotType = '2x2'; function drawSubplots() { const canvas = $('canvas-subplots'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = COLORS.text; ctx.font = '14px Inter, sans-serif'; ctx.textAlign = 'center'; if (subplotType === '2x2') { ctx.fillText('2x2 Grid Layout', canvas.width / 2, 30); drawRect(ctx, 50, 60, 300, 180, COLORS.primary, false); drawRect(ctx, 400, 60, 300, 180, COLORS.secondary, false); drawRect(ctx, 50, 270, 300, 180, COLORS.accent, false); drawRect(ctx, 400, 270, 300, 180, COLORS.success, false); ctx.fillText('ax1', 200, 150); ctx.fillText('ax2', 550, 150); ctx.fillText('ax3', 200, 360); ctx.fillText('ax4', 550, 360); } else if (subplotType === 'uneven') { ctx.fillText('Uneven Grid (1 top, 2 bottom)', canvas.width / 2, 30); drawRect(ctx, 50, 60, 650, 180, COLORS.primary, false); drawRect(ctx, 50, 270, 300, 180, COLORS.secondary, false); drawRect(ctx, 400, 270, 300, 180, COLORS.accent, false); ctx.fillText('ax1 (colspan=2)', 375, 150); ctx.fillText('ax2', 200, 360); ctx.fillText('ax3', 550, 360); } else if (subplotType === 'gridspec') { ctx.fillText('GridSpec Layout (Complex sizing)', canvas.width / 2, 30); drawRect(ctx, 50, 60, 450, 390, COLORS.primary, false); drawRect(ctx, 530, 60, 200, 180, COLORS.secondary, false); drawRect(ctx, 530, 270, 200, 180, COLORS.accent, false); ctx.fillText('Main Plot (3x3 grid, spans 2x3)', 275, 255); ctx.fillText('Side Plot 1', 630, 150); ctx.fillText('Side Plot 2', 630, 360); } } // ==================== STYLING & THEMES ==================== let styleType = 'default'; function drawStyling() { const canvas = $('canvas-styling'); if (!canvas) return; const ctx = canvas.getContext('2d'); // Background based on style let bg = '#ffffff'; let fg = '#333333'; let lineColors = ['#1f77b4', '#ff7f0e', '#2ca02c']; if (styleType === 'dark') { bg = '#121212'; fg = '#eeeeee'; lineColors = ['#00d4ff', '#ff5555', '#55ff55']; } else if (styleType === 'seaborn') { bg = '#eaeaf2'; fg = '#222222'; lineColors = ['#4c72b0', '#55a868', '#c44e52']; } else if (styleType === 'ggplot') { bg = '#e5e5e5'; fg = '#555555'; lineColors = ['#f8766d', '#00ba38', '#619cff']; } else if (styleType === '538') { bg = '#f0f0f0'; fg = '#333333'; lineColors = ['#008fd5', '#fc4f30', '#e5ae38']; } ctx.fillStyle = bg; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = fg; ctx.font = styleType === '538' ? 'bold 16px Arial' : '14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`${styleType.toUpperCase()} Theme Preview`, canvas.width / 2, 30); // Draw generic line chart const startX = 100, endX = 700, baseY = 300; // Axes ctx.strokeStyle = (styleType === 'seaborn' || styleType === 'ggplot') ? '#ffffff' : fg; ctx.lineWidth = (styleType === 'seaborn' || styleType === 'ggplot') ? 2 : 1; ctx.beginPath(); ctx.moveTo(startX, 50); ctx.lineTo(startX, baseY); ctx.lineTo(endX, baseY); ctx.stroke(); // Grid ctx.strokeStyle = (styleType === 'dark') ? '#333' : '#ddd'; for (let i = 1; i <= 5; i++) { let y = baseY - (i * 50); ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(endX, y); ctx.stroke(); } // Lines for (let l = 0; l < 3; l++) { ctx.strokeStyle = lineColors[l]; ctx.lineWidth = styleType === '538' ? 4 : 2; ctx.beginPath(); for (let i = 0; i <= 10; i++) { let x = startX + (i * 60); let y = baseY - 50 - (Math.sin(i * 0.5 + l) * 50) - (l * 40); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } } // ==================== SEABORN INTRO ==================== function drawSeabornIntro() { const canvas = $('canvas-seaborn-intro'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = COLORS.text; ctx.font = '14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Matplotlib vs Seaborn Comparison', canvas.width / 2, 30); // Fake Matplotlib code drawRect(ctx, 50, 60, 300, 200, '#1e293b', true); ctx.fillStyle = '#fff'; ctx.font = '12px Courier'; ctx.textAlign = 'left'; ctx.fillText('import matplotlib.pyplot as plt', 60, 80); ctx.fillText('fig, ax = plt.subplots()', 60, 100); ctx.fillText('ax.scatter(df[\'target\'], df[\'value\'])', 60, 120); ctx.fillText('ax.set_title(\'Complex to Style\')', 60, 140); // Fake Seaborn drawRect(ctx, 400, 60, 300, 200, '#1e293b', true); ctx.fillStyle = '#fff'; ctx.fillText('import seaborn as sns', 410, 80); ctx.fillText('sns.scatterplot(', 410, 100); ctx.fillText(' data=df, x=\'target\', y=\'value\',', 410, 120); ctx.fillText(' hue=\'category\', style=\'type\')', 410, 140); ctx.fillStyle = COLORS.accent; ctx.fillText('// 1 line creates a styled mapped plot!', 410, 180); } // ==================== CATEGORICAL PLOTS ==================== let catPlotType = 'boxplot'; function drawCategorical() { const canvas = $('canvas-categorical'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const categories = ['Group A', 'Group B', 'Group C']; const colors = [COLORS.primary, COLORS.accent, COLORS.warning]; ctx.fillStyle = COLORS.text; ctx.textAlign = 'center'; ctx.font = 'bold 14px Inter, sans-serif'; ctx.fillText(catPlotType.toUpperCase() + ' PREVIEW', canvas.width / 2, 30); const startX = 150; const spacing = 200; const baseY = 320; // Axis ctx.strokeStyle = COLORS.textSecondary; ctx.beginPath(); ctx.moveTo(50, baseY); ctx.lineTo(750, baseY); ctx.stroke(); for (let c = 0; c < 3; c++) { let cx = startX + c * spacing; ctx.fillStyle = COLORS.text; ctx.fillText(categories[c], cx, baseY + 20); ctx.fillStyle = colors[c]; ctx.strokeStyle = colors[c]; if (catPlotType === 'boxplot') { // Box ctx.fillRect(cx - 30, baseY - 150 + c * 10, 60, 80); // Whiskers ctx.beginPath(); ctx.moveTo(cx, baseY - 150 + c * 10); ctx.lineTo(cx, baseY - 200 + c * 20); ctx.moveTo(cx, baseY - 70 + c * 10); ctx.lineTo(cx, baseY - 30 - c * 10); // Caps ctx.moveTo(cx - 15, baseY - 200 + c * 20); ctx.lineTo(cx + 15, baseY - 200 + c * 20); ctx.moveTo(cx - 15, baseY - 30 - c * 10); ctx.lineTo(cx + 15, baseY - 30 - c * 10); ctx.stroke(); // Median (black line) ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx - 30, baseY - 110 + c * 10); ctx.lineTo(cx + 30, baseY - 110 + c * 10); ctx.stroke(); } else if (catPlotType === 'violinplot') { ctx.beginPath(); ctx.moveTo(cx, baseY - 50); ctx.bezierCurveTo(cx - 50, baseY - 100, cx - 70, baseY - 180, cx, baseY - 230); ctx.bezierCurveTo(cx + 70, baseY - 180, cx + 50, baseY - 100, cx, baseY - 50); ctx.globalAlpha = 0.5; ctx.fill(); ctx.globalAlpha = 1; ctx.stroke(); } else if (catPlotType === 'barplot') { let h = 150 + c * 30; ctx.fillRect(cx - 40, baseY - h, 80, h); // Error bar ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, baseY - h - 20); ctx.lineTo(cx, baseY - h + 20); ctx.moveTo(cx - 10, baseY - h - 20); ctx.lineTo(cx + 10, baseY - h - 20); ctx.moveTo(cx - 10, baseY - h + 20); ctx.lineTo(cx + 10, baseY - h + 20); ctx.stroke(); } else if (catPlotType === 'stripplot' || catPlotType === 'swarmplot') { ctx.globalAlpha = 0.6; for (let i = 0; i < 40; i++) { let y = baseY - 50 - Math.random() * 150; let x = (catPlotType === 'swarmplot') ? cx + (Math.sin(y) * 30 * Math.random()) : cx - 30 + Math.random() * 60; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } } } // ==================== PLOTLY & DASHBOARDS ==================== function drawPlotly() { const canvas = $('canvas-plotly'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#1e293b'; ctx.fillRect(40, 40, canvas.width - 80, canvas.height - 80); ctx.fillStyle = COLORS.cyan || '#06b6d4'; ctx.font = '16px Inter'; ctx.textAlign = 'center'; ctx.fillText('Plotly / Interactive Charts Visualization', canvas.width / 2, 80); ctx.font = '12px Inter'; ctx.fillText('Hover tooltips, zooming, and panning enabled out of the box.', canvas.width / 2, 100); // Draw a fake cursor and tooltip ctx.beginPath(); ctx.arc(400, 200, 8, 0, Math.PI * 2); ctx.fillStyle = COLORS.orange; ctx.fill(); ctx.fillStyle = '#fff'; ctx.fillRect(415, 175, 100, 50); ctx.fillStyle = '#000'; ctx.fillText('X: 2024-01-01', 465, 195); ctx.fillText('Y: $450.2K', 465, 215); } function drawDashboard() { const canvas = $('canvas-dashboard'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = COLORS.text; ctx.font = '16px Inter'; ctx.textAlign = 'center'; ctx.fillText('Streamlit Dashboard Architecture', canvas.width / 2, 30); drawRect(ctx, 100, 60, 200, 250, '#1e293b', true); ctx.fillStyle = '#fff'; ctx.fillText('Sidebar / Inputs', 200, 90); drawRect(ctx, 120, 120, 160, 30, '#334155', true); drawRect(ctx, 120, 170, 160, 30, '#334155', true); drawRect(ctx, 350, 60, 400, 250, '#0f172a', true); ctx.fillStyle = COLORS.cyan || '#06b6d4'; ctx.fillText('Main Chart Area', 550, 90); ctx.beginPath(); ctx.arc(550, 180, 50, 0, Math.PI * 2); ctx.strokeStyle = COLORS.accent; ctx.lineWidth = 15; ctx.stroke(); } function drawRect(ctx, x, y, width, height, color, filled = true) { if (filled) { ctx.fillStyle = color; ctx.fillRect(x, y, width, height); } else { ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.strokeRect(x, y, width, height); } } // ==================== 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(); }); // Subplots $('btn-grid-2x2')?.addEventListener('click', () => { subplotType = '2x2'; drawSubplots(); }); $('btn-grid-uneven')?.addEventListener('click', () => { subplotType = 'uneven'; drawSubplots(); }); $('btn-gridspec')?.addEventListener('click', () => { subplotType = 'gridspec'; drawSubplots(); }); // Styling $('btn-style-default')?.addEventListener('click', () => { styleType = 'default'; drawStyling(); }); $('btn-style-seaborn')?.addEventListener('click', () => { styleType = 'seaborn'; drawStyling(); }); $('btn-style-ggplot')?.addEventListener('click', () => { styleType = 'ggplot'; drawStyling(); }); $('btn-style-dark')?.addEventListener('click', () => { styleType = 'dark'; drawStyling(); }); $('btn-style-538')?.addEventListener('click', () => { styleType = '538'; drawStyling(); }); // Categorical $('btn-stripplot')?.addEventListener('click', () => { catPlotType = 'stripplot'; drawCategorical(); }); $('btn-swarmplot')?.addEventListener('click', () => { catPlotType = 'swarmplot'; drawCategorical(); }); $('btn-boxplot')?.addEventListener('click', () => { catPlotType = 'boxplot'; drawCategorical(); }); $('btn-violinplot')?.addEventListener('click', () => { catPlotType = 'violinplot'; drawCategorical(); }); $('btn-barplot')?.addEventListener('click', () => { catPlotType = 'barplot'; drawCategorical(); }); // 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(); }); // Relationship buttons $('btn-scatter-hue')?.addEventListener('click', () => { relPlotType = 'scatter'; drawRelationships(); }); $('btn-regplot')?.addEventListener('click', () => { relPlotType = 'regplot'; drawRelationships(); }); $('btn-residplot')?.addEventListener('click', () => { relPlotType = 'residplot'; drawRelationships(); }); $('btn-pairplot')?.addEventListener('click', () => { relPlotType = 'pairplot'; drawRelationships(); }); // 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); // Geospatial $('btn-choropleth')?.addEventListener('click', () => { geoType = 'choropleth'; drawGeo(); }); $('btn-scatter-geo')?.addEventListener('click', () => { geoType = 'scatter'; drawGeo(); }); $('btn-heatmap-geo')?.addEventListener('click', () => { geoType = 'heatmap'; drawGeo(); }); // 3D Plots $('btn-3d-scatter')?.addEventListener('click', () => { plot3DType = 'scatter'; draw3D(); }); $('btn-3d-surface')?.addEventListener('click', () => { plot3DType = 'surface'; draw3D(); }); $('btn-3d-wireframe')?.addEventListener('click', () => { plot3DType = 'wireframe'; draw3D(); }); // 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 drawSubplots(); drawStyling(); drawSeabornIntro(); drawCategorical(); drawPlotly(); drawDashboard(); drawAnscombe(); perceptionMode = 'position'; drawPerception(); drawGrammar(); chartPurpose = 'comparison'; drawChoosingCharts(); drawAnatomy(); basicPlotType = 'line'; drawBasicPlots(); distPlotType = 'histplot'; drawDistributions(); relPlotType = 'scatter'; drawRelationships(); heatmapType = 'basic'; drawHeatmaps(); drawAnimation(); // Draw Advanced Topics initial state geoType = 'choropleth'; drawGeo(); plot3DType = 'scatter'; draw3D(); drawStorytelling(); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();