Merge remote-tracking branch 'origin/main'

This commit is contained in:
Kim 2023-05-11 13:26:18 +02:00
commit 69c105296f
12 changed files with 586 additions and 277 deletions

View File

@ -40,7 +40,6 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => {
.then(() => { .then(() => {
setToken(data.token); setToken(data.token);
saveCurrentUser(data.user); saveCurrentUser(data.user);
console.log(data.user);
setLoading(false); setLoading(false);
setLanguage(data.user.language); setLanguage(data.user.language);
}) })

View File

@ -34,7 +34,6 @@ const ResetPassword = () => {
}) })
.then((res) => { .then((res) => {
saveCurrentUser(res.data); saveCurrentUser(res.data);
console.log(res.data);
setLoading(false); setLoading(false);
}) })
.catch((err) => setError(err)); .catch((err) => setError(err));

View File

@ -0,0 +1,38 @@
import { createContext, ReactNode, SetStateAction, useState } from "react";
import { TreeElement, ToggleElement } from "../Installations/Log/CheckboxTree";
import React from "react";
interface LogContextProviderProps {
toggles: TreeElement[] | null;
setToggles: (value: TreeElement[]) => void;
checkedToggles: ToggleElement | null;
setCheckedToggles: React.Dispatch<SetStateAction<ToggleElement | null>>;
}
export const LogContext = createContext<LogContextProviderProps>({
toggles: [],
setToggles: () => {},
checkedToggles: {},
setCheckedToggles: () => {},
});
const LogContextProvider = ({ children }: { children: ReactNode }) => {
const [toggles, setToggles] = useState<TreeElement[] | null>(null);
const [checkedToggles, setCheckedToggles] = useState<ToggleElement | null>(
null
);
return (
<LogContext.Provider
value={{
toggles,
setToggles,
checkedToggles,
setCheckedToggles,
}}
>
{children}
</LogContext.Provider>
);
};
export default LogContextProvider;

View File

