Virtualize history panel (#12824)
This commit is contained in:
parent
afd41e79f0
commit
ceda911670
|
@ -37,6 +37,26 @@ export default class HaChartBase extends LitElement {
|
|||
|
||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||
|
||||
private _releaseCanvas() {
|
||||
// release the canvas memory to prevent
|
||||
// safari from running out of memory.
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._releaseCanvas();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setupChart();
|
||||
this.data.datasets.forEach((dataset, index) => {
|
||||
|
|
|
@ -28,11 +28,11 @@ class StateHistoryChartLine extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
|
||||
@state() private _chartOptions?: ChartOptions<"line">;
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
|
@ -57,6 +57,7 @@ class StateHistoryChartLine extends LitElement {
|
|||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
|
@ -130,28 +131,11 @@ class StateHistoryChartLine extends LitElement {
|
|||
const computedStyles = getComputedStyle(this);
|
||||
const entityStates = this.data;
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
if (entityStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
endTime =
|
||||
this.endTime ||
|
||||
// Get the highest date from the last date of each device
|
||||
new Date(
|
||||
Math.max(
|
||||
...entityStates.map((devSts) =>
|
||||
new Date(
|
||||
devSts.states[devSts.states.length - 1].last_changed
|
||||
).getTime()
|
||||
)
|
||||
)
|
||||
);
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
const endTime = this.endTime;
|
||||
const names = this.names || {};
|
||||
entityStates.forEach((states) => {
|
||||
const domain = states.domain;
|
||||
|
|
|
@ -83,6 +83,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public names: boolean | Record<string, string> = false;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
@ -91,7 +93,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
@property({ type: Boolean }) public dataHasMultipleRows = false;
|
||||
|
||||
@property({ attribute: false }) public startTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@state() private _chartData?: ChartData<"timeline">;
|
||||
|
||||
|
@ -110,6 +116,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
const narrow = this.narrow;
|
||||
const multipleRows = this.data.length !== 1 || this.dataHasMultipleRows;
|
||||
this._chartOptions = {
|
||||
maintainAspectRatio: false,
|
||||
parsing: false,
|
||||
|
@ -123,6 +131,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
locale: this.hass.locale,
|
||||
},
|
||||
},
|
||||
suggestedMin: this.startTime,
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxRotation: 0,
|
||||
|
@ -153,11 +163,17 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display: this.data.length !== 1,
|
||||
display: multipleRows,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
},
|
||||
afterFit: function (scaleInstance) {
|
||||
if (multipleRows) {
|
||||
// ensure all the chart labels are the same width
|
||||
scaleInstance.width = narrow ? 105 : 185;
|
||||
}
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
},
|
||||
},
|
||||
|
@ -208,34 +224,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||
stateHistory = [];
|
||||
}
|
||||
|
||||
const startTime = new Date(
|
||||
stateHistory.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
// end time is Math.max(startTime, last_event)
|
||||
let endTime =
|
||||
this.endTime ||
|
||||
new Date(
|
||||
stateHistory.reduce(
|
||||
(maxTime, stateInfo) =>
|
||||
Math.max(
|
||||
maxTime,
|
||||
new Date(
|
||||
stateInfo.data[stateInfo.data.length - 1].last_changed
|
||||
).getTime()
|
||||
),
|
||||
startTime.getTime()
|
||||
)
|
||||
);
|
||||
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const labels: string[] = [];
|
||||
const datasets: ChartDataset<"timeline">[] = [];
|
||||
const names = this.names || {};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@lit-labs/virtualizer";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
|
@ -6,12 +7,29 @@ import {
|
|||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state, eventOptions } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { HistoryResult } from "../../data/history";
|
||||
import {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
} from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||
|
||||
const chunkData = (inputArray: any[], chunks: number) =>
|
||||
inputArray.reduce((results, item, idx) => {
|
||||
const chunkIdx = Math.floor(idx / chunks);
|
||||
if (!results[chunkIdx]) {
|
||||
results[chunkIdx] = [];
|
||||
}
|
||||
results[chunkIdx].push(item);
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
@customElement("state-history-charts")
|
||||
class StateHistoryCharts extends LitElement {
|
||||
|
@ -19,8 +37,13 @@ class StateHistoryCharts extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public historyData!: HistoryResult;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public names = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "virtualize", reflect: true })
|
||||
public virtualize = false;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
@ -29,6 +52,14 @@ class StateHistoryCharts extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@state() private _computedStartTime!: Date;
|
||||
|
||||
@state() private _computedEndTime!: Date;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html` <div class="info">
|
||||
|
@ -48,40 +79,76 @@ class StateHistoryCharts extends LitElement {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
const computedEndTime = this.upToNow
|
||||
? new Date()
|
||||
: this.endTime || new Date();
|
||||
const now = new Date();
|
||||
|
||||
return html`
|
||||
${this.historyData.timeline.length
|
||||
? html`
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${this.historyData.timeline}
|
||||
.endTime=${computedEndTime}
|
||||
.noSingle=${this.noSingle}
|
||||
.names=${this.names}
|
||||
></state-history-chart-timeline>
|
||||
`
|
||||
: html``}
|
||||
${this.historyData.line.map(
|
||||
(line) => html`
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${line.unit}
|
||||
.data=${line.data}
|
||||
.identifier=${line.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
line.data &&
|
||||
line.data.length === 1}
|
||||
.endTime=${computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
|
||||
const combinedItems = chunkData(
|
||||
this.historyData.timeline,
|
||||
CANVAS_TIMELINE_ROWS_CHUNK
|
||||
).concat(this.historyData.line);
|
||||
|
||||
return this.virtualize
|
||||
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${combinedItems}
|
||||
.renderItem=${this._renderHistoryItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderHistoryItem = (
|
||||
item: TimelineEntity[] | LineChartUnit,
|
||||
index: number
|
||||
): TemplateResult => {
|
||||
if (!item || index === undefined) {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.line &&
|
||||
this.historyData.line.length === 1}
|
||||
.endTime=${this._computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.noSingle=${this.noSingle}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.dataHasMultipleRows=${this.historyData.timeline.length &&
|
||||
this.historyData.timeline.length > 1}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return !(changedProps.size === 1 && changedProps.has("hass"));
|
||||
}
|
||||
|
@ -96,6 +163,11 @@ class StateHistoryCharts extends LitElement {
|
|||
return !this.isLoadingData && historyDataEmpty;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
|
@ -103,11 +175,47 @@ class StateHistoryCharts extends LitElement {
|
|||
/* height of single timeline chart = 60px */
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.container {
|
||||
max-height: var(--history-max-height);
|
||||
}
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:host([virtualize]) .entry-container {
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.container,
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
state-history-chart-timeline,
|
||||
state-history-chart-line {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,44 +80,44 @@ class HaPanelHistory extends LitElement {
|
|||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class="flex content">
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
.hass=${this.hass}
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
.ranges=${this._ranges}
|
||||
@change=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
<div class="filters">
|
||||
<ha-date-range-picker
|
||||
.hass=${this.hass}
|
||||
?disabled=${this._isLoading}
|
||||
.startDate=${this._startDate}
|
||||
.endDate=${this._endDate}
|
||||
.ranges=${this._ranges}
|
||||
@change=${this._dateRangeChanged}
|
||||
></ha-date-range-picker>
|
||||
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.entity"
|
||||
)}
|
||||
.disabled=${this._isLoading}
|
||||
@change=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`
|
||||
<state-history-charts
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.entity"
|
||||
)}
|
||||
.disabled=${this._isLoading}
|
||||
@change=${this._entityPicked}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
${this._isLoading
|
||||
? html`<div class="progress-wrapper">
|
||||
<ha-circular-progress
|
||||
active
|
||||
alt=${this.hass.localize("ui.common.loading")}
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`
|
||||
<state-history-charts
|
||||
virtualize
|
||||
.hass=${this.hass}
|
||||
.historyData=${this._stateHistory}
|
||||
.endTime=${this._endDate}
|
||||
.narrow=${this.narrow}
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
@ -235,6 +235,14 @@ class HaPanelHistory extends LitElement {
|
|||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
state-history-charts {
|
||||
height: calc(100vh - 136px);
|
||||
}
|
||||
|
||||
:host([narrow]) state-history-charts {
|
||||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
height: calc(100vh - 136px);
|
||||
}
|
||||
|
@ -243,6 +251,10 @@ class HaPanelHistory extends LitElement {
|
|||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
:host([virtualize]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue