diff --git a/.eslintrc.yml b/.eslintrc.yml index 916a67f..4faa01d 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -10,10 +10,14 @@ settings: - ".ts" - ".tsx" extends: + - eslint:recommended - react-app - airbnb - plugin:jsx-a11y/recommended - prettier + - plugin:import/errors + - plugin:import/warnings + - plugin:import/typescript plugins: - jsx-a11y - prettier diff --git a/package-lock.json b/package-lock.json index d2eb2a3..93d4760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "license": "MIT", "dependencies": { "@reduxjs/toolkit": "^1.5.1", - "antd": "^4.15.0", + "antd": "^4.15.1", "electron-squirrel-startup": "^1.0.0", "js-yaml": "^4.0.0 ", "mlg-converter": "^0.5.0", @@ -41,8 +41,8 @@ "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "concurrently": "^6.0.1", - "electron": "^12.0.1", - "eslint": "^7.23.0", + "electron": "^12.0.4", + "eslint": "^7.24.0", "eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.1.0", @@ -52,7 +52,7 @@ "eslint-plugin-prettier": "^3.3.1", "less-loader": "^6.1.0", "prettier": "^2.2.1", - "typescript": "^4.1.5", + "typescript": "^4.2.4", "wait-on": "^5.3.0", "worker-loader": "^3.0.8" } @@ -4085,9 +4085,9 @@ } }, "node_modules/antd": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.0.tgz", - "integrity": "sha512-24HMixmQAhCyqb0ND5wX5DYRTbPactCT36mfVKowqgr77eT7XQ59Uu6aS513mbeiVhXcHrNlrlCKNZBSeEDgPg==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.1.tgz", + "integrity": "sha512-zTZz8GY9yERNjSnH6xWU3Rw5sC3RtHEs/LOTKcSMTtU3Q5jHXIbAHKd1C6bYLQT6Ru75p+/UyKvJoNip/ax/WQ==", "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.6.2", @@ -4096,7 +4096,7 @@ "array-tree-filter": "^2.1.0", "classnames": "^2.2.6", "copy-to-clipboard": "^3.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "moment": "^2.25.3", "rc-cascader": "~1.4.0", "rc-checkbox": "~2.3.0", @@ -7839,9 +7839,9 @@ } }, "node_modules/electron": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-12.0.2.tgz", - "integrity": "sha512-14luh9mGzfL4e0sncyy0+kW37IU7Y0Y1tvI97FDRSW0ZBQxi5cmAwSs5dmPmNBFBIGtzkaGaEB01j9RjZuCmow==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-12.0.4.tgz", + "integrity": "sha512-A8Lq3YMZ1CaO1z5z5nsyFxIwkgwXLHUwL2pf9MVUHpq7fv3XUewCMD98EnLL3DdtiyCvw5KMkeT1WGsZh8qFug==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -8924,9 +8924,9 @@ } }, "node_modules/eslint": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz", - "integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz", + "integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==", "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.0", @@ -23715,9 +23715,9 @@ } }, "node_modules/typescript": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -29539,9 +29539,9 @@ } }, "antd": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.0.tgz", - "integrity": "sha512-24HMixmQAhCyqb0ND5wX5DYRTbPactCT36mfVKowqgr77eT7XQ59Uu6aS513mbeiVhXcHrNlrlCKNZBSeEDgPg==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.1.tgz", + "integrity": "sha512-zTZz8GY9yERNjSnH6xWU3Rw5sC3RtHEs/LOTKcSMTtU3Q5jHXIbAHKd1C6bYLQT6Ru75p+/UyKvJoNip/ax/WQ==", "requires": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.6.2", @@ -29550,7 +29550,7 @@ "array-tree-filter": "^2.1.0", "classnames": "^2.2.6", "copy-to-clipboard": "^3.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "moment": "^2.25.3", "rc-cascader": "~1.4.0", "rc-checkbox": "~2.3.0", @@ -32589,9 +32589,9 @@ "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" }, "electron": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-12.0.2.tgz", - "integrity": "sha512-14luh9mGzfL4e0sncyy0+kW37IU7Y0Y1tvI97FDRSW0ZBQxi5cmAwSs5dmPmNBFBIGtzkaGaEB01j9RjZuCmow==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-12.0.4.tgz", + "integrity": "sha512-A8Lq3YMZ1CaO1z5z5nsyFxIwkgwXLHUwL2pf9MVUHpq7fv3XUewCMD98EnLL3DdtiyCvw5KMkeT1WGsZh8qFug==", "dev": true, "requires": { "@electron/get": "^1.0.1", @@ -33438,9 +33438,9 @@ } }, "eslint": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz", - "integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz", + "integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==", "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.0", @@ -44992,9 +44992,9 @@ } }, "typescript": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" }, "ua-parser-js": { "version": "0.7.27", diff --git a/package.json b/package.json index 1b1b0af..34204b0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@reduxjs/toolkit": "^1.5.1", - "antd": "^4.15.0", + "antd": "^4.15.1", "electron-squirrel-startup": "^1.0.0", "js-yaml": "^4.0.0 ", "mlg-converter": "^0.5.0", @@ -68,8 +68,8 @@ "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "concurrently": "^6.0.1", - "electron": "^12.0.1", - "eslint": "^7.23.0", + "electron": "^12.0.4", + "eslint": "^7.24.0", "eslint-config-airbnb": "^18.2.1", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.1.0", @@ -79,7 +79,7 @@ "eslint-plugin-prettier": "^3.3.1", "less-loader": "^6.1.0", "prettier": "^2.2.1", - "typescript": "^4.1.5", + "typescript": "^4.2.4", "wait-on": "^5.3.0", "worker-loader": "^3.0.8" }, diff --git a/src/App.tsx b/src/App.tsx index 52b33f0..79fb636 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,4 @@ -import { - useEffect, - useMemo, -} from 'react'; + import { useLocation, Switch, @@ -15,6 +12,10 @@ import { Result, } from 'antd'; import { connect } from 'react-redux'; +import { + useEffect, + useMemo, +} from 'react'; import Dialog from './components/Dialog'; import { loadAll } from './utils/api'; import SideBar, { DialogMatchedPathType } from './components/SideBar'; diff --git a/src/components/Log/Canvas.tsx b/src/components/Log/Canvas.tsx index a30949b..0b7eda2 100644 --- a/src/components/Log/Canvas.tsx +++ b/src/components/Log/Canvas.tsx @@ -9,6 +9,7 @@ import { WheelEvent, TouchEvent, Touch, + useMemo, } from 'react'; import { isDown, @@ -85,33 +86,40 @@ const Canvas = ({ return value; }, [rightBoundary]); - const plot = useCallback(() => { - const canvas = canvasRef.current!; - const hsl = (fieldIndex: number, allFields: number) => { - const [hue] = colorHsl(0, allFields - 1, fieldIndex); - return `hsl(${hue}, 90%, 50%)`; - }; - const ctx = canvas.getContext('2d')!; - const lastEntry = data[data.length - 1]; - const maxTime = (lastEntry.Time as number) / (zoom < 1 ? 1 : zoom); - const areaWidth = canvas.width; - const areaHeight = canvas.height - 30; // leave some space in the bottom - const xScale = areaWidth / maxTime; - const firstEntry = data[0]; - const scaledWidth = areaWidth * zoom / 1; - const start = pan; - // TODO: adjust this based on FPS / preference - const resolution = Math.round(data.length / 1000 / zoom) || 1; // 1..x where 1 is max + const hsl = useCallback((fieldIndex: number, allFields: number) => { + const [hue] = colorHsl(0, allFields - 1, fieldIndex); + return `hsl(${hue}, 90%, 50%)`; + }, []); - setRightBoundary(-(scaledWidth - areaWidth)); + const canvas = canvasRef.current!; + const ctx = useMemo(() => canvas && canvas.getContext('2d', { alpha: false })!, [canvas]); + const canvasWidth = canvas ? canvas.width : 0; + const canvasHeight = canvas ? canvas.height : 0; + const areaWidth = canvas ? canvasWidth : 0; + const areaHeight = canvas ? canvasHeight - 30 : 0; // leave some space in the bottom + const lastIndex = data.length - 1; + const lastEntry = useMemo(() => data[lastIndex], [data, lastIndex]); + const maxTime = useMemo(() => (lastEntry.Time as number) / (zoom < 1 ? 1 : zoom), [lastEntry.Time, zoom]); + const maxIndex = useMemo(() => Math.round(lastIndex / (zoom < 1 ? 1 : zoom)), [lastIndex, zoom]); + const timeScale = areaWidth / maxTime; + // const indexScale = areaWidth / maxIndex; + const firstEntry = data[0]; + const scaledWidth = useMemo(() => areaWidth * zoom / 1, [areaWidth, zoom]); + const startTime = pan; + const startIndex = useMemo( + () => Math.round(startTime >= 0 ? 0 : -(startTime * maxIndex / areaWidth)), + [areaWidth, maxIndex, startTime], + ); + + // find max values for each selected field so we can calculate scale + const fieldsToPlot = useMemo(() => { + const temp: { [index: string]: PlottableField } = {}; - // find max values for each selected field so we can calculate scale - const fieldsToPlot: { [index: string]: PlottableField } = {}; data.forEach((record) => { selectedFields.forEach(({ name, scale, transform, units, format }) => { const value = record[name]; - if (!fieldsToPlot[name]) { - fieldsToPlot[name] = { + if (!temp[name]) { + temp[name] = { min: 0, max: 0, scale: scale as number, @@ -120,15 +128,120 @@ const Canvas = ({ format, }; } - if (value > fieldsToPlot[name].max) { - fieldsToPlot[name].max = record[name] as number; + if (value > temp[name].max) { + temp[name].max = record[name] as number; } - if (value < fieldsToPlot[name].min) { - fieldsToPlot[name].min = record[name] as number; + if (value < temp[name].min) { + temp[name].min = record[name] as number; } }); }); - const fieldsKeys = Object.keys(fieldsToPlot); + + return temp; + }, [data, selectedFields]); + + const fieldsKeys = useMemo(() => Object.keys(fieldsToPlot), [fieldsToPlot]); + + // 1..x where 1 is max + const resolution = useMemo(() => + Math.round(data.length / 1_000 / zoom) || 1, [data.length, zoom]); + + const dataWindow = useMemo( + () => data + .slice(startIndex, startIndex + maxIndex) // slice the data array + .filter((_, index) => index % resolution === 0), + [data, maxIndex, resolution, startIndex], + ); + + const plotField = useCallback((field: string, min: number, max: number, color: string) => { + ctx.strokeStyle = color; + ctx.beginPath(); + + // initial value + ctx.moveTo(startTime, areaHeight - remap(firstEntry[field] as number, min, max, 0, areaHeight)); + + dataWindow.forEach((entry) => { + // draw marker on top of the record + if (entry.type === 'marker') { + // TODO: draw actual marker + return; + } + + const time = (entry.Time as number) * timeScale; // scale time to max width + const value = areaHeight - remap(entry[field] as number, min, max, 0, areaHeight); // scale the value + + ctx.lineTo(Math.round(startTime + time), Math.round(value)); + }); + + ctx.stroke(); + }, [areaHeight, ctx, dataWindow, firstEntry, startTime, timeScale]); + + const drawText = useCallback((left: number, top: number, text: string, color: string, textAlign = 'left') => { + ctx.textAlign = textAlign as any; + ctx.fillStyle = Colors.BG; + ctx.fillText(text, left + 2, top + 2); + ctx.fillStyle = color; + ctx.fillText(text, left, top); + }, [ctx]); + + const drawIndicator = useCallback(() => { + ctx.setLineDash([5]); + ctx.strokeStyle = Colors.WHITE; + ctx.beginPath(); + + // switch to time + let index = Math.round(indicatorPos * (data.length - 1) / areaWidth); + if (index < 0) { + index = 0; + } + + const currentData = data[index]; + + ctx.moveTo(indicatorPos, 0); + + let left = indicatorPos + 10; + let textAlign = 'left'; + if (indicatorPos > areaWidth / 2) { + // flip text to the left side of the indicator + textAlign = 'right'; + left = indicatorPos - 10; + } + + let top = 0; + fieldsKeys.forEach((name, fieldIndex) => { + const field = fieldsToPlot[name]; + const { units, scale, transform, format } = field; + const value = formatNumber((currentData[name] as number * scale) + transform, format); + top += 20; + + drawText( + left, + top, + `${name}: ${value}${units ? ` (${units})` : ''}`, + hsl(fieldIndex, fieldsKeys.length), + textAlign, + ); + }); + + // draw Time + drawText( + left, + areaHeight + 20, + msToTime(Math.round(currentData.Time as number * 1000)), + Colors.GREY, textAlign, + ); + + ctx.lineTo(indicatorPos, canvasHeight); + ctx.stroke(); + ctx.setLineDash([]); + }, [areaHeight, areaWidth, canvasHeight, ctx, data, drawText, fieldsKeys, fieldsToPlot, hsl, indicatorPos]); + + const plot = useCallback(() => { + if (!ctx) { + return; + } + + setRightBoundary(-(scaledWidth - areaWidth)); // basic settings ctx.font = '14px Arial'; @@ -144,95 +257,8 @@ const Canvas = ({ return; } - const drawText = (left: number, top: number, text: string, color: string, textAlign = 'left') => { - ctx.textAlign = textAlign as any; - ctx.fillStyle = Colors.BG; - ctx.fillText(text, left + 2, top + 2); - ctx.fillStyle = color; - ctx.fillText(text, left, top); - }; - - const plotField = (field: string, min: number, max: number, color: string) => { - ctx.strokeStyle = color; - ctx.beginPath(); - - // initial value - ctx.moveTo(start, areaHeight - remap(firstEntry[field] as number, min, max, 0, areaHeight)); - - let index = 0; - data.forEach((entry) => { - index++; - if (index % resolution !== 0) { - return; - } - - // draw marker on top of the record - if (entry.type === 'marker') { - // TODO: draw actual marker - return; - } - - const time = (entry.Time as number) * xScale; // scale time to max width - const value = areaHeight - remap(entry[field] as number, min, max, 0, areaHeight); // scale the value - - ctx.lineTo(start + time, value); - }); - - ctx.stroke(); - }; - - const drawIndicator = () => { - ctx.setLineDash([5]); - ctx.strokeStyle = Colors.WHITE; - ctx.beginPath(); - - // switch to time - let index = Math.round(indicatorPos * (data.length - 1) / areaWidth); - if (index < 0) { - index = 0; - } - - ctx.moveTo(indicatorPos, 0); - - let left = indicatorPos + 10; - let textAlign = 'left'; - if (indicatorPos > areaWidth / 2) { - // flip text to the left side of the indicator - textAlign = 'right'; - left = indicatorPos - 10; - } - - let top = 0; - fieldsKeys.forEach((name, fieldIndex) => { - const field = fieldsToPlot[name]; - const { units, scale, transform, format } = field; - const value = formatNumber((data[index][name] as number * scale) + transform, format); - top += 20; - - drawText( - left, - top, - `${name}: ${value}${units ? ` (${units})` : ''}`, - hsl(fieldIndex, fieldsKeys.length), - textAlign, - ); - }); - - // draw Time - drawText( - left, - areaHeight + 20, - msToTime(Math.round(data[index].Time as number * 1000)), - Colors.GREY, textAlign, - ); - - ctx.lineTo(indicatorPos, canvas.height); - ctx.stroke(); - ctx.setLineDash([]); - }; - // clear - ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvasWidth, canvasHeight); fieldsKeys.forEach((name, fieldIndex) => plotField( name, @@ -241,7 +267,22 @@ const Canvas = ({ hsl(fieldIndex, fieldsKeys.length)), ); drawIndicator(); - }, [data, zoom, pan, rightBoundary, selectedFields, indicatorPos]); + }, [ + ctx, + scaledWidth, + areaWidth, + areaHeight, + zoom, + pan, + rightBoundary, + canvasWidth, + canvasHeight, + fieldsKeys, + drawIndicator, + plotField, + fieldsToPlot, + hsl, + ]); const onWheel = (e: WheelEvent) => { if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { diff --git a/src/data/help.ts b/src/data/help.ts index 9bb8a89..4dab96f 100644 --- a/src/data/help.ts +++ b/src/data/help.ts @@ -1,6 +1,6 @@ import { Help as HelpType } from '../types/config'; -export const help: HelpType = { +const help: HelpType = { reqFuel: 'The base reference pulse width required to achieve stoichiometric at 100% VE and a manifold absolute pressure (MAP) of 100kPa using current settings.', algorithm: 'Fueling calculation algorithm', alternate: 'Whether or not the injectors should be fired at the same time.\nThis setting is ignored when Sequential is selected below, however it will still affect req_fuel value.',