mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-14 12:56:37 +00:00
Replace Google Charts with Chart.js (#429)
* chartjs test * [WIP] Modified for Chart.js * Tweaking styles ( tooltips and lines ) * Almost done TODO: Change tooltips to HTML tag Improve color function * More work on Tooltips * Improve update logic Fix linting * resolve conflict * [WIP] Create new tooltip mode hack. Add axis padding to top and botton to prevent axis cutoff * TODO: cleanup * FIXME: tooltip in history graph not working correctly reorganize some code * fix build problem * Fix color and tooltip issue Fix label max width for timeline chart * update dep * Fix strange color after build due to `uglify` bug with reference the minified version. Make line chart behavior more similar to Google Charts. Make the chart honor to `unknown` and other state by manually calculate point value. * fix bugs * Remove label for only one data in timeline chart. Fix bug for infinite loop in some cases * Add HTML legend to chart. * Fix isSingleDevice bug due to calculation. Add isSingleDevice property support. * fix for lint * Replace innerHTML code with polymer node. * Replace tooltip with HTML code * fix tooltip style * move default tooltip mode to plugin * LINTING * fix Move localize history data to Timeline Chart. Fix timeline static color. Rework on chart resize. * Bug fix: Chart may disappear on some case. Timeline chart calculation issue. Change timeline chart hidden logic. * fix tooltip rework for resize event * lint * element * Replace `var` to `let`. Move import and ChartJs injection code to `ha-chart-scripts.html`. * lint: convert more let to const * fix font fix undef * update bower.json * move * Load chart code on demand
This commit is contained in:
parent
500edbad0d
commit
c6030e6edc
@ -16,6 +16,7 @@
|
|||||||
"__DEMO__": false,
|
"__DEMO__": false,
|
||||||
"__BUILD__": false,
|
"__BUILD__": false,
|
||||||
"__VERSION__": false,
|
"__VERSION__": false,
|
||||||
|
"__ROOT__": false,
|
||||||
"Polymer": true,
|
"Polymer": true,
|
||||||
"webkitSpeechRecognition": false,
|
"webkitSpeechRecognition": false,
|
||||||
"ResizeObserver": false
|
"ResizeObserver": false
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"browser": true
|
"browser": true
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-unresolved": 2
|
"import/no-unresolved": 2,
|
||||||
|
"linebreak-style": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,10 @@
|
|||||||
"vaadin-combo-box": "vaadin/vaadin-combo-box#^3.0.2",
|
"vaadin-combo-box": "vaadin/vaadin-combo-box#^3.0.2",
|
||||||
"vaadin-date-picker": "vaadin/vaadin-date-picker#^2.0.0",
|
"vaadin-date-picker": "vaadin/vaadin-date-picker#^2.0.0",
|
||||||
"web-animations-js": "^2.2.5",
|
"web-animations-js": "^2.2.5",
|
||||||
"webcomponentsjs": "^1.0.10"
|
"webcomponentsjs": "^1.0.10",
|
||||||
|
"chart.js": "~2.7.1",
|
||||||
|
"moment": "^2.20.0",
|
||||||
|
"chartjs-chart-timeline": "fanthos/chartjs-chart-timeline#^0.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"web-component-tester": "^6.3.0"
|
"web-component-tester": "^6.3.0"
|
||||||
|
@ -36,6 +36,7 @@ function build(es6) {
|
|||||||
stripImportsStrategy([
|
stripImportsStrategy([
|
||||||
'bower_components/font-roboto/roboto.html',
|
'bower_components/font-roboto/roboto.html',
|
||||||
'bower_components/paper-styles/color.html',
|
'bower_components/paper-styles/color.html',
|
||||||
|
'src/resources/ha-chart-scripts.html',
|
||||||
]),
|
]),
|
||||||
stripAllButEntrypointStrategy('panels/hassio/ha-panel-hassio.html')
|
stripAllButEntrypointStrategy('panels/hassio/ha-panel-hassio.html')
|
||||||
]);
|
]);
|
||||||
|
@ -58,6 +58,7 @@ function getRollupInputOptions(es6) {
|
|||||||
__DEMO__: JSON.stringify(DEMO),
|
__DEMO__: JSON.stringify(DEMO),
|
||||||
__BUILD__: JSON.stringify(es6 ? 'latest' : 'es5'),
|
__BUILD__: JSON.stringify(es6 ? 'latest' : 'es5'),
|
||||||
__VERSION__: JSON.stringify(VERSION),
|
__VERSION__: JSON.stringify(VERSION),
|
||||||
|
__ROOT__: JSON.stringify(es6 ? 'frontend_latest' : 'frontend_es5'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -5,6 +5,7 @@ window.HASS_DEMO = __DEMO__;
|
|||||||
window.HASS_DEV = __DEV__;
|
window.HASS_DEV = __DEV__;
|
||||||
window.HASS_BUILD = __BUILD__;
|
window.HASS_BUILD = __BUILD__;
|
||||||
window.HASS_VERSION = __VERSION__;
|
window.HASS_VERSION = __VERSION__;
|
||||||
|
window.HASS_ROOT = __ROOT__;
|
||||||
|
|
||||||
const init = window.createHassConnection = function (password) {
|
const init = window.createHassConnection = function (password) {
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
@ -82,9 +82,11 @@
|
|||||||
</paper-dropdown-menu>
|
</paper-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
<state-history-charts
|
<state-history-charts
|
||||||
|
hass='[[hass]]'
|
||||||
history-data="[[stateHistoryOutput]]"
|
history-data="[[stateHistoryOutput]]"
|
||||||
is-loading-data="[[isLoadingData]]"
|
is-loading-data="[[isLoadingData]]"
|
||||||
end-time="[[endTime]]">
|
end-time="[[endTime]]"
|
||||||
|
no-single>
|
||||||
</state-history-charts>
|
</state-history-charts>
|
||||||
</div>
|
</div>
|
||||||
</app-header-layout>
|
</app-header-layout>
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"panels/logbook/ha-panel-logbook.html",
|
"panels/logbook/ha-panel-logbook.html",
|
||||||
"panels/map/ha-panel-map.html",
|
"panels/map/ha-panel-map.html",
|
||||||
"panels/shopping-list/ha-panel-shopping-list.html",
|
"panels/shopping-list/ha-panel-shopping-list.html",
|
||||||
"panels/mailbox/ha-panel-mailbox.html"
|
"panels/mailbox/ha-panel-mailbox.html",
|
||||||
|
"src/resources/ha-chart-scripts.html"
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
@ -27,6 +27,10 @@ cp build/panels/*.html $OUTPUT_DIR/panels
|
|||||||
mkdir $OUTPUT_DIR_ES5/panels
|
mkdir $OUTPUT_DIR_ES5/panels
|
||||||
cp build-es5/panels/*.html $OUTPUT_DIR_ES5/panels
|
cp build-es5/panels/*.html $OUTPUT_DIR_ES5/panels
|
||||||
|
|
||||||
|
# Chart code
|
||||||
|
cp build/src/resources/ha-chart-scripts.html $OUTPUT_DIR
|
||||||
|
cp build-es5/src/resources/ha-chart-scripts.html $OUTPUT_DIR_ES5
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
cp -r build-translations/output $OUTPUT_DIR/translations
|
cp -r build-translations/output $OUTPUT_DIR/translations
|
||||||
|
|
||||||
|
484
src/components/entity/ha-chart-base.html
Normal file
484
src/components/entity/ha-chart-base.html
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
<link rel='import' href='../../../bower_components/polymer/polymer-element.html'>
|
||||||
|
<link rel='import' href='../../resources/ha-chart-scripts.html'>
|
||||||
|
|
||||||
|
<dom-module id="ha-chart-base">
|
||||||
|
<template>
|
||||||
|
<style>
|
||||||
|
.chartHeader {
|
||||||
|
padding: 6px 0 0 0;
|
||||||
|
}
|
||||||
|
.chartHeader div {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.chartTitle {
|
||||||
|
margin: 0 12px 0 8px;
|
||||||
|
}
|
||||||
|
:root{
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
.chartTooltip {
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, .7);
|
||||||
|
color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.chartLegend ul,
|
||||||
|
.chartTooltip ul {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0px;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
.chartTooltip li {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.chartTooltip .title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.chartLegend li {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 5px;
|
||||||
|
max-width: 49%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.chartLegend li.hidden {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.chartLegend em,
|
||||||
|
.chartTooltip em {
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<template is="dom-if" if="[[unit]]">
|
||||||
|
<div class="chartHeader">
|
||||||
|
<div class="chartTitle">[[unit]]</div>
|
||||||
|
<div class="chartLegend">
|
||||||
|
<ul>
|
||||||
|
<template is="dom-repeat" items="[[metas]]">
|
||||||
|
<li data-lid$="[[itemsIndex]]" on-click="_legendClick" class$="[[item.hidden]]">
|
||||||
|
<em style$="background-color:[[item.bgColor]]"></em>
|
||||||
|
[[item.label]]
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div id="chartTarget" style="height:40px; width:100%">
|
||||||
|
<canvas id="chartCanvas"></canvas>
|
||||||
|
<div class$="chartTooltip [[tooltip.yAlign]]"
|
||||||
|
style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px">
|
||||||
|
<div class="title">[[tooltip.title]]</div>
|
||||||
|
<div>
|
||||||
|
<ul >
|
||||||
|
<template is="dom-repeat" items="[[tooltip.lines]]">
|
||||||
|
<li><em style$="background-color:[[item.bgColor]]"></em>[[item.text]]</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
|
<script>
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
/* global Chart moment Color */
|
||||||
|
{
|
||||||
|
let SCRIPT_LOADED = window.HASS_DEV;
|
||||||
|
|
||||||
|
class HaChartBase extends Polymer.Element {
|
||||||
|
get chart() {
|
||||||
|
return this._chart;
|
||||||
|
}
|
||||||
|
static get is() { return 'ha-chart-base'; }
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
publish: {
|
||||||
|
type: Boolean,
|
||||||
|
observer: 'onPropsChange'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'onPropsChange'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._isAttached = true;
|
||||||
|
this.set('tooltip', {
|
||||||
|
opacity: '0',
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
xPadding: '0',
|
||||||
|
yPadding: '0'
|
||||||
|
});
|
||||||
|
if (!this._chart) {
|
||||||
|
this.onPropsChange();
|
||||||
|
}
|
||||||
|
this._resizeListener = () => {
|
||||||
|
this._debouncer = Polymer.Debouncer.debounce(
|
||||||
|
this._debouncer,
|
||||||
|
Polymer.Async.timeOut.after(10),
|
||||||
|
() => {
|
||||||
|
if (this._isAttached) {
|
||||||
|
this.resizeChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', this._resizeListener);
|
||||||
|
|
||||||
|
if (!SCRIPT_LOADED) {
|
||||||
|
Polymer.importHref(
|
||||||
|
`${window.HASS_ROOT}/ha-chart-scripts.html`,
|
||||||
|
() => {
|
||||||
|
SCRIPT_LOADED = true;
|
||||||
|
this.onPropsChange();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._isAttached = false;
|
||||||
|
window.removeEventListener('resize', this._resizeListener);
|
||||||
|
|
||||||
|
if (this._resizeTimer !== undefined) {
|
||||||
|
clearInterval(this._resizeTimer);
|
||||||
|
this._resizeTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPropsChange() {
|
||||||
|
if (!this._isAttached || !SCRIPT_LOADED || !this.publish || !this.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawChart();
|
||||||
|
}
|
||||||
|
_customTooltips(tooltip) {
|
||||||
|
// Hide if no tooltip
|
||||||
|
if (tooltip.opacity === 0) {
|
||||||
|
this.set(['tooltip', 'opacity'], 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set caret Position
|
||||||
|
if (tooltip.yAlign) {
|
||||||
|
this.set(['tooltip', 'yAlign'], tooltip.yAlign);
|
||||||
|
} else {
|
||||||
|
this.set(['tooltip', 'yAlign'], 'no-transform');
|
||||||
|
}
|
||||||
|
const title = tooltip.title ? tooltip.title[0] || '' : '';
|
||||||
|
let str1;
|
||||||
|
if (title instanceof Date) {
|
||||||
|
str1 = moment(title).format('L LTS');
|
||||||
|
} else if (title instanceof moment) {
|
||||||
|
str1 = title.format('L LTS');
|
||||||
|
} else {
|
||||||
|
str1 = title;
|
||||||
|
}
|
||||||
|
this.set(['tooltip', 'title'], str1);
|
||||||
|
const bodyLines = tooltip.body.map(n => n.lines);
|
||||||
|
|
||||||
|
// Set Text
|
||||||
|
if (tooltip.body) {
|
||||||
|
this.set(['tooltip', 'lines'], bodyLines.map((body, i) => {
|
||||||
|
const colors = tooltip.labelColors[i];
|
||||||
|
return {
|
||||||
|
color: colors.borderColor,
|
||||||
|
bgColor: colors.backgroundColor,
|
||||||
|
text: body.join('\n'),
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const parentWidth = this.$.chartTarget.clientWidth;
|
||||||
|
let positionX = tooltip.caretX;
|
||||||
|
const positionY = this._chart.canvas.offsetTop + tooltip.caretY;
|
||||||
|
if (tooltip.caretX + 100 > parentWidth) {
|
||||||
|
positionX = parentWidth - 100;
|
||||||
|
} else if (tooltip.caretX < 100) {
|
||||||
|
positionX = 100;
|
||||||
|
}
|
||||||
|
positionX += this._chart.canvas.offsetLeft;
|
||||||
|
// Display, position, and set styles for font
|
||||||
|
this.set(['tooltip', 'opacity'], 1);
|
||||||
|
this.set(['tooltip', 'left'], positionX + 'px');
|
||||||
|
this.set(['tooltip', 'top'], positionY + 'px');
|
||||||
|
this.set(['tooltip', 'yPadding'], tooltip.yPadding);
|
||||||
|
this.set(['tooltip', 'xPadding'], tooltip.xPadding);
|
||||||
|
}
|
||||||
|
_legendClick(event) {
|
||||||
|
event = event || window.event;
|
||||||
|
let target = event.target || event.srcElement;
|
||||||
|
while (target.nodeName !== 'LI') {
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
const index = target.getAttribute('data-lid');
|
||||||
|
|
||||||
|
const meta = this._chart.getDatasetMeta(index);
|
||||||
|
meta.hidden = meta.hidden === null ? !this._chart.data.datasets[index].hidden : null;
|
||||||
|
this.set(['metas', index, 'hidden'], this._chart.isDatasetVisible(index) ? null : 'hidden');
|
||||||
|
this._chart.update();
|
||||||
|
}
|
||||||
|
_drawLegend() {
|
||||||
|
const chart = this._chart;
|
||||||
|
this.set('metas', this._chart.data.datasets.map((x, i) => ({
|
||||||
|
label: x.label,
|
||||||
|
color: x.color,
|
||||||
|
bgColor: x.backgroundColor,
|
||||||
|
hidden: chart.isDatasetVisible(i)
|
||||||
|
})));
|
||||||
|
this.set('unit', this.data.unit);
|
||||||
|
}
|
||||||
|
drawChart() {
|
||||||
|
const data = this.data.data;
|
||||||
|
const ctx = this.$.chartCanvas;
|
||||||
|
|
||||||
|
if ((!data.datasets || !data.datasets.length) && !this._chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.data.type !== 'timeline' && data.datasets.length > 0) {
|
||||||
|
let cnt = 0;
|
||||||
|
cnt = data.datasets.length;
|
||||||
|
const colors = this.constructor.getColorList(cnt);
|
||||||
|
for (let loopI = 0; loopI < cnt; loopI++) {
|
||||||
|
data.datasets[loopI].borderColor = colors[loopI].rgbString();
|
||||||
|
data.datasets[loopI].backgroundColor = colors[loopI].alpha(0.6).rgbaString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._chart) {
|
||||||
|
this._customTooltips({ opacity: 0 });
|
||||||
|
this._chart.data = data;
|
||||||
|
this._chart.update({ duration: 0 });
|
||||||
|
if (this.isTimeline !== true && this.data.legend === true) {
|
||||||
|
this._drawLegend();
|
||||||
|
}
|
||||||
|
this.resizeChart();
|
||||||
|
} else {
|
||||||
|
if (!data.datasets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._customTooltips({ opacity: 0 });
|
||||||
|
let options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0,
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
tooltips: {
|
||||||
|
enabled: false,
|
||||||
|
custom: this._customTooltips.bind(this),
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
spanGaps: true,
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
font: "12px 'Roboto', 'sans-serif'",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
fontFamily: "'Roboto', 'sans-serif'",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options = Chart.helpers.merge(options, this.data.options);
|
||||||
|
if (this.data.type === 'timeline') {
|
||||||
|
this.set('isTimeline', true);
|
||||||
|
if (this.data.colors !== undefined) {
|
||||||
|
this._colorFunc = this.constructor.getColorGenerator(
|
||||||
|
this.data.colors.staticColors,
|
||||||
|
this.data.colors.staticColorIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this._colorFunc !== undefined) {
|
||||||
|
options.colorFunction = this._colorFunc;
|
||||||
|
}
|
||||||
|
if (data.datasets.length === 1) {
|
||||||
|
if (options.scales.yAxes[0].ticks) {
|
||||||
|
options.scales.yAxes[0].ticks.display = false;
|
||||||
|
} else {
|
||||||
|
options.scales.yAxes[0].ticks = { display: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$.chartTarget.style.height = '160px';
|
||||||
|
this.$.chartTarget.height = '160px';
|
||||||
|
const chartData = {
|
||||||
|
type: this.data.type,
|
||||||
|
data: this.data.data,
|
||||||
|
options: options
|
||||||
|
};
|
||||||
|
// Async resize after dom update
|
||||||
|
this._chart = new Chart(ctx, chartData);
|
||||||
|
if (this.isTimeline !== true && this.data.legend === true) {
|
||||||
|
this._drawLegend();
|
||||||
|
}
|
||||||
|
this.resizeChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resizeChart() {
|
||||||
|
if (!this._chart) return;
|
||||||
|
// Chart not ready
|
||||||
|
if (this.$.chartTarget.clientWidth === 0) {
|
||||||
|
if (this._resizeTimer === undefined) {
|
||||||
|
this._resizeTimer = setInterval(this.resizeChart.bind(this), 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this._resizeTimer);
|
||||||
|
this._resizeTimer = undefined;
|
||||||
|
|
||||||
|
this._resizeChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
_resizeChart() {
|
||||||
|
const chartTarget = this.$.chartTarget;
|
||||||
|
|
||||||
|
const options = this.data;
|
||||||
|
const data = options.data;
|
||||||
|
|
||||||
|
if (data.datasets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isTimeline) {
|
||||||
|
this._chart.resize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate chart height for Timeline chart
|
||||||
|
var axis = this._chart.boxes.filter(x => x.position === 'bottom')[0];
|
||||||
|
if (axis && axis.height > 0) {
|
||||||
|
this._axisHeight = axis.height;
|
||||||
|
}
|
||||||
|
if (!this._axisHeight) {
|
||||||
|
chartTarget.style.height = '100px';
|
||||||
|
chartTarget.height = '100px';
|
||||||
|
this._chart.resize();
|
||||||
|
axis = this._chart.boxes.filter(x => x.position === 'bottom')[0];
|
||||||
|
if (axis && axis.height > 0) {
|
||||||
|
this._axisHeight = axis.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._axisHeight) {
|
||||||
|
const cnt = data.datasets.length;
|
||||||
|
const targetHeight = ((30 * cnt) + this._axisHeight) + 'px';
|
||||||
|
if (chartTarget.style.height !== targetHeight) {
|
||||||
|
chartTarget.style.height = targetHeight;
|
||||||
|
chartTarget.height = targetHeight;
|
||||||
|
}
|
||||||
|
this._chart.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get HSL distributed color list
|
||||||
|
static getColorList(count) {
|
||||||
|
let processL = false;
|
||||||
|
if (count > 10) {
|
||||||
|
processL = true;
|
||||||
|
count = Math.ceil(count / 2);
|
||||||
|
}
|
||||||
|
const h1 = 360 / count;
|
||||||
|
const result = [];
|
||||||
|
for (let loopI = 0; loopI < count; loopI++) {
|
||||||
|
result[loopI] = Color().hsl(h1 * loopI, 80, 38);
|
||||||
|
if (processL) {
|
||||||
|
result[loopI + count] = Color().hsl(h1 * loopI, 80, 62);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static getColorGenerator(staticColors, startIndex) {
|
||||||
|
// Known colors for static data,
|
||||||
|
// should add for very common state string manually.
|
||||||
|
// Distribute the color data like complete binary tree
|
||||||
|
function getColorRange(x) {
|
||||||
|
if (x === 0) return 0;
|
||||||
|
if (x === 1) return 0.5;
|
||||||
|
const y = Math.floor(Math.log(x) / Math.LN2);
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
const e = Math.pow(2, y);
|
||||||
|
const n = x - e;
|
||||||
|
let a;
|
||||||
|
if (y % 2 === 1) {
|
||||||
|
if (n % 2 === 0) {
|
||||||
|
a = n + 1;
|
||||||
|
} else {
|
||||||
|
a = n + e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-lonely-if
|
||||||
|
if (n % 2 === 0) {
|
||||||
|
a = e - n - 1;
|
||||||
|
} else {
|
||||||
|
a = (e + e) - n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a / (e + e);
|
||||||
|
}
|
||||||
|
function getColorIndex(idx) {
|
||||||
|
const hIndex = Math.floor(idx / 6);
|
||||||
|
const h1 = getColorRange(hIndex);
|
||||||
|
const c1 = (h1 + (idx % 3)) * 120;
|
||||||
|
const l1 = idx % 6 < 3 ? 62 : 38;
|
||||||
|
return Color().hsl(c1, 75, l1);
|
||||||
|
}
|
||||||
|
const colorDict = {};
|
||||||
|
let colorIndex = 0;
|
||||||
|
if (startIndex > 0) colorIndex = startIndex;
|
||||||
|
if (staticColors) {
|
||||||
|
Object.keys(staticColors).forEach((c) => {
|
||||||
|
const c1 = staticColors[c];
|
||||||
|
if (isFinite(c1)) {
|
||||||
|
colorDict[c.toLowerCase()] = getColorIndex(c1);
|
||||||
|
} else {
|
||||||
|
colorDict[c.toLowerCase()] = Color(staticColors[c]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Custom color assign
|
||||||
|
function getColor(__, data) {
|
||||||
|
let ret;
|
||||||
|
const name = data[3];
|
||||||
|
if (name === null) return Color().hsl(0, 40, 38);
|
||||||
|
if (name === undefined) return Color().hsl(120, 40, 38);
|
||||||
|
const name1 = name.toLowerCase();
|
||||||
|
if (ret === undefined) {
|
||||||
|
ret = colorDict[name1];
|
||||||
|
}
|
||||||
|
if (ret === undefined) {
|
||||||
|
ret = getColorIndex(colorIndex);
|
||||||
|
colorIndex++;
|
||||||
|
colorDict[name1] = ret;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return getColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define(HaChartBase.is, HaChartBase);
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,299 +1,274 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
<link rel="import" href="../../bower_components/polymer/lib/utils/debounce.html">
|
<link rel="import" href="../../bower_components/polymer/lib/utils/debounce.html">
|
||||||
|
<link rel="import" href="./entity/ha-chart-base.html">
|
||||||
|
|
||||||
|
<dom-module id='state-history-chart-line'>
|
||||||
|
<template>
|
||||||
|
<ha-chart-base publish data="[[chartData]]"></ha-chart-base>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
<script>
|
<script>
|
||||||
{
|
class StateHistoryChartLine extends Polymer.Element {
|
||||||
function range(start, end) {
|
static get is() { return 'state-history-chart-line'; }
|
||||||
var result = [];
|
static get properties() {
|
||||||
var i;
|
return {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
for (i = start; i < end; i++) {
|
unit: {
|
||||||
result.push(i);
|
type: String,
|
||||||
}
|
},
|
||||||
|
|
||||||
return result;
|
isSingleDevice: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
endTime: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static get observers() {
|
||||||
|
return ['dataChanged(data, endTime)'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveParseFloat(value) {
|
connectedCallback() {
|
||||||
var parsed = parseFloat(value);
|
super.connectedCallback();
|
||||||
return !isNaN(parsed) && isFinite(parsed) ? parsed : null;
|
this._isAttached = true;
|
||||||
|
this.drawChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
class StateHistoryChartLine extends Polymer.Element {
|
dataChanged() {
|
||||||
static get is() { return 'state-history-chart-line'; }
|
this.drawChart();
|
||||||
static get properties() {
|
}
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
unit: {
|
drawChart() {
|
||||||
type: String,
|
const unit = this.unit;
|
||||||
},
|
const deviceStates = this.data;
|
||||||
|
const datasets = [];
|
||||||
|
let endTime;
|
||||||
|
|
||||||
isSingleDevice: {
|
if (!this._isAttached) {
|
||||||
type: Boolean,
|
return;
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
endTime: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
chartEngine: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get observers() {
|
if (deviceStates.length === 0) {
|
||||||
return ['dataChanged(data, endTime)'];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
function safeParseFloat(value) {
|
||||||
super.connectedCallback();
|
const parsed = parseFloat(value);
|
||||||
this._isAttached = true;
|
return isFinite(parsed) ? parsed : null;
|
||||||
this.drawChart();
|
|
||||||
|
|
||||||
this._resizeListener = () => {
|
|
||||||
this._debouncer = Polymer.Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
Polymer.Async.timeOut.after(10),
|
|
||||||
() => {
|
|
||||||
if (this._isAttached) {
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', this._resizeListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
endTime = this.endTime ||
|
||||||
super.disconnectedCallback();
|
new Date(Math.max.apply(null, deviceStates.map(states =>
|
||||||
this._isAttached = false;
|
new Date(states.states[states.states.length - 1].last_changed))));
|
||||||
window.removeEventListener('resize', this._resizeListener);
|
if (endTime > new Date()) {
|
||||||
|
endTime = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
dataChanged() {
|
deviceStates.forEach((states) => {
|
||||||
this.drawChart();
|
const domain = states.domain;
|
||||||
}
|
const name = states.name;
|
||||||
|
// array containing [value1, value2, etc]
|
||||||
|
let prevValues;
|
||||||
|
const data = [];
|
||||||
|
|
||||||
drawChart() {
|
|
||||||
var unit = this.unit;
|
|
||||||
var deviceStates = this.data;
|
|
||||||
var options;
|
|
||||||
var startTime;
|
|
||||||
var endTime;
|
|
||||||
var dataTables;
|
|
||||||
var finalDataTable;
|
|
||||||
var daysDelta;
|
|
||||||
|
|
||||||
if (!this._isAttached) {
|
function pushData(timestamp, datavalues) {
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.chartEngine) {
|
function addColumn(nameY, step, fill) {
|
||||||
this.chartEngine = new window.google.visualization.ComboChart(this);
|
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 (deviceStates.length === 0) {
|
if (domain === 'thermostat' || domain === 'climate') {
|
||||||
return;
|
// We differentiate between thermostats that have a target temperature
|
||||||
}
|
// range versus ones that have just a target temperature
|
||||||
|
|
||||||
options = {
|
// Using step chart by step-before so manually interpolation not needed.
|
||||||
backgroundColor: '#fafafa',
|
const hasTargetRange = states.states.some(state => state.attributes &&
|
||||||
legend: { position: 'top' },
|
state.attributes.target_temp_high !== state.attributes.target_temp_low);
|
||||||
interpolateNulls: true,
|
const hasHeat = states.states.some(state => state.state === 'heat');
|
||||||
titlePosition: 'none',
|
const hasCool = states.states.some(state => state.state === 'cool');
|
||||||
vAxes: {
|
|
||||||
// Adds units to the left hand side of the graph
|
|
||||||
0: { title: unit },
|
|
||||||
},
|
|
||||||
hAxis: {
|
|
||||||
format: 'H:mm',
|
|
||||||
},
|
|
||||||
chartArea: { left: '60', width: '95%' },
|
|
||||||
explorer: {
|
|
||||||
actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'],
|
|
||||||
keepInBounds: true,
|
|
||||||
axis: 'horizontal',
|
|
||||||
maxZoomIn: 0.1,
|
|
||||||
},
|
|
||||||
seriesType: 'line',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.isSingleDevice) {
|
|
||||||
options.legend.position = 'none';
|
|
||||||
options.vAxes[0].title = null;
|
|
||||||
options.chartArea.left = 40;
|
|
||||||
options.chartArea.height = '80%';
|
|
||||||
options.chartArea.top = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = new Date(Math.min.apply(null, deviceStates.map(function (states) {
|
addColumn(name + ' current temperature', true);
|
||||||
return new Date(states.states[0].last_changed);
|
if (hasHeat) {
|
||||||
})));
|
addColumn(name + ' heating', true, true);
|
||||||
|
// The "heating" series uses steppedArea to shade the area below the current
|
||||||
endTime = this.endTime ||
|
// temperature when the thermostat is calling for heat.
|
||||||
new Date(Math.max.apply(null, deviceStates.map(states =>
|
}
|
||||||
new Date(states.states[states.states.length - 1].last_changed))));
|
if (hasCool) {
|
||||||
if (endTime > new Date()) {
|
addColumn(name + ' cooling', true, true);
|
||||||
endTime = new Date();
|
// The "cooling" series uses steppedArea to shade the area below the current
|
||||||
}
|
// temperature when the thermostat is calling for heat.
|
||||||
|
|
||||||
daysDelta = (endTime - startTime) / (24 * 3600 * 1000);
|
|
||||||
// Avoid rounding up when the API returns a few extra seconds.
|
|
||||||
if (daysDelta > 30.1) {
|
|
||||||
options.hAxis.format = 'MMM d';
|
|
||||||
} else if (daysDelta > 3.1) {
|
|
||||||
options.hAxis.format = 'EEE, MMM d';
|
|
||||||
} else if (daysDelta > 1.1) {
|
|
||||||
options.hAxis.format = 'EEE, MMM d, H:mm';
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTables = deviceStates.map(function (states) {
|
|
||||||
var domain = states.domain;
|
|
||||||
var name = states.name;
|
|
||||||
var data = [];
|
|
||||||
var dataTable = new window.google.visualization.DataTable();
|
|
||||||
// array containing [time, value1, value2, etc]
|
|
||||||
var prevValues;
|
|
||||||
var processState;
|
|
||||||
var noInterpolations;
|
|
||||||
var series;
|
|
||||||
dataTable.addColumn({ type: 'datetime', id: 'Time' });
|
|
||||||
|
|
||||||
function pushData(values, noInterpolationValues) {
|
|
||||||
var timestamp = values[0];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (prevValues && noInterpolationValues) {
|
|
||||||
// if we have to prevent interpolation, we add an old value for each
|
|
||||||
// value that should not be interpolated at the same time that our new
|
|
||||||
// line will be published.
|
|
||||||
data.push([timestamp].concat(prevValues.slice(1).map(function (val, index) {
|
|
||||||
return noInterpolationValues[index] ? val : null;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
data.push(values);
|
|
||||||
prevValues = values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === 'thermostat' || domain === 'climate') {
|
if (hasTargetRange) {
|
||||||
// We differentiate between thermostats that have a target temperature
|
addColumn(name + ' target temperature high', true);
|
||||||
// range versus ones that have just a target temperature
|
addColumn(name + ' target temperature low', true);
|
||||||
const hasTargetRange = states.states.some(state => state.attributes &&
|
} else {
|
||||||
state.attributes.target_temp_high !== state.attributes.target_temp_low);
|
addColumn(name + ' target temperature', true);
|
||||||
const hasHeat = states.states.some(state => state.state === 'heat');
|
}
|
||||||
const hasCool = states.states.some(state => state.state === 'cool');
|
|
||||||
|
|
||||||
|
states.states.forEach((state) => {
|
||||||
dataTable.addColumn('number', name + ' current temperature');
|
if (!state.attributes) return;
|
||||||
if (hasHeat || hasCool) {
|
const curTemp = safeParseFloat(state.attributes.current_temperature);
|
||||||
options.series = Object.assign({}, options.series);
|
const series = [curTemp];
|
||||||
}
|
|
||||||
if (hasHeat) {
|
if (hasHeat) {
|
||||||
dataTable.addColumn('number', name + ' heating');
|
series.push(state.state === 'heat' ? curTemp : null);
|
||||||
// The "heating" series uses steppedArea to shade the area below the current
|
|
||||||
// temperature when the thermostat is calling for heat.
|
|
||||||
options.series[dataTable.getNumberOfColumns() - 1] =
|
|
||||||
{ type: 'steppedArea' };
|
|
||||||
}
|
}
|
||||||
if (hasCool) {
|
if (hasCool) {
|
||||||
dataTable.addColumn('number', name + ' cooling');
|
series.push(state.state === 'cool' ? curTemp : null);
|
||||||
// The "cooling" series uses steppedArea to shade the area below the current
|
|
||||||
// temperature when the thermostat is calling for heat.
|
|
||||||
options.series[dataTable.getNumberOfColumns() - 1] =
|
|
||||||
{ type: 'steppedArea' };
|
|
||||||
}
|
}
|
||||||
if (hasTargetRange) {
|
if (hasTargetRange) {
|
||||||
dataTable.addColumn('number', name + ' target temperature high');
|
const targetHigh = safeParseFloat(state.attributes.target_temp_high);
|
||||||
dataTable.addColumn('number', name + ' target temperature low');
|
const targetLow = safeParseFloat(state.attributes.target_temp_low);
|
||||||
} else {
|
series.push(targetHigh, targetLow);
|
||||||
dataTable.addColumn('number', name + ' target temperature');
|
|
||||||
}
|
|
||||||
|
|
||||||
processState = function (state) {
|
|
||||||
if (!state.attributes) return;
|
|
||||||
const curTemp = saveParseFloat(state.attributes.current_temperature);
|
|
||||||
|
|
||||||
series = [curTemp];
|
|
||||||
noInterpolations = [false];
|
|
||||||
|
|
||||||
// Drawing the 'heating'/'cooling' area up to the current temp should keep it from
|
|
||||||
// overlapping but avoid any weird gaps or range mismatches
|
|
||||||
if (hasHeat) {
|
|
||||||
series.push(state.state === 'heat' ? curTemp : null);
|
|
||||||
noInterpolations.push(true);
|
|
||||||
}
|
|
||||||
if (hasCool) {
|
|
||||||
series.push(state.state === 'cool' ? curTemp : null);
|
|
||||||
noInterpolations.push(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTargetRange) {
|
|
||||||
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
|
|
||||||
var targetLow = saveParseFloat(state.attributes.target_temp_low);
|
|
||||||
|
|
||||||
series = series.concat([targetHigh, targetLow]);
|
|
||||||
noInterpolations = noInterpolations.concat([true, true]);
|
|
||||||
} else {
|
|
||||||
var target = saveParseFloat(state.attributes.temperature);
|
|
||||||
|
|
||||||
series.push(target);
|
|
||||||
noInterpolations.push(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushData(
|
pushData(
|
||||||
[new Date(state.last_changed)].concat(series),
|
new Date(state.last_changed),
|
||||||
noInterpolations
|
series
|
||||||
);
|
);
|
||||||
};
|
} else {
|
||||||
|
const target = safeParseFloat(state.attributes.temperature);
|
||||||
states.states.forEach(processState);
|
series.push(target);
|
||||||
} else {
|
pushData(
|
||||||
dataTable.addColumn('number', name);
|
new Date(state.last_changed),
|
||||||
|
series
|
||||||
// Only disable interpolation for sensors
|
);
|
||||||
noInterpolations = domain !== 'sensor' && [true];
|
}
|
||||||
|
});
|
||||||
states.states.forEach(function (state) {
|
|
||||||
var value = saveParseFloat(state.state);
|
|
||||||
pushData([new Date(state.last_changed), value], noInterpolations);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an entry for final values
|
|
||||||
if (prevValues) {
|
|
||||||
pushData([endTime].concat(prevValues.slice(1)), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTable.addRows(data);
|
|
||||||
return dataTable;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dataTables.length === 1) {
|
|
||||||
finalDataTable = dataTables[0];
|
|
||||||
} else {
|
} else {
|
||||||
finalDataTable = dataTables.slice(1).reduce(
|
// Only disable interpolation for sensors
|
||||||
function (tot, cur) {
|
const isStep = domain === 'sensor';
|
||||||
return window.google.visualization.data.join(
|
addColumn(name, isStep);
|
||||||
tot, cur, 'full', [[0, 0]],
|
|
||||||
range(1, tot.getNumberOfColumns()),
|
let lastValue = null;
|
||||||
range(1, cur.getNumberOfColumns())
|
let lastDate = null;
|
||||||
);
|
let lastNullDate = null;
|
||||||
},
|
|
||||||
dataTables[0]
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chartEngine.draw(finalDataTable, options);
|
// Add an entry for final values
|
||||||
}
|
pushData(endTime, prevValues, false);
|
||||||
|
|
||||||
|
// Concat two arrays
|
||||||
|
Array.prototype.push.apply(datasets, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
type: 'line',
|
||||||
|
unit: unit,
|
||||||
|
legend: !this.isSingleDevice,
|
||||||
|
options: {
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
ticks: {
|
||||||
|
major: {
|
||||||
|
fontStyle: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 7,
|
||||||
|
},
|
||||||
|
// Add space to prevent cut-off.
|
||||||
|
afterDataLimits: (axis) => {
|
||||||
|
const min = axis.min;
|
||||||
|
const max = axis.max;
|
||||||
|
if (isFinite(min) && isFinite(max)) {
|
||||||
|
const padding = (max - min) * 0.05;
|
||||||
|
axis.min -= padding;
|
||||||
|
axis.max += padding;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: 'neareach',
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'neareach',
|
||||||
|
},
|
||||||
|
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(StateHistoryChartLine.is, StateHistoryChartLine);
|
|
||||||
}
|
}
|
||||||
|
customElements.define(StateHistoryChartLine.is, StateHistoryChartLine);
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
|
||||||
<link rel="import" href="../../bower_components/polymer/lib/utils/debounce.html">
|
<link rel="import" href="../../bower_components/polymer/lib/utils/debounce.html">
|
||||||
|
<link rel='import' href='./entity/ha-chart-base.html'>
|
||||||
|
|
||||||
|
<dom-module id='state-history-chart-timeline'>
|
||||||
|
<template>
|
||||||
|
<ha-chart-base publish data="[[chartData]]"></ha-chart-base>
|
||||||
|
</template>
|
||||||
|
</dom-module>
|
||||||
<script>
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
class StateHistoryChartTimeline extends Polymer.Element {
|
class StateHistoryChartTimeline extends Polymer.Element {
|
||||||
static get is() { return 'state-history-chart-timeline'; }
|
static get is() { return 'state-history-chart-timeline'; }
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
observer: 'dataChanged',
|
||||||
},
|
},
|
||||||
noSingle: Boolean,
|
noSingle: Boolean,
|
||||||
endTime: Date,
|
endTime: Date,
|
||||||
@ -15,32 +27,13 @@ class StateHistoryChartTimeline extends Polymer.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get observers() {
|
static get observers() {
|
||||||
return ['dataChanged(data, endTime)'];
|
return ['dataChanged(data, endTime, localize, language)'];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._isAttached = true;
|
this._isAttached = true;
|
||||||
this.drawChart();
|
this.drawChart();
|
||||||
|
|
||||||
this._resizeListener = () => {
|
|
||||||
this._debouncer = Polymer.Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
Polymer.Async.timeOut.after(10),
|
|
||||||
() => {
|
|
||||||
if (this._isAttached) {
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', this._resizeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._isAttached = false;
|
|
||||||
window.removeEventListener('resize', this._resizeListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataChanged() {
|
dataChanged() {
|
||||||
@ -48,119 +41,124 @@ class StateHistoryChartTimeline extends Polymer.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawChart() {
|
drawChart() {
|
||||||
var stateHistory = this.data;
|
const staticColors = {
|
||||||
var chart;
|
on: 1,
|
||||||
var dataTable;
|
off: 0,
|
||||||
var startTime;
|
unavailable: '#a0a0a0',
|
||||||
var endTime;
|
unknown: '#606060',
|
||||||
var numTimelines;
|
idle: 2
|
||||||
var format;
|
};
|
||||||
var daysDelta;
|
let stateHistory = this.data;
|
||||||
|
|
||||||
if (!this._isAttached) {
|
if (!this._isAttached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this.lastChild) {
|
if (!stateHistory) {
|
||||||
this.removeChild(this.lastChild);
|
stateHistory = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateHistory || stateHistory.length === 0) {
|
const startTime = new Date(stateHistory.reduce(
|
||||||
return;
|
(minTime, stateInfo) => Math.min(minTime, new Date(stateInfo.data[0].last_changed)),
|
||||||
}
|
new Date()
|
||||||
|
));
|
||||||
chart = new window.google.visualization.Timeline(this);
|
|
||||||
dataTable = new window.google.visualization.DataTable();
|
|
||||||
|
|
||||||
dataTable.addColumn({ type: 'string', id: 'Entity' });
|
|
||||||
dataTable.addColumn({ type: 'string', id: 'State' });
|
|
||||||
dataTable.addColumn({ type: 'date', id: 'Start' });
|
|
||||||
dataTable.addColumn({ type: 'date', id: 'End' });
|
|
||||||
|
|
||||||
function addRow(entityDisplay, stateStr, start, end) {
|
|
||||||
var stateDisplay = stateStr.replace(/_/g, ' ');
|
|
||||||
dataTable.addRow([entityDisplay, stateDisplay, start, end]);
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = new Date(stateHistory.reduce(function (minTime, stateInfo) {
|
|
||||||
return Math.min(minTime, new Date(stateInfo.data[0].last_changed));
|
|
||||||
}, new Date()));
|
|
||||||
|
|
||||||
// end time is Math.max(startTime, last_event)
|
// end time is Math.max(startTime, last_event)
|
||||||
endTime = this.endTime ||
|
let endTime = this.endTime ||
|
||||||
new Date(stateHistory.reduce(function (maxTime, stateInfo) {
|
new Date(stateHistory.reduce((maxTime, stateInfo) => Math.max(
|
||||||
return Math.max(
|
maxTime,
|
||||||
maxTime,
|
new Date(stateInfo.data[stateInfo.data.length - 1].last_changed)
|
||||||
new Date(stateInfo.data[stateInfo.data.length - 1].last_changed)
|
), startTime));
|
||||||
);
|
|
||||||
}, startTime));
|
|
||||||
|
|
||||||
if (endTime > new Date()) {
|
if (endTime > new Date()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
}
|
}
|
||||||
format = 'H:mm';
|
|
||||||
daysDelta = (endTime - startTime) / (24 * 3600 * 1000);
|
|
||||||
// Avoid rounding up when the API returns a few extra seconds.
|
|
||||||
if (daysDelta > 30.1) {
|
|
||||||
format = 'MMM d';
|
|
||||||
} else if (daysDelta > 3.1) {
|
|
||||||
format = 'EEE, MMM d';
|
|
||||||
} else if (daysDelta > 1.1) {
|
|
||||||
format = 'EEE, MMM d, H:mm';
|
|
||||||
}
|
|
||||||
|
|
||||||
numTimelines = 0;
|
const labels = [];
|
||||||
|
const datasets = [];
|
||||||
// stateHistory is a list of lists of sorted state objects
|
// stateHistory is a list of lists of sorted state objects
|
||||||
stateHistory.forEach(function (stateInfo) {
|
stateHistory.forEach((stateInfo) => {
|
||||||
var entityDisplay;
|
let newLastChanged;
|
||||||
var newLastChanged;
|
let prevState = null;
|
||||||
var prevState = null;
|
let locState = null;
|
||||||
var prevLastChanged = null;
|
let prevLastChanged = startTime;
|
||||||
|
const entityDisplay = stateInfo.name;
|
||||||
|
const dataRow = [];
|
||||||
|
|
||||||
if (stateInfo.data.length === 0) return;
|
|
||||||
|
|
||||||
entityDisplay = stateInfo.name;
|
stateInfo.data.forEach((state) => {
|
||||||
|
let newState = state.state;
|
||||||
stateInfo.data.forEach(function (state) {
|
const timeStamp = new Date(state.last_changed);
|
||||||
var timeStamp = new Date(state.last_changed);
|
if (newState === undefined || newState === '') {
|
||||||
|
newState = null;
|
||||||
|
}
|
||||||
if (timeStamp > endTime) {
|
if (timeStamp > endTime) {
|
||||||
// Drop datapoints that are after the requested endTime. This could happen if
|
// 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.
|
// endTime is 'now' and client time is not in sync with server time.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (prevState !== null && state.state !== prevState) {
|
if (prevState !== null && newState !== prevState) {
|
||||||
newLastChanged = new Date(state.last_changed);
|
newLastChanged = new Date(state.last_changed);
|
||||||
|
|
||||||
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
|
dataRow.push([
|
||||||
|
prevLastChanged,
|
||||||
|
newLastChanged,
|
||||||
|
locState,
|
||||||
|
prevState,
|
||||||
|
]);
|
||||||
|
|
||||||
prevState = state.state;
|
prevState = newState;
|
||||||
|
locState = state.state_localize;
|
||||||
prevLastChanged = newLastChanged;
|
prevLastChanged = newLastChanged;
|
||||||
} else if (prevState === null) {
|
} else if (prevState === null) {
|
||||||
prevState = state.state;
|
prevState = newState;
|
||||||
|
locState = state.state_localize;
|
||||||
prevLastChanged = new Date(state.last_changed);
|
prevLastChanged = new Date(state.last_changed);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (prevState !== null) {
|
if (prevState !== null) {
|
||||||
addRow(entityDisplay, prevState, prevLastChanged, endTime);
|
dataRow.push([
|
||||||
|
prevLastChanged,
|
||||||
|
endTime,
|
||||||
|
locState,
|
||||||
|
prevState,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
numTimelines++;
|
datasets.push({ data: dataRow });
|
||||||
|
labels.push(entityDisplay);
|
||||||
});
|
});
|
||||||
|
|
||||||
chart.draw(dataTable, {
|
const chartOptions = {
|
||||||
backgroundColor: '#fafafa',
|
type: 'timeline',
|
||||||
|
options: {
|
||||||
height: 55 + (numTimelines * 42),
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
timeline: {
|
ticks: {
|
||||||
showRowLabels: this.noSingle || stateHistory.length > 1,
|
major: {
|
||||||
|
fontStyle: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
afterSetDimensions: (yaxe) => {
|
||||||
|
yaxe.maxWidth = yaxe.chart.width * 0.18;
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
hAxis: {
|
labels: labels,
|
||||||
format: format
|
datasets: datasets
|
||||||
},
|
},
|
||||||
});
|
colors: {
|
||||||
|
staticColors: staticColors,
|
||||||
|
staticColorIndex: 3,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.chartData = chartOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define(StateHistoryChartTimeline.is, StateHistoryChartTimeline);
|
customElements.define(StateHistoryChartTimeline.is, StateHistoryChartTimeline);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
<link rel="import" href="../../bower_components/polymer/polymer-element.html">
|
||||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||||
|
|
||||||
<link rel="import" href="../../bower_components/google-apis/google-legacy-loader.html">
|
|
||||||
|
|
||||||
<link rel="import" href="./state-history-chart-timeline.html">
|
<link rel="import" href="./state-history-chart-timeline.html">
|
||||||
<link rel="import" href="./state-history-chart-line.html">
|
<link rel="import" href="./state-history-chart-line.html">
|
||||||
|
|
||||||
<dom-module id="state-history-charts">
|
<dom-module id="state-history-charts">
|
||||||
<template>
|
<template>
|
||||||
<link href="https://ajax.googleapis.com/ajax/static/modules/gviz/1.0/core/tooltip.css" rel="stylesheet" type="text/css">
|
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.google-visualization-tooltip {
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
state-history-chart-timeline, state-history-chart-line {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -33,33 +22,25 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<google-legacy-loader on-api-load="_googleApiLoaded"></google-legacy-loader>
|
<template is='dom-if' if='[[_computeIsEmpty(historyData)]]'>
|
||||||
|
No state history found.
|
||||||
<template is='dom-if' if='[[_isLoading]]'>
|
|
||||||
<div class='loading-container'>
|
|
||||||
<paper-spinner active alt='Updating history data'></paper-spinner>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is='dom-if' if='[[!_isLoading]]'>
|
<template is='dom-if' if='[[historyData.timeline.length]]'>
|
||||||
<template is='dom-if' if='[[_computeIsEmpty(historyData)]]'>
|
|
||||||
No state history found.
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<state-history-chart-timeline
|
<state-history-chart-timeline
|
||||||
data='[[historyData.timeline]]'
|
data='[[historyData.timeline]]'
|
||||||
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'
|
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'
|
||||||
no-single='[[noSingle]]'>
|
no-single='[[noSingle]]'>
|
||||||
</state-history-chart-timeline>
|
</state-history-chart-timeline>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template is='dom-repeat' items='[[historyData.line]]'>
|
<template is='dom-repeat' items='[[historyData.line]]'>
|
||||||
<state-history-chart-line
|
<state-history-chart-line
|
||||||
unit='[[item.unit]]'
|
unit='[[item.unit]]'
|
||||||
data='[[item.data]]'
|
data='[[item.data]]'
|
||||||
is-single-device='[[_computeIsSingleLineChart(historyData, noSingle)]]'
|
is-single-device='[[_computeIsSingleLineChart(item.data, noSingle)]]'
|
||||||
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'>
|
end-time='[[_computeEndTime(endTime, upToNow, historyData)]]'>
|
||||||
</state-history-chart-line>
|
</state-history-chart-line>
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</dom-module>
|
</dom-module>
|
||||||
@ -69,6 +50,9 @@ class StateHistoryCharts extends Polymer.Element {
|
|||||||
static get is() { return 'state-history-charts'; }
|
static get is() { return 'state-history-charts'; }
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
hass: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
historyData: {
|
historyData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
value: null,
|
value: null,
|
||||||
@ -85,35 +69,11 @@ class StateHistoryCharts extends Polymer.Element {
|
|||||||
|
|
||||||
upToNow: Boolean,
|
upToNow: Boolean,
|
||||||
noSingle: Boolean,
|
noSingle: Boolean,
|
||||||
|
|
||||||
_apiLoaded: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
_isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
computed: '_computeIsLoading(isLoadingData, _apiLoaded)',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeIsSingleLineChart(historyData, noSingle) {
|
_computeIsSingleLineChart(data, noSingle) {
|
||||||
return !noSingle && historyData && historyData.line.length === 1;
|
return !noSingle && data && data.length === 1;
|
||||||
}
|
|
||||||
|
|
||||||
_googleApiLoaded() {
|
|
||||||
window.google.load('visualization', '1', {
|
|
||||||
packages: ['timeline', 'corechart'],
|
|
||||||
language: navigator.language,
|
|
||||||
callback: function () {
|
|
||||||
this._apiLoaded = true;
|
|
||||||
}.bind(this),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeIsLoading(_isLoadingData, _apiLoaded) {
|
|
||||||
return _isLoadingData || !_apiLoaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeIsEmpty(historyData) {
|
_computeIsEmpty(historyData) {
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
entity_id: stateInfo[0].entity_id,
|
entity_id: stateInfo[0].entity_id,
|
||||||
data: stateInfo
|
data: stateInfo
|
||||||
.map(state => ({
|
.map(state => ({
|
||||||
state: window.hassUtil.computeStateDisplay(localize, state, language),
|
state_localize: window.hassUtil.computeStateDisplay(localize, state, language),
|
||||||
|
state: state.state,
|
||||||
last_changed: state.last_changed,
|
last_changed: state.last_changed,
|
||||||
}))
|
}))
|
||||||
.filter((element, index, arr) => {
|
.filter((element, index, arr) => {
|
||||||
@ -140,7 +141,7 @@
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.filterChanged(
|
this.filterChanged(
|
||||||
this.filterType, this.entityId, this.startTime, this.endTime,
|
this.filterType, this.entityId, this.startTime, this.endTime,
|
||||||
this.cacheConfig, this.localize, this.language
|
this.cacheConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +165,7 @@
|
|||||||
filterChanged(filterType, entityId, startTime, endTime, cacheConfig, localize, language) {
|
filterChanged(filterType, entityId, startTime, endTime, cacheConfig, localize, language) {
|
||||||
if (!this.hass) return;
|
if (!this.hass) return;
|
||||||
if (cacheConfig && !cacheConfig.cacheKey) return;
|
if (cacheConfig && !cacheConfig.cacheKey) return;
|
||||||
|
if (!localize || !language) return;
|
||||||
this._madeFirstCall = true;
|
this._madeFirstCall = true;
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
|
74
src/resources/ha-chart-scripts.html
Normal file
74
src/resources/ha-chart-scripts.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script src="../../bower_components/moment/moment.js"></script>
|
||||||
|
<script src="../../bower_components/chart.js/dist/Chart.min.js"></script>
|
||||||
|
<script src="../../bower_components/chartjs-chart-timeline/timeline.js"></script>
|
||||||
|
<script>
|
||||||
|
// Use minified(Chart.min.js) version to fix strange color after uglify
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
/* global Chart moment */
|
||||||
|
{
|
||||||
|
// This function add a new interaction mode to Chart.js that
|
||||||
|
// returns one point for every dataset.
|
||||||
|
Chart.Interaction.modes.neareach = function (chart, e, options) {
|
||||||
|
const getRange = {
|
||||||
|
x: (a, b) => Math.abs(a.x - b.x),
|
||||||
|
y: (a, b) => Math.abs(a.y - b.y),
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
xy: (a, b) => Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2),
|
||||||
|
};
|
||||||
|
const getRangeMax = {
|
||||||
|
x: r => r,
|
||||||
|
y: r => r,
|
||||||
|
xy: r => r * r,
|
||||||
|
};
|
||||||
|
let position;
|
||||||
|
if (e.native) {
|
||||||
|
position = {
|
||||||
|
x: e.x,
|
||||||
|
y: e.y
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
position = Chart.helpers.getRelativePosition(e, chart);
|
||||||
|
}
|
||||||
|
const elements = [];
|
||||||
|
const elementsRange = [];
|
||||||
|
const datasets = chart.data.datasets;
|
||||||
|
let meta;
|
||||||
|
options.axis = options.axis || 'xy';
|
||||||
|
const rangeFunc = getRange[options.axis];
|
||||||
|
const rangeMaxFunc = getRangeMax[options.axis];
|
||||||
|
|
||||||
|
for (let i = 0, ilen = datasets.length; i < ilen; ++i) {
|
||||||
|
if (!chart.isDatasetVisible(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
meta = chart.getDatasetMeta(i);
|
||||||
|
for (let j = 0, jlen = meta.data.length; j < jlen; ++j) {
|
||||||
|
const element = meta.data[j];
|
||||||
|
if (!element._view.skip) {
|
||||||
|
const vm = element._view;
|
||||||
|
const range = rangeFunc(vm, position);
|
||||||
|
const oldRange = elementsRange[i];
|
||||||
|
if (range < rangeMaxFunc(vm.radius + vm.hitRadius)) {
|
||||||
|
if (oldRange === undefined || oldRange > range) {
|
||||||
|
elementsRange[i] = range;
|
||||||
|
elements[i] = element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ret = elements.filter(n => n !== undefined);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fix infinite loop bug in Chart.js 2.7.1
|
||||||
|
const x = Chart.scaleService.constructors.time.prototype;
|
||||||
|
x._getLabelCapacity = x.getLabelCapacity;
|
||||||
|
x.getLabelCapacity = function () {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
const ret = this._getLabelCapacity.apply(this, arguments);
|
||||||
|
return ret > 0 ? ret : 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user