Browse Source

Added extra information (track info) in the data explorer. Making it dragable and resizable.

refactor/modularize
RUSHIL AMBARISH KADU 7 months ago
parent
commit
4e1469ceec
  1. 46
      steps/index.html
  2. 293
      steps/src/dataExplorer.js

46
steps/index.html

@ -69,6 +69,29 @@
#progress-bar { #progress-bar {
transition: width 0.1s linear; transition: width 0.1s linear;
} }
/* --- START: CSS for Resizable/Draggable Panel --- */
.resizer {
position: absolute;
width: 10px;
height: 10px;
background: transparent;
z-index: 10;
}
.resizer-t { top: -5px; left: 5px; right: 5px; height: 10px; cursor: ns-resize; }
.resizer-r { top: 5px; right: -5px; bottom: 5px; width: 10px; cursor: ew-resize; }
.resizer-b { bottom: -5px; left: 5px; right: 5px; height: 10px; cursor: ns-resize; }
.resizer-l { top: 5px; left: -5px; bottom: 5px; width: 10px; cursor: ew-resize; }
.resizer-tl { top: -5px; left: -5px; cursor: nwse-resize; }
.resizer-tr { top: -5px; right: -5px; cursor: nesw-resize; }
.resizer-br { bottom: -5px; right: -5px; cursor: nwse-resize; }
.resizer-bl { bottom: -5px; left: -5px; cursor: nesw-resize; }
/* When dragging/resizing, prevent text selection on the page */
body.resizing, body.dragging {
user-select: none;
-webkit-user-select: none;
}
/* --- END: CSS for Resizable/Draggable Panel --- */
</style> </style>
</head> </head>
@ -275,24 +298,39 @@
<div id="data-explorer-panel" <div id="data-explorer-panel"
class="hidden fixed bottom-24 right-4 bg-white dark:bg-gray-800 shadow-2xl rounded-lg z-30 flex flex-col w-full max-w-2xl h-1/2 border dark:border-gray-600"> class="hidden fixed bottom-24 right-4 bg-white dark:bg-gray-800 shadow-2xl rounded-lg z-30 flex flex-col w-full max-w-2xl h-1/2 border dark:border-gray-600">
<div class="flex items-center justify-between p-2 border-b dark:border-gray-700">
<!-- START: Add Draggable Header and Resizer Handles -->
<div id="data-explorer-header" class="flex items-center justify-between p-2 border-b dark:border-gray-700 cursor-move">
<h2 class="text-lg font-bold ml-2">Data Explorer</h2> <h2 class="text-lg font-bold ml-2">Data Explorer</h2>
<button id="close-explorer-btn" <button id="close-explorer-btn"
class="text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-1.5">&times;</button> class="text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg p-1.5">&times;</button>
</div> </div>
<div class="resizer resizer-t"></div>
<div class="resizer resizer-r"></div>
<div class="resizer resizer-b"></div>
<div class="resizer resizer-l"></div>
<div class="resizer resizer-tl"></div>
<div class="resizer resizer-tr"></div>
<div class="resizer resizer-br"></div>
<div class="resizer resizer-bl"></div>
<!-- END: Add Draggable Header and Resizer Handles -->
<div class="flex border-b dark:border-gray-700"> <div class="flex border-b dark:border-gray-700">
<button id="tab-btn-tree" class="px-4 py-2 text-sm font-medium border-b-2 border-blue-500">Tree View</button> <button id="tab-btn-tree" class="px-4 py-2 text-sm font-medium border-b-2 border-blue-500">Tree View</button>
<button id="tab-btn-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Grid <button id="tab-btn-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Grid
View</button> View</button>
<button id="tab-btn-track-grid" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Track Grid</button>
<button id="tab-btn-plot" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Plot <button id="tab-btn-plot" class="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400">Plot
View</button> View</button>
</div> </div>
<div class="flex-grow p-2 overflow-auto">
<div class="flex-grow overflow-auto">
<div id="tab-panel-tree" class="font-mono text-xs"></div> <div id="tab-panel-tree" class="font-mono text-xs"></div>
<div id="tab-panel-grid" class="hidden h-full">
<div id="data-grid" class="ag-theme-alpine-dark h-full"></div>
<div id="tab-panel-grid" class="hidden h-full p-2">
<div id="data-grid" class="ag-theme-alpine-dark h-full w-full"></div>
</div>
<div id="tab-panel-track-grid" class="hidden h-full p-2">
<div id="track-data-grid" class="ag-theme-alpine-dark h-full w-full"></div>
</div> </div>
<div id="tab-panel-plot" class="hidden p-2"> <div id="tab-panel-plot" class="hidden p-2">
<canvas id="data-chart"></canvas> <canvas id="data-chart"></canvas>

