Skip to content

Commit 3fcbb55

Browse files
author
David Buezas
committed
added unit_of_measurement and lambdas
1 parent 13a61a3 commit 3fcbb55

File tree

4 files changed

+129
-18
lines changed

4 files changed

+129
-18
lines changed

dist/plotly-graph-card.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readme.md

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ You may find some extra info there in this link
99

1010
## Share and see what others are doing with this
1111

12-
Check out the [Discussion](https://github.com/dbuezas/lovelace-plotly-graph-card/discussions) section (new!)
12+
Check out the [Discussion](https://github.com/dbuezas/lovelace-plotly-graph-card/discussions) section (new!)
1313
Eventually I hope we accumulate a bunch of nice looking plots and yaml examples there :)
1414

15-
1615
## Install through HACS
1716

1817
1. Go to `HACS` / `Frontend` / `click [⋮] on the top right` / `Custom Repositories`
@@ -108,15 +107,110 @@ For now only the only allowed chart types are:
108107
- Bar charts https://plotly.com/javascript/bar-charts/#basic-bar-chart
109108
- Line and scatter https://plotly.com/javascript/line-and-scatter/
110109
111-
## entities:
110+
## Entities:
112111
113112
- `entities` translates to the `data` argument in PlotlyJS
114113

115114
- each `entity` will be translated to a trace inside the data array.
116115
- `x` (states) and `y` (timestamps of stored states)
117-
- you can add any attribute that works in a trace
116+
- you can add any attribute that works in a plotly trace
117+
- see https://plotly.com/javascript/reference/scatter/#scatter-line for more
118+
119+
```yaml
120+
entities:
121+
- entity: sensor.temperature
122+
- entity: sensor.humidity
123+
```
124+
125+
## Extra entity attributes:
126+
127+
```yaml
128+
entities:
129+
- entity: sensor.temperature_in_celsius
130+
name: living temperature in Farenheit # Overrides the entity name
131+
lambda: |- # Transforms the data
132+
(ys) => ys.map(y => (y × 9/5) + 32)
133+
unit_of_measurement: °F # Overrides the unit
134+
```
135+
136+
### `lambda:` transforms
137+
138+
`lambda` takes a js function (as a string) to pre process the data before plotting it. Here you can do things like normalisation, integration. For example:
139+
140+
#### Normalisation wrt to last
118141

119-
- see https://plotly.com/javascript/reference/scatter/#scatter-line for more
142+
```yaml
143+
type: custom:plotly-graph
144+
entities:
145+
- entity: sensor.my_sensor
146+
lambda: |-
147+
(ys) => ys.map(y => y/ys[ys.length-1])
148+
```
149+
150+
#### Normalisation wrt to first fetched value
151+
152+
```yaml
153+
- entity: sensor.my_sensor
154+
lambda: |-
155+
(ys) => ys.map(y => y/ys[0])
156+
```
157+
158+
note: `ys[0]` represents the first "known" value, which is the value furthest to the past among the downloaded data. This value will change if you scroll, zoom out, change the hours_to_show, or just let time pass.
159+
160+
#### Accumulated value
161+
162+
```yaml
163+
- entity: sensor.my_sensor
164+
unit_of_measurement: "total pulses"
165+
lambda: |-
166+
(ys) => {
167+
let accumulator = 0;
168+
return ys.map(y => accumulator + y)
169+
}
170+
```
171+
172+
#### Derivative
173+
174+
```yaml
175+
- entity: sensor.my_sensor
176+
unit_of_measurement: "pulses / second"
177+
lambda: |-
178+
(ys, xs) => {
179+
let last = {
180+
x: new Date(),
181+
y: 0,
182+
}
183+
return ys.map((y, index) => {
184+
const x = xs[index];
185+
const dateDelta = x - last.x;
186+
accumulator += (y - last.y) / dateDelta;
187+
last = { x, y };
188+
return accumulator;
189+
})
190+
}
191+
```
192+
193+
#### Right hand riemann integration
194+
195+
```yaml
196+
- entity: sensor.my_sensor
197+
unit_of_measurement: "kWh"
198+
lambda: |-
199+
(ys, xs) => {
200+
let accumulator = 0;
201+
let last = {
202+
x: new Date(),
203+
y: 0,
204+
}
205+
return ys.map((y, index) => {
206+
const x = xs[index]
207+
const dateDelta = x - last.x;
208+
accumulator += last.y * dateDelta;
209+
last = { x, y };
210+
return accumulator;
211+
})
212+
}
213+
```
120214

121215
## layout:
122216

src/plotly-graph-card.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ export class PlotlyGraph extends HTMLElement {
183183
config.entities = config.entities.map((entity) =>
184184
typeof entity === "string" ? { entity } : entity
185185
);
186+
config.entities = config.entities.map((entity) => ({
187+
...entity,
188+
lambda: entity.lambda ? window.eval(entity.lambda) : (y: number[]) => y,
189+
}));
186190
if (config.title) {
187191
config = {
188192
...config,
@@ -218,9 +222,22 @@ export class PlotlyGraph extends HTMLElement {
218222
);
219223
while (!this.hass) await sleep(100);
220224
await this.cache.update(range, !this.isBrowsing, entityNames, this.hass);
221-
222225
await this.plot();
223226
};
227+
getAllUnitsOfMeasurement() {
228+
const all = this.config.entities.map(({ entity }) =>
229+
this.getUnitOfMeasurement(entity)
230+
);
231+
return Array.from(new Set(all));
232+
}
233+
getUnitOfMeasurement(entityName: string) {
234+
return (
235+
this.config.entities.find((trace) => trace.entity === entityName)
236+
?.unit_of_measurement ||
237+
this.cache.attributes[entityName]?.unit_of_measurement ||
238+
""
239+
);
240+
}
224241
getThemedLayout() {
225242
const styles = window.getComputedStyle(this.contentEl);
226243
let haTheme = {
@@ -238,17 +255,19 @@ export class PlotlyGraph extends HTMLElement {
238255
const entities = this.config.entities;
239256
const { histories, attributes } = this.cache;
240257

241-
const units = Array.from(
242-
new Set(Object.values(attributes).map((a) => a.unit_of_measurement))
243-
);
258+
const units = this.getAllUnitsOfMeasurement();
244259

245260
return entities.map((trace) => {
246261
const entity_id = trace.entity;
247262
const history = histories[entity_id] || {};
248263
const attribute = attributes[entity_id] || {};
249-
const unit = attribute.unit_of_measurement;
264+
const unit = this.getUnitOfMeasurement(entity_id);
250265
const yaxis_idx = units.indexOf(unit);
251266
const name = trace.name || attribute.friendly_name || entity_id;
267+
const xs = history.map(({ last_changed }) => new Date(last_changed));
268+
const ys = history.map(({ state }) =>
269+
state === "unavailable" ? undefined : state
270+
);
252271
return merge(
253272
{
254273
entity_id,
@@ -259,10 +278,8 @@ export class PlotlyGraph extends HTMLElement {
259278
width: 1,
260279
shape: "hv",
261280
},
262-
x: history.map(({ last_changed }) => new Date(last_changed)),
263-
y: history.map(({ state }) =>
264-
state === "unavailable" ? undefined : state
265-
),
281+
x: xs,
282+
y: trace.lambda(ys, xs),
266283
yaxis: "y" + (yaxis_idx == 0 ? "" : yaxis_idx + 1),
267284
},
268285
trace
@@ -272,9 +289,7 @@ export class PlotlyGraph extends HTMLElement {
272289

273290
getLayout(): Plotly.Layout {
274291
const { attributes } = this.cache;
275-
const units = Array.from(
276-
new Set(Object.values(attributes).map((a) => a.unit_of_measurement))
277-
);
292+
const units = this.getAllUnitsOfMeasurement();
278293

279294
const yAxisTitles = Object.fromEntries(
280295
units.map((unit, i) => ["yaxis" + (i == 0 ? "" : i + 1), { title: unit }])

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export type Config = {
44
refresh_interval?: number; // in seconds
55
entities: (Partial<Plotly.PlotData> & {
66
entity: string;
7+
unit_of_measurement?: string;
8+
lambda: (y: any[], x: Date[]) => number[];
79
})[];
810
layout?: Partial<Plotly.Layout>;
911
config?: Partial<Plotly.Config>;

0 commit comments

Comments
 (0)