[WIP] modify pology, style it a bit

This commit is contained in:
Sina Blattmann 2023-06-13 14:48:28 +02:00
parent 3a4c768074
commit 4b9f3e6a84
12 changed files with 192 additions and 215 deletions

View File

@ -103,6 +103,7 @@ const CheckboxTree = () => {
overflowX: "hidden",
position: ["sticky", "-webkit-sticky"],
top: 1,
maxHeight: "90vh",
}}
>
{renderTree(toggles)}

View File

@ -1,16 +1,9 @@
import * as React from "react";
import { Button } from "@mui/material";
import { UnixTime, TimeSpan } from "../../../dataCache/time";
import { createTimes } from "../../../util/graph.util";
import {
DatePicker,
DateTimeValidationError,
LocalizationProvider,
} from "@mui/x-date-pickers";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import { FormattedMessage } from "react-intl";
import { useState } from "react";
import ShortcutButton from "./ShortcutButton";
interface DateRangePickerProps {
setRange: (value: Date[]) => void;
@ -19,10 +12,6 @@ interface DateRangePickerProps {
}
const DateRangePicker = (props: DateRangePickerProps) => {
const { setRange, range, getCacheSeries } = props;
const [fromDateError, setFromDateError] =
useState<DateTimeValidationError | null>(null);
const [toDateError, setToDateError] =
useState<DateTimeValidationError | null>(null);
const handleChange = (fromDate: Date, toDate: Date) => {
setRange([fromDate, toDate]);
@ -31,26 +20,6 @@ const DateRangePicker = (props: DateRangePickerProps) => {
return (
<>
<div>
<Button
onClick={() => {
const weekRange = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(7)),
100
);
setRange([
weekRange[0].toDate(),
weekRange[weekRange.length - 1].toDate(),
]);
getCacheSeries(
weekRange[0].ticks,
weekRange[weekRange.length - 1].ticks
);
}}
>
<FormattedMessage id="lastWeek" defaultMessage="Last week" />
</Button>
</div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
disableFuture
@ -62,16 +31,6 @@ const DateRangePicker = (props: DateRangePickerProps) => {
handleChange(newValue.toDate(), range[1]);
}
}}
onError={(err) => setFromDateError(err)}
slotProps={{
textField: {
variant: "outlined",
error: !!fromDateError,
helperText: fromDateError
? "From date needs to be before to date"
: "",
},
}}
/>
<DatePicker
disableFuture
@ -83,23 +42,36 @@ const DateRangePicker = (props: DateRangePickerProps) => {
color: "red",
},
}}
onError={(err) => setToDateError(err)}
onChange={(newValue) => {
if (newValue) {
handleChange(range[0], newValue.toDate());
}
}}
slotProps={{
textField: {
variant: "outlined",
error: !!toDateError,
helperText: toDateError
? "To date needs to be after from date"
: "",
},
}}
/>
</LocalizationProvider>
<div>
<ShortcutButton
dayRange={1}
setRange={setRange}
getCacheSeries={getCacheSeries}
>
<FormattedMessage id="today" defaultMessage="Today" />
</ShortcutButton>
<ShortcutButton
dayRange={7}
setRange={setRange}
getCacheSeries={getCacheSeries}
>
<FormattedMessage id="lastWeek" defaultMessage="Last week" />
</ShortcutButton>
<ShortcutButton
dayRange={30}
setRange={setRange}
getCacheSeries={getCacheSeries}
>
<FormattedMessage id="lastMonth" defaultMessage="Last month" />
</ShortcutButton>
</div>
</>
);
};

View File

