added tons of files for component functionality; manual component adding added temporarily
This commit is contained in:
parent
58830d480e
commit
45ff49bcea
121 changed files with 25883 additions and 65 deletions
200
src/charts/GrainDryingChart.tsx
Normal file
200
src/charts/GrainDryingChart.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { useTheme } from "@mui/material";
|
||||
import { blue, indigo, orange, red } from "@mui/material/colors";
|
||||
import MaterialChartTooltip from "charts/MaterialChartTooltip";
|
||||
import { usePrevious } from "hooks";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Legend,
|
||||
ReferenceArea,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { roundTo } from "utils/numbers";
|
||||
|
||||
export interface GrainDryingPoint {
|
||||
timestamp: number;
|
||||
dryScore: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: GrainDryingPoint[];
|
||||
showStroke?: boolean;
|
||||
useGradient?: boolean;
|
||||
displayY?: boolean;
|
||||
newXDomain?: number[] | string[];
|
||||
multiGraphZoom?: (domain: number[] | string[]) => void;
|
||||
multiGraphZoomOut?: boolean;
|
||||
}
|
||||
|
||||
export default function GrainDryingChart(props: Props) {
|
||||
const { data, showStroke, useGradient, displayY, newXDomain, multiGraphZoom } = props;
|
||||
const prevData = usePrevious(data);
|
||||
const theme = useTheme();
|
||||
const [offset, setOffset] = useState(0);
|
||||
const now = moment();
|
||||
const veryDry = red[800];
|
||||
const dry = orange[500];
|
||||
const damp = blue[500];
|
||||
const veryDamp = indigo[800];
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [refLeft, setRefLeft] = useState<number | undefined>();
|
||||
const [refRight, setRefRight] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (newXDomain) {
|
||||
setXDomain(newXDomain);
|
||||
}
|
||||
}, [newXDomain]);
|
||||
|
||||
useEffect(() => {
|
||||
const gradientOffset = () => {
|
||||
const vpdMax = Math.max(...data.map(p => p.dryScore));
|
||||
const vpdMin = Math.min(...data.map(p => p.dryScore));
|
||||
|
||||
if (vpdMax <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (vpdMin >= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return vpdMax / (vpdMax - vpdMin);
|
||||
};
|
||||
|
||||
if (!prevData || prevData !== data) {
|
||||
setOffset(gradientOffset());
|
||||
}
|
||||
}, [data, prevData, now]);
|
||||
|
||||
const zoom = () => {
|
||||
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
||||
if (refLeft && refRight && refLeft !== refRight) {
|
||||
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
if (multiGraphZoom) {
|
||||
multiGraphZoom(newDomain);
|
||||
} else {
|
||||
setXDomain(newDomain);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//just commenting this out as this charts zoom will likely never be controlled from inside
|
||||
// const zoomOut = () => {
|
||||
// setXDomain(["dataMin", "dataMax"]);
|
||||
// };
|
||||
|
||||
if (data.length <= 1) return null;
|
||||
return (
|
||||
<ResponsiveContainer width={"100%"} height={270}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e) {
|
||||
setRefLeft(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e: any) => {
|
||||
if (e) {
|
||||
setRefRight(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
zoom();
|
||||
}}>
|
||||
{displayY && (
|
||||
<YAxis
|
||||
tickFormatter={vpd => {
|
||||
let val = vpd.toFixed(2);
|
||||
return val;
|
||||
}}
|
||||
type="number"
|
||||
// label={{
|
||||
// value: "Water Content",
|
||||
// angle: -90,
|
||||
// position: "insideLeft",
|
||||
// fill: theme.palette.text.primary
|
||||
// }}
|
||||
domain={["auto", "auto"]}
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
/>
|
||||
)}
|
||||
<XAxis
|
||||
allowDataOverflow
|
||||
dataKey="timestamp"
|
||||
domain={xDomain}
|
||||
name="Time"
|
||||
tickFormatter={timestamp => {
|
||||
let t = moment(timestamp);
|
||||
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
||||
}}
|
||||
scale="time"
|
||||
type="number"
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
stroke={theme.palette.divider}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
payload={[
|
||||
{ value: "Drying", type: "square", id: "drying", color: orange[800] },
|
||||
{ value: "Hydrating", type: "square", id: "hydrating", color: blue[800] }
|
||||
]}
|
||||
/>
|
||||
<Tooltip
|
||||
animationEasing="ease-out"
|
||||
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
||||
content={(props: TooltipProps<any, any>) => (
|
||||
<MaterialChartTooltip
|
||||
{...props}
|
||||
valueFormatter={score =>
|
||||
`${roundTo(parseFloat(String(score)), 2).toString()} ${
|
||||
score === 0 ? "" : score as number > 0 ? " (Drying)" : " (Hydrating)"
|
||||
}`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="gradientColour" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={veryDry} stopOpacity="90%" />
|
||||
<stop offset={offset} stopColor={dry} stopOpacity="75%" />
|
||||
<stop offset={offset} stopColor={damp} stopOpacity="75%" />
|
||||
<stop offset="100%" stopColor={veryDamp} stopOpacity="90%" />
|
||||
</linearGradient>
|
||||
<linearGradient id="solidColour" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset={offset} stopColor={dry} stopOpacity="75%" />
|
||||
<stop offset={offset} stopColor={damp} stopOpacity="75%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={"dryScore"}
|
||||
name="Score"
|
||||
stroke={
|
||||
showStroke
|
||||
? useGradient
|
||||
? "url(#gradientColour)"
|
||||
: "url(#solidColour)"
|
||||
: "transparent"
|
||||
}
|
||||
strokeWidth={3}
|
||||
fill={useGradient ? "url(#gradientColour)" : "url(#solidColour)"}
|
||||
/>
|
||||
{refLeft && refRight ? (
|
||||
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
|
||||
) : null}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
134
src/charts/GraphSettings.tsx
Normal file
134
src/charts/GraphSettings.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
MenuItem,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useComponentAPI, useSnackbar } from "hooks";
|
||||
import { Component } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
// import { MatchParams } from "navigation/Routes";
|
||||
// import { useRouteMatch } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
newChart?: boolean;
|
||||
closeDialog: () => void;
|
||||
component: Component;
|
||||
mutation?: pond.Mutator;
|
||||
currentMin: number | string;
|
||||
currentMax: number | string;
|
||||
adjustMin: (val: number) => void;
|
||||
adjustMax: (val: number) => void;
|
||||
}
|
||||
|
||||
export default function GraphSettings(props: Props) {
|
||||
const {
|
||||
open,
|
||||
component,
|
||||
mutation,
|
||||
closeDialog,
|
||||
adjustMin,
|
||||
adjustMax,
|
||||
currentMin,
|
||||
currentMax,
|
||||
newChart
|
||||
} = props;
|
||||
const [newMutation, setNewMutation] = useState<pond.Mutator>(0);
|
||||
const componentAPI = useComponentAPI();
|
||||
const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
if (mutation !== undefined) {
|
||||
setNewMutation(mutation);
|
||||
}
|
||||
}, [mutation]);
|
||||
|
||||
const saveMutation = () => {
|
||||
let comp = component;
|
||||
let defaultMutations = comp.settings.defaultMutations;
|
||||
if (!defaultMutations.includes(newMutation) && newMutation !== pond.Mutator.MUTATOR_NONE) {
|
||||
defaultMutations.push(newMutation);
|
||||
}
|
||||
componentAPI
|
||||
.update(parseInt(deviceID), comp.settings)
|
||||
.then(() => openSnack("Mutations Updated"));
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const deleteDerivedChart = () => {
|
||||
let comp = component;
|
||||
let defaultMuts = comp.settings.defaultMutations;
|
||||
if (mutation && defaultMuts.includes(mutation)) {
|
||||
defaultMuts.splice(defaultMuts.indexOf(mutation));
|
||||
}
|
||||
componentAPI
|
||||
.update(parseInt(deviceID), comp.settings)
|
||||
.then(() => openSnack("Mutations Updated"));
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
setNewMutation(event.target.value as pond.Mutator);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={closeDialog}>
|
||||
<DialogTitle>Chart Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
{newChart && (
|
||||
<TextField select fullWidth value={newMutation} onChange={handleChange}>
|
||||
{/* TODO: change the menu item to be from the available mutations in the component settings (doesn't exist yet) */}
|
||||
<MenuItem value={pond.Mutator.MUTATOR_NONE}>None</MenuItem>
|
||||
<MenuItem value={pond.Mutator.MUTATOR_EMC}>Grain Moisture(EMC)</MenuItem>
|
||||
<MenuItem value={pond.Mutator.MUTATOR_CFM}>CFM</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
{!newChart && (
|
||||
<Grid container direction="column">
|
||||
<Grid>
|
||||
<TextField
|
||||
label="Min Range"
|
||||
type="number"
|
||||
fullWidth
|
||||
defaultValue={currentMin}
|
||||
onChange={e => adjustMin(parseInt(e.target.value))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
label="Max Range"
|
||||
type="number"
|
||||
fullWidth
|
||||
defaultValue={currentMax}
|
||||
onChange={e => adjustMax(parseInt(e.target.value))}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{mutation !== undefined && mutation > pond.Mutator.MUTATOR_NONE && (
|
||||
<Button onClick={deleteDerivedChart} variant="contained" style={{ background: "red" }}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={closeDialog} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
{mutation === pond.Mutator.MUTATOR_NONE && (
|
||||
<Button variant="contained" onClick={saveMutation} color="primary">
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
47
src/charts/MaterialChartTooltip.tsx
Normal file
47
src/charts/MaterialChartTooltip.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { TooltipProps } from "recharts";
|
||||
|
||||
interface Props extends TooltipProps<any, any> {
|
||||
valueFormatter?: (
|
||||
value: React.ReactText | string | number | readonly (string | number)[]
|
||||
) => string;
|
||||
}
|
||||
export default function MaterialChartTooltip(props: Props) {
|
||||
const { active, label, payload, labelFormatter, valueFormatter } = props;
|
||||
const formattedLabel = () => {
|
||||
if (!label) return "Unknown";
|
||||
return labelFormatter ? labelFormatter(label, payload ?? []) : label;
|
||||
};
|
||||
|
||||
const payloadContent = (p: any) => {
|
||||
const { name, value, dataKey } = p;
|
||||
//if (!value) return null; // this was preventing 0's from displaying correctly
|
||||
return (
|
||||
<Typography color="textPrimary" variant="subtitle1" key={dataKey}>
|
||||
{p.payload.name ?? name ?? "Unknown"}
|
||||
{": "}
|
||||
<Box component="span">{valueFormatter ? valueFormatter(value) : value.toString()}</Box>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<Paper variant="outlined" style={{ opacity: 0.9 }}>
|
||||
<Box padding={1}>
|
||||
{payload && payload.map(p => payloadContent(p))}
|
||||
<Typography color="textSecondary" variant="caption">
|
||||
{formattedLabel()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
410
src/charts/MeasurementsChart.tsx
Normal file
410
src/charts/MeasurementsChart.tsx
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { extension, GraphFilters, showMultilineCable } from "pbHelpers/ComponentType";
|
||||
import { Component, Interaction } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { GraphType } from "common/Graph";
|
||||
import AreaGraph, { AreaData } from "./measurementCharts/AreaGraph";
|
||||
import moment, { Moment } from "moment";
|
||||
import MultiLineGraph, { LineData } from "./measurementCharts/MultiLineGraph";
|
||||
import { ReferenceArea, ReferenceLine } from "recharts";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import GroupSettingsIcon from "@mui/icons-material/Settings";
|
||||
import { green } from "@mui/material/colors";
|
||||
import GraphSettings from "./GraphSettings";
|
||||
import TimeBar from "common/time/TimeBar";
|
||||
import { useMobile } from "hooks";
|
||||
import { useGlobalState } from "providers";
|
||||
|
||||
interface Props {
|
||||
startDate: Moment;
|
||||
endDate: Moment;
|
||||
component: Component;
|
||||
interactions: Interaction[];
|
||||
filters?: GraphFilters;
|
||||
unitMeasurements: UnitMeasurement[];
|
||||
sampleLimit?: number;
|
||||
updateDateRange: (start: Moment, end: Moment, live: boolean) => void;
|
||||
showOriginal: boolean;
|
||||
showAveraged: boolean;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
min: number | string;
|
||||
max: number | string;
|
||||
}
|
||||
|
||||
export default function MeasurementsChart(props: Props) {
|
||||
const {
|
||||
component,
|
||||
interactions,
|
||||
filters,
|
||||
unitMeasurements,
|
||||
updateDateRange,
|
||||
startDate,
|
||||
endDate,
|
||||
showOriginal,
|
||||
showAveraged,
|
||||
sampleLimit
|
||||
} = props;
|
||||
const [currentMutator, setCurrentMutator] = useState<pond.Mutator | undefined>();
|
||||
const [graphSettingsDialog, setGraphSettingsDialog] = useState(false);
|
||||
const [graphRanges, setGraphRanges] = useState<Range[]>([]);
|
||||
const [rangeIndex, setRangeIndex] = useState(0);
|
||||
const [currentMin, setCurrentMin] = useState<number | string>("dataMin");
|
||||
const [currentMax, setCurrentMax] = useState<number | string>("dataMax");
|
||||
const [newChart, setNewChart] = useState(false);
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [{ showErrors }, dispatch] = useGlobalState();
|
||||
const isMobile = useMobile();
|
||||
// const [zoomStart, setZoomStart] = useState<Moment>();
|
||||
// const [zoomEnd, setZoomEnd] = useState<Moment>();
|
||||
// const [zoomed, setZoomed] = useState<boolean>(false);
|
||||
|
||||
// const zoomOut = () => {
|
||||
// setXDomain(["dataMin", "dataMax"]);
|
||||
// setZoomed(false);
|
||||
// };
|
||||
|
||||
const zoomIn = (domain: number[] | string[]) => {
|
||||
let start;
|
||||
let end;
|
||||
if (domain[0]) {
|
||||
start = moment(domain[0]);
|
||||
}
|
||||
if (domain[1]) {
|
||||
//add 1 second to the end time to prevent the last measurement from being cut off due to the milliseconds on the time in the database
|
||||
end = moment(domain[1]).add(1, "second");
|
||||
}
|
||||
setXDomain(domain);
|
||||
//setZoomed(true);
|
||||
//console.log(sampleLimit);
|
||||
//console.log(unitMeasurements);
|
||||
//check if the sample limit was set and we have unit measurements
|
||||
if (sampleLimit && unitMeasurements.length > 0) {
|
||||
//if the number of measurements in the unit measurement is less than half of the sample limit it means there are no more measurements to get,
|
||||
//so if it is bigger then will need to hit the backend to get a new set of measurements
|
||||
if (unitMeasurements[0].values.length >= sampleLimit / 2) {
|
||||
if (start && end) {
|
||||
updateDateRange(start, end, false);
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ranges: Range[] = [];
|
||||
for (let i = 0; i < unitMeasurements.length; i++) {
|
||||
ranges.push({
|
||||
min: "dataMax",
|
||||
max: "dataMin"
|
||||
});
|
||||
}
|
||||
setGraphRanges(ranges);
|
||||
}, [unitMeasurements]);
|
||||
|
||||
//builds the reference lines to show on the graph
|
||||
const getInteractionLines = (measurementType: quack.MeasurementType) => {
|
||||
let selectedInteractions: JSX.Element[] = [];
|
||||
interactions.forEach(interaction => {
|
||||
if (filters?.selectedInteractions?.includes(interaction.key())) {
|
||||
interaction.settings.conditions.forEach((condition, j) => {
|
||||
if (condition.measurementType === measurementType) {
|
||||
let describer = describeMeasurement(condition.measurementType);
|
||||
selectedInteractions.push(
|
||||
<ReferenceLine
|
||||
key={interaction.key() + ":condition" + j}
|
||||
y={describer.toDisplay(condition.value)}
|
||||
ifOverflow="extendDomain"
|
||||
stroke="red"
|
||||
strokeDasharray={"5 2"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return selectedInteractions;
|
||||
};
|
||||
|
||||
//builds the overlays to show on the graph
|
||||
const getOverlays = (measurementType: quack.MeasurementType) => {
|
||||
let overlays: JSX.Element[] = [];
|
||||
component.settings.overlays.forEach((overlay, i) => {
|
||||
if (
|
||||
overlay.measurementType === measurementType &&
|
||||
filters?.selectedOverlays?.includes(i.toString())
|
||||
) {
|
||||
overlays.push(
|
||||
<ReferenceArea
|
||||
key={i}
|
||||
y1={overlay.max}
|
||||
y2={overlay.min}
|
||||
stroke={overlay.colour}
|
||||
strokeWidth={2}
|
||||
opacity={0.75}
|
||||
fill={overlay.colour}
|
||||
ifOverflow="extendDomain">
|
||||
{/* could possibly show overlay message here */}
|
||||
</ReferenceArea>
|
||||
);
|
||||
}
|
||||
});
|
||||
return overlays;
|
||||
};
|
||||
|
||||
const buildGraphs = (
|
||||
unitMeasurements: UnitMeasurement[],
|
||||
component: Component,
|
||||
filters?: GraphFilters
|
||||
) => {
|
||||
let graphs: JSX.Element[] = [];
|
||||
let hasData = false;
|
||||
let noData: JSX.Element = (
|
||||
<Box width="100%" height={350} paddingTop={15}>
|
||||
<Typography variant="h6" color="textPrimary" align="center">
|
||||
Not enough data to display
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" color="textSecondary" align="center">
|
||||
{"(Queried between " +
|
||||
moment(startDate).calendar() +
|
||||
" and " +
|
||||
moment(endDate).calendar() +
|
||||
")"}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
let elem: JSX.Element;
|
||||
let graph: JSX.Element = noData;
|
||||
let numBasicGraphs = unitMeasurements.length - 1;
|
||||
let ext = extension(component.type(), component.subType());
|
||||
|
||||
unitMeasurements.forEach((measurements, i) => {
|
||||
let m = pond.UnitMeasurementsForComponent.fromObject(measurements);
|
||||
let describer = describeMeasurement(
|
||||
m.type,
|
||||
component.settings.type,
|
||||
component.settings.subtype
|
||||
);
|
||||
let min: string | number = "dataMin";
|
||||
let max: string | number = "dataMax";
|
||||
if (graphRanges[i]) {
|
||||
min = graphRanges[i].min;
|
||||
max = graphRanges[i].max;
|
||||
}
|
||||
if (showMultilineCable(component.type(), filters)) {
|
||||
let lineChartData = ext.lineChartData(m, component.settings.smoothingAverages, filters);
|
||||
let lineData: LineData[] = lineChartData.lines;
|
||||
let averagedData: LineData[] = lineChartData.average;
|
||||
if (lineData.length > 0) {
|
||||
hasData = true;
|
||||
graph = (
|
||||
<MultiLineGraph
|
||||
yMin={min}
|
||||
yMax={max}
|
||||
customHeight={isMobile ? 200 : 350}
|
||||
lineData={showOriginal ? lineData : []}
|
||||
averagedData={showAveraged ? averagedData : []}
|
||||
numLines={m.values.length > 1 ? m.values[0].values.length : 0}
|
||||
describer={describer}
|
||||
interactionLines={getInteractionLines(m.type)}
|
||||
overlays={getOverlays(m.type)}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
zoomIn(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
switch (describer.graph()) {
|
||||
case GraphType.AREA:
|
||||
let chartData = ext.areaChartData(m, component.settings.smoothingAverages, filters);
|
||||
let areaData: AreaData[] = chartData.area;
|
||||
let avgData: AreaData[] = chartData.average;
|
||||
if (areaData.length > 0) {
|
||||
hasData = true;
|
||||
graph = (
|
||||
<AreaGraph
|
||||
customHeight={isMobile ? 200 : 350}
|
||||
yMin={min}
|
||||
yMax={max}
|
||||
data={showOriginal ? areaData : []}
|
||||
averagedData={showAveraged ? avgData : []}
|
||||
describer={describer}
|
||||
interactionLines={getInteractionLines(m.type)}
|
||||
overlays={getOverlays(m.type)}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
zoomIn(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
//for the moment not going to worry about these graphs and possibly just make them as needed
|
||||
case GraphType.SCATTER:
|
||||
break;
|
||||
case GraphType.RADAR:
|
||||
break;
|
||||
case GraphType.BAR:
|
||||
break;
|
||||
default:
|
||||
//line graph
|
||||
let lineChartData = ext.lineChartData(m, component.settings.smoothingAverages, filters);
|
||||
let lineData: LineData[] = lineChartData.lines;
|
||||
let averagedData: LineData[] = lineChartData.average;
|
||||
if (lineData.length > 0) {
|
||||
hasData = true;
|
||||
graph = (
|
||||
<MultiLineGraph
|
||||
yMin={min}
|
||||
yMax={max}
|
||||
customHeight={isMobile ? 200 : 350}
|
||||
lineData={showOriginal ? lineData : []}
|
||||
averagedData={showAveraged ? averagedData : []}
|
||||
numLines={m.values.length > 1 ? m.values[0].values.length : 0}
|
||||
describer={describer}
|
||||
interactionLines={getInteractionLines(m.type)}
|
||||
overlays={getOverlays(m.type)}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
zoomIn(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
elem = (
|
||||
<React.Fragment key={i}>
|
||||
<Card raised style={{ width: "100%", margin: 5 }}>
|
||||
<CardContent>
|
||||
<Box display="flex">
|
||||
<Box width="97%">{graph}</Box>
|
||||
<Box justifyContent="center">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (i + 1 > numBasicGraphs) {
|
||||
setCurrentMutator(component.settings.defaultMutations[i - numBasicGraphs]);
|
||||
} else {
|
||||
setCurrentMutator(undefined);
|
||||
}
|
||||
setGraphSettingsDialog(true);
|
||||
setRangeIndex(i);
|
||||
setNewChart(false);
|
||||
if (graphRanges[i]) {
|
||||
setCurrentMax(max);
|
||||
setCurrentMin(min);
|
||||
}
|
||||
}}>
|
||||
<GroupSettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
graphs.push(elem);
|
||||
});
|
||||
return hasData ? graphs : noData;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!isMobile ? (
|
||||
<Grid container direction="row" justifyContent="space-between" style={{ marginBottom: 10 }}>
|
||||
<Grid>
|
||||
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
|
||||
</Grid>
|
||||
<Grid>
|
||||
<FormControlLabel
|
||||
label="Show Errors"
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showErrors}
|
||||
onChange={() => {
|
||||
//setShowErrors(!showErrors);
|
||||
dispatch({ key: "showErrors", value: !showErrors });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
{/* <Grid item>
|
||||
{zoomed && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
if (zoomStart && zoomEnd) {
|
||||
updateDateRange(zoomStart, zoomEnd, false);
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
}
|
||||
}}>
|
||||
Enhance
|
||||
</Button>
|
||||
)}
|
||||
{zoomed && (
|
||||
<Button style={{ marginLeft: 5 }} variant="outlined" onClick={zoomOut}>
|
||||
Zoom Out
|
||||
</Button>
|
||||
)}
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
) : (
|
||||
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
|
||||
)}
|
||||
|
||||
{buildGraphs(unitMeasurements, component, filters)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCurrentMutator(pond.Mutator.MUTATOR_NONE);
|
||||
setGraphSettingsDialog(true);
|
||||
setNewChart(true);
|
||||
}}
|
||||
variant="contained"
|
||||
style={{ width: "100%", height: 50, background: green["500"] }}>
|
||||
+
|
||||
</Button>
|
||||
<GraphSettings
|
||||
open={graphSettingsDialog}
|
||||
closeDialog={() => {
|
||||
setGraphSettingsDialog(false);
|
||||
}}
|
||||
newChart={newChart}
|
||||
component={component}
|
||||
mutation={currentMutator}
|
||||
currentMin={currentMin}
|
||||
currentMax={currentMax}
|
||||
adjustMin={val => {
|
||||
graphRanges[rangeIndex].min = val;
|
||||
}}
|
||||
adjustMax={val => {
|
||||
graphRanges[rangeIndex].max = val;
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
213
src/charts/measurementCharts/AreaGraph.tsx
Normal file
213
src/charts/measurementCharts/AreaGraph.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import MaterialChartTooltip from "charts/MaterialChartTooltip";
|
||||
import moment from "moment";
|
||||
import { MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGlobalState } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
ResponsiveContainer,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
ReferenceArea
|
||||
} from "recharts";
|
||||
import { roundTo } from "utils";
|
||||
|
||||
export interface AreaData {
|
||||
timestamp: number;
|
||||
value: [number, number];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: AreaData[];
|
||||
averagedData?: AreaData[];
|
||||
describer: MeasurementDescriber;
|
||||
interactionLines?: JSX.Element[];
|
||||
overlays?: JSX.Element[];
|
||||
yMin?: number | string;
|
||||
yMax?: number | string;
|
||||
tooltip?: boolean;
|
||||
newXDomain?: number[] | string[];
|
||||
multiGraphZoom?: (domain: number[] | string[]) => void;
|
||||
multiGraphZoomOut?: boolean;
|
||||
customHeight?: string | number;
|
||||
}
|
||||
|
||||
export default function AreaGraph(props: Props) {
|
||||
const {
|
||||
data,
|
||||
describer,
|
||||
interactionLines,
|
||||
overlays,
|
||||
yMin,
|
||||
yMax,
|
||||
tooltip,
|
||||
newXDomain,
|
||||
multiGraphZoom,
|
||||
multiGraphZoomOut,
|
||||
customHeight,
|
||||
averagedData
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const now = moment();
|
||||
const [refLeft, setRefLeft] = useState<number | undefined>();
|
||||
const [refRight, setRefRight] = useState<number | undefined>();
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
// const [{ newStructure }] = useGlobalState();
|
||||
const [displayData, setDisplayData] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newXDomain) {
|
||||
setXDomain(newXDomain);
|
||||
}
|
||||
}, [newXDomain]);
|
||||
|
||||
useEffect(() => {
|
||||
let d: any[] = [];
|
||||
if (data) {
|
||||
data.forEach(areaData => {
|
||||
let pointSet: any = {
|
||||
timestamp: areaData.timestamp
|
||||
};
|
||||
pointSet[describer.label()] = areaData.value;
|
||||
d.push(pointSet);
|
||||
});
|
||||
}
|
||||
if (averagedData) {
|
||||
averagedData.forEach(areaData => {
|
||||
let pointSet: any = {
|
||||
timestamp: areaData.timestamp
|
||||
};
|
||||
pointSet[describer.label() + "(avg)"] = areaData.value;
|
||||
d.push(pointSet);
|
||||
});
|
||||
}
|
||||
setDisplayData(d);
|
||||
}, [data, averagedData, describer]);
|
||||
|
||||
const zoom = () => {
|
||||
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
||||
if (refLeft && refRight && refLeft !== refRight) {
|
||||
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
if (multiGraphZoom) {
|
||||
multiGraphZoom(newDomain);
|
||||
} else {
|
||||
setXDomain(newDomain);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!multiGraphZoomOut && (
|
||||
<Button variant="outlined" onClick={zoomOut}>
|
||||
Zoom Out
|
||||
</Button>
|
||||
)}
|
||||
<ResponsiveContainer width={"100%"} height={customHeight ?? 350}>
|
||||
<AreaChart
|
||||
data={displayData}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e) {
|
||||
setRefLeft(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e: any) => {
|
||||
if (e) {
|
||||
setRefRight(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
zoom();
|
||||
}}>
|
||||
<YAxis
|
||||
domain={[yMin || yMin === 0 ? yMin : "auto", yMax || yMax === 0 ? yMax : "auto"]}
|
||||
label={{
|
||||
value: describer.label() + " (" + describer.unit() + ")",
|
||||
position: "insideLeft",
|
||||
angle: -90,
|
||||
fill: theme.palette.text.primary
|
||||
}}
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
allowDataOverflow
|
||||
/>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
animationEasing="ease-out"
|
||||
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
||||
content={(props: TooltipProps<any, any>) => {
|
||||
return (
|
||||
<MaterialChartTooltip
|
||||
{...props}
|
||||
valueFormatter={value => {
|
||||
let str = "";
|
||||
if (Array.isArray(value)) {
|
||||
str = value[0].toFixed(1) + ", " + value[1].toFixed(1);
|
||||
} else {
|
||||
str = describer.convertWithoutUnits(
|
||||
roundTo(parseFloat(String(value)), 2),
|
||||
// newStructure
|
||||
);
|
||||
}
|
||||
return str + describer.GetUnit();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<XAxis
|
||||
allowDataOverflow
|
||||
dataKey="timestamp"
|
||||
domain={xDomain}
|
||||
name="Time"
|
||||
tickFormatter={timestamp => {
|
||||
let t = moment(timestamp);
|
||||
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
||||
}}
|
||||
scale="time"
|
||||
type="number"
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
stroke={theme.palette.divider}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<Area
|
||||
dataKey={describer.label()}
|
||||
fill={describer.colour() + "75"}
|
||||
stroke={describer.colour()}
|
||||
strokeWidth={3}
|
||||
type="monotone"
|
||||
/>
|
||||
<Area
|
||||
dataKey={describer.label() + "(avg)"}
|
||||
fill={"white"}
|
||||
stroke={"white"}
|
||||
strokeWidth={3}
|
||||
type="monotone"
|
||||
/>
|
||||
{refLeft && refRight ? (
|
||||
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
|
||||
) : null}
|
||||
{interactionLines}
|
||||
{overlays}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
259
src/charts/measurementCharts/MultiLineGraph.tsx
Normal file
259
src/charts/measurementCharts/MultiLineGraph.tsx
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import { Box, Button, useTheme } from "@material-ui/core";
|
||||
import MaterialChartTooltip from "charts/MaterialChartTooltip";
|
||||
import moment from "moment";
|
||||
import { MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGlobalState } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceArea,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import { roundTo } from "utils";
|
||||
|
||||
export interface LineData {
|
||||
timestamp: number;
|
||||
points: Point[];
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
value: number;
|
||||
node: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
lineData: LineData[];
|
||||
averagedData?: LineData[];
|
||||
numLines: number;
|
||||
describer: MeasurementDescriber;
|
||||
interactionLines?: JSX.Element[];
|
||||
overlays?: JSX.Element[];
|
||||
customHeight?: string | number;
|
||||
tooltip?: boolean;
|
||||
yMin?: number | string;
|
||||
yMax?: number | string;
|
||||
hideY?: boolean;
|
||||
newXDomain?: number[] | string[];
|
||||
multiGraphZoom?: (domain: number[] | string[]) => void;
|
||||
multiGraphZoomOut?: boolean;
|
||||
}
|
||||
|
||||
export default function MultiLineGraph(props: Props) {
|
||||
const {
|
||||
lineData,
|
||||
numLines,
|
||||
describer,
|
||||
interactionLines,
|
||||
overlays,
|
||||
customHeight,
|
||||
tooltip,
|
||||
yMin,
|
||||
yMax,
|
||||
hideY,
|
||||
newXDomain,
|
||||
multiGraphZoom,
|
||||
multiGraphZoomOut,
|
||||
averagedData
|
||||
} = props;
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const theme = useTheme();
|
||||
const now = moment();
|
||||
const [refLeft, setRefLeft] = useState<number | undefined>();
|
||||
const [refRight, setRefRight] = useState<number | undefined>();
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [{ newStructure }] = useGlobalState();
|
||||
|
||||
useEffect(() => {
|
||||
if (newXDomain) {
|
||||
setXDomain(newXDomain);
|
||||
}
|
||||
}, [newXDomain]);
|
||||
|
||||
useEffect(() => {
|
||||
let ld: any[] = [];
|
||||
lineData.forEach(line => {
|
||||
let lines = {
|
||||
timestamp: line.timestamp
|
||||
} as any;
|
||||
line.points.forEach(point => {
|
||||
let nodeLabel = (numLines > 1 ? "Node " + (point.node + 1) + " " : "") + describer.label();
|
||||
let nodeDetails = describer.nodeDetails();
|
||||
if (nodeDetails) {
|
||||
nodeLabel = nodeDetails.labels[point.node];
|
||||
}
|
||||
lines[nodeLabel] = point.value;
|
||||
});
|
||||
ld.push(lines);
|
||||
});
|
||||
if (averagedData) {
|
||||
averagedData.forEach(line => {
|
||||
let lines = {
|
||||
timestamp: line.timestamp
|
||||
} as any;
|
||||
line.points.forEach(point => {
|
||||
let nodeLabel =
|
||||
(numLines > 1 ? "Node " + (point.node + 1) + " " : "") + describer.label() + "(avg)";
|
||||
let nodeDetails = describer.nodeDetails();
|
||||
if (nodeDetails) {
|
||||
nodeLabel = nodeDetails.labels[point.node] + "(avg)";
|
||||
}
|
||||
lines[nodeLabel] = point.value;
|
||||
});
|
||||
ld.push(lines);
|
||||
});
|
||||
}
|
||||
setData(ld);
|
||||
}, [lineData, describer, numLines, averagedData]);
|
||||
|
||||
const buildLines = () => {
|
||||
let lines: JSX.Element[] = [];
|
||||
if (lineData.length > 0) {
|
||||
lineData[0].points.forEach(point => {
|
||||
let nodeLabel = (numLines > 1 ? "Node " + (point.node + 1) + " " : "") + describer.label();
|
||||
let nodeColour = describer.colour();
|
||||
let nodeDetails = describer.nodeDetails();
|
||||
if (nodeDetails) {
|
||||
nodeLabel = nodeDetails.labels[point.node];
|
||||
nodeColour = nodeDetails.colours[point.node];
|
||||
}
|
||||
lines.push(
|
||||
<Line
|
||||
strokeWidth={4}
|
||||
key={nodeLabel}
|
||||
dataKey={nodeLabel}
|
||||
stroke={nodeColour}
|
||||
type="monotone"
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
if (averagedData && averagedData.length > 0) {
|
||||
averagedData[0].points.forEach(point => {
|
||||
let nodeLabel =
|
||||
(numLines > 1 ? "Node " + (point.node + 1) + " " : "") + describer.label() + "(avg)";
|
||||
let nodeDetails = describer.nodeDetails();
|
||||
if (nodeDetails) {
|
||||
nodeLabel = nodeDetails.labels[point.node] + "(avg)";
|
||||
}
|
||||
lines.push(
|
||||
<Line
|
||||
strokeWidth={5}
|
||||
key={nodeLabel}
|
||||
dataKey={nodeLabel}
|
||||
stroke={"white"}
|
||||
type="monotone"
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
const zoom = () => {
|
||||
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
||||
if (refLeft && refRight && refLeft !== refRight) {
|
||||
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
if (multiGraphZoom) {
|
||||
multiGraphZoom(newDomain);
|
||||
} else {
|
||||
setXDomain(newDomain);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height={customHeight ?? 350}>
|
||||
{!multiGraphZoomOut && (
|
||||
<Button variant="outlined" onClick={zoomOut}>
|
||||
Zoom Out
|
||||
</Button>
|
||||
)}
|
||||
<ResponsiveContainer width={"100%"} height={"100%"}>
|
||||
<LineChart
|
||||
data={data}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e) {
|
||||
setRefLeft(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e: any) => {
|
||||
if (e) {
|
||||
setRefRight(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
zoom();
|
||||
}}>
|
||||
{describer.nodeDetails() && <Legend verticalAlign="top" />}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
animationEasing="ease-out"
|
||||
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
||||
content={(props: TooltipProps<any, any>) => {
|
||||
return (
|
||||
<MaterialChartTooltip
|
||||
{...props}
|
||||
valueFormatter={value => {
|
||||
return describer.convertWithUnits(
|
||||
roundTo(parseFloat(String(value)), 2),
|
||||
newStructure
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<YAxis
|
||||
axisLine={hideY}
|
||||
domain={[yMin || yMin === 0 ? yMin : "auto", yMax || yMax === 0 ? yMax : "auto"]}
|
||||
label={{
|
||||
value: describer.label() + " (" + describer.unit() + ")",
|
||||
position: "insideLeft",
|
||||
angle: -90,
|
||||
fill: theme.palette.text.primary
|
||||
}}
|
||||
tick={hideY ? false : { fill: theme.palette.text.primary }}
|
||||
allowDataOverflow
|
||||
/>
|
||||
<XAxis
|
||||
allowDataOverflow
|
||||
dataKey="timestamp"
|
||||
domain={xDomain}
|
||||
name="Time"
|
||||
tickFormatter={timestamp => {
|
||||
let t = moment(timestamp);
|
||||
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
||||
}}
|
||||
scale="time"
|
||||
type="number"
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
stroke={theme.palette.divider}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
{refLeft && refRight ? (
|
||||
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
|
||||
) : null}
|
||||
{buildLines()}
|
||||
{interactionLines}
|
||||
{overlays}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue