Skip to content

Commit b40e641

Browse files
zanna-37dbuezas
andauthored
Various fixes (#107)
* Explicit x-axis autorange handling * Fix NaN dates corner case * Update libs and types * Partial types fix now that HTML is created in the constructor * Minor code style in themes * Fix: Types * Fix: guard possibly undefined variable * Fix ghost traces: Extend patchLonelyDatapoints to cover undefined too Co-authored-by: David Buezas <[email protected]>
1 parent 2550b71 commit b40e641

File tree

9 files changed

+3564
-3872
lines changed

9 files changed

+3564
-3872
lines changed

jest.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
22
module.exports = {
3-
preset: 'ts-jest',
4-
testEnvironment: 'node',
5-
};
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
maxWorkers: 1, // this makes local testing faster
6+
};

package-lock.json

Lines changed: 3488 additions & 3816 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,17 @@
1414
"license": "ISC",
1515
"devDependencies": {
1616
"@types/jest": "^27.0.2",
17+
"@types/lodash": "^4.14.175",
1718
"@types/node": "^16.10.3",
18-
"@types/plotly.js": "^1.54.16",
19+
"@types/plotly.js": "^2.12.8",
1920
"esbuild": "^0.13.4",
20-
"pretier": "0.0.1",
21-
"ts-jest": "^27.0.6"
21+
"ts-jest": "^29.0.3"
2222
},
2323
"dependencies": {
24-
"@types/lodash": "^4.14.175",
2524
"custom-card-helpers": "^1.8.0",
2625
"date-fns": "^2.28.0",
2726
"deepmerge": "^4.2.2",
2827
"lodash": "^4.17.21",
29-
"plotly.js": "^2.8.3"
28+
"plotly.js": "^2.16.1"
3029
}
3130
}

src/cache/Cache.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
TimestampRange,
77
History,
88
isEntityIdAttrConfig,
9-
EntityIdConfig,
9+
EntityConfig,
1010
isEntityIdStateConfig,
1111
isEntityIdStatisticsConfig,
1212
HistoryInRange,
@@ -20,7 +20,7 @@ export function mapValues<T, S>(
2020
}
2121
async function fetchSingleRange(
2222
hass: HomeAssistant,
23-
entity: EntityIdConfig,
23+
entity: EntityConfig,
2424
[startT, endT]: number[],
2525
significant_changes_only: boolean,
2626
minimal_response: boolean
@@ -63,7 +63,7 @@ async function fetchSingleRange(
6363
};
6464
}
6565