@ -90,7 +90,6 @@ const UsersContextProvider = ({ children }: { children: ReactNode }) => {
const fetchAvailableUsers = async (): Promise<void> => { const fetchAvailableUsers = async (): Promise<void> => {
return axiosConfig.get("/GetAllChildUsers").then((res) => { return axiosConfig.get("/GetAllChildUsers").then((res) => {
console.log(res);
setAvailableUsers(res.data); setAvailableUsers(res.data);
}); });
}; };

View File

@ -8,30 +8,35 @@ import InstallationContextProvider from "../Context/InstallationContextProvider"
import SearchSidebar from "../Layout/Search"; import SearchSidebar from "../Layout/Search";
import InstallationList from "./InstallationList"; import InstallationList from "./InstallationList";
import Installation from "./Installation"; import Installation from "./Installation";
import CheckboxTree from "./Log/CheckboxTree";
import LogContextProvider from "../Context/LogContextProvider";
const Installations = () => { const Installations = () => {
return ( return (
<InstallationContextProvider> <InstallationContextProvider>
<Grid container spacing={2} height="100%"> <LogContextProvider>
<Grid item xs={3}> <Grid container spacing={2} height="100%">
<SearchSidebar <Grid item xs={3}>
id="installations-search-sidebar" <SearchSidebar
listComponent={InstallationList} id="installations-search-sidebar"
/> listComponent={InstallationList}
</Grid>
<Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={<Installation />}
index
/> />
<Route path={routes.alarms + ":id"} element={<Alarms />} /> <CheckboxTree />
<Route path={routes.log + ":id"} element={<Log />} /> </Grid>
</Routes> <Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={<Installation />}
index
/>
<Route path={routes.alarms + ":id"} element={<Alarms />} />
<Route path={routes.log + ":id"} element={<Log />} />
</Routes>
</Grid>
</Grid> </Grid>
</Grid> </LogContextProvider>
</InstallationContextProvider> </InstallationContextProvider>
); );
}; };

View File

@ -0,0 +1,109 @@
import { TreeItem, TreeView } from "@mui/lab";
import { Checkbox, Divider } from "@mui/material";
import { useContext, ReactNode } from "react";
import { LogContext } from "../../Context/LogContextProvider";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import useRouteMatch from "../../../hooks/useRouteMatch";
import routes from "../../../routes.json";
export interface ToggleElement {
[key: string]: boolean;
}
export interface TreeElement {
id: string;
name: string;
children: TreeElement[];
checked?: boolean;
}
const CheckboxTree = () => {
const { toggles, setCheckedToggles, checkedToggles } = useContext(LogContext);
const routeMatch = useRouteMatch([
routes.installations + routes.list + routes.log + ":id",
]);
const getNodes = (element: TreeElement): null | ReactNode => {
return element.children.length > 0 ? renderTree(element.children) : null;
};
const handleCheckChildren = (children: TreeElement[], checked?: boolean) => {
if (children.length > 0) {
children.forEach((child) => {
setCheckedToggles((prevState) => ({
...prevState,
[child.id]: !checked,
}));
if (child.children.length > 0) {
handleCheckChildren(child.children, checked);
}
});
}
};
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
element: TreeElement,
checked?: boolean
) => {
event.stopPropagation();
handleCheckChildren([element], checked);
};
const handleExpandClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>
) => {
event.stopPropagation();
};
const renderTree = (data: TreeElement[]): ReactNode => {
return data.map((element) => {
const checked = checkedToggles?.[element.id];
const splitName = element.name.split("/");
return (
<TreeItem
id={"checkbox-tree-" + element.name}
key={element.name}
nodeId={element.name}
onClick={handleExpandClick}
label={
<>
<Checkbox
checked={checked}
onClick={(e) => handleClick(e, element, checked)}
/>
{splitName[splitName.length - 1]}
</>
}
sx={{
".MuiTreeItem-content": { paddingY: "5px" },
}}
>
{getNodes(element)}
</TreeItem>
);
});
};
return (
<>
<Divider sx={{ mt: 2 }} />
{toggles !== null && routeMatch !== null && (
<TreeView
aria-label="rich object"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{
height: 480,
flexGrow: 1,
overflow: "auto",
overflowX: "hidden",
}}
>
{renderTree(toggles)}
</TreeView>
)}
</>
);
};
export default CheckboxTree;

View File

@ -1,65 +1,10 @@
import React, { useState } from "react"; import React from "react";
import ScalarGraph from "./ScalarGraph"; import ScalarGraph from "./ScalarGraph";
import DataCache, { FetchResult } from "../../../dataCache/dataCache";
import { TimeSpan, UnixTime } from "../../../dataCache/time";
import { S3Access } from "../../../dataCache/S3/S3Access";
import { map, debounceTime } from "rxjs/operators";
import { RecordSeries } from "../../../dataCache/data";
import { parseCsv } from "../../../util/graph.util";
const Log = () => { const Log = () => {
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
const s3Access = new S3Access(
"saliomameiringen",
"sos-ch-dk-2",
"exo.io",
"EXO18e7ae9e53fae71ee55cf35b",
"3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc",
""
);
const fetchData = (
timestamp: UnixTime
): Promise<FetchResult<Record<string, number>>> => {
const s3Path = `${timestamp.ticks}.csv`;
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
const text = await r.text();
return parseCsv(text);
} else {
console.error("unexpected status code");
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
console.log(e);
return Promise.resolve(FetchResult.tryLater);
});
};
const cache = new DataCache(fetchData, TimeSpan.fromSeconds(2));
const sampleTimes = UnixTime.fromTicks(1682085650)
.earlier(TimeSpan.fromMinutes(1))
.rangeBefore(TimeSpan.fromMinutes(1))
.sample(TimeSpan.fromSeconds(2));
cache.getSeries(sampleTimes);
const update = cache.gotData.pipe(
debounceTime(2000),
map((_) => setTimeSeries(cache.getSeries(sampleTimes)))
);
update.subscribe();
return ( return (
<> <>
<ScalarGraph data={timeSeries} /> <ScalarGraph />
</> </>
); );
}; };

View File

