-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: both rebound compression scatter and histogram use the same… #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,133 @@ | ||
| import React, { useMemo } from "react"; | ||
| import { | ||
| LineHistogram, | ||
| import { | ||
| LineHistogram, | ||
| LineHistogramSeries } from "../base/LineHistogram"; | ||
| import { RawSuspensionData } from '../../../lib/telemetryUtils'; | ||
| import { | ||
| buildVelocitySamples, | ||
| RawSuspensionData} from '../../../lib/telemetryUtils'; | ||
| processCompressions, | ||
| filterLowActivityOutliers, | ||
| } from "app/lib/run-analysis"; | ||
| import { getSeriesColor } from "app/lib/graphColors"; | ||
|
|
||
| interface VelocityHistogramSeries { | ||
| label: string; | ||
| rawData: RawSuspensionData[]; | ||
| freq: number; | ||
| fillColor?: string; | ||
| min?: number; | ||
| max?: number; | ||
| length?: number; | ||
| } | ||
|
|
||
| interface VelocityHistogramProps { | ||
| rawData?: RawSuspensionData[]; | ||
| freq?: number; | ||
| series?: { | ||
| label: string; | ||
| rawData: RawSuspensionData[]; | ||
| freq: number; | ||
| fillColor?: string; | ||
| min?: number; | ||
| max?: number; | ||
| }[]; | ||
| series: VelocityHistogramSeries[]; | ||
| title?: string; | ||
| fillColor?: string; | ||
| height?: number; | ||
| min?: number; | ||
| max?: number; | ||
| } | ||
|
|
||
| // Renders a histogram of velocity values | ||
| // Cache filtered velocities keyed by the stable rawData array reference, so the | ||
| // heavy processCompressions + filtering work is not repeated on every render. | ||
| const velocityCache = new WeakMap< | ||
| RawSuspensionData[], | ||
| { | ||
| freq: number; | ||
| length: number; | ||
| min: number; | ||
| max: number; | ||
| result: number[]; | ||
| } | ||
| >(); | ||
|
|
||
| // Builds the same stroke-velocity points (mm/s) the speed scatter plots use, | ||
| // so the histogram is a distribution view of identical, identically-filtered data. | ||
| function buildFilteredVelocities(seriesItem: VelocityHistogramSeries): number[] { | ||
| const length = seriesItem.length ?? 220; | ||
| const min = seriesItem.min ?? 0; | ||
| const max = seriesItem.max ?? 4096; | ||
| const freq = seriesItem.freq; | ||
|
|
||
| const cached = velocityCache.get(seriesItem.rawData); | ||
| if ( | ||
| cached && | ||
| cached.freq === freq && | ||
| cached.length === length && | ||
| cached.min === min && | ||
| cached.max === max | ||
| ) { | ||
| return cached.result; | ||
| } | ||
|
|
||
| const activities = processCompressions( | ||
| seriesItem.rawData, | ||
| freq, | ||
| length, | ||
| min, | ||
| max, | ||
| ); | ||
|
|
||
| // Filter compression and rebound subsets separately, exactly as the scatter | ||
| // does, then combine so the histogram bins precisely the scatter's points. | ||
| const kept = [ | ||
| ...filterLowActivityOutliers( | ||
| activities.filter((a) => a.type === "compression"), | ||
| length, | ||
| ), | ||
| ...filterLowActivityOutliers( | ||
| activities.filter((a) => a.type === "rebound"), | ||
| length, | ||
| ), | ||
| ]; | ||
|
|
||
| const result = kept.map((a) => a.velocity); | ||
| velocityCache.set(seriesItem.rawData, { freq, length, min, max, result }); | ||
| return result; | ||
| } | ||
|
|
||
| // Renders a histogram of suspension stroke speeds (mm/s) | ||
| export const VelocityHistogram: React.FC<VelocityHistogramProps> = ({ | ||
| rawData = [], | ||
| freq = 100, | ||
| series, | ||
| title = "Suspension Velocity", | ||
| title = "Suspension Speed", | ||
| fillColor = "hsl(var(--chart-1))", | ||
| height = 200, | ||
| min, | ||
| max, | ||
| }) => { | ||
| // buildVelocitySamples on rawData to get velocity samples | ||
| const histogramSeries = useMemo<LineHistogramSeries[]>(() => { | ||
| if (series && series.length > 0) { | ||
| return series.map((seriesItem, index) => ({ | ||
| label: seriesItem.label, | ||
| color: getSeriesColor(index, seriesItem.fillColor, fillColor), | ||
| data: buildVelocitySamples(seriesItem.rawData, seriesItem.freq, seriesItem.min, seriesItem.max).map(s => s.velocity), | ||
| })); | ||
| } | ||
|
|
||
| return [ | ||
| { | ||
| label: title, | ||
| color: fillColor, | ||
| data: buildVelocitySamples(rawData, freq, min, max).map(s => s.velocity), | ||
| }, | ||
| ]; | ||
| }, [series, rawData, freq, min, max, fillColor, title]); | ||
| const histogramSeries = useMemo<LineHistogramSeries[]>( | ||
| () => | ||
| series.map((seriesItem, index) => ({ | ||
| label: seriesItem.label, | ||
| color: getSeriesColor(index, seriesItem.fillColor, fillColor), | ||
| data: buildFilteredVelocities(seriesItem), | ||
| })), | ||
| [series, fillColor], | ||
| ); | ||
|
|
||
| // Keep layout stable | ||
| if (!histogramSeries.some((seriesItem) => seriesItem.data.length > 0)) { | ||
| return <div className="h-40 flex items-center justify-center text-gray-500 text-sm">No data</div>; | ||
| // Auto-scale the x-axis to the actual mm/s velocity range (symmetric about 0). | ||
| const xDomain = useMemo<[number, number]>(() => { | ||
| let maxAbs = 0; | ||
| for (const s of histogramSeries) { | ||
| for (const v of s.data) { | ||
| const abs = Math.abs(v); | ||
| if (abs > maxAbs) maxAbs = abs; | ||
| } | ||
| } | ||
| const bound = maxAbs > 0 ? maxAbs * 1.05 : 100; | ||
| return [-bound, bound]; | ||
| }, [histogramSeries]); | ||
|
|
||
| // Keep layout stable | ||
| if (!histogramSeries.some((seriesItem) => seriesItem.data.length > 0)) { | ||
| return <div className="h-40 flex items-center justify-center text-gray-500 text-sm">No data</div>; | ||
| } | ||
|
|
||
| return ( | ||
| return ( | ||
| <div className="w-full"> | ||
| <LineHistogram | ||
| <LineHistogram | ||
| series={histogramSeries} | ||
| xDomain={[-4000,4000]} | ||
| xDomain={xDomain} | ||
| height={height} | ||
| title={title} | ||
| binCount={50} | ||
| /> | ||
| /> | ||
| </div> | ||
| ); | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,52 @@ export interface SuspensionActivity extends VelocityReading { | |||||||||||||||||||||||||||||||||||||||||||||||||
| type: "rebound" | "compression" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Low-activity threshold fractions, expressed relative to the suspension's full travel. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export const MIN_VELOCITY_TRAVEL_FRACTION = 0.4 // 40% of full travel, used as mm/s | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export const MIN_DISPLACEMENT_TRAVEL_FRACTION = 0.03 // 3% of full travel, in mm | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| interface VelocityDisplacement { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| velocity: number // mm/s | ||||||||||||||||||||||||||||||||||||||||||||||||||
| displacement: number // mm of movement | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Removes statistical outliers (>5 std dev) and low-activity events (slow AND | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // small relative to full travel) from velocity/displacement data. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function filterLowActivityOutliers<T extends VelocityDisplacement>( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| items: T[], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fullTravel: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): T[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (items.length === 0) return items | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const velocities = items.map((a) => a.velocity) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const displacements = items.map((a) => a.displacement) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const mean = (arr: number[]) => arr.reduce((s, v) => s + v, 0) / arr.length | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const std = (arr: number[], m: number) => | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const vMean = mean(velocities) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const vStd = std(velocities, vMean) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dMean = mean(displacements) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dStd = std(displacements, dMean) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const minVelocity = MIN_VELOCITY_TRAVEL_FRACTION * fullTravel | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const minDisplacement = MIN_DISPLACEMENT_TRAVEL_FRACTION * fullTravel | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return items.filter((a) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const passesStdDev = | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (vStd === 0 || Math.abs(a.velocity - vMean) < 5 * vStd) && | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (dStd === 0 || Math.abs(a.displacement - dMean) < 5 * dStd) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Drop only low-activity points: slow AND small relative to full travel. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const isLowActivity = | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Math.abs(a.velocity) < minVelocity && | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Math.abs(a.displacement) < minDisplacement | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return passesStdDev && !isLowActivity | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If all elements in
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| function convertDisplacementToMm(reading: RawReading, suspensionLength: number, min: number, max: number): Reading { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const displacementPercentage = (reading.displacement - min) / (max - min) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
buildFilteredVelocitiesfunction performs heavy computations (includingprocessCompressionsand multiple array filtering/mapping operations) on telemetry data. Since theseriesprop is often recreated on every render by parent components,useMemowill re-run this function frequently, leading to UI lag. We can introduce aWeakMapcache keyed by the stablerawDataarray reference to cache the computed velocities and avoid redundant calculations.