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
5 changes: 0 additions & 5 deletions src/TrackLabConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,3 @@ export const MIN_CALIB_DISTANCE = 1e-9; // minimum real-world calibration distan
// ── Webcam panel ──────────────────────────────────────────────────────────────
export const WEBCAM_PREVIEW_WIDTH = 480; // width of the preview and review video elements
export const WEBCAM_PREVIEW_HEIGHT = 270; // height of the preview and review video elements

// ── Graph decimation ─────────────────────────────────────────────────────────
// Controls how many sub-step data points are skipped when plotting to reduce
// rendering overhead while maintaining visual fidelity.
export const SUB_STEP_DECIMATION = 1; // keep every Nth point (1 = no decimation)
70 changes: 14 additions & 56 deletions src/screen-name/graph/ConfigurableGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,11 @@ import {
} from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
import TrackLabColors from "../../TrackLabColors.js";
import { SUB_STEP_DECIMATION } from "../../TrackLabConstants.js";
import trackLab from "../../TrackLabNamespace.js";
import GraphControlsPanel from "./GraphControlsPanel.js";
import GraphDataManager from "./GraphDataManager.js";
import GraphInteractionHandler from "./GraphInteractionHandler.js";
import type {
PlottableProperty,
SubStepDataPoint,
} from "./PlottableProperty.js";
import type { PlottableProperty } from "./PlottableProperty.js";

