Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/roadrunner-view/public/guide/ActiveVehiclePlot.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The Active Vehicle Plot displays a chart that shows the number of vehicles that have been run over the span of the retention period. The X-axis is time, and the Y-axis is the number of concurrent sessions that were occuring at that time.
The Active Vehicle Plot displays a chart that shows the number of vehicles that have been run over the span of the available data window (with a minimum window of 1 day). The X-axis is time, and the Y-axis is the number of concurrent sessions that were occuring at that time.

When invoked from the Home page, the plot opens with the plot zoomed out to show the entire retention period. If invoked from the Driver's View page, the plot will automatically zoom in to the span of the selected vehicle's lifetime.
When invoked from the Home page, the plot opens with the plot zoomed out to show the entire available window. If invoked from the Driver's View page, the plot will automatically zoom in to the span of the selected vehicle's lifetime.

Below is a typical view after invoking from the Home Page:

Expand Down
2 changes: 1 addition & 1 deletion apps/roadrunner-view/public/guide/SimTable.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Sim Table Panel
===============
The Sim Table Panel provides a listing of all vehicle simulations that have occurred during the retention period. For each simulation session, the vehicle ID, initiating user, and start time are listed. A playback button is provided to allow starting playback of that simulation session. The table has controls that allow paging through the sessions. There is also a button to set the playback time to "Now".
The Sim Table Panel provides a listing of all vehicle simulations that have occurred during the available data window. For each simulation session, the vehicle ID, initiating user, and start time are listed. A playback button is provided to allow starting playback of that simulation session. The table has controls that allow paging through the sessions. There is also a button to set the playback time to "Now".

A typical Sim Table Panel is shown below:
![Sim Table Panel](/guide/images/SimTable.png)
89 changes: 75 additions & 14 deletions apps/roadrunner-view/src/components/Shared/ActiveVehiclePlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
ReferenceDot
} from 'recharts';

const ONE_DAY_MS = 24 * 60 * 60 * 1000;

