Skip to content

Implemented time string (1d) and dynamic relative time support (current_day) for hours_to_show #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ layout:
config:
scrollZoom: false

hours_to_show: 1
hours_to_show: 1h
refresh_interval: 10 # in seconds
```

Expand All @@ -104,7 +104,7 @@ type: custom:plotly-graph
entities:
- entity: sensor.temperature
refresh_interval: 10
hours_to_show: 12
hours_to_show: 12h
layout:
xaxis:
rangeselector:
Expand Down Expand Up @@ -298,31 +298,31 @@ defaults:

## Offsets

Offsets are useful to shift data in the temporal axis. For example, if you have a sensor that reports the forecasted temperature 3 hours from now, it means that the current value should be plotted in the future. With the `offset` attribute you can shift the data so it is placed in the correct position.
Offsets are useful to shift data in the temporal axis. For example, if you have a sensor that reports the forecasted temperature 3 hours from now, it means that the current value should be plotted in the future. With the `time_offset` attribute you can shift the data so it is placed in the correct position.
Another possible use is to compare past data with the current one. For example, you can plot yesterday's temperature and the current one on top of each other.

The `offset` flag can be specified in two places.
**1)** When used at the top level of the configuration, it specifies how much "future" the graph shows by default. For example, if `hours_to_show` is 16 and `offset` is 3h, the graph shows the past 13 hours (16-3) plus the next 3 hours.
The `time_offset` flag can be specified in two places.
**1)** When used at the top level of the configuration, it specifies how much "future" the graph shows by default. For example, if `hours_to_show` is 16 and `time_offset` is 3h, the graph shows the past 13 hours (16-3) plus the next 3 hours.
**2)** When used at the trace level, it offsets the trace by the specified amount.

```yaml
type: custom:plotly-graph
hours_to_show: 16
offset: 3h
time_offset: 3h
entities:
- entity: sensor.current_temperature
line:
width: 3
color: orange
- entity: sensor.current_temperature
name: Temperature yesterday
offset: 1d
time_offset: 1d
line:
width: 1
dash: dot
color: orange
- entity: sensor.temperature_12h_forecast
offset: 12h
time_offset: 12h
name: Forecast temperature
line:
width: 1
Expand All @@ -346,13 +346,13 @@ When using offsets, it is useful to have a line that indicates the current time.

```yaml
type: custom:plotly-graph
hours_to_show: 6
offset: 3h
hours_to_show: 6h
time_offset: 3h
entities:
- entity: sensor.forecast_temperature
yaxis: y1
offset: 3h
- entity: ''
time_offset: 3h
- entity: ""
name: Now
yaxis: y9
showlegend: false
Expand Down Expand Up @@ -388,7 +388,7 @@ Whenever a time duration can be specified, this is the notation to use:
Example:

```yaml
offset: 3h
time_offset: 3h
```

## Extra entity attributes:
Expand Down Expand Up @@ -818,7 +818,7 @@ entities:
x: $fn ({ys,vars}) => ys
type: histogram
title: Temperature Histogram last 10 days
hours_to_show: 240
hours_to_show: 10d
raw_plotly_config: true
layout:
margin:
Expand Down Expand Up @@ -850,7 +850,7 @@ entities:
<b>Target:</b>%{y}</br>
<b>Current:</b>%{customdata.current_temperature}
<extra></extra>
hours_to_show: 24
hours_to_show: current_day
```

## Default trace & axis styling
Expand Down Expand Up @@ -923,7 +923,18 @@ When true, the custom implementations of pinch-to-zoom and double-tap-drag-to-zo
## hours_to_show:

How many hours are shown.
Exactly the same as the history card, except decimal values (e.g `0.1`) do actually work
Exactly the same as the history card, but more powerful

### Fixed Relative Time

- Decimal values (e.g `hours_to_show: 0.5`)
- Duration strings (e.g `hours_to_show: 2h`, `3d`, `1w`, `1M`). See #Duration

### Dynamic Relative Time

Shows the current day, hour, etc from beginning to end.
The options are: `current_minute`, `current_hour`, `current_day`, `current_week`, `current_month`, `current_quarter`, `current_year`
It can be combined with the global `time_offset`.

## refresh_interval:

Expand Down
56 changes: 56 additions & 0 deletions src/duration/duration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import {
endOfDay,
endOfHour,
endOfMinute,
endOfMonth,
endOfQuarter,
endOfWeek,
endOfYear,
startOfDay,
startOfHour,
startOfMinute,
startOfMonth,
startOfQuarter,
startOfWeek,
startOfYear,
} from "date-fns";

