diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index 93c48e15c..a0e5b9402 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -3,19 +3,25 @@ import { RecordSeries } from "../../../dataCache/data"; import { GraphCoordinates, GraphData, - mergeDeep, + flattenBarGraphData, + flattenToggles, + insertTreeElements, + isNumeric, + isText, parseCsv, + stringToColor, + transformToBarGraphData, } 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"; - import { LogContext } from "../../Context/LogContextProvider"; import { TreeElement, ToggleElement } from "./CheckboxTree"; import { isDefined } from "../../../dataCache/utils/maybe"; -import { timeStamp } from "console"; + +import { Data } from "plotly.js"; export const createTimes = ( range: TimeRange, @@ -39,8 +45,8 @@ const ScalarGraph = () => { ); const [timeSeries, setTimeSeries] = useState([]); const [range, setRange] = useState([ - timeRange[0].toDate().getTime(), - timeRange[timeRange.length - 1].toDate().getTime(), + timeRange[0].toDate(), + timeRange[timeRange.length - 1].toDate(), ]); const [uiRevision, setUiRevision] = useState(Math.random()); const [plotTitles, setPlotTitles] = useState([]); @@ -54,43 +60,11 @@ const ScalarGraph = () => { "saliomameiringen", "sos-ch-dk-2", "exo.io", - "EXO18e7ae9e53fae71ee55cf35b", - "3Cyonq8gMQ0a3elTH2vP7Yv-czcCj8iE2lBcPB9XhSc", + "EXO464a9ff62fdfa407aa742855", + "f2KtCWN4EHFqtvH2kotdyI0w5SjjdHVPAADdcD3ik8g", "" ); - 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( @@ -118,7 +92,7 @@ const ScalarGraph = () => { .slice(1); }) .reduce( - (children, path) => insert(children, path), + (children, path) => insertTreeElements(children, path), [] as TreeElement[] ); setToggles(treeElements); @@ -156,120 +130,154 @@ const ScalarGraph = () => { [] ); - 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; - }, + const transformToGraphData = (input: RecordSeries): GraphData => { + const transformedObject: any = {}; + input.forEach((item) => { + if (isDefined(item.value)) { + Object.keys(item.value).forEach((key) => { + if (!transformedObject.hasOwnProperty(key)) { + transformedObject[key] = { + x: [], + y: [], + }; + } + transformedObject[key].x.push( + new Date(item.time.ticks * 1000).toISOString() + ); + transformedObject[key].y.push(item.value?.[key]); + }); + } + if (plotTitles.length === 0) { + setPlotTitles(Object.keys(transformedObject)); + } + }); + return Object.keys(transformedObject).length > 0 + ? transformedObject + : plotTitles.reduce( + (acc, curr) => ({ + ...acc, + [curr]: { + x: [], + y: [], + }, + }), {} 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 renderGraphs = () => { - const coordinateTimeSeries = transformToGraphData(timeSeries); - 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 ( - { - const xaxisRange0 = params["xaxis.range[0]"]; - const xaxisRange1 = params["xaxis.range[1]"]; + if (checkedToggles) { + const coordinateTimeSeries = transformToGraphData(timeSeries); + if (plotTitles.length === 0) { + setPlotTitles(Object.keys(coordinateTimeSeries)); + } + return Object.keys(coordinateTimeSeries) + .filter((path) => { + return checkedToggles[path]; + }) + .map((path) => { + const data = coordinateTimeSeries[path] ?? { x: [], y: [] }; + const isScalar = isNumeric(data.y[0]); + if (!isScalar) { + const barGraphData = transformToBarGraphData(data); + return ( + { + 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); - } - }} - /> - ); + 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); + } + }} + /> + ); + } + 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); + } + }} + /> + ); + }); } }; return <>{renderGraphs()}; diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index 07a15f4a9..721f207ba 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -1,30 +1,18 @@ import { Datum, TypedArray } from "plotly.js"; - -export const mergeDeep = (...objects: any[]) => { - const isObject = (obj: GraphCoordinates) => obj && typeof obj === "object"; - return objects.reduce((prev, obj) => { - Object.keys(obj).forEach((key) => { - const pVal = prev[key]; - const oVal = obj[key]; - - if (Array.isArray(pVal) && Array.isArray(oVal)) { - prev[key] = pVal.concat(...oVal); - } else if (isObject(pVal) && isObject(oVal)) { - prev[key] = mergeDeep(pVal, oVal); - } else { - prev[key] = oVal; - } - }); - - return prev; - }, {} as GraphData); -}; +import { + TreeElement, + ToggleElement, +} from "../components/Installations/Log/CheckboxTree"; export interface GraphCoordinates { x: Datum[] | Datum[][] | TypedArray; y: Datum[] | Datum[][] | TypedArray; xaxis?: string; yaxis?: string; + barmode?: string; + marker?: { color: string }; + type?: string; + name?: string; } export interface GraphData { @@ -41,19 +29,104 @@ export const parseCsv = (text: string) => { } return l.split(";"); }); - console.log("text", y); - /* .filter((fields) => !isNaN(parseFloat(fields[1]))); - */ const x = y .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; }; + +export 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); +}; + +export const insertTreeElements = ( + 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) { + insertTreeElements(child.children, tail); + } + return children; +}; + +export const isText = (data: any): data is string => { + return typeof data === "string"; +}; + +export const flattenBarGraphData = (arr: any): GraphCoordinates[] => { + return arr.reduce((flat: any, toFlatten: any) => { + return flat.concat( + Array.isArray(toFlatten) ? flattenBarGraphData(toFlatten) : toFlatten + ); + }, []); +}; + +export const stringToColor = (str: string) => { + if (str.length === 0) { + return "#FFFFFF"; + } + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = "#"; + for (var i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += ("02" + value.toString(16)).substr(-2); + } + return colour; +}; + +export const isNumeric = (value: any) => { + return !isNaN(value) && !isNaN(parseFloat(value)); +}; + +export const transformToBarGraphData = (data: GraphCoordinates) => { + let names: string[] = []; + const barGraphData = data.y.map((text, i) => { + if (isText(text)) { + const splitText = text.split(","); + return splitText.map((split) => { + const foundName = !!names.find((value) => value === split); + if (!foundName) { + names.push(split); + } + return { + x: [data.x[i]], + y: [1 / splitText.length], + barmode: "stack", + marker: { color: stringToColor(split) }, + type: "bar", + name: split, + showlegend: !foundName, + } as any; + }); + } + return [] as any; + }); + return flattenBarGraphData(barGraphData); +};