-
+
+
diff --git a/steps/src/dataExplorer.js b/steps/src/dataExplorer.js
index c31763e..feb40dd 100644
--- a/steps/src/dataExplorer.js
+++ b/steps/src/dataExplorer.js
@@ -4,7 +4,8 @@ import { appState } from './state.js';
import { throttle } from './utils.js';
import {
canvasContainer,
- explorerBtn
+ explorerBtn,
+ mainContent
} from './dom.js'; // Import the DOM elements we need to listen to
// --- DOM Elements (Internal to this module) ---
@@ -16,14 +17,17 @@ const plotBtn = document.getElementById('plot-selected-btn');
const tabs = {
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') },
+ 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') },
};
const gridDiv = document.getElementById('data-grid');
+const trackGridDiv = document.getElementById('track-data-grid');
const chartCanvas = document.getElementById('data-chart');
// --- Module-Local State ---
let gridApi = null;
+let trackGridApi = null;
let chartInstance = null;
let currentGridData = null;
@@ -35,9 +39,42 @@ const gridOptions = {
sortable: true,
filter: 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 ---
function createChart(data, label) {
if (chartInstance) {
@@ -72,7 +109,7 @@ function hideExplorer() {
panel.classList.add('hidden');
}
-function switchTab(targetTab) {
+/* function switchTab(targetTab) {
Object.values(tabs).forEach(tab => {
tab.panel.classList.add('hidden');
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.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');
}
@@ -100,33 +156,109 @@ function updateExplorer() {
const frame = appState.vizData.radarFrames[appState.currentFrame];
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.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) {
- 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,
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('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 ---
let throttleTimer = null;
let debounceTimer = null;
@@ -145,7 +277,7 @@ export function throttledUpdateExplorer() {
if (!throttleTimer) {
updateExplorer(); // ...execute the update immediately.
// 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.
@@ -153,6 +285,112 @@ export function throttledUpdateExplorer() {
}
// --- 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) ---
export function initializeDataExplorer() {
@@ -161,6 +399,14 @@ export function initializeDataExplorer() {
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 ---
// 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
document.addEventListener("keydown", (event) => {
// Ignore if typing in an input