diff --git a/package-lock.json b/package-lock.json index 5028ba4..7b1bc37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "license": "MIT", "dependencies": { "@reduxjs/toolkit": "^1.5.1", - "antd": "^4.15.2", + "antd": "^4.15.3", "electron-squirrel-startup": "^1.0.0", "js-yaml": "^4.1.0 ", "mlg-converter": "^0.5.0", @@ -4085,9 +4085,9 @@ } }, "node_modules/antd": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.2.tgz", - "integrity": "sha512-9IwlR022xSQrG+iJG8d7adSo9gy4n7AuS5x0/OxDi/V3CYSTTfaE24jp1wnmgmXOVAT/77Llg9OEbKcSyFim2g==", + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.4.tgz", + "integrity": "sha512-1c0ykHGomcd7QhEeRtynxN3i7fb7JBdnEq2/Yqhf7yzMIhGSfZm+h+A2lTqMOMheCVL6q2ie7lxqhtNLq6sWoQ==", "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.6.2", @@ -4106,7 +4106,7 @@ "rc-dropdown": "~3.2.0", "rc-field-form": "~1.20.0", "rc-image": "~5.2.4", - "rc-input-number": "~7.0.1", + "rc-input-number": "~7.1.0", "rc-mentions": "~1.5.0", "rc-menu": "~8.10.0", "rc-motion": "^2.4.0", @@ -19036,13 +19036,13 @@ } }, "node_modules/rc-input-number": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.0.3.tgz", - "integrity": "sha512-y0nVqVANWyxQbm/vdhz1p5E1V5Y6Yd2+3MGKntSzCxrYgw0F7/COXkbRdcTECnXwiDv8ZrbYQ1pTP3u43PqE4Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.1.0.tgz", + "integrity": "sha512-ewgtKZaDmwbOWX8DXBGV+amp1IiGS8G+5xDqn85CK1BiQMwsQdrmMEqNkbTdxO8EmYbwN1iQQ4t82IkAaIoa3A==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "rc-util": "^5.0.1" + "rc-util": "^5.9.8" }, "peerDependencies": { "react": ">=16.9.0", @@ -29544,9 +29544,9 @@ } }, "antd": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.2.tgz", - "integrity": "sha512-9IwlR022xSQrG+iJG8d7adSo9gy4n7AuS5x0/OxDi/V3CYSTTfaE24jp1wnmgmXOVAT/77Llg9OEbKcSyFim2g==", + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-4.15.4.tgz", + "integrity": "sha512-1c0ykHGomcd7QhEeRtynxN3i7fb7JBdnEq2/Yqhf7yzMIhGSfZm+h+A2lTqMOMheCVL6q2ie7lxqhtNLq6sWoQ==", "requires": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.6.2", @@ -29565,7 +29565,7 @@ "rc-dropdown": "~3.2.0", "rc-field-form": "~1.20.0", "rc-image": "~5.2.4", - "rc-input-number": "~7.0.1", + "rc-input-number": "~7.1.0", "rc-mentions": "~1.5.0", "rc-menu": "~8.10.0", "rc-motion": "^2.4.0", @@ -41363,13 +41363,13 @@ } }, "rc-input-number": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.0.3.tgz", - "integrity": "sha512-y0nVqVANWyxQbm/vdhz1p5E1V5Y6Yd2+3MGKntSzCxrYgw0F7/COXkbRdcTECnXwiDv8ZrbYQ1pTP3u43PqE4Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.1.0.tgz", + "integrity": "sha512-ewgtKZaDmwbOWX8DXBGV+amp1IiGS8G+5xDqn85CK1BiQMwsQdrmMEqNkbTdxO8EmYbwN1iQQ4t82IkAaIoa3A==", "requires": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "rc-util": "^5.0.1" + "rc-util": "^5.9.8" } }, "rc-mentions": { diff --git a/package.json b/package.json index 20735f6..340250d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@reduxjs/toolkit": "^1.5.1", - "antd": "^4.15.2", + "antd": "^4.15.3", "electron-squirrel-startup": "^1.0.0", "js-yaml": "^4.1.0 ", "mlg-converter": "^0.5.0", diff --git a/src/components/Log/Canvas.tsx b/src/components/Log/Canvas.tsx index 8198a3a..3448d14 100644 --- a/src/components/Log/Canvas.tsx +++ b/src/components/Log/Canvas.tsx @@ -21,6 +21,7 @@ import { colorHsl, formatNumber, msToTime, + round, remap, } from '../../utils/number'; @@ -100,16 +101,19 @@ const Canvas = ({ 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 maxIndex = useMemo(() => lastIndex / (zoom < 1 ? 1 : zoom), [lastIndex, zoom]); + const timeScale = useMemo(() => areaWidth / maxTime, [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)), + () => startTime >= 0 ? 0 : -(startTime * maxIndex / areaWidth), [areaWidth, maxIndex, startTime], ); + const pixelsOnScreen = (maxIndex - startIndex) / areaWidth; + // map available pixels to the number of data entries + const resolution = pixelsOnScreen < 1 ? 1 : Math.round(pixelsOnScreen); // find max values for each selected field so we can calculate scale const fieldsToPlot = useMemo(() => { @@ -142,39 +146,15 @@ const Canvas = ({ const fieldsKeys = useMemo(() => Object.keys(fieldsToPlot), [fieldsToPlot]); - // 1..x where 1 is max - const resolution = useMemo(() => - Math.round(maxIndex / 5_000 / zoom) || 1, [maxIndex, zoom]); + const dataWindow = useMemo(() => { + const sliced = data.slice(startIndex, startIndex + maxIndex); // slice data + // skip n-th element to reduce number of data points + if (resolution > 1) { + return sliced.filter((_, index) => index % resolution === 0); + } - 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]); + return sliced; + }, [data, maxIndex, resolution, startIndex]); const drawText = useCallback((left: number, top: number, text: string, color: string, textAlign = 'left') => { ctx.textAlign = textAlign as any; @@ -184,17 +164,65 @@ const Canvas = ({ ctx.fillText(text, left, top); }, [ctx]); + const drawMarker = useCallback((position: number) => { + const prevStyle = ctx.strokeStyle; + ctx.strokeStyle = Colors.RED; + ctx.setLineDash([5]); + ctx.beginPath(); + ctx.moveTo(position, 0); + ctx.lineTo(position, canvasHeight); + ctx.stroke(); + ctx.setLineDash([]); + ctx.strokeStyle = prevStyle; + }, [canvasHeight, ctx]); + + const plotField = useCallback((field: string, min: number, max: number, color: string) => { + ctx.strokeStyle = color; + ctx.beginPath(); + + // initial position + const initialValue = areaHeight - remap(firstEntry[field] as number, min, max, 0, areaHeight); + ctx.moveTo(startTime, initialValue); + + dataWindow.forEach((entry, index) => { + const lastRecord: LogEntry = dataWindow[index - 1] ?? { Time: 0 }; + // scale time to max width + const time = (entry.Time ? entry.Time : lastRecord.Time) as number * timeScale; + // scale the value + const value = areaHeight - remap(entry[field] as number, min, max, 0, areaHeight); + const position = Math.round(startTime + time); + + switch (entry.type) { + case 'field': + ctx.lineTo(position, Math.round(value)); + break; + case 'marker': + drawText(position, areaHeight / 2, `Marker at: ${lastRecord.Time}`, Colors.GREEN); + // drawMarker(position); // TODO: fix moveTo + break; + default: + break; + } + }); + + ctx.stroke(); + }, [areaHeight, ctx, dataWindow, drawText, firstEntry, startTime, timeScale]); + const drawIndicator = useCallback(() => { ctx.setLineDash([5]); ctx.strokeStyle = Colors.WHITE; ctx.beginPath(); - // switch to time - let index = Math.round(indicatorPos * (data.length - 1) / areaWidth); + // remap indicator position to index in the data array + // FIXME: this is bad + let index = Math.floor(remap(indicatorPos, 0, areaWidth, startIndex, maxIndex)); if (index < 0) { index = 0; } + // TODO: + // 1px = 1 index % resolution + // index = indicatorPos || 0; const currentData = data[index]; ctx.moveTo(indicatorPos, 0); @@ -227,14 +255,24 @@ const Canvas = ({ drawText( left, areaHeight + 20, - msToTime(Math.round(currentData.Time as number * 1000)), + `${round(currentData.Time as number, 3)}s`, + // msToTime(Math.round(currentData.Time as number * 1000)), Colors.GREY, textAlign, ); + // TODO: DEBUG + // 1px = 1 index % resolution + drawText( + left, + areaHeight - 20, + `${index} - ${indicatorPos}`, + Colors.RED, textAlign, + ); + ctx.lineTo(indicatorPos, canvasHeight); ctx.stroke(); ctx.setLineDash([]); - }, [areaHeight, areaWidth, canvasHeight, ctx, data, drawText, fieldsKeys, fieldsToPlot, hsl, indicatorPos]); + }, [areaHeight, areaWidth, canvasHeight, ctx, data, drawText, fieldsKeys, fieldsToPlot, hsl, indicatorPos, maxIndex, startIndex]); const plot = useCallback(() => { if (!ctx) { @@ -266,23 +304,9 @@ const Canvas = ({ fieldsToPlot[name].max, hsl(fieldIndex, fieldsKeys.length)), ); + drawIndicator(); - }, [ - ctx, - scaledWidth, - areaWidth, - areaHeight, - zoom, - pan, - rightBoundary, - canvasWidth, - canvasHeight, - fieldsKeys, - drawIndicator, - plotField, - fieldsToPlot, - hsl, - ]); + }, [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)) { @@ -318,8 +342,6 @@ const Canvas = ({ }; const keyboardListener = useCallback((e: KeyboardEvent) => { - // TODO: - // onKeyLeft if (isUp(e)) { setZoom((current) => current + 0.1); } diff --git a/src/utils/number.ts b/src/utils/number.ts index e569aba..92c5e0b 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -1,3 +1,5 @@ +export type HslType = [number, number, number]; + export const formatBytes = (bytes: number, decimals = 2): string => { if (bytes === 0) return '0 Bytes'; @@ -10,6 +12,7 @@ export const formatBytes = (bytes: number, decimals = 2): string => { return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; }; +// fix this pad export const leftPad = (n: number, z = 2) => (`00${n}`).slice(-z); export const msToTime = (input: number) => { @@ -26,8 +29,6 @@ export const msToTime = (input: number) => { export const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; -export type HslType = [number, number, number]; - export const colorHsl = (min: number, max: number, value: number): HslType => { const saturation = 60; const lightness = 40; @@ -46,6 +47,7 @@ export const colorHsl = (min: number, max: number, value: number): HslType => { // eslint-disable-next-line prefer-template export const round = (value: number, digits: number | string) => +(Math.round(value + `e+${digits}` as any) + `e-${digits}`); +// TODO: move this or rename to MS export const formatNumber = (value: number, format: string): string => { if (format === '%d') { return `${Math.round(value)}`;