abril4416 commited on
Commit
73376e0
·
1 Parent(s): b2061c4

Fix Plotly 3D resize jitter loops across regression visualizations

Browse files
linear-regression/gradient-descent.html CHANGED
@@ -90,7 +90,7 @@
90
  <div id="landscapePlot" class="landscape-plot"></div>
91
  </div>
92
 
93
- <p class="slice-label">Drag to rotate · Scroll to zoom · Resize from the corner</p>
94
  </section>
95
 
96
  <section class="panel output">
@@ -122,6 +122,6 @@
122
  </main>
123
 
124
  <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
125
- <script src="gradient.js?v=20260813a"></script>
126
  </body>
127
  </html>
 
90
  <div id="landscapePlot" class="landscape-plot"></div>
91
  </div>
92
 
93
+ <p class="slice-label">Drag to rotate · Scroll to zoom · Use Fit / +/- / Fullscreen to resize</p>
94
  </section>
95
 
96
  <section class="panel output">
 
122
  </main>
123
 
124
  <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
125
+ <script src="gradient.js?v=20260814b"></script>
126
  </body>
127
  </html>
linear-regression/gradient.js CHANGED
@@ -37,6 +37,9 @@ const DEFAULT_CAMERA = {
37
  up: { x: 0, y: 0, z: 1 },
38
  };
