ha-frontend-cdce8p/src/components/state-history-chart-line.js
c727 0c6f8c34fb
P3: Replace hassUtil with imports (#1181)
* P3: Remove get is()

* P3: Replace hassUtil with imports

* Remove hass-util imports

* Fix errors
2018-05-16 14:03:04 +02:00

300 lines
8.3 KiB
JavaScript

import '@polymer/polymer/lib/utils/debounce.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './entity/ha-chart-base.js';
import formatDateTime from '../../js/common/datetime/format_date_time.js';
class StateHistoryChartLine extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
overflow: hidden;
height: 0;
transition: height 0.3s ease-in-out;
}
</style>
<ha-chart-base id="chart" data="[[chartData]]" identifier="[[identifier]]" rendered="{{rendered}}"></ha-chart-base>
`;
}
static get properties() {
return {
chartData: Object,
data: Object,
unit: String,
identifier: String,
isSingleDevice: {
type: Boolean,
value: false,
},
endTime: Object,
rendered: {
type: Boolean,
value: false,
observer: '_onRenderedChanged'
}
};
}
static get observers() {
return ['dataChanged(data, endTime, isSingleDevice)'];
}
connectedCallback() {
super.connectedCallback();
this._isAttached = true;
this.drawChart();
}
dataChanged() {
this.drawChart();
}
_onRenderedChanged(rendered) {
if (rendered) this.animateHeight();
}
animateHeight() {
requestAnimationFrame(() => requestAnimationFrame(() => {
this.style.height = this.$.chart.scrollHeight + 'px';
}));
}
drawChart() {
const unit = this.unit;
const deviceStates = this.data;
const datasets = [];
let endTime;
if (!this._isAttached) {
return;
}
if (deviceStates.length === 0) {
return;
}
function safeParseFloat(value) {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
}
endTime = this.endTime ||
new Date(Math.max.apply(null, deviceStates.map(states =>
new Date(states.states[states.states.length - 1].last_changed))));
if (endTime > new Date()) {
endTime = new Date();
}
deviceStates.forEach((states) => {
const domain = states.domain;
const name = states.name;
// array containing [value1, value2, etc]
let prevValues;
const data = [];
function pushData(timestamp, datavalues) {
if (!datavalues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
data.forEach((d, i) => {
d.data.push({ x: timestamp, y: datavalues[i] });
});
prevValues = datavalues;
}
function addColumn(nameY, step, fill) {
let dataFill = false;
let dataStep = false;
if (fill) {
dataFill = 'origin';
}
if (step) {
dataStep = 'before';
}
data.push({
label: nameY,
fill: dataFill,
steppedLine: dataStep,
pointRadius: 0,
data: [],
unitText: unit
});
}
if (domain === 'thermostat' || domain === 'climate') {
// We differentiate between thermostats that have a target temperature
// range versus ones that have just a target temperature
// Using step chart by step-before so manually interpolation not needed.
const hasTargetRange = states.states.some(state => state.attributes &&
state.attributes.target_temp_high !== state.attributes.target_temp_low);
const hasHeat = states.states.some(state => state.state === 'heat');
const hasCool = states.states.some(state => state.state === 'cool');
addColumn(name + ' current temperature', true);
if (hasHeat) {
addColumn(name + ' heating', true, true);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addColumn(name + ' cooling', true, true);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
addColumn(name + ' target temperature high', true);
addColumn(name + ' target temperature low', true);
} else {
addColumn(name + ' target temperature', true);
}
states.states.forEach((state) => {
if (!state.attributes) return;
const curTemp = safeParseFloat(state.attributes.current_temperature);
const series = [curTemp];
if (hasHeat) {
series.push(state.state === 'heat' ? curTemp : null);
}
if (hasCool) {
series.push(state.state === 'cool' ? curTemp : null);
}
if (hasTargetRange) {
const targetHigh = safeParseFloat(state.attributes.target_temp_high);
const targetLow = safeParseFloat(state.attributes.target_temp_low);
series.push(targetHigh, targetLow);
pushData(
new Date(state.last_changed),
series
);
} else {
const target = safeParseFloat(state.attributes.temperature);
series.push(target);
pushData(
new Date(state.last_changed),
series
);
}
});
} else {
// Only disable interpolation for sensors
const isStep = domain === 'sensor';
addColumn(name, isStep);
let lastValue = null;
let lastDate = null;
let lastNullDate = null;
// Process chart data.
// When state is `unknown`, calculate the value and break the line.
states.states.forEach((state) => {
const value = safeParseFloat(state.state);
const date = new Date(state.last_changed);
if (value !== null && lastNullDate !== null) {
const dateTime = date.getTime();
const lastNullDateTime = lastNullDate.getTime();
const lastDateTime = lastDate.getTime();
const tmpValue = ((value - lastValue) *
((lastNullDateTime - lastDateTime) / (dateTime - lastDateTime))) + lastValue;
pushData(lastNullDate, [tmpValue]);
pushData(new Date(lastNullDateTime + 1), [null]);
pushData(date, [value]);
lastDate = date;
lastValue = value;
lastNullDate = null;
} else if (value !== null && lastNullDate === null) {
pushData(date, [value]);
lastDate = date;
lastValue = value;
} else if (value === null && lastNullDate === null && lastValue !== null) {
lastNullDate = date;
}
});
}
// Add an entry for final values
pushData(endTime, prevValues, false);
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});
const formatTooltipTitle = function (items, data) {
const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTime(date);
};
const chartOptions = {
type: 'line',
unit: unit,
legend: !this.isSingleDevice,
options: {
scales: {
xAxes: [{
type: 'time',
ticks: {
major: {
fontStyle: 'bold',
},
},
}],
yAxes: [{
ticks: {
maxTicksLimit: 7,
},
}],
},
tooltips: {
mode: 'neareach',
callbacks: {
title: formatTooltipTitle
},
},
hover: {
mode: 'neareach',
},
layout: {
padding: {
top: 5
},
},
elements: {
line: {
tension: 0.1,
pointRadius: 0,
borderWidth: 1.5,
},
point: {
hitRadius: 5,
}
},
plugins: {
filler: {
propagate: true,
}
},
},
data: {
labels: [],
datasets: datasets
}
};
this.chartData = chartOptions;
}
}
customElements.define('state-history-chart-line', StateHistoryChartLine);