Skip to content

Commit 6731fac

Browse files
authored
Implemented time string and dynamic relative time support for hours_to_show (#226)
1 parent 43c44ea commit 6731fac

File tree

5 files changed

+127
-35
lines changed

5 files changed

+127
-35
lines changed

readme.md

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ layout:
9191
config:
9292
scrollZoom: false
9393

94-
hours_to_show: 1
94+
hours_to_show: 1h
9595
refresh_interval: 10 # in seconds
9696
```
9797
@@ -104,7 +104,7 @@ type: custom:plotly-graph
104104
entities:
105105
- entity: sensor.temperature
106106
refresh_interval: 10
107-
hours_to_show: 12
107+
hours_to_show: 12h
108108
layout:
109109
xaxis:
110110
rangeselector:
@@ -298,31 +298,31 @@ defaults:
298298

299299
## Offsets
300300

301-
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.
301+
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.
302302
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.
303303

304-
The `offset` flag can be specified in two places.
305-
**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.
304+
The `time_offset` flag can be specified in two places.
305+
**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.
306306
**2)** When used at the trace level, it offsets the trace by the specified amount.
307307

308308
```yaml
309309
type: custom:plotly-graph
310310
hours_to_show: 16
311-
offset: 3h
311+
time_offset: 3h
312312
entities:
313313
- entity: sensor.current_temperature
314314
line:
315315
width: 3
316316
color: orange
317317
- entity: sensor.current_temperature
318318
name: Temperature yesterday
319-
offset: 1d
319+
time_offset: 1d
320320
line:
321321
width: 1
322322
dash: dot
323323
color: orange
324324
- entity: sensor.temperature_12h_forecast
325-
offset: 12h
325+
time_offset: 12h
326326
name: Forecast temperature
327327
line:
328328
width: 1
@@ -346,13 +346,13 @@ When using offsets, it is useful to have a line that indicates the current time.
346346

347347
```yaml
348348
type: custom:plotly-graph
349-
hours_to_show: 6
350-
offset: 3h
349+
hours_to_show: 6h
350+
time_offset: 3h
351351
entities:
352352
- entity: sensor.forecast_temperature
353353
yaxis: y1
354-
offset: 3h
355-
- entity: ''
354+
time_offset: 3h
355+
- entity: ""
356356
name: Now
357357
yaxis: y9
358358
showlegend: false
@@ -388,7 +388,7 @@ Whenever a time duration can be specified, this is the notation to use:
388388
Example:
389389

390390
```yaml
391-
offset: 3h
391+
time_offset: 3h
392392
```
393393

394394
## Extra entity attributes:
@@ -818,7 +818,7 @@ entities:
818818
x: $fn ({ys,vars}) => ys
819819
type: histogram
820820
title: Temperature Histogram last 10 days
821-
hours_to_show: 240
821+
hours_to_show: 10d
822822
raw_plotly_config: true
823823
layout:
824824
margin:
@@ -850,7 +850,7 @@ entities:
850850
<b>Target:</b>%{y}</br>
851851
<b>Current:</b>%{customdata.current_temperature}
852852
<extra></extra>
853-
hours_to_show: 24
853+
hours_to_show: current_day
854854
```
855855

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

925925
How many hours are shown.
926-
Exactly the same as the history card, except decimal values (e.g `0.1`) do actually work
926+
Exactly the same as the history card, but more powerful
927+
928+
### Fixed Relative Time
929+
930+
- Decimal values (e.g `hours_to_show: 0.5`)
931+
- Duration strings (e.g `hours_to_show: 2h`, `3d`, `1w`, `1M`). See #Duration
932+
933+
### Dynamic Relative Time
934+
935+
Shows the current day, hour, etc from beginning to end.
936+
The options are: `current_minute`, `current_hour`, `current_day`, `current_week`, `current_month`, `current_quarter`, `current_year`
937+
It can be combined with the global `time_offset`.
927938

928939
## refresh_interval:
929940

src/duration/duration.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
import {
2+
endOfDay,
3+
endOfHour,
4+
endOfMinute,
5+
endOfMonth,
6+
endOfQuarter,
7+
endOfWeek,
8+
endOfYear,
9+
startOfDay,
10+
startOfHour,
11+
startOfMinute,
12+
startOfMonth,
13+
startOfQuarter,
14+
startOfWeek,
15+
startOfYear,
16+
} from "date-fns";
17+
118
export const timeUnits = {
219
ms: 1,
320
s: 1000,
@@ -36,3 +53,42 @@ export const parseTimeDuration = (str: TimeDurationStr | undefined): number => {
3653

3754
return sign * number * unit;
3855
};
56+
57+
export const isTimeDuration = (str: any) => {
58+
try {
59+
parseTimeDuration(str);
60+
return true;
61+
} catch (e) {
62+
return false;
63+
}
64+
};
65+
66+
export const parseRelativeTime = (str: string): [number, number] => {
67+
const now = new Date();
68+
switch (str) {
69+
case "current_minute":
70+
return [+startOfMinute(now), +endOfMinute(now)];
71+
case "current_hour":
72+
return [+startOfHour(now), +endOfHour(now)];
73+
case "current_day":
74+
return [+startOfDay(now), +endOfDay(now)];
75+
case "current_week":
76+
return [+startOfWeek(now), +endOfWeek(now)];
77+
case "current_month":
78+
return [+startOfMonth(now), +endOfMonth(now)];
79+
case "current_quarter":
80+
return [+startOfQuarter(now), +endOfQuarter(now)];
81+
case "current_year":
82+
return [+startOfYear(now), +endOfYear(now)];
83+
}
84+
throw new Error(`${str} is not a dynamic relative time`);
85+
};
86+
87+
export const isRelativeTime = (str: any) => {
88+
try {
89+
parseRelativeTime(str);
90+
return true;
91+
} catch (e) {
92+
return false;
93+
}
94+
};

src/filters/filters.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const filters = {
9999
},
100100
delta:
101101
() =>
102-
({ ys, meta }) => {
102+
({ ys, meta, xs, statistics, states }) => {
103103
const last = {
104104
y: NaN,
105105
};
@@ -112,7 +112,10 @@ const filters = {
112112
const yDelta = y - last.y;
113113
last.y = y;
114114
return yDelta;
115-
}),
115+
}).slice(1),
116+
xs: xs.slice(1),
117+
statistics: statistics.slice(1),
118+
states: states.slice(1),
116119
};
117120
},
118121
derivate:

