Use TimeChart for plotting logs (#317)

This commit is contained in:
Piotr Rogowski 2021-12-14 22:46:23 +01:00 committed by GitHub
parent 3feb2b864e
commit 6e616cd85d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 99 additions and 1052 deletions

1027
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,6 @@
"@sentry/tracing": "^6.16.1", "@sentry/tracing": "^6.16.1",
"@speedy-tuner/types": "^0.2.1", "@speedy-tuner/types": "^0.2.1",
"antd": "^4.17.3", "antd": "^4.17.3",
"d3": "^7.0.4",
"d3fc": "^15.2.4",
"mlg-converter": "^0.5.1", "mlg-converter": "^0.5.1",
"pako": "^2.0.4", "pako": "^2.0.4",
"react": "^17.0.1", "react": "^17.0.1",
@ -45,7 +43,8 @@
"react-router-dom": "^5.2.1", "react-router-dom": "^5.2.1",
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
"react-table-drag-select": "^0.3.1", "react-table-drag-select": "^0.3.1",
"recharts": "^2.1.6" "recharts": "^2.1.8",
"timechart": "^1.0.0-beta.4"
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^6.4.3", "@craco/craco": "^6.4.3",

View File

@ -102,8 +102,7 @@ html, body {
} }
} }
.plot { .log-canvas {
&:active { color: @text;
cursor: grab; --background-overlay: transparent;
}
} }

View File

@ -88,7 +88,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
const [logs, setLogs] = useState<ParserResult>(); const [logs, setLogs] = useState<ParserResult>();
const [fields, setFields] = useState<DatalogEntry[]>([]); const [fields, setFields] = useState<DatalogEntry[]>([]);
const [selectedFields, setSelectedFields] = useState<CheckboxValueType[]>([ const [selectedFields, setSelectedFields] = useState<CheckboxValueType[]>([
'rpm', // 'rpm',
'tps', 'tps',
'afrTarget', 'afrTarget',
'afr', 'afr',
@ -229,7 +229,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
<LogCanvas <LogCanvas
data={loadedLogs || (logs!.records as Logs)} data={loadedLogs || (logs!.records as Logs)}
width={canvasWidth} width={canvasWidth}
height={600} height={800}
selectedFields={prepareSelectedFields} selectedFields={prepareSelectedFields}
/> />
: :

View File

@ -3,21 +3,12 @@ import {
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState,
} from 'react'; } from 'react';
import { import {
Logs, Logs,
LogEntry, LogEntry,
} from '@speedy-tuner/types'; } from '@speedy-tuner/types';
import { import TimeChart from 'timechart';
scaleLinear,
max,
zoom,
zoomTransform,
select,
ZoomTransform,
} from 'd3';
import { seriesCanvasLine } from 'd3fc';
import { colorHsl } from '../../utils/number'; import { colorHsl } from '../../utils/number';
// enum Colors { // enum Colors {
@ -58,8 +49,7 @@ export interface PlottableField {
}; };
const LogCanvas = ({ data, width, height, selectedFields }: Props) => { const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLDivElement | null>(null);
const [zoomState, setZoomState] = useState<ZoomTransform | null>(null);
const hsl = useCallback((fieldIndex: number, allFields: number) => { const hsl = useCallback((fieldIndex: number, allFields: number) => {
const [hue] = colorHsl(0, allFields - 1, fieldIndex); const [hue] = colorHsl(0, allFields - 1, fieldIndex);
@ -71,6 +61,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
const filtered = useMemo(() => data.filter(fieldsOnly), [data]); const filtered = useMemo(() => data.filter(fieldsOnly), [data]);
// find max values for each selected field so we can calculate scale // find max values for each selected field so we can calculate scale
// TODO: unused
const fieldsToPlot = useMemo(() => { const fieldsToPlot = useMemo(() => {
const temp: { [index: string]: PlottableField } = {}; const temp: { [index: string]: PlottableField } = {};
@ -102,76 +93,43 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
return temp; return temp;
}, [filtered, selectedFields]); }, [filtered, selectedFields]);
const xValue = useCallback((entry: LogEntry): number => (entry.Time || 0) as number, []);
const yValue = useCallback((entry: LogEntry, field: SelectedField): number => {
if (!(field.label in entry)) {
console.error(`Field [${field.label}] doesn't exist in this log file.`);
return 0;
}
return entry[field.label] as number;
}, []);
const xScale = useMemo(() => {
const tempXScale = scaleLinear()
.domain([0, max(filtered, xValue) as number])
.range([0, width]);
let newXScale = tempXScale;
if (zoomState) {
newXScale = zoomState.rescaleX(tempXScale);
tempXScale.domain(newXScale.domain());
}
return newXScale;
}, [filtered, width, xValue, zoomState]);
useEffect(() => { useEffect(() => {
const canvas = select(canvasRef.current); let chart: TimeChart;
const context = (canvas.node() as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D;
context.clearRect(0, 0, width, height); if (canvasRef.current) {
context.lineWidth = 2; const series = selectedFields.map((field) => ({
name: field.label,
color: hsl(selectedFields.indexOf(field), selectedFields.length),
data: data.map((entry) => ({
x: entry.Time as number,
y: entry[field.label] as number,
})).filter((entry) => entry.x !== undefined || entry.y !== undefined),
}));
const linesRaw = () => selectedFields.forEach((field, index) => { chart = new TimeChart(canvasRef.current as HTMLDivElement, {
const yScale = (() => { series,
const yField = (fieldsToPlot || {})[field.label] || { min: 0, max: 0 }; lineWidth: 2,
tooltip: true,
legend: false,
zoom: {
x: { autoRange: true },
y: { autoRange: true },
},
});
}
return scaleLinear() return () => {
.domain([yField.min, yField.max]) if (chart) {
.range([height, 0]); chart.dispose();
})(); }
};
seriesCanvasLine() }, [data, fieldsToPlot, filtered, hsl, selectedFields, width, height]);
.xScale(xScale)
.yScale(yScale)
.crossValue((entry: LogEntry) => xValue(entry))
.mainValue((entry: LogEntry) => yValue(entry, field))
.context(context)
// eslint-disable-next-line no-return-assign
.decorate((ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = hsl(index, selectedFields.length);
})(filtered);
});
const zoomed = () => setZoomState(zoomTransform(canvas.node() as any));
const zoomBehavior = zoom()
.scaleExtent([1, 1000]) // zoom boundaries
.translateExtent([[0, 0], [width, height]]) // pan boundaries
.extent([[0, 0], [width, height]])
.on('zoom', zoomed);
canvas.call(zoomBehavior as any);
linesRaw();
}, [data, fieldsToPlot, filtered, height, hsl, selectedFields, width, xScale, xValue, yValue]);
return ( return (
<canvas <div
ref={canvasRef} ref={canvasRef}
width={width} style={{ width, height }}
height={height} className="log-canvas"
/> />
); );
}; };