Apply value transformation, add units, refactor config functions (#50)

This commit is contained in:
Piotr Rogowski 2021-04-10 23:49:48 +02:00 committed by GitHub
parent 8f1665ba3c
commit 75967afeaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 170 additions and 65 deletions

View File

@ -37,12 +37,12 @@ rules:
object-curly-spacing:
- error
- always
object-curly-newline: [2, {
object-curly-newline: [1, {
"ImportDeclaration": { "multiline": true, "minProperties": 2 },
"ExportDeclaration": { "multiline": true, "minProperties": 1 }
}]
modules-newline/import-declaration-newline: 2
modules-newline/export-declaration-newline: 2
modules-newline/import-declaration-newline: 1
modules-newline/export-declaration-newline: 1
quotes:
- error
- single

View File

@ -1,6 +1,6 @@
import {
useEffect,
useMemo,
useMemo,
} from 'react';
import {
useLocation,
@ -12,7 +12,7 @@ import {
} from 'react-router-dom';
import {
Layout,
Result,
Result,
} from 'antd';
import { connect } from 'react-redux';
import Dialog from './components/Dialog';
@ -20,7 +20,7 @@ import { loadAll } from './utils/api';
import SideBar, { DialogMatchedPathType } from './components/SideBar';
import {
AppState,
UIState,
UIState,
} from './types/state';
import BurnButton from './components/BurnButton';
import TopBar from './components/TopBar';
@ -32,6 +32,7 @@ import { Routes } from './routes';
import { Config as ConfigType } from './types/config';
import Log from './components/Log';
import useStorage from './hooks/useStorage';
import useConfig from './hooks/useConfig';
const { Content } = Layout;
@ -46,6 +47,7 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
// const [lastDialogPath, setLastDialogPath] = useState<string|null>();
const { pathname } = useLocation();
const { storageGetSync } = useStorage();
const { isConfigReady } = useConfig(config);
const lastDialogPath = storageGetSync('lastDialog');
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
path: Routes.DIALOG,
@ -53,14 +55,14 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
const firstDialogPath = useMemo(() => {
if (!config.menus) {
if (!isConfigReady) {
return null;
}
const firstCategory = Object.keys(config.menus)[0];
const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0];
return generatePath(Routes.DIALOG, { category: firstCategory, dialog: firstDialog });
}, [config.menus]);
}, [config.menus, isConfigReady]);
useEffect(() => {
loadAll();

View File

@ -27,7 +27,6 @@ import {
ConstantTypes,
} from '../types/config';
import { Tune as TuneType } from '../types/tune';
import { findOnPage } from '../utils/config/find';
import {
parseXy,
parseZ,
@ -38,6 +37,7 @@ import {
isExpression,
} from '../utils/tune/expression';
import useStorage from '../hooks/useStorage';
import useConfig from '../hooks/useConfig';
interface DialogsAndCurves {
[name: string]: DialogType | CurveType | TableType,
@ -103,6 +103,8 @@ const Dialog = ({
}) => {
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
const { storageSet } = useStorage();
const { findConstantOnPage } = useConfig(config);
useEffect(() => {
storageSet('lastDialog', url);
}, [storageSet, url]);
@ -127,8 +129,8 @@ const Dialog = ({
const renderCurve = (curve: CurveType) => {
const x = tune.constants[curve.xBins[0]];
const y = tune.constants[curve.yBins[0]];
const xConstant = findOnPage(config, curve.xBins[0]) as ScalarConstantType;
const yConstant = findOnPage(config, curve.yBins[0]) as ScalarConstantType;
const xConstant = findConstantOnPage(curve.xBins[0]) as ScalarConstantType;
const yConstant = findConstantOnPage(curve.yBins[0]) as ScalarConstantType;
return (
<Curve
@ -154,7 +156,7 @@ const Dialog = ({
const x = tune.constants[table.xBins[0]];
const y = tune.constants[table.yBins[0]];
const z = tune.constants[table.zBins[0]];
const zConstant = findOnPage(config, table.zBins[0]) as ScalarConstantType;
const zConstant = findConstantOnPage(table.zBins[0]) as ScalarConstantType;
let max = zConstant.max as number;
if (isExpression(zConstant.max)) {
@ -334,7 +336,7 @@ const Dialog = ({
<Col key={panel.name} {...calculateSpan(panel.type as PanelTypes, panels.length)}>
<Divider>{panel.title}</Divider>
{(panel.fields).map((field: FieldType) => {
const constant = findOnPage(config, field.name);
const constant = findConstantOnPage(field.name);
const tuneField = tune.constants[field.name];
const help = config.help[field.name];
let input;

View File

@ -34,17 +34,24 @@ import PerfectScrollbar from 'react-perfect-scrollbar';
// eslint-disable-next-line import/no-unresolved
import MlgParserWorker from 'worker-loader!../workers/mlgParser.worker';
import { loadLogs } from '../utils/api';
import Canvas, { LogEntry } from './Log/Canvas';
import Canvas, {
LogEntry,
SelectedField,
} from './Log/Canvas';
import {
AppState,
UIState,
} from '../types/state';
import { Config } from '../types/config';
import {
Config,
OutputChannel,
} from '../types/config';
import store from '../store';
import {
formatBytes,
msToTime,
} from '../utils/number';
import useConfig from '../hooks/useConfig';
const { TabPane } = Tabs;
const { Content } = Layout;
@ -92,19 +99,27 @@ const Log = ({ ui, config }: { ui: UIState, config: Config }) => {
'AFR',
'MAP',
]);
const prepareSelectedFields = useMemo(() => selectedFields.map((field) => {
const { isConfigReady, findOutputChannel, findDatalogNameByLabel, findDatalog } = useConfig(config);
const prepareSelectedFields = useMemo<SelectedField[]>(() => isConfigReady ? selectedFields.map((field) => {
const name = field.toString();
// TODO: parse [Datalog]
const logName = findDatalogNameByLabel(name);
const { format } = findDatalog(logName);
const { units, scale, transform } = findOutputChannel(logName) as OutputChannel;
return {
name,
units: '&', // out.units,
scale: 1, // out.scale,
transform: 1, // out.transform,
units,
scale,
transform,
format,
};
}), [selectedFields]);
}).filter((entry) => entry !== null) as [] : [], [
isConfigReady,
selectedFields,
findDatalogNameByLabel,
findDatalog,
findOutputChannel,
]);
useEffect(() => {
const worker = new MlgParserWorker();
@ -198,7 +213,15 @@ const Log = ({ ui, config }: { ui: UIState, config: Config }) => {
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
<Content>
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
{!logs ?
{logs
?
<Canvas
data={logs!.records as LogEntry[]}
width={canvasWidth}
height={600}
selectedFields={prepareSelectedFields}
/>
:
<Space
direction="vertical"
size="large"
@ -235,13 +258,6 @@ const Log = ({ ui, config }: { ui: UIState, config: Config }) => {
/>
</Steps>
</Space>
:
<Canvas
data={logs!.records as LogEntry[]}
width={canvasWidth}
height={600}
selectedFields={prepareSelectedFields}
/>
}
</div>
</Content>

View File

@ -18,6 +18,7 @@ import {
} from '../../utils/keyboard/shortcuts';
import {
colorHsl,
formatNumber,
msToTime,
remap,
} from '../../utils/number';
@ -43,6 +44,16 @@ export interface SelectedField {
units: string;
scale: string | number;
transform: string | number;
format: string;
};
export interface PlottableField {
min: number;
max: number;
scale: number;
transform: number;
units: string;
format: string;
};
const Canvas = ({
@ -78,7 +89,7 @@ const Canvas = ({
const canvas = canvasRef.current!;
const hsl = (fieldIndex: number, allFields: number) => {
const [hue] = colorHsl(0, allFields - 1, fieldIndex);
return `hsl(${hue}, 80%, 50%)`;
return `hsl(${hue}, 90%, 50%)`;
};
const ctx = canvas.getContext('2d')!;
const lastEntry = data[data.length - 1];
@ -91,29 +102,33 @@ const Canvas = ({
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
setRightBoundary(-(scaledWidth - areaWidth));
// find max values for each selected field so we can calculate scale
const fieldsToPlot: { [index: string]: { min: number, max: number, scale: number, transform: number } } = {};
const fieldsToPlot: { [index: string]: PlottableField } = {};
data.forEach((record) => {
selectedFields.forEach((field) => {
const value = record[field.name];
if (!fieldsToPlot[field.name]) {
fieldsToPlot[field.name] = {
selectedFields.forEach(({ name, scale, transform, units, format }) => {
const value = record[name];
if (!fieldsToPlot[name]) {
fieldsToPlot[name] = {
min: 0,
max: 0,
scale: field.scale as number,
transform: field.transform as number,
scale: scale as number,
transform: transform as number,
units,
format,
};
}
if (value > fieldsToPlot[field.name].max) {
fieldsToPlot[field.name].max = record[field.name] as number;
if (value > fieldsToPlot[name].max) {
fieldsToPlot[name].max = record[name] as number;
}
if (value < fieldsToPlot[field.name].min) {
fieldsToPlot[field.name].min = record[field.name] as number;
if (value < fieldsToPlot[name].min) {
fieldsToPlot[name].min = record[name] as number;
}
});
});
const fieldsKeys = Object.keys(fieldsToPlot);
// basic settings
ctx.font = '14px Arial';
@ -188,13 +203,17 @@ const Canvas = ({
}
let top = 0;
Object.keys(fieldsToPlot).forEach((name, fieldIndex) => {
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}: ${data[index][name]}`,
hsl(fieldIndex, Object.keys(fieldsToPlot).length),
`${name}: ${value}${units ? ` (${units})` : ''}`,
hsl(fieldIndex, fieldsKeys.length),
textAlign,
);
});
@ -215,11 +234,11 @@ const Canvas = ({
// clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
Object.keys(fieldsToPlot).forEach((name, fieldIndex) => plotField(
fieldsKeys.forEach((name, fieldIndex) => plotField(
name,
fieldsToPlot[name].min,
fieldsToPlot[name].max,
hsl(fieldIndex, Object.keys(fieldsToPlot).length)),
hsl(fieldIndex, fieldsKeys.length)),
);
drawIndicator();
}, [data, zoom, pan, rightBoundary, selectedFields, indicatorPos]);

View File

@ -23,6 +23,7 @@ import { Tune as TuneType } from '../types/tune';
import Icon from './SideBar/Icon';
import { evaluateExpression } from '../utils/tune/expression';
import { Routes } from '../routes';
import useConfig from '../hooks/useConfig';
const { Sider } = Layout;
const { SubMenu } = Menu;
@ -74,6 +75,7 @@ const SideBar = ({
collapsed: ui.sidebarCollapsed,
onCollapse: (collapsed: boolean) => store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed }),
} as any;
const { isConfigReady } = useConfig(config);
const checkCondition = useCallback((condition: string) => evaluateExpression(condition, tune.constants, config), [tune.constants, config]);
const buildLink = useCallback((main: string, sub: string) => generatePath(Routes.DIALOG, {
category: main,
@ -124,7 +126,7 @@ const SideBar = ({
})
), [buildLink, checkCondition]);
if (!config || !config.constants) {
if (!isConfigReady) {
return (
<Sider {...siderProps} className="app-sidebar" >
<div style={{ paddingLeft: 10 }}>

60
src/hooks/useConfig.ts Normal file
View File

@ -0,0 +1,60 @@
import { useMemo } from 'react';
import {
Config as ConfigType,
Page as PageType,
Constant,
OutputChannel,
SimpleConstant,
DatalogEntry,
} from '../types/config';
import { round } from '../utils/number';
const findConstantOnPage = (config: ConfigType, fieldName: string): Constant => {
const foundPage = config
.constants
.pages
.find((page: PageType) => fieldName in page.data) || { data: {} } as PageType;
if (!foundPage) {
throw new Error(`Constant [${fieldName}] not found`);
}
return foundPage.data[fieldName];
};
const findOutputChannel = (config: ConfigType, name: string): OutputChannel | SimpleConstant => {
const result = config.outputChannels[name];
if (!result) {
throw new Error(`Output channel [${name}] not found`);
}
return result;
};
const findDatalogNameByLabel = (config: ConfigType, label: string): string => {
const found = Object.keys(config.datalog).find((name: string) => config.datalog[name].label === label);
if (!found) {
throw new Error(`Datalog entry [${label}] not found`);
}
return found;
};
const findDatalog = (config: ConfigType, name: string): DatalogEntry => {
const result = config.datalog[name];
if (!result) {
throw new Error(`Datalog entry [${name}] not found`);
}
return result;
};
const useConfig = (config: ConfigType) => useMemo(() => ({
isConfigReady: !!config.constants,
findOutputChannel: (name: string) => findOutputChannel(config, name),
findConstantOnPage: (name: string) => findConstantOnPage(config, name),
findDatalogNameByLabel: (label: string) => findDatalogNameByLabel(config, label),
findDatalog: (name: string) => findDatalog(config, name),
}), [config]);
export default useConfig;

View File

@ -225,6 +225,7 @@ class INI {
.tryParse(line);
this.result.datalog[result.name] = {
name: result.name,
label: INI.sanitize(result.label),
type: result.type,
format: INI.sanitize(result.format),

View File

@ -166,6 +166,7 @@ export interface OutputChannels {
}
export interface DatalogEntry {
name: string;
label: string;
type: 'int' | 'float';
format: string;

View File

@ -1,16 +0,0 @@
import {
Config as ConfigType,
Page as PageType,
Constant,
} from '../../types/config';
export const findOnPage = (config: ConfigType, fieldName: string): Constant => {
const foundPage = config
.constants
.pages
.find((page: PageType) => fieldName in page.data) || { data: {} } as PageType;
return foundPage.data[fieldName];
};
export const todo = '';

View File

@ -42,3 +42,21 @@ export const colorHsl = (min: number, max: number, value: number): HslType => {
return [hue, saturation, lightness];
};
// eslint-disable-next-line prefer-template
export const round = (value: number, digits: number | string) => +(Math.round(value + `e+${digits}` as any) + `e-${digits}`);
export const formatNumber = (value: number, format: string): string => {
if (format === '%d') {
return `${Math.round(value)}`;
}
const match = format.match(/%\.(?<digits>\d)f/);
if (!match) {
throw new Error(`Datalog format [${format}] not supported`);
}
const { digits } = match.groups!;
return round(value, digits).toFixed(digits as any);
};