src/parse-config/parse-config.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import propose from "propose";
55

66
import get from "lodash/get";
77
import { addPreParsingDefaults, addPostParsingDefaults } from "./defaults";
8-
import { parseTimeDuration } from "../duration/duration";
8+
import {
9+
isRelativeTime,
10+
isTimeDuration,
11+
parseRelativeTime,
12+
parseTimeDuration,
13+
} from "../duration/duration";
914
import { parseStatistics } from "./parse-statistics";
1015
import { HomeAssistant } from "custom-card-helpers";
1116
import filters from "../filters/filters";
@@ -207,15 +212,32 @@ class ConfigParser {
207212
private async fetchDataForEntity(path: string) {
208213
let visible_range = this.fnParam.getFromConfig("visible_range");
209214
if (!visible_range) {
210-
const hours_to_show = this.fnParam.getFromConfig("hours_to_show");
211-
const global_offset = parseTimeDuration(
215+
let global_offset = parseTimeDuration(
212216
this.fnParam.getFromConfig("time_offset")
213217
);
214-
const ms = hours_to_show * 60 * 60 * 1000;
215-
visible_range = [
216-
+new Date() - ms + global_offset,
217-
+new Date() + global_offset,
218-
] as [number, number];
218+
const hours_to_show = this.fnParam.getFromConfig("hours_to_show");
219+
if (isRelativeTime(hours_to_show)) {
220+
const [start, end] = parseRelativeTime(hours_to_show);
221+
visible_range = [start + global_offset, end + global_offset] as [
222+
number,
223+
number
224+
];
225+
} else {
226+
let ms_to_show;
227+
if (isTimeDuration(hours_to_show)) {
228+
ms_to_show = parseTimeDuration(hours_to_show);
229+
} else if (typeof hours_to_show === "number") {
230+
ms_to_show = hours_to_show * 60 * 60 * 1000;
231+
} else {
232+
throw new Error(
233+
`${hours_to_show} is not a valid duration. Use numbers, durations (e.g 1d) or dynamic time (e.g current_day)`
234+
);
235+
}
236+
visible_range = [
237+
+new Date() - ms_to_show + global_offset,
238+
+new Date() + global_offset,
239+
] as [number, number];
240+
}
219241
this.yaml.visible_range = visible_range;
220242
}
221243
this.observed_range[0] = Math.min(this.observed_range[0], visible_range[0]);

src/plotly-graph-card.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,19 +184,19 @@ export class PlotlyGraph extends HTMLElement {
184184
}
185185

186186
disconnectedCallback() {
187-
this.handles.resizeObserver!.disconnect();
188-
this.handles.relayoutListener!.off("plotly_relayout", this.onRelayout);
189-
this.handles.restyleListener!.off("plotly_restyle", this.onRestyle);
190-
this.handles.legendItemClick!.off(
187+
this.handles.resizeObserver?.disconnect();
188+
this.handles.relayoutListener?.off("plotly_relayout", this.onRelayout);
189+
this.handles.restyleListener?.off("plotly_restyle", this.onRestyle);
190+
this.handles.legendItemClick?.off(
191191
"plotly_legendclick",
192192
this.onLegendItemClick
193-
)!;
194-
this.handles.legendItemDoubleclick!.off(
193+
);
194+
this.handles.legendItemDoubleclick?.off(
195195
"plotly_legenddoubleclick",
196196
this.onLegendItemDoubleclick
197-
)!;
198-
this.handles.dataClick!.off("plotly_click", this.onDataClick)!;
199-
this.handles.doubleclick!.off("plotly_doubleclick", this.onDoubleclick)!;
197+
);
198+
this.handles.dataClick?.off("plotly_click", this.onDataClick);
199+
this.handles.doubleclick?.off("plotly_doubleclick", this.onDoubleclick);
200200
clearTimeout(this.handles.refreshTimeout!);
201201
this.resetButtonEl.removeEventListener("click", this.exitBrowsingMode);
202202
this.touchController.disconnect();

0 commit comments

Comments
 (0)