Use TimeChart for plotting logs (#317)
This commit is contained in:
parent
3feb2b864e
commit
6e616cd85d
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -102,8 +102,7 @@ html, body {
|
|||
}
|
||||
}
|
||||
|
||||
.plot {
|
||||
&:active {
|
||||
cursor: grab;
|
||||
}
|
||||
.log-canvas {
|
||||
color: @text;
|
||||
--background-overlay: transparent;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
:
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue