Display second log viewer (#327)
This commit is contained in:
parent
f3e9095861
commit
2e485d0493
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/icons/icon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1" />
|
||||
<meta name="theme-color" content="#222629" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
@ -38,7 +37,7 @@ import {
|
|||
DatalogEntry,
|
||||
} from '@speedy-tuner/types';
|
||||
import { loadLogs } from '../utils/api';
|
||||
import LogCanvas, { SelectedField } from './Log/LogCanvas';
|
||||
import LogCanvas2 from './Log/LogCanvas2';
|
||||
import store from '../store';
|
||||
import {
|
||||
formatBytes,
|
||||
|
@ -72,8 +71,16 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const margin = 30;
|
||||
const [canvasWidth, setCanvasWidth] = useState(0);
|
||||
const [canvasHeight, setCanvasHeight] = useState(0);
|
||||
const sidebarWidth = 250;
|
||||
const calculateCanvasWidth = useCallback(() => setCanvasWidth((contentRef.current?.clientWidth || 0) - margin), []);
|
||||
const calculateCanvasSize = useCallback(() => {
|
||||
setCanvasWidth((contentRef.current?.clientWidth || 0) - margin);
|
||||
|
||||
if (window.innerHeight > 600) {
|
||||
setCanvasHeight(Math.round((window.innerHeight - 250) / 2));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const siderProps = {
|
||||
width: sidebarWidth,
|
||||
collapsible: true,
|
||||
|
@ -81,23 +88,26 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
collapsed: ui.sidebarCollapsed,
|
||||
onCollapse: (collapsed: boolean) => {
|
||||
store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed });
|
||||
setTimeout(calculateCanvasWidth, 1);
|
||||
setTimeout(calculateCanvasSize, 1);
|
||||
},
|
||||
};
|
||||
const [logs, setLogs] = useState<ParserResult>();
|
||||
const [fields, setFields] = useState<DatalogEntry[]>([]);
|
||||
const [selectedFields, setSelectedFields] = useState<CheckboxValueType[]>([
|
||||
// 'rpm',
|
||||
const [selectedFields1, setSelectedFields1] = useState<CheckboxValueType[]>([
|
||||
'rpm',
|
||||
'tps',
|
||||
'map',
|
||||
]);
|
||||
const [selectedFields2, setSelectedFields2] = useState<CheckboxValueType[]>([
|
||||
'afrTarget',
|
||||
'afr',
|
||||
'map',
|
||||
'dwell',
|
||||
]);
|
||||
const {
|
||||
isConfigReady,
|
||||
findOutputChannel,
|
||||
} = useConfig(config);
|
||||
const prepareSelectedFields = useMemo<SelectedField[]>(() => {
|
||||
const prepareSelectedFields = useCallback((selectedFields: CheckboxValueType[]) => {
|
||||
if (!isConfigReady) {
|
||||
return [];
|
||||
}
|
||||
|
@ -121,7 +131,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
};
|
||||
}).filter((val) => !!val);
|
||||
|
||||
}, [config.datalog, findOutputChannel, isConfigReady, selectedFields]);
|
||||
}, [config.datalog, findOutputChannel, isConfigReady]);
|
||||
|
||||
useEffect(() => {
|
||||
const worker = new MlgParserWorker();
|
||||
|
@ -176,16 +186,16 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
setFields(Object.values(config.datalog));
|
||||
}
|
||||
|
||||
calculateCanvasWidth();
|
||||
calculateCanvasSize();
|
||||
|
||||
window.addEventListener('resize', calculateCanvasWidth);
|
||||
window.addEventListener('resize', calculateCanvasSize);
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
worker.terminate();
|
||||
window.removeEventListener('resize', calculateCanvasWidth);
|
||||
window.removeEventListener('resize', calculateCanvasSize);
|
||||
};
|
||||
}, [calculateCanvasWidth, config.datalog, config.outputChannels, loadedLogs]);
|
||||
}, [calculateCanvasSize, config.datalog, config.outputChannels, loadedLogs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -196,18 +206,35 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
!ui.sidebarCollapsed &&
|
||||
<Tabs defaultActiveKey="fields" style={{ marginLeft: 20 }}>
|
||||
<TabPane tab={<EditOutlined />} key="fields">
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
<Checkbox.Group onChange={setSelectedFields} value={selectedFields}>
|
||||
{fields.map((field) => (
|
||||
<Row key={field.name}>
|
||||
<Checkbox key={field.name} value={field.name}>
|
||||
{field.label}
|
||||
{/* {field.units && ` (${field.units})`} */}
|
||||
</Checkbox>
|
||||
</Row>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</PerfectScrollbar>
|
||||
<div style={{ height: '45%' }}>
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
<Checkbox.Group onChange={setSelectedFields1} value={selectedFields1}>
|
||||
{fields.map((field) => (
|
||||
<Row key={field.name}>
|
||||
<Checkbox key={field.name} value={field.name}>
|
||||
{field.label}
|
||||
{/* {field.units && ` (${field.units})`} */}
|
||||
</Checkbox>
|
||||
</Row>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ height: '45%' }}>
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
<Checkbox.Group onChange={setSelectedFields2} value={selectedFields2}>
|
||||
{fields.map((field) => (
|
||||
<Row key={field.name}>
|
||||
<Checkbox key={field.name} value={field.name}>
|
||||
{field.label}
|
||||
{/* {field.units && ` (${field.units})`} */}
|
||||
</Checkbox>
|
||||
</Row>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab={<FileTextOutlined />} key="files">
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
|
@ -222,11 +249,12 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
|
|||
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
|
||||
{logs || !!loadedLogs.length
|
||||
?
|
||||
<LogCanvas
|
||||
<LogCanvas2
|
||||
data={loadedLogs || (logs!.records as Logs)}
|
||||
width={canvasWidth}
|
||||
height={canvasWidth * 0.4}
|
||||
selectedFields={prepareSelectedFields}
|
||||
height={canvasHeight}
|
||||
selectedFields1={prepareSelectedFields(selectedFields1)}
|
||||
selectedFields2={prepareSelectedFields(selectedFields2)}
|
||||
/>
|
||||
:
|
||||
<Space
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Logs } from '@speedy-tuner/types';
|
||||
import {
|
||||
Grid,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import UplotReact from 'uplot-react';
|
||||
import uPlot from 'uplot';
|
||||
import { colorHsl } from '../../utils/number';
|
||||
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
||||
import { Colors } from '../../utils/colors';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
|
||||
import 'uplot/dist/uPlot.min.css';
|
||||
import { isNumber } from '../../utils/tune/expression';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export interface SelectedField {
|
||||
name: string;
|
||||
label: string;
|
||||
units: string;
|
||||
scale: string | number;
|
||||
transform: string | number;
|
||||
format: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: Logs;
|
||||
width: number;
|
||||
height: number;
|
||||
selectedFields1: SelectedField[];
|
||||
selectedFields2: SelectedField[];
|
||||
};
|
||||
|
||||
export interface PlottableField {
|
||||
min: number;
|
||||
max: number;
|
||||
scale: number;
|
||||
transform: number;
|
||||
units: string;
|
||||
format: string;
|
||||
};
|
||||
|
||||
const LogCanvas2 = ({ data, width, height, selectedFields1, selectedFields2 }: Props) => {
|
||||
const { sm } = useBreakpoint();
|
||||
const hsl = useCallback((fieldIndex: number, allFields: number) => {
|
||||
const [hue] = colorHsl(0, allFields - 1, fieldIndex);
|
||||
return `hsl(${hue}, 90%, 50%)`;
|
||||
}, []);
|
||||
const [options1, setOptions1] = useState<uPlot.Options>();
|
||||
const [plotData1, setPlotData1] = useState<uPlot.AlignedData>();
|
||||
const [options2, setOptions2] = useState<uPlot.Options>();
|
||||
const [plotData2, setPlotData2] = useState<uPlot.AlignedData>();
|
||||
|
||||
const generateFieldsToPlot = useCallback((selectedFields: SelectedField[]) => {
|
||||
const temp: { [index: string]: PlottableField } = {};
|
||||
|
||||
data.forEach((entry) => {
|
||||
selectedFields.forEach(({ label, scale, transform, units, format }) => {
|
||||
const value = entry[label];
|
||||
|
||||
if (!temp[label]) {
|
||||
temp[label] = {
|
||||
min: 0,
|
||||
max: 0,
|
||||
scale: (scale || 1) as number,
|
||||
transform: (transform || 0) as number,
|
||||
units: units || '',
|
||||
format: format || '',
|
||||
};
|
||||
}
|
||||
|
||||
if (value > temp[label].max) {
|
||||
temp[label].max = entry[label] as number;
|
||||
}
|
||||
|
||||
if (value < temp[label].min) {
|
||||
temp[label].min = entry[label] as number;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return temp;
|
||||
}, [data]);
|
||||
|
||||
const generatePlotConfig = useCallback((fieldsToPlot: { [index: string]: PlottableField }, selectedFieldsLength: number, plotSyncKey: string) => {
|
||||
const dataSeries: uPlot.Series[] = [];
|
||||
const xData: number[] = [];
|
||||
const yData: (number | null)[][] = [];
|
||||
|
||||
Object.keys(fieldsToPlot).forEach((label, index) => {
|
||||
const field = fieldsToPlot[label];
|
||||
|
||||
dataSeries.push({
|
||||
label: field.units ? `${label} (${field.units})` : label,
|
||||
points: { show: false },
|
||||
stroke: hsl(index, selectedFieldsLength),
|
||||
scale: field.units,
|
||||
width: 2,
|
||||
value: (_self, val) => isNumber(val) ? val.toFixed(2) : 0,
|
||||
});
|
||||
|
||||
data.forEach((entry) => {
|
||||
if (entry.type !== 'marker') {
|
||||
xData.push(entry.Time as number);
|
||||
|
||||
let value = entry[label];
|
||||
|
||||
if (value !== undefined) {
|
||||
value = (value as number * field.scale) + field.transform;
|
||||
}
|
||||
|
||||
if (!yData[index]) {
|
||||
yData[index] = [];
|
||||
}
|
||||
|
||||
yData[index].push(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
xData,
|
||||
yData,
|
||||
options: {
|
||||
width,
|
||||
height,
|
||||
scales: { x: { time: false } },
|
||||
series: [
|
||||
{ label: 'Time (s)' },
|
||||
...dataSeries,
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
stroke: Colors.TEXT,
|
||||
grid: { stroke: Colors.MAIN_LIGHT },
|
||||
},
|
||||
],
|
||||
cursor: {
|
||||
drag: { y: false },
|
||||
sync: {
|
||||
key: plotSyncKey,
|
||||
},
|
||||
},
|
||||
plugins: [touchZoomPlugin()],
|
||||
},
|
||||
};
|
||||
}, [data, height, hsl, width]);
|
||||
|
||||
useEffect(() => {
|
||||
const plotSync = uPlot.sync('logs');
|
||||
|
||||
const result1 = generatePlotConfig(generateFieldsToPlot(selectedFields1), selectedFields1.length, plotSync.key);
|
||||
setOptions1(result1.options);
|
||||
setPlotData1([result1.xData, ...result1.yData]);
|
||||
|
||||
const result2 = generatePlotConfig(generateFieldsToPlot(selectedFields2), selectedFields2.length, plotSync.key);
|
||||
setOptions2(result2.options);
|
||||
setPlotData2([result2.xData, ...result2.yData]);
|
||||
|
||||
}, [data, hsl, width, height, sm, generatePlotConfig, generateFieldsToPlot, selectedFields1, selectedFields2]);
|
||||
|
||||
if (!sm) {
|
||||
return <LandscapeNotice />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large">
|
||||
<UplotReact
|
||||
options={options1!}
|
||||
data={plotData1!}
|
||||
/>
|
||||
<UplotReact
|
||||
options={options2!}
|
||||
data={plotData2!}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogCanvas2;
|
|
@ -5,6 +5,7 @@ import {
|
|||
import { Grid } from 'antd';
|
||||
import UplotReact from 'uplot-react';
|
||||
import uPlot from 'uplot';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
||||
import {
|
||||
ToothLogEntry,
|
||||
|
@ -12,7 +13,6 @@ import {
|
|||
} from '../../utils/logs/TriggerLogsParser';
|
||||
import CanvasHelp from '../CanvasHelp';
|
||||
import { Colors } from '../../utils/colors';
|
||||
import touchZoomPlugin from '../../utils/uPlot/touchZoomPlugin';
|
||||
|
||||
import 'uplot/dist/uPlot.min.css';
|
||||
|
||||
|
@ -63,7 +63,7 @@ const ToothCanvas = ({ data, width, height }: Props) => {
|
|||
stroke: Colors.ACCENT,
|
||||
fill: Colors.ACCENT,
|
||||
scale: 'toothTime',
|
||||
value: (self, rawValue) => `${rawValue.toLocaleString()}μs`,
|
||||
value: (_self, rawValue) => `${rawValue.toLocaleString()}μs`,
|
||||
paths: bars!({ size: [0.6, 100] }),
|
||||
},
|
||||
],
|
||||
|
|
|
@ -47,8 +47,8 @@ const touchZoomPlugin = () => {
|
|||
const yMax = Math.max(t0y, t1y);
|
||||
|
||||
// mid points
|
||||
t.y = (yMin+yMax)/2;
|
||||
t.x = (xMin+xMax)/2;
|
||||
t.y = (yMin + yMax) / 2;
|
||||
t.x = (xMin + xMax) / 2;
|
||||
|
||||
t.dx = xMax - xMin;
|
||||
t.dy = yMax - yMin;
|
||||
|
@ -74,8 +74,8 @@ const touchZoomPlugin = () => {
|
|||
const xFactor = fr.d! / to.d!;
|
||||
const yFactor = fr.d! / to.d!;
|
||||
|
||||
const leftPct = left/rect.width;
|
||||
const btmPct = 1 - top/rect.height;
|
||||
const leftPct = left / rect.width;
|
||||
const btmPct = 1 - top / rect.height;
|
||||
|
||||
const nxRange = oxRange * xFactor;
|
||||
const nxMin = xVal - leftPct * nxRange;
|
||||
|
|
Loading…
Reference in New Issue