39
 
 
 
 
40
  const state = {
41
  data: [],
42
  steps: [],
@@ -46,8 +49,35 @@ const state = {
46
  landscapeCamera: null,
47
  landscapePlotReady: false,
48
  landscapeResizeObserver: null,
 
 
49
  };
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  function init() {
52
  resizeCanvases();
53
  fitLandscapeSize();
@@ -92,21 +122,36 @@ function initLandscapeInteractions() {
92
 
93
  document.addEventListener("fullscreenchange", () => {
94
  plotFullscreenBtn.textContent = document.fullscreenElement === landscapePlotWrap ? "Exit Fullscreen" : "Fullscreen";
95
- resizeLandscapePlot();
 
 
 
 
 
 
 
96
  });
97
 
98
  if (window.ResizeObserver) {
99
- state.landscapeResizeObserver = new ResizeObserver(() => {
100
- resizeLandscapePlot();
 
 
 
 
 
101
  });
102
  state.landscapeResizeObserver.observe(landscapePlotWrap);
103
  }
104
  }
105
 
106
  function handleResize() {
 
107
  resizeCanvases();
108
 
109
- if (document.fullscreenElement !== landscapePlotWrap) {
 
 
110
  constrainLandscapeSizeToContainer();
111
  }
112
 
@@ -122,7 +167,7 @@ function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
122
  const parent = canvas.parentElement;
123
  if (!parent) return;
124
 
125
- const width = Math.max(320, Math.floor(parent.clientWidth - 2));
126
  const targetHeight = Math.floor(width * ratio);
127
  const height = Math.max(minHeight, Math.min(maxHeight, targetHeight));
128
 
@@ -134,7 +179,7 @@ function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
134
 
135
  function getLandscapeFitSize() {
136
  const host = landscapePlotWrap.parentElement;
137
- const hostWidth = host ? host.clientWidth : window.innerWidth;
138
  const maxW = Math.max(320, Math.min(1000, hostWidth - 4));
139
 
140
  if (window.innerWidth < 680) {
@@ -155,7 +200,7 @@ function fitLandscapeSize(forceResize = false) {
155
 
156
  function setLandscapeSize(rawW, rawH, forceResize = false) {
157
  const host = landscapePlotWrap.parentElement;
158
- const hostMaxW = host ? host.clientWidth - 4 : rawW;
159
  const isFullscreen = document.fullscreenElement === landscapePlotWrap;
160
 
161
  const minW = 320;
@@ -165,25 +210,54 @@ function setLandscapeSize(rawW, rawH, forceResize = false) {
165
 
166
  const width = Math.round(Math.max(minW, Math.min(maxW, rawW)));
167
  const height = Math.round(Math.max(minH, Math.min(maxH, rawH)));
 
 
 
168
 
169
- if (forceResize || landscapePlotWrap.clientWidth !== width || landscapePlotWrap.clientHeight !== height) {
170
  landscapePlotWrap.style.width = `${width}px`;
171
  landscapePlotWrap.style.height = `${height}px`;
172
- resizeLandscapePlot();
 
 
 
 
173
  }
174
  }
175
 
176
  function constrainLandscapeSizeToContainer() {
177
- setLandscapeSize(landscapePlotWrap.clientWidth, landscapePlotWrap.clientHeight, true);
 
178
  }
179
 
180
  function scaleLandscapeSize(factor) {
181
- setLandscapeSize(landscapePlotWrap.clientWidth * factor, landscapePlotWrap.clientHeight * factor, true);
 
182
  }
183
 
184
- function resizeLandscapePlot() {
185
  if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
186
- requestAnimationFrame(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  Plotly.Plots.resize(landscapePlot);
188
  });
189
  }
@@ -657,7 +731,7 @@ function renderLandscapePlotly(data) {
657
  };
658
 
659
  const config = {
660
- responsive: true,
661
  displaylogo: false,
662
  scrollZoom: true,
663
  doubleClick: "reset",
@@ -671,14 +745,17 @@ function renderLandscapePlotly(data) {
671
  plotPromise.then(() => {
672
  if (!state.landscapePlotReady) {
673
  state.landscapePlotReady = true;
 
674
  landscapePlot.on("plotly_relayout", (ev) => {
675
  if (ev && ev["scene.camera"]) {
676
  state.landscapeCamera = cloneCamera(ev["scene.camera"]);
677
  }
 
 
 
678
  });
 
679
  }
680
-
681
- resizeLandscapePlot();
682
  });
683
  }
684
 
 
37
  up: { x: 0, y: 0, z: 1 },
38
  };
39
 
40
+ const RESIZE_EPSILON = 1;
41
+ const DEBUG_RESIZE = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("debugResize");
42
+
43
  const state = {
44
  data: [],
45
  steps: [],
 
49
  landscapeCamera: null,
50
  landscapePlotReady: false,
51
  landscapeResizeObserver: null,
52
+ landscapeResizeRaf: null,
53
+ lastLandscapeSize: null,
54
  };
55
 
56
+ function debugResizeLog(source, extra = {}) {
57
+ if (!DEBUG_RESIZE) return;
58
+
59
+ const wrapRect = landscapePlotWrap.getBoundingClientRect();
60
+ const parent = landscapePlotWrap.parentElement;
61
+ const parentRect = parent ? parent.getBoundingClientRect() : null;
62
+ const docEl = document.documentElement;
63
+
64
+ const payload = {
65
+ source,
66
+ wrapClient: `${landscapePlotWrap.clientWidth}x${landscapePlotWrap.clientHeight}`,
67
+ wrapRect: `${wrapRect.width.toFixed(2)}x${wrapRect.height.toFixed(2)}`,
68
+ parentRect: parentRect ? `${parentRect.width.toFixed(2)}x${parentRect.height.toFixed(2)}` : "n/a",
69
+ win: `${window.innerWidth}x${window.innerHeight}`,
70
+ viewport: `${docEl.clientWidth}x${docEl.clientHeight}`,
71
+ scroll: `${docEl.scrollWidth}x${docEl.scrollHeight}`,
72
+ bodyScrollY: window.scrollY,
73
+ hasVScrollbar: docEl.scrollHeight > docEl.clientHeight,
74
+ hasHScrollbar: docEl.scrollWidth > docEl.clientWidth,
75
+ ...extra,
76
+ };
77
+
78
+ console.debug("[linear-resize]", payload);
79
+ }
80
+
81
  function init() {
82
  resizeCanvases();
83
  fitLandscapeSize();
 
122
 
123
  document.addEventListener("fullscreenchange", () => {
124
  plotFullscreenBtn.textContent = document.fullscreenElement === landscapePlotWrap ? "Exit Fullscreen" : "Fullscreen";
125
+ debugResizeLog("fullscreenchange", { inFullscreen: document.fullscreenElement === landscapePlotWrap });
126
+
127
+ if (document.fullscreenElement === landscapePlotWrap) {
128
+ setLandscapeSize(window.innerWidth - 24, window.innerHeight - 24, true);
129
+ return;
130
+ }
131
+
132
+ constrainLandscapeSizeToContainer();
133
  });
134
 
135
  if (window.ResizeObserver) {
136
+ state.landscapeResizeObserver = new ResizeObserver((entries) => {
137
+ const entry = entries && entries[0];
138
+ const contentRect = entry ? entry.contentRect : null;
139
+ debugResizeLog("ResizeObserver", {
140
+ observed: contentRect ? `${contentRect.width.toFixed(2)}x${contentRect.height.toFixed(2)}` : "n/a",
141
+ });
142
+ queueLandscapeResize(false);
143
  });
144
  state.landscapeResizeObserver.observe(landscapePlotWrap);
145
  }
146
  }
147
 
148
  function handleResize() {
149
+ debugResizeLog("window.resize");
150
  resizeCanvases();
151
 
152
+ if (document.fullscreenElement === landscapePlotWrap) {
153
+ setLandscapeSize(window.innerWidth - 24, window.innerHeight - 24, false);
154
+ } else {
155
  constrainLandscapeSizeToContainer();
156
  }
157
 
 
167
  const parent = canvas.parentElement;
168
  if (!parent) return;
169
 
170
+ const width = Math.max(320, Math.round(parent.getBoundingClientRect().width));
171
  const targetHeight = Math.floor(width * ratio);
172
  const height = Math.max(minHeight, Math.min(maxHeight, targetHeight));
173
 
 
179
 
180
  function getLandscapeFitSize() {
181
  const host = landscapePlotWrap.parentElement;
182
+ const hostWidth = host ? host.getBoundingClientRect().width : window.innerWidth;
183
  const maxW = Math.max(320, Math.min(1000, hostWidth - 4));
184
 
185
  if (window.innerWidth < 680) {
 
200
 
201
  function setLandscapeSize(rawW, rawH, forceResize = false) {
202
  const host = landscapePlotWrap.parentElement;
203
+ const hostMaxW = host ? host.getBoundingClientRect().width - 4 : rawW;
204
  const isFullscreen = document.fullscreenElement === landscapePlotWrap;
205
 
206
  const minW = 320;
 
210
 
211
  const width = Math.round(Math.max(minW, Math.min(maxW, rawW)));
212
  const height = Math.round(Math.max(minH, Math.min(maxH, rawH)));
213
+ const rect = landscapePlotWrap.getBoundingClientRect();
214
+ const widthChanged = Math.abs(Math.round(rect.width) - width) > RESIZE_EPSILON;
215
+ const heightChanged = Math.abs(Math.round(rect.height) - height) > RESIZE_EPSILON;
216
 
217
+ if (widthChanged || heightChanged) {
218
  landscapePlotWrap.style.width = `${width}px`;
219
  landscapePlotWrap.style.height = `${height}px`;
220
+ debugResizeLog("setLandscapeSize.apply", { width, height, forceResize });
221
+ }
222
+
223
+ if (forceResize || widthChanged || heightChanged) {
224
+ queueLandscapeResize(true);
225
  }
226
  }
227
 
228
  function constrainLandscapeSizeToContainer() {
229
+ const rect = landscapePlotWrap.getBoundingClientRect();
230
+ setLandscapeSize(rect.width, rect.height, false);
231
  }
232
 
233
  function scaleLandscapeSize(factor) {
234
+ const rect = landscapePlotWrap.getBoundingClientRect();
235
+ setLandscapeSize(rect.width * factor, rect.height * factor, false);
236
  }
237
 
238
+ function queueLandscapeResize(force = false) {
239
  if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
240
+
241
+ const width = Math.round(landscapePlotWrap.clientWidth);
242
+ const height = Math.round(landscapePlotWrap.clientHeight);
243
+ if (width <= 0 || height <= 0) return;
244
+
245
+ if (!force && state.lastLandscapeSize) {
246
+ const dw = Math.abs(width - state.lastLandscapeSize.width);
247
+ const dh = Math.abs(height - state.lastLandscapeSize.height);
248
+ if (dw <= RESIZE_EPSILON && dh <= RESIZE_EPSILON) return;
249
+ }
250
+
251
+ state.lastLandscapeSize = { width, height };
252
+ debugResizeLog("queueLandscapeResize", { width, height, force });
253
+
254
+ if (state.landscapeResizeRaf) {
255
+ cancelAnimationFrame(state.landscapeResizeRaf);
256
+ }
257
+
258
+ state.landscapeResizeRaf = requestAnimationFrame(() => {
259
+ state.landscapeResizeRaf = null;
260
+ debugResizeLog("Plotly.Plots.resize", { width, height });
261
  Plotly.Plots.resize(landscapePlot);
262
  });
263
  }
 
731
  };
732
 
733
  const config = {
734
+ responsive: false,
735
  displaylogo: false,
736
  scrollZoom: true,
737
  doubleClick: "reset",
 
745
  plotPromise.then(() => {
746
  if (!state.landscapePlotReady) {
747
  state.landscapePlotReady = true;
748
+ state.lastLandscapeSize = null;
749
  landscapePlot.on("plotly_relayout", (ev) => {
750
  if (ev && ev["scene.camera"]) {
751
  state.landscapeCamera = cloneCamera(ev["scene.camera"]);
752
  }
753
+ debugResizeLog("plotly_relayout", {
754
+ hasCamera: !!(ev && ev["scene.camera"]),
755
+ });
756
  });
757
+ queueLandscapeResize(true);
758
  }
 
 
759
  });
760
  }
761
 
linear-regression/styles.css CHANGED
@@ -18,9 +18,10 @@ html,
18
  body {
19
  height: auto;
20
  min-height: 100%;
21
- overflow-y: auto !important;
22
  overflow-x: hidden;
23
  -webkit-overflow-scrolling: touch;
 
24
  }
25
 
26
  body {
@@ -437,18 +438,32 @@ input[type="checkbox"] {
437
  min-height: 320px;
438
  max-width: 1400px;
439
  max-height: 900px;
440
- resize: both;
441
- overflow: auto;
442
  border: 1px solid var(--line);
443
  border-radius: 12px;
444
  background: #fff;
445
  position: relative;
 
 
 
446
  }
447
 
448
  .landscape-plot {
449
  width: 100%;
450
  height: 100%;
451
- min-height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
452
  }
453
 
454
  .log-wrap {
 
18
  body {
19
  height: auto;
20
  min-height: 100%;
21
+ overflow-y: scroll !important;
22
  overflow-x: hidden;
23
  -webkit-overflow-scrolling: touch;
24
+ scrollbar-gutter: stable both-edges;
25
  }
26
 
27
  body {
 
438
  min-height: 320px;
439
  max-width: 1400px;
440
  max-height: 900px;
441
+ overflow: hidden;
 
442
  border: 1px solid var(--line);
443
  border-radius: 12px;
444
  background: #fff;
445
  position: relative;
446
+ contain: layout size paint;
447
+ min-inline-size: 0;
448
+ min-block-size: 0;
449
  }
450
 
451
  .landscape-plot {
452
  width: 100%;
453
  height: 100%;
454
+ display: block;
455
+ overflow: hidden;
456
+ min-width: 0;
457
+ min-height: 0;
458
+ }
459
+
460
+ .landscape-plot-wrap:fullscreen {
461
+ width: 100vw !important;
462
+ height: 100vh !important;
463
+ max-width: none;
464
+ max-height: none;
465
+ border-radius: 0;
466
+ border-width: 0;
467
  }
468
 
469
  .log-wrap {
logistic-regression/cost-visualization.html CHANGED
@@ -112,7 +112,7 @@
112
  <div id="landscapePlot" class="landscape-plot"></div>
113
  </div>
114
 
115
- <p class="slice-label">Drag to rotate · Scroll to zoom · Hover to inspect · Resize from the corner</p>
116
 
117
  <div class="iteration-block">
118
  <label for="landscapeStep">Gradient Descent Step</label>
@@ -130,6 +130,6 @@
130
  </main>
131
 
132
  <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
133
- <script src="cost.js?v=20260813b"></script>
134
  </body>
135
  </html>
 
112
  <div id="landscapePlot" class="landscape-plot"></div>
113
  </div>
114
 
115
+ <p class="slice-label">Drag to rotate · Scroll to zoom · Hover to inspect · Use Fit / +/- / Fullscreen to resize</p>
116
 
117
  <div class="iteration-block">
118
  <label for="landscapeStep">Gradient Descent Step</label>
 
130
  </main>
131
 
132
  <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
133
+ <script src="cost.js?v=20260814b"></script>
134
  </body>
135
  </html>
logistic-regression/cost.js CHANGED
@@ -53,6 +53,9 @@ const DEFAULT_CAMERA = {
53
  up: { x: 0, y: 0, z: 1 },
54
  };
55
 
 
 
 
56
  const GRID_N = 60;
57
  const MIN_SPAN = 2.0;
58
 
@@ -95,8 +98,35 @@ const state = {
95
  plotReady: false,
96
  camera3d: null,
97
  resizeObserver: null,
 
 
98
  };
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  function hasPlotly() {
101
  return typeof window !== "undefined" && typeof window.Plotly !== "undefined";
102
  }
@@ -471,7 +501,7 @@ function buildHover(meta, point) {
471
 
472
  function getFitSize() {
473
  const host = plotWrap.parentElement;
474
- const hostW = host ? host.clientWidth : window.innerWidth;
475
  const maxW = Math.max(320, Math.min(1000, hostW - 4));
476
 
477
  if (window.innerWidth < 680) return { width: maxW, height: 390 };
@@ -481,7 +511,7 @@ function getFitSize() {
481
 
482
  function setPlotSize(rawW, rawH, forceResize = false) {
483
  const host = plotWrap.parentElement;
484
- const hostMaxW = host ? host.clientWidth - 4 : rawW;
485
  const inFullscreen = document.fullscreenElement === plotWrap;
486
 
487
  const minW = 320;
@@ -491,11 +521,18 @@ function setPlotSize(rawW, rawH, forceResize = false) {
491
 
492
  const w = Math.round(Math.max(minW, Math.min(maxW, rawW)));
493
  const h = Math.round(Math.max(minH, Math.min(maxH, rawH)));
 
 
 
494
 
495
- if (forceResize || plotWrap.clientWidth !== w || plotWrap.clientHeight !== h) {
496
  plotWrap.style.width = `${w}px`;
497
  plotWrap.style.height = `${h}px`;
498
- resizePlot();
 
 
 
 
499
  }
500
  }
501
 
@@ -504,9 +541,31 @@ function fitPlotSize(forceResize = false) {
504
  setPlotSize(size.width, size.height, forceResize);
505
  }
506
 
507
- function resizePlot() {
508
  if (!state.plotReady || !hasPlotly()) return;
509
- requestAnimationFrame(() => window.Plotly.Plots.resize(plotEl));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  }
511
 
512
  function toggleFullscreen() {
@@ -717,7 +776,7 @@ function renderLandscape(caseKey, grid) {
717
  };
718
 
719
  const config2d = {
720
- responsive: true,
721
  displaylogo: false,
722
  scrollZoom: true,
723
  modeBarButtonsToRemove: ["lasso2d", "select2d"],
@@ -730,9 +789,10 @@ function renderLandscape(caseKey, grid) {
730
  p2d.then(() => {
731
  if (!state.plotReady) {
732
  state.plotReady = true;
 
733
  bindPlotClick();
 
734
  }
735
- resizePlot();
736
  });
737
  return;
738
  }
@@ -838,7 +898,7 @@ function renderLandscape(caseKey, grid) {
838
  };
839
 
840
  const config3d = {
841
- responsive: true,
842
  displaylogo: false,
843
  scrollZoom: true,
844
  doubleClick: "reset",
@@ -852,14 +912,18 @@ function renderLandscape(caseKey, grid) {
852
  p3d.then(() => {
853
  if (!state.plotReady) {
854
  state.plotReady = true;
 
855
  bindPlotClick();
856
  plotEl.on("plotly_relayout", (ev) => {
857
  if (ev && ev["scene.camera"]) {
858
  state.camera3d = cloneCamera(ev["scene.camera"]);
859
  }
 
 
 
860
  });
 
861
  }
862
- resizePlot();
863
  });
864
  }
865
 
@@ -884,7 +948,7 @@ function bindPlotClick() {
884
 
885
  function drawClassifier(caseKey) {
886
  const parent = classifierCanvas.parentElement;
887
- const width = Math.max(320, Math.floor(parent.clientWidth - 2));
888
  const height = 310;
889
  if (classifierCanvas.width !== width || classifierCanvas.height !== height) {
890
  classifierCanvas.width = width;
@@ -1218,24 +1282,41 @@ function bindEvents() {
1218
 
1219
  document.addEventListener("fullscreenchange", () => {
1220
  controls.plotFullscreenBtn.textContent = document.fullscreenElement === plotWrap ? "Exit Fullscreen" : "Fullscreen";
1221
- resizePlot();
 
 
 
 
 
 
 
 
1222
  });
1223
 
1224
  window.addEventListener("resize", () => {
1225
- if (document.fullscreenElement !== plotWrap) {
1226
- setPlotSize(plotWrap.clientWidth, plotWrap.clientHeight, true);
 
 
 
 
1227
  }
 
1228
  drawClassifier(controls.caseSelect.value);
1229
- resizePlot();
1230
  });
1231
 
1232
  if (window.ResizeObserver) {
1233
- state.resizeObserver = new window.ResizeObserver(() => {
1234
- resizePlot();
1235
- drawClassifier(controls.caseSelect.value);
 
 
 
 
 
 
1236
  });
1237
  state.resizeObserver.observe(plotWrap);
1238
- state.resizeObserver.observe(classifierCanvas.parentElement);
1239
  }
1240
  }
1241
 
 
53
  up: { x: 0, y: 0, z: 1 },
54
  };
55
 
56
+ const RESIZE_EPSILON = 1;
57
+ const DEBUG_RESIZE = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("debugResize");
58
+
59
  const GRID_N = 60;
60
  const MIN_SPAN = 2.0;
61
 
 
98
  plotReady: false,
99
  camera3d: null,
100
  resizeObserver: null,
101
+ resizeRaf: null,
102
+ lastPlotSize: null,
103
  };
104
 
105
+ function debugResizeLog(source, extra = {}) {
106
+ if (!DEBUG_RESIZE) return;
107
+
108
+ const wrapRect = plotWrap.getBoundingClientRect();
109
+ const parent = plotWrap.parentElement;
110
+ const parentRect = parent ? parent.getBoundingClientRect() : null;
111
+ const docEl = document.documentElement;
112
+
113
+ const payload = {
114
+ source,
115
+ wrapClient: `${plotWrap.clientWidth}x${plotWrap.clientHeight}`,
116
+ wrapRect: `${wrapRect.width.toFixed(2)}x${wrapRect.height.toFixed(2)}`,
117
+ parentRect: parentRect ? `${parentRect.width.toFixed(2)}x${parentRect.height.toFixed(2)}` : "n/a",
118
+ win: `${window.innerWidth}x${window.innerHeight}`,
119
+ viewport: `${docEl.clientWidth}x${docEl.clientHeight}`,
120
+ scroll: `${docEl.scrollWidth}x${docEl.scrollHeight}`,
121
+ bodyScrollY: window.scrollY,
122
+ hasVScrollbar: docEl.scrollHeight > docEl.clientHeight,
123
+ hasHScrollbar: docEl.scrollWidth > docEl.clientWidth,
124
+ ...extra,
125
+ };
126
+
127
+ console.debug("[logistic-resize]", payload);
128
+ }
129
+
130
  function hasPlotly() {
131
  return typeof window !== "undefined" && typeof window.Plotly !== "undefined";
132
  }
 
501
 
502
  function getFitSize() {
503
  const host = plotWrap.parentElement;
504
+ const hostW = host ? host.getBoundingClientRect().width : window.innerWidth;
505
  const maxW = Math.max(320, Math.min(1000, hostW - 4));
506
 
507
  if (window.innerWidth < 680) return { width: maxW, height: 390 };
 
511
 
512
  function setPlotSize(rawW, rawH, forceResize = false) {
513
  const host = plotWrap.parentElement;
514
+ const hostMaxW = host ? host.getBoundingClientRect().width - 4 : rawW;
515
  const inFullscreen = document.fullscreenElement === plotWrap;
516
 
517
  const minW = 320;
 
521
 
522
  const w = Math.round(Math.max(minW, Math.min(maxW, rawW)));
523
  const h = Math.round(Math.max(minH, Math.min(maxH, rawH)));
524
+ const rect = plotWrap.getBoundingClientRect();
525
+ const widthChanged = Math.abs(Math.round(rect.width) - w) > RESIZE_EPSILON;
526
+ const heightChanged = Math.abs(Math.round(rect.height) - h) > RESIZE_EPSILON;
527
 
528
+ if (widthChanged || heightChanged) {
529
  plotWrap.style.width = `${w}px`;
530
  plotWrap.style.height = `${h}px`;
531
+ debugResizeLog("setPlotSize.apply", { width: w, height: h, forceResize });
532
+ }
533
+
534
+ if (forceResize || widthChanged || heightChanged) {
535
+ queuePlotResize(true);
536
  }
537
  }
538
 
 
541
  setPlotSize(size.width, size.height, forceResize);
542
  }
543
 
544
+ function queuePlotResize(force = false) {
545
  if (!state.plotReady || !hasPlotly()) return;
546
+
547
+ const width = Math.round(plotWrap.clientWidth);
548
+ const height = Math.round(plotWrap.clientHeight);
549
+ if (width <= 0 || height <= 0) return;
550
+
551
+ if (!force && state.lastPlotSize) {
552
+ const dw = Math.abs(width - state.lastPlotSize.width);
553
+ const dh = Math.abs(height - state.lastPlotSize.height);
554
+ if (dw <= RESIZE_EPSILON && dh <= RESIZE_EPSILON) return;
555
+ }
556
+
557
+ state.lastPlotSize = { width, height };
558
+ debugResizeLog("queuePlotResize", { width, height, force });
559
+
560
+ if (state.resizeRaf) {
561
+ cancelAnimationFrame(state.resizeRaf);
562
+ }
563
+
564
+ state.resizeRaf = requestAnimationFrame(() => {
565
+ state.resizeRaf = null;
566
+ debugResizeLog("Plotly.Plots.resize", { width, height });
567
+ window.Plotly.Plots.resize(plotEl);
568
+ });
569
  }
570
 
571
  function toggleFullscreen() {
 
776
  };
777
 
778
  const config2d = {
779
+ responsive: false,
780
  displaylogo: false,
781
  scrollZoom: true,
782
  modeBarButtonsToRemove: ["lasso2d", "select2d"],
 
789
  p2d.then(() => {
790
  if (!state.plotReady) {
791
  state.plotReady = true;
792
+ state.lastPlotSize = null;
793
  bindPlotClick();
794
+ queuePlotResize(true);
795
  }
 
796
  });
797
  return;
798
  }
 
898
  };
899
 
900
  const config3d = {
901
+ responsive: false,
902
  displaylogo: false,
903
  scrollZoom: true,
904
  doubleClick: "reset",
 
912
  p3d.then(() => {
913
  if (!state.plotReady) {
914
  state.plotReady = true;
915
+ state.lastPlotSize = null;
916
  bindPlotClick();
917
  plotEl.on("plotly_relayout", (ev) => {
918
  if (ev && ev["scene.camera"]) {
919
  state.camera3d = cloneCamera(ev["scene.camera"]);
920
  }
921
+ debugResizeLog("plotly_relayout", {
922
+ hasCamera: !!(ev && ev["scene.camera"]),
923
+ });
924
  });
925
+ queuePlotResize(true);
926
  }
 
927
  });
928
  }
929
 
 
948
 
949
  function drawClassifier(caseKey) {
950
  const parent = classifierCanvas.parentElement;
951
+ const width = Math.max(320, Math.round(parent.getBoundingClientRect().width));
952
  const height = 310;
953
  if (classifierCanvas.width !== width || classifierCanvas.height !== height) {
954
  classifierCanvas.width = width;
 
1282
 
1283
  document.addEventListener("fullscreenchange", () => {
1284
  controls.plotFullscreenBtn.textContent = document.fullscreenElement === plotWrap ? "Exit Fullscreen" : "Fullscreen";
1285
+ debugResizeLog("fullscreenchange", { inFullscreen: document.fullscreenElement === plotWrap });
1286
+
1287
+ if (document.fullscreenElement === plotWrap) {
1288
+ setPlotSize(window.innerWidth - 24, window.innerHeight - 24, true);
1289
+ return;
1290
+ }
1291
+
1292
+ const rect = plotWrap.getBoundingClientRect();
1293
+ setPlotSize(rect.width, rect.height, false);
1294
  });
1295
 
1296
  window.addEventListener("resize", () => {
1297
+ debugResizeLog("window.resize");
1298
+ if (document.fullscreenElement === plotWrap) {
1299
+ setPlotSize(window.innerWidth - 24, window.innerHeight - 24, false);
1300
+ } else {
1301
+ const rect = plotWrap.getBoundingClientRect();
1302
+ setPlotSize(rect.width, rect.height, false);
1303
  }
1304
+
1305
  drawClassifier(controls.caseSelect.value);
 
1306
  });
1307
 
1308
  if (window.ResizeObserver) {
1309
+ state.resizeObserver = new window.ResizeObserver((entries) => {
1310
+ for (const entry of entries) {
1311
+ if (entry.target === plotWrap) {
1312
+ debugResizeLog("ResizeObserver", {
1313
+ observed: `${entry.contentRect.width.toFixed(2)}x${entry.contentRect.height.toFixed(2)}`,
1314
+ });
1315
+ queuePlotResize(false);
1316
+ }
1317
+ }
1318
  });
1319
  state.resizeObserver.observe(plotWrap);
 
1320
  }
1321
  }
1322
 
logistic-regression/styles.css CHANGED
@@ -17,9 +17,10 @@ html,
17
  body {
18
  height: auto;
19
  min-height: 100%;
20
- overflow-y: auto !important;
21
  overflow-x: hidden;
22
  -webkit-overflow-scrolling: touch;
 
23
  }
24
 
25
  body {
@@ -245,17 +246,31 @@ select {
245
  min-height: 320px;
246
  max-width: 1400px;
247
  max-height: 900px;
248
- resize: both;
249
- overflow: auto;
250
  border: 1px solid var(--line);
251
  border-radius: 10px;
252
  background: #fff;
 
 
 
253
  }
254
 
255
  .landscape-plot {
256
  width: 100%;
257
  height: 100%;
258
- min-height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
259
  }
260
 
261
  .plot-fallback {
 
17
  body {
18
  height: auto;
19
  min-height: 100%;
20
+ overflow-y: scroll !important;
21
  overflow-x: hidden;
22
  -webkit-overflow-scrolling: touch;
23
+ scrollbar-gutter: stable both-edges;
24
  }
25
 
26
  body {
 
246
  min-height: 320px;
247
  max-width: 1400px;
248
  max-height: 900px;
249
+ overflow: hidden;
 
250
  border: 1px solid var(--line);
251
  border-radius: 10px;
252
  background: #fff;
253
+ contain: layout size paint;
254
+ min-inline-size: 0;
255
+ min-block-size: 0;
256
  }
257
 
258
  .landscape-plot {
259
  width: 100%;
260
  height: 100%;
261
+ display: block;
262
+ overflow: hidden;
263
+ min-width: 0;
264
+ min-height: 0;
265
+ }
266
+
267
+ .landscape-plot-wrap:fullscreen {
268
+ width: 100vw !important;
269
+ height: 100vh !important;
270
+ max-width: none;
271
+ max-height: none;
272
+ border-radius: 0;
273
+ border-width: 0;
274
  }
275
 
276
  .plot-fallback {