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

View File

@ -102,8 +102,7 @@ html, body {
}
}
.plot {
&:active {
cursor: grab;
}
.log-canvas {
color: @text;
--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 [fields, setFields] = useState<DatalogEntry[]>([]);
const [selectedFields, setSelectedFields] = useState<CheckboxValueType[]>([
'rpm',
// 'rpm',
'tps',
'afrTarget',
'afr',
@ -229,7 +229,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
<LogCanvas
data={loadedLogs || (logs!.records as Logs)}
width={canvasWidth}
height={600}
height={800}
selectedFields={prepareSelectedFields}
/>
:

View File

@ -3,21 +3,12 @@ import {
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Logs,
LogEntry,
} from '@speedy-tuner/types';
import {
scaleLinear,
max,
zoom,
zoomTransform,
select,
ZoomTransform,
} from 'd3';
import { seriesCanvasLine } from 'd3fc';
import TimeChart from 'timechart';
import { colorHsl } from '../../utils/number';
// enum Colors {
@ -58,8 +49,7 @@ export interface PlottableField {
};
const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [zoomState, setZoomState] = useState<ZoomTransform | null>(null);
const canvasRef = useRef<HTMLDivElement | null>(null);
const hsl = useCallback((fieldIndex: number, allFields: number) => {
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]);
// find max values for each selected field so we can calculate scale
// TODO: unused
const fieldsToPlot = useMemo(() => {
const temp: { [index: string]: PlottableField } = {};
@ -102,76 +93,43 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
return temp;
}, [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(() => {
const canvas = select(canvasRef.current);
const context = (canvas.node() as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D;
let chart: TimeChart;
context.clearRect(0, 0, width, height);
context.lineWidth = 2;
if (canvasRef.current) {
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) => {
const yScale = (() => {
const yField = (fieldsToPlot || {})[field.label] || { min: 0, max: 0 };
chart = new TimeChart(canvasRef.current as HTMLDivElement, {
series,
lineWidth: 2,
tooltip: true,
legend: false,
zoom: {
x: { autoRange: true },
y: { autoRange: true },
},
});
}
return scaleLinear()
.domain([yField.min, yField.max])
.range([height, 0]);
})();
seriesCanvasLine()
.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 () => {
if (chart) {
chart.dispose();
}
};
}, [data, fieldsToPlot, filtered, hsl, selectedFields, width, height]);
return (
<canvas
<div
ref={canvasRef}
width={width}
height={height}
style={{ width, height }}
className="log-canvas"
/>
);
};