192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
import * as React from "react";
|
|
import moment from "moment";
|
|
import { range, clamp } from "lodash";
|
|
|
|
import { SensorReadingPlotProps } from "./interfaces";
|
|
import { calcEndOfPeriod } from "./filter_readings";
|
|
import { t } from "../../i18next_wrapper";
|
|
|
|
/** For SensorReadings plot. */
|
|
export const calcTimeParams = (timePeriod: number): {
|
|
timeStep: number, timeScale: number
|
|
} => {
|
|
if (timePeriod > 3600 * 24 * 32) { // year
|
|
return { timeStep: 3600 * 24 * 30, timeScale: 40 * 30 * 12 };
|
|
}
|
|
if (timePeriod > 3600 * 24 * 8) { // month
|
|
return { timeStep: 3600 * 24, timeScale: 40 * 30 };
|
|
}
|
|
if (timePeriod > 3600 * 25) { // week
|
|
return { timeStep: 3600 * 24, timeScale: 40 * 7 };
|
|
}
|
|
// day
|
|
return { timeStep: 3600, timeScale: 40 };
|
|
};
|
|
|
|
interface PlotProps {
|
|
timePeriod: number;
|
|
timeStep: number;
|
|
timeScale: number;
|
|
timeMax: moment.Moment;
|
|
yZero: number;
|
|
yMax: number;
|
|
xMax: number;
|
|
showPreviousPeriod: boolean;
|
|
}
|
|
|
|
/** Plot axes and labels. */
|
|
const Axes =
|
|
({ yZero, yMax, xMax }: { yZero: number, yMax: number, xMax: number }) =>
|
|
<g id="axes">
|
|
<line id="y-axis" strokeWidth="5px"
|
|
x1={0} y1={yZero} x2={0} y2={yMax} />
|
|
|
|
<text id="y-axis-label" textAnchor="middle" fontWeight="normal"
|
|
x={-250} y={yZero - 500}>
|
|
{t("analog")}
|
|
</text>
|
|
|
|
{[0, 1].map(v =>
|
|
<text key={"digital_value_y_axis_label_" + v}
|
|
textAnchor="start" alignmentBaseline="middle"
|
|
x={xMax + 50} y={yZero - v * 1023}>
|
|
{v}
|
|
</text>)}
|
|
|
|
<text id="y-axis-label" textAnchor="start" fontWeight="normal"
|
|
x={xMax + 50} y={yZero - 500}>
|
|
{t("digital")}
|
|
</text>
|
|
|
|
<line id="x-axis" strokeWidth="5px"
|
|
x1={0} y1={yZero} x2={xMax} y2={yZero} />
|
|
</g>;
|
|
|
|
/** y-axis (SensorReading value) */
|
|
const HorizontalGridlines = (props: PlotProps) =>
|
|
<g id="horizontal-gridlines">
|
|
{range(0, 1100, 100).map(y => {
|
|
const id = "horizontal_gridline_" + y;
|
|
return <g id={id} key={id}>
|
|
<text textAnchor="end" alignmentBaseline="middle"
|
|
x={-50} y={props.yZero - y}>
|
|
{y}
|
|
</text>
|
|
<line stroke="gray"
|
|
x1={0} y1={props.yZero - y} x2={props.xMax} y2={props.yZero - y} />
|
|
</g>;
|
|
})}
|
|
</g>;
|
|
|
|
/** x-axis (time) labels */
|
|
const createTimeLabel =
|
|
(x: number, timePeriod: number, timeStep: number, timeMax: moment.Moment) =>
|
|
(period: "current" | "previous"): string => {
|
|
const calcFormat = () => {
|
|
if (timePeriod > 3600 * 24 * 32) { return "MMM D YYYY"; }
|
|
if (timeStep > 3600) { return "MMM D"; }
|
|
return "h:mm A";
|
|
};
|
|
return timeMax.clone()
|
|
.subtract(timePeriod * (period === "current" ? 1 : 2) - x,
|
|
"seconds")
|
|
.format(calcFormat());
|
|
};
|
|
|
|
/** x-axis (time) */
|
|
const VerticalGridlines = (props: PlotProps) =>
|
|
<g id="vertical-gridlines">
|
|
{range(props.timeStep, props.timePeriod + 1, props.timeStep).map(x => {
|
|
const id = "vertical_gridline_" + x;
|
|
/** label & major gridline every 3 hours/days/months and every week day */
|
|
const major = (x / props.timeStep)
|
|
% (props.timePeriod == 3600 * 24 * 7 ? 1 : 3) == 0;
|
|
const createLabel =
|
|
createTimeLabel(x, props.timePeriod, props.timeStep, props.timeMax);
|
|
return <g id={id} key={id}>
|
|
{major &&
|
|
<text textAnchor="middle"
|
|
x={x / props.timeScale}
|
|
y={props.yZero + 100}>
|
|
{createLabel("current")}
|
|
</text>}
|
|
{major && props.showPreviousPeriod &&
|
|
<text textAnchor="middle" stroke={"gray"}
|
|
x={x / props.timeScale}
|
|
y={props.yZero + 200}>
|
|
{createLabel("previous")}
|
|
</text>}
|
|
<line stroke={major ? "black" : "gray"}
|
|
x1={x / props.timeScale}
|
|
y1={props.yZero}
|
|
x2={x / props.timeScale}
|
|
y2={props.yMax} />
|
|
</g>;
|
|
})}
|
|
</g>;
|
|
|
|
/** SensorReadings (current and maybe previous time periods) */
|
|
const DataPoints = ({ plotProps, parentProps }: {
|
|
plotProps: PlotProps, parentProps: SensorReadingPlotProps
|
|
}) =>
|
|
<g id="sensor-readings">
|
|
{["previous", "current"].map((period: "current" | "previous") =>
|
|
<g id={period} key={period}>
|
|
{parentProps.readingsForPeriod(period).map(r => {
|
|
const created_at =
|
|
moment(r.body.created_at).utcOffset(parentProps.timeOffset);
|
|
const unixMax = calcEndOfPeriod(plotProps.timePeriod,
|
|
plotProps.timeMax.unix(), period);
|
|
/** calculated using scaled plot distance from x-axis end */
|
|
const cx = plotProps.xMax
|
|
- (unixMax - created_at.unix()) / plotProps.timeScale;
|
|
const cy = plotProps.yZero - clamp(r.body.value
|
|
* (r.body.mode == 0 && r.body.value <= 1 ? 1023 : 1), 0, 1023);
|
|
const color = period === "current" ? "black" : "gray";
|
|
const selected = parentProps.hovered === r.uuid;
|
|
return <g id={r.uuid} key={r.uuid}>
|
|
<circle fill={color} stroke={color}
|
|
onMouseEnter={() => parentProps.hover(r.uuid)}
|
|
onMouseLeave={() => parentProps.hover(undefined)}
|
|
r={selected ? 25 : 15}
|
|
cx={cx}
|
|
cy={cy} />
|
|
{selected &&
|
|
<text
|
|
x={cx + 30}
|
|
y={cy - 10}>
|
|
{r.body.value}
|
|
</text>}
|
|
</g>;
|
|
})}
|
|
</g>)}
|
|
</g>;
|
|
|
|
export const SensorReadingsPlot = (props: SensorReadingPlotProps) => {
|
|
const { timePeriod, endDate, timeOffset, showPreviousPeriod } = props;
|
|
const timeVBMax = 2800;
|
|
const yZero = 1100;
|
|
const { timeStep, timeScale } = calcTimeParams(props.timePeriod);
|
|
const plotProps: PlotProps = {
|
|
timePeriod,
|
|
timeStep,
|
|
timeScale,
|
|
timeMax: moment.unix(endDate).startOf("hour").utcOffset(timeOffset),
|
|
yZero,
|
|
yMax: yZero - 1023,
|
|
xMax: timeVBMax - 640,
|
|
showPreviousPeriod,
|
|
};
|
|
|
|
return <svg
|
|
className="sensor-readings-plot"
|
|
width="100%"
|
|
height="100%"
|
|
viewBox={`-350 -100 ${timeVBMax} ${plotProps.yZero + 400}`}>
|
|
<Axes yZero={plotProps.yZero} yMax={plotProps.yMax} xMax={plotProps.xMax} />
|
|
<VerticalGridlines {...plotProps} />
|
|
<HorizontalGridlines {...plotProps} />
|
|
<DataPoints plotProps={plotProps} parentProps={props} />
|
|
</svg>;
|
|
};
|