@ -6,6 +6,7 @@ const Log = () => {
return (
<>
<TopologyView />
<ScalarGraph />
</>
);

View File

@ -1,6 +1,7 @@
import Plot from "react-plotly.js";
import { RecordSeries } from "../../../dataCache/data";
import {
Csv,
GraphData,
createTimes,
flattenToggles,
@ -19,6 +20,11 @@ import { LogContext } from "../../Context/LogContextProvider";
import { isDefined } from "../../../dataCache/utils/maybe";
import { Data, Layout } from "plotly.js";
import { VariableSizeList as List, areEqual } from "react-window";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { FormattedMessage } from "react-intl";
import DateRangePicker from "./DateRangePicker";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { Alert } from "@mui/material";
const NUMBER_OF_NODES = 100;
@ -526,9 +532,7 @@ const ScalarGraph = () => {
return () => subscription.unsubscribe();
}, [toggles]);
const fetchData = (
timestamp: UnixTime
): Promise<FetchResult<Record<string, number>>> => {
const fetchData = (timestamp: UnixTime): Promise<FetchResult<Csv>> => {
const s3Path = `${timestamp.ticks}.csv`;
return s3Access
.get(s3Path)
@ -569,7 +573,7 @@ const ScalarGraph = () => {
transformedObject[key].x.push(
new Date(item.time.ticks * 1000).toISOString()
);
transformedObject[key].y.push(item.value?.[key]);
transformedObject[key].y.push(item.value?.[key].value);
});
}
if (
@ -601,6 +605,7 @@ const ScalarGraph = () => {
),
NUMBER_OF_NODES
);
console.log("getcacheseries");
cache.getSeries(times);
times$.next(times);
};
@ -630,9 +635,6 @@ const ScalarGraph = () => {
barnorm: "percent",
}
: {};
if (!isScalar) {
console.log("graphData", data[visibleGraphs[index]]);
}
return (
<div style={style}>
<Plot
@ -679,116 +681,43 @@ const ScalarGraph = () => {
return null;
}, areEqual);
/* const renderGraphs = () => {
if (checkedToggles) {
const coordinateTimeSeries = transformToGraphData(timeSeries);
const visibleGraphs = Object.keys(coordinateTimeSeries).filter((path) => {
return checkedToggles[path];
});
if (visibleGraphs.length > 0) {
return (
<>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateRangePicker
setRange={setRange}
range={range}
getCacheSeries={getCacheSeries}
/>
</LocalizationProvider>
{visibleGraphs.map((path) => {
const isScalar = isNumeric(coordinateTimeSeries[path].y[0]);
const data = isScalar
? [
{
...coordinateTimeSeries[path],
type: "scatter",
mode: "lines+markers",
fill: "tozeroy",
},
]
: transformToBarGraphData(coordinateTimeSeries[path]);
const barGraphLayout: Partial<Layout> = !isScalar
? {
bargap: 0,
barmode: "stack",
barnorm: "percent",
}
: {};
return (
<Plot
key={path}
data={data as Data[]}
layout={{
width: 1000,
height: 500,
title: path,
uirevision: uiRevision,
xaxis: {
autorange: false,
range: range,
type: "date",
},
yaxis: {
rangemode: "tozero",
},
...barGraphLayout,
}}
config={{
modeBarButtonsToRemove: [
"lasso2d",
"select2d",
"pan2d",
"autoScale2d",
],
}}
onRelayout={(params) => {
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());
getCacheSeries(xaxisRange0, xaxisRange1);
}
}}
/>
);
})}
</>
);
}
return (
<Alert sx={{ mt: 2 }} severity="info">
<FormattedMessage
id="makeASelection"
defaultMessage="Please make a selection on the left"
/>
</Alert>
);
}
}; */
if (checkedToggles) {
if (
checkedToggles &&
Object.keys(checkedToggles).find((toggle) => checkedToggles[toggle])
) {
const coordinateTimeSeries = transformToGraphData(timeSeries);
console.log(
"length",
Object.keys(checkedToggles).filter((toggle) => checkedToggles[toggle])
.length
);
return (
<List
height={1000}
itemCount={
Object.keys(checkedToggles).filter((toggle) => checkedToggles[toggle])
.length
}
itemSize={() => 500}
width="100%"
itemData={coordinateTimeSeries}
>
{Row}
</List>
<>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateRangePicker
setRange={setRange}
range={range}
getCacheSeries={getCacheSeries}
/>
</LocalizationProvider>
<List
height={1000}
itemCount={
Object.keys(checkedToggles).filter(
(toggle) => checkedToggles[toggle]
).length
}
itemSize={() => 500}
width="100%"
itemData={coordinateTimeSeries}
>
{Row}
</List>
</>
);
}
return null;
return (
<Alert sx={{ mt: 2 }} severity="info">
<FormattedMessage
id="makeASelection"
defaultMessage="Please make a selection on the left"
/>
</Alert>
);
};
export default ScalarGraph;

View File

@ -0,0 +1,36 @@
import { UnixTime, TimeSpan } from "../../../dataCache/time";
import { createTimes } from "../../../util/graph.util";
import InnovenergyButton from "../../Layout/InnovenergyButton";
interface ShortcutButtonProps {
setRange: (value: Date[]) => void;
getCacheSeries: (xaxisRange0: number, xaxisRange1: number) => void;
dayRange: number;
children?: React.ReactNode;
}
const ShortcutButton = (props: ShortcutButtonProps) => {
return (
<InnovenergyButton
onClick={() => {
const weekRange = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(props.dayRange)),
100
);
props.setRange([
weekRange[0].toDate(),
weekRange[weekRange.length - 1].toDate(),
]);
props.getCacheSeries(
weekRange[0].ticks,
weekRange[weekRange.length - 1].ticks
);
}}
sx={{ mt: 2, mb: 2, mr: 2 }}
>
{props.children}
</InnovenergyButton>
);
};
export default ShortcutButton;

View File

