Browse Source

feat(layout): implement GridStack.js for resizable dashboards and fix p5.js resize stability

- Implemented GridStack.js as the core dashboard manager, enabling responsive dragging and resizing of the Radar, Video, and SpeedGraph panels.
- Replaced global windowResized events with debounced ResizeObserver instances attached to the specific panel containers.
- Fixed a massive memory leak in p5.js sketches by explicitly calling .remove() on old Graphics buffers before recreation.
- Optimized zoomSketch.js by removing destructive canvas teardown calls from radarSketch and using smoothed camera coordinates for tooltip bounding logic.
- Patched tooltip overflow bugs in drawUtils.js and speedGraphSketch.js to intelligently clamp within panel edges, even in narrow columns.
- Disabled native p5 windowResized triggers to prevent double-firing when moving the web app across multiple monitors.
refactor/sync-centralize
RUSHIL AMBARISH KADU 2 months ago
parent
commit
dc19e848d0
  1. 55
      steps/index.html
  2. 15
      steps/src/drawUtils.js
  3. 36
      steps/src/p5/radarSketch.js
  4. 24
      steps/src/p5/speedGraphSketch.js
  5. 40
      steps/src/p5/zoomSketch.js
  6. 11
      steps/src/ui.js

55
steps/index.html

@ -71,6 +71,9 @@
})();
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/gridstack.js/10.1.2/gridstack.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gridstack.js/10.1.2/gridstack-all.js"></script>
<script>
tailwind.config = {
darkMode: "class",
@ -473,13 +476,16 @@
<!-- END: ADD THIS NEW SCRIM ELEMENT -->
<!-- Main Content -->
<main class="flex-grow container mx-auto p-4 pt-8 flex flex-col lg:flex-row gap-6">
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="relative">
<div id="canvas-container"
class="w-full h-[75vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center relative">
<p id="canvas-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start
visualization</p>
<main class="flex-grow w-full max-w-[1920px] mx-auto p-4 pt-8">
<div class="grid-stack grid-stack-12">
<!-- Radar Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-x="0" gs-y="0" gs-w="6" gs-h="12">
<div class="grid-stack-item-content bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg overflow-visible flex flex-col relative group">
<div class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-gray-300/30 z-[60] transition-colors rounded-t-lg" title="Drag to move panel"></div>
<div class="relative w-full h-full flex-grow">
<div id="canvas-container" class="w-full h-full flex items-center justify-center relative">
<p id="canvas-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start visualization</p>
<!-- Range Slider (Vertical) -->
<div class="absolute bottom-8 left-2 flex flex-col items-center gap-2 z-20 group">
@ -491,9 +497,9 @@
</div>
</div>
<div id="radar-info-overlay"
class="absolute top-[-35px] left-0 right-0 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden">
class="absolute top-4 left-0 right-0 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden pointer-events-none">
</div>
<div class="absolute bottom left-1/2 -translate-x-1/2 flex items-center justify-center gap-4">
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center justify-center gap-4 z-20 pointer-events-none">
<div id="ego-speed-display"
class="bg-black bg-opacity-60 text-white text-sm px-3 py-1.5 rounded-md hidden font-mono"></div>
<div id="can-speed-display"
@ -501,21 +507,32 @@
</div>
</div>
</div>
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="w-full h-[50vh] bg-black rounded-lg shadow-inner flex items-center justify-center relative">
</div>
<!-- Video Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-x="6" gs-y="0" gs-w="6" gs-h="8">
<div class="grid-stack-item-content bg-black rounded-lg overflow-hidden flex items-center justify-center relative group">
<div class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-white/20 z-[60] transition-colors rounded-t-lg" title="Drag to move panel"></div>
<video id="video-player" class="w-full h-full object-contain hidden" muted playsinline></video>
<p id="video-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load a video file</p>
<p id="video-placeholder" class="hidden text-gray-500 text-lg">Load a video file</p>
<div id="video-info-overlay"
class="absolute top-1 left-2 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden">
class="absolute top-4 left-2 z-[20] bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden pointer-events-none">
</div>
<div id="debug-overlay"
class="absolute top-0 left-0 bg-black bg-opacity-60 text-white p-2 font-mono text-xs hidden w-full"></div>
class="absolute top-0 left-0 bg-black bg-opacity-60 text-white p-2 font-mono text-xs hidden w-full z-[20] pointer-events-none"></div>
</div>
</div>
<!-- SpeedGraph Panel -->
<div class="grid-stack-item shadow-md rounded-lg" gs-x="6" gs-y="8" gs-w="6" gs-h="4">
<div class="grid-stack-item-content bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden flex items-center justify-center relative group">
<div class="absolute top-0 left-0 w-full h-4 cursor-grab active:cursor-grabbing bg-transparent hover:bg-gray-300/30 z-[60] transition-colors rounded-t-lg" title="Drag to move panel"></div>
<div id="speed-graph-container" class="w-full h-full flex items-center justify-center p-1 relative">
<p id="speed-graph-placeholder" class="text-gray-500 dark:text-gray-400 text-lg text-center absolute pointer-events-none">Load JSON & Video to see speed graph</p>
</div>
</div>
<div id="speed-graph-container"
class="w-full h-[25vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center">
<p id="speed-graph-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON & Video to see speed
graph
</p>
</div>
</div>
<div id="data-explorer-panel"

15
steps/src/drawUtils.js

@ -956,12 +956,19 @@ export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) {
const xOffset = 20;
let boxX, lineAnchorX;
if (mouseX + xOffset + boxWidth > p.width) { // Use smoothed values
boxX = mouseX - boxWidth - xOffset;
lineAnchorX = boxX + boxWidth;
} else {
// Strategy: Try placing on the right. If it overflows, try the left. If it still overflows, clamp it to screen edges.
if (mouseX + xOffset + boxWidth <= p.width) {
boxX = mouseX + xOffset;
lineAnchorX = boxX;
} else if (mouseX - xOffset - boxWidth >= 0) {
boxX = mouseX - xOffset - boxWidth;
lineAnchorX = boxX + boxWidth;
} else {
// Doesn't cleanly fit on either side. Clamp it to the canvas bounds.
boxX = Math.max(0, Math.min(mouseX + xOffset, p.width - boxWidth));
// Point the anchor to whichever side of the box is closer to the mouse
lineAnchorX = (boxX + boxWidth / 2 < mouseX) ? boxX + boxWidth : boxX;
}
let boxY = mouseY - boxHeight / 2;
boxY = p.constrain(boxY, 0, p.height - boxHeight);

36
steps/src/p5/radarSketch.js

@ -186,6 +186,20 @@ export const radarSketch = function (p) {
}
// --- END: Radar Range Slider Logic ---
// --- START: ResizeObserver for GridStack ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
// Debounce to prevent massive memory/CPU spikes during fast dragging
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (canvasContainer && canvasContainer.offsetWidth > 0 && canvasContainer.offsetHeight > 0) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
ro.observe(canvasContainer);
// --- END: ResizeObserver for GridStack ---
p.noLoop();
// Disable continuous looping, redraw will be called manually
};
@ -588,27 +602,25 @@ export const radarSketch = function (p) {
p.windowResized = function () {
console.log("radarSketch: windowResized triggered!");
p.windowResized = function () {}; // Disable native p5 window event to prevent multi-monitor dragging double-fires
p.handleContainerResize = function () {
console.log("radarSketch: handleContainerResize triggered!");
// Immediately resize the elements that we know are stable.
p.resizeCanvas(canvasContainer.offsetWidth, canvasContainer.offsetHeight);
// PREVENT MEMORY LEAK: Destroy old buffers to free memory
if (staticBackgroundBuffer) staticBackgroundBuffer.remove();
if (trackLegendBuffer) trackLegendBuffer.remove();
staticBackgroundBuffer = p.createGraphics(p.width, p.height);
trackLegendBuffer = p.createGraphics(120, 120);
p.drawTrackLegendToBuffer();
calculatePlotScales();
drawStaticRegionsToBuffer(p, staticBackgroundBuffer, plotScales);
// Defer the call to destroy the zoom canvas.
if (appState.zoomSketchInstance && appState.isCloseUpMode) {
setTimeout(() => {
console.log(
"radarSketch: Executing deferred call to zoomSketch.handleResize()."
);
appState.zoomSketchInstance.handleResize();
}, 10); // A 10ms delay is slightly more robust than 0.
}
if (appState.vizData) {
p.redraw();
}

24
steps/src/p5/speedGraphSketch.js

@ -402,6 +402,21 @@ export const speedGraphSketch = function (p) {
});
staticBuffer = p.createGraphics(p.width, p.height);
// --- START: ResizeObserver for GridStack ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
// Debounce to prevent massive memory/CPU spikes during fast dragging
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (speedGraphContainer && speedGraphContainer.offsetWidth > 0 && speedGraphContainer.offsetHeight > 0) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
ro.observe(speedGraphContainer);
// --- END: ResizeObserver for GridStack ---
p.noLoop();
};
@ -479,6 +494,7 @@ export const speedGraphSketch = function (p) {
// compute box position (avoid overflowing right edge)
let boxX = hoverX + 12;
if (boxX + boxW > p.width) boxX = hoverX - 12 - boxW;
boxX = Math.max(0, boxX); // avoid overflowing left edge
const boxY = pad.top + 6;
// Draw background box
@ -527,9 +543,15 @@ export const speedGraphSketch = function (p) {
}
}
p.windowResized = function () {
p.windowResized = function () {}; // Disable native p5 window event
p.handleContainerResize = function () {
p.resizeCanvas(speedGraphContainer.offsetWidth, speedGraphContainer.offsetHeight);
// PREVENT MEMORY LEAK: Destroy old buffer before recreating
if (staticBuffer) staticBuffer.remove();
staticBuffer = p.createGraphics(p.width, p.height);
hoverX = null; // reset hover on resize
if (appState.vizData && videoDuration > 0) {
p.drawStaticGraphToBuffer(appState.vizData);

40
steps/src/p5/zoomSketch.js

@ -17,7 +17,7 @@ import {
toggleCovariance,
} from "../dom.js";
function drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY) {
function drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY) {
if (!hoveredItems || hoveredItems.length === 0 || smoothedAvgX === null) return;
// 1. Generate text content
@ -153,10 +153,11 @@ function drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX,
// Calculate the visible bounds in the current coordinate system (which is scaled and translated)
const visibleW = p.width / zoomFactor;
const visibleH = p.height / zoomFactor;
const minVisX = mainMouseX - visibleW / 2;
const maxVisX = mainMouseX + visibleW / 2;
const minVisY = mainMouseY - visibleH / 2;
const maxVisY = mainMouseY + visibleH / 2;
// Use camera center instead of raw mouse position to accurately represent the visible viewport
const minVisX = smoothedCamX - visibleW / 2;
const maxVisX = smoothedCamX + visibleW / 2;
const minVisY = smoothedCamY - visibleH / 2;
const maxVisY = smoothedCamY + visibleH / 2;
const edgePad = 10 / zoomFactor;
// Constrain X & Y to keep tooltip within the zoom view
@ -229,6 +230,20 @@ export const zoomSketch = function (p) {
p.frameRate(144);
// We enable looping so the lerp smoothing can animate between frames
p.loop();
// --- START: ResizeObserver for ZoomSketch ---
let resizeDebounce = null;
const ro = new ResizeObserver(() => {
if (resizeDebounce) clearTimeout(resizeDebounce);
resizeDebounce = setTimeout(() => {
if (canvas) {
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
}
}, 100);
});
const container = document.getElementById(containerId);
if(container) ro.observe(container);
// --- END: ResizeObserver for ZoomSketch ---
};
p.updateAndDraw = function (mainMouseX, mainMouseY, hoveredItems, scales) {
@ -251,14 +266,13 @@ export const zoomSketch = function (p) {
// but it helps if updateAndDraw is called less frequently than the frame rate.
};
p.handleResize = function () {
console.log("zoomSketch: handleResize triggered. Destroying old canvas.");
if (canvas) {
canvas.remove(); // p5.js function to properly remove the canvas from the DOM
canvas = null; // Set the internal reference to null
p.windowResized = function () {}; // Disable native p5 window resize
p.handleContainerResize = function () {
const container = document.getElementById(containerId);
if (container && canvas) {
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
}
// The canvas will be recreated automatically the next time updateAndDraw() is called,
// at which point the container will have its correct, final dimensions.
};
p.draw = function () {
if (!appState.vizData || !canvas) return;
@ -393,7 +407,7 @@ export const zoomSketch = function (p) {
p.pop(); // End radar transformations
// --- Call the new, self-contained tooltip function with smoothed coords ---
drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY);
drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY);
// --- START: Draw Purple Debug Circle ---
// This circle represents the hover radius, drawn in the zoomed coordinate space.

11
steps/src/ui.js

@ -119,6 +119,17 @@ function handleColorToggles(e) {
}
export function initUIEventListeners() {
// --- Initialize GridStack ---
if (typeof GridStack !== 'undefined') {
GridStack.init({
margin: 10,
cellHeight: '6vh', // Radar is 12h=72vh, Video is 8h=48vh, Graph is 4h=24vh
disableOneColumnMode: true,
animate: true,
handle: '.grid-stack-item-content > .cursor-grab', // use the specific drag handle we added
});
}
// --- Shortcuts Modal ---
shortcutsBtn.addEventListener("click", (e) => {
e.preventDefault();

Loading…
Cancel
Save