export const ActiveVehiclePlot = (props: {
toggleShowActiveVehiclePlot: any,
vehicleId?: any | null
Expand All @@ -36,11 +38,59 @@ export const ActiveVehiclePlot = (props: {

const chartRef = React.useRef<HTMLDivElement>(null);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const INITIAL_END = Date.now();
const INITIAL_START = INITIAL_END - ONE_WEEK_MS;
// Calculate full available time window from session data
const [availableStart, availableEnd] = useMemo(() => {
const now = Date.now();
let minStart = Infinity;
let maxEnd = now;

if (simulationSessionMap && simulationSessionMap.size() > 0) {
simulationSessionMap.forEach((s: any) => {
if (s.start) {
const startMs = typeof s.start === 'number' ? s.start : new Date(s.start).getTime();
if (!isNaN(startMs) && startMs < minStart) {
minStart = startMs;
}
}
if (s.end) {
const endMs = typeof s.end === 'number' ? s.end : new Date(s.end).getTime();
if (!isNaN(endMs) && endMs > maxEnd) {
maxEnd = endMs;
}
}
});
}

const [domain, setDomain] = useState<[number, number]>([INITIAL_START, INITIAL_END]);
if (sortedCountKeys && sortedCountKeys.length > 0) {
const firstKey = sortedCountKeys[0];
const lastKey = sortedCountKeys[sortedCountKeys.length - 1];
if (firstKey < minStart) minStart = firstKey;
if (lastKey > maxEnd) maxEnd = lastKey;
}

if (minStart === Infinity) {
minStart = now - ONE_DAY_MS;
}

let start = minStart;
const end = maxEnd;

// Enforce minimum window of 1 day if runs are all within the last day or no valid runs
if (end - start < ONE_DAY_MS) {
start = end - ONE_DAY_MS;
}

return [start, end];
}, [simulationSessionMap, sortedCountKeys]);

const [domain, setDomain] = useState<[number, number]>([availableStart, availableEnd]);

// Sync default domain when availableStart or availableEnd changes, if not scoped to a single vehicleId and not zoomed manually
useEffect(() => {
if (!props.vehicleId && allowResize) {
setDomain([availableStart, availableEnd]);
}
}, [availableStart, availableEnd, props.vehicleId, allowResize]);

useEffect(() => {
if(!chartRef || !chartRef.current || !mouseX || !mouseY) return;
Expand Down Expand Up @@ -130,21 +180,31 @@ export const ActiveVehiclePlot = (props: {

}, [sortedCountKeys, activeCountMap, simulationSessionMap, props.vehicleId, allowResize]);

const getMidnightTicks = (start: number, end: number) => {
const getMidnightTicks = useCallback((start: number, end: number) => {
const ticks = [];
// Create a date object starting at the beginning of the domain
let current = new Date(start);

// Normalize to the start of the next UTC day
current.setUTCHours(24, 0, 0, 0);

const span = end - start;
let stepDays = 1;
if (span > 180 * ONE_DAY_MS) {
stepDays = 30;
} else if (span > 60 * ONE_DAY_MS) {
stepDays = 7;
} else if (span > 14 * ONE_DAY_MS) {
stepDays = 2;
}

while (current.getTime() <= end) {
ticks.push(current.getTime());
// Advance by exactly 24 hours
current.setUTCDate(current.getUTCDate() + 1);
// Advance by stepDays days
current.setUTCDate(current.getUTCDate() + stepDays);
}
return ticks;
};
}, []);

const isMultiDay = useMemo(() => {
const startDate = new Date(domain[0]);
Expand All @@ -170,7 +230,7 @@ export const ActiveVehiclePlot = (props: {
return getMidnightTicks(domain[0], domain[1]);
}
return undefined; // Fall back to automatic even spacing for small time spans
}, [domain]);
}, [domain, getMidnightTicks]);

const hoveredCount = useMemo(() => {
if (!msXPoint || !chartData || chartData.length === 0) return 0;
Expand Down Expand Up @@ -205,15 +265,16 @@ export const ActiveVehiclePlot = (props: {

// Extracted zoom logic for reuse between Wheel and Touch
const performZoom = useCallback((isZoomIn: boolean, anchor: number) => {
setAllowResize(false);
const [currentStart, currentEnd] = domain;
const zoomFactor = 0.1;

if (isZoomIn) {
let newStart = currentStart + (anchor - currentStart) * zoomFactor;
let newEnd = currentEnd - (currentEnd - anchor) * zoomFactor;

newStart = Math.max(INITIAL_START, newStart);
newEnd = Math.min(INITIAL_END, newEnd);
newStart = Math.max(availableStart, newStart);
newEnd = Math.min(availableEnd, newEnd);

// Prevent zooming in too far (e.g., closer than 1 minute)
if (newEnd - newStart > 60000) {
Expand All @@ -223,12 +284,12 @@ export const ActiveVehiclePlot = (props: {
let newStart = currentStart - (anchor - currentStart) * zoomFactor;
let newEnd = currentEnd + (currentEnd - anchor) * zoomFactor;

newStart = Math.max(INITIAL_START, newStart);
newEnd = Math.min(INITIAL_END, newEnd);
newStart = Math.max(availableStart, newStart);
newEnd = Math.min(availableEnd, newEnd);

setDomain([newStart, newEnd]);
}
}, [domain, INITIAL_END, INITIAL_START]);
}, [domain, availableStart, availableEnd]);

// Helper to format the title based on the viewable span
const getDynamicTitle = (start: number, end: number) => {
Expand Down
13 changes: 0 additions & 13 deletions apps/roadrunner-view/src/hooks/useSimulationSessionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,6 @@ export const useSimulationSessionData = () => {
useEffect(() => {
if (!isDataLoaded || (simulationSessionMapRef.current.size() === 0)) return;

// Remove any SimulationSessions that have timed out.
let clearList: string[] = [];
const msTimeout = new Date().getTime() - 7 * 24 * 60 * 60 * 1000;
simulationSessionMapRef.current.forEach((s: SimulationSession) => {
const msStart = new Date(s.start).getTime();
if (msStart < msTimeout) {
clearList.push(s.id);
}
});
clearList.forEach((id: string) => {
simulationSessionMapRef.current.delete(id);
})

// Generate a Map of session events
const eventMap = new MapWrapper<number, number>();
const msNow = new Date().getTime();
Expand Down
Loading