// Grid line styling
const GRID_LINE_WIDTH = 0.5;
Expand Down Expand Up @@ -101,9 +97,6 @@ export default class ConfigurableGraph extends Node {
// Title panel with combo boxes (needs to be on top of header bar)
private readonly titlePanel: Node;

// Sub-step decimation counter for high-resolution data
private decimationCounter: number = 0;

// Drag and resize state properties (kept for dispose)
private readonly isDraggingProperty: BooleanProperty;
private readonly isResizingProperty: BooleanProperty;
Expand Down Expand Up @@ -623,60 +616,28 @@ export default class ConfigurableGraph extends Node {
this.interactionHandler.updateResizeHandlePositions();
}

/**
* Add a new data point based on current property values.
* @deprecated Not currently called anywhere — superseded by addDataPointsFromSubSteps.
*/
public addDataPoint(): void {
const xValue = this.xPropertyProperty.value.property?.value;
const yValue = this.yPropertyProperty.value.property?.value;
if (xValue === undefined || yValue === undefined) return;

this.dataManager.addDataPoint(xValue, yValue);
}

/**
* Clear all data points
*/
public clearData(): void {
this.dataManager.clearData();
this.decimationCounter = 0;
}

/**
* Add data points from sub-step data collected during ODE integration.
* Maps the sub-step data to the currently selected x and y axes.
* Uses decimation to prevent memory overflow while maintaining smooth curves.
* @param subStepData - Array of sub-step data points from the model
* Add data points from a record array, mapping each record to the selected axes.
*/
public addDataPointsFromSubSteps(subStepData: SubStepDataPoint[]): void {
if (subStepData.length === 0) return;
public addDataPoints(dataPoints: Array<Record<string, number>>): void {
if (dataPoints.length === 0) return;

const xProperty = this.xPropertyProperty.value;
const yProperty = this.yPropertyProperty.value;

// Map sub-step data to x/y values with decimation
const mappedPoints: Array<{ x: number; y: number }> = [];
const decimation = SUB_STEP_DECIMATION;

for (const point of subStepData) {
this.decimationCounter++;

// Only keep every Nth point
if (this.decimationCounter >= decimation) {
this.decimationCounter = 0;

const x = this.getValueForAxis(xProperty, point);
const y = this.getValueForAxis(yProperty, point);

if (
x !== null &&
y !== null &&
Number.isFinite(x) &&
Number.isFinite(y)
) {
mappedPoints.push({ x, y });
}
for (const point of dataPoints) {
const x = this.getValueForAxis(xProperty, point);
const y = this.getValueForAxis(yProperty, point);
if (x !== null && y !== null && Number.isFinite(x) && Number.isFinite(y)) {
mappedPoints.push({ x, y });
}
}

Expand All @@ -686,19 +647,16 @@ export default class ConfigurableGraph extends Node {
}

/**
* Get the value for a specific axis from a sub-step data point.
* Uses the type-safe subStepAccessor when available, otherwise falls back
* to the current property value for derived quantities (energy, RMS, etc.).
* Get the value for a specific axis from a data point record.
* Uses the accessor when available, otherwise falls back to property.value.
*/
private getValueForAxis(
axisProperty: PlottableProperty,
point: SubStepDataPoint,
point: Record<string, number>,
): number | null {
if (axisProperty.subStepAccessor) {
return axisProperty.subStepAccessor(point);
if (axisProperty.accessor) {
return axisProperty.accessor(point);
}
// For properties without sub-step data, fall back to current property value.
// This handles derived properties like energy, RMS values, etc.
return axisProperty.property?.value ?? null;
}

Expand Down
4 changes: 2 additions & 2 deletions src/screen-name/graph/GraphDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ export default class GraphDataManager {
}

/**
* Add multiple data points at once (for sub-step data).
* Add multiple data points at once.
* More efficient than calling addDataPoint repeatedly.
* @param points - Array of [x, y] value pairs
* @param points - Array of x/y value pairs
*/
public addDataPoints(points: Array<{ x: number; y: number }>): void {
if (points.length === 0) return;
Expand Down
17 changes: 4 additions & 13 deletions src/screen-name/graph/PlottableProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,19 @@

import type { TReadOnlyProperty } from "scenerystack/axon";

/**
* A data point from sub-step simulation data.
* This is a flexible type that can hold any numeric data keyed by string names.
*/
export type SubStepDataPoint = Record<string, number>;

export type PlottableProperty = {
// The name to display in the selector (can be a string or a localized string property)
name: string | TReadOnlyProperty<string>;

// The property to read values from.
// Required when subStepAccessor is absent; may be omitted when subStepAccessor
// covers all usage paths (e.g. kinematic variables that are always pushed via
// addDataPointsFromSubSteps rather than polled with addDataPoint).
// Required when accessor is absent.
property?: TReadOnlyProperty<number>;

// Optional unit string for axis label (e.g., "m", "m/s", "J")
// Can be a static string or a dynamic property for units that depend on calibration
unit?: string | TReadOnlyProperty<string>;

// Optional accessor for extracting this property's value from sub-step data.
// When provided, high-resolution sub-step data is used for smooth phase-space plots.
// When absent, falls back to the current property value.
subStepAccessor?: (point: SubStepDataPoint) => number;
// Optional accessor for extracting this property's value from a data point record.
// When provided, data is extracted from the record; when absent, falls back to property.value.
accessor?: (point: Record<string, number>) => number;
};
25 changes: 6 additions & 19 deletions src/screen-name/view/KinematicsGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,20 @@ import { HBox, Node, Text, VBox } from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
import { ComboBox, type ComboBoxItem } from "scenerystack/sun";
import ConfigurableGraph from "../graph/ConfigurableGraph.js";
import type {
PlottableProperty,
SubStepDataPoint,
} from "../graph/PlottableProperty.js";
import type { PlottableProperty } from "../graph/PlottableProperty.js";
import type { SimModel } from "../model/SimModel.js";

// Graph dimensions
const GRAPH_WIDTH = 300;
const GRAPH_HEIGHT = 200;
const MAX_DATA_POINTS = 5000;

/**
* Creates a PlottableProperty for a kinematic variable driven entirely by
* subStepAccessor. No backing Property is needed because KinematicsGraphNode
* always feeds data via addDataPointsFromSubSteps rather than addDataPoint.
*/
function createPlottableProperty(
name: string,
unit: string | TReadOnlyProperty<string>,
accessor: (point: SubStepDataPoint) => number,
accessor: (point: Record<string, number>) => number,
): PlottableProperty {
return {
name,
unit,
subStepAccessor: accessor,
};
return { name, unit, accessor };
}

export class KinematicsGraphNode extends VBox {
Expand Down Expand Up @@ -282,8 +270,7 @@ export class KinematicsGraphNode extends VBox {

if (!trackData || trackData.points.length === 0) return;

// Convert kinematic points to SubStepDataPoint format for the graph
const subStepData: SubStepDataPoint[] = trackData.points.map((pt) => ({
const dataPoints = trackData.points.map((pt) => ({
t: pt.time,
x: pt.x,
y: pt.y,
Expand All @@ -295,8 +282,8 @@ export class KinematicsGraphNode extends VBox {
aMag: pt.accelerationMagnitude ?? Number.NaN,
}));

if (subStepData.length > 0) {
this.graph.addDataPointsFromSubSteps(subStepData);
if (dataPoints.length > 0) {
this.graph.addDataPoints(dataPoints);
}
}
}