From 9fa0fd1f22b49938ce18a2d7b31333490305e464 Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Wed, 26 Apr 2023 13:54:57 +0200 Subject: [PATCH 1/5] add working (not yet perfect) graphs, all have same range and toggle for graphs not there yet --- .../src/components/Installations/Log/Log.tsx | 59 +-------- .../Installations/Log/ScalarGraph.tsx | 122 ++++++++++++++++-- typescript/Frontend/src/util/graph.util.tsx | 3 +- 3 files changed, 116 insertions(+), 68 deletions(-) diff --git a/typescript/Frontend/src/components/Installations/Log/Log.tsx b/typescript/Frontend/src/components/Installations/Log/Log.tsx index 550ef8f83..d1156f14e 100644 --- a/typescript/Frontend/src/components/Installations/Log/Log.tsx +++ b/typescript/Frontend/src/components/Installations/Log/Log.tsx @@ -1,65 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; 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 [timeSeries, setTimeSeries] = useState([]); - const s3Access = new S3Access( - "saliomameiringen", - "sos-ch-dk-2", - "exo.io", - "EXO18e7ae9e53fae71ee55cf35b", - "3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc", - "" - ); - - const fetchData = ( - timestamp: UnixTime - ): Promise>> => { - 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 ( <> - + ); }; diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 69e6e5218..655eb220f 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,26 +1,112 @@ import Plot from "react-plotly.js"; import { RecordSeries } from "../../../dataCache/data"; -import { transformToGraphData } from "../../../util/graph.util"; +import { parseCsv, transformToGraphData } from "../../../util/graph.util"; +import { TimeSpan, UnixTime } from "../../../dataCache/time"; +import { 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 { - data: RecordSeries; -} +const ScalarGraph = () => { + const timeRange = UnixTime.fromTicks(1682085650) + .rangeBefore(TimeSpan.fromDays(1)) + .sample(TimeSpan.fromMinutes(30)); + const [timeSeries, setTimeSeries] = useState([]); + const [uiRevision, setUiRevision] = useState(Math.random()); + const [range, setRange] = useState([ + timeRange[0].toDate(), + timeRange[timeRange.length - 1].toDate(), + ]); + const [toggles, setToggles] = useState(null); + + const times$ = useMemo(() => new BehaviorSubject(timeRange), []); + + const s3Access = new S3Access( + "saliomameiringen", + "sos-ch-dk-2", + "exo.io", + "EXO18e7ae9e53fae71ee55cf35b", + "3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc", + "" + ); + + useEffect(() => { + const subscription = cache.gotData + .pipe( + startWith(0), + throttleTime(200, undefined, { leading: true, trailing: true }), + withLatestFrom(times$) + ) + .subscribe(([_, times]) => { + const timeSeries = cache.getSeries(times); + console.log("GOT DATA", _, times, timeSeries); + setTimeSeries(timeSeries); + if (toggles === null) setToggles(timeSeries); + }); + + return () => subscription.unsubscribe(); + }, []); + + const roundToNearest30 = (date: Date) => { + const minutes = 30; + const ms = 1000 * 60 * minutes; + + return new Date(Math.round(date.getTime() / ms) * ms); + }; + + const fetchData = ( + timestamp: UnixTime + ): Promise>> => { + 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 = useMemo( + () => new DataCache(fetchData, TimeSpan.fromSeconds(2)), + [] + ); -const ScalarGraph = (props: I_ScalarGraphProps) => { const renderGraphs = () => { - const coordinateTimeSeries = transformToGraphData(props.data); + const coordinateTimeSeries = transformToGraphData(timeSeries); return Object.keys(coordinateTimeSeries).map((path) => { + const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; return ( { "autoScale2d", ], }} - onUpdate={(figure) => { - //console.log(figure); + onRelayout={(params) => { + console.log("PARAMS", params); + const xaxisRange0 = params["xaxis.range[0]"]; + const xaxisRange1 = params["xaxis.range[1]"]; + + if (xaxisRange0 && xaxisRange1) { + const epoch0 = + roundToNearest30(new Date(xaxisRange0)).getTime() / 1000; + const epoch1 = + roundToNearest30(new Date(xaxisRange1)).getTime() / 1000; + setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); + setUiRevision(Math.random()); + const times = UnixTime.fromTicks(epoch1) + .rangeBefore(TimeSpan.fromSeconds(epoch1 - epoch0)) + .sample(TimeSpan.fromMinutes(30)); + cache.getSeries(times); + times$.next(times); + } }} /> ); diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index 8bbf435c8..b1a56f793 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -31,7 +31,7 @@ export const transformToGraphData = (timeStampData: RecordSeries) => { return { ...pathAcc, [currPath]: { - x: [curr.time.ticks], + x: [new Date(curr.time.ticks * 1000)], y: [curr.value ? curr.value[currPath] : 0], }, }; @@ -40,6 +40,7 @@ export const transformToGraphData = (timeStampData: RecordSeries) => { }, {} as GraphData ); + console.log("acc", acc, timeStampObj); return mergeDeep(acc, timeStampObj); } return acc; From b7c443fc93c05727b0e004050ee26e5d9c4ae3be Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Tue, 2 May 2023 11:08:08 +0200 Subject: [PATCH 2/5] round graph times correctly --- .../Installations/Log/ScalarGraph.tsx | 153 +++++++++++++++--- typescript/Frontend/src/util/graph.util.tsx | 1 - 2 files changed, 134 insertions(+), 20 deletions(-) diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 655eb220f..95c5c57cf 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,23 +1,42 @@ import Plot from "react-plotly.js"; import { RecordSeries } from "../../../dataCache/data"; import { parseCsv, transformToGraphData } from "../../../util/graph.util"; -import { TimeSpan, UnixTime } from "../../../dataCache/time"; -import { useEffect, useMemo, useState } from "react"; +import { TimeRange, TimeSpan, UnixTime } from "../../../dataCache/time"; +import { ReactNode, useEffect, useMemo, useState } from "react"; import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from "rxjs"; import { S3Access } from "../../../dataCache/S3/S3Access"; import DataCache, { FetchResult } from "../../../dataCache/dataCache"; +import { TreeItem, TreeView } from "@mui/lab"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { Checkbox } from "@mui/material"; + +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 = UnixTime.fromTicks(1682085650) - .rangeBefore(TimeSpan.fromDays(1)) - .sample(TimeSpan.fromMinutes(30)); + const timeRange = createTimes( + UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(1)), + NUMBER_OF_NODES + ); const [timeSeries, setTimeSeries] = useState([]); const [uiRevision, setUiRevision] = useState(Math.random()); const [range, setRange] = useState([ timeRange[0].toDate(), timeRange[timeRange.length - 1].toDate(), ]); - const [toggles, setToggles] = useState(null); + const [toggles, setToggles] = useState(null); const times$ = useMemo(() => new BehaviorSubject(timeRange), []); @@ -30,6 +49,24 @@ const ScalarGraph = () => { "" ); + function insert( + children: TreeElement[] = [], + [head, ...tail]: string[] + ): TreeElement[] { + let child = children.find((child) => child.name === head); + if (!child) + children.push( + (child = { + id: head + tail.join("_"), + name: head, + checked: false, + children: [], + }) + ); + if (tail.length > 0) insert(child.children, tail); + return children; + } + useEffect(() => { const subscription = cache.gotData .pipe( @@ -39,15 +76,76 @@ const ScalarGraph = () => { ) .subscribe(([_, times]) => { const timeSeries = cache.getSeries(times); - console.log("GOT DATA", _, times, timeSeries); setTimeSeries(timeSeries); - if (toggles === null) setToggles(timeSeries); + const toggleValues = timeSeries.find((timeStamp) => timeStamp.value); + + if (toggles === null && toggleValues && toggleValues.value) { + setToggles( + Object.keys(toggleValues.value) + .map((path) => path.split("/").slice(1)) + .reduce( + (children, path) => insert(children, path), + [] as TreeElement[] + ) + ); + } }); return () => subscription.unsubscribe(); }, []); - const roundToNearest30 = (date: Date) => { + interface TreeElement { + id: string; + name: string; + children: TreeElement[]; + checked: boolean; + } + + const getNodes = (element: TreeElement): null | ReactNode => { + return element.children.length > 0 ? renderTree(element.children) : null; + }; + + const renderTree = (data: TreeElement[]): ReactNode => { + return data.map((element) => { + return ( + + { + if (toggles) { + const togglesValue = toggles; + const index = toggles?.findIndex((toggle) => { + return element.id === toggle.id; + }); + if (index > 0) { + togglesValue[index] = { + ...element, + checked: !element.checked, + }; + setToggles(togglesValue); + } + } + }} + /> + {element.name} + + } + sx={{ + ".MuiTreeItem-content": { paddingY: "12px" }, + }} + > + {getNodes(element)} + + ); + }); + }; + + const round = (date: Date) => { const minutes = 30; const ms = 1000 * 60 * minutes; @@ -72,7 +170,6 @@ const ScalarGraph = () => { } }) .catch((e) => { - console.log(e); return Promise.resolve(FetchResult.tryLater); }); }; @@ -116,20 +213,19 @@ const ScalarGraph = () => { ], }} onRelayout={(params) => { - console.log("PARAMS", params); const xaxisRange0 = params["xaxis.range[0]"]; const xaxisRange1 = params["xaxis.range[1]"]; if (xaxisRange0 && xaxisRange1) { - const epoch0 = - roundToNearest30(new Date(xaxisRange0)).getTime() / 1000; - const epoch1 = - roundToNearest30(new Date(xaxisRange1)).getTime() / 1000; setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); setUiRevision(Math.random()); - const times = UnixTime.fromTicks(epoch1) - .rangeBefore(TimeSpan.fromSeconds(epoch1 - epoch0)) - .sample(TimeSpan.fromMinutes(30)); + const times = createTimes( + TimeRange.fromTimes( + UnixTime.fromDate(new Date(xaxisRange0)), + UnixTime.fromDate(new Date(xaxisRange1)) + ), + NUMBER_OF_NODES + ); cache.getSeries(times); times$.next(times); } @@ -139,6 +235,25 @@ const ScalarGraph = () => { }); }; - return <>{renderGraphs()}; + return ( + <> + {renderGraphs()} + {toggles !== null && ( + } + defaultExpandIcon={} + sx={{ + height: 480, + flexGrow: 1, + overflow: "auto", + overflowX: "hidden", + }} + > + {renderTree(toggles)} + + )} + + ); }; export default ScalarGraph; diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index b1a56f793..040c091dd 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -40,7 +40,6 @@ export const transformToGraphData = (timeStampData: RecordSeries) => { }, {} as GraphData ); - console.log("acc", acc, timeStampObj); return mergeDeep(acc, timeStampObj); } return acc; From d722f42d992db502e2f470f3e0a4ba6310da58bc Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Tue, 9 May 2023 13:50:47 +0200 Subject: [PATCH 3/5] working graphs and toggles --- typescript/Frontend/src/Login.tsx | 1 - typescript/Frontend/src/ResetPassword.tsx | 1 - .../components/Context/LogContextProvider.tsx | 39 +++ .../Context/UsersContextProvider.tsx | 1 - .../Installations/Installations.tsx | 41 ++- .../Installations/Log/CheckboxTree.tsx | 110 ++++++ .../Installations/Log/ScalarGraph.tsx | 316 +++++++++--------- .../Frontend/src/components/Layout/Search.tsx | 2 +- typescript/Frontend/src/util/graph.util.tsx | 24 -- 9 files changed, 332 insertions(+), 203 deletions(-) create mode 100644 typescript/Frontend/src/components/Context/LogContextProvider.tsx create mode 100644 typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx diff --git a/typescript/Frontend/src/Login.tsx b/typescript/Frontend/src/Login.tsx index 11069fd2d..7d0b5104f 100644 --- a/typescript/Frontend/src/Login.tsx +++ b/typescript/Frontend/src/Login.tsx @@ -40,7 +40,6 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => { .then(() => { setToken(data.token); saveCurrentUser(data.user); - console.log(data.user); setLoading(false); setLanguage(data.user.language); }) diff --git a/typescript/Frontend/src/ResetPassword.tsx b/typescript/Frontend/src/ResetPassword.tsx index 3b07db745..ea893289a 100644 --- a/typescript/Frontend/src/ResetPassword.tsx +++ b/typescript/Frontend/src/ResetPassword.tsx @@ -34,7 +34,6 @@ const ResetPassword = () => { }) .then((res) => { saveCurrentUser(res.data); - console.log(res.data); setLoading(false); }) .catch((err) => setError(err)); diff --git a/typescript/Frontend/src/components/Context/LogContextProvider.tsx b/typescript/Frontend/src/components/Context/LogContextProvider.tsx new file mode 100644 index 000000000..8bbe6d2df --- /dev/null +++ b/typescript/Frontend/src/components/Context/LogContextProvider.tsx @@ -0,0 +1,39 @@ +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>; +} + +export const LogContext = createContext({ + toggles: [], + setToggles: () => {}, + checkedToggles: {}, + setCheckedToggles: () => {}, +}); + +const LogContextProvider = ({ children }: { children: ReactNode }) => { + const [toggles, setToggles] = useState(null); + const [checkedToggles, setCheckedToggles] = useState( + null + ); + console.log("provider", toggles); + return ( + + {children} + + ); +}; + +export default LogContextProvider; diff --git a/typescript/Frontend/src/components/Context/UsersContextProvider.tsx b/typescript/Frontend/src/components/Context/UsersContextProvider.tsx index af77b5e9c..9952b2b7d 100644 --- a/typescript/Frontend/src/components/Context/UsersContextProvider.tsx +++ b/typescript/Frontend/src/components/Context/UsersContextProvider.tsx @@ -90,7 +90,6 @@ const UsersContextProvider = ({ children }: { children: ReactNode }) => { const fetchAvailableUsers = async (): Promise => { return axiosConfig.get("/GetAllChildUsers").then((res) => { - console.log(res); setAvailableUsers(res.data); }); }; diff --git a/typescript/Frontend/src/components/Installations/Installations.tsx b/typescript/Frontend/src/components/Installations/Installations.tsx index b79f34d9b..16602a401 100644 --- a/typescript/Frontend/src/components/Installations/Installations.tsx +++ b/typescript/Frontend/src/components/Installations/Installations.tsx @@ -8,30 +8,35 @@ import InstallationContextProvider from "../Context/InstallationContextProvider" import SearchSidebar from "../Layout/Search"; import InstallationList from "./InstallationList"; import Installation from "./Installation"; +import CheckboxTree from "./Log/CheckboxTree"; +import LogContextProvider from "../Context/LogContextProvider"; const Installations = () => { return ( - - - - - - - - } - index + + + + - } /> - } /> - + + + + + + } + index + /> + } /> + } /> + + - + ); }; diff --git a/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx b/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx new file mode 100644 index 000000000..f7d266c39 --- /dev/null +++ b/typescript/Frontend/src/components/Installations/Log/CheckboxTree.tsx @@ -0,0 +1,110 @@ +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, + element: TreeElement, + checked?: boolean + ) => { + event.stopPropagation(); + handleCheckChildren([element], checked); + }; + + const handleExpandClick = ( + event: React.MouseEvent + ) => { + event.stopPropagation(); + }; + + const renderTree = (data: TreeElement[]): ReactNode => { + return data.map((element) => { + const checked = checkedToggles?.[element.id]; + const splitName = element.name.split("/"); + return ( + + handleClick(e, element, checked)} + /> + {splitName[splitName.length - 1]} + + } + sx={{ + ".MuiTreeItem-content": { paddingY: "5px" }, + }} + > + {getNodes(element)} + + ); + }); + }; + return ( + <> + + {toggles !== null && routeMatch !== null && ( + } + defaultExpandIcon={} + sx={{ + height: 480, + flexGrow: 1, + overflow: "auto", + overflowX: "hidden", + }} + > + {renderTree(toggles)} + + )} + + ); +}; + +export default CheckboxTree; diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 95c5c57cf..05248d03a 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,15 +1,15 @@ import Plot from "react-plotly.js"; import { RecordSeries } from "../../../dataCache/data"; -import { parseCsv, transformToGraphData } from "../../../util/graph.util"; +import { GraphData, mergeDeep, parseCsv } from "../../../util/graph.util"; import { TimeRange, TimeSpan, UnixTime } from "../../../dataCache/time"; -import { ReactNode, useEffect, useMemo, useState } from "react"; +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"; -import { TreeItem, TreeView } from "@mui/lab"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import { Checkbox } from "@mui/material"; + +import { LogContext } from "../../Context/LogContextProvider"; +import { TreeElement, ToggleElement } from "./CheckboxTree"; +import { isDefined } from "../../../dataCache/utils/maybe"; export const createTimes = ( range: TimeRange, @@ -27,16 +27,19 @@ const NUMBER_OF_NODES = 100; const ScalarGraph = () => { const timeRange = createTimes( - UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(1)), + UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(4)), NUMBER_OF_NODES ); const [timeSeries, setTimeSeries] = useState([]); - const [uiRevision, setUiRevision] = useState(Math.random()); const [range, setRange] = useState([ timeRange[0].toDate(), timeRange[timeRange.length - 1].toDate(), ]); - const [toggles, setToggles] = useState(null); + const [uiRevision, setUiRevision] = useState(Math.random()); + const [plotTitles, setPlotTitles] = useState([]); + + const { toggles, setToggles, setCheckedToggles, checkedToggles } = + useContext(LogContext); const times$ = useMemo(() => new BehaviorSubject(timeRange), []); @@ -49,23 +52,37 @@ const ScalarGraph = () => { "" ); - function insert( + const insert = ( children: TreeElement[] = [], [head, ...tail]: string[] - ): TreeElement[] { + ): TreeElement[] => { let child = children.find((child) => child.name === head); - if (!child) + + if (!child) { children.push( (child = { - id: head + tail.join("_"), + id: head, name: head, - checked: false, children: [], }) ); - if (tail.length > 0) insert(child.children, tail); + } + 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 @@ -78,79 +95,34 @@ const ScalarGraph = () => { const timeSeries = cache.getSeries(times); setTimeSeries(timeSeries); const toggleValues = timeSeries.find((timeStamp) => timeStamp.value); - if (toggles === null && toggleValues && toggleValues.value) { - setToggles( - Object.keys(toggleValues.value) - .map((path) => path.split("/").slice(1)) - .reduce( - (children, path) => insert(children, path), - [] as TreeElement[] - ) - ); + console.log("toggles inside", toggles); + 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[] + ); + console.log("elements", treeElements); + setToggles(treeElements); + setCheckedToggles(flattenToggles(treeElements)); } }); return () => subscription.unsubscribe(); - }, []); - - interface TreeElement { - id: string; - name: string; - children: TreeElement[]; - checked: boolean; - } - - const getNodes = (element: TreeElement): null | ReactNode => { - return element.children.length > 0 ? renderTree(element.children) : null; - }; - - const renderTree = (data: TreeElement[]): ReactNode => { - return data.map((element) => { - return ( - - { - if (toggles) { - const togglesValue = toggles; - const index = toggles?.findIndex((toggle) => { - return element.id === toggle.id; - }); - if (index > 0) { - togglesValue[index] = { - ...element, - checked: !element.checked, - }; - setToggles(togglesValue); - } - } - }} - /> - {element.name} - - } - sx={{ - ".MuiTreeItem-content": { paddingY: "12px" }, - }} - > - {getNodes(element)} - - ); - }); - }; - - const round = (date: Date) => { - const minutes = 30; - const ms = 1000 * 60 * minutes; - - return new Date(Math.round(date.getTime() / ms) * ms); - }; + }, [toggles]); const fetchData = ( timestamp: UnixTime @@ -179,81 +151,111 @@ const ScalarGraph = () => { [] ); - const renderGraphs = () => { - const coordinateTimeSeries = transformToGraphData(timeSeries); - return Object.keys(coordinateTimeSeries).map((path) => { - const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; - return ( - { - const xaxisRange0 = params["xaxis.range[0]"]; - const xaxisRange1 = params["xaxis.range[1]"]; - - if (xaxisRange0 && xaxisRange1) { - setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); - setUiRevision(Math.random()); - const times = createTimes( - TimeRange.fromTimes( - UnixTime.fromDate(new Date(xaxisRange0)), - UnixTime.fromDate(new Date(xaxisRange1)) - ), - NUMBER_OF_NODES - ); - cache.getSeries(times); - times$.next(times); + 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 + ); }; - return ( - <> - {renderGraphs()} - {toggles !== null && ( - } - defaultExpandIcon={} - sx={{ - height: 480, - flexGrow: 1, - overflow: "auto", - overflowX: "hidden", - }} - > - {renderTree(toggles)} - - )} - - ); + const renderGraphs = () => { + if (checkedToggles) { + const coordinateTimeSeries = transformToGraphData(timeSeries); + console.log("coordinateTimeSeries", coordinateTimeSeries, checkedToggles); + return Object.keys(coordinateTimeSeries) + .filter((path) => { + return checkedToggles[path]; + }) + .map((path) => { + const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; + return ( + { + const xaxisRange0 = params["xaxis.range[0]"]; + const xaxisRange1 = params["xaxis.range[1]"]; + + if (xaxisRange0 && xaxisRange1) { + setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); + 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()}; }; export default ScalarGraph; diff --git a/typescript/Frontend/src/components/Layout/Search.tsx b/typescript/Frontend/src/components/Layout/Search.tsx index 52bc74d1f..c787d54e4 100644 --- a/typescript/Frontend/src/components/Layout/Search.tsx +++ b/typescript/Frontend/src/components/Layout/Search.tsx @@ -12,7 +12,7 @@ const SearchSidebar = (props: SearchSidebarProps) => { const intl = useIntl(); return ( -
+
{ }, {} 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: [new Date(curr.time.ticks * 1000)], - y: [curr.value ? curr.value[currPath] : 0], - }, - }; - } - return pathAcc; - }, - {} as GraphData - ); - return mergeDeep(acc, timeStampObj); - } - return acc; - }, {} as GraphData); -}; - export interface GraphCoordinates { x: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray; From 06bc9b6b2e39c8e30a099955fe61534274fd4296 Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Wed, 10 May 2023 10:40:08 +0200 Subject: [PATCH 4/5] working version with all the graphs with the same x axis --- .../Installations/Log/ScalarGraph.tsx | 162 +++++++++++------- typescript/Frontend/src/util/graph.util.tsx | 2 + 2 files changed, 100 insertions(+), 64 deletions(-) diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 05248d03a..ce77c6cba 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,6 +1,11 @@ import Plot from "react-plotly.js"; import { RecordSeries } from "../../../dataCache/data"; -import { GraphData, mergeDeep, parseCsv } 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"; @@ -27,13 +32,14 @@ const NUMBER_OF_NODES = 100; const ScalarGraph = () => { const timeRange = createTimes( - UnixTime.fromTicks(1682085650).rangeBefore(TimeSpan.fromDays(4)), + UnixTime.now() /* .fromTicks(1682085650) */ + .rangeBefore(TimeSpan.fromDays(4)), NUMBER_OF_NODES ); const [timeSeries, setTimeSeries] = useState([]); const [range, setRange] = useState([ - timeRange[0].toDate(), - timeRange[timeRange.length - 1].toDate(), + timeRange[0].toDate().getTime(), + timeRange[timeRange.length - 1].toDate().getTime(), ]); const [uiRevision, setUiRevision] = useState(Math.random()); const [plotTitles, setPlotTitles] = useState([]); @@ -193,67 +199,95 @@ const ScalarGraph = () => { }; const renderGraphs = () => { - if (checkedToggles) { - const coordinateTimeSeries = transformToGraphData(timeSeries); - console.log("coordinateTimeSeries", coordinateTimeSeries, checkedToggles); - return Object.keys(coordinateTimeSeries) - .filter((path) => { - return checkedToggles[path]; - }) - .map((path) => { - const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; - return ( - { - const xaxisRange0 = params["xaxis.range[0]"]; - const xaxisRange1 = params["xaxis.range[1]"]; + const coordinateTimeSeries = transformToGraphData(timeSeries); + 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 ( + { + const xaxisRange0 = params["xaxis.range[0]"]; + const xaxisRange1 = params["xaxis.range[1]"]; - if (xaxisRange0 && xaxisRange1) { - setRange([new Date(xaxisRange0), new Date(xaxisRange1)]); - 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); - } - }} - /> - ); - }); + 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()}; diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index f54f33159..18e0e33c8 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -25,6 +25,8 @@ export const mergeDeep = (...objects: any[]) => { export interface GraphCoordinates { x: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray; + xaxis?: string; + yaxis?: string; } export interface GraphData { From 8cecf2885e5e65915d902dcbfc4669d5462d8441 Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Thu, 11 May 2023 10:49:01 +0200 Subject: [PATCH 5/5] change interpolation datacache, add string values to graphs [WIP] --- .../components/Context/LogContextProvider.tsx | 1 - .../Installations/Log/CheckboxTree.tsx | 1 - .../Installations/Log/ScalarGraph.tsx | 24 +- typescript/Frontend/src/dataCache/data.ts | 6 +- .../Frontend/src/dataCache/dataCache.ts | 281 +++++++++--------- typescript/Frontend/src/util/graph.util.tsx | 25 +- 6 files changed, 160 insertions(+), 178 deletions(-) diff --git a/typescript/Frontend/src/components/Context/LogContextProvider.tsx b/typescript/Frontend/src/components/Context/LogContextProvider.tsx index 8bbe6d2df..349ed883a 100644 --- a/typescript/Frontend/src/components/Context/LogContextProvider.tsx +++ b/typescript/Frontend/src/components/Context/LogContextProvider.tsx @@ -21,7 +21,6 @@ const LogContextProvider = ({ children }: { children: ReactNode }) => { const [checkedToggles, setCheckedToggles] = useState( null ); - console.log("provider", toggles); return ( { ) => { event.stopPropagation(); }; - const renderTree = (data: TreeElement[]): ReactNode => { return data.map((element) => { const checked = checkedToggles?.[element.id]; diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index ce77c6cba..93c48e15c 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -15,6 +15,7 @@ import DataCache, { FetchResult } from "../../../dataCache/dataCache"; import { LogContext } from "../../Context/LogContextProvider"; import { TreeElement, ToggleElement } from "./CheckboxTree"; import { isDefined } from "../../../dataCache/utils/maybe"; +import { timeStamp } from "console"; export const createTimes = ( range: TimeRange, @@ -102,7 +103,6 @@ const ScalarGraph = () => { setTimeSeries(timeSeries); const toggleValues = timeSeries.find((timeStamp) => timeStamp.value); if (toggles === null && toggleValues && toggleValues.value) { - console.log("toggles inside", toggles); const treeElements = Object.keys(toggleValues.value) .map((path) => { return path @@ -121,7 +121,6 @@ const ScalarGraph = () => { (children, path) => insert(children, path), [] as TreeElement[] ); - console.log("elements", treeElements); setToggles(treeElements); setCheckedToggles(flattenToggles(treeElements)); } @@ -200,6 +199,7 @@ const ScalarGraph = () => { const renderGraphs = () => { const coordinateTimeSeries = transformToGraphData(timeSeries); + console.log("coordinates", coordinateTimeSeries); const graphCoordinates: GraphCoordinates[] = Object.keys( coordinateTimeSeries ) @@ -229,7 +229,7 @@ const ScalarGraph = () => { autorange: false, range: range, type: "date", - mirror: "allticks", + showticklabels: true, }, grid: { subplots: subplots as any, @@ -237,24 +237,6 @@ const ScalarGraph = () => { ygap: 0.1, }, height: graphCoordinates.length * 300, - annotations: [ - { - text: "X1/Y1 title", - showarrow: false, - x: 0, - y: 1, - yref: "paper", - yanchor: "bottom", - }, - { - text: "X2/Y2 title", - showarrow: false, - x: 0, - y: 1, - yref: "paper", - yanchor: "bottom", - }, - ], }} config={{ modeBarButtonsToRemove: [ diff --git a/typescript/Frontend/src/dataCache/data.ts b/typescript/Frontend/src/dataCache/data.ts index 8fdb168e1..ffe3f8a44 100644 --- a/typescript/Frontend/src/dataCache/data.ts +++ b/typescript/Frontend/src/dataCache/data.ts @@ -2,12 +2,12 @@ import { Maybe } from "yup"; import {Timestamped} from "./types"; import { isDefined } from "./utils/maybe"; -export type DataRecord = Record +export type DataRecord = Record export type DataPoint = Timestamped> export type RecordSeries = Array -export type PointSeries = Array>> -export type DataSeries = Array> +export type PointSeries = Array>> +export type DataSeries = Array> export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries { diff --git a/typescript/Frontend/src/dataCache/dataCache.ts b/typescript/Frontend/src/dataCache/dataCache.ts index 71f230a99..c123658ed 100644 --- a/typescript/Frontend/src/dataCache/dataCache.ts +++ b/typescript/Frontend/src/dataCache/dataCache.ts @@ -1,169 +1,158 @@ /* eslint-disable no-mixed-operators */ -import {TimeSpan, UnixTime} from "./time"; -import {Observable, Subject} from "rxjs"; -import {SkipList} from "./skipList/skipList"; -import {createDispatchQueue} from "./promiseQueue"; -import {SkipListNode} from "./skipList/skipListNode"; -import {RecordSeries} from "./data"; +import { TimeSpan, UnixTime } from "./time"; +import { Observable, Subject } from "rxjs"; +import { SkipList } from "./skipList/skipList"; +import { createDispatchQueue } from "./promiseQueue"; +import { SkipListNode } from "./skipList/skipListNode"; +import { RecordSeries } from "./data"; import { Maybe, isUndefined } from "./utils/maybe"; +import { isNumber, isString } from "./utils/runtimeTypeChecking"; - -export const FetchResult = -{ - notAvailable : "N/A", - tryLater : "Try Later" -} as const +export const FetchResult = { + notAvailable: "N/A", + tryLater: "Try Later", +} as const; export type FetchResult = - | T - | typeof FetchResult.notAvailable - | typeof FetchResult.tryLater + | T + | typeof FetchResult.notAvailable + | typeof FetchResult.tryLater; -function reverseBits(x : number): number -{ - // https://stackoverflow.com/a/60227327/141397 +function reverseBits(x: number): number { + // https://stackoverflow.com/a/60227327/141397 - x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >> 1; - x = (x & 0x33333333) << 2 | (x & 0xCCCCCCCC) >> 2; - x = (x & 0x0F0F0F0F) << 4 | (x & 0xF0F0F0F0) >> 4; - x = (x & 0x00FF00FF) << 8 | (x & 0xFF00FF00) >> 8; - x = (x & 0x0000FFFF) << 16 | (x & 0xFFFF0000) >> 16; + x = ((x & 0x55555555) << 1) | ((x & 0xaaaaaaaa) >> 1); + x = ((x & 0x33333333) << 2) | ((x & 0xcccccccc) >> 2); + x = ((x & 0x0f0f0f0f) << 4) | ((x & 0xf0f0f0f0) >> 4); + x = ((x & 0x00ff00ff) << 8) | ((x & 0xff00ff00) >> 8); + x = ((x & 0x0000ffff) << 16) | ((x & 0xffff0000) >> 16); - return x >>> 0; + return x >>> 0; } +export default class DataCache> { + private readonly cache: SkipList> = new SkipList>(); + private readonly resolution: TimeSpan; -export default class DataCache> -{ - private readonly cache: SkipList> = new SkipList>() - private readonly resolution: TimeSpan; + readonly _fetch: (t: UnixTime) => Promise>; - readonly _fetch: (t: UnixTime) => Promise>; + private readonly fetchQueue = createDispatchQueue(6); + private readonly fetching: Set = new Set(); - private readonly fetchQueue = createDispatchQueue(6) - private readonly fetching: Set = new Set() + public readonly gotData: Observable; - public readonly gotData: Observable; + constructor( + fetch: (t: UnixTime) => Promise>, + resolution: TimeSpan + ) { + this._fetch = fetch; + this.resolution = resolution; + this.gotData = new Subject(); + } - constructor(fetch: (t: UnixTime) => Promise>, resolution: TimeSpan) - { - this._fetch = fetch; - this.resolution = resolution; - this.gotData = new Subject() + public prefetch(times: Array, clear = true) { + if (clear) { + this.fetching.clear(); + this.fetchQueue.clear(); } - public prefetch(times: Array, clear = true) - { - if (clear) - { - this.fetching.clear() - this.fetchQueue.clear() + const timesWithPriority = times.map((time, index) => ({ + time, + priority: reverseBits(index), + })); + timesWithPriority.sort((x, y) => x.priority - y.priority); + + 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 { + 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>, t: number): Maybe { + 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> = {}; + + //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) => { + if (data === FetchResult.tryLater) { + console.warn(FetchResult.tryLater); + return; } - const timesWithPriority = times.map((time, index) => ({time, priority: reverseBits(index)})) - timesWithPriority.sort((x, y) => x.priority - y.priority) + const value = data === FetchResult.notAvailable ? undefined : data; + this.cache.insert(value, t); + }; - for (let i = 0; i < timesWithPriority.length; i++) - { - const time = timesWithPriority[i].time.round(this.resolution) - const t = time.ticks; + const onFailure = (_: unknown) => { + console.error(time.ticks + " FAILED!"); // should not happen + }; - const node = this.cache.find(t); - if (node.index !== t) - this.fetchData(time); - } - } + const dispatch = () => { + this.fetching.delete(time.ticks); + (this.gotData as Subject).next(time); + }; - public get(timeStamp: UnixTime, fetch = true): Maybe - { - const time = timeStamp.round(this.resolution) - const t = time.ticks; + return this._fetch(time) + .then( + (d) => onSuccess(d), + (f) => onFailure(f) + ) + .finally(() => dispatch()); + }; - 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>, t: number): Maybe - { - 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> = {} - - //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) => - { - 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).next(time); - } - - return this._fetch(time) - .then(d => onSuccess(d), f => onFailure(f)) - .finally(() => dispatch()) - }; - - this.fetching.add(t) - this.fetchQueue.dispatch(() => fetchTask()); - } - -} \ No newline at end of file + this.fetching.add(t); + this.fetchQueue.dispatch(() => fetchTask()); + } +} diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index 18e0e33c8..07a15f4a9 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -1,6 +1,4 @@ import { Datum, TypedArray } from "plotly.js"; -import { RecordSeries } from "../dataCache/data"; -import { isDefined } from "../dataCache/utils/maybe"; export const mergeDeep = (...objects: any[]) => { const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object"; @@ -36,11 +34,26 @@ export interface GraphData { export const parseCsv = (text: string) => { const y = text .split(/\r?\n/) - .map((l) => l.split(";")) - .filter((fields) => !isNaN(parseFloat(fields[1]))); + .filter((split) => split.length > 0) + .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 - .map((fields) => ({ [fields[0]]: parseFloat(fields[1]) })) - .reduce((acc, current) => ({ ...acc, ...current }), {}); + .map((fields) => { + 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; };