Spaces:
Running
Running
Commit ·
939a3b2
1
Parent(s): 8876fbf
Enhance Visualization module with Python code snippets and fix broken interactive canvases
Browse files- Visualization/app.js +603 -31
- Visualization/index.html +597 -1
Visualization/app.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
| 3 |
* Demonstrates core visualization concepts through live examples
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
(function() {
|
| 7 |
'use strict';
|
| 8 |
|
| 9 |
// Utility functions
|
| 10 |
const $ = id => document.getElementById(id);
|
| 11 |
const $$ = sel => document.querySelectorAll(sel);
|
| 12 |
-
|
| 13 |
// Color palette
|
| 14 |
const COLORS = {
|
| 15 |
primary: '#6366f1',
|
|
@@ -72,7 +72,7 @@
|
|
| 72 |
data.x.forEach((x, j) => {
|
| 73 |
const px = offsetX + ((x - xMin) / (xMax - xMin)) * (panelWidth - 50);
|
| 74 |
const py = offsetY + panelHeight - 25 - ((data.y[j] - yMin) / (yMax - yMin)) * (panelHeight - 40);
|
| 75 |
-
|
| 76 |
ctx.beginPath();
|
| 77 |
ctx.arc(px, py, 5, 0, Math.PI * 2);
|
| 78 |
ctx.fillStyle = [COLORS.primary, COLORS.secondary, COLORS.accent, COLORS.success][i];
|
|
@@ -111,10 +111,10 @@
|
|
| 111 |
data.forEach((v, i) => {
|
| 112 |
const x = startX + i * (barWidth + 15);
|
| 113 |
const height = v * 2.5;
|
| 114 |
-
|
| 115 |
ctx.fillStyle = COLORS.primary;
|
| 116 |
ctx.fillRect(x, 280 - height, barWidth, height);
|
| 117 |
-
|
| 118 |
ctx.fillStyle = COLORS.dark;
|
| 119 |
ctx.font = '11px Inter, sans-serif';
|
| 120 |
ctx.textAlign = 'center';
|
|
@@ -135,7 +135,7 @@
|
|
| 135 |
const hue = (v / 100) * 240; // Blue to red gradient
|
| 136 |
ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`;
|
| 137 |
ctx.fillRect(x, 80, squareSize, squareSize);
|
| 138 |
-
|
| 139 |
ctx.fillStyle = COLORS.dark;
|
| 140 |
ctx.font = '11px Inter, sans-serif';
|
| 141 |
ctx.textAlign = 'center';
|
|
@@ -150,7 +150,7 @@
|
|
| 150 |
ctx.textAlign = 'left';
|
| 151 |
ctx.fillText('Low', 50, 220);
|
| 152 |
ctx.fillText('High', 650, 220);
|
| 153 |
-
|
| 154 |
const gradWidth = 600;
|
| 155 |
for (let i = 0; i < gradWidth; i++) {
|
| 156 |
const hue = 240 - (i / gradWidth) * 240;
|
|
@@ -168,14 +168,14 @@
|
|
| 168 |
data.forEach((v, i) => {
|
| 169 |
const x = startX + i * 65;
|
| 170 |
const radius = Math.sqrt(v) * 3.5;
|
| 171 |
-
|
| 172 |
ctx.beginPath();
|
| 173 |
ctx.arc(x, 150, radius, 0, Math.PI * 2);
|
| 174 |
ctx.fillStyle = COLORS.accent;
|
| 175 |
ctx.globalAlpha = 0.7;
|
| 176 |
ctx.fill();
|
| 177 |
ctx.globalAlpha = 1;
|
| 178 |
-
|
| 179 |
ctx.fillStyle = COLORS.dark;
|
| 180 |
ctx.font = '10px Inter, sans-serif';
|
| 181 |
ctx.textAlign = 'center';
|
|
@@ -210,7 +210,7 @@
|
|
| 210 |
components.forEach((comp, i) => {
|
| 211 |
const y = startY + i * layerHeight;
|
| 212 |
const width = 180 - i * 10;
|
| 213 |
-
|
| 214 |
// Layer rectangle
|
| 215 |
ctx.fillStyle = comp.color;
|
| 216 |
ctx.globalAlpha = 0.8;
|
|
@@ -218,13 +218,13 @@
|
|
| 218 |
ctx.roundRect(centerX - width / 2, y, width, 35, 5);
|
| 219 |
ctx.fill();
|
| 220 |
ctx.globalAlpha = 1;
|
| 221 |
-
|
| 222 |
// Text
|
| 223 |
ctx.fillStyle = 'white';
|
| 224 |
ctx.font = 'bold 13px Inter, sans-serif';
|
| 225 |
ctx.textAlign = 'center';
|
| 226 |
ctx.fillText(`${comp.icon} ${comp.name}`, centerX, y + 22);
|
| 227 |
-
|
| 228 |
// Connector
|
| 229 |
if (i < components.length - 1) {
|
| 230 |
ctx.strokeStyle = '#94a3b8';
|
|
@@ -276,7 +276,7 @@
|
|
| 276 |
|
| 277 |
if (chartPurpose === 'comparison') {
|
| 278 |
ctx.fillText('Comparison: Bar Chart / Grouped Bar / Line', canvas.width / 2, 25);
|
| 279 |
-
|
| 280 |
const data = [
|
| 281 |
{ name: 'Q1', values: [40, 55, 30] },
|
| 282 |
{ name: 'Q2', values: [65, 45, 50] },
|
|
@@ -312,7 +312,7 @@
|
|
| 312 |
});
|
| 313 |
} else if (chartPurpose === 'composition') {
|
| 314 |
ctx.fillText('Composition: Stacked Bar / Pie Chart (use sparingly!)', canvas.width / 2, 25);
|
| 315 |
-
|
| 316 |
// Stacked bar
|
| 317 |
const data = [
|
| 318 |
{ name: '2020', values: [30, 25, 45] },
|
|
@@ -353,7 +353,7 @@
|
|
| 353 |
});
|
| 354 |
} else if (chartPurpose === 'distribution') {
|
| 355 |
ctx.fillText('Distribution: Histogram / Box Plot / Violin', canvas.width / 2, 25);
|
| 356 |
-
|
| 357 |
// Simple histogram
|
| 358 |
const bins = [5, 12, 25, 42, 55, 48, 30, 18, 8, 3];
|
| 359 |
const barWidth = 50;
|
|
@@ -376,7 +376,7 @@
|
|
| 376 |
ctx.fillText('← Histogram shows full distribution', 350, 340);
|
| 377 |
} else if (chartPurpose === 'relationship') {
|
| 378 |
ctx.fillText('Relationship: Scatter Plot / Bubble / Heatmap', canvas.width / 2, 25);
|
| 379 |
-
|
| 380 |
// Random scatter with trend
|
| 381 |
ctx.strokeStyle = '#e5e7eb';
|
| 382 |
ctx.lineWidth = 1;
|
|
@@ -513,7 +513,7 @@
|
|
| 513 |
ctx.fillStyle = COLORS.dark;
|
| 514 |
ctx.textAlign = 'left';
|
| 515 |
ctx.fillText(a.label, a.x, a.y);
|
| 516 |
-
|
| 517 |
ctx.strokeStyle = '#94a3b8';
|
| 518 |
ctx.lineWidth = 1;
|
| 519 |
ctx.setLineDash([3, 3]);
|
|
@@ -607,7 +607,7 @@
|
|
| 607 |
const x = margin.left + Math.random() * width;
|
| 608 |
const baseY = margin.top + height - ((x - margin.left) / width) * height * 0.7;
|
| 609 |
const y = baseY + (Math.random() - 0.5) * 100;
|
| 610 |
-
|
| 611 |
ctx.beginPath();
|
| 612 |
ctx.arc(x, Math.max(margin.top, Math.min(margin.top + height, y)), 6, 0, Math.PI * 2);
|
| 613 |
ctx.fillStyle = COLORS.accent;
|
|
@@ -628,10 +628,10 @@
|
|
| 628 |
data.forEach((v, i) => {
|
| 629 |
const x = margin.left + i * (barWidth + 20) + 10;
|
| 630 |
const barHeight = (v / 100) * height;
|
| 631 |
-
|
| 632 |
ctx.fillStyle = COLORS.primary;
|
| 633 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
| 634 |
-
|
| 635 |
ctx.fillStyle = COLORS.dark;
|
| 636 |
ctx.font = '11px Inter, sans-serif';
|
| 637 |
ctx.textAlign = 'center';
|
|
@@ -652,12 +652,12 @@
|
|
| 652 |
bins.forEach((v, i) => {
|
| 653 |
const x = margin.left + i * (barWidth + 2);
|
| 654 |
const barHeight = (v / maxVal) * height;
|
| 655 |
-
|
| 656 |
ctx.fillStyle = COLORS.secondary;
|
| 657 |
ctx.globalAlpha = 0.8;
|
| 658 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
| 659 |
ctx.globalAlpha = 1;
|
| 660 |
-
|
| 661 |
ctx.strokeStyle = 'white';
|
| 662 |
ctx.lineWidth = 1;
|
| 663 |
ctx.strokeRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
|
@@ -728,6 +728,192 @@
|
|
| 728 |
ctx.stroke();
|
| 729 |
}
|
| 730 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
// ==================== SEABORN DISTRIBUTION PLOTS ====================
|
| 732 |
let distPlotType = 'histplot';
|
| 733 |
|
|
@@ -754,7 +940,7 @@
|
|
| 754 |
bins.forEach((v, i) => {
|
| 755 |
const x = margin.left + i * (barWidth + 4);
|
| 756 |
const barHeight = (v / maxVal) * height * 0.9;
|
| 757 |
-
|
| 758 |
ctx.fillStyle = COLORS.primary;
|
| 759 |
ctx.globalAlpha = 0.6;
|
| 760 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
|
@@ -1011,7 +1197,7 @@
|
|
| 1011 |
// Draw dendrogram (simplified tree)
|
| 1012 |
ctx.strokeStyle = COLORS.gray;
|
| 1013 |
ctx.lineWidth = 1;
|
| 1014 |
-
|
| 1015 |
// Top dendrogram
|
| 1016 |
const dendroY = 60;
|
| 1017 |
ctx.beginPath();
|
|
@@ -1040,12 +1226,12 @@
|
|
| 1040 |
for (let j = 0; j < cols; j++) {
|
| 1041 |
const x = startX + j * cellSize;
|
| 1042 |
const y = startY + i * cellSize;
|
| 1043 |
-
|
| 1044 |
// Clustered pattern
|
| 1045 |
let val = Math.random();
|
| 1046 |
if ((i < 2 && j < 3) || (i >= 2 && j >= 3)) val = val * 0.3 + 0.7; // high values in clusters
|
| 1047 |
else val = val * 0.3;
|
| 1048 |
-
|
| 1049 |
const hue = 240 - val * 240;
|
| 1050 |
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
| 1051 |
ctx.fillRect(x, y, cellSize - 2, cellSize - 2);
|
|
@@ -1112,15 +1298,15 @@
|
|
| 1112 |
|
| 1113 |
// Animate movement
|
| 1114 |
const progress = (animFrame % 100) / 100;
|
| 1115 |
-
|
| 1116 |
countries.forEach(country => {
|
| 1117 |
// Countries move up and right over time (improving)
|
| 1118 |
const xOffset = progress * 0.3 * Math.sin(progress * Math.PI);
|
| 1119 |
const yOffset = progress * 0.2;
|
| 1120 |
-
|
| 1121 |
const x = margin.left + (country.baseX + xOffset) * width;
|
| 1122 |
const y = margin.top + (country.baseY - yOffset) * height;
|
| 1123 |
-
|
| 1124 |
ctx.beginPath();
|
| 1125 |
ctx.arc(x, y, country.size * (1 + progress * 0.3), 0, Math.PI * 2);
|
| 1126 |
ctx.fillStyle = country.color;
|
|
@@ -1186,6 +1372,367 @@
|
|
| 1186 |
}
|
| 1187 |
}
|
| 1188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1189 |
// ==================== EVENT LISTENERS ====================
|
| 1190 |
function bindEvents() {
|
| 1191 |
// Perception buttons
|
|
@@ -1212,6 +1759,12 @@
|
|
| 1212 |
$('btn-ecdfplot')?.addEventListener('click', () => { distPlotType = 'ecdfplot'; drawDistributions(); });
|
| 1213 |
$('btn-rugplot')?.addEventListener('click', () => { distPlotType = 'rugplot'; drawDistributions(); });
|
| 1214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
// Heatmap buttons
|
| 1216 |
$('btn-heatmap-basic')?.addEventListener('click', () => { heatmapType = 'basic'; drawHeatmaps(); });
|
| 1217 |
$('btn-corr-matrix')?.addEventListener('click', () => { heatmapType = 'corr'; drawHeatmaps(); });
|
|
@@ -1221,6 +1774,16 @@
|
|
| 1221 |
$('btn-animate')?.addEventListener('click', startAnimation);
|
| 1222 |
$('btn-stop')?.addEventListener('click', stopAnimation);
|
| 1223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
// Smooth scroll for nav links
|
| 1225 |
$$('.nav__link').forEach(link => {
|
| 1226 |
link.addEventListener('click', (e) => {
|
|
@@ -1230,7 +1793,7 @@
|
|
| 1230 |
if (target) {
|
| 1231 |
target.scrollIntoView({ behavior: 'smooth' });
|
| 1232 |
}
|
| 1233 |
-
|
| 1234 |
// Update active state
|
| 1235 |
$$('.nav__link').forEach(l => l.classList.remove('active'));
|
| 1236 |
link.classList.add('active');
|
|
@@ -1241,7 +1804,7 @@
|
|
| 1241 |
// ==================== INITIALIZATION ====================
|
| 1242 |
function init() {
|
| 1243 |
bindEvents();
|
| 1244 |
-
|
| 1245 |
// Draw all initial visualizations
|
| 1246 |
drawAnscombe();
|
| 1247 |
perceptionMode = 'position';
|
|
@@ -1254,9 +1817,18 @@
|
|
| 1254 |
drawBasicPlots();
|
| 1255 |
distPlotType = 'histplot';
|
| 1256 |
drawDistributions();
|
|
|
|
|
|
|
| 1257 |
heatmapType = 'basic';
|
| 1258 |
drawHeatmaps();
|
| 1259 |
drawAnimation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1260 |
}
|
| 1261 |
|
| 1262 |
// Run on DOM ready
|
|
|
|
| 3 |
* Demonstrates core visualization concepts through live examples
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
(function () {
|
| 7 |
'use strict';
|
| 8 |
|
| 9 |
// Utility functions
|
| 10 |
const $ = id => document.getElementById(id);
|
| 11 |
const $$ = sel => document.querySelectorAll(sel);
|
| 12 |
+
|
| 13 |
// Color palette
|
| 14 |
const COLORS = {
|
| 15 |
primary: '#6366f1',
|
|
|
|
| 72 |
data.x.forEach((x, j) => {
|
| 73 |
const px = offsetX + ((x - xMin) / (xMax - xMin)) * (panelWidth - 50);
|
| 74 |
const py = offsetY + panelHeight - 25 - ((data.y[j] - yMin) / (yMax - yMin)) * (panelHeight - 40);
|
| 75 |
+
|
| 76 |
ctx.beginPath();
|
| 77 |
ctx.arc(px, py, 5, 0, Math.PI * 2);
|
| 78 |
ctx.fillStyle = [COLORS.primary, COLORS.secondary, COLORS.accent, COLORS.success][i];
|
|
|
|
| 111 |
data.forEach((v, i) => {
|
| 112 |
const x = startX + i * (barWidth + 15);
|
| 113 |
const height = v * 2.5;
|
| 114 |
+
|
| 115 |
ctx.fillStyle = COLORS.primary;
|
| 116 |
ctx.fillRect(x, 280 - height, barWidth, height);
|
| 117 |
+
|
| 118 |
ctx.fillStyle = COLORS.dark;
|
| 119 |
ctx.font = '11px Inter, sans-serif';
|
| 120 |
ctx.textAlign = 'center';
|
|
|
|
| 135 |
const hue = (v / 100) * 240; // Blue to red gradient
|
| 136 |
ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`;
|
| 137 |
ctx.fillRect(x, 80, squareSize, squareSize);
|
| 138 |
+
|
| 139 |
ctx.fillStyle = COLORS.dark;
|
| 140 |
ctx.font = '11px Inter, sans-serif';
|
| 141 |
ctx.textAlign = 'center';
|
|
|
|
| 150 |
ctx.textAlign = 'left';
|
| 151 |
ctx.fillText('Low', 50, 220);
|
| 152 |
ctx.fillText('High', 650, 220);
|
| 153 |
+
|
| 154 |
const gradWidth = 600;
|
| 155 |
for (let i = 0; i < gradWidth; i++) {
|
| 156 |
const hue = 240 - (i / gradWidth) * 240;
|
|
|
|
| 168 |
data.forEach((v, i) => {
|
| 169 |
const x = startX + i * 65;
|
| 170 |
const radius = Math.sqrt(v) * 3.5;
|
| 171 |
+
|
| 172 |
ctx.beginPath();
|
| 173 |
ctx.arc(x, 150, radius, 0, Math.PI * 2);
|
| 174 |
ctx.fillStyle = COLORS.accent;
|
| 175 |
ctx.globalAlpha = 0.7;
|
| 176 |
ctx.fill();
|
| 177 |
ctx.globalAlpha = 1;
|
| 178 |
+
|
| 179 |
ctx.fillStyle = COLORS.dark;
|
| 180 |
ctx.font = '10px Inter, sans-serif';
|
| 181 |
ctx.textAlign = 'center';
|
|
|
|
| 210 |
components.forEach((comp, i) => {
|
| 211 |
const y = startY + i * layerHeight;
|
| 212 |
const width = 180 - i * 10;
|
| 213 |
+
|
| 214 |
// Layer rectangle
|
| 215 |
ctx.fillStyle = comp.color;
|
| 216 |
ctx.globalAlpha = 0.8;
|
|
|
|
| 218 |
ctx.roundRect(centerX - width / 2, y, width, 35, 5);
|
| 219 |
ctx.fill();
|
| 220 |
ctx.globalAlpha = 1;
|
| 221 |
+
|
| 222 |
// Text
|
| 223 |
ctx.fillStyle = 'white';
|
| 224 |
ctx.font = 'bold 13px Inter, sans-serif';
|
| 225 |
ctx.textAlign = 'center';
|
| 226 |
ctx.fillText(`${comp.icon} ${comp.name}`, centerX, y + 22);
|
| 227 |
+
|
| 228 |
// Connector
|
| 229 |
if (i < components.length - 1) {
|
| 230 |
ctx.strokeStyle = '#94a3b8';
|
|
|
|
| 276 |
|
| 277 |
if (chartPurpose === 'comparison') {
|
| 278 |
ctx.fillText('Comparison: Bar Chart / Grouped Bar / Line', canvas.width / 2, 25);
|
| 279 |
+
|
| 280 |
const data = [
|
| 281 |
{ name: 'Q1', values: [40, 55, 30] },
|
| 282 |
{ name: 'Q2', values: [65, 45, 50] },
|
|
|
|
| 312 |
});
|
| 313 |
} else if (chartPurpose === 'composition') {
|
| 314 |
ctx.fillText('Composition: Stacked Bar / Pie Chart (use sparingly!)', canvas.width / 2, 25);
|
| 315 |
+
|
| 316 |
// Stacked bar
|
| 317 |
const data = [
|
| 318 |
{ name: '2020', values: [30, 25, 45] },
|
|
|
|
| 353 |
});
|
| 354 |
} else if (chartPurpose === 'distribution') {
|
| 355 |
ctx.fillText('Distribution: Histogram / Box Plot / Violin', canvas.width / 2, 25);
|
| 356 |
+
|
| 357 |
// Simple histogram
|
| 358 |
const bins = [5, 12, 25, 42, 55, 48, 30, 18, 8, 3];
|
| 359 |
const barWidth = 50;
|
|
|
|
| 376 |
ctx.fillText('← Histogram shows full distribution', 350, 340);
|
| 377 |
} else if (chartPurpose === 'relationship') {
|
| 378 |
ctx.fillText('Relationship: Scatter Plot / Bubble / Heatmap', canvas.width / 2, 25);
|
| 379 |
+
|
| 380 |
// Random scatter with trend
|
| 381 |
ctx.strokeStyle = '#e5e7eb';
|
| 382 |
ctx.lineWidth = 1;
|
|
|
|
| 513 |
ctx.fillStyle = COLORS.dark;
|
| 514 |
ctx.textAlign = 'left';
|
| 515 |
ctx.fillText(a.label, a.x, a.y);
|
| 516 |
+
|
| 517 |
ctx.strokeStyle = '#94a3b8';
|
| 518 |
ctx.lineWidth = 1;
|
| 519 |
ctx.setLineDash([3, 3]);
|
|
|
|
| 607 |
const x = margin.left + Math.random() * width;
|
| 608 |
const baseY = margin.top + height - ((x - margin.left) / width) * height * 0.7;
|
| 609 |
const y = baseY + (Math.random() - 0.5) * 100;
|
| 610 |
+
|
| 611 |
ctx.beginPath();
|
| 612 |
ctx.arc(x, Math.max(margin.top, Math.min(margin.top + height, y)), 6, 0, Math.PI * 2);
|
| 613 |
ctx.fillStyle = COLORS.accent;
|
|
|
|
| 628 |
data.forEach((v, i) => {
|
| 629 |
const x = margin.left + i * (barWidth + 20) + 10;
|
| 630 |
const barHeight = (v / 100) * height;
|
| 631 |
+
|
| 632 |
ctx.fillStyle = COLORS.primary;
|
| 633 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
| 634 |
+
|
| 635 |
ctx.fillStyle = COLORS.dark;
|
| 636 |
ctx.font = '11px Inter, sans-serif';
|
| 637 |
ctx.textAlign = 'center';
|
|
|
|
| 652 |
bins.forEach((v, i) => {
|
| 653 |
const x = margin.left + i * (barWidth + 2);
|
| 654 |
const barHeight = (v / maxVal) * height;
|
| 655 |
+
|
| 656 |
ctx.fillStyle = COLORS.secondary;
|
| 657 |
ctx.globalAlpha = 0.8;
|
| 658 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
| 659 |
ctx.globalAlpha = 1;
|
| 660 |
+
|
| 661 |
ctx.strokeStyle = 'white';
|
| 662 |
ctx.lineWidth = 1;
|
| 663 |
ctx.strokeRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
|
|
|
| 728 |
ctx.stroke();
|
| 729 |
}
|
| 730 |
|
| 731 |
+
// ==================== SEABORN RELATIONSHIP PLOTS ====================
|
| 732 |
+
let relPlotType = 'scatter';
|
| 733 |
+
|
| 734 |
+
function drawRelationships() {
|
| 735 |
+
const canvas = $('canvas-relationships');
|
| 736 |
+
if (!canvas) return;
|
| 737 |
+
const ctx = canvas.getContext('2d');
|
| 738 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 739 |
+
|
| 740 |
+
const margin = { top: 50, right: 150, bottom: 50, left: 70 };
|
| 741 |
+
const width = canvas.width - margin.left - margin.right;
|
| 742 |
+
const height = canvas.height - margin.top - margin.bottom;
|
| 743 |
+
|
| 744 |
+
// Generate correlated data
|
| 745 |
+
const n = 60;
|
| 746 |
+
const data = [];
|
| 747 |
+
for (let i = 0; i < n; i++) {
|
| 748 |
+
const x = margin.left + (i / n) * width;
|
| 749 |
+
const group = i % 2 === 0 ? 'A' : 'B';
|
| 750 |
+
const noise = (Math.random() - 0.5) * 50;
|
| 751 |
+
const slope = group === 'A' ? 0.5 : 0.8;
|
| 752 |
+
const intercept = group === 'A' ? 20 : -30;
|
| 753 |
+
const yRaw = (i / n) * 100 * slope + intercept + noise;
|
| 754 |
+
// Map to canvas Y
|
| 755 |
+
const y = margin.top + height - ((yRaw + 50) / 150) * height;
|
| 756 |
+
data.push({ x, y, group });
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
if (relPlotType === 'scatter') {
|
| 760 |
+
ctx.fillStyle = COLORS.dark;
|
| 761 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 762 |
+
ctx.textAlign = 'center';
|
| 763 |
+
ctx.fillText('sns.scatterplot(..., hue="group")', canvas.width / 2, 25);
|
| 764 |
+
|
| 765 |
+
data.forEach(d => {
|
| 766 |
+
ctx.beginPath();
|
| 767 |
+
ctx.arc(d.x, d.y, 6, 0, Math.PI * 2);
|
| 768 |
+
ctx.fillStyle = d.group === 'A' ? COLORS.primary : COLORS.secondary;
|
| 769 |
+
ctx.globalAlpha = 0.7;
|
| 770 |
+
ctx.fill();
|
| 771 |
+
ctx.strokeStyle = 'white';
|
| 772 |
+
ctx.globalAlpha = 1;
|
| 773 |
+
ctx.stroke();
|
| 774 |
+
});
|
| 775 |
+
|
| 776 |
+
// Legend
|
| 777 |
+
ctx.fillStyle = COLORS.primary;
|
| 778 |
+
ctx.fillRect(canvas.width - 120, 60, 15, 15);
|
| 779 |
+
ctx.fillStyle = COLORS.secondary;
|
| 780 |
+
ctx.fillRect(canvas.width - 120, 85, 15, 15);
|
| 781 |
+
ctx.fillStyle = COLORS.dark;
|
| 782 |
+
ctx.font = '12px Inter, sans-serif';
|
| 783 |
+
ctx.textAlign = 'left';
|
| 784 |
+
ctx.fillText('Group A', canvas.width - 95, 72);
|
| 785 |
+
ctx.fillText('Group B', canvas.width - 95, 97);
|
| 786 |
+
|
| 787 |
+
} else if (relPlotType === 'regplot') {
|
| 788 |
+
ctx.fillStyle = COLORS.dark;
|
| 789 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 790 |
+
ctx.textAlign = 'center';
|
| 791 |
+
ctx.fillText('sns.regplot() - Scatter + Regression Line + 95% CI', canvas.width / 2, 25);
|
| 792 |
+
|
| 793 |
+
// Draw confidence interval band (approximated)
|
| 794 |
+
ctx.beginPath();
|
| 795 |
+
ctx.moveTo(margin.left, margin.top + height - 30);
|
| 796 |
+
ctx.lineTo(margin.left + width, margin.top + 20);
|
| 797 |
+
ctx.lineTo(margin.left + width, margin.top + 80);
|
| 798 |
+
ctx.lineTo(margin.left, margin.top + height - (-30));
|
| 799 |
+
ctx.fillStyle = COLORS.primary;
|
| 800 |
+
ctx.globalAlpha = 0.2;
|
| 801 |
+
ctx.fill();
|
| 802 |
+
ctx.globalAlpha = 1;
|
| 803 |
+
|
| 804 |
+
// Draw regression line
|
| 805 |
+
ctx.beginPath();
|
| 806 |
+
ctx.moveTo(margin.left, margin.top + height - 10);
|
| 807 |
+
ctx.lineTo(margin.left + width, margin.top + 50);
|
| 808 |
+
ctx.strokeStyle = COLORS.primary;
|
| 809 |
+
ctx.lineWidth = 3;
|
| 810 |
+
ctx.stroke();
|
| 811 |
+
|
| 812 |
+
// Draw points
|
| 813 |
+
data.forEach(d => {
|
| 814 |
+
ctx.beginPath();
|
| 815 |
+
ctx.arc(d.x, d.y, 5, 0, Math.PI * 2);
|
| 816 |
+
ctx.fillStyle = COLORS.dark;
|
| 817 |
+
ctx.globalAlpha = 0.5;
|
| 818 |
+
ctx.fill();
|
| 819 |
+
ctx.globalAlpha = 1;
|
| 820 |
+
});
|
| 821 |
+
} else if (relPlotType === 'residplot') {
|
| 822 |
+
ctx.fillStyle = COLORS.dark;
|
| 823 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 824 |
+
ctx.textAlign = 'center';
|
| 825 |
+
ctx.fillText('sns.residplot() - Plotting Regression Residuals', canvas.width / 2, 25);
|
| 826 |
+
|
| 827 |
+
// Zero line
|
| 828 |
+
const midY = margin.top + height / 2;
|
| 829 |
+
ctx.beginPath();
|
| 830 |
+
ctx.moveTo(margin.left, midY);
|
| 831 |
+
ctx.lineTo(margin.left + width, midY);
|
| 832 |
+
ctx.strokeStyle = COLORS.danger;
|
| 833 |
+
ctx.setLineDash([5, 5]);
|
| 834 |
+
ctx.stroke();
|
| 835 |
+
ctx.setLineDash([]);
|
| 836 |
+
|
| 837 |
+
// Residuals
|
| 838 |
+
data.forEach(d => {
|
| 839 |
+
// Calculate distance from an imaginary regression line
|
| 840 |
+
const expectedY = margin.top + height - 10 - ((d.x - margin.left) / width) * (height - 60);
|
| 841 |
+
const residual = d.y - expectedY;
|
| 842 |
+
|
| 843 |
+
ctx.beginPath();
|
| 844 |
+
ctx.arc(d.x, midY + residual, 5, 0, Math.PI * 2);
|
| 845 |
+
ctx.fillStyle = COLORS.blue;
|
| 846 |
+
ctx.globalAlpha = 0.6;
|
| 847 |
+
ctx.fill();
|
| 848 |
+
ctx.globalAlpha = 1;
|
| 849 |
+
|
| 850 |
+
// Stem
|
| 851 |
+
ctx.beginPath();
|
| 852 |
+
ctx.moveTo(d.x, midY);
|
| 853 |
+
ctx.lineTo(d.x, midY + residual);
|
| 854 |
+
ctx.strokeStyle = COLORS.blue;
|
| 855 |
+
ctx.globalAlpha = 0.3;
|
| 856 |
+
ctx.stroke();
|
| 857 |
+
ctx.globalAlpha = 1;
|
| 858 |
+
});
|
| 859 |
+
} else if (relPlotType === 'pairplot') {
|
| 860 |
+
ctx.fillStyle = COLORS.dark;
|
| 861 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 862 |
+
ctx.textAlign = 'center';
|
| 863 |
+
ctx.fillText('sns.pairplot() - Pairwise relationships', canvas.width / 2, 25);
|
| 864 |
+
|
| 865 |
+
const gridSize = 3;
|
| 866 |
+
const cellW = width / gridSize;
|
| 867 |
+
const cellH = height / gridSize;
|
| 868 |
+
|
| 869 |
+
ctx.lineWidth = 1;
|
| 870 |
+
for (let r = 0; r < gridSize; r++) {
|
| 871 |
+
for (let c = 0; c < gridSize; c++) {
|
| 872 |
+
const cx = margin.left + c * cellW;
|
| 873 |
+
const cy = margin.top + r * cellH;
|
| 874 |
+
|
| 875 |
+
ctx.strokeStyle = COLORS.gray;
|
| 876 |
+
ctx.strokeRect(cx, cy, cellW, cellH);
|
| 877 |
+
|
| 878 |
+
// Diagonal: KDE
|
| 879 |
+
if (r === c) {
|
| 880 |
+
ctx.beginPath();
|
| 881 |
+
ctx.moveTo(cx, cy + cellH);
|
| 882 |
+
for (let i = 0; i <= cellW; i += 5) {
|
| 883 |
+
const h = Math.sin((i / cellW) * Math.PI) * (cellH * 0.8) + (Math.random() * 10 - 5);
|
| 884 |
+
ctx.lineTo(cx + i, cy + cellH - Math.max(0, h));
|
| 885 |
+
}
|
| 886 |
+
ctx.lineTo(cx + cellW, cy + cellH);
|
| 887 |
+
ctx.fillStyle = COLORS.success;
|
| 888 |
+
ctx.globalAlpha = 0.3;
|
| 889 |
+
ctx.fill();
|
| 890 |
+
ctx.globalAlpha = 1;
|
| 891 |
+
}
|
| 892 |
+
// Off-diagonal: Scatter
|
| 893 |
+
else {
|
| 894 |
+
for (let i = 0; i < 20; i++) {
|
| 895 |
+
ctx.beginPath();
|
| 896 |
+
ctx.arc(cx + 5 + Math.random() * (cellW - 10), cy + 5 + Math.random() * (cellH - 10), 3, 0, Math.PI * 2);
|
| 897 |
+
ctx.fillStyle = COLORS.dark;
|
| 898 |
+
ctx.globalAlpha = 0.4;
|
| 899 |
+
ctx.fill();
|
| 900 |
+
ctx.globalAlpha = 1;
|
| 901 |
+
}
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
// Axes
|
| 908 |
+
ctx.strokeStyle = COLORS.dark;
|
| 909 |
+
ctx.lineWidth = 2;
|
| 910 |
+
ctx.beginPath();
|
| 911 |
+
ctx.moveTo(margin.left, margin.top);
|
| 912 |
+
ctx.lineTo(margin.left, margin.top + height);
|
| 913 |
+
ctx.lineTo(margin.left + width, margin.top + height);
|
| 914 |
+
ctx.stroke();
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
// ==================== SEABORN DISTRIBUTION PLOTS ====================
|
| 918 |
let distPlotType = 'histplot';
|
| 919 |
|
|
|
|
| 940 |
bins.forEach((v, i) => {
|
| 941 |
const x = margin.left + i * (barWidth + 4);
|
| 942 |
const barHeight = (v / maxVal) * height * 0.9;
|
| 943 |
+
|
| 944 |
ctx.fillStyle = COLORS.primary;
|
| 945 |
ctx.globalAlpha = 0.6;
|
| 946 |
ctx.fillRect(x, margin.top + height - barHeight, barWidth, barHeight);
|
|
|
|
| 1197 |
// Draw dendrogram (simplified tree)
|
| 1198 |
ctx.strokeStyle = COLORS.gray;
|
| 1199 |
ctx.lineWidth = 1;
|
| 1200 |
+
|
| 1201 |
// Top dendrogram
|
| 1202 |
const dendroY = 60;
|
| 1203 |
ctx.beginPath();
|
|
|
|
| 1226 |
for (let j = 0; j < cols; j++) {
|
| 1227 |
const x = startX + j * cellSize;
|
| 1228 |
const y = startY + i * cellSize;
|
| 1229 |
+
|
| 1230 |
// Clustered pattern
|
| 1231 |
let val = Math.random();
|
| 1232 |
if ((i < 2 && j < 3) || (i >= 2 && j >= 3)) val = val * 0.3 + 0.7; // high values in clusters
|
| 1233 |
else val = val * 0.3;
|
| 1234 |
+
|
| 1235 |
const hue = 240 - val * 240;
|
| 1236 |
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
|
| 1237 |
ctx.fillRect(x, y, cellSize - 2, cellSize - 2);
|
|
|
|
| 1298 |
|
| 1299 |
// Animate movement
|
| 1300 |
const progress = (animFrame % 100) / 100;
|
| 1301 |
+
|
| 1302 |
countries.forEach(country => {
|
| 1303 |
// Countries move up and right over time (improving)
|
| 1304 |
const xOffset = progress * 0.3 * Math.sin(progress * Math.PI);
|
| 1305 |
const yOffset = progress * 0.2;
|
| 1306 |
+
|
| 1307 |
const x = margin.left + (country.baseX + xOffset) * width;
|
| 1308 |
const y = margin.top + (country.baseY - yOffset) * height;
|
| 1309 |
+
|
| 1310 |
ctx.beginPath();
|
| 1311 |
ctx.arc(x, y, country.size * (1 + progress * 0.3), 0, Math.PI * 2);
|
| 1312 |
ctx.fillStyle = country.color;
|
|
|
|
| 1372 |
}
|
| 1373 |
}
|
| 1374 |
|
| 1375 |
+
// ==================== GEOSPATIAL ====================
|
| 1376 |
+
let geoType = 'choropleth';
|
| 1377 |
+
|
| 1378 |
+
function drawGeo() {
|
| 1379 |
+
const canvas = $('canvas-geo');
|
| 1380 |
+
if (!canvas) return;
|
| 1381 |
+
const ctx = canvas.getContext('2d');
|
| 1382 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1383 |
+
|
| 1384 |
+
ctx.fillStyle = COLORS.dark;
|
| 1385 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 1386 |
+
ctx.textAlign = 'center';
|
| 1387 |
+
|
| 1388 |
+
if (geoType === 'choropleth') {
|
| 1389 |
+
ctx.fillText('Choropleth Map: Values mapped to regions', canvas.width / 2, 25);
|
| 1390 |
+
|
| 1391 |
+
// Draw grid lines (longitude/latitude)
|
| 1392 |
+
ctx.strokeStyle = '#e5e7eb';
|
| 1393 |
+
ctx.lineWidth = 1;
|
| 1394 |
+
for (let i = 0; i <= 8; i++) {
|
| 1395 |
+
ctx.beginPath();
|
| 1396 |
+
ctx.moveTo(i * 100, 50);
|
| 1397 |
+
ctx.lineTo(i * 100, 450);
|
| 1398 |
+
ctx.stroke();
|
| 1399 |
+
}
|
| 1400 |
+
for (let i = 0; i <= 4; i++) {
|
| 1401 |
+
ctx.beginPath();
|
| 1402 |
+
ctx.moveTo(0, 50 + i * 100);
|
| 1403 |
+
ctx.lineTo(800, 50 + i * 100);
|
| 1404 |
+
ctx.stroke();
|
| 1405 |
+
}
|
| 1406 |
+
|
| 1407 |
+
// Draw stylized map regions
|
| 1408 |
+
const regions = [
|
| 1409 |
+
{ path: [[100, 100], [300, 80], [350, 200], [150, 250]], val: 0.8 },
|
| 1410 |
+
{ path: [[350, 200], [500, 150], [600, 300], [400, 350]], val: 0.4 },
|
| 1411 |
+
{ path: [[150, 250], [400, 350], [300, 450], [100, 400]], val: 0.2 },
|
| 1412 |
+
{ path: [[500, 150], [700, 100], [750, 250], [600, 300]], val: 0.9 },
|
| 1413 |
+
{ path: [[400, 350], [600, 300], [650, 450], [450, 480]], val: 0.6 }
|
| 1414 |
+
];
|
| 1415 |
+
|
| 1416 |
+
regions.forEach(r => {
|
| 1417 |
+
ctx.beginPath();
|
| 1418 |
+
ctx.moveTo(r.path[0][0], r.path[0][1]);
|
| 1419 |
+
for (let i = 1; i < r.path.length; i++) {
|
| 1420 |
+
ctx.lineTo(r.path[i][0], r.path[i][1]);
|
| 1421 |
+
}
|
| 1422 |
+
ctx.closePath();
|
| 1423 |
+
|
| 1424 |
+
// Color mapping
|
| 1425 |
+
const hue = 240 - r.val * 240;
|
| 1426 |
+
ctx.fillStyle = `hsla(${hue}, 70%, 50%, 0.7)`;
|
| 1427 |
+
ctx.fill();
|
| 1428 |
+
ctx.strokeStyle = 'white';
|
| 1429 |
+
ctx.lineWidth = 2;
|
| 1430 |
+
ctx.stroke();
|
| 1431 |
+
});
|
| 1432 |
+
} else if (geoType === 'scatter') {
|
| 1433 |
+
ctx.fillText('Scatter on Map: Point data over geography', canvas.width / 2, 25);
|
| 1434 |
+
|
| 1435 |
+
// Draw basic continent outlines
|
| 1436 |
+
ctx.strokeStyle = COLORS.gray;
|
| 1437 |
+
ctx.lineWidth = 2;
|
| 1438 |
+
ctx.beginPath();
|
| 1439 |
+
// Rough Americas
|
| 1440 |
+
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();
|
| 1441 |
+
// Rough Afro-Eurasia
|
| 1442 |
+
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();
|
| 1443 |
+
ctx.stroke();
|
| 1444 |
+
ctx.fillStyle = '#f3f4f6';
|
| 1445 |
+
ctx.fill();
|
| 1446 |
+
|
| 1447 |
+
// Scatter points
|
| 1448 |
+
for (let i = 0; i < 40; i++) {
|
| 1449 |
+
const x = 150 + Math.random() * 600;
|
| 1450 |
+
const y = 80 + Math.random() * 350;
|
| 1451 |
+
ctx.beginPath();
|
| 1452 |
+
ctx.arc(x, y, 3 + Math.random() * 8, 0, Math.PI * 2);
|
| 1453 |
+
ctx.fillStyle = COLORS.primary;
|
| 1454 |
+
ctx.globalAlpha = 0.6;
|
| 1455 |
+
ctx.fill();
|
| 1456 |
+
ctx.globalAlpha = 1;
|
| 1457 |
+
}
|
| 1458 |
+
} else if (geoType === 'heatmap') {
|
| 1459 |
+
ctx.fillText('Density Heatmap: Clustering over geography', canvas.width / 2, 25);
|
| 1460 |
+
|
| 1461 |
+
// Base map
|
| 1462 |
+
ctx.fillStyle = '#1f2937';
|
| 1463 |
+
ctx.fillRect(50, 50, 700, 400);
|
| 1464 |
+
|
| 1465 |
+
// Gradient density clusters
|
| 1466 |
+
const clusters = [
|
| 1467 |
+
{ x: 250, y: 150, r: 100, intensity: 1 },
|
| 1468 |
+
{ x: 550, y: 200, r: 150, intensity: 0.8 },
|
| 1469 |
+
{ x: 300, y: 350, r: 120, intensity: 0.9 },
|
| 1470 |
+
{ x: 650, y: 350, r: 80, intensity: 0.6 }
|
| 1471 |
+
];
|
| 1472 |
+
|
| 1473 |
+
clusters.forEach(c => {
|
| 1474 |
+
const grad = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, c.r);
|
| 1475 |
+
grad.addColorStop(0, `rgba(239, 68, 68, ${c.intensity})`); // red
|
| 1476 |
+
grad.addColorStop(0.3, `rgba(245, 158, 11, ${c.intensity * 0.7})`); // orange
|
| 1477 |
+
grad.addColorStop(0.6, `rgba(16, 185, 129, ${c.intensity * 0.4})`); // green
|
| 1478 |
+
grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
| 1479 |
+
|
| 1480 |
+
ctx.fillStyle = grad;
|
| 1481 |
+
ctx.beginPath();
|
| 1482 |
+
ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
|
| 1483 |
+
ctx.fill();
|
| 1484 |
+
});
|
| 1485 |
+
}
|
| 1486 |
+
}
|
| 1487 |
+
|
| 1488 |
+
// ==================== 3D PLOTS ====================
|
| 1489 |
+
let plot3DType = 'scatter';
|
| 1490 |
+
|
| 1491 |
+
function draw3D() {
|
| 1492 |
+
const canvas = $('canvas-3d');
|
| 1493 |
+
if (!canvas) return;
|
| 1494 |
+
const ctx = canvas.getContext('2d');
|
| 1495 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1496 |
+
|
| 1497 |
+
ctx.fillStyle = COLORS.dark;
|
| 1498 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 1499 |
+
ctx.textAlign = 'center';
|
| 1500 |
+
|
| 1501 |
+
const cx = canvas.width / 2;
|
| 1502 |
+
const cy = canvas.height / 2 + 20;
|
| 1503 |
+
|
| 1504 |
+
// Draw isometric axes
|
| 1505 |
+
ctx.strokeStyle = COLORS.gray;
|
| 1506 |
+
ctx.lineWidth = 2;
|
| 1507 |
+
ctx.beginPath();
|
| 1508 |
+
// Z axis (up)
|
| 1509 |
+
ctx.moveTo(cx, cy); ctx.lineTo(cx, cy - 150);
|
| 1510 |
+
// X axis (down-left)
|
| 1511 |
+
ctx.moveTo(cx, cy); ctx.lineTo(cx - 150, cy + 86);
|
| 1512 |
+
// Y axis (down-right)
|
| 1513 |
+
ctx.moveTo(cx, cy); ctx.lineTo(cx + 150, cy + 86);
|
| 1514 |
+
ctx.stroke();
|
| 1515 |
+
|
| 1516 |
+
ctx.fillStyle = COLORS.gray;
|
| 1517 |
+
ctx.font = '12px Inter, sans-serif';
|
| 1518 |
+
ctx.fillText('Z', cx, cy - 160);
|
| 1519 |
+
ctx.fillText('X', cx - 160, cy + 96);
|
| 1520 |
+
ctx.fillText('Y', cx + 160, cy + 96);
|
| 1521 |
+
|
| 1522 |
+
// Helper for isometric projection
|
| 1523 |
+
const isoMap = (x, y, z) => {
|
| 1524 |
+
// 30 degree isometric projection
|
| 1525 |
+
const cos30 = Math.cos(Math.PI / 6);
|
| 1526 |
+
const sin30 = Math.sin(Math.PI / 6);
|
| 1527 |
+
return {
|
| 1528 |
+
px: cx + (y - x) * cos30,
|
| 1529 |
+
py: cy + (x + y) * sin30 - z
|
| 1530 |
+
};
|
| 1531 |
+
};
|
| 1532 |
+
|
| 1533 |
+
if (plot3DType === 'scatter') {
|
| 1534 |
+
ctx.fillText('3D Scatter Plot', canvas.width / 2, 25);
|
| 1535 |
+
|
| 1536 |
+
for (let i = 0; i < 80; i++) {
|
| 1537 |
+
// Random 3D coords [0, 100]
|
| 1538 |
+
const x = Math.random() * 100;
|
| 1539 |
+
const y = Math.random() * 100;
|
| 1540 |
+
const z = (x + y) / 2 + (Math.random() - 0.5) * 40; // correlated Z
|
| 1541 |
+
|
| 1542 |
+
const pos = isoMap(x, y, z);
|
| 1543 |
+
|
| 1544 |
+
ctx.beginPath();
|
| 1545 |
+
ctx.arc(pos.px, pos.py, 4, 0, Math.PI * 2);
|
| 1546 |
+
const hue = (z / 120) * 240;
|
| 1547 |
+
ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`;
|
| 1548 |
+
ctx.fill();
|
| 1549 |
+
ctx.strokeStyle = 'white';
|
| 1550 |
+
ctx.lineWidth = 1;
|
| 1551 |
+
ctx.stroke();
|
| 1552 |
+
}
|
| 1553 |
+
} else if (plot3DType === 'surface' || plot3DType === 'wireframe') {
|
| 1554 |
+
ctx.fillText(`3D ${plot3DType === 'surface' ? 'Surface' : 'Wireframe'} Plot (sin(R)/R)`, canvas.width / 2, 25);
|
| 1555 |
+
|
| 1556 |
+
const resolution = 15;
|
| 1557 |
+
const step = 120 / resolution;
|
| 1558 |
+
|
| 1559 |
+
const grid = [];
|
| 1560 |
+
for (let x = -60; x <= 60; x += step) {
|
| 1561 |
+
const row = [];
|
| 1562 |
+
for (let y = -60; y <= 60; y += step) {
|
| 1563 |
+
const r = Math.sqrt(x * x + y * y);
|
| 1564 |
+
const z = r === 0 ? 80 : Math.sin(r * 0.15) / (r * 0.15) * 80;
|
| 1565 |
+
row.push({ x: x + 60, y: y + 60, z: z + 40 });
|
| 1566 |
+
}
|
| 1567 |
+
grid.push(row);
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
ctx.lineWidth = 1;
|
| 1571 |
+
|
| 1572 |
+
// Draw polygons from back to front (rough Painter's Algorithm)
|
| 1573 |
+
// Since it's symmetric, a simple loop works decently
|
| 1574 |
+
for (let i = 0; i < grid.length - 1; i++) {
|
| 1575 |
+
for (let j = 0; j < grid[i].length - 1; j++) {
|
| 1576 |
+
const p1 = isoMap(grid[i][j].x, grid[i][j].y, grid[i][j].z);
|
| 1577 |
+
const p2 = isoMap(grid[i + 1][j].x, grid[i + 1][j].y, grid[i + 1][j].z);
|
| 1578 |
+
const p3 = isoMap(grid[i + 1][j + 1].x, grid[i + 1][j + 1].y, grid[i + 1][j + 1].z);
|
| 1579 |
+
const p4 = isoMap(grid[i][j + 1].x, grid[i][j + 1].y, grid[i][j + 1].z);
|
| 1580 |
+
|
| 1581 |
+
ctx.beginPath();
|
| 1582 |
+
ctx.moveTo(p1.px, p1.py);
|
| 1583 |
+
ctx.lineTo(p2.px, p2.py);
|
| 1584 |
+
ctx.lineTo(p3.px, p3.py);
|
| 1585 |
+
ctx.lineTo(p4.px, p4.py);
|
| 1586 |
+
ctx.closePath();
|
| 1587 |
+
|
| 1588 |
+
const avgZ = (grid[i][j].z + grid[i + 1][j].z + grid[i + 1][j + 1].z + grid[i][j + 1].z) / 4;
|
| 1589 |
+
const hue = (avgZ / 120) * 240 + 60; // offset hue
|
| 1590 |
+
|
| 1591 |
+
if (plot3DType === 'surface') {
|
| 1592 |
+
ctx.fillStyle = `hsl(${240 - hue}, 70%, 50%)`;
|
| 1593 |
+
ctx.fill();
|
| 1594 |
+
ctx.strokeStyle = `hsl(${240 - hue}, 70%, 30%)`;
|
| 1595 |
+
} else {
|
| 1596 |
+
ctx.strokeStyle = COLORS.primary;
|
| 1597 |
+
}
|
| 1598 |
+
ctx.stroke();
|
| 1599 |
+
}
|
| 1600 |
+
}
|
| 1601 |
+
}
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
// ==================== STORYTELLING ====================
|
| 1605 |
+
function drawStorytelling() {
|
| 1606 |
+
const canvas = $('canvas-storytelling');
|
| 1607 |
+
if (!canvas) return;
|
| 1608 |
+
const ctx = canvas.getContext('2d');
|
| 1609 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1610 |
+
|
| 1611 |
+
// Title: Big, bold conclusion
|
| 1612 |
+
ctx.fillStyle = COLORS.dark;
|
| 1613 |
+
ctx.font = 'bold 22px Inter, sans-serif';
|
| 1614 |
+
ctx.textAlign = 'left';
|
| 1615 |
+
ctx.fillText('Product B exceeded sales targets by 45% in Q4', 50, 40);
|
| 1616 |
+
|
| 1617 |
+
// Subtitle / context
|
| 1618 |
+
ctx.fillStyle = COLORS.gray;
|
| 1619 |
+
ctx.font = '14px Inter, sans-serif';
|
| 1620 |
+
ctx.fillText('Driven by the new marketing campaign launched in September.', 50, 65);
|
| 1621 |
+
|
| 1622 |
+
// Plot Area
|
| 1623 |
+
const margin = { left: 50, right: 150, top: 120, bottom: 50 };
|
| 1624 |
+
const width = canvas.width - margin.left - margin.right;
|
| 1625 |
+
const height = canvas.height - margin.top - margin.bottom;
|
| 1626 |
+
|
| 1627 |
+
// Remove harsh gridlines, only light Y grid
|
| 1628 |
+
ctx.strokeStyle = '#f3f4f6';
|
| 1629 |
+
ctx.lineWidth = 1;
|
| 1630 |
+
for (let i = 0; i <= 4; i++) {
|
| 1631 |
+
const y = margin.top + (i / 4) * height;
|
| 1632 |
+
ctx.beginPath();
|
| 1633 |
+
ctx.moveTo(margin.left, y);
|
| 1634 |
+
ctx.lineTo(margin.left + width, y);
|
| 1635 |
+
ctx.stroke();
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
// Data Series A (Context - Greyed out)
|
| 1639 |
+
const dataA = [120, 130, 125, 140];
|
| 1640 |
+
const dataB = [90, 100, 110, 205]; // Massive spike
|
| 1641 |
+
const target = 140;
|
| 1642 |
+
|
| 1643 |
+
const quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
| 1644 |
+
const xStep = width / 3;
|
| 1645 |
+
|
| 1646 |
+
// Draw Target Line
|
| 1647 |
+
const targetY = margin.top + height - (target / 250) * height;
|
| 1648 |
+
ctx.beginPath();
|
| 1649 |
+
ctx.moveTo(margin.left, targetY);
|
| 1650 |
+
ctx.lineTo(margin.left + width, targetY);
|
| 1651 |
+
ctx.strokeStyle = COLORS.warning;
|
| 1652 |
+
ctx.setLineDash([5, 5]);
|
| 1653 |
+
ctx.lineWidth = 2;
|
| 1654 |
+
ctx.stroke();
|
| 1655 |
+
ctx.setLineDash([]);
|
| 1656 |
+
ctx.fillStyle = COLORS.warning;
|
| 1657 |
+
ctx.fillText('Q4 Target (140)', margin.left + width + 10, targetY + 5);
|
| 1658 |
+
|
| 1659 |
+
// Draw Line A (Less important)
|
| 1660 |
+
ctx.beginPath();
|
| 1661 |
+
dataA.forEach((v, i) => {
|
| 1662 |
+
const x = margin.left + i * xStep;
|
| 1663 |
+
const y = margin.top + height - (v / 250) * height;
|
| 1664 |
+
if (i === 0) ctx.moveTo(x, y);
|
| 1665 |
+
else ctx.lineTo(x, y);
|
| 1666 |
+
});
|
| 1667 |
+
ctx.strokeStyle = '#cbd5e1'; // light slate
|
| 1668 |
+
ctx.lineWidth = 3;
|
| 1669 |
+
ctx.stroke();
|
| 1670 |
+
ctx.fillStyle = '#94a3b8';
|
| 1671 |
+
ctx.fillText('Product A', margin.left + width + 10, margin.top + height - (dataA[3] / 250) * height + 5);
|
| 1672 |
+
|
| 1673 |
+
// Draw Line B (The Hero)
|
| 1674 |
+
ctx.beginPath();
|
| 1675 |
+
dataB.forEach((v, i) => {
|
| 1676 |
+
const x = margin.left + i * xStep;
|
| 1677 |
+
const y = margin.top + height - (v / 250) * height;
|
| 1678 |
+
if (i === 0) ctx.moveTo(x, y);
|
| 1679 |
+
else ctx.lineTo(x, y);
|
| 1680 |
+
});
|
| 1681 |
+
ctx.strokeStyle = COLORS.primary;
|
| 1682 |
+
ctx.lineWidth = 5;
|
| 1683 |
+
ctx.stroke();
|
| 1684 |
+
|
| 1685 |
+
// Highlight points for B
|
| 1686 |
+
dataB.forEach((v, i) => {
|
| 1687 |
+
const x = margin.left + i * xStep;
|
| 1688 |
+
const y = margin.top + height - (v / 250) * height;
|
| 1689 |
+
ctx.beginPath();
|
| 1690 |
+
ctx.arc(x, y, 6, 0, Math.PI * 2);
|
| 1691 |
+
ctx.fillStyle = i === 3 ? COLORS.success : COLORS.primary; // Make final point stand out more
|
| 1692 |
+
ctx.fill();
|
| 1693 |
+
|
| 1694 |
+
// Data labels directly on line
|
| 1695 |
+
ctx.fillStyle = i === 3 ? COLORS.success : COLORS.dark;
|
| 1696 |
+
ctx.font = i === 3 ? 'bold 16px Inter, sans-serif' : '12px Inter, sans-serif';
|
| 1697 |
+
ctx.textAlign = 'center';
|
| 1698 |
+
ctx.fillText(v, x, y - 15);
|
| 1699 |
+
});
|
| 1700 |
+
|
| 1701 |
+
// Label Hero line
|
| 1702 |
+
ctx.fillStyle = COLORS.primary;
|
| 1703 |
+
ctx.textAlign = 'left';
|
| 1704 |
+
ctx.font = 'bold 14px Inter, sans-serif';
|
| 1705 |
+
ctx.fillText('Product B', margin.left + width + 10, margin.top + height - (dataB[3] / 250) * height + 5);
|
| 1706 |
+
|
| 1707 |
+
// X Axis
|
| 1708 |
+
ctx.strokeStyle = COLORS.dark;
|
| 1709 |
+
ctx.lineWidth = 2;
|
| 1710 |
+
ctx.beginPath();
|
| 1711 |
+
ctx.moveTo(margin.left, margin.top + height);
|
| 1712 |
+
ctx.lineTo(margin.left + width, margin.top + height);
|
| 1713 |
+
ctx.stroke();
|
| 1714 |
+
|
| 1715 |
+
ctx.fillStyle = COLORS.dark;
|
| 1716 |
+
ctx.textAlign = 'center';
|
| 1717 |
+
quarters.forEach((q, i) => {
|
| 1718 |
+
ctx.fillText(q, margin.left + i * xStep, margin.top + height + 20);
|
| 1719 |
+
});
|
| 1720 |
+
|
| 1721 |
+
// Storytelling annotation
|
| 1722 |
+
ctx.beginPath();
|
| 1723 |
+
const finalX = margin.left + 3 * xStep;
|
| 1724 |
+
const finalY = margin.top + height - (dataB[3] / 250) * height;
|
| 1725 |
+
ctx.moveTo(finalX - 60, finalY + 40);
|
| 1726 |
+
ctx.lineTo(finalX - 10, finalY + 10);
|
| 1727 |
+
ctx.strokeStyle = COLORS.success;
|
| 1728 |
+
ctx.lineWidth = 2;
|
| 1729 |
+
ctx.stroke();
|
| 1730 |
+
|
| 1731 |
+
ctx.fillStyle = COLORS.success;
|
| 1732 |
+
ctx.textAlign = 'right';
|
| 1733 |
+
ctx.fillText('+45% over Target!', finalX - 65, finalY + 45);
|
| 1734 |
+
}
|
| 1735 |
+
|
| 1736 |
// ==================== EVENT LISTENERS ====================
|
| 1737 |
function bindEvents() {
|
| 1738 |
// Perception buttons
|
|
|
|
| 1759 |
$('btn-ecdfplot')?.addEventListener('click', () => { distPlotType = 'ecdfplot'; drawDistributions(); });
|
| 1760 |
$('btn-rugplot')?.addEventListener('click', () => { distPlotType = 'rugplot'; drawDistributions(); });
|
| 1761 |
|
| 1762 |
+
// Relationship buttons
|
| 1763 |
+
$('btn-scatter-hue')?.addEventListener('click', () => { relPlotType = 'scatter'; drawRelationships(); });
|
| 1764 |
+
$('btn-regplot')?.addEventListener('click', () => { relPlotType = 'regplot'; drawRelationships(); });
|
| 1765 |
+
$('btn-residplot')?.addEventListener('click', () => { relPlotType = 'residplot'; drawRelationships(); });
|
| 1766 |
+
$('btn-pairplot')?.addEventListener('click', () => { relPlotType = 'pairplot'; drawRelationships(); });
|
| 1767 |
+
|
| 1768 |
// Heatmap buttons
|
| 1769 |
$('btn-heatmap-basic')?.addEventListener('click', () => { heatmapType = 'basic'; drawHeatmaps(); });
|
| 1770 |
$('btn-corr-matrix')?.addEventListener('click', () => { heatmapType = 'corr'; drawHeatmaps(); });
|
|
|
|
| 1774 |
$('btn-animate')?.addEventListener('click', startAnimation);
|
| 1775 |
$('btn-stop')?.addEventListener('click', stopAnimation);
|
| 1776 |
|
| 1777 |
+
// Geospatial
|
| 1778 |
+
$('btn-choropleth')?.addEventListener('click', () => { geoType = 'choropleth'; drawGeo(); });
|
| 1779 |
+
$('btn-scatter-geo')?.addEventListener('click', () => { geoType = 'scatter'; drawGeo(); });
|
| 1780 |
+
$('btn-heatmap-geo')?.addEventListener('click', () => { geoType = 'heatmap'; drawGeo(); });
|
| 1781 |
+
|
| 1782 |
+
// 3D Plots
|
| 1783 |
+
$('btn-3d-scatter')?.addEventListener('click', () => { plot3DType = 'scatter'; draw3D(); });
|
| 1784 |
+
$('btn-3d-surface')?.addEventListener('click', () => { plot3DType = 'surface'; draw3D(); });
|
| 1785 |
+
$('btn-3d-wireframe')?.addEventListener('click', () => { plot3DType = 'wireframe'; draw3D(); });
|
| 1786 |
+
|
| 1787 |
// Smooth scroll for nav links
|
| 1788 |
$$('.nav__link').forEach(link => {
|
| 1789 |
link.addEventListener('click', (e) => {
|
|
|
|
| 1793 |
if (target) {
|
| 1794 |
target.scrollIntoView({ behavior: 'smooth' });
|
| 1795 |
}
|
| 1796 |
+
|
| 1797 |
// Update active state
|
| 1798 |
$$('.nav__link').forEach(l => l.classList.remove('active'));
|
| 1799 |
link.classList.add('active');
|
|
|
|
| 1804 |
// ==================== INITIALIZATION ====================
|
| 1805 |
function init() {
|
| 1806 |
bindEvents();
|
| 1807 |
+
|
| 1808 |
// Draw all initial visualizations
|
| 1809 |
drawAnscombe();
|
| 1810 |
perceptionMode = 'position';
|
|
|
|
| 1817 |
drawBasicPlots();
|
| 1818 |
distPlotType = 'histplot';
|
| 1819 |
drawDistributions();
|
| 1820 |
+
relPlotType = 'scatter';
|
| 1821 |
+
drawRelationships();
|
| 1822 |
heatmapType = 'basic';
|
| 1823 |
drawHeatmaps();
|
| 1824 |
drawAnimation();
|
| 1825 |
+
|
| 1826 |
+
// Draw Advanced Topics initial state
|
| 1827 |
+
geoType = 'choropleth';
|
| 1828 |
+
drawGeo();
|
| 1829 |
+
plot3DType = 'scatter';
|
| 1830 |
+
draw3D();
|
| 1831 |
+
drawStorytelling();
|
| 1832 |
}
|
| 1833 |
|
| 1834 |
// Run on DOM ready
|
Visualization/index.html
CHANGED
|
@@ -6,6 +6,10 @@
|
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Data Visualization Masterclass</title>
|
| 8 |
<link rel="stylesheet" href="style.css" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
</head>
|
| 10 |
|
| 11 |
<body>
|
|
@@ -58,7 +62,8 @@
|
|
| 58 |
<h2>🎯 Why Visualize Data?</h2>
|
| 59 |
<p>Data visualization transforms abstract numbers into visual stories. The human brain processes images 60,000×
|
| 60 |
faster than text. Visualization helps us <strong>explore</strong>, <strong>analyze</strong>, and
|
| 61 |
-
<strong>communicate</strong> data effectively.
|
|
|
|
| 62 |
|
| 63 |
<div class="info-card">
|
| 64 |
<strong>Anscombe's Quartet:</strong> Four datasets with nearly identical statistical properties (mean,
|
|
@@ -126,6 +131,23 @@
|
|
| 126 |
<div class="callout callout--mistake">⚠️ Pie charts use angle (low accuracy). Bar charts are almost always
|
| 127 |
better!</div>
|
| 128 |
<div class="callout callout--tip">✅ Use position for most important data, color for categories.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
</section>
|
| 130 |
|
| 131 |
<!-- ====================== 3. GRAMMAR OF GRAPHICS ================== -->
|
|
@@ -153,6 +175,41 @@
|
|
| 153 |
|
| 154 |
<div class="callout callout--insight">💡 Understanding Grammar of Graphics makes you a better visualizer in ANY
|
| 155 |
library.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
</section>
|
| 157 |
|
| 158 |
<!-- ====================== 4. CHOOSING CHARTS ================== -->
|
|
@@ -195,6 +252,20 @@
|
|
| 195 |
<div class="callout callout--mistake">⚠️ <strong>3D effects on 2D data</strong> - Distorts perception</div>
|
| 196 |
<div class="callout callout--mistake">⚠️ <strong>Truncated Y-axis</strong> - Exaggerates differences</div>
|
| 197 |
<div class="callout callout--mistake">⚠️ <strong>Rainbow color scales</strong> - Not perceptually uniform</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
</section>
|
| 199 |
|
| 200 |
<!-- ====================== 5. MATPLOTLIB ANATOMY ================== -->
|
|
@@ -230,6 +301,53 @@
|
|
| 230 |
</div>
|
| 231 |
|
| 232 |
<div class="callout callout--tip">✅ Always use OO interface for publication-quality plots.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
</section>
|
| 234 |
|
| 235 |
<!-- ====================== 6. BASIC PLOTS ================== -->
|
|
@@ -263,6 +381,54 @@
|
|
| 263 |
<strong>Histogram:</strong><br>
|
| 264 |
<code>ax.hist(data, bins=30, edgecolor='white', density=True)</code>
|
| 265 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
</section>
|
| 267 |
|
| 268 |
<!-- ====================== 7. SUBPLOTS ================== -->
|
|
@@ -289,6 +455,33 @@
|
|
| 289 |
|
| 290 |
<div class="callout callout--tip">✅ Use plt.tight_layout() or fig.set_constrained_layout(True) to prevent
|
| 291 |
overlaps.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
</section>
|
| 293 |
|
| 294 |
<!-- ====================== 8. STYLING ================== -->
|
|
@@ -322,6 +515,53 @@
|
|
| 322 |
<strong>Diverging:</strong> coolwarm, RdBu (for +/- deviations)<br>
|
| 323 |
<strong>Categorical:</strong> tab10, Set2, Paired (discrete groups)
|
| 324 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
</section>
|
| 326 |
|
| 327 |
<!-- ====================== 9. SEABORN INTRO ================== -->
|
|
@@ -382,6 +622,55 @@
|
|
| 382 |
|
| 383 |
<div class="callout callout--insight">💡 ECDF (Empirical Cumulative Distribution Function) avoids binning issues
|
| 384 |
entirely.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
</section>
|
| 386 |
|
| 387 |
<!-- ====================== 11. RELATIONSHIPS ================== -->
|
|
@@ -406,6 +695,56 @@
|
|
| 406 |
<code>sns.regplot(data=df, x='x', y='y', scatter_kws={'alpha':0.5})</code><br>
|
| 407 |
<code>sns.pairplot(df, hue='species', diag_kind='kde')</code>
|
| 408 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
</section>
|
| 410 |
|
| 411 |
<!-- ====================== 12. CATEGORICAL ================== -->
|
|
@@ -432,6 +771,56 @@
|
|
| 432 |
• <strong>Violin:</strong> Full distribution shape + summary<br>
|
| 433 |
• <strong>Bar:</strong> Mean/count with error bars
|
| 434 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
</section>
|
| 436 |
|
| 437 |
<!-- ====================== 13. HEATMAPS ================== -->
|
|
@@ -458,6 +847,60 @@
|
|
| 458 |
</div>
|
| 459 |
|
| 460 |
<div class="callout callout--insight">💡 Clustermap automatically clusters similar rows/columns together.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</section>
|
| 462 |
|
| 463 |
<!-- ====================== 14. PLOTLY ================== -->
|
|
@@ -514,6 +957,31 @@
|
|
| 514 |
|
| 515 |
<div class="callout callout--tip">✅ Hans Rosling's Gapminder is the classic example of animated scatter plots!
|
| 516 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
</section>
|
| 518 |
|
| 519 |
<!-- ====================== 16. DASHBOARDS ================== -->
|
|
@@ -537,6 +1005,43 @@
|
|
| 537 |
</div>
|
| 538 |
|
| 539 |
<div class="callout callout--insight">💡 Streamlit auto-reruns when input changes - no callbacks needed!</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
</section>
|
| 541 |
|
| 542 |
<!-- ====================== 17. GEOSPATIAL ================== -->
|
|
@@ -561,6 +1066,52 @@
|
|
| 561 |
• <strong>Geopandas + Matplotlib:</strong> Static maps with shapefiles<br>
|
| 562 |
• <strong>Kepler.gl:</strong> Large-scale geospatial visualization
|
| 563 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
</section>
|
| 565 |
|
| 566 |
<!-- ====================== 18. 3D PLOTS ================== -->
|
|
@@ -582,6 +1133,51 @@
|
|
| 582 |
</div>
|
| 583 |
<div class="callout callout--tip">✅ Use Plotly for interactive 3D (rotate, zoom) instead of static Matplotlib
|
| 584 |
3D.</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
</section>
|
| 586 |
|
| 587 |
<!-- ====================== 19. STORYTELLING ================== -->
|
|
|
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Data Visualization Masterclass</title>
|
| 8 |
<link rel="stylesheet" href="style.css" />
|
| 9 |
+
|
| 10 |
+
<!-- MathJax for rendering LaTeX formulas -->
|
| 11 |
+
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
| 12 |
+
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
| 13 |
</head>
|
| 14 |
|
| 15 |
<body>
|
|
|
|
| 62 |
<h2>🎯 Why Visualize Data?</h2>
|
| 63 |
<p>Data visualization transforms abstract numbers into visual stories. The human brain processes images 60,000×
|
| 64 |
faster than text. Visualization helps us <strong>explore</strong>, <strong>analyze</strong>, and
|
| 65 |
+
<strong>communicate</strong> data effectively.
|
| 66 |
+
</p>
|
| 67 |
|
| 68 |
<div class="info-card">
|
| 69 |
<strong>Anscombe's Quartet:</strong> Four datasets with nearly identical statistical properties (mean,
|
|
|
|
| 131 |
<div class="callout callout--mistake">⚠️ Pie charts use angle (low accuracy). Bar charts are almost always
|
| 132 |
better!</div>
|
| 133 |
<div class="callout callout--tip">✅ Use position for most important data, color for categories.</div>
|
| 134 |
+
|
| 135 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 136 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: The Weber-Fechner Law</h3>
|
| 137 |
+
<p>Why are humans bad at comparing bubble sizes (area) but great at comparing bar chart heights
|
| 138 |
+
(length/position)? Human perception of physical magnitudes follows a logarithmic scale, not a linear one.
|
| 139 |
+
</p>
|
| 140 |
+
<div
|
| 141 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 142 |
+
$$ \frac{\Delta I}{I} = k \quad \Rightarrow \quad S = c \ln\left(\frac{I}{I_0}\right) $$
|
| 143 |
+
</div>
|
| 144 |
+
<ul style="margin-bottom: 0;">
|
| 145 |
+
<li><strong>$I$</strong>: Initial stimulus intensity (e.g., initial bubble area)</li>
|
| 146 |
+
<li><strong>$\Delta I$</strong>: Just Noticeable Difference (JND) required to perceive a change</li>
|
| 147 |
+
<li><strong>$k$</strong>: Weber's constant. For length/position $k \approx 0.03$ (very sensitive), but for
|
| 148 |
+
area $k \approx 0.10$ to $0.20$ (very insensitive).</li>
|
| 149 |
+
</ul>
|
| 150 |
+
</div>
|
| 151 |
</section>
|
| 152 |
|
| 153 |
<!-- ====================== 3. GRAMMAR OF GRAPHICS ================== -->
|
|
|
|
| 175 |
|
| 176 |
<div class="callout callout--insight">💡 Understanding Grammar of Graphics makes you a better visualizer in ANY
|
| 177 |
library.</div>
|
| 178 |
+
|
| 179 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 180 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Coordinate Transformations</h3>
|
| 181 |
+
<p>When mapping data to visuals, the coordinate system applies a mathematical transformation matrix. For
|
| 182 |
+
example, converting standard Cartesian coordinates $(x, y)$ to Polar coordinates $(r, \theta)$ to render a
|
| 183 |
+
pie chart or Coxcomb plot:</p>
|
| 184 |
+
<div
|
| 185 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; overflow-x: auto; color: #e4e6eb;">
|
| 186 |
+
$$ r = \sqrt{x^2 + y^2} $$
|
| 187 |
+
$$ \theta = \text{atan2}(y, x) $$
|
| 188 |
+
$$ \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} r \cos(\theta) \\ r \sin(\theta) \end{bmatrix} $$
|
| 189 |
+
</div>
|
| 190 |
+
<p style="margin-bottom: 0;">This is why pie charts are computationally and perceptually different from bar
|
| 191 |
+
charts—they apply a non-linear polar transformation to the linear data dimensions.</p>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 195 |
+
<div class="code-header">
|
| 196 |
+
<span>app.py - Grammar of Graphics with Plotnine (Python)</span>
|
| 197 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 198 |
+
</div>
|
| 199 |
+
<pre><code>import pandas as pd
|
| 200 |
+
from plotnine import *
|
| 201 |
+
|
| 202 |
+
# Following Grammar of Graphics exactly:
|
| 203 |
+
# Data (mpg) -> Aesthetics (x,y,color) -> Geometries (point, smooth)
|
| 204 |
+
plot = (
|
| 205 |
+
ggplot(mpg, aes(x='displ', y='hwy', color='class'))
|
| 206 |
+
+ geom_point(size=3, alpha=0.7)
|
| 207 |
+
+ geom_smooth(method='lm', se=False) # Add regression line
|
| 208 |
+
+ theme_minimal() # Add theme
|
| 209 |
+
+ labs(title='Engine Displacement vs Highway MPG')
|
| 210 |
+
)
|
| 211 |
+
print(plot)</code></pre>
|
| 212 |
+
</div>
|
| 213 |
</section>
|
| 214 |
|
| 215 |
<!-- ====================== 4. CHOOSING CHARTS ================== -->
|
|
|
|
| 252 |
<div class="callout callout--mistake">⚠️ <strong>3D effects on 2D data</strong> - Distorts perception</div>
|
| 253 |
<div class="callout callout--mistake">⚠️ <strong>Truncated Y-axis</strong> - Exaggerates differences</div>
|
| 254 |
<div class="callout callout--mistake">⚠️ <strong>Rainbow color scales</strong> - Not perceptually uniform</div>
|
| 255 |
+
|
| 256 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 257 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Information Entropy in Visuals</h3>
|
| 258 |
+
<p>How much data can a chart "handle" before it becomes cluttered? We can use Shannon Entropy ($H$) to
|
| 259 |
+
quantify the visual information density. If a chart has $n$ visual marks (dots, lines) with probabilities
|
| 260 |
+
$p_i$ of drawing attention:</p>
|
| 261 |
+
<div
|
| 262 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; color: #e4e6eb;">
|
| 263 |
+
$$ H(X) = - \sum_{i=1}^{n} p_i \log_2(p_i) $$
|
| 264 |
+
</div>
|
| 265 |
+
<p style="margin-bottom: 0;"><strong>Takeaway:</strong> If you add too many dimensions (color, size, shape
|
| 266 |
+
simultaneously) on a single plot, the entropy $H$ exceeds human working memory limits ($\approx 2.5$ bits),
|
| 267 |
+
leading to chart fatigue. This is mathematically why "less is more" in dashboard design.</p>
|
| 268 |
+
</div>
|
| 269 |
</section>
|
| 270 |
|
| 271 |
<!-- ====================== 5. MATPLOTLIB ANATOMY ================== -->
|
|
|
|
| 301 |
</div>
|
| 302 |
|
| 303 |
<div class="callout callout--tip">✅ Always use OO interface for publication-quality plots.</div>
|
| 304 |
+
|
| 305 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 306 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Affine Transformations</h3>
|
| 307 |
+
<p>How does Matplotlib convert your data coordinates (e.g., $x \in [0, 1000]$) into physical pixels on your
|
| 308 |
+
screen? It uses a continuous pipeline of <strong>Affine Transformation Matrices</strong>:</p>
|
| 309 |
+
<div
|
| 310 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; overflow-x: auto; color: #e4e6eb;">
|
| 311 |
+
$$ \begin{bmatrix} x_{\text{display}} \\ y_{\text{display}} \\ 1 \end{bmatrix} = \begin{bmatrix} s_x & 0 &
|
| 312 |
+
t_x \\ 0 & s_y & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x_{\text{data}} \\ y_{\text{data}} \\ 1
|
| 313 |
+
\end{bmatrix} $$
|
| 314 |
+
</div>
|
| 315 |
+
<p style="margin-bottom: 0;">This matrix $T$ scales ($s_x, s_y$) and translates ($t_x, t_y$) data points. The
|
| 316 |
+
transformation pipeline is: Data $\rightarrow$ Axes (relative 0-1) $\rightarrow$ Figure (inches)
|
| 317 |
+
$\rightarrow$ Display (pixels based on DPI).</p>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 321 |
+
<div class="code-header">
|
| 322 |
+
<span>plot.py - Matplotlib Object-Oriented Setup</span>
|
| 323 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 324 |
+
</div>
|
| 325 |
+
<pre><code>import matplotlib.pyplot as plt
|
| 326 |
+
|
| 327 |
+
# 1. Create the Figure (The Canvas) and Axes (The Artist)
|
| 328 |
+
fig, ax = plt.subplots(figsize=(10, 6), dpi=100)
|
| 329 |
+
|
| 330 |
+
# 2. Draw on the Axes
|
| 331 |
+
ax.plot([1, 2, 3], [4, 5, 2], marker='o', label='Data A')
|
| 332 |
+
|
| 333 |
+
# 3. Configure the Axes (Anatomy elements)
|
| 334 |
+
ax.set_title("My First OOP Plot", fontsize=16, fontweight='bold')
|
| 335 |
+
ax.set_xlabel("X-Axis (Units)", fontsize=12)
|
| 336 |
+
ax.set_ylabel("Y-Axis (Units)", fontsize=12)
|
| 337 |
+
|
| 338 |
+
# Set limits and ticks
|
| 339 |
+
ax.set_xlim(0, 4)
|
| 340 |
+
ax.set_ylim(0, 6)
|
| 341 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
| 342 |
+
|
| 343 |
+
# 4. Add accessories
|
| 344 |
+
ax.legend(loc='upper right')
|
| 345 |
+
|
| 346 |
+
# 5. Render or Save
|
| 347 |
+
plt.tight_layout() # Prevent clipping
|
| 348 |
+
plt.show()
|
| 349 |
+
# fig.savefig('my_plot.png', dpi=300)</code></pre>
|
| 350 |
+
</div>
|
| 351 |
</section>
|
| 352 |
|
| 353 |
<!-- ====================== 6. BASIC PLOTS ================== -->
|
|
|
|
| 381 |
<strong>Histogram:</strong><br>
|
| 382 |
<code>ax.hist(data, bins=30, edgecolor='white', density=True)</code>
|
| 383 |
</div>
|
| 384 |
+
|
| 385 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 386 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: The Freedman-Diaconis Rule</h3>
|
| 387 |
+
<p>When you call a histogram without specifying bins, how does the library choose the optimal bin width?
|
| 388 |
+
Advanced statistical libraries use the Freedman-Diaconis rule, which minimizes the integral of the squared
|
| 389 |
+
difference between the histogram and the true underlying probability density:</p>
|
| 390 |
+
<div
|
| 391 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 392 |
+
$$ \text{Bin Width } (h) = 2 \frac{\text{IQR}(x)}{\sqrt[3]{n}} $$
|
| 393 |
+
</div>
|
| 394 |
+
<p style="margin-bottom: 0;">Where $\text{IQR}$ is the Interquartile Range and $n$ is the number of
|
| 395 |
+
observations. Unlike simpler rules (e.g., Sturges' rule), this mathematical method is extremely robust to
|
| 396 |
+
heavy-tailed distributions and outliers.</p>
|
| 397 |
+
</div>
|
| 398 |
+
|
| 399 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 400 |
+
<div class="code-header">
|
| 401 |
+
<span>basic_plots.py - Common Matplotlib Patterns</span>
|
| 402 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 403 |
+
</div>
|
| 404 |
+
<pre><code>import matplotlib.pyplot as plt
|
| 405 |
+
import numpy as np
|
| 406 |
+
|
| 407 |
+
fig, axs = plt.subplots(1, 2, figsize=(15, 5))
|
| 408 |
+
|
| 409 |
+
# 1. Scatter Plot (Color & Size mapping)
|
| 410 |
+
x = np.random.randn(100)
|
| 411 |
+
y = x + np.random.randn(100)*0.5
|
| 412 |
+
sizes = np.random.uniform(10, 200, 100)
|
| 413 |
+
colors = x
|
| 414 |
+
|
| 415 |
+
sc = axs[0].scatter(x, y, s=sizes, c=colors, cmap='viridis', alpha=0.7)
|
| 416 |
+
axs[0].set_title('Scatter Plot')
|
| 417 |
+
fig.colorbar(sc, ax=axs[0], label='Color Value')
|
| 418 |
+
|
| 419 |
+
# 2. Bar Chart (with Error Bars)
|
| 420 |
+
categories = ['Group A', 'Group B', 'Group C']
|
| 421 |
+
values = [10, 22, 15]
|
| 422 |
+
errors = [1.5, 3.0, 2.0]
|
| 423 |
+
|
| 424 |
+
axs[1].bar(categories, values, yerr=errors, capsize=5, color='coral', alpha=0.8)
|
| 425 |
+
axs[1].set_title('Bar Chart with Error Bars')
|
| 426 |
+
for i, v in enumerate(values):
|
| 427 |
+
axs[1].text(i, v + 0.5, str(v), ha='center')
|
| 428 |
+
|
| 429 |
+
plt.tight_layout()
|
| 430 |
+
plt.show()</code></pre>
|
| 431 |
+
</div>
|
| 432 |
</section>
|
| 433 |
|
| 434 |
<!-- ====================== 7. SUBPLOTS ================== -->
|
|
|
|
| 455 |
|
| 456 |
<div class="callout callout--tip">✅ Use plt.tight_layout() or fig.set_constrained_layout(True) to prevent
|
| 457 |
overlaps.</div>
|
| 458 |
+
|
| 459 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 460 |
+
<div class="code-header">
|
| 461 |
+
<span>subplots.py - Complex Layouts with GridSpec</span>
|
| 462 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 463 |
+
</div>
|
| 464 |
+
<pre><code>import matplotlib.pyplot as plt
|
| 465 |
+
import matplotlib.gridspec as gridspec
|
| 466 |
+
|
| 467 |
+
fig = plt.figure(figsize=(10, 8))
|
| 468 |
+
gs = gridspec.GridSpec(3, 3, figure=fig)
|
| 469 |
+
|
| 470 |
+
# 1. Main large plot (spans 2x2 grid)
|
| 471 |
+
ax_main = fig.add_subplot(gs[0:2, 0:2])
|
| 472 |
+
ax_main.set_title('Main View')
|
| 473 |
+
|
| 474 |
+
# 2. Side plots (Top right, Bottom right)
|
| 475 |
+
ax_side1 = fig.add_subplot(gs[0, 2])
|
| 476 |
+
ax_side2 = fig.add_subplot(gs[1, 2])
|
| 477 |
+
|
| 478 |
+
# 3. Bottom wide plot (spans 1x3 grid)
|
| 479 |
+
ax_bottom = fig.add_subplot(gs[2, :])
|
| 480 |
+
ax_bottom.set_title('Timeline View')
|
| 481 |
+
|
| 482 |
+
plt.tight_layout()
|
| 483 |
+
plt.show()</code></pre>
|
| 484 |
+
</div>
|
| 485 |
</section>
|
| 486 |
|
| 487 |
<!-- ====================== 8. STYLING ================== -->
|
|
|
|
| 515 |
<strong>Diverging:</strong> coolwarm, RdBu (for +/- deviations)<br>
|
| 516 |
<strong>Categorical:</strong> tab10, Set2, Paired (discrete groups)
|
| 517 |
</div>
|
| 518 |
+
|
| 519 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 520 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Perceptually Uniform Colors (CIELAB)</h3>
|
| 521 |
+
<p>Why do we use "viridis" instead of "rainbow" colormaps? A color map is a mathematical function mapping data
|
| 522 |
+
$f(x) \rightarrow (R, G, B)$. However, standard RGB math doesn't match human perception (Euclidean distance
|
| 523 |
+
in RGB $\neq$ perceived color distance).</p>
|
| 524 |
+
<div
|
| 525 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 526 |
+
$$ \Delta E^* = \sqrt{(\Delta L^*)^2 + (\Delta a^*)^2 + (\Delta b^*)^2} $$
|
| 527 |
+
</div>
|
| 528 |
+
<p style="margin-bottom: 0;">Advanced colormaps like <em>viridis</em> are calculated in the <strong>CIELAB
|
| 529 |
+
($L^*a^*b^*$) color space</strong>. In this space, the mathematical distance formula $\Delta E^*$
|
| 530 |
+
perfectly matches how the retina and brain perceive brightness and hue differences, ensuring data is never
|
| 531 |
+
visually distorted.</p>
|
| 532 |
+
</div>
|
| 533 |
+
|
| 534 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 535 |
+
<div class="code-header">
|
| 536 |
+
<span>styling.py - Applying Professional Aesthetics</span>
|
| 537 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 538 |
+
</div>
|
| 539 |
+
<pre><code>import matplotlib.pyplot as plt
|
| 540 |
+
import seaborn as sns
|
| 541 |
+
|
| 542 |
+
# 1. Apply a global Seaborn theme
|
| 543 |
+
sns.set_theme(style="whitegrid", palette="muted")
|
| 544 |
+
|
| 545 |
+
# 2. Customize fonts globally
|
| 546 |
+
plt.rcParams.update({
|
| 547 |
+
'font.family': 'sans-serif',
|
| 548 |
+
'font.sans-serif': ['Helvetica', 'Arial'],
|
| 549 |
+
'axes.titleweight': 'bold',
|
| 550 |
+
'axes.titlesize': 16,
|
| 551 |
+
'axes.labelsize': 12,
|
| 552 |
+
'lines.linewidth': 2
|
| 553 |
+
})
|
| 554 |
+
|
| 555 |
+
# 3. Plotting with the new theme
|
| 556 |
+
fig, ax = plt.subplots(figsize=(8, 5))
|
| 557 |
+
ax.plot([1, 2, 3], [4, 5, 2], label='Data')
|
| 558 |
+
ax.legend()
|
| 559 |
+
|
| 560 |
+
# 4. Remove top and right spines (cleaner look)
|
| 561 |
+
sns.despine(ax=ax)
|
| 562 |
+
|
| 563 |
+
plt.show()</code></pre>
|
| 564 |
+
</div>
|
| 565 |
</section>
|
| 566 |
|
| 567 |
<!-- ====================== 9. SEABORN INTRO ================== -->
|
|
|
|
| 622 |
|
| 623 |
<div class="callout callout--insight">💡 ECDF (Empirical Cumulative Distribution Function) avoids binning issues
|
| 624 |
entirely.</div>
|
| 625 |
+
|
| 626 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 627 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Kernel Density Estimation (KDE)</h3>
|
| 628 |
+
<p>A KDE plot is not just a smoothed line; it's a mathematical sum of continuous probability distributions
|
| 629 |
+
(kernels) placed at every single data point $x_i$:</p>
|
| 630 |
+
<div
|
| 631 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 632 |
+
$$ \hat{f}_h(x) = \frac{1}{n h} \sum_{i=1}^{n} K\left(\frac{x - x_i}{h}\right) $$
|
| 633 |
+
</div>
|
| 634 |
+
<p style="margin-bottom: 0;">Here, $K$ is typically the Standard Normal Gaussian density function, and $h$ is
|
| 635 |
+
the bandwidth parameter. If $h$ is too small, the curve is jagged (overfit); if $h$ is too large, it hides
|
| 636 |
+
important statistical features (underfit).</p>
|
| 637 |
+
</div>
|
| 638 |
+
|
| 639 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 640 |
+
<div class="code-header">
|
| 641 |
+
<span>distributions.py - Visualizing Distributions</span>
|
| 642 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 643 |
+
</div>
|
| 644 |
+
<pre><code>import seaborn as sns
|
| 645 |
+
import matplotlib.pyplot as plt
|
| 646 |
+
|
| 647 |
+
penguins = sns.load_dataset("penguins")
|
| 648 |
+
|
| 649 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
| 650 |
+
|
| 651 |
+
# 1. Histogram + KDE overlay
|
| 652 |
+
sns.histplot(
|
| 653 |
+
data=penguins, x="flipper_length_mm", hue="species",
|
| 654 |
+
element="step", stat="density", common_norm=False,
|
| 655 |
+
ax=axes[0]
|
| 656 |
+
)
|
| 657 |
+
axes[0].set_title("Histogram with Step Fill")
|
| 658 |
+
|
| 659 |
+
# 2. KDE Plot with Rug Plot
|
| 660 |
+
sns.kdeplot(
|
| 661 |
+
data=penguins, x="body_mass_g", hue="species",
|
| 662 |
+
fill=True, common_norm=False, palette="crest",
|
| 663 |
+
alpha=0.5, linewidth=1.5, ax=axes[1]
|
| 664 |
+
)
|
| 665 |
+
sns.rugplot(
|
| 666 |
+
data=penguins, x="body_mass_g", hue="species",
|
| 667 |
+
height=0.05, ax=axes[1]
|
| 668 |
+
)
|
| 669 |
+
axes[1].set_title("KDE Density + Rug Plot")
|
| 670 |
+
|
| 671 |
+
sns.despine()
|
| 672 |
+
plt.show()</code></pre>
|
| 673 |
+
</div>
|
| 674 |
</section>
|
| 675 |
|
| 676 |
<!-- ====================== 11. RELATIONSHIPS ================== -->
|
|
|
|
| 695 |
<code>sns.regplot(data=df, x='x', y='y', scatter_kws={'alpha':0.5})</code><br>
|
| 696 |
<code>sns.pairplot(df, hue='species', diag_kind='kde')</code>
|
| 697 |
</div>
|
| 698 |
+
|
| 699 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 700 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Ordinary Least Squares (OLS)</h3>
|
| 701 |
+
<p>When you use <code>sns.regplot</code>, Seaborn calculates the line of best fit by minimizing the sum of the
|
| 702 |
+
squared residuals ($e_i^2$). The exact matrix algebra closed-form solution for the coefficients
|
| 703 |
+
$\hat{\boldsymbol{\beta}}$ is:</p>
|
| 704 |
+
<div
|
| 705 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 706 |
+
$$ \hat{\boldsymbol{\beta}} = (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{y} $$
|
| 707 |
+
</div>
|
| 708 |
+
<p style="margin-bottom: 0;">The shaded region around the line represents the 95% confidence interval, meaning
|
| 709 |
+
if we resampled the data 100 times, the true regression line would fall inside this shaded band 95 times
|
| 710 |
+
(usually computed via bootstrapping).</p>
|
| 711 |
+
</div>
|
| 712 |
+
|
| 713 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 714 |
+
<div class="code-header">
|
| 715 |
+
<span>relationships.py - Scatter and Regression</span>
|
| 716 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 717 |
+
</div>
|
| 718 |
+
<pre><code>import seaborn as sns
|
| 719 |
+
import matplotlib.pyplot as plt
|
| 720 |
+
|
| 721 |
+
tips = sns.load_dataset("tips")
|
| 722 |
+
|
| 723 |
+
# 1. Advanced Scatter (4 dimensions: x, y, color, size)
|
| 724 |
+
plt.figure(figsize=(8, 6))
|
| 725 |
+
sns.scatterplot(
|
| 726 |
+
data=tips, x="total_bill", y="tip",
|
| 727 |
+
hue="time", size="size", sizes=(20, 200),
|
| 728 |
+
palette="deep", alpha=0.8
|
| 729 |
+
)
|
| 730 |
+
plt.title("4D Scatter Plot (Total Bill vs Tip)")
|
| 731 |
+
plt.show()
|
| 732 |
+
|
| 733 |
+
# 2. Regression Plot with Subplots (Using lmplot)
|
| 734 |
+
# lmplot is a figure-level function that creates multiple subplots automatically
|
| 735 |
+
sns.lmplot(
|
| 736 |
+
data=tips, x="total_bill", y="tip", col="time", hue="smoker",
|
| 737 |
+
height=5, aspect=1.2, scatter_kws={'alpha':0.5}
|
| 738 |
+
)
|
| 739 |
+
plt.show()
|
| 740 |
+
|
| 741 |
+
# 3. Pairplot (Explore all pairwise relationships)
|
| 742 |
+
sns.pairplot(
|
| 743 |
+
data=tips, hue="smoker",
|
| 744 |
+
diag_kind="kde", markers=["o", "s"]
|
| 745 |
+
)
|
| 746 |
+
plt.show()</code></pre>
|
| 747 |
+
</div>
|
| 748 |
</section>
|
| 749 |
|
| 750 |
<!-- ====================== 12. CATEGORICAL ================== -->
|
|
|
|
| 771 |
• <strong>Violin:</strong> Full distribution shape + summary<br>
|
| 772 |
• <strong>Bar:</strong> Mean/count with error bars
|
| 773 |
</div>
|
| 774 |
+
|
| 775 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 776 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: The IQR Outlier Rule</h3>
|
| 777 |
+
<p>Box plots identify "outliers" (the individual dots beyond the whiskers) purely mathematically, not
|
| 778 |
+
visually. They use John Tukey's Interquartile Range (IQR) method:</p>
|
| 779 |
+
<div
|
| 780 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; color: #e4e6eb;">
|
| 781 |
+
$$ \text{IQR} = Q_3 - Q_1 $$
|
| 782 |
+
$$ \text{Lower Fence} = Q_1 - 1.5 \times \text{IQR} $$
|
| 783 |
+
$$ \text{Upper Fence} = Q_3 + 1.5 \times \text{IQR} $$
|
| 784 |
+
</div>
|
| 785 |
+
<p style="margin-bottom: 0;">Any point strictly outside $[Lower, Upper]$ is plotted as an outlier. <strong>Fun
|
| 786 |
+
Fact:</strong> In a perfectly normal Gaussian distribution $\mathcal{N}(\mu, \sigma^2)$, exactly 0.70% of
|
| 787 |
+
the data will be incorrectly flagged as outliers by this static math rule!</p>
|
| 788 |
+
</div>
|
| 789 |
+
|
| 790 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 791 |
+
<div class="code-header">
|
| 792 |
+
<span>categorical.py - Categories and Factor Variables</span>
|
| 793 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 794 |
+
</div>
|
| 795 |
+
<pre><code>import seaborn as sns
|
| 796 |
+
import matplotlib.pyplot as plt
|
| 797 |
+
|
| 798 |
+
tips = sns.load_dataset("tips")
|
| 799 |
+
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
| 800 |
+
|
| 801 |
+
# 1. Violin Plot (Distribution density across categories)
|
| 802 |
+
sns.violinplot(
|
| 803 |
+
data=tips, x="day", y="total_bill", hue="sex",
|
| 804 |
+
split=True, inner="quart", palette="muted",
|
| 805 |
+
ax=axes[0]
|
| 806 |
+
)
|
| 807 |
+
axes[0].set_title("Violin Plot (Split by Sex)")
|
| 808 |
+
|
| 809 |
+
# 2. Boxplot + Swarmplot Overlay
|
| 810 |
+
# Good for showing summary stats PLUS underlying data points
|
| 811 |
+
sns.boxplot(
|
| 812 |
+
data=tips, x="day", y="total_bill", color="white",
|
| 813 |
+
width=.5, showfliers=False, ax=axes[1] # hide boxplot outliers to avoid overlap
|
| 814 |
+
)
|
| 815 |
+
sns.swarmplot(
|
| 816 |
+
data=tips, x="day", y="total_bill", hue="time",
|
| 817 |
+
size=6, alpha=0.7, ax=axes[1]
|
| 818 |
+
)
|
| 819 |
+
axes[1].set_title("Boxplot + Swarmplot Overlay")
|
| 820 |
+
|
| 821 |
+
plt.tight_layout()
|
| 822 |
+
plt.show()</code></pre>
|
| 823 |
+
</div>
|
| 824 |
</section>
|
| 825 |
|
| 826 |
<!-- ====================== 13. HEATMAPS ================== -->
|
|
|
|
| 847 |
</div>
|
| 848 |
|
| 849 |
<div class="callout callout--insight">💡 Clustermap automatically clusters similar rows/columns together.</div>
|
| 850 |
+
|
| 851 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 852 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Correlation Coefficients</h3>
|
| 853 |
+
<p>Correlation heatmaps display the strength of linear relationships between variables, typically mapping the
|
| 854 |
+
<strong>Pearson Correlation Coefficient ($r$)</strong> to a discrete color gradient hexbin:
|
| 855 |
+
</p>
|
| 856 |
+
<div
|
| 857 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 858 |
+
$$ r = \frac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum (x_i - \bar{x})^2 \sum (y_i - \bar{y})^2}} $$
|
| 859 |
+
</div>
|
| 860 |
+
<p style="margin-bottom: 0;">For non-linear but monotonic relationships, you should switch pandas to use
|
| 861 |
+
<strong>Spearman's Rank Correlation ($\rho$)</strong>, which mathematically converts raw values to ranks
|
| 862 |
+
$R(x_i)$ before applying the same formula. Both map perfectly bounds of $[-1, 1]$.
|
| 863 |
+
</p>
|
| 864 |
+
</div>
|
| 865 |
+
|
| 866 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 867 |
+
<div class="code-header">
|
| 868 |
+
<span>heatmaps.py - Correlation Matrix</span>
|
| 869 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 870 |
+
</div>
|
| 871 |
+
<pre><code>import seaborn as sns
|
| 872 |
+
import matplotlib.pyplot as plt
|
| 873 |
+
import numpy as np
|
| 874 |
+
|
| 875 |
+
# Load data and calculate correlation matrix
|
| 876 |
+
penguins = sns.load_dataset("penguins")
|
| 877 |
+
# Select only numerical columns for correlation
|
| 878 |
+
numerical_df = penguins.select_dtypes(include=[np.number])
|
| 879 |
+
corr = numerical_df.corr()
|
| 880 |
+
|
| 881 |
+
# Create a mask for the upper triangle
|
| 882 |
+
mask = np.triu(np.ones_like(corr, dtype=bool))
|
| 883 |
+
|
| 884 |
+
plt.figure(figsize=(8, 6))
|
| 885 |
+
|
| 886 |
+
# Draw the heatmap with the mask and correct aspect ratio
|
| 887 |
+
sns.heatmap(
|
| 888 |
+
corr,
|
| 889 |
+
mask=mask,
|
| 890 |
+
cmap='coolwarm',
|
| 891 |
+
vmax=1, vmin=-1,
|
| 892 |
+
center=0,
|
| 893 |
+
square=True,
|
| 894 |
+
linewidths=.5,
|
| 895 |
+
annot=True,
|
| 896 |
+
fmt=".2f",
|
| 897 |
+
cbar_kws={"shrink": .8}
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
plt.title("Penguin Feature Correlation")
|
| 901 |
+
plt.tight_layout()
|
| 902 |
+
plt.show()</code></pre>
|
| 903 |
+
</div>
|
| 904 |
</section>
|
| 905 |
|
| 906 |
<!-- ====================== 14. PLOTLY ================== -->
|
|
|
|
| 957 |
|
| 958 |
<div class="callout callout--tip">✅ Hans Rosling's Gapminder is the classic example of animated scatter plots!
|
| 959 |
</div>
|
| 960 |
+
|
| 961 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 962 |
+
<div class="code-header">
|
| 963 |
+
<span>animation_example.py - Gapminder Scatter</span>
|
| 964 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 965 |
+
</div>
|
| 966 |
+
<pre><code>import plotly.express as px
|
| 967 |
+
|
| 968 |
+
df = px.data.gapminder()
|
| 969 |
+
|
| 970 |
+
# Plotly makes animations incredibly easy with two arguments:
|
| 971 |
+
# 'animation_frame' (the time dimension) and 'animation_group' (the entity)
|
| 972 |
+
fig = px.scatter(
|
| 973 |
+
df,
|
| 974 |
+
x="gdpPercap", y="lifeExp",
|
| 975 |
+
animation_frame="year", animation_group="country",
|
| 976 |
+
size="pop", color="continent",
|
| 977 |
+
hover_name="country",
|
| 978 |
+
log_x=True, size_max=55,
|
| 979 |
+
range_x=[100,100000], range_y=[25,90],
|
| 980 |
+
title="Global Development 1952 - 2007"
|
| 981 |
+
)
|
| 982 |
+
|
| 983 |
+
fig.show()</code></pre>
|
| 984 |
+
</div>
|
| 985 |
</section>
|
| 986 |
|
| 987 |
<!-- ====================== 16. DASHBOARDS ================== -->
|
|
|
|
| 1005 |
</div>
|
| 1006 |
|
| 1007 |
<div class="callout callout--insight">💡 Streamlit auto-reruns when input changes - no callbacks needed!</div>
|
| 1008 |
+
|
| 1009 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 1010 |
+
<div class="code-header">
|
| 1011 |
+
<span>app.py - Minimal Streamlit Dashboard</span>
|
| 1012 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 1013 |
+
</div>
|
| 1014 |
+
<pre><code>import streamlit as st
|
| 1015 |
+
import pandas as pd
|
| 1016 |
+
import plotly.express as px
|
| 1017 |
+
|
| 1018 |
+
# 1. Page Configuration
|
| 1019 |
+
st.set_page_config(page_title="Sales Dashboard", layout="wide")
|
| 1020 |
+
st.title("Interactive Sales Dashboard 📊")
|
| 1021 |
+
|
| 1022 |
+
# 2. Sidebar Filters
|
| 1023 |
+
st.sidebar.header("Filters")
|
| 1024 |
+
category = st.sidebar.selectbox("Select Category", ["Electronics", "Clothing", "Home"])
|
| 1025 |
+
min_sales = st.sidebar.slider("Minimum Sales", 0, 1000, 200)
|
| 1026 |
+
|
| 1027 |
+
# Mock Data Generation
|
| 1028 |
+
df = pd.DataFrame({
|
| 1029 |
+
'Date': pd.date_range(start='2023-01-01', periods=30),
|
| 1030 |
+
'Sales': [x * 10 for x in range(30)],
|
| 1031 |
+
'Category': [category] * 30
|
| 1032 |
+
})
|
| 1033 |
+
filtered_df = df[df['Sales'] >= min_sales]
|
| 1034 |
+
|
| 1035 |
+
# 3. Layout with Columns
|
| 1036 |
+
col1, col2 = st.columns(2)
|
| 1037 |
+
|
| 1038 |
+
# KPI Metric
|
| 1039 |
+
col1.metric("Total Filtered Sales", f"${filtered_df['Sales'].sum()}")
|
| 1040 |
+
|
| 1041 |
+
# 4. Insert Plotly Chart
|
| 1042 |
+
fig = px.line(filtered_df, x='Date', y='Sales', title=f"{category} Sales Trend")
|
| 1043 |
+
col2.plotly_chart(fig, use_container_width=True)</code></pre>
|
| 1044 |
+
</div>
|
| 1045 |
</section>
|
| 1046 |
|
| 1047 |
<!-- ====================== 17. GEOSPATIAL ================== -->
|
|
|
|
| 1066 |
• <strong>Geopandas + Matplotlib:</strong> Static maps with shapefiles<br>
|
| 1067 |
• <strong>Kepler.gl:</strong> Large-scale geospatial visualization
|
| 1068 |
</div>
|
| 1069 |
+
|
| 1070 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 1071 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: Geospatial Math</h3>
|
| 1072 |
+
<p>Visualizing data on a map requires mathematically converting a 3D spherical Earth into 2D screen pixels.
|
| 1073 |
+
The <strong>Web Mercator Projection</strong> (used by Google Maps and Plotly) achieves this by preserving
|
| 1074 |
+
angles (conformal) but heavily distorting sizes near the poles:</p>
|
| 1075 |
+
<div
|
| 1076 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; font-size: 1.1em; color: #e4e6eb;">
|
| 1077 |
+
$$ x = R \cdot \lambda \qquad y = R \ln\left[\tan\left(\frac{\pi}{4} + \frac{\varphi}{2}\right)\right] $$
|
| 1078 |
+
</div>
|
| 1079 |
+
<p style="margin-bottom: 0;">Furthermore, when calculating distances between two GPS coordinates (e.g., to
|
| 1080 |
+
color a density heatmap), you cannot use straight Euclidean distance $d = \sqrt{x^2+y^2}$. Advanced
|
| 1081 |
+
libraries compute the <strong>Haversine formula</strong> to find the true great-circle distance over the
|
| 1082 |
+
sphere.</p>
|
| 1083 |
+
</div>
|
| 1084 |
+
|
| 1085 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 1086 |
+
<div class="code-header">
|
| 1087 |
+
<span>geospatial.py - Plotly Choropleth</span>
|
| 1088 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 1089 |
+
</div>
|
| 1090 |
+
<pre><code>import plotly.express as px
|
| 1091 |
+
|
| 1092 |
+
# Plotly includes built-in geospatial data
|
| 1093 |
+
df = px.data.gapminder().query("year==2007")
|
| 1094 |
+
|
| 1095 |
+
# Create a choropleth map
|
| 1096 |
+
# 'locations' takes ISO-3 country codes by default
|
| 1097 |
+
fig = px.choropleth(
|
| 1098 |
+
df,
|
| 1099 |
+
locations="iso_alpha", # Geopolitical boundaries
|
| 1100 |
+
color="lifeExp", # Data to map to color
|
| 1101 |
+
hover_name="country", # Tooltip label
|
| 1102 |
+
color_continuous_scale=px.colors.sequential.Plasma,
|
| 1103 |
+
title="Global Life Expectancy (2007)"
|
| 1104 |
+
)
|
| 1105 |
+
|
| 1106 |
+
# Customize the map projection type
|
| 1107 |
+
fig.update_geos(
|
| 1108 |
+
projection_type="orthographic", # "natural earth", "mercator", etc.
|
| 1109 |
+
showcoastlines=True,
|
| 1110 |
+
coastlinecolor="DarkBlue"
|
| 1111 |
+
)
|
| 1112 |
+
|
| 1113 |
+
fig.show()</code></pre>
|
| 1114 |
+
</div>
|
| 1115 |
</section>
|
| 1116 |
|
| 1117 |
<!-- ====================== 18. 3D PLOTS ================== -->
|
|
|
|
| 1133 |
</div>
|
| 1134 |
<div class="callout callout--tip">✅ Use Plotly for interactive 3D (rotate, zoom) instead of static Matplotlib
|
| 1135 |
3D.</div>
|
| 1136 |
+
|
| 1137 |
+
<div class="info-card" style="margin-top: 20px; border-left-color: #9900ff;">
|
| 1138 |
+
<h3 style="margin-top: 0; color: #9900ff;">🧠 Under the Hood: 3D Perspective Projection Matrix</h3>
|
| 1139 |
+
<p>To render 3D data $(x, y, z)$ on a 2D screen browser, libraries like Plotly.js apply a <strong>Perspective
|
| 1140 |
+
Projection Matrix</strong>. This creates the optical illusion of depth by scaling $x$ and $y$ inversely
|
| 1141 |
+
with distance $z$:</p>
|
| 1142 |
+
<div
|
| 1143 |
+
style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; text-align: center; margin: 15px 0; overflow-x: auto; color: #e4e6eb;">
|
| 1144 |
+
$$ \begin{bmatrix} x' \\ y' \\ z' \\ w \end{bmatrix} = \begin{bmatrix} \frac{1}{\text{aspect} \cdot
|
| 1145 |
+
\tan(\frac{fov}{2})} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(\frac{fov}{2})} & 0 & 0 \\ 0 & 0 & \frac{f+n}{f-n} &
|
| 1146 |
+
\frac{-2fn}{f-n} \\ 0 & 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} $$
|
| 1147 |
+
</div>
|
| 1148 |
+
<p style="margin-bottom: 0;">Once multiplied out, the final screen coordinates are $(x'/w, y'/w)$. When you
|
| 1149 |
+
rapidly drag to rotate a 3D Plotly graph, your browser's WebGL engine is recalculating this exact matrix
|
| 1150 |
+
millions of times per second to update the viewpoint mapping in real-time!</p>
|
| 1151 |
+
</div>
|
| 1152 |
+
|
| 1153 |
+
<div class="code-block" style="margin-top: 20px;">
|
| 1154 |
+
<div class="code-header">
|
| 1155 |
+
<span>3d_plots.py - Interactive 3D Scatter</span>
|
| 1156 |
+
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
| 1157 |
+
</div>
|
| 1158 |
+
<pre><code>import plotly.express as px
|
| 1159 |
+
|
| 1160 |
+
# Load Iris dataset
|
| 1161 |
+
df = px.data.iris()
|
| 1162 |
+
|
| 1163 |
+
# Create interactive 3D scatter plot
|
| 1164 |
+
fig = px.scatter_3d(
|
| 1165 |
+
df,
|
| 1166 |
+
x='sepal_length',
|
| 1167 |
+
y='sepal_width',
|
| 1168 |
+
z='petal_width',
|
| 1169 |
+
color='species',
|
| 1170 |
+
size='petal_length',
|
| 1171 |
+
size_max=18,
|
| 1172 |
+
symbol='species',
|
| 1173 |
+
opacity=0.7,
|
| 1174 |
+
title="Iris 3D Feature Space"
|
| 1175 |
+
)
|
| 1176 |
+
|
| 1177 |
+
# Tight layout for 3D plot
|
| 1178 |
+
fig.update_layout(margin=dict(l=0, r=0, b=0, t=40))
|
| 1179 |
+
fig.show()</code></pre>
|
| 1180 |
+
</div>
|
| 1181 |
</section>
|
| 1182 |
|
| 1183 |
<!-- ====================== 19. STORYTELLING ================== -->
|