AashishAIHub commited on
Commit
939a3b2
·
1 Parent(s): 8876fbf

Enhance Visualization module with Python code snippets and fix broken interactive canvases

Browse files
Files changed (2) hide show
  1. Visualization/app.js +603 -31
  2. 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.</p>
 
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 ================== -->