293
steps/src/dataExplorer.js

@ -4,7 +4,8 @@ import { appState } from './state.js';
import { throttle } from './utils.js'; import { throttle } from './utils.js';
import { import {
canvasContainer, canvasContainer,
explorerBtn
explorerBtn,
mainContent
} from './dom.js'; // Import the DOM elements we need to listen to } from './dom.js'; // Import the DOM elements we need to listen to
// --- DOM Elements (Internal to this module) --- // --- DOM Elements (Internal to this module) ---
@ -16,14 +17,17 @@ const plotBtn = document.getElementById('plot-selected-btn');
const tabs = { const tabs = {
tree: { btn: document.getElementById('tab-btn-tree'), panel: document.getElementById('tab-panel-tree') }, tree: { btn: document.getElementById('tab-btn-tree'), panel: document.getElementById('tab-panel-tree') },
grid: { btn: document.getElementById('tab-btn-grid'), panel: document.getElementById('tab-panel-grid') }, grid: { btn: document.getElementById('tab-btn-grid'), panel: document.getElementById('tab-panel-grid') },
trackGrid: { btn: document.getElementById('tab-btn-track-grid'), panel: document.getElementById('tab-panel-track-grid') },
plot: { btn: document.getElementById('tab-btn-plot'), panel: document.getElementById('tab-panel-plot') }, plot: { btn: document.getElementById('tab-btn-plot'), panel: document.getElementById('tab-panel-plot') },
}; };
const gridDiv = document.getElementById('data-grid'); const gridDiv = document.getElementById('data-grid');
const trackGridDiv = document.getElementById('track-data-grid');
const chartCanvas = document.getElementById('data-chart'); const chartCanvas = document.getElementById('data-chart');
// --- Module-Local State --- // --- Module-Local State ---
let gridApi = null; let gridApi = null;
let trackGridApi = null;
let chartInstance = null; let chartInstance = null;
let currentGridData = null; let currentGridData = null;
@ -35,9 +39,42 @@ const gridOptions = {
sortable: true, sortable: true,
filter: true, filter: true,
resizable: true, resizable: true,
// --- START: Set a default width for all columns ---
width: 100, // Keep width at 100 as requested
// --- END: Set a default width ---
}, },
// --- START: Define a specific column type for numbers ---
// This allows us to apply special formatting only to numeric columns.
columnTypes: {
numberColumn: {
// --- START: Add column-specific number formatting ---
// This formatter checks the column ID and applies the correct precision.
valueFormatter: params => {
const { value, colDef } = params;
// Do nothing if value is not a number or is an integer (like 'index')
if (typeof value !== 'number' || value === null || Number.isInteger(value)) {
return value;
}
// Apply formatting based on the column's field name
switch (colDef.field) {
case 'snr':
return value.toFixed(2); // SNR gets 2 decimal places
default:
return value.toFixed(4); // All other numbers get 4 decimal places
}
}
// --- END: Add column-specific number formatting ---
}
}
// --- END: Define a specific column type for numbers ---
};
const trackGridOptions = {
...gridOptions
}; };
// --- Chart.js Configuration --- // --- Chart.js Configuration ---
function createChart(data, label) { function createChart(data, label) {
if (chartInstance) { if (chartInstance) {
@ -72,7 +109,7 @@ function hideExplorer() {
panel.classList.add('hidden'); panel.classList.add('hidden');
} }
function switchTab(targetTab) {
/* function switchTab(targetTab) {
Object.values(tabs).forEach(tab => { Object.values(tabs).forEach(tab => {
tab.panel.classList.add('hidden'); tab.panel.classList.add('hidden');
tab.btn.classList.remove('border-blue-500', 'text-gray-900', 'dark:text-white'); tab.btn.classList.remove('border-blue-500', 'text-gray-900', 'dark:text-white');
@ -83,6 +120,25 @@ function switchTab(targetTab) {
tabs[targetTab].btn.classList.add('border-blue-500', 'text-gray-900', 'dark:text-white'); tabs[targetTab].btn.classList.add('border-blue-500', 'text-gray-900', 'dark:text-white');
tabs[targetTab].btn.classList.remove('text-gray-500', 'dark:text-gray-400'); tabs[targetTab].btn.classList.remove('text-gray-500', 'dark:text-gray-400');
footer.classList.toggle('hidden', targetTab !== 'grid');
} */
// In src/dataExplorer.js
function switchTab(targetTab) {
Object.values(tabs).forEach(tab => {
tab.panel.classList.add('hidden');
// Remove active classes (border and color)
tab.btn.classList.remove('border-b-2', 'border-blue-500');
// Add inactive classes (gray text)
tab.btn.classList.add('text-gray-500', 'dark:text-gray-400');
});
tabs[targetTab].panel.classList.remove('hidden');
// Add active classes (border and color)
tabs[targetTab].btn.classList.add('border-b-2', 'border-blue-500');
// Remove inactive classes (gray text)
tabs[targetTab].btn.classList.remove('text-gray-500', 'dark:text-gray-400');
footer.classList.toggle('hidden', targetTab !== 'grid'); footer.classList.toggle('hidden', targetTab !== 'grid');
} }
@ -100,33 +156,109 @@ function updateExplorer() {
const frame = appState.vizData.radarFrames[appState.currentFrame]; const frame = appState.vizData.radarFrames[appState.currentFrame];
if (!frame) return; if (!frame) return;
// --- START: Correctly gather track data for the current frame ---
// We iterate through all tracks and find the history log entry for the current frame.
const tracksForCurrentFrame = appState.vizData.tracks
.map(track => {
const log = track.historyLog.find(log => log.frameIdx === appState.currentFrame);
// Return a new object combining track ID with its log for this frame, if it exists.
return log ? { trackId: track.id, ...log } : null;
})
.filter(Boolean); // Filter out any null entries for tracks not present in this frame.
// --- END: Correctly gather track data for the current frame ---
tabs.tree.panel.innerHTML = ''; tabs.tree.panel.innerHTML = '';
tabs.tree.panel.appendChild(createTreeView({ tabs.tree.panel.appendChild(createTreeView({
currentFrame: appState.currentFrame,
frameData: frame
currentFrame: appState.currentFrame + 1,
frameData: frame,
trackData: tracksForCurrentFrame // Use the newly created array
})); }));
// --- START: Auto-update Point Cloud Grid ---
displayInGrid(frame.pointCloud, `${appState.currentFrame + 1}`);
// --- END: Auto-update Point Cloud Grid ---
displayTracksInGrid(tracksForCurrentFrame);
} }
function displayInGrid(data, title) { function displayInGrid(data, title) {
if (!data || data.length === 0 || !gridApi) return;
if (!Array.isArray(data) || data.length === 0 || !gridApi) return;
// --- START: Add index and prepare data ---
// We map the original data to a new array, adding an 'index' property to each object.
const indexedData = data.map((row, index) => ({
index: index,
...row
}));
currentGridData = indexedData; // Store the new indexed data
// --- END: Add index and prepare data ---
currentGridData = data;
// Auto-generate columns from the first data object
const columns = Object.keys(data[0]).map(key => ({
// --- START: More robust column generation ---
// Auto-generate columns, and assign a 'type' if the data is numeric.
const columns = Object.keys(indexedData[0]).map(key => ({
field: key, field: key,
headerName: key, // Set header name headerName: key, // Set header name
sortable: true, // Ensure all generated columns are sortable
filter: true, // Ensure all generated columns are filterable
// If the first row's value for this key is a number, assign our custom number type.
type: typeof indexedData[0][key] === 'number' ? 'numberColumn' : undefined
})); }));
// --- END: More robust column generation ---
gridApi.setGridOption('columnDefs', columns); gridApi.setGridOption('columnDefs', columns);
gridApi.setGridOption('rowData', data);
tabs.grid.btn.textContent = `Grid View: ${title}`;
switchTab('grid');
gridApi.setGridOption('rowData', indexedData);
// --- START: Apply default sort ---
// After setting the data, apply a sort model to sort by the 'index' column ascending.
gridApi.applyColumnState({ state: [{ colId: 'index', sort: 'asc' }] });
// --- END: Apply default sort ---
tabs.grid.btn.textContent = `Point Cloud: Frame ${title}`;
}
function displayTracksInGrid(trackData) {
if (!trackGridApi || !Array.isArray(trackData) || trackData.length === 0) {
if (trackGridApi) trackGridApi.setGridOption('rowData', []);
return;
}
// --- START: Update Track Grid Title ---
tabs.trackGrid.btn.textContent = `Track Grid: Frame ${appState.currentFrame + 1}`;
// --- END: Update Track Grid Title ---
// --- START: Build a complete set of columns from ALL tracks ---
// Create a Set of all unique keys from all track objects in the current frame.
// This prevents missing columns if the first track has fewer properties than others.
const allKeys = new Set();
trackData.forEach(track => {
Object.keys(track).forEach(key => allKeys.add(key));
});
// --- START: Fix for column type detection ---
const columns = Array.from(allKeys).map(key => ({
field: key,
headerName: key,
// Find the first track that has a non-null value for this key to determine its type.
// This is much more reliable than only checking the first track in the list.
type: typeof trackData.find(t => t[key] !== null && t[key] !== undefined)?.[key] === 'number' ? 'numberColumn' : undefined,
valueFormatter: params => {
if (Array.isArray(params.value)) {
// --- START: Robust Array Formatting ---
// Check if the item 'v' is a number before calling toFixed.
// If it's not a number (e.g., it's another array), stringify it.
return `[${params.value.map(v => (typeof v === 'number' && v !== null) ? v.toFixed(3) : JSON.stringify(v)).join(', ')}]`;
// --- END: Robust Array Formatting ---
}
return params.value;
}
}));
// --- END: Fix for column type detection ---
// --- END: Build a complete set of columns from ALL tracks ---
trackGridApi.setGridOption('columnDefs', columns);
trackGridApi.setGridOption('rowData', trackData);
} }
// --- START: New Robust Update Logic --- // --- START: New Robust Update Logic ---
let throttleTimer = null; let throttleTimer = null;
let debounceTimer = null; let debounceTimer = null;
@ -145,7 +277,7 @@ export function throttledUpdateExplorer() {
if (!throttleTimer) { if (!throttleTimer) {
updateExplorer(); // ...execute the update immediately. updateExplorer(); // ...execute the update immediately.
// Then, set a cool-down timer to prevent another immediate execution. // Then, set a cool-down timer to prevent another immediate execution.
throttleTimer = setTimeout(() => { throttleTimer = null; }, 400); // 400ms throttle
throttleTimer = setTimeout(() => { throttleTimer = null; }, 100); // 100ms throttle for snappy display
} }
// Schedule a final, debounced update for after the interactions stop. // Schedule a final, debounced update for after the interactions stop.
@ -153,6 +285,112 @@ export function throttledUpdateExplorer() {
} }
// --- END: New Robust Update Logic --- // --- END: New Robust Update Logic ---
// --- START: Resizable and Draggable Panel Logic ---
function makePanelInteractive(panel) {
const header = document.getElementById('data-explorer-header');
const resizers = panel.querySelectorAll('.resizer');
const minWidth = 400;
const minHeight = 300;
let original_width = 0;
let original_height = 0;
let original_x = 0;
let original_y = 0;
let original_mouse_x = 0;
let original_mouse_y = 0;
// --- Dragging Logic ---
header.addEventListener('mousedown', (e) => {
e.preventDefault();
original_x = panel.offsetLeft;
original_y = panel.offsetTop;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
document.body.classList.add('dragging');
window.addEventListener('mousemove', dragPanel);
window.addEventListener('mouseup', stopDrag);
});
function dragPanel(e) {
const dx = e.pageX - original_mouse_x;
const dy = e.pageY - original_mouse_y;
panel.style.left = `${original_x + dx}px`;
panel.style.top = `${original_y + dy}px`;
}
function stopDrag() {
document.body.classList.remove('dragging');
window.removeEventListener('mousemove', dragPanel);
window.removeEventListener('mouseup', stopDrag);
}
// --- Resizing Logic ---
resizers.forEach(resizer => {
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
original_width = parseFloat(getComputedStyle(panel, null).getPropertyValue('width').replace('px', ''));
original_height = parseFloat(getComputedStyle(panel, null).getPropertyValue('height').replace('px', ''));
original_x = panel.getBoundingClientRect().left;
original_y = panel.getBoundingClientRect().top;
original_mouse_x = e.pageX;
original_mouse_y = e.pageY;
const resizeFunc = (event) => resizePanel(event, resizer.classList);
document.body.classList.add('resizing');
window.addEventListener('mousemove', resizeFunc);
window.addEventListener('mouseup', () => {
document.body.classList.remove('resizing');
window.removeEventListener('mousemove', resizeFunc);
});
});
});
function resizePanel(e, direction) {
// --- START: Fix for Resizing Logic ---
// The logic is updated to handle corners correctly by checking for 't', 'b', 'l', 'r' substrings.
// This allows a corner like 'resizer-br' to trigger both bottom and right resizing logic.
if (direction.toString().includes('r')) { // Check for right edge
const width = original_width + (e.pageX - original_mouse_x);
if (width > minWidth) panel.style.width = `${width}px`;
}
if (direction.toString().includes('b')) { // Check for bottom edge
const height = original_height + (e.pageY - original_mouse_y);
if (height > minHeight) panel.style.height = `${height}px`;
}
if (direction.toString().includes('l')) { // Check for left edge
const newWidth = original_width - (e.pageX - original_mouse_x);
if (newWidth > minWidth) {
panel.style.width = `${newWidth}px`;
panel.style.left = `${original_x + (e.pageX - original_mouse_x)}px`;
}
}
if (direction.toString().includes('t')) { // Check for top edge
const newHeight = original_height - (e.pageY - original_mouse_y);
if (newHeight > minHeight) {
panel.style.height = `${newHeight}px`;
panel.style.top = `${original_y + (e.pageY - original_mouse_y)}px`;
}
}
// --- END: Fix for Resizing Logic ---
}
}
function initializePanelPosition(panel) {
// Remove Tailwind classes that conflict with dynamic positioning/sizing
panel.classList.remove('bottom-24', 'right-4', 'w-full', 'max-w-2xl', 'h-1/2');
// Set initial size and position with inline styles
const initialWidth = 896; // Corresponds to max-w-2xl
const initialHeight = window.innerHeight / 2;
panel.style.width = `${initialWidth}px`;
panel.style.height = `${initialHeight}px`;
panel.style.top = `${window.innerHeight - initialHeight - 96}px`; // 96px is roughly bottom-24
panel.style.left = `${window.innerWidth - initialWidth - 16}px`; // 16px is right-4
}
// --- END: Resizable and Draggable Panel Logic ---
// --- Initialization Function (The file's only export) --- // --- Initialization Function (The file's only export) ---
export function initializeDataExplorer() { export function initializeDataExplorer() {
@ -161,6 +399,14 @@ export function initializeDataExplorer() {
gridApi = agGrid.createGrid(gridDiv, gridOptions); gridApi = agGrid.createGrid(gridDiv, gridOptions);
} }
if (!trackGridApi) {
trackGridApi = agGrid.createGrid(trackGridDiv, trackGridOptions);
}
// --- START: Make panel interactive ---
initializePanelPosition(panel);
makePanelInteractive(panel);
// --- END: Make panel interactive ---
// --- Wire up all event listeners --- // --- Wire up all event listeners ---
// Toggle panel visibility // Toggle panel visibility
@ -198,21 +444,6 @@ export function initializeDataExplorer() {
} }
}); });
// Main canvas click listener
canvasContainer.addEventListener('click', () => {
if (!appState.vizData) return;
const currentFrameData = appState.vizData.radarFrames[appState.currentFrame];
if (currentFrameData && currentFrameData.pointCloud) {
// Send point cloud data to the grid
displayInGrid(currentFrameData.pointCloud, `Frame ${appState.currentFrame} - Point Cloud`);
// Show the explorer if it's hidden
if (panel.classList.contains('hidden')) {
showExplorer();
}
}
});
// Keyboard shortcut listener // Keyboard shortcut listener
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
// Ignore if typing in an input // Ignore if typing in an input

Loading…
Cancel
Save