Apply value transformation, add units, refactor config functions (#50)
This commit is contained in:
parent
8f1665ba3c
commit
75967afeaf
|
@ -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
|
||||
|
|
12
src/App.tsx
12
src/App.tsx
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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;
|
|
@ -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),
|
||||
|
|
|
@ -166,6 +166,7 @@ export interface OutputChannels {
|
|||
}
|
||||
|
||||
export interface DatalogEntry {
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'int' | 'float';
|
||||
format: string;
|
||||
|
|
|
@ -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 = '';
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue