Base work for tooth logs (#325)
This commit is contained in:
parent
0f1d35093c
commit
117ab6615d
|
@ -24,7 +24,9 @@
|
||||||
"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.8",
|
"recharts": "^2.1.8",
|
||||||
"timechart": "^1.0.0-beta.4"
|
"timechart": "^1.0.0-beta.4",
|
||||||
|
"uplot": "^1.6.18",
|
||||||
|
"uplot-react": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^6.4.3",
|
"@craco/craco": "^6.4.3",
|
||||||
|
@ -21479,6 +21481,23 @@
|
||||||
"yarn": "*"
|
"yarn": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uplot": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ=="
|
||||||
|
},
|
||||||
|
"node_modules/uplot-react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.6",
|
||||||
|
"uplot": "^1.6.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
@ -40124,6 +40143,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
||||||
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
|
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
|
||||||
},
|
},
|
||||||
|
"uplot": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ=="
|
||||||
|
},
|
||||||
|
"uplot-react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"uri-js": {
|
"uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
|
|
@ -45,7 +45,9 @@
|
||||||
"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.8",
|
"recharts": "^2.1.8",
|
||||||
"timechart": "^1.0.0-beta.4"
|
"timechart": "^1.0.0-beta.4",
|
||||||
|
"uplot": "^1.6.18",
|
||||||
|
"uplot-react": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^6.4.3",
|
"@craco/craco": "^6.4.3",
|
||||||
|
|
|
@ -123,6 +123,7 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms
|
||||||
0.0,0.0,0.0,1.0,564200.94,564200.94,35.864,564200.94
|
0.0,0.0,0.0,1.0,564200.94,564200.94,35.864,564200.94
|
||||||
0.0,1.0,1.0,1.0,564250.3,564250.3,49.376,564250.3
|
0.0,1.0,1.0,1.0,564250.3,564250.3,49.376,564250.3
|
||||||
1.0,1.0,0.0,1.0,564258.0,564258.0,7.728,564258.0
|
1.0,1.0,0.0,1.0,564258.0,564258.0,7.728,564258.0
|
||||||
|
MARK 000
|
||||||
1.0,0.0,1.0,1.0,564286.5,564286.5,28.484,564286.5
|
1.0,0.0,1.0,1.0,564286.5,564286.5,28.484,564286.5
|
||||||
0.0,0.0,0.0,1.0,564294.0,564294.0,7.468,564294.0
|
0.0,0.0,0.0,1.0,564294.0,564294.0,7.468,564294.0
|
||||||
1.0,0.0,0.0,1.0,564351.06,564351.06,57.056,564351.06
|
1.0,0.0,0.0,1.0,564351.06,564351.06,57.056,564351.06
|
||||||
|
@ -382,4 +383,4 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms
|
||||||
1.0,0.0,0.0,1.0,568452.75,568452.75,57.408,568452.75
|
1.0,0.0,0.0,1.0,568452.75,568452.75,57.408,568452.75
|
||||||
0.0,0.0,0.0,1.0,568488.8,568488.8,36.024,568488.8
|
0.0,0.0,0.0,1.0,568488.8,568488.8,36.024,568488.8
|
||||||
0.0,1.0,1.0,1.0,568538.44,568538.44,49.624,568538.44
|
0.0,1.0,1.0,1.0,568538.44,568538.44,49.624,568538.44
|
||||||
MARK 000
|
MARK 001
|
||||||
|
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,29 @@
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const CanvasHelp = () => (
|
||||||
|
<div style={{ marginTop: -20, marginBottom: 10, textAlign: 'left', marginLeft: 20 }}>
|
||||||
|
<Popover
|
||||||
|
placement="bottom"
|
||||||
|
content={
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Title level={5}>Navigation</Title>
|
||||||
|
<Text>Pinch to zoom</Text>
|
||||||
|
<Text>Drag to pan</Text>
|
||||||
|
<Text>Ctrl + wheel scroll to zoom X axis</Text>
|
||||||
|
<Text>Hold Shift to speed up zoom 5 times</Text>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CanvasHelp;
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Steps,
|
Steps,
|
||||||
Space,
|
Space,
|
||||||
Divider,
|
Divider,
|
||||||
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
@ -28,15 +29,23 @@ import {
|
||||||
Config,
|
Config,
|
||||||
Logs,
|
Logs,
|
||||||
} from '@speedy-tuner/types';
|
} from '@speedy-tuner/types';
|
||||||
import { loadCompositeLogs } from '../utils/api';
|
import {
|
||||||
|
loadCompositeLogs,
|
||||||
|
loadToothLogs,
|
||||||
|
} from '../utils/api';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import { formatBytes } from '../utils/number';
|
import { formatBytes } from '../utils/number';
|
||||||
import CompositeCanvas from './TriggerLog/CompositeCanvas';
|
import CompositeCanvas from './TriggerLog/CompositeCanvas';
|
||||||
import { isNumber } from '../utils/tune/expression';
|
import TriggerLogsParser, {
|
||||||
|
CompositeLogEntry,
|
||||||
|
ToothLogEntry,
|
||||||
|
} from '../utils/logs/TriggerLogsParser';
|
||||||
|
import ToothCanvas from './TriggerLog/ToothCanvas';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
|
|
||||||
const edgeUnknown = 'Unknown';
|
const edgeUnknown = 'Unknown';
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => ({
|
const mapStateToProps = (state: AppState) => ({
|
||||||
|
@ -46,19 +55,6 @@ const mapStateToProps = (state: AppState) => ({
|
||||||
loadedLogs: state.logs,
|
loadedLogs: state.logs,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: extract this to types package
|
|
||||||
interface CompositeLogEntry {
|
|
||||||
type: 'trigger' | 'marker';
|
|
||||||
primaryLevel: number;
|
|
||||||
secondaryLevel: number;
|
|
||||||
trigger: number;
|
|
||||||
sync: number;
|
|
||||||
refTime: number;
|
|
||||||
maxTime: number;
|
|
||||||
toothTime: number;
|
|
||||||
time: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLogs: Logs }) => {
|
const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLogs: Logs }) => {
|
||||||
const { lg } = useBreakpoint();
|
const { lg } = useBreakpoint();
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
@ -83,6 +79,7 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [logs, setLogs] = useState<CompositeLogEntry[]>();
|
const [logs, setLogs] = useState<CompositeLogEntry[]>();
|
||||||
|
const [toothLogs, setToothLogs] = useState<ToothLogEntry[]>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
@ -90,79 +87,24 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const raw = await loadCompositeLogs((percent, total, edge) => {
|
const compositeRaw = await loadCompositeLogs((percent, total, edge) => {
|
||||||
setProgress(percent);
|
setProgress(percent);
|
||||||
setFileSize(formatBytes(total));
|
setFileSize(formatBytes(total));
|
||||||
setEdgeLocation(edge || edgeUnknown);
|
setEdgeLocation(edge || edgeUnknown);
|
||||||
}, signal);
|
}, signal);
|
||||||
|
|
||||||
setFileSize(formatBytes(raw.byteLength));
|
const toothRaw = await loadToothLogs(undefined, signal);
|
||||||
|
|
||||||
const buff = pako.inflate(new Uint8Array(raw));
|
|
||||||
const string = (new TextDecoder()).decode(buff);
|
|
||||||
const result: CompositeLogEntry[] = [];
|
|
||||||
|
|
||||||
|
setFileSize(formatBytes(compositeRaw.byteLength));
|
||||||
setStep(1);
|
setStep(1);
|
||||||
|
|
||||||
// TODO: extract this, make a parser class
|
const parser = new TriggerLogsParser();
|
||||||
string.split('\n').forEach((line, index) => {
|
const resultComposite = parser.parse(pako.inflate(new Uint8Array(compositeRaw))).getCompositeLogs();
|
||||||
const trimmed = line.trim();
|
const resultTooth = parser.parse(pako.inflate(new Uint8Array(toothRaw))).getToothLogs();
|
||||||
|
|
||||||
// skip comments
|
setLogs(resultComposite);
|
||||||
if (trimmed.startsWith('#')) {
|
setToothLogs(resultTooth);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// markers
|
|
||||||
if (trimmed.startsWith('MARK')) {
|
|
||||||
const previous = result[result.length - 1] || {
|
|
||||||
primaryLevel: 0,
|
|
||||||
secondaryLevel: 0,
|
|
||||||
trigger: 0,
|
|
||||||
sync: 0,
|
|
||||||
refTime: 0,
|
|
||||||
maxTime: 0,
|
|
||||||
toothTime: 0,
|
|
||||||
time: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
type: 'marker',
|
|
||||||
primaryLevel: previous.primaryLevel,
|
|
||||||
secondaryLevel: previous.secondaryLevel,
|
|
||||||
trigger: previous.trigger,
|
|
||||||
sync: previous.sync,
|
|
||||||
refTime: previous.refTime,
|
|
||||||
maxTime: previous.maxTime,
|
|
||||||
toothTime: previous.toothTime,
|
|
||||||
time: previous.time,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const split = trimmed.split(',');
|
|
||||||
if (!isNumber(split[0])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = Number(split[7]);
|
|
||||||
if (!time) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
type: 'trigger',
|
|
||||||
primaryLevel: Number(split[0]),
|
|
||||||
secondaryLevel: Number(split[1]),
|
|
||||||
trigger: Number(split[2]),
|
|
||||||
sync: Number(split[3]),
|
|
||||||
refTime: Number(split[4]),
|
|
||||||
maxTime: Number(split[5]),
|
|
||||||
toothTime: Number(split[6]),
|
|
||||||
time,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setLogs(result);
|
|
||||||
setStep(2);
|
setStep(2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFetchError(error as Error);
|
setFetchError(error as Error);
|
||||||
|
@ -191,7 +133,8 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
|
||||||
<Tabs defaultActiveKey="files" style={{ marginLeft: 20 }}>
|
<Tabs defaultActiveKey="files" style={{ marginLeft: 20 }}>
|
||||||
<TabPane tab={<FileTextOutlined />} key="files">
|
<TabPane tab={<FileTextOutlined />} key="files">
|
||||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||||
composite.csv
|
<Typography.Paragraph>tooth.csv</Typography.Paragraph>
|
||||||
|
<Typography.Paragraph>composite.csv</Typography.Paragraph>
|
||||||
</PerfectScrollbar>
|
</PerfectScrollbar>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -200,13 +143,22 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
|
||||||
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
|
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
|
||||||
<Content>
|
<Content>
|
||||||
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
|
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
|
||||||
{logs
|
{toothLogs && logs
|
||||||
?
|
?
|
||||||
|
(
|
||||||
|
<>
|
||||||
|
<ToothCanvas
|
||||||
|
data={toothLogs!}
|
||||||
|
width={canvasWidth}
|
||||||
|
height={canvasWidth * 0.3}
|
||||||
|
/>
|
||||||
<CompositeCanvas
|
<CompositeCanvas
|
||||||
data={logs!}
|
data={logs!}
|
||||||
width={canvasWidth}
|
width={canvasWidth}
|
||||||
height={canvasWidth * 0.4}
|
height={canvasWidth * 0.3}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
:
|
:
|
||||||
<Space
|
<Space
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Label,
|
Label,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { Colors } from '../../utils/colors';
|
||||||
import LandscapeNotice from './LandscapeNotice';
|
import LandscapeNotice from './LandscapeNotice';
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
|
|
||||||
|
@ -54,8 +55,6 @@ const Curve = ({
|
||||||
const [data, setData] = useState(mapData([yData, xData]));
|
const [data, setData] = useState(mapData([yData, xData]));
|
||||||
const { sm } = useBreakpoint();
|
const { sm } = useBreakpoint();
|
||||||
const margin = 15;
|
const margin = 15;
|
||||||
const mainColor = '#ccc';
|
|
||||||
const tooltipBg = '#2E3338';
|
|
||||||
const animationDuration = 500;
|
const animationDuration = 500;
|
||||||
|
|
||||||
if (!sm) {
|
if (!sm) {
|
||||||
|
@ -85,7 +84,7 @@ const Curve = ({
|
||||||
<Label
|
<Label
|
||||||
value={`${xLabel} (${xUnits})`}
|
value={`${xLabel} (${xUnits})`}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
style={{ fill: mainColor }}
|
style={{ fill: Colors.TEXT }}
|
||||||
/>
|
/>
|
||||||
</XAxis>
|
</XAxis>
|
||||||
<YAxis domain={['auto', 'auto']}>
|
<YAxis domain={['auto', 'auto']}>
|
||||||
|
@ -93,14 +92,14 @@ const Curve = ({
|
||||||
value={`${yLabel} (${yUnits})`}
|
value={`${yLabel} (${yUnits})`}
|
||||||
position="left"
|
position="left"
|
||||||
angle={-90}
|
angle={-90}
|
||||||
style={{ fill: mainColor }}
|
style={{ fill: Colors.TEXT }}
|
||||||
/>
|
/>
|
||||||
</YAxis>
|
</YAxis>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
labelFormatter={(value) => `${xLabel} : ${value} ${xUnits}`}
|
labelFormatter={(value) => `${xLabel} : ${value} ${xUnits}`}
|
||||||
formatter={(value: number) => [`${value} ${yUnits}`, yLabel]}
|
formatter={(value: number) => [`${value} ${yUnits}`, yLabel]}
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
backgroundColor: tooltipBg,
|
backgroundColor: Colors.MAIN,
|
||||||
border: 0,
|
border: 0,
|
||||||
boxShadow: '0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)',
|
boxShadow: '0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)',
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
|
@ -111,7 +110,7 @@ const Curve = ({
|
||||||
strokeWidth={3}
|
strokeWidth={3}
|
||||||
type="linear"
|
type="linear"
|
||||||
dataKey="y"
|
dataKey="y"
|
||||||
stroke="#1e88ea"
|
stroke={Colors.ACCENT}
|
||||||
animationDuration={animationDuration}
|
animationDuration={animationDuration}
|
||||||
/>
|
/>
|
||||||
</LineChart>
|
</LineChart>
|
||||||
|
|
|
@ -5,31 +5,13 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Logs } from '@speedy-tuner/types';
|
import { Logs } from '@speedy-tuner/types';
|
||||||
import {
|
import { Grid } from 'antd';
|
||||||
Popover,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
} from 'antd';
|
|
||||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
||||||
import TimeChart from 'timechart';
|
import TimeChart from 'timechart';
|
||||||
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
|
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
|
||||||
import { colorHsl } from '../../utils/number';
|
import { colorHsl } from '../../utils/number';
|
||||||
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
||||||
|
import CanvasHelp from '../CanvasHelp';
|
||||||
|
|
||||||
// enum Colors {
|
|
||||||
// RED = '#f32450',
|
|
||||||
// CYAN = '#8dd3c7',
|
|
||||||
// YELLOW = '#ffff00',
|
|
||||||
// PURPLE = '#bebada',
|
|
||||||
// GREEN = '#77de3c',
|
|
||||||
// BLUE = '#2fe3ff',
|
|
||||||
// GREY = '#334455',
|
|
||||||
// WHITE = '#fff',
|
|
||||||
// BG = '#222629',
|
|
||||||
// }
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
export interface SelectedField {
|
export interface SelectedField {
|
||||||
|
@ -155,22 +137,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginTop: -20, marginBottom: 10, textAlign: 'left', marginLeft: 20 }}>
|
<CanvasHelp />
|
||||||
<Popover
|
|
||||||
placement="bottom"
|
|
||||||
content={
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Typography.Title level={5}>Navigation</Typography.Title>
|
|
||||||
<Text>Pinch to zoom</Text>
|
|
||||||
<Text>Drag to pan</Text>
|
|
||||||
<Text>Ctrl + wheel scroll to zoom X axis</Text>
|
|
||||||
<Text>Hold Shift to speed up zoom 5 times</Text>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<QuestionCircleOutlined />
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
|
|
|
@ -2,54 +2,19 @@ import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { Grid } from 'antd';
|
||||||
Popover,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
} from 'antd';
|
|
||||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
||||||
import TimeChart from 'timechart';
|
import TimeChart from 'timechart';
|
||||||
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
|
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
|
||||||
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
||||||
|
import {
|
||||||
|
CompositeLogEntry,
|
||||||
|
EntryType,
|
||||||
|
} from '../../utils/logs/TriggerLogsParser';
|
||||||
|
import CanvasHelp from '../CanvasHelp';
|
||||||
|
import { Colors } from '../../utils/colors';
|
||||||
|
|
||||||
enum Colors {
|
|
||||||
RED = '#f32450',
|
|
||||||
CYAN = '#8dd3c7',
|
|
||||||
YELLOW = '#ffff00',
|
|
||||||
PURPLE = '#bebada',
|
|
||||||
GREEN = '#77de3c',
|
|
||||||
BLUE = '#2fe3ff',
|
|
||||||
GREY = '#334455',
|
|
||||||
WHITE = '#fff',
|
|
||||||
BG = '#222629',
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
export interface SelectedField {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
units: string;
|
|
||||||
scale: string | number;
|
|
||||||
transform: string | number;
|
|
||||||
format: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: extract this to types package
|
|
||||||
interface CompositeLogEntry {
|
|
||||||
type: 'trigger' | 'marker';
|
|
||||||
primaryLevel: number;
|
|
||||||
secondaryLevel: number;
|
|
||||||
trigger: number;
|
|
||||||
sync: number;
|
|
||||||
refTime: number;
|
|
||||||
maxTime: number;
|
|
||||||
toothTime: number;
|
|
||||||
time: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: CompositeLogEntry[];
|
data: CompositeLogEntry[];
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -73,14 +38,14 @@ const CompositeCanvas = ({ data, width, height }: Props) => {
|
||||||
const sync: DataPoint[] = [];
|
const sync: DataPoint[] = [];
|
||||||
|
|
||||||
data.forEach((entry, index) => {
|
data.forEach((entry, index) => {
|
||||||
if (entry.type === 'marker') {
|
if (entry.type === EntryType.MARKER) {
|
||||||
markers.push({
|
markers.push({
|
||||||
x: index,
|
x: index,
|
||||||
name: '',
|
name: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'trigger') {
|
if (entry.type === EntryType.TRIGGER) {
|
||||||
const prevSecondary = data[index - 1] ? data[index - 1].secondaryLevel : 0;
|
const prevSecondary = data[index - 1] ? data[index - 1].secondaryLevel : 0;
|
||||||
const currentSecondary = (entry.secondaryLevel + 3) * 2; // apply scale
|
const currentSecondary = (entry.secondaryLevel + 3) * 2; // apply scale
|
||||||
|
|
||||||
|
@ -166,22 +131,7 @@ const CompositeCanvas = ({ data, width, height }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginTop: -20, marginBottom: 10, textAlign: 'left', marginLeft: 20 }}>
|
<CanvasHelp />
|
||||||
<Popover
|
|
||||||
placement="bottom"
|
|
||||||
content={
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Typography.Title level={5}>Navigation</Typography.Title>
|
|
||||||
<Text>Pinch to zoom</Text>
|
|
||||||
<Text>Drag to pan</Text>
|
|
||||||
<Text>Ctrl + wheel scroll to zoom X axis</Text>
|
|
||||||
<Text>Hold Shift to speed up zoom 5 times</Text>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<QuestionCircleOutlined />
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Grid } from 'antd';
|
||||||
|
import UplotReact from 'uplot-react';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
import LandscapeNotice from '../Dialog/LandscapeNotice';
|
||||||
|
import {
|
||||||
|
ToothLogEntry,
|
||||||
|
EntryType,
|
||||||
|
} from '../../utils/logs/TriggerLogsParser';
|
||||||
|
import CanvasHelp from '../CanvasHelp';
|
||||||
|
|
||||||
|
import 'uplot/dist/uPlot.min.css';
|
||||||
|
import { Colors } from '../../utils/colors';
|
||||||
|
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
const { bars } = uPlot.paths;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: ToothLogEntry[];
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToothCanvas = ({ data, width, height }: Props) => {
|
||||||
|
const { sm } = useBreakpoint();
|
||||||
|
const [options, setOptions] = useState<uPlot.Options>();
|
||||||
|
const [plotData, setPlotData] = useState<uPlot.AlignedData>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const xData: number[] = [];
|
||||||
|
const yData: (number | null)[] = [];
|
||||||
|
|
||||||
|
data.forEach((entry: ToothLogEntry, index) => {
|
||||||
|
if (entry.type === EntryType.TRIGGER) {
|
||||||
|
yData.push(entry.toothTime);
|
||||||
|
xData.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setPlotData([
|
||||||
|
xData,
|
||||||
|
yData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
setOptions({
|
||||||
|
title: 'Tooth logs',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scales: {
|
||||||
|
x: { time: false },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: 'Event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tooth time',
|
||||||
|
points: { show: false },
|
||||||
|
stroke: Colors.ACCENT,
|
||||||
|
fill: Colors.ACCENT,
|
||||||
|
scale: 'toothTime',
|
||||||
|
value: (self, rawValue) => `${rawValue.toLocaleString()}μs`,
|
||||||
|
paths: bars!({ size: [0.6, 100] }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
stroke: Colors.TEXT,
|
||||||
|
grid: { stroke: Colors.MAIN_LIGHT },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 'toothTime',
|
||||||
|
label: '',
|
||||||
|
stroke: Colors.TEXT,
|
||||||
|
grid: { stroke: Colors.MAIN_LIGHT },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cursor: {
|
||||||
|
drag: { y: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [data, width, height, sm]);
|
||||||
|
|
||||||
|
if (!sm) {
|
||||||
|
return <LandscapeNotice />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CanvasHelp />
|
||||||
|
<UplotReact
|
||||||
|
options={options!}
|
||||||
|
data={plotData!}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToothCanvas;
|
|
@ -90,8 +90,15 @@ export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
||||||
|
|
||||||
export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
||||||
fetchWithProgress(
|
fetchWithProgress(
|
||||||
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1.csv.gz',
|
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1_2.csv.gz',
|
||||||
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz',
|
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz',
|
||||||
onProgress,
|
onProgress,
|
||||||
signal,
|
signal,
|
||||||
).then((response) => response);
|
).then((response) => response);
|
||||||
|
|
||||||
|
export const loadToothLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
||||||
|
fetchWithProgress(
|
||||||
|
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/tooth_3.csv.gz',
|
||||||
|
onProgress,
|
||||||
|
signal,
|
||||||
|
).then((response) => response);
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export enum Colors {
|
||||||
|
RED = '#f32450',
|
||||||
|
CYAN = '#8dd3c7',
|
||||||
|
YELLOW = '#ffff00',
|
||||||
|
PURPLE = '#bebada',
|
||||||
|
GREEN = '#77de3c',
|
||||||
|
BLUE = '#2fe3ff',
|
||||||
|
GREY = '#334455',
|
||||||
|
|
||||||
|
// dark theme
|
||||||
|
ACCENT = '#1e88ea',
|
||||||
|
TEXT = '#ddd',
|
||||||
|
MAIN = '#222629',
|
||||||
|
MAIN_DARK = '#191C1E',
|
||||||
|
MAIN_LIGHT = '#2E3338',
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { isNumber } from '../tune/expression';
|
||||||
|
|
||||||
|
export enum EntryType {
|
||||||
|
TRIGGER = 'trigger',
|
||||||
|
MARKER = 'marker',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompositeLogEntry {
|
||||||
|
type: EntryType;
|
||||||
|
primaryLevel: number;
|
||||||
|
secondaryLevel: number;
|
||||||
|
trigger: number;
|
||||||
|
sync: number;
|
||||||
|
refTime: number;
|
||||||
|
maxTime: number;
|
||||||
|
toothTime: number;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToothLogEntry {
|
||||||
|
type: EntryType;
|
||||||
|
toothTime: number;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerLogsParser {
|
||||||
|
COMMENT_PREFIX = '#';
|
||||||
|
|
||||||
|
MARKER_PREFIX = 'MARK';
|
||||||
|
|
||||||
|
isTooth: boolean = false;
|
||||||
|
|
||||||
|
isComposite: boolean = false;
|
||||||
|
|
||||||
|
resultComposite: CompositeLogEntry[] = [];
|
||||||
|
|
||||||
|
resultTooth: ToothLogEntry[] = [];
|
||||||
|
|
||||||
|
parse(buffer: ArrayBuffer): TriggerLogsParser {
|
||||||
|
const raw = (new TextDecoder()).decode(buffer);
|
||||||
|
this.parseCompositeLogs(raw);
|
||||||
|
this.parseToothLogs(raw);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompositeLogs(): CompositeLogEntry[] {
|
||||||
|
return this.resultComposite;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToothLogs(): ToothLogEntry[] {
|
||||||
|
return this.resultTooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseToothLogs(raw: string): void {
|
||||||
|
this.resultTooth = [];
|
||||||
|
|
||||||
|
raw.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith(this.MARKER_PREFIX)) {
|
||||||
|
const previous = this.resultTooth[this.resultTooth.length - 1] || {
|
||||||
|
toothTime: 0,
|
||||||
|
time: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resultTooth.push({
|
||||||
|
type: EntryType.MARKER,
|
||||||
|
toothTime: previous.toothTime,
|
||||||
|
time: previous.time,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = trimmed.split(',');
|
||||||
|
if (!isNumber(split[0])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = Number(split[1]);
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultTooth.push({
|
||||||
|
type: EntryType.TRIGGER,
|
||||||
|
toothTime: Number(split[0]),
|
||||||
|
time,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCompositeLogs(raw: string): void {
|
||||||
|
this.resultComposite = [];
|
||||||
|
|
||||||
|
raw.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith(this.MARKER_PREFIX)) {
|
||||||
|
const previous = this.resultComposite[this.resultComposite.length - 1] || {
|
||||||
|
primaryLevel: 0,
|
||||||
|
secondaryLevel: 0,
|
||||||
|
trigger: 0,
|
||||||
|
sync: 0,
|
||||||
|
refTime: 0,
|
||||||
|
maxTime: 0,
|
||||||
|
toothTime: 0,
|
||||||
|
time: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resultComposite.push({
|
||||||
|
type: EntryType.MARKER,
|
||||||
|
primaryLevel: previous.primaryLevel,
|
||||||
|
secondaryLevel: previous.secondaryLevel,
|
||||||
|
trigger: previous.trigger,
|
||||||
|
sync: previous.sync,
|
||||||
|
refTime: previous.refTime,
|
||||||
|
maxTime: previous.maxTime,
|
||||||
|
toothTime: previous.toothTime,
|
||||||
|
time: previous.time,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = trimmed.split(',');
|
||||||
|
if (!isNumber(split[0])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = Number(split[7]);
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultComposite.push({
|
||||||
|
type: EntryType.TRIGGER,
|
||||||
|
primaryLevel: Number(split[0]),
|
||||||
|
secondaryLevel: Number(split[1]),
|
||||||
|
trigger: Number(split[2]),
|
||||||
|
sync: Number(split[3]),
|
||||||
|
refTime: Number(split[4]),
|
||||||
|
maxTime: Number(split[5]),
|
||||||
|
toothTime: Number(split[6]),
|
||||||
|
time,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TriggerLogsParser;
|
Loading…
Reference in New Issue