@ -1,26 +1,243 @@
import Plot from "react-plotly.js"; import Plot from "react-plotly.js";
import { RecordSeries } from "../../../dataCache/data"; import { RecordSeries } from "../../../dataCache/data";
import { transformToGraphData } from "../../../util/graph.util"; import {
GraphCoordinates,
GraphData,
mergeDeep,
parseCsv,
} from "../../../util/graph.util";
import { TimeRange, TimeSpan, UnixTime } from "../../../dataCache/time";
import { useContext, useEffect, useMemo, useState } from "react";
import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from "rxjs";
import { S3Access } from "../../../dataCache/S3/S3Access";
import DataCache, { FetchResult } from "../../../dataCache/dataCache";
interface I_ScalarGraphProps { import { LogContext } from "../../Context/LogContextProvider";
data: RecordSeries; import { TreeElement, ToggleElement } from "./CheckboxTree";
} import { isDefined } from "../../../dataCache/utils/maybe";
import { timeStamp } from "console";
export const createTimes = (
range: TimeRange,
numberOfNodes: number
): UnixTime[] => {
const oneSpan = range.duration.divide(numberOfNodes);
const roundedRange = TimeRange.fromTimes(
range.start.round(oneSpan),
range.end.round(oneSpan)
);
return roundedRange.sample(oneSpan);
};
const NUMBER_OF_NODES = 100;
const ScalarGraph = () => {
const timeRange = createTimes(
UnixTime.now() /* .fromTicks(1682085650) */
.rangeBefore(TimeSpan.fromDays(4)),
NUMBER_OF_NODES
);
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
const [range, setRange] = useState([
timeRange[0].toDate().getTime(),
timeRange[timeRange.length - 1].toDate().getTime(),
]);
const [uiRevision, setUiRevision] = useState(Math.random());
const [plotTitles, setPlotTitles] = useState<string[]>([]);
const { toggles, setToggles, setCheckedToggles, checkedToggles } =
useContext(LogContext);
const times$ = useMemo(() => new BehaviorSubject(timeRange), []);
const s3Access = new S3Access(
"saliomameiringen",
"sos-ch-dk-2",
"exo.io",
"EXO18e7ae9e53fae71ee55cf35b",
"3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc",
""
);
const insert = (
children: TreeElement[] = [],
[head, ...tail]: string[]
): TreeElement[] => {
let child = children.find((child) => child.name === head);
if (!child) {
children.push(
(child = {
id: head,
name: head,
children: [],
})
);
}
if (tail.length > 0) {
insert(child.children, tail);
}
return children;
};
const flattenToggles = (toggles: TreeElement[]): ToggleElement => {
return toggles.reduce((acc, current) => {
if (current.children.length > 0) {
acc[current.id] = false;
return { ...acc, ...flattenToggles(current.children) };
}
acc[current.id] = false;
return acc;
}, {} as ToggleElement);
};
useEffect(() => {
const subscription = cache.gotData
.pipe(
startWith(0),
throttleTime(200, undefined, { leading: true, trailing: true }),
withLatestFrom(times$)
)
.subscribe(([_, times]) => {
const timeSeries = cache.getSeries(times);
setTimeSeries(timeSeries);
const toggleValues = timeSeries.find((timeStamp) => timeStamp.value);
if (toggles === null && toggleValues && toggleValues.value) {
const treeElements = Object.keys(toggleValues.value)
.map((path) => {
return path
.split("/")
.map(
(split, i) =>
"/" +
path
.split("/")
.slice(1, i + 1)
.join("/")
)
.slice(1);
})
.reduce(
(children, path) => insert(children, path),
[] as TreeElement[]
);
setToggles(treeElements);
setCheckedToggles(flattenToggles(treeElements));
}
});
return () => subscription.unsubscribe();
}, [toggles]);
const fetchData = (
timestamp: UnixTime
): Promise<FetchResult<Record<string, number>>> => {
const s3Path = `${timestamp.ticks}.csv`;
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
const text = await r.text();
return parseCsv(text);
} else {
console.error("unexpected status code");
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
};
const cache = useMemo(
() => new DataCache(fetchData, TimeSpan.fromSeconds(2)),
[]
);
const transformToGraphData = (timeStampData: RecordSeries) => {
const graphData = timeStampData.reduce((acc, curr) => {
if (isDefined(curr.value)) {
const timeStampObj = Object.keys(curr.value).reduce(
(pathAcc, currPath) => {
if (currPath) {
return {
...pathAcc,
[currPath]: {
x: [new Date(curr.time.ticks * 1000)],
y: [curr.value ? curr.value[currPath] : 0],
},
};
}
return pathAcc;
},
{} as GraphData
);
if (plotTitles.length === 0) {
setPlotTitles(Object.keys(curr.value));
}
return mergeDeep(acc, timeStampObj);
}
return acc;
}, {} as GraphData);
if (Object.keys(graphData).length > 0) {
return graphData;
}
return plotTitles.reduce(
(acc, curr) => ({
...acc,
[curr]: {
x: [],
y: [],
},
}),
{} as GraphData
);
};
const ScalarGraph = (props: I_ScalarGraphProps) => {
const renderGraphs = () => { const renderGraphs = () => {
const coordinateTimeSeries = transformToGraphData(props.data); const coordinateTimeSeries = transformToGraphData(timeSeries);
return Object.keys(coordinateTimeSeries).map((path) => { console.log("coordinates", coordinateTimeSeries);
const graphCoordinates: GraphCoordinates[] = Object.keys(
coordinateTimeSeries
)
.filter((path) => {
return checkedToggles?.[path];
})
.map((path, i) => {
return {
...coordinateTimeSeries[path],
xaxis: "x",
yaxis: i === 0 ? "y" : "y" + (i + 1),
type: "scatter",
};
});
if (checkedToggles && graphCoordinates.length > 0) {
const subplots = graphCoordinates.map((coordinate) => [
(coordinate?.xaxis || "") + (coordinate.yaxis || ""),
]);
return ( return (
<Plot <Plot
data={[ data={graphCoordinates}
{ layout={{
...coordinateTimeSeries[path], title: "Graphs",
type: "scatter", uirevision: uiRevision,
mode: "lines+markers", showlegend: false,
marker: { color: "red" }, xaxis: {
autorange: false,
range: range,
type: "date",
showticklabels: true,
}, },
]} grid: {
layout={{ width: 1000, height: 500, title: path }} subplots: subplots as any,
xside: "top",
ygap: 0.1,
},
height: graphCoordinates.length * 300,
}}
config={{ config={{
modeBarButtonsToRemove: [ modeBarButtonsToRemove: [
"lasso2d", "lasso2d",
@ -29,14 +246,32 @@ const ScalarGraph = (props: I_ScalarGraphProps) => {
"autoScale2d", "autoScale2d",
], ],
}} }}
onUpdate={(figure) => { onRelayout={(params) => {
//console.log(figure); const xaxisRange0 = params["xaxis.range[0]"];
const xaxisRange1 = params["xaxis.range[1]"];
if (xaxisRange0 && xaxisRange1) {
setRange([
new Date(xaxisRange0).getTime(),
new Date(xaxisRange1).getTime(),
]);
setUiRevision(Math.random());
const times = createTimes(
TimeRange.fromTimes(
UnixTime.fromDate(new Date(xaxisRange0)),
UnixTime.fromDate(new Date(xaxisRange1))
),
NUMBER_OF_NODES
);
console.log("times", times);
cache.getSeries(times);
times$.next(times);
}
}} }}
/> />
); );
}); }
}; };
return <>{renderGraphs()}</>; return <>{renderGraphs()}</>;
}; };
export default ScalarGraph; export default ScalarGraph;

View File

@ -12,7 +12,7 @@ const SearchSidebar = (props: SearchSidebarProps) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div style={{ height: "100%" }}> <div style={{ height: "500px", overflow: "hidden" }}>
<TextField <TextField
id={id} id={id}
label={intl.formatMessage({ label={intl.formatMessage({

View File

@ -2,12 +2,12 @@ import { Maybe } from "yup";
import {Timestamped} from "./types"; import {Timestamped} from "./types";
import { isDefined } from "./utils/maybe"; import { isDefined } from "./utils/maybe";
export type DataRecord = Record<string, number> export type DataRecord = Record<string, number | string>
export type DataPoint = Timestamped<Maybe<DataRecord>> export type DataPoint = Timestamped<Maybe<DataRecord>>
export type RecordSeries = Array<DataPoint> export type RecordSeries = Array<DataPoint>
export type PointSeries = Array<Timestamped<Maybe<number>>> export type PointSeries = Array<Timestamped<Maybe<number| string>>>
export type DataSeries = Array<Maybe<number>> export type DataSeries = Array<Maybe<number| string>>
export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries
{ {

View File

@ -1,169 +1,158 @@
/* eslint-disable no-mixed-operators */ /* eslint-disable no-mixed-operators */
import {TimeSpan, UnixTime} from "./time"; import { TimeSpan, UnixTime } from "./time";
import {Observable, Subject} from "rxjs"; import { Observable, Subject } from "rxjs";
import {SkipList} from "./skipList/skipList"; import { SkipList } from "./skipList/skipList";
import {createDispatchQueue} from "./promiseQueue"; import { createDispatchQueue } from "./promiseQueue";
import {SkipListNode} from "./skipList/skipListNode"; import { SkipListNode } from "./skipList/skipListNode";
import {RecordSeries} from "./data"; import { RecordSeries } from "./data";
import { Maybe, isUndefined } from "./utils/maybe"; import { Maybe, isUndefined } from "./utils/maybe";
import { isNumber, isString } from "./utils/runtimeTypeChecking";
export const FetchResult = {
export const FetchResult = notAvailable: "N/A",
{ tryLater: "Try Later",
notAvailable : "N/A", } as const;
tryLater : "Try Later"
} as const
export type FetchResult<T> = export type FetchResult<T> =
| T | T
| typeof FetchResult.notAvailable | typeof FetchResult.notAvailable
| typeof FetchResult.tryLater | typeof FetchResult.tryLater;
function reverseBits(x : number): number function reverseBits(x: number): number {
{ // https://stackoverflow.com/a/60227327/141397
// https://stackoverflow.com/a/60227327/141397
x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >> 1; x = ((x & 0x55555555) << 1) | ((x & 0xaaaaaaaa) >> 1);
x = (x & 0x33333333) << 2 | (x & 0xCCCCCCCC) >> 2; x = ((x & 0x33333333) << 2) | ((x & 0xcccccccc) >> 2);
x = (x & 0x0F0F0F0F) << 4 | (x & 0xF0F0F0F0) >> 4; x = ((x & 0x0f0f0f0f) << 4) | ((x & 0xf0f0f0f0) >> 4);
x = (x & 0x00FF00FF) << 8 | (x & 0xFF00FF00) >> 8; x = ((x & 0x00ff00ff) << 8) | ((x & 0xff00ff00) >> 8);
x = (x & 0x0000FFFF) << 16 | (x & 0xFFFF0000) >> 16; x = ((x & 0x0000ffff) << 16) | ((x & 0xffff0000) >> 16);
return x >>> 0; return x >>> 0;
} }
export default class DataCache<T extends Record<string, number>> {
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>();
private readonly resolution: TimeSpan;
export default class DataCache<T extends Record<string, number>> readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>;
{
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>()
private readonly resolution: TimeSpan;
readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>; private readonly fetchQueue = createDispatchQueue(6);
private readonly fetching: Set<number> = new Set<number>();
private readonly fetchQueue = createDispatchQueue(6) public readonly gotData: Observable<UnixTime>;
private readonly fetching: Set<number> = new Set<number>()
public readonly gotData: Observable<UnixTime>; constructor(
fetch: (t: UnixTime) => Promise<FetchResult<T>>,
resolution: TimeSpan
) {
this._fetch = fetch;
this.resolution = resolution;
this.gotData = new Subject<UnixTime>();
}
constructor(fetch: (t: UnixTime) => Promise<FetchResult<T>>, resolution: TimeSpan) public prefetch(times: Array<UnixTime>, clear = true) {
{ if (clear) {
this._fetch = fetch; this.fetching.clear();
this.resolution = resolution; this.fetchQueue.clear();
this.gotData = new Subject<UnixTime>()
} }
public prefetch(times: Array<UnixTime>, clear = true) const timesWithPriority = times.map((time, index) => ({
{ time,
if (clear) priority: reverseBits(index),
{ }));
this.fetching.clear() timesWithPriority.sort((x, y) => x.priority - y.priority);
this.fetchQueue.clear()
for (let i = 0; i < timesWithPriority.length; i++) {
const time = timesWithPriority[i].time.round(this.resolution);
const t = time.ticks;
const node = this.cache.find(t);
if (node.index !== t) this.fetchData(time);
}
}
public get(timeStamp: UnixTime, fetch = true): Maybe<T> {
const time = timeStamp.round(this.resolution);
const t = time.ticks;
const node = this.cache.find(t);
if (node.index === t) return node.value;
if (fetch) this.fetchData(time);
return this.interpolate(node, t);
}
public getSeries(sampleTimes: UnixTime[]): RecordSeries {
this.prefetch(sampleTimes);
return sampleTimes.map((time) => ({ time, value: this.get(time, false) }));
}
private interpolate(before: SkipListNode<Maybe<T>>, t: number): Maybe<T> {
const dataBefore = before.value;
const after = before.next[0];
const dataAfter = after.value;
if (isUndefined(dataBefore) && isUndefined(dataAfter)) return undefined;
if (isUndefined(dataBefore)) return dataAfter;
if (isUndefined(dataAfter)) return dataBefore;
const p = t - before.index;
const n = after.index - t;
const pn = p + n;
let interpolated: Partial<Record<string, number>> = {};
//What about string nodes? like Alarms
for (const k of Object.keys(dataBefore)) {
interpolated[k] = isNumber(dataBefore[k])
? (dataBefore[k] * n + dataAfter[k] * p) / pn
: n < p
? dataAfter[k]
: dataBefore[k];
}
return interpolated as T;
}
private fetchData(time: UnixTime) {
const t = time.ticks;
if (this.fetching.has(t))
// we are already fetching t
return;
const fetchTask = () => {
const onSuccess = (data: FetchResult<T>) => {
if (data === FetchResult.tryLater) {
console.warn(FetchResult.tryLater);
return;
} }
const timesWithPriority = times.map((time, index) => ({time, priority: reverseBits(index)})) const value = data === FetchResult.notAvailable ? undefined : data;
timesWithPriority.sort((x, y) => x.priority - y.priority) this.cache.insert(value, t);
};
for (let i = 0; i < timesWithPriority.length; i++) const onFailure = (_: unknown) => {
{ console.error(time.ticks + " FAILED!"); // should not happen
const time = timesWithPriority[i].time.round(this.resolution) };
const t = time.ticks;
const node = this.cache.find(t); const dispatch = () => {
if (node.index !== t) this.fetching.delete(time.ticks);
this.fetchData(time); (this.gotData as Subject<UnixTime>).next(time);
} };
}
public get(timeStamp: UnixTime, fetch = true): Maybe<T> return this._fetch(time)
{ .then(
const time = timeStamp.round(this.resolution) (d) => onSuccess(d),
const t = time.ticks; (f) => onFailure(f)
)
const node = this.cache.find(t); .finally(() => dispatch());
if (node.index === t) };
return node.value
if (fetch)
this.fetchData(time);
return this.interpolate(node, t)
}
public getSeries(sampleTimes: UnixTime[]): RecordSeries
{
this.prefetch(sampleTimes)
return sampleTimes.map(time => ({time, value: this.get(time, false)}))
}
private interpolate(before: SkipListNode<Maybe<T>>, t: number): Maybe<T>
{
const dataBefore = before.value
const after = before.next[0];
const dataAfter = after.value
if (isUndefined(dataBefore) && isUndefined(dataAfter))
return undefined
if (isUndefined(dataBefore))
return dataAfter
if (isUndefined(dataAfter))
return dataBefore
const p = t - before.index
const n = after.index - t
const pn = p + n
let interpolated: Partial<Record<string, number>> = {}
//What about string nodes? like Alarms
for (const k of Object.keys(dataBefore))
{
interpolated[k] = (dataBefore[k] * n + dataAfter[k] * p) / pn
}
return interpolated as T
}
private fetchData(time: UnixTime)
{
const t = time.ticks;
if (this.fetching.has(t)) // we are already fetching t
return
const fetchTask = () =>
{
const onSuccess = (data: FetchResult<T>) =>
{
if (data === FetchResult.tryLater)
{
console.warn(FetchResult.tryLater)
return
}
const value = data === FetchResult.notAvailable ? undefined : data;
this.cache.insert(value, t)
}
const onFailure = (_: unknown) =>
{
console.error(time.ticks + " FAILED!") // should not happen
}
const dispatch = () =>
{
this.fetching.delete(time.ticks);
(this.gotData as Subject<UnixTime>).next(time);
}
return this._fetch(time)
.then(d => onSuccess(d), f => onFailure(f))
.finally(() => dispatch())
};
this.fetching.add(t)
this.fetchQueue.dispatch(() => fetchTask());
}
this.fetching.add(t);
this.fetchQueue.dispatch(() => fetchTask());
}
} }

View File

@ -1,6 +1,4 @@
import { Datum, TypedArray } from "plotly.js"; import { Datum, TypedArray } from "plotly.js";
import { RecordSeries } from "../dataCache/data";
import { isDefined } from "../dataCache/utils/maybe";
export const mergeDeep = (...objects: any[]) => { export const mergeDeep = (...objects: any[]) => {
const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object"; const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object";
@ -22,33 +20,11 @@ export const mergeDeep = (...objects: any[]) => {
}, {} as GraphData); }, {} as GraphData);
}; };
export const transformToGraphData = (timeStampData: RecordSeries) => {
return timeStampData.reduce((acc, curr) => {
if (isDefined(curr.value)) {
const timeStampObj = Object.keys(curr.value).reduce(
(pathAcc, currPath) => {
if (currPath) {
return {
...pathAcc,
[currPath]: {
x: [curr.time.ticks],
y: [curr.value ? curr.value[currPath] : 0],
},
};
}
return pathAcc;
},
{} as GraphData
);
return mergeDeep(acc, timeStampObj);
}
return acc;
}, {} as GraphData);
};
export interface GraphCoordinates { export interface GraphCoordinates {
x: Datum[] | Datum[][] | TypedArray; x: Datum[] | Datum[][] | TypedArray;
y: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray;
xaxis?: string;
yaxis?: string;
} }
export interface GraphData { export interface GraphData {
@ -58,11 +34,26 @@ export interface GraphData {
export const parseCsv = (text: string) => { export const parseCsv = (text: string) => {
const y = text const y = text
.split(/\r?\n/) .split(/\r?\n/)
.map((l) => l.split(";")) .filter((split) => split.length > 0)
.filter((fields) => !isNaN(parseFloat(fields[1]))); .map((l) => {
if (l.length === 0) {
console.log("splitting", l, l.split(";"));
}
return l.split(";");
});
console.log("text", y);
/* .filter((fields) => !isNaN(parseFloat(fields[1])));
*/
const x = y const x = y
.map((fields) => ({ [fields[0]]: parseFloat(fields[1]) })) .map((fields) => {
.reduce((acc, current) => ({ ...acc, ...current }), {}); if (typeof fields[1] === "string") {
console.log("if inside", fields, { [fields[0]]: fields[1] });
return { [fields[0]]: fields[1] };
}
console.log("if outside", fields, { [fields[0]]: parseFloat(fields[1]) });
return { [fields[0]]: parseFloat(fields[1]) };
})
.reduce((acc, current) => ({ ...acc, ...current }), {} as any);
return x; return x;
}; };