export const timeUnits = {
ms: 1,
s: 1000,
Expand Down Expand Up @@ -36,3 +53,42 @@ export const parseTimeDuration = (str: TimeDurationStr | undefined): number => {

return sign * number * unit;
};

export const isTimeDuration = (str: any) => {
try {
parseTimeDuration(str);
return true;
} catch (e) {
return false;
}
};

export const parseRelativeTime = (str: string): [number, number] => {
const now = new Date();
switch (str) {
case "current_minute":
return [+startOfMinute(now), +endOfMinute(now)];
case "current_hour":
return [+startOfHour(now), +endOfHour(now)];
case "current_day":
return [+startOfDay(now), +endOfDay(now)];
case "current_week":
return [+startOfWeek(now), +endOfWeek(now)];
case "current_month":
return [+startOfMonth(now), +endOfMonth(now)];
case "current_quarter":
return [+startOfQuarter(now), +endOfQuarter(now)];
case "current_year":
return [+startOfYear(now), +endOfYear(now)];
}
throw new Error(`${str} is not a dynamic relative time`);
};

export const isRelativeTime = (str: any) => {
try {
parseRelativeTime(str);
return true;
} catch (e) {
return false;
}
};
7 changes: 5 additions & 2 deletions src/filters/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const filters = {
},
delta:
() =>
({ ys, meta }) => {
({ ys, meta, xs, statistics, states }) => {
const last = {
y: NaN,
};
Expand All @@ -112,7 +112,10 @@ const filters = {
const yDelta = y - last.y;
last.y = y;
return yDelta;
}),
}).slice(1),
xs: xs.slice(1),
statistics: statistics.slice(1),
states: states.slice(1),
};
},
derivate:
Expand Down
38 changes: 30 additions & 8 deletions src/parse-config/parse-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import propose from "propose";

import get from "lodash/get";
import { addPreParsingDefaults, addPostParsingDefaults } from "./defaults";
import { parseTimeDuration } from "../duration/duration";
import {
isRelativeTime,
isTimeDuration,
parseRelativeTime,
parseTimeDuration,
} from "../duration/duration";
import { parseStatistics } from "./parse-statistics";
import { HomeAssistant } from "custom-card-helpers";
import filters from "../filters/filters";
Expand Down Expand Up @@ -207,15 +212,32 @@ class ConfigParser {
private async fetchDataForEntity(path: string) {
let visible_range = this.fnParam.getFromConfig("visible_range");
if (!visible_range) {
const hours_to_show = this.fnParam.getFromConfig("hours_to_show");
const global_offset = parseTimeDuration(
let global_offset = parseTimeDuration(
this.fnParam.getFromConfig("time_offset")
);
const ms = hours_to_show * 60 * 60 * 1000;
visible_range = [
+new Date() - ms + global_offset,
+new Date() + global_offset,
] as [number, number];
const hours_to_show = this.fnParam.getFromConfig("hours_to_show");
if (isRelativeTime(hours_to_show)) {
const [start, end] = parseRelativeTime(hours_to_show);
visible_range = [start + global_offset, end + global_offset] as [
number,
number
];
} else {
let ms_to_show;
if (isTimeDuration(hours_to_show)) {
ms_to_show = parseTimeDuration(hours_to_show);
} else if (typeof hours_to_show === "number") {
ms_to_show = hours_to_show * 60 * 60 * 1000;
} else {
throw new Error(
`${hours_to_show} is not a valid duration. Use numbers, durations (e.g 1d) or dynamic time (e.g current_day)`
);
}
visible_range = [
+new Date() - ms_to_show + global_offset,
+new Date() + global_offset,
] as [number, number];
}
this.yaml.visible_range = visible_range;
}
this.observed_range[0] = Math.min(this.observed_range[0], visible_range[0]);
Expand Down
18 changes: 9 additions & 9 deletions src/plotly-graph-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,19 @@ export class PlotlyGraph extends HTMLElement {
}

disconnectedCallback() {
this.handles.resizeObserver!.disconnect();
this.handles.relayoutListener!.off("plotly_relayout", this.onRelayout);
this.handles.restyleListener!.off("plotly_restyle", this.onRestyle);
this.handles.legendItemClick!.off(
this.handles.resizeObserver?.disconnect();
this.handles.relayoutListener?.off("plotly_relayout", this.onRelayout);
this.handles.restyleListener?.off("plotly_restyle", this.onRestyle);
this.handles.legendItemClick?.off(
"plotly_legendclick",
this.onLegendItemClick
)!;
this.handles.legendItemDoubleclick!.off(
);
this.handles.legendItemDoubleclick?.off(
"plotly_legenddoubleclick",
this.onLegendItemDoubleclick
)!;
this.handles.dataClick!.off("plotly_click", this.onDataClick)!;
this.handles.doubleclick!.off("plotly_doubleclick", this.onDoubleclick)!;
);
this.handles.dataClick?.off("plotly_click", this.onDataClick);
this.handles.doubleclick?.off("plotly_doubleclick", this.onDoubleclick);
clearTimeout(this.handles.refreshTimeout!);
this.resetButtonEl.removeEventListener("click", this.exitBrowsingMode);
this.touchController.disconnect();
Expand Down