@ -34,6 +34,7 @@ const TopologyBox = (props: TopologyBoxProps) => {
<div>
{el.label}
{el.values.map((value) => {
console.log("value", value);
return (
<p
style={{ marginBlockStart: "2px", marginBlockEnd: "2px" }}

View File

@ -1,5 +1,5 @@
import { Box } from "@mui/material";
import TopologyBox, { TopologyBoxProps } from "./ToplogyBox";
import TopologyBox, { TopologyBoxProps } from "./TopologyBox";
import TopologyFlow, { TopologyFlowProps } from "./TopologyFlow";
type TopologyColumnProps = {

View File

@ -1,39 +1,65 @@
import { Box } from "@mui/material";
import { BOX_SIZE, BoxData } from "./ToplogyBox";
import { BOX_SIZE, BoxData } from "./TopologyBox";
import "./TopologyFlow.scss";
export type TopologyFlowProps = {
orientation?: "vertical" | "horizontal";
amount?: number;
direction?: "leftToRight" | "rightToLeft";
rightToLeft?: boolean;
data?: BoxData[];
hidden?: boolean;
};
const TopologyFlow = (props: TopologyFlowProps) => {
const length = Math.abs((props.amount ?? 1) * (BOX_SIZE - 20));
const length = Math.abs((props.amount ?? 1) * BOX_SIZE);
return (
<>
<div
style={{
display: "flex",
justifyContent: "center",
height: BOX_SIZE,
width: BOX_SIZE,
alignItems: "center",
}}
>
<Box
sx={{
width: props.orientation === "horizontal" ? BOX_SIZE - 20 : length,
height: props.orientation === "vertical" ? BOX_SIZE - 20 : length,
width: props.orientation === "horizontal" ? BOX_SIZE : length,
height: props.orientation === "vertical" ? BOX_SIZE : length,
backgroundColor: "#f4b3504d",
visibility: props.hidden || !props.data ? "hidden" : "visible",
display: "flex",
}}
>
{props.data?.map((value) => value.values)}
<div
className="container"
style={{
transform:
props.orientation === "vertical"
? "rotate(90deg)"
: props.direction === "rightToLeft"
: props.rightToLeft
? "rotate(180deg)"
: "",
overflow: "hidden",
display: "flex",
height: BOX_SIZE,
width: BOX_SIZE,
}}
>
<div className="data-flow">
<p
style={{
position: "absolute",
zIndex: 1,
transform:
props.orientation === "vertical"
? "rotate(-90deg)"
: props.rightToLeft
? "rotate(-180deg)"
: "",
}}
>
{props.data?.map((value) => value.values)}
</p>
<div className="data-flow" style={{ overflow: "hidden" }}>
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
@ -47,7 +73,7 @@ const TopologyFlow = (props: TopologyFlowProps) => {
</div>
</div>
</Box>
</>
</div>
);
};

View File

@ -18,6 +18,7 @@ const TopologyView = () => {
flexDirection: "row",
overflow: "auto",
padding: 2,
fontFamily: `"Ubuntu", sans-serif`,
}}
>
<div>
@ -27,9 +28,9 @@ const TopologyView = () => {
data: values.acInBus,
}}
centerConnection={{
amount: 0.5,
amount: 0.6,
data: values.gridToAcIn,
direction: "rightToLeft",
rightToLeft: true,
}}
/>
</div>
@ -40,9 +41,8 @@ const TopologyView = () => {
data: values.acInBus,
}}
centerConnection={{
amount: 0.5,
amount: 0.3,
data: values.gridToAcIn,
direction: "leftToRight",
}}
/>
</div>
@ -55,7 +55,6 @@ const TopologyView = () => {
centerConnection={{
amount: 0.5,
data: values.gridToAcIn,
direction: "leftToRight",
}}
/>
</div>
@ -66,9 +65,8 @@ const TopologyView = () => {
data: values.acOutBus,
}}
centerConnection={{
amount: 0.5,
amount: 0.3,
data: values.gridToAcIn,
direction: "leftToRight",
}}
/>
</div>
@ -79,7 +77,7 @@ const TopologyView = () => {
data: values.acOutBus,
}}
topConnection={{
amount: 0.5,
amount: 0.6,
data: values.gridToAcIn,
}}
centerBox={{
@ -87,9 +85,9 @@ const TopologyView = () => {
data: values.acOutBus,
}}
centerConnection={{
amount: 0.5,
amount: 0.2,
data: values.gridToAcIn,
direction: "rightToLeft",
rightToLeft: true,
}}
/>
</div>
@ -100,7 +98,7 @@ const TopologyView = () => {
data: values.acOutBus,
}}
centerConnection={{
amount: 0.5,
amount: 0.8,
data: values.gridToAcIn,
}}
/>

View File

@ -1,13 +1,14 @@
import { Maybe } from "yup";
import { Timestamped } from "./types";
import { isDefined } from "./utils/maybe";
import { CsvEntry } from "../util/graph.util";
export type DataRecord = Record<string, number | string>;
export type DataRecord = Record<string, CsvEntry>;
export type DataPoint = Timestamped<Maybe<DataRecord>>;
export type RecordSeries = Array<DataPoint>;
export type PointSeries = Array<Timestamped<Maybe<number | string>>>;
export type DataSeries = Array<Maybe<number | string>>;
export type PointSeries = Array<Timestamped<Maybe<CsvEntry>>>;
export type DataSeries = Array<Maybe<CsvEntry>>;
export function getPoints(
recordSeries: RecordSeries,

View File

@ -6,7 +6,8 @@ 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";
import { isNumber } from "./utils/runtimeTypeChecking";
import { CsvEntry } from "../util/graph.util";
export const FetchResult = {
notAvailable: "N/A",
@ -30,7 +31,7 @@ function reverseBits(x: number): number {
return x >>> 0;
}
export default class DataCache<T extends Record<string, number>> {
export default class DataCache<T extends Record<string, CsvEntry>> {
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>();
private readonly resolution: TimeSpan;
@ -103,15 +104,20 @@ export default class DataCache<T extends Record<string, number>> {
const n = after.index - t;
const pn = p + n;
let interpolated: Partial<Record<string, number>> = {};
let interpolated: Partial<Record<string, CsvEntry>> = {};
//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];
const beforeData = dataBefore[k].value;
const afterData = Number(dataAfter[k].value);
let foo = interpolated[k];
if (foo) {
foo.value = isNumber(beforeData)
? (beforeData * n + afterData * p) / pn
: n < p
? afterData
: beforeData;
}
}
return interpolated as T;

View File

@ -6,7 +6,7 @@ import {
import { TimeRange, UnixTime } from "../dataCache/time";
import { DataPoint, DataRecord } from "../dataCache/data";
import { isDefined } from "../dataCache/utils/maybe";
import { BoxData } from "../components/Installations/Log/ToplogyBox";
import { BoxData } from "../components/Installations/Log/TopologyBox";
export interface GraphCoordinates {
x: Datum[] | Datum[][] | TypedArray;
@ -50,22 +50,28 @@ export const extractTopologyValues = (
timeSeriesData: DataPoint
): TopologyValues | null => {
const timeSeriesValue = timeSeriesData.value;
let topologyValues: (string | number)[];
if (isDefined(timeSeriesValue)) {
return Object.keys(topologyValues).reduce((acc, topologyKey) => {
const values = topologyValues[topologyKey].map(
return Object.keys(topologyPaths).reduce((acc, topologyKey) => {
const values = topologyPaths[topologyKey].map(
(topologyPath) => timeSeriesValue[topologyPath]
);
console.log("values", topologyValues);
switch (topologyKey) {
case "gridToAcIn":
topologyValues = [
values.reduce((acc, curr) => Number(acc) + Number(curr.value), 0),
];
break;
default:
topologyValues = values.map(({ value }) => value);
}
return {
...acc,
[topologyKey]: [
{
values:
topologyKey === "gridToAcIn"
? [values.reduce((acc, curr) => Number(acc) + Number(curr))]
: values,
label: topologyValues[topologyKey][0].split("/").pop(),
unit: "V",
values: topologyValues,
label: topologyPaths[topologyKey][0].split("/").pop(),
unit: values[0].unit,
} as BoxData,
],
};
@ -74,7 +80,7 @@ export const extractTopologyValues = (
return null;
};
export const topologyValues: { [key: string]: string[] } = {
export const topologyPaths: { [key: string]: string[] } = {
gridToAcIn: [
"/GridMeter/Ac/L1/Power/Apparent",
"/GridMeter/Ac/L2/Power/Apparent",
@ -101,7 +107,7 @@ export interface Csv {
[key: string]: CsvEntry;
}
export const parseCsv = (text: string) => {
export const parseCsv = (text: string): Csv => {
console.log("split", text.split(/\r?\n/));
const y = text
.split(/\r?\n/)
@ -113,11 +119,11 @@ export const parseCsv = (text: string) => {
const x = y
.map((fields) => {
if (typeof fields[1] === "string") {
return { [fields[0]]: fields[1] };
return { [fields[0]]: { value: fields[1], unit: fields[2] } };
}
return { [fields[0]]: parseFloat(fields[1]) };
return { [fields[0]]: { value: parseFloat(fields[1]), unit: fields[2] } };
})
.reduce((acc, current) => ({ ...acc, ...current }), {} as any);
.reduce((acc, current) => ({ ...acc, ...current }), {} as Csv);
return x;
};