66-
export function getEntityKey(entity: EntityIdConfig) {
66+
export function getEntityKey(entity: EntityConfig) {
6767
if (isEntityIdAttrConfig(entity)) {
6868
return `${entity.entity}::${entity.attribute}`;
6969
} else if (isEntityIdStatisticsConfig(entity)) {
@@ -81,14 +81,14 @@ export default class Cache {
8181
this.ranges = {};
8282
this.histories = {};
8383
}
84-
getHistory(entity: EntityIdConfig) {
84+
getHistory(entity: EntityConfig) {
8585
let key = getEntityKey(entity);
8686
return this.histories[key] || [];
8787
}
8888
async update(
8989
range: TimestampRange,
9090
removeOutsideRange: boolean,
91-
entities: EntityIdConfig[],
91+
entities: EntityConfig[],
9292
hass: HomeAssistant,
9393
significant_changes_only: boolean,
9494
minimal_response: boolean

src/cache/fetch-states.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ async function fetchStates(
4040
)}`
4141
);
4242
}
43-
if (!list) list = []; //throw new Error(`Error fetching ${entity.entity}`); // shutup typescript
43+
if (!list) list = [];
4444
return {
4545
range: [+start, +end],
4646
history: list
4747
.map((entry) => ({
4848
...entry,
4949
state: isEntityIdAttrConfig(entity)
50-
? entry.attributes[entity.attribute]
50+
? entry.attributes[entity.attribute] || null
5151
: entry.state,
5252
last_updated: +new Date(entry.last_updated || entry.last_changed),
5353
}))

src/plotly-graph-card.ts

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,19 @@ import {
2424
STATISTIC_PERIODS,
2525
STATISTIC_TYPES,
2626
StatisticPeriod,
27-
isAutoPeriodConfig as getIsAutoPeriodConfig,
27+
getIsAutoPeriodConfig,
2828
} from "./recorder-types";
2929
import { parseTimeDuration } from "./duration/duration";
3030

3131
const componentName = isProduction ? "plotly-graph" : "plotly-graph-dev";
3232

33+
const isDefined = (y: any) => y !== null && y !== undefined;
3334
function patchLonelyDatapoints(xs: Datum[], ys: Datum[]) {
3435
/* Ghost traces when data has single non-unavailable states sandwiched between unavailable ones
3536
see: https://github.com/dbuezas/lovelace-plotly-graph-card/issues/103
3637
*/
3738
for (let i = 1; i < xs.length - 1; i++) {
38-
if (ys[i - 1] === null && ys[i] !== null && ys[i + 1] === null) {
39+
if (!isDefined(ys[i - 1]) && isDefined(ys[i]) && !isDefined(ys[i + 1])) {
3940
ys.splice(i, 0, ys[i]);
4041
xs.splice(i, 0, xs[i]);
4142
}
@@ -50,14 +51,14 @@ console.info(
5051

5152
const padding = 1;
5253
export class PlotlyGraph extends HTMLElement {
53-
contentEl!: Plotly.PlotlyHTMLElement & {
54+
contentEl: Plotly.PlotlyHTMLElement & {
5455
data: (Plotly.PlotData & { entity: string })[];
5556
layout: Plotly.Layout;
5657
};
57-
msgEl!: HTMLElement;
58-
cardEl!: HTMLElement;
59-
buttonEl!: HTMLButtonElement;
60-
titleEl!: HTMLElement;
58+
msgEl: HTMLElement;
59+
cardEl: HTMLElement;
60+
resetButtonEl: HTMLButtonElement;
61+
titleEl: HTMLElement;
6162
config!: InputConfig;
6263
parsed_config!: Config;
6364
cache = new Cache();
@@ -78,10 +79,10 @@ export class PlotlyGraph extends HTMLElement {
7879
this.handles.restyleListener!.off("plotly_restyle", this.onRestyle);
7980
clearTimeout(this.handles.refreshTimeout!);
8081
}
81-
connectedCallback() {
82-
if (!this.contentEl) {
83-
const shadow = this.attachShadow({ mode: "open" });
84-
shadow.innerHTML = `
82+
constructor() {
83+
super();
84+
const shadow = this.attachShadow({ mode: "open" });
85+
shadow.innerHTML = `
8586
<ha-card>
8687
<style>
8788
ha-card{
@@ -124,20 +125,21 @@ export class PlotlyGraph extends HTMLElement {
124125
<span id="msg"> </span>
125126
<button id="reset" class="hidden">↻</button>
126127
</ha-card>`;
127-
this.msgEl = shadow.querySelector("#msg")!;
128-
this.cardEl = shadow.querySelector("ha-card")!;
129-
this.contentEl = shadow.querySelector("div#plotly")!;
130-
this.buttonEl = shadow.querySelector("button#reset")!;
131-
this.titleEl = shadow.querySelector("ha-card > #title")!;
132-
this.buttonEl.addEventListener("click", this.exitBrowsingMode);
133-
insertStyleHack(shadow.querySelector("style")!);
134-
this.contentEl.style.visibility = "hidden";
135-
this.withoutRelayout(() => Plotly.newPlot(this.contentEl, [], {}));
136-
}
128+
this.msgEl = shadow.querySelector("#msg")!;
129+
this.cardEl = shadow.querySelector("ha-card")!;
130+
this.contentEl = shadow.querySelector("div#plotly")!;
131+
this.resetButtonEl = shadow.querySelector("button#reset")!;
132+
this.titleEl = shadow.querySelector("ha-card > #title")!;
133+
this.resetButtonEl.addEventListener("click", this.exitBrowsingMode);
134+
insertStyleHack(shadow.querySelector("style")!);
135+
this.contentEl.style.visibility = "hidden";
136+
this.withoutRelayout(() => Plotly.newPlot(this.contentEl, [], {}));
137+
}
138+
connectedCallback() {
137139
this.setupListeners();
138-
this.fetch(this.getAutoFetchRange())
139-
.then(() => this.fetch(this.getAutoFetchRange())) // again so home assistant extends until end of time axis
140-
.then(() => (this.contentEl.style.visibility = ""));
140+
this.fetch(this.getAutoFetchRange()).then(
141+
() => (this.contentEl.style.visibility = "")
142+
);
141143
}
142144
async withoutRelayout(fn: Function) {
143145
this.isInternalRelayout++;
@@ -184,21 +186,23 @@ export class PlotlyGraph extends HTMLElement {
184186
return [+new Date() - ms, +new Date()] as [number, number];
185187
}
186188
getVisibleRange() {
187-
return this.contentEl.layout.xaxis!.range!.map((date) => +parseISO(date));
189+
return this.contentEl.layout.xaxis!.range!.map((date) =>
190+
// if autoscale is used after scrolling, plotly returns the dates as numbers instead of strings
191+
Number.isFinite(date) ? date : +parseISO(date)
192+
);
188193
}
189194
async enterBrowsingMode() {
190195
this.isBrowsing = true;
191-
this.buttonEl.classList.remove("hidden");
196+
this.resetButtonEl.classList.remove("hidden");
192197
}
193198
exitBrowsingMode = async () => {
194199
this.isBrowsing = false;
195-
this.buttonEl.classList.add("hidden");
200+
this.resetButtonEl.classList.add("hidden");
196201
this.withoutRelayout(async () => {
197202
await Plotly.relayout(this.contentEl, {
198-
uirevision: Math.random(),
199-
xaxis: { range: this.getAutoFetchRange() },
203+
uirevision: Math.random(), // to trigger the autoranges in all y-yaxes
204+
xaxis: { range: this.getAutoFetchRange() }, // to reset xaxis to hours_to_show quickly, before refetching
200205
});
201-
await Plotly.restyle(this.contentEl, { visible: true });
202206
});
203207
await this.fetch(this.getAutoFetchRange());
204208
};
@@ -218,6 +222,18 @@ export class PlotlyGraph extends HTMLElement {
218222
// The user supplied configuration. Throw an exception and Lovelace will
219223
// render an error card.
220224
async setConfig(config: InputConfig) {
225+
try {
226+
this.msgEl.innerText = "";
227+
return await this._setConfig(config);
228+
} catch (e: any) {
229+
console.error(e);
230+
this.msgEl.innerText = JSON.stringify(e.message || "").replace(
231+
/\\"/g,
232+
'"'
233+
);
234+
}
235+
}
236+
async _setConfig(config: InputConfig) {
221237
config = JSON.parse(JSON.stringify(config));
222238
this.config = config;
223239
const schemeName = config.color_scheme ?? "category10";
@@ -328,7 +344,7 @@ export class PlotlyGraph extends HTMLElement {
328344
this.parsed_config = newConfig;
329345
const is = this.parsed_config;
330346
if (!this.contentEl) return;
331-
if (is.hours_to_show !== was.hours_to_show) {
347+
if (is.hours_to_show !== was?.hours_to_show) {
332348
this.exitBrowsingMode();
333349
}
334350
await this.fetch(this.getAutoFetchRange());
@@ -460,7 +476,6 @@ export class PlotlyGraph extends HTMLElement {
460476
if (mergedTrace.show_value) {
461477
mergedTrace.legendgroup ??= "group" + traceIdx;
462478
show_value_traces.push({
463-
// @ts-expect-error (texttemplate missing in plotly typings)
464479
texttemplate: `%{y:.2~f}%{customdata.unit_of_measurement}`, // here so it can be overwritten
465480
...mergedTrace,
466481
mode: "text+markers",
@@ -507,9 +522,15 @@ export class PlotlyGraph extends HTMLElement {
507522
const yAxisTitles = Object.fromEntries(
508523
units.map((unit, i) => ["yaxis" + (i == 0 ? "" : i + 1), { title: unit }])
509524
);
510-
511525
const layout = merge(
512526
{ uirevision: true },
527+
{
528+
xaxis: {
529+
range: this.isBrowsing
530+
? this.getVisibleRange()
531+
: this.getAutoFetchRange(),
532+
},
533+
},
513534
this.parsed_config.no_default_layout ? {} : yAxisTitles,
514535
this.getThemedLayout(),
515536
this.size,
@@ -579,9 +600,9 @@ export class PlotlyGraph extends HTMLElement {
579600
return historyGraphCard.constructor.getConfigElement();
580601
}
581602
}
582-
//@ts-ignore
603+
//@ts-expect-error
583604
window.customCards = window.customCards || [];
584-
//@ts-ignore
605+
//@ts-expect-error
585606
window.customCards.push({
586607
type: componentName,
587608
name: "Plotly Graph Card",

src/plotly.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
window.global = window;
55
var Plotly = require("plotly.js/lib/core") as typeof import("plotly.js");
6-
//@ts-ignore
76
Plotly.register([
87
// traces
98
require("plotly.js/lib/bar"),

src/recorder-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const STATISTIC_PERIODS = [
3030
export type StatisticPeriod = typeof STATISTIC_PERIODS[number];
3131
export type AutoPeriodConfig = Record<TimeDurationStr, StatisticPeriod>;
3232

33-
export function isAutoPeriodConfig(val: any): val is AutoPeriodConfig {
33+
export function getIsAutoPeriodConfig(val: any): val is AutoPeriodConfig {
3434
const isObject =
3535
typeof val === "object" && val !== null && !Array.isArray(val);
3636
if (!isObject) return false;

src/themed-layout.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ const defaultLayout: Partial<Plotly.Layout> = {
1313
height: 285,
1414
dragmode: "pan",
1515
xaxis: {
16+
autorange: false,
1617
// automargin: true, // it makes zooming very jumpy
1718
},
1819
yaxis: {
1920
// automargin: true, // it makes zooming very jumpy
2021
},
2122
yaxis2: {
2223
// automargin: true, // it makes zooming very jumpy
23-
side: "right",
24-
showgrid: false,
25-
overlaying: "y",
24+
...defaultExtraYAxes,
25+
visible: true,
2626
},
2727
yaxis3: { ...defaultExtraYAxes },
2828
yaxis4: { ...defaultExtraYAxes },
@@ -31,7 +31,7 @@ const defaultLayout: Partial<Plotly.Layout> = {
3131
yaxis7: { ...defaultExtraYAxes },
3232
yaxis8: { ...defaultExtraYAxes },
3333
yaxis9: { ...defaultExtraYAxes },
34-
// @ts-ignore (the types are missing yaxes > 9)
34+
// @ts-expect-error (the types are missing yaxes > 9)
3535
yaxis10: { ...defaultExtraYAxes },
3636
yaxis11: { ...defaultExtraYAxes },
3737
yaxis12: { ...defaultExtraYAxes },

0 commit comments

Comments
 (0)