mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Bump chartjs to version 3 (#9401)
This commit is contained in:
parent
f88e238d41
commit
0ef3421fa2
@ -5,8 +5,6 @@ const paths = require("./paths.js");
|
|||||||
|
|
||||||
// Files from NPM Packages that should not be imported
|
// Files from NPM Packages that should not be imported
|
||||||
module.exports.ignorePackages = ({ latestBuild }) => [
|
module.exports.ignorePackages = ({ latestBuild }) => [
|
||||||
// Bloats bundle and it's not used.
|
|
||||||
path.resolve(require.resolve("moment"), "../locale"),
|
|
||||||
// Part of yaml.js and only used for !!js functions that we don't use
|
// Part of yaml.js and only used for !!js functions that we don't use
|
||||||
require.resolve("esprima"),
|
require.resolve("esprima"),
|
||||||
];
|
];
|
||||||
|
@ -96,11 +96,11 @@
|
|||||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||||
"@vue/web-component-wrapper": "^1.2.0",
|
"@vue/web-component-wrapper": "^1.2.0",
|
||||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^3.3.2",
|
||||||
"chartjs-chart-timeline": "^0.4.0",
|
|
||||||
"comlink": "^4.3.1",
|
"comlink": "^4.3.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.5.11",
|
||||||
|
"date-fns": "^2.22.1",
|
||||||
"deep-clone-simple": "^1.1.1",
|
"deep-clone-simple": "^1.1.1",
|
||||||
"deep-freeze": "^0.0.1",
|
"deep-freeze": "^0.0.1",
|
||||||
"fecha": "^4.2.0",
|
"fecha": "^4.2.0",
|
||||||
|
63
src/common/color/colors.ts
Normal file
63
src/common/color/colors.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
export const COLORS = [
|
||||||
|
"#377eb8",
|
||||||
|
"#984ea3",
|
||||||
|
"#00d2d5",
|
||||||
|
"#ff7f00",
|
||||||
|
"#af8d00",
|
||||||
|
"#7f80cd",
|
||||||
|
"#b3e900",
|
||||||
|
"#c42e60",
|
||||||
|
"#a65628",
|
||||||
|
"#f781bf",
|
||||||
|
"#8dd3c7",
|
||||||
|
"#bebada",
|
||||||
|
"#fb8072",
|
||||||
|
"#80b1d3",
|
||||||
|
"#fdb462",
|
||||||
|
"#fccde5",
|
||||||
|
"#bc80bd",
|
||||||
|
"#ffed6f",
|
||||||
|
"#c4eaff",
|
||||||
|
"#cf8c00",
|
||||||
|
"#1b9e77",
|
||||||
|
"#d95f02",
|
||||||
|
"#e7298a",
|
||||||
|
"#e6ab02",
|
||||||
|
"#a6761d",
|
||||||
|
"#0097ff",
|
||||||
|
"#00d067",
|
||||||
|
"#f43600",
|
||||||
|
"#4ba93b",
|
||||||
|
"#5779bb",
|
||||||
|
"#927acc",
|
||||||
|
"#97ee3f",
|
||||||
|
"#bf3947",
|
||||||
|
"#9f5b00",
|
||||||
|
"#f48758",
|
||||||
|
"#8caed6",
|
||||||
|
"#f2b94f",
|
||||||
|
"#eff26e",
|
||||||
|
"#e43872",
|
||||||
|
"#d9b100",
|
||||||
|
"#9d7a00",
|
||||||
|
"#698cff",
|
||||||
|
"#d9d9d9",
|
||||||
|
"#00d27e",
|
||||||
|
"#d06800",
|
||||||
|
"#009f82",
|
||||||
|
"#c49200",
|
||||||
|
"#cbe8ff",
|
||||||
|
"#fecddf",
|
||||||
|
"#c27eb6",
|
||||||
|
"#8cd2ce",
|
||||||
|
"#c4b8d9",
|
||||||
|
"#f883b0",
|
||||||
|
"#a49100",
|
||||||
|
"#f48800",
|
||||||
|
"#27d0df",
|
||||||
|
"#a04a9b",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getColorByIndex(index: number) {
|
||||||
|
return COLORS[index % COLORS.length];
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
const luminosity = (rgb: [number, number, number]): number => {
|
export const luminosity = (rgb: [number, number, number]): number => {
|
||||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||||
const lum: [number, number, number] = [0, 0, 0];
|
const lum: [number, number, number] = [0, 0, 0];
|
||||||
for (let i = 0; i < rgb.length; i++) {
|
for (let i = 0; i < rgb.length; i++) {
|
||||||
|
@ -17,6 +17,19 @@ export const formatDate = toLocaleDateStringSupportsOptions
|
|||||||
formatDateMem(locale).format(dateObj)
|
formatDateMem(locale).format(dateObj)
|
||||||
: (dateObj: Date) => format(dateObj, "longDate");
|
: (dateObj: Date) => format(dateObj, "longDate");
|
||||||
|
|
||||||
|
const formatDateShortMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const formatDateShort = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateShortMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "shortDate");
|
||||||
|
|
||||||
const formatDateWeekdayMem = memoizeOne(
|
const formatDateWeekdayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
2
src/common/number/clamp.ts
Normal file
2
src/common/number/clamp.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const clamp = (value: number, min: number, max: number) =>
|
||||||
|
Math.min(Math.max(value, min), max);
|
@ -29,31 +29,28 @@ export const iconColorCSS = css`
|
|||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="climate"][data-state="cooling"] {
|
ha-icon[data-domain="climate"][data-state="cooling"] {
|
||||||
color: var(--cool-color, #2b9af9);
|
color: var(--cool-color, var(--state-climate-cool-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="climate"][data-state="heating"] {
|
ha-icon[data-domain="climate"][data-state="heating"] {
|
||||||
color: var(--heat-color, #ff8100);
|
color: var(--heat-color, var(--state-climate-heat-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="climate"][data-state="drying"] {
|
ha-icon[data-domain="climate"][data-state="drying"] {
|
||||||
color: var(--dry-color, #efbd07);
|
color: var(--dry-color, var(--state-climate-dry-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="alarm_control_panel"] {
|
ha-icon[data-domain="alarm_control_panel"] {
|
||||||
color: var(--alarm-color-armed, var(--label-badge-red));
|
color: var(--alarm-color-armed, var(--label-badge-red));
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
|
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
|
||||||
color: var(--alarm-color-disarmed, var(--label-badge-green));
|
color: var(--alarm-color-disarmed, var(--label-badge-green));
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||||
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
|
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
|
||||||
color: var(--alarm-color-pending, var(--label-badge-yellow));
|
color: var(--alarm-color-pending, var(--label-badge-yellow));
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
|
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||||
color: var(--alarm-color-triggered, var(--label-badge-red));
|
color: var(--alarm-color-triggered, var(--label-badge-red));
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
@ -73,11 +70,11 @@ export const iconColorCSS = css`
|
|||||||
|
|
||||||
ha-icon[data-domain="plant"][data-state="problem"],
|
ha-icon[data-domain="plant"][data-state="problem"],
|
||||||
ha-icon[data-domain="zwave"][data-state="dead"] {
|
ha-icon[data-domain="zwave"][data-state="dead"] {
|
||||||
color: var(--error-state-color, #db4437);
|
color: var(--state-icon-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color the icon if unavailable */
|
/* Color the icon if unavailable */
|
||||||
ha-icon[data-state="unavailable"] {
|
ha-icon[data-state="unavailable"] {
|
||||||
color: var(--state-icon-unavailable-color);
|
color: var(--state-unavailable-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -5,32 +5,20 @@
|
|||||||
// as much as it can, without ever going more than once per `wait` duration;
|
// as much as it can, without ever going more than once per `wait` duration;
|
||||||
// but if you'd like to disable the execution on the leading edge, pass
|
// but if you'd like to disable the execution on the leading edge, pass
|
||||||
// `false for leading`. To disable execution on the trailing edge, ditto.
|
// `false for leading`. To disable execution on the trailing edge, ditto.
|
||||||
export const throttle = <T extends (...args) => unknown>(
|
export const throttle = <T extends any[]>(
|
||||||
func: T,
|
func: (...args: T) => void,
|
||||||
wait: number,
|
wait: number,
|
||||||
leading = true,
|
leading = true,
|
||||||
trailing = true
|
trailing = true
|
||||||
): T => {
|
) => {
|
||||||
let timeout: number | undefined;
|
let timeout: number | undefined;
|
||||||
let previous = 0;
|
let previous = 0;
|
||||||
let context: any;
|
return (...args: T): void => {
|
||||||
let args: any;
|
const later = () => {
|
||||||
const later = () => {
|
previous = leading === false ? 0 : Date.now();
|
||||||
previous = leading === false ? 0 : Date.now();
|
timeout = undefined;
|
||||||
timeout = undefined;
|
func(...args);
|
||||||
func.apply(context, args);
|
};
|
||||||
if (!timeout) {
|
|
||||||
context = null;
|
|
||||||
args = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// @ts-ignore
|
|
||||||
return function (...argmnts) {
|
|
||||||
// @ts-ignore
|
|
||||||
// @typescript-eslint/no-this-alias
|
|
||||||
context = this;
|
|
||||||
args = argmnts;
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!previous && leading === false) {
|
if (!previous && leading === false) {
|
||||||
previous = now;
|
previous = now;
|
||||||
@ -42,7 +30,7 @@ export const throttle = <T extends (...args) => unknown>(
|
|||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
}
|
}
|
||||||
previous = now;
|
previous = now;
|
||||||
func.apply(context, args);
|
func(...args);
|
||||||
} else if (!timeout && trailing !== false) {
|
} else if (!timeout && trailing !== false) {
|
||||||
timeout = window.setTimeout(later, remaining);
|
timeout = window.setTimeout(later, remaining);
|
||||||
}
|
}
|
||||||
|
197
src/components/chart/chart-date-adapter.ts
Normal file
197
src/components/chart/chart-date-adapter.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import { _adapters } from "chart.js";
|
||||||
|
import {
|
||||||
|
startOfSecond,
|
||||||
|
startOfMinute,
|
||||||
|
startOfHour,
|
||||||
|
startOfDay,
|
||||||
|
startOfWeek,
|
||||||
|
startOfMonth,
|
||||||
|
startOfQuarter,
|
||||||
|
startOfYear,
|
||||||
|
addMilliseconds,
|
||||||
|
addSeconds,
|
||||||
|
addMinutes,
|
||||||
|
addHours,
|
||||||
|
addDays,
|
||||||
|
addWeeks,
|
||||||
|
addMonths,
|
||||||
|
addQuarters,
|
||||||
|
addYears,
|
||||||
|
differenceInMilliseconds,
|
||||||
|
differenceInSeconds,
|
||||||
|
differenceInMinutes,
|
||||||
|
differenceInHours,
|
||||||
|
differenceInDays,
|
||||||
|
differenceInWeeks,
|
||||||
|
differenceInMonths,
|
||||||
|
differenceInQuarters,
|
||||||
|
differenceInYears,
|
||||||
|
endOfSecond,
|
||||||
|
endOfMinute,
|
||||||
|
endOfHour,
|
||||||
|
endOfDay,
|
||||||
|
endOfWeek,
|
||||||
|
endOfMonth,
|
||||||
|
endOfQuarter,
|
||||||
|
endOfYear,
|
||||||
|
} from "date-fns";
|
||||||
|
import { formatDate, formatDateShort } from "../../common/datetime/format_date";
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatDateTimeWithSeconds,
|
||||||
|
} from "../../common/datetime/format_date_time";
|
||||||
|
import {
|
||||||
|
formatTime,
|
||||||
|
formatTimeWithSeconds,
|
||||||
|
} from "../../common/datetime/format_time";
|
||||||
|
|
||||||
|
const FORMATS = {
|
||||||
|
datetime: "datetime",
|
||||||
|
datetimeseconds: "datetimeseconds",
|
||||||
|
millisecond: "millisecond",
|
||||||
|
second: "second",
|
||||||
|
minute: "minute",
|
||||||
|
hour: "hour",
|
||||||
|
day: "day",
|
||||||
|
week: "week",
|
||||||
|
month: "month",
|
||||||
|
quarter: "quarter",
|
||||||
|
year: "year",
|
||||||
|
};
|
||||||
|
|
||||||
|
_adapters._date.override({
|
||||||
|
formats: () => FORMATS,
|
||||||
|
parse: (value: Date | number) => {
|
||||||
|
if (!(value instanceof Date)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.getTime();
|
||||||
|
},
|
||||||
|
format: function (time, fmt: keyof typeof FORMATS) {
|
||||||
|
switch (fmt) {
|
||||||
|
case "datetime":
|
||||||
|
return formatDateTime(new Date(time), this.options.locale);
|
||||||
|
case "datetimeseconds":
|
||||||
|
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
|
||||||
|
case "millisecond":
|
||||||
|
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
||||||
|
case "second":
|
||||||
|
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
||||||
|
case "minute":
|
||||||
|
return formatTime(new Date(time), this.options.locale);
|
||||||
|
case "hour":
|
||||||
|
return formatTime(new Date(time), this.options.locale);
|
||||||
|
case "day":
|
||||||
|
return formatDateShort(new Date(time), this.options.locale);
|
||||||
|
case "week":
|
||||||
|
return formatDate(new Date(time), this.options.locale);
|
||||||
|
case "month":
|
||||||
|
return formatDate(new Date(time), this.options.locale);
|
||||||
|
case "quarter":
|
||||||
|
return formatDate(new Date(time), this.options.locale);
|
||||||
|
case "year":
|
||||||
|
return formatDate(new Date(time), this.options.locale);
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
add: (time, amount, unit) => {
|
||||||
|
switch (unit) {
|
||||||
|
case "millisecond":
|
||||||
|
return addMilliseconds(time, amount);
|
||||||
|
case "second":
|
||||||
|
return addSeconds(time, amount);
|
||||||
|
case "minute":
|
||||||
|
return addMinutes(time, amount);
|
||||||
|
case "hour":
|
||||||
|
return addHours(time, amount);
|
||||||
|
case "day":
|
||||||
|
return addDays(time, amount);
|
||||||
|
case "week":
|
||||||
|
return addWeeks(time, amount);
|
||||||
|
case "month":
|
||||||
|
return addMonths(time, amount);
|
||||||
|
case "quarter":
|
||||||
|
return addQuarters(time, amount);
|
||||||
|
case "year":
|
||||||
|
return addYears(time, amount);
|
||||||
|
default:
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
diff: (max, min, unit) => {
|
||||||
|
switch (unit) {
|
||||||
|
case "millisecond":
|
||||||
|
return differenceInMilliseconds(max, min);
|
||||||
|
case "second":
|
||||||
|
return differenceInSeconds(max, min);
|
||||||
|
case "minute":
|
||||||
|
return differenceInMinutes(max, min);
|
||||||
|
case "hour":
|
||||||
|
return differenceInHours(max, min);
|
||||||
|
case "day":
|
||||||
|
return differenceInDays(max, min);
|
||||||
|
case "week":
|
||||||
|
return differenceInWeeks(max, min);
|
||||||
|
case "month":
|
||||||
|
return differenceInMonths(max, min);
|
||||||
|
case "quarter":
|
||||||
|
return differenceInQuarters(max, min);
|
||||||
|
case "year":
|
||||||
|
return differenceInYears(max, min);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
startOf: (time, unit, weekday) => {
|
||||||
|
switch (unit) {
|
||||||
|
case "second":
|
||||||
|
return startOfSecond(time);
|
||||||
|
case "minute":
|
||||||
|
return startOfMinute(time);
|
||||||
|
case "hour":
|
||||||
|
return startOfHour(time);
|
||||||
|
case "day":
|
||||||
|
return startOfDay(time);
|
||||||
|
case "week":
|
||||||
|
return startOfWeek(time);
|
||||||
|
case "isoWeek":
|
||||||
|
return startOfWeek(time, {
|
||||||
|
weekStartsOn: +weekday! as 0 | 1 | 2 | 3 | 4 | 5 | 6,
|
||||||
|
});
|
||||||
|
case "month":
|
||||||
|
return startOfMonth(time);
|
||||||
|
case "quarter":
|
||||||
|
return startOfQuarter(time);
|
||||||
|
case "year":
|
||||||
|
return startOfYear(time);
|
||||||
|
default:
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
endOf: (time, unit) => {
|
||||||
|
switch (unit) {
|
||||||
|
case "second":
|
||||||
|
return endOfSecond(time);
|
||||||
|
case "minute":
|
||||||
|
return endOfMinute(time);
|
||||||
|
case "hour":
|
||||||
|
return endOfHour(time);
|
||||||
|
case "day":
|
||||||
|
return endOfDay(time);
|
||||||
|
case "week":
|
||||||
|
return endOfWeek(time);
|
||||||
|
case "month":
|
||||||
|
return endOfMonth(time);
|
||||||
|
case "quarter":
|
||||||
|
return endOfQuarter(time);
|
||||||
|
case "year":
|
||||||
|
return endOfYear(time);
|
||||||
|
default:
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
229
src/components/chart/ha-chart-base.ts
Normal file
229
src/components/chart/ha-chart-base.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import type {
|
||||||
|
Chart,
|
||||||
|
ChartType,
|
||||||
|
ChartData,
|
||||||
|
ChartOptions,
|
||||||
|
TooltipModel,
|
||||||
|
} from "chart.js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { clamp } from "../../common/number/clamp";
|
||||||
|
|
||||||
|
@customElement("ha-chart-base")
|
||||||
|
export default class HaChartBase extends LitElement {
|
||||||
|
public chart?: Chart;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public chartType: ChartType = "line";
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public data: ChartData = { datasets: [] };
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public options?: ChartOptions;
|
||||||
|
|
||||||
|
@state() private _tooltip?: TooltipModel<any>;
|
||||||
|
|
||||||
|
@state() private _height?: string;
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
this._setupChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this.hasUpdated || !this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("type")) {
|
||||||
|
this.chart.config.type = this.chartType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("data")) {
|
||||||
|
this.chart.data = this.data;
|
||||||
|
}
|
||||||
|
if (changedProps.has("options")) {
|
||||||
|
this.chart.options = this._createOptions();
|
||||||
|
}
|
||||||
|
this.chart.update("none");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="chartContainer"
|
||||||
|
style=${styleMap({
|
||||||
|
height:
|
||||||
|
this.chartType === "timeline"
|
||||||
|
? `${this.data.datasets.length * 30 + 30}px`
|
||||||
|
: this._height,
|
||||||
|
overflow: this._height ? "initial" : "hidden",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<canvas></canvas>
|
||||||
|
${this._tooltip
|
||||||
|
? html`<div
|
||||||
|
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
|
||||||
|
style=${styleMap({
|
||||||
|
left:
|
||||||
|
clamp(this._tooltip.caretX, 100, this.clientWidth - 100) +
|
||||||
|
"px",
|
||||||
|
top: this._tooltip.caretY + "px",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div class="title">${this._tooltip.title}</div>
|
||||||
|
${this._tooltip.beforeBody
|
||||||
|
? html`<div class="beforeBody">
|
||||||
|
${this._tooltip.beforeBody}
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
${this._tooltip.body.map(
|
||||||
|
(item, i) => html`<li>
|
||||||
|
<em
|
||||||
|
style=${styleMap({
|
||||||
|
backgroundColor: this._tooltip!.labelColors[i]
|
||||||
|
.backgroundColor as string,
|
||||||
|
borderColor: this._tooltip!.labelColors[i]
|
||||||
|
.borderColor as string,
|
||||||
|
})}
|
||||||
|
></em
|
||||||
|
>${item.lines.join("\n")}
|
||||||
|
</li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _setupChart() {
|
||||||
|
const ctx: CanvasRenderingContext2D = this.renderRoot
|
||||||
|
.querySelector("canvas")!
|
||||||
|
.getContext("2d")!;
|
||||||
|
|
||||||
|
this.chart = new (await import("../../resources/chartjs")).Chart(ctx, {
|
||||||
|
type: this.chartType,
|
||||||
|
data: this.data,
|
||||||
|
options: this._createOptions(),
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
id: "afterRenderHook",
|
||||||
|
afterRender: (chart) => {
|
||||||
|
this._height = `${chart.height}px`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createOptions() {
|
||||||
|
return {
|
||||||
|
...this.options,
|
||||||
|
plugins: {
|
||||||
|
...this.options?.plugins,
|
||||||
|
tooltip: {
|
||||||
|
...this.options?.plugins?.tooltip,
|
||||||
|
enabled: false,
|
||||||
|
external: (context) => this._handleTooltip(context),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTooltip(context: {
|
||||||
|
chart: Chart;
|
||||||
|
tooltip: TooltipModel<any>;
|
||||||
|
}) {
|
||||||
|
if (context.tooltip.opacity === 0) {
|
||||||
|
this._tooltip = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._tooltip = { ...context.tooltip };
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateChart = (): void => {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.chartContainer {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.chartTooltip {
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 90%;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(80, 80, 80, 0.9);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, 12px);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 200px;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
:host([rtl]) .chartTooltip {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
.chartTooltip ul {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0px;
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.chartTooltip ul {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
.chartTooltip li {
|
||||||
|
display: block;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.chartTooltip li::first-line {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.chartTooltip .title {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.chartTooltip .beforeBody {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.chartTooltip em {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 4px;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
:host([rtl]) .chartTooltip em {
|
||||||
|
margin-right: inherit;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-chart-base": HaChartBase;
|
||||||
|
}
|
||||||
|
}
|
392
src/components/chart/state-history-chart-line.ts
Normal file
392
src/components/chart/state-history-chart-line.ts
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
|
import { html, LitElement, PropertyValues } from "lit";
|
||||||
|
import { property, state } from "lit/decorators";
|
||||||
|
import { getColorByIndex } from "../../common/color/colors";
|
||||||
|
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "./ha-chart-base";
|
||||||
|
|
||||||
|
class StateHistoryChartLine extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public names = false;
|
||||||
|
|
||||||
|
@property() public unit?: string;
|
||||||
|
|
||||||
|
@property() public identifier?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isSingleDevice = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public endTime?: Date;
|
||||||
|
|
||||||
|
@state() private _chartData?: ChartData<"line">;
|
||||||
|
|
||||||
|
@state() private _chartOptions?: ChartOptions<"line">;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-chart-base
|
||||||
|
.data=${this._chartData}
|
||||||
|
.options=${this._chartOptions}
|
||||||
|
chartType="line"
|
||||||
|
></ha-chart-base>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._chartOptions = {
|
||||||
|
parsing: false,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: "time",
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: this.hass.locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 0,
|
||||||
|
sampleSize: 5,
|
||||||
|
autoSkipPadding: 20,
|
||||||
|
major: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
font: (context) =>
|
||||||
|
context.tick && context.tick.major
|
||||||
|
? ({ weight: "bold" } as any)
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
tooltipFormat: "datetimeseconds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 7,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: this.unit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "nearest",
|
||||||
|
callbacks: {
|
||||||
|
label: (context) =>
|
||||||
|
`${context.dataset.label}: ${context.parsed.y} ${this.unit}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filler: {
|
||||||
|
propagate: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: !this.isSingleDevice,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: "nearest",
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
tension: 0.1,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
},
|
||||||
|
point: {
|
||||||
|
hitRadius: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (changedProps.has("data")) {
|
||||||
|
this._generateData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateData() {
|
||||||
|
let colorIndex = 0;
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
const deviceStates = this.data;
|
||||||
|
const datasets: ChartDataset<"line">[] = [];
|
||||||
|
let endTime: Date;
|
||||||
|
|
||||||
|
if (deviceStates.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParseFloat(value) {
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
return isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
endTime =
|
||||||
|
this.endTime ||
|
||||||
|
// Get the highest date from the last date of each device
|
||||||
|
new Date(
|
||||||
|
Math.max.apply(
|
||||||
|
null,
|
||||||
|
deviceStates.map((devSts) =>
|
||||||
|
new Date(
|
||||||
|
devSts.states[devSts.states.length - 1].last_changed
|
||||||
|
).getMilliseconds()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (endTime > new Date()) {
|
||||||
|
endTime = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = this.names || {};
|
||||||
|
deviceStates.forEach((states) => {
|
||||||
|
const domain = states.domain;
|
||||||
|
const name = names[states.entity_id] || states.name;
|
||||||
|
// array containing [value1, value2, etc]
|
||||||
|
let prevValues: any[] | null = null;
|
||||||
|
|
||||||
|
const data: ChartDataset<"line">[] = [];
|
||||||
|
|
||||||
|
const pushData = (timestamp: Date, datavalues: any[] | null) => {
|
||||||
|
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) => {
|
||||||
|
if (datavalues[i] === null && prevValues && prevValues[i] !== null) {
|
||||||
|
// null data values show up as gaps in the chart.
|
||||||
|
// If the current value for the dataset is null and the previous
|
||||||
|
// value of the data set is not null, then add an 'end' point
|
||||||
|
// to the chart for the previous value. Otherwise the gap will
|
||||||
|
// be too big. It will go from the start of the previous data
|
||||||
|
// value until the start of the next data value.
|
||||||
|
d.data.push({ x: timestamp.getTime(), y: prevValues[i] });
|
||||||
|
}
|
||||||
|
d.data.push({ x: timestamp.getTime(), y: datavalues[i] });
|
||||||
|
});
|
||||||
|
prevValues = datavalues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDataSet = (
|
||||||
|
nameY: string,
|
||||||
|
step = false,
|
||||||
|
fill = false,
|
||||||
|
color?: string
|
||||||
|
) => {
|
||||||
|
if (!color) {
|
||||||
|
color = getColorByIndex(colorIndex);
|
||||||
|
colorIndex++;
|
||||||
|
}
|
||||||
|
data.push({
|
||||||
|
label: nameY,
|
||||||
|
fill: fill ? "origin" : false,
|
||||||
|
borderColor: color,
|
||||||
|
backgroundColor: color + "7F",
|
||||||
|
stepped: step ? "before" : false,
|
||||||
|
pointRadius: 0,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
domain === "thermostat" ||
|
||||||
|
domain === "climate" ||
|
||||||
|
domain === "water_heater"
|
||||||
|
) {
|
||||||
|
const hasHvacAction = states.states.some(
|
||||||
|
(entityState) => entityState.attributes?.hvac_action
|
||||||
|
);
|
||||||
|
|
||||||
|
const isHeating =
|
||||||
|
domain === "climate" && hasHvacAction
|
||||||
|
? (entityState: LineChartState) =>
|
||||||
|
entityState.attributes?.hvac_action === "heating"
|
||||||
|
: (entityState: LineChartState) => entityState.state === "heat";
|
||||||
|
const isCooling =
|
||||||
|
domain === "climate" && hasHvacAction
|
||||||
|
? (entityState: LineChartState) =>
|
||||||
|
entityState.attributes?.hvac_action === "cooling"
|
||||||
|
: (entityState: LineChartState) => entityState.state === "cool";
|
||||||
|
|
||||||
|
const hasHeat = states.states.some(isHeating);
|
||||||
|
const hasCool = states.states.some(isCooling);
|
||||||
|
// 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(
|
||||||
|
(entityState) =>
|
||||||
|
entityState.attributes &&
|
||||||
|
entityState.attributes.target_temp_high !==
|
||||||
|
entityState.attributes.target_temp_low
|
||||||
|
);
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.current_temperature", {
|
||||||
|
name: name,
|
||||||
|
})}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (hasHeat) {
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
computedStyles.getPropertyValue("--state-climate-heat-color")
|
||||||
|
);
|
||||||
|
// The "heating" series uses steppedArea to shade the area below the current
|
||||||
|
// temperature when the thermostat is calling for heat.
|
||||||
|
}
|
||||||
|
if (hasCool) {
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
computedStyles.getPropertyValue("--state-climate-cool-color")
|
||||||
|
);
|
||||||
|
// The "cooling" series uses steppedArea to shade the area below the current
|
||||||
|
// temperature when the thermostat is calling for heat.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTargetRange) {
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||||
|
name: name,
|
||||||
|
mode: this.hass.localize("ui.card.climate.high"),
|
||||||
|
})}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||||
|
name: name,
|
||||||
|
mode: this.hass.localize("ui.card.climate.low"),
|
||||||
|
})}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
||||||
|
name: name,
|
||||||
|
})}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
states.states.forEach((entityState) => {
|
||||||
|
if (!entityState.attributes) return;
|
||||||
|
const curTemp = safeParseFloat(
|
||||||
|
entityState.attributes.current_temperature
|
||||||
|
);
|
||||||
|
const series = [curTemp];
|
||||||
|
if (hasHeat) {
|
||||||
|
series.push(isHeating(entityState) ? curTemp : null);
|
||||||
|
}
|
||||||
|
if (hasCool) {
|
||||||
|
series.push(isCooling(entityState) ? curTemp : null);
|
||||||
|
}
|
||||||
|
if (hasTargetRange) {
|
||||||
|
const targetHigh = safeParseFloat(
|
||||||
|
entityState.attributes.target_temp_high
|
||||||
|
);
|
||||||
|
const targetLow = safeParseFloat(
|
||||||
|
entityState.attributes.target_temp_low
|
||||||
|
);
|
||||||
|
series.push(targetHigh, targetLow);
|
||||||
|
pushData(new Date(entityState.last_changed), series);
|
||||||
|
} else {
|
||||||
|
const target = safeParseFloat(entityState.attributes.temperature);
|
||||||
|
series.push(target);
|
||||||
|
pushData(new Date(entityState.last_changed), series);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (domain === "humidifier") {
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||||
|
name: name,
|
||||||
|
})}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
addDataSet(
|
||||||
|
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
||||||
|
name: name,
|
||||||
|
})}`,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
states.states.forEach((entityState) => {
|
||||||
|
if (!entityState.attributes) return;
|
||||||
|
const target = safeParseFloat(entityState.attributes.humidity);
|
||||||
|
const series = [target];
|
||||||
|
series.push(entityState.state === "on" ? target : null);
|
||||||
|
pushData(new Date(entityState.last_changed), series);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Only disable interpolation for sensors
|
||||||
|
const isStep = domain === "sensor";
|
||||||
|
addDataSet(name, isStep);
|
||||||
|
|
||||||
|
let lastValue: number;
|
||||||
|
let lastDate: Date;
|
||||||
|
let lastNullDate: Date | null = null;
|
||||||
|
|
||||||
|
// Process chart data.
|
||||||
|
// When state is `unknown`, calculate the value and break the line.
|
||||||
|
states.states.forEach((entityState) => {
|
||||||
|
const value = safeParseFloat(entityState.state);
|
||||||
|
const date = new Date(entityState.last_changed);
|
||||||
|
if (value !== null && lastNullDate) {
|
||||||
|
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 !== undefined
|
||||||
|
) {
|
||||||
|
lastNullDate = date;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an entry for final values
|
||||||
|
pushData(endTime, prevValues);
|
||||||
|
|
||||||
|
// Concat two arrays
|
||||||
|
Array.prototype.push.apply(datasets, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._chartData = {
|
||||||
|
datasets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"state-history-chart-line": StateHistoryChartLine;
|
||||||
|
}
|
||||||
|
}
|
310
src/components/chart/state-history-chart-timeline.ts
Normal file
310
src/components/chart/state-history-chart-timeline.ts
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { html, LitElement, PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { getColorByIndex } from "../../common/color/colors";
|
||||||
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { TimelineEntity } from "../../data/history";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "./ha-chart-base";
|
||||||
|
import type { TimeLineData } from "./timeline-chart/const";
|
||||||
|
|
||||||
|
/** Binary sensor device classes for which the static colors for on/off need to be inverted.
|
||||||
|
* List the ones were "off" = good or normal state = should be rendered "green".
|
||||||
|
*/
|
||||||
|
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
|
||||||
|
"battery",
|
||||||
|
"door",
|
||||||
|
"garage_door",
|
||||||
|
"gas",
|
||||||
|
"lock",
|
||||||
|
"opening",
|
||||||
|
"problem",
|
||||||
|
"safety",
|
||||||
|
"smoke",
|
||||||
|
"window",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const STATIC_STATE_COLORS = new Set([
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"home",
|
||||||
|
"not_home",
|
||||||
|
"unavailable",
|
||||||
|
"unknown",
|
||||||
|
"idle",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stateColorMap: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
let colorIndex = 0;
|
||||||
|
|
||||||
|
const invertOnOff = (entityState?: HassEntity) =>
|
||||||
|
entityState &&
|
||||||
|
computeDomain(entityState.entity_id) === "binary_sensor" &&
|
||||||
|
"device_class" in entityState.attributes &&
|
||||||
|
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
|
||||||
|
entityState.attributes.device_class!
|
||||||
|
);
|
||||||
|
|
||||||
|
const getColor = (
|
||||||
|
stateString: string,
|
||||||
|
entityState: HassEntity,
|
||||||
|
computedStyles: CSSStyleDeclaration
|
||||||
|
) => {
|
||||||
|
if (invertOnOff(entityState)) {
|
||||||
|
stateString = stateString === "on" ? "off" : "on";
|
||||||
|
}
|
||||||
|
if (stateColorMap.has(stateString)) {
|
||||||
|
return stateColorMap.get(stateString);
|
||||||
|
}
|
||||||
|
if (STATIC_STATE_COLORS.has(stateString)) {
|
||||||
|
const color = computedStyles.getPropertyValue(
|
||||||
|
`--state-${stateString}-color`
|
||||||
|
);
|
||||||
|
stateColorMap.set(stateString, color);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
const color = getColorByIndex(colorIndex);
|
||||||
|
colorIndex++;
|
||||||
|
stateColorMap.set(stateString, color);
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("state-history-chart-timeline")
|
||||||
|
export class StateHistoryChartTimeline extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public names = false;
|
||||||
|
|
||||||
|
@property() public unit?: string;
|
||||||
|
|
||||||
|
@property() public identifier?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isSingleDevice = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public endTime?: Date;
|
||||||
|
|
||||||
|
@state() private _chartData?: ChartData<"timeline">;
|
||||||
|
|
||||||
|
@state() private _chartOptions?: ChartOptions<"timeline">;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-chart-base
|
||||||
|
.data=${this._chartData}
|
||||||
|
.options=${this._chartOptions}
|
||||||
|
chartType="timeline"
|
||||||
|
></ha-chart-base>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._chartOptions = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
parsing: false,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: "timeline",
|
||||||
|
position: "bottom",
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: this.hass.locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
maxRotation: 0,
|
||||||
|
sampleSize: 5,
|
||||||
|
autoSkipPadding: 20,
|
||||||
|
major: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
font: (context) =>
|
||||||
|
context.tick && context.tick.major
|
||||||
|
? ({ weight: "bold" } as any)
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
offset: false,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
tooltipFormat: "datetimeseconds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: "category",
|
||||||
|
barThickness: 20,
|
||||||
|
offset: true,
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
drawBorder: false,
|
||||||
|
drawTicks: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: this.data.length !== 1,
|
||||||
|
},
|
||||||
|
afterSetDimensions: (y) => {
|
||||||
|
y.maxWidth = y.chart.width * 0.18;
|
||||||
|
},
|
||||||
|
position: computeRTL(this.hass) ? "right" : "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "nearest",
|
||||||
|
callbacks: {
|
||||||
|
title: (context) =>
|
||||||
|
context![0].chart!.data!.labels![
|
||||||
|
context[0].datasetIndex
|
||||||
|
] as string,
|
||||||
|
beforeBody: (context) => context[0].dataset.label || "",
|
||||||
|
label: (item) => {
|
||||||
|
const d = item.dataset.data[item.dataIndex] as TimeLineData;
|
||||||
|
return [
|
||||||
|
d.label || "",
|
||||||
|
formatDateTimeWithSeconds(d.start, this.hass.locale),
|
||||||
|
formatDateTimeWithSeconds(d.end, this.hass.locale),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
labelColor: (item) => ({
|
||||||
|
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
|
||||||
|
.color!,
|
||||||
|
backgroundColor: (item.dataset.data[
|
||||||
|
item.dataIndex
|
||||||
|
] as TimeLineData).color!,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filler: {
|
||||||
|
propagate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (changedProps.has("data")) {
|
||||||
|
this._generateData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateData() {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
let stateHistory = this.data;
|
||||||
|
|
||||||
|
if (!stateHistory) {
|
||||||
|
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 labels: string[] = [];
|
||||||
|
const datasets: ChartDataset<"timeline">[] = [];
|
||||||
|
const names = this.names || {};
|
||||||
|
// stateHistory is a list of lists of sorted state objects
|
||||||
|
stateHistory.forEach((stateInfo) => {
|
||||||
|
let newLastChanged: Date;
|
||||||
|
let prevState: string | null = null;
|
||||||
|
let locState: string | null = null;
|
||||||
|
let prevLastChanged = startTime;
|
||||||
|
const entityDisplay: string =
|
||||||
|
names[stateInfo.entity_id] || stateInfo.name;
|
||||||
|
|
||||||
|
const dataRow: TimeLineData[] = [];
|
||||||
|
stateInfo.data.forEach((entityState) => {
|
||||||
|
let newState: string | null = entityState.state;
|
||||||
|
const timeStamp = new Date(entityState.last_changed);
|
||||||
|
if (!newState) {
|
||||||
|
newState = null;
|
||||||
|
}
|
||||||
|
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 (prevState === null) {
|
||||||
|
prevState = newState;
|
||||||
|
locState = entityState.state_localize;
|
||||||
|
prevLastChanged = new Date(entityState.last_changed);
|
||||||
|
} else if (newState !== prevState) {
|
||||||
|
newLastChanged = new Date(entityState.last_changed);
|
||||||
|
|
||||||
|
dataRow.push({
|
||||||
|
start: prevLastChanged,
|
||||||
|
end: newLastChanged,
|
||||||
|
label: locState,
|
||||||
|
color: getColor(
|
||||||
|
prevState,
|
||||||
|
this.hass.states[stateInfo.entity_id],
|
||||||
|
computedStyles
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
prevState = newState;
|
||||||
|
locState = entityState.state_localize;
|
||||||
|
prevLastChanged = newLastChanged;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevState !== null) {
|
||||||
|
dataRow.push({
|
||||||
|
start: prevLastChanged,
|
||||||
|
end: endTime,
|
||||||
|
label: locState,
|
||||||
|
color: getColor(
|
||||||
|
prevState,
|
||||||
|
this.hass.states[stateInfo.entity_id],
|
||||||
|
computedStyles
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
datasets.push({
|
||||||
|
data: dataRow,
|
||||||
|
label: stateInfo.entity_id,
|
||||||
|
});
|
||||||
|
labels.push(entityDisplay);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._chartData = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: datasets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"state-history-chart-timeline": StateHistoryChartTimeline;
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,10 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { HistoryResult } from "../data/history";
|
import { HistoryResult } from "../../data/history";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-circular-progress";
|
import "../ha-circular-progress";
|
||||||
import "./state-history-chart-line";
|
import "./state-history-chart-line";
|
||||||
import "./state-history-chart-timeline";
|
import "./state-history-chart-timeline";
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class StateHistoryCharts extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public endTime?: Date;
|
@property({ attribute: false }) public endTime?: Date;
|
||||||
|
|
||||||
@property({ type: Boolean }) public upToNow = false;
|
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "no-single" }) public noSingle = false;
|
@property({ type: Boolean, attribute: "no-single" }) public noSingle = false;
|
||||||
|
|
||||||
@ -101,12 +101,12 @@ class StateHistoryCharts extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
/* height of single timeline chart = 58px */
|
/* height of single timeline chart = 60px */
|
||||||
min-height: 58px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
.info {
|
.info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 58px;
|
line-height: 60px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
18
src/components/chart/timeline-chart/const.ts
Normal file
18
src/components/chart/timeline-chart/const.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface TimeLineData {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
label?: string | null;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "chart.js" {
|
||||||
|
interface ChartTypeRegistry {
|
||||||
|
timeline: {
|
||||||
|
chartOptions: BarControllerChartOptions;
|
||||||
|
datasetOptions: BarControllerDatasetOptions;
|
||||||
|
defaultDataPoint: TimeLineData;
|
||||||
|
parsedDataType: any;
|
||||||
|
scales: "timeline";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
60
src/components/chart/timeline-chart/textbar-element.ts
Normal file
60
src/components/chart/timeline-chart/textbar-element.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { BarElement, BarOptions, BarProps } from "chart.js";
|
||||||
|
import { hex2rgb } from "../../../common/color/convert-color";
|
||||||
|
import { luminosity } from "../../../common/color/rgb";
|
||||||
|
|
||||||
|
export interface TextBarProps extends BarProps {
|
||||||
|
text?: string | null;
|
||||||
|
options?: Partial<TextBaroptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextBaroptions extends BarOptions {
|
||||||
|
textPad?: number;
|
||||||
|
textColor?: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextBarElement extends BarElement {
|
||||||
|
static id = "textbar";
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
super.draw(ctx);
|
||||||
|
const options = this.options as TextBaroptions;
|
||||||
|
const { x, y, base, width, text } = (this as BarElement<
|
||||||
|
TextBarProps,
|
||||||
|
TextBaroptions
|
||||||
|
>).getProps(["x", "y", "base", "width", "text"]);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
const textRect = ctx.measureText(text);
|
||||||
|
if (
|
||||||
|
textRect.width === 0 ||
|
||||||
|
textRect.width + (options.textPad || 4) + 2 > width
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const textColor =
|
||||||
|
options.textColor ||
|
||||||
|
(options.backgroundColor &&
|
||||||
|
(luminosity(hex2rgb(options.backgroundColor)) > 0.5 ? "#000" : "#fff"));
|
||||||
|
|
||||||
|
// ctx.font = "12px arial";
|
||||||
|
ctx.fillStyle = textColor;
|
||||||
|
ctx.lineWidth = 0;
|
||||||
|
ctx.strokeStyle = textColor;
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(
|
||||||
|
text,
|
||||||
|
x - width / 2 + (options.textPad || 4),
|
||||||
|
y + (base - y) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipPosition(useFinalPosition: boolean) {
|
||||||
|
const { x, y, base } = this.getProps(["x", "y", "base"], useFinalPosition);
|
||||||
|
return { x, y: y + (base - y) / 2 };
|
||||||
|
}
|
||||||
|
}
|
160
src/components/chart/timeline-chart/timeline-controller.ts
Normal file
160
src/components/chart/timeline-chart/timeline-controller.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { BarController, BarElement } from "chart.js";
|
||||||
|
import { TimeLineData } from "./const";
|
||||||
|
import { TextBarProps } from "./textbar-element";
|
||||||
|
|
||||||
|
function parseValue(entry, item, vScale, i) {
|
||||||
|
const startValue = vScale.parse(entry.start, i);
|
||||||
|
const endValue = vScale.parse(entry.end, i);
|
||||||
|
const min = Math.min(startValue, endValue);
|
||||||
|
const max = Math.max(startValue, endValue);
|
||||||
|
let barStart = min;
|
||||||
|
let barEnd = max;
|
||||||
|
|
||||||
|
if (Math.abs(min) > Math.abs(max)) {
|
||||||
|
barStart = max;
|
||||||
|
barEnd = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store `barEnd` (furthest away from origin) as parsed value,
|
||||||
|
// to make stacking straight forward
|
||||||
|
item[vScale.axis] = barEnd;
|
||||||
|
|
||||||
|
item._custom = {
|
||||||
|
barStart,
|
||||||
|
barEnd,
|
||||||
|
start: startValue,
|
||||||
|
end: endValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimelineController extends BarController {
|
||||||
|
static id = "timeline";
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
dataElementType: "textbar",
|
||||||
|
dataElementOptions: ["text", "textColor", "textPadding"],
|
||||||
|
elements: {
|
||||||
|
showText: true,
|
||||||
|
textPadding: 4,
|
||||||
|
minBarWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static overrides = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
parseObjectData(meta, data, start, count) {
|
||||||
|
const iScale = meta.iScale;
|
||||||
|
const vScale = meta.vScale;
|
||||||
|
const labels = iScale.getLabels();
|
||||||
|
const singleScale = iScale === vScale;
|
||||||
|
const parsed: any[] = [];
|
||||||
|
let i;
|
||||||
|
let ilen;
|
||||||
|
let item;
|
||||||
|
let entry;
|
||||||
|
|
||||||
|
for (i = start, ilen = start + count; i < ilen; ++i) {
|
||||||
|
entry = data[i];
|
||||||
|
item = {};
|
||||||
|
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
|
||||||
|
parsed.push(parseValue(entry, item, vScale, i));
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabelAndValue(index) {
|
||||||
|
const meta = this._cachedMeta;
|
||||||
|
const { vScale } = meta;
|
||||||
|
const data = this.getDataset().data[index] as TimeLineData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: vScale!.getLabelForValue(this.index) || "",
|
||||||
|
value: data.label || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElements(
|
||||||
|
bars: BarElement[],
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
|
||||||
|
) {
|
||||||
|
const vScale = this._cachedMeta.vScale!;
|
||||||
|
const iScale = this._cachedMeta.iScale!;
|
||||||
|
const dataset = this.getDataset();
|
||||||
|
|
||||||
|
const firstOpts = this.resolveDataElementOptions(start, mode);
|
||||||
|
const sharedOptions = this.getSharedOptions(firstOpts);
|
||||||
|
const includeOptions = this.includeOptions(mode, sharedOptions!);
|
||||||
|
|
||||||
|
const horizontal = vScale.isHorizontal();
|
||||||
|
|
||||||
|
this.updateSharedOptions(sharedOptions!, mode, firstOpts);
|
||||||
|
|
||||||
|
for (let index = start; index < start + count; index++) {
|
||||||
|
const data = dataset.data[index] as TimeLineData;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const y = vScale.getPixelForValue(this.index);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const xStart = iScale.getPixelForValue(data.start.getTime());
|
||||||
|
// @ts-ignore
|
||||||
|
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||||
|
const width = xEnd - xStart;
|
||||||
|
|
||||||
|
const height = 10;
|
||||||
|
|
||||||
|
const properties: TextBarProps = {
|
||||||
|
horizontal,
|
||||||
|
x: xStart + width / 2, // Center of the bar
|
||||||
|
y: y - height, // Top of bar
|
||||||
|
width,
|
||||||
|
height: 0,
|
||||||
|
base: y + height, // Bottom of bar,
|
||||||
|
// Text
|
||||||
|
text: data.label,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeOptions) {
|
||||||
|
properties.options =
|
||||||
|
sharedOptions || this.resolveDataElementOptions(index, mode);
|
||||||
|
|
||||||
|
properties.options = {
|
||||||
|
...properties.options,
|
||||||
|
backgroundColor: data.color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateElement(bars[index], index, properties as any, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHoverStyle(_element, _datasetIndex, _index) {
|
||||||
|
// this._setStyle(element, index, 'active', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHoverStyle(_element, _datasetIndex, _index) {
|
||||||
|
// this._setStyle(element, index, 'active', true);
|
||||||
|
}
|
||||||
|
}
|
55
src/components/chart/timeline-chart/timeline-scale.ts
Normal file
55
src/components/chart/timeline-chart/timeline-scale.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { TimeScale } from "chart.js";
|
||||||
|
import { TimeLineData } from "./const";
|
||||||
|
|
||||||
|
export class TimeLineScale extends TimeScale {
|
||||||
|
static id = "timeline";
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
position: "bottom",
|
||||||
|
tooltips: {
|
||||||
|
mode: "nearest",
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
autoSkip: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
determineDataLimits() {
|
||||||
|
const options = this.options;
|
||||||
|
// @ts-ignore
|
||||||
|
const adapter = this._adapter;
|
||||||
|
const unit = options.time.unit || "day";
|
||||||
|
let { min, max } = this.getUserBounds();
|
||||||
|
|
||||||
|
const chart = this.chart;
|
||||||
|
|
||||||
|
// Convert data to timestamps
|
||||||
|
chart.data.datasets.forEach((dataset, index) => {
|
||||||
|
if (!chart.isDatasetVisible(index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const data of dataset.data as TimeLineData[]) {
|
||||||
|
let timestamp0 = adapter.parse(data.start, this);
|
||||||
|
let timestamp1 = adapter.parse(data.end, this);
|
||||||
|
if (timestamp0 > timestamp1) {
|
||||||
|
[timestamp0, timestamp1] = [timestamp1, timestamp0];
|
||||||
|
}
|
||||||
|
if (min > timestamp0 && timestamp0) {
|
||||||
|
min = timestamp0;
|
||||||
|
}
|
||||||
|
if (max < timestamp1 && timestamp1) {
|
||||||
|
max = timestamp1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In case there is no valid min/max, var's use today limits
|
||||||
|
min =
|
||||||
|
isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
|
||||||
|
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
|
||||||
|
|
||||||
|
// Make sure that max is strictly higher than min (required by the lookup table)
|
||||||
|
this.min = Math.min(min, max - 1);
|
||||||
|
this.max = Math.max(min + 1, max);
|
||||||
|
}
|
||||||
|
}
|
@ -1,661 +0,0 @@
|
|||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
|
|
||||||
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
|
|
||||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
|
||||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { formatTime } from "../../common/datetime/format_time";
|
|
||||||
import "../ha-icon-button";
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
/* global Chart moment Color */
|
|
||||||
|
|
||||||
let scriptsLoaded = null;
|
|
||||||
|
|
||||||
class HaChartBase extends mixinBehaviors(
|
|
||||||
[IronResizableBehavior],
|
|
||||||
PolymerElement
|
|
||||||
) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.chartHeader {
|
|
||||||
padding: 6px 0 0 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.chartHeader > div {
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
.chartHeader > div.chartTitle {
|
|
||||||
padding-top: 8px;
|
|
||||||
flex: 0 0 0;
|
|
||||||
max-width: 30%;
|
|
||||||
}
|
|
||||||
.chartHeader > div.chartLegend {
|
|
||||||
flex: 1 1;
|
|
||||||
min-width: 70%;
|
|
||||||
}
|
|
||||||
:root {
|
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
.chartTooltip {
|
|
||||||
font-size: 90%;
|
|
||||||
opacity: 1;
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(80, 80, 80, 0.9);
|
|
||||||
color: white;
|
|
||||||
border-radius: 3px;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translate(-50%, 12px);
|
|
||||||
z-index: 1000;
|
|
||||||
width: 200px;
|
|
||||||
transition: opacity 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
:host([rtl]) .chartTooltip {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
.chartLegend ul,
|
|
||||||
.chartTooltip ul {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 0px;
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.chartTooltip ul {
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
.chartTooltip li {
|
|
||||||
display: block;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
.chartTooltip li::first-line {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
.chartTooltip .title {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.chartTooltip .beforeBody {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 300;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.chartLegend li {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 6px;
|
|
||||||
max-width: 49%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.chartLegend li:nth-child(odd):last-of-type {
|
|
||||||
/* Make last item take full width if it is odd-numbered. */
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.chartLegend li[data-hidden] {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
.chartLegend em,
|
|
||||||
.chartTooltip em {
|
|
||||||
border-radius: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
height: 10px;
|
|
||||||
margin-right: 4px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
:host([rtl]) .chartTooltip em {
|
|
||||||
margin-right: inherit;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
ha-icon-button {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
</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 on-click="_legendClick" data-hidden$="[[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>
|
|
||||||
<template is="dom-if" if="[[tooltip.beforeBody]]">
|
|
||||||
<div class="beforeBody">[[tooltip.beforeBody]]</div>
|
|
||||||
</template>
|
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get chart() {
|
|
||||||
return this._chart;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
data: Object,
|
|
||||||
identifier: String,
|
|
||||||
rendered: {
|
|
||||||
type: Boolean,
|
|
||||||
notify: true,
|
|
||||||
value: false,
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
metas: {
|
|
||||||
type: Array,
|
|
||||||
value: () => [],
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
type: Object,
|
|
||||||
value: () => ({
|
|
||||||
opacity: "0",
|
|
||||||
left: "0",
|
|
||||||
top: "0",
|
|
||||||
xPadding: "5",
|
|
||||||
yPadding: "3",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
unit: Object,
|
|
||||||
rtl: {
|
|
||||||
type: Boolean,
|
|
||||||
reflectToAttribute: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observers() {
|
|
||||||
return ["onPropsChange(data)"];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._isAttached = true;
|
|
||||||
this.onPropsChange();
|
|
||||||
this._resizeListener = () => {
|
|
||||||
this._debouncer = Debouncer.debounce(
|
|
||||||
this._debouncer,
|
|
||||||
timeOut.after(10),
|
|
||||||
() => {
|
|
||||||
if (this._isAttached) {
|
|
||||||
this.resizeChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof ResizeObserver === "function") {
|
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
|
||||||
entries.forEach(() => {
|
|
||||||
this._resizeListener();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.$.chartTarget);
|
|
||||||
} else {
|
|
||||||
this.addEventListener("iron-resize", this._resizeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scriptsLoaded === null) {
|
|
||||||
scriptsLoaded = import("../../resources/ha-chart-scripts.js");
|
|
||||||
}
|
|
||||||
scriptsLoaded.then((ChartModule) => {
|
|
||||||
this.ChartClass = ChartModule.default;
|
|
||||||
this.onPropsChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._isAttached = false;
|
|
||||||
if (this.resizeObserver) {
|
|
||||||
this.resizeObserver.unobserve(this.$.chartTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeEventListener("iron-resize", this._resizeListener);
|
|
||||||
|
|
||||||
if (this._resizeTimer !== undefined) {
|
|
||||||
clearInterval(this._resizeTimer);
|
|
||||||
this._resizeTimer = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPropsChange() {
|
|
||||||
if (!this._isAttached || !this.ChartClass || !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] || "" : "";
|
|
||||||
this.set(["tooltip", "title"], title);
|
|
||||||
|
|
||||||
if (tooltip.beforeBody) {
|
|
||||||
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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.tooltip = {
|
|
||||||
...this.tooltip,
|
|
||||||
opacity: 1,
|
|
||||||
left: `${positionX}px`,
|
|
||||||
top: `${positionY}px`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_legendClick(event) {
|
|
||||||
event = event || window.event;
|
|
||||||
event.stopPropagation();
|
|
||||||
let target = event.target || event.srcElement;
|
|
||||||
while (target.nodeName !== "LI") {
|
|
||||||
// user clicked child, find parent LI
|
|
||||||
target = target.parentElement;
|
|
||||||
}
|
|
||||||
const index = event.model.itemsIndex;
|
|
||||||
|
|
||||||
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;
|
|
||||||
// New data for old graph. Keep metadata.
|
|
||||||
const preserveVisibility =
|
|
||||||
this._oldIdentifier && this.identifier === this._oldIdentifier;
|
|
||||||
this._oldIdentifier = this.identifier;
|
|
||||||
this.set(
|
|
||||||
"metas",
|
|
||||||
this._chart.data.datasets.map((x, i) => ({
|
|
||||||
label: x.label,
|
|
||||||
color: x.color,
|
|
||||||
bgColor: x.backgroundColor,
|
|
||||||
hidden:
|
|
||||||
preserveVisibility && i < this.metas.length
|
|
||||||
? this.metas[i].hidden
|
|
||||||
: !chart.isDatasetVisible(i),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
let updateNeeded = false;
|
|
||||||
if (preserveVisibility) {
|
|
||||||
for (let i = 0; i < this.metas.length; i++) {
|
|
||||||
const meta = chart.getDatasetMeta(i);
|
|
||||||
if (!!meta.hidden !== !!this.metas[i].hidden) updateNeeded = true;
|
|
||||||
meta.hidden = this.metas[i].hidden ? true : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateNeeded) {
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
this.unit = this.data.unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
_formatTickValue(value, index, values) {
|
|
||||||
if (values.length === 0) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const date = new Date(values[index].value);
|
|
||||||
return formatTime(date, this.hass.locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const 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) {
|
|
||||||
this._chart.options.scales.yAxes[0].gridLines.display = data.length > 1;
|
|
||||||
} else if (this.data.legend === true) {
|
|
||||||
this._drawLegend();
|
|
||||||
}
|
|
||||||
this.resizeChart();
|
|
||||||
} else {
|
|
||||||
if (!data.datasets) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._customTooltips({ opacity: 0 });
|
|
||||||
const plugins = [{ afterRender: () => this._setRendered(true) }];
|
|
||||||
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);
|
|
||||||
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
|
|
||||||
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.elements.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 };
|
|
||||||
}
|
|
||||||
if (options.scales.yAxes[0].gridLines) {
|
|
||||||
options.scales.yAxes[0].gridLines.display = false;
|
|
||||||
} else {
|
|
||||||
options.scales.yAxes[0].gridLines = { display: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$.chartTarget.style.height = "50px";
|
|
||||||
} else {
|
|
||||||
this.$.chartTarget.style.height = "160px";
|
|
||||||
}
|
|
||||||
const chartData = {
|
|
||||||
type: this.data.type,
|
|
||||||
data: this.data.data,
|
|
||||||
options: options,
|
|
||||||
plugins: plugins,
|
|
||||||
};
|
|
||||||
// Async resize after dom update
|
|
||||||
this._chart = new this.ChartClass(ctx, chartData);
|
|
||||||
if (this.isTimeline !== true && this.data.legend === true) {
|
|
||||||
this._drawLegend();
|
|
||||||
}
|
|
||||||
this.resizeChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resizeChart() {
|
|
||||||
if (!this._chart) return;
|
|
||||||
// Chart not ready
|
|
||||||
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
|
|
||||||
const areaTop = this._chart.chartArea.top;
|
|
||||||
const areaBot = this._chart.chartArea.bottom;
|
|
||||||
const height1 = this._chart.canvas.clientHeight;
|
|
||||||
if (areaBot > 0) {
|
|
||||||
this._axisHeight = height1 - areaBot + areaTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._axisHeight) {
|
|
||||||
chartTarget.style.height = "50px";
|
|
||||||
this._chart.resize();
|
|
||||||
this.resizeChart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._axisHeight) {
|
|
||||||
const cnt = data.datasets.length;
|
|
||||||
const targetHeight = 30 * cnt + this._axisHeight + "px";
|
|
||||||
if (chartTarget.style.height !== targetHeight) {
|
|
||||||
chartTarget.style.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.
|
|
||||||
// Palette modified from http://google.github.io/palette.js/ mpn65, Apache 2.0
|
|
||||||
const palette = [
|
|
||||||
"ff0029",
|
|
||||||
"66a61e",
|
|
||||||
"377eb8",
|
|
||||||
"984ea3",
|
|
||||||
"00d2d5",
|
|
||||||
"ff7f00",
|
|
||||||
"af8d00",
|
|
||||||
"7f80cd",
|
|
||||||
"b3e900",
|
|
||||||
"c42e60",
|
|
||||||
"a65628",
|
|
||||||
"f781bf",
|
|
||||||
"8dd3c7",
|
|
||||||
"bebada",
|
|
||||||
"fb8072",
|
|
||||||
"80b1d3",
|
|
||||||
"fdb462",
|
|
||||||
"fccde5",
|
|
||||||
"bc80bd",
|
|
||||||
"ffed6f",
|
|
||||||
"c4eaff",
|
|
||||||
"cf8c00",
|
|
||||||
"1b9e77",
|
|
||||||
"d95f02",
|
|
||||||
"e7298a",
|
|
||||||
"e6ab02",
|
|
||||||
"a6761d",
|
|
||||||
"0097ff",
|
|
||||||
"00d067",
|
|
||||||
"f43600",
|
|
||||||
"4ba93b",
|
|
||||||
"5779bb",
|
|
||||||
"927acc",
|
|
||||||
"97ee3f",
|
|
||||||
"bf3947",
|
|
||||||
"9f5b00",
|
|
||||||
"f48758",
|
|
||||||
"8caed6",
|
|
||||||
"f2b94f",
|
|
||||||
"eff26e",
|
|
||||||
"e43872",
|
|
||||||
"d9b100",
|
|
||||||
"9d7a00",
|
|
||||||
"698cff",
|
|
||||||
"d9d9d9",
|
|
||||||
"00d27e",
|
|
||||||
"d06800",
|
|
||||||
"009f82",
|
|
||||||
"c49200",
|
|
||||||
"cbe8ff",
|
|
||||||
"fecddf",
|
|
||||||
"c27eb6",
|
|
||||||
"8cd2ce",
|
|
||||||
"c4b8d9",
|
|
||||||
"f883b0",
|
|
||||||
"a49100",
|
|
||||||
"f48800",
|
|
||||||
"27d0df",
|
|
||||||
"a04a9b",
|
|
||||||
];
|
|
||||||
function getColorIndex(idx) {
|
|
||||||
// Reuse the color if index too large.
|
|
||||||
return Color("#" + palette[idx % palette.length]);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
let name1 = name.toLowerCase();
|
|
||||||
if (ret === undefined) {
|
|
||||||
if (data[4]) {
|
|
||||||
// Invert on/off if data[4] is true. Required for some binary_sensor device classes
|
|
||||||
// (BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED) where "off" is the good (= green color) value.
|
|
||||||
name1 = name1 === "on" ? "off" : name1 === "off" ? "on" : name1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = colorDict[name1];
|
|
||||||
}
|
|
||||||
if (ret === undefined) {
|
|
||||||
ret = getColorIndex(colorIndex);
|
|
||||||
colorIndex++;
|
|
||||||
colorDict[name1] = ret;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
return getColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("ha-chart-base", HaChartBase);
|
|
@ -1,433 +0,0 @@
|
|||||||
import "@polymer/polymer/lib/utils/debounce";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import "./entity/ha-chart-base";
|
|
||||||
|
|
||||||
class StateHistoryChartLine extends LocalizeMixin(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"
|
|
||||||
hass="[[hass]]"
|
|
||||||
data="[[chartData]]"
|
|
||||||
identifier="[[identifier]]"
|
|
||||||
rendered="{{rendered}}"
|
|
||||||
></ha-chart-base>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
chartData: Object,
|
|
||||||
data: Object,
|
|
||||||
names: 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
// safari doesn't always render the canvas when we animate it, so we remove overflow hidden when the animation is complete
|
|
||||||
this.addEventListener("transitionend", () => {
|
|
||||||
this.style.overflow = "auto";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dataChanged() {
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRenderedChanged(rendered) {
|
|
||||||
if (rendered) {
|
|
||||||
this.animateHeight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animateHeight() {
|
|
||||||
requestAnimationFrame(() =>
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.style.height = this.$.chart.scrollHeight + "px";
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawChart() {
|
|
||||||
if (!this._isAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unit = this.unit;
|
|
||||||
const deviceStates = this.data;
|
|
||||||
const datasets = [];
|
|
||||||
let endTime;
|
|
||||||
|
|
||||||
if (deviceStates.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeParseFloat(value) {
|
|
||||||
const parsed = parseFloat(value);
|
|
||||||
return isFinite(parsed) ? parsed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
endTime =
|
|
||||||
this.endTime ||
|
|
||||||
// Get the highest date from the last date of each device
|
|
||||||
new Date(
|
|
||||||
Math.max.apply(
|
|
||||||
null,
|
|
||||||
deviceStates.map(
|
|
||||||
(devSts) =>
|
|
||||||
new Date(devSts.states[devSts.states.length - 1].last_changed)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (endTime > new Date()) {
|
|
||||||
endTime = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
const names = this.names || {};
|
|
||||||
deviceStates.forEach((states) => {
|
|
||||||
const domain = states.domain;
|
|
||||||
const name = names[states.entity_id] || 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) => {
|
|
||||||
if (datavalues[i] === null && prevValues && prevValues[i] !== null) {
|
|
||||||
// null data values show up as gaps in the chart.
|
|
||||||
// If the current value for the dataset is null and the previous
|
|
||||||
// value of the data set is not null, then add an 'end' point
|
|
||||||
// to the chart for the previous value. Otherwise the gap will
|
|
||||||
// be too big. It will go from the start of the previous data
|
|
||||||
// value until the start of the next data value.
|
|
||||||
d.data.push({ x: timestamp, y: prevValues[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" ||
|
|
||||||
domain === "water_heater"
|
|
||||||
) {
|
|
||||||
const hasHvacAction = states.states.some(
|
|
||||||
(state) => state.attributes && state.attributes.hvac_action
|
|
||||||
);
|
|
||||||
|
|
||||||
const isHeating =
|
|
||||||
domain === "climate" && hasHvacAction
|
|
||||||
? (state) => state.attributes.hvac_action === "heating"
|
|
||||||
: (state) => state.state === "heat";
|
|
||||||
const isCooling =
|
|
||||||
domain === "climate" && hasHvacAction
|
|
||||||
? (state) => state.attributes.hvac_action === "cooling"
|
|
||||||
: (state) => state.state === "cool";
|
|
||||||
|
|
||||||
const hasHeat = states.states.some(isHeating);
|
|
||||||
const hasCool = states.states.some(isCooling);
|
|
||||||
// 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
|
|
||||||
);
|
|
||||||
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.card.climate.current_temperature",
|
|
||||||
"name",
|
|
||||||
name
|
|
||||||
)}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (hasHeat) {
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize("ui.card.climate.heating", "name", name)}`,
|
|
||||||
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(
|
|
||||||
`${this.hass.localize("ui.card.climate.cooling", "name", name)}`,
|
|
||||||
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(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.card.climate.target_temperature_mode",
|
|
||||||
"name",
|
|
||||||
name,
|
|
||||||
"mode",
|
|
||||||
this.hass.localize("ui.card.climate.high")
|
|
||||||
)}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.card.climate.target_temperature_mode",
|
|
||||||
"name",
|
|
||||||
name,
|
|
||||||
"mode",
|
|
||||||
this.hass.localize("ui.card.climate.low")
|
|
||||||
)}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.card.climate.target_temperature_entity",
|
|
||||||
"name",
|
|
||||||
name
|
|
||||||
)}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
states.states.forEach((state) => {
|
|
||||||
if (!state.attributes) return;
|
|
||||||
const curTemp = safeParseFloat(state.attributes.current_temperature);
|
|
||||||
const series = [curTemp];
|
|
||||||
if (hasHeat) {
|
|
||||||
series.push(isHeating(state) ? curTemp : null);
|
|
||||||
}
|
|
||||||
if (hasCool) {
|
|
||||||
series.push(isCooling(state) ? 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 if (domain === "humidifier") {
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.card.humidifier.target_humidity_entity",
|
|
||||||
"name",
|
|
||||||
name
|
|
||||||
)}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
addColumn(
|
|
||||||
`${this.hass.localize("ui.card.humidifier.on_entity", "name", name)}`,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
states.states.forEach((state) => {
|
|
||||||
if (!state.attributes) return;
|
|
||||||
const target = safeParseFloat(state.attributes.humidity);
|
|
||||||
const series = [target];
|
|
||||||
series.push(state.state === "on" ? target : null);
|
|
||||||
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 = (items, data) => {
|
|
||||||
const item = items[0];
|
|
||||||
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
|
||||||
|
|
||||||
return formatDateTimeWithSeconds(date, this.hass.locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartOptions = {
|
|
||||||
type: "line",
|
|
||||||
unit: unit,
|
|
||||||
legend: !this.isSingleDevice,
|
|
||||||
options: {
|
|
||||||
scales: {
|
|
||||||
xAxes: [
|
|
||||||
{
|
|
||||||
type: "time",
|
|
||||||
ticks: {
|
|
||||||
major: {
|
|
||||||
fontStyle: "bold",
|
|
||||||
},
|
|
||||||
source: "auto",
|
|
||||||
sampleSize: 5,
|
|
||||||
autoSkipPadding: 20,
|
|
||||||
maxRotation: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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);
|
|
@ -1,286 +0,0 @@
|
|||||||
import "@polymer/polymer/lib/utils/debounce";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
|
||||||
import "./entity/ha-chart-base";
|
|
||||||
|
|
||||||
/** Binary sensor device classes for which the static colors for on/off need to be inverted.
|
|
||||||
* List the ones were "off" = good or normal state = should be rendered "green".
|
|
||||||
*/
|
|
||||||
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
|
|
||||||
"battery",
|
|
||||||
"door",
|
|
||||||
"garage_door",
|
|
||||||
"gas",
|
|
||||||
"lock",
|
|
||||||
"opening",
|
|
||||||
"problem",
|
|
||||||
"safety",
|
|
||||||
"smoke",
|
|
||||||
"window",
|
|
||||||
]);
|
|
||||||
|
|
||||||
class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
:host([rendered]) {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-chart-base {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<ha-chart-base
|
|
||||||
hass="[[hass]]"
|
|
||||||
data="[[chartData]]"
|
|
||||||
rendered="{{rendered}}"
|
|
||||||
rtl="{{rtl}}"
|
|
||||||
></ha-chart-base>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
chartData: Object,
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
observer: "dataChanged",
|
|
||||||
},
|
|
||||||
names: Object,
|
|
||||||
noSingle: Boolean,
|
|
||||||
endTime: Date,
|
|
||||||
rendered: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
reflectToAttribute: true,
|
|
||||||
},
|
|
||||||
rtl: {
|
|
||||||
reflectToAttribute: true,
|
|
||||||
computed: "_computeRTL(hass)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observers() {
|
|
||||||
return ["dataChanged(data, endTime, localize, language)"];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._isAttached = true;
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
dataChanged() {
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawChart() {
|
|
||||||
const staticColors = {
|
|
||||||
on: 1,
|
|
||||||
off: 0,
|
|
||||||
home: 1,
|
|
||||||
not_home: 0,
|
|
||||||
unavailable: "#a0a0a0",
|
|
||||||
unknown: "#606060",
|
|
||||||
idle: 2,
|
|
||||||
};
|
|
||||||
let stateHistory = this.data;
|
|
||||||
|
|
||||||
if (!this._isAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stateHistory) {
|
|
||||||
stateHistory = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = new Date(
|
|
||||||
stateHistory.reduce(
|
|
||||||
(minTime, stateInfo) =>
|
|
||||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed)),
|
|
||||||
new Date()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
),
|
|
||||||
startTime
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (endTime > new Date()) {
|
|
||||||
endTime = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = [];
|
|
||||||
const datasets = [];
|
|
||||||
// stateHistory is a list of lists of sorted state objects
|
|
||||||
const names = this.names || {};
|
|
||||||
stateHistory.forEach((stateInfo) => {
|
|
||||||
let newLastChanged;
|
|
||||||
let prevState = null;
|
|
||||||
let locState = null;
|
|
||||||
let prevLastChanged = startTime;
|
|
||||||
const entityDisplay = names[stateInfo.entity_id] || stateInfo.name;
|
|
||||||
|
|
||||||
const invertOnOff =
|
|
||||||
computeDomain(stateInfo.entity_id) === "binary_sensor" &&
|
|
||||||
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
|
|
||||||
this.hass.states[stateInfo.entity_id].attributes.device_class
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataRow = [];
|
|
||||||
stateInfo.data.forEach((state) => {
|
|
||||||
let newState = state.state;
|
|
||||||
const timeStamp = new Date(state.last_changed);
|
|
||||||
if (newState === undefined || newState === "") {
|
|
||||||
newState = null;
|
|
||||||
}
|
|
||||||
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 (prevState !== null && newState !== prevState) {
|
|
||||||
newLastChanged = new Date(state.last_changed);
|
|
||||||
|
|
||||||
dataRow.push([
|
|
||||||
prevLastChanged,
|
|
||||||
newLastChanged,
|
|
||||||
locState,
|
|
||||||
prevState,
|
|
||||||
invertOnOff,
|
|
||||||
]);
|
|
||||||
|
|
||||||
prevState = newState;
|
|
||||||
locState = state.state_localize;
|
|
||||||
prevLastChanged = newLastChanged;
|
|
||||||
} else if (prevState === null) {
|
|
||||||
prevState = newState;
|
|
||||||
locState = state.state_localize;
|
|
||||||
prevLastChanged = new Date(state.last_changed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prevState !== null) {
|
|
||||||
dataRow.push([
|
|
||||||
prevLastChanged,
|
|
||||||
endTime,
|
|
||||||
locState,
|
|
||||||
prevState,
|
|
||||||
invertOnOff,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
datasets.push({
|
|
||||||
data: dataRow,
|
|
||||||
entity_id: stateInfo.entity_id,
|
|
||||||
});
|
|
||||||
labels.push(entityDisplay);
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatTooltipLabel = (item, data) => {
|
|
||||||
const values = data.datasets[item.datasetIndex].data[item.index];
|
|
||||||
|
|
||||||
const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
|
|
||||||
const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
|
|
||||||
const state = values[2];
|
|
||||||
|
|
||||||
return [state, start, end];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTooltipBeforeBody = (item, data) => {
|
|
||||||
if (!this.hass.userData || !this.hass.userData.showAdvanced || !item[0]) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
// Extract the entity ID from the dataset.
|
|
||||||
const values = data.datasets[item[0].datasetIndex];
|
|
||||||
return values.entity_id || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartOptions = {
|
|
||||||
type: "timeline",
|
|
||||||
options: {
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: formatTooltipLabel,
|
|
||||||
beforeBody: formatTooltipBeforeBody,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
xAxes: [
|
|
||||||
{
|
|
||||||
ticks: {
|
|
||||||
major: {
|
|
||||||
fontStyle: "bold",
|
|
||||||
},
|
|
||||||
sampleSize: 5,
|
|
||||||
autoSkipPadding: 50,
|
|
||||||
maxRotation: 0,
|
|
||||||
},
|
|
||||||
categoryPercentage: undefined,
|
|
||||||
barPercentage: undefined,
|
|
||||||
time: {
|
|
||||||
format: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
yAxes: [
|
|
||||||
{
|
|
||||||
afterSetDimensions: (yaxe) => {
|
|
||||||
yaxe.maxWidth = yaxe.chart.width * 0.18;
|
|
||||||
},
|
|
||||||
position: this._computeRTL ? "right" : "left",
|
|
||||||
categoryPercentage: undefined,
|
|
||||||
barPercentage: undefined,
|
|
||||||
time: { format: undefined },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
datasets: {
|
|
||||||
categoryPercentage: 0.8,
|
|
||||||
barPercentage: 0.9,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: datasets,
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
staticColors: staticColors,
|
|
||||||
staticColorIndex: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.chartData = chartOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeRTL(hass) {
|
|
||||||
return computeRTL(hass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define(
|
|
||||||
"state-history-chart-timeline",
|
|
||||||
StateHistoryChartTimeline
|
|
||||||
);
|
|
@ -2,7 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { throttle } from "../../common/util/throttle";
|
import { throttle } from "../../common/util/throttle";
|
||||||
import "../../components/state-history-charts";
|
import "../../components/chart/state-history-charts";
|
||||||
import { getRecentWithCache } from "../../data/cached-history";
|
import { getRecentWithCache } from "../../data/cached-history";
|
||||||
import { HistoryResult } from "../../data/history";
|
import { HistoryResult } from "../../data/history";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
@ -4,7 +4,6 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
|||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { throttle } from "../../common/util/throttle";
|
import { throttle } from "../../common/util/throttle";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/state-history-charts";
|
|
||||||
import { fetchUsers } from "../../data/user";
|
import { fetchUsers } from "../../data/user";
|
||||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
import { getLogbookData, LogbookEntry } from "../../data/logbook";
|
||||||
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
import { loadTraceContexts, TraceContexts } from "../../data/trace";
|
||||||
|
@ -8,7 +8,7 @@ import "../../components/ha-circular-progress";
|
|||||||
import "../../components/ha-date-range-picker";
|
import "../../components/ha-date-range-picker";
|
||||||
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
|
||||||
import "../../components/ha-menu-button";
|
import "../../components/ha-menu-button";
|
||||||
import "../../components/state-history-charts";
|
import "../../components/chart/state-history-charts";
|
||||||
import { computeHistory, fetchDate } from "../../data/history";
|
import { computeHistory, fetchDate } from "../../data/history";
|
||||||
import "../../layouts/ha-app-layout";
|
import "../../layouts/ha-app-layout";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
|
@ -10,7 +10,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { throttle } from "../../../common/util/throttle";
|
import { throttle } from "../../../common/util/throttle";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/state-history-charts";
|
import "../../../components/chart/state-history-charts";
|
||||||
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history";
|
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history";
|
||||||
import { HistoryResult } from "../../../data/history";
|
import { HistoryResult } from "../../../data/history";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
@ -139,8 +139,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
|||||||
.isLoadingData=${!this._stateHistory}
|
.isLoadingData=${!this._stateHistory}
|
||||||
.historyData=${this._stateHistory}
|
.historyData=${this._stateHistory}
|
||||||
.names=${this._names}
|
.names=${this._names}
|
||||||
.upToNow=${true}
|
up-to-now
|
||||||
.noSingle=${true}
|
no-single
|
||||||
></state-history-charts>
|
></state-history-charts>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
@ -25,23 +25,10 @@ import "../../../components/map/ha-map";
|
|||||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||||
import type { HaMap, HaMapPaths } from "../../../components/map/ha-map";
|
import type { HaMap, HaMapPaths } from "../../../components/map/ha-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { getColorByIndex } from "../../../common/color/colors";
|
||||||
|
|
||||||
const MINUTE = 60000;
|
const MINUTE = 60000;
|
||||||
|
|
||||||
const COLORS = [
|
|
||||||
"#0288D1",
|
|
||||||
"#00AA00",
|
|
||||||
"#984ea3",
|
|
||||||
"#00d2d5",
|
|
||||||
"#ff7f00",
|
|
||||||
"#af8d00",
|
|
||||||
"#7f80cd",
|
|
||||||
"#b3e900",
|
|
||||||
"#c42e60",
|
|
||||||
"#a65628",
|
|
||||||
"#f781bf",
|
|
||||||
"#8dd3c7",
|
|
||||||
];
|
|
||||||
@customElement("hui-map-card")
|
@customElement("hui-map-card")
|
||||||
class HuiMapCard extends LitElement implements LovelaceCard {
|
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -225,7 +212,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
if (color) {
|
if (color) {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
color = COLORS[this._colorIndex % COLORS.length];
|
color = getColorByIndex(this._colorIndex);
|
||||||
this._colorIndex++;
|
this._colorIndex++;
|
||||||
this._colorDict[entityId] = color;
|
this._colorDict[entityId] = color;
|
||||||
return color;
|
return color;
|
||||||
|
@ -447,47 +447,37 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
|||||||
--name-font-size: 1.2rem;
|
--name-font-size: 1.2rem;
|
||||||
--brightness-font-size: 1.2rem;
|
--brightness-font-size: 1.2rem;
|
||||||
--rail-border-color: transparent;
|
--rail-border-color: transparent;
|
||||||
--auto-color: green;
|
|
||||||
--eco-color: springgreen;
|
|
||||||
--cool-color: #2b9af9;
|
|
||||||
--heat-color: #ff8100;
|
|
||||||
--manual-color: #44739e;
|
|
||||||
--off-color: #8a8a8a;
|
|
||||||
--fan_only-color: #8a8a8a;
|
|
||||||
--dry-color: #efbd07;
|
|
||||||
--idle-color: #8a8a8a;
|
|
||||||
--unknown-color: #bac;
|
|
||||||
}
|
}
|
||||||
.auto,
|
.auto,
|
||||||
.heat_cool {
|
.heat_cool {
|
||||||
--mode-color: var(--auto-color);
|
--mode-color: var(--state-climate-auto-color);
|
||||||
}
|
}
|
||||||
.cool {
|
.cool {
|
||||||
--mode-color: var(--cool-color);
|
--mode-color: var(--state-climate-cool-color);
|
||||||
}
|
}
|
||||||
.heat {
|
.heat {
|
||||||
--mode-color: var(--heat-color);
|
--mode-color: var(--state-climate-heat-color);
|
||||||
}
|
}
|
||||||
.manual {
|
.manual {
|
||||||
--mode-color: var(--manual-color);
|
--mode-color: var(--state-climate-manual-color);
|
||||||
}
|
}
|
||||||
.off {
|
.off {
|
||||||
--mode-color: var(--off-color);
|
--mode-color: var(--state-climate-off-color);
|
||||||
}
|
}
|
||||||
.fan_only {
|
.fan_only {
|
||||||
--mode-color: var(--fan_only-color);
|
--mode-color: var(--state-climate-fan_only-color);
|
||||||
}
|
}
|
||||||
.eco {
|
.eco {
|
||||||
--mode-color: var(--eco-color);
|
--mode-color: var(--state-climate-eco-color);
|
||||||
}
|
}
|
||||||
.dry {
|
.dry {
|
||||||
--mode-color: var(--dry-color);
|
--mode-color: var(--state-climate-dry-color);
|
||||||
}
|
}
|
||||||
.idle {
|
.idle {
|
||||||
--mode-color: var(--idle-color);
|
--mode-color: var(--state-climate-idle-color);
|
||||||
}
|
}
|
||||||
.unknown-mode {
|
.unknown-mode {
|
||||||
--mode-color: var(--unknown-color);
|
--mode-color: var(--state-unknown-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-info {
|
.more-info {
|
||||||
|
35
src/resources/chartjs.ts
Normal file
35
src/resources/chartjs.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
LineController,
|
||||||
|
TimeScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
CategoryScale,
|
||||||
|
Chart,
|
||||||
|
} from "chart.js";
|
||||||
|
import { TextBarElement } from "../components/chart/timeline-chart/textbar-element";
|
||||||
|
import { TimelineController } from "../components/chart/timeline-chart/timeline-controller";
|
||||||
|
import { TimeLineScale } from "../components/chart/timeline-chart/timeline-scale";
|
||||||
|
import "../components/chart/chart-date-adapter";
|
||||||
|
|
||||||
|
export { Chart } from "chart.js";
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
Tooltip,
|
||||||
|
Title,
|
||||||
|
Legend,
|
||||||
|
Filler,
|
||||||
|
TimeScale,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
TextBarElement,
|
||||||
|
TimeLineScale,
|
||||||
|
TimelineController,
|
||||||
|
CategoryScale
|
||||||
|
);
|
@ -1,60 +0,0 @@
|
|||||||
import Chart from "chart.js";
|
|
||||||
import "chartjs-chart-timeline";
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Chart;
|
|
@ -42,10 +42,6 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--success-color: #0f9d58;
|
--success-color: #0f9d58;
|
||||||
--info-color: #4285f4;
|
--info-color: #4285f4;
|
||||||
|
|
||||||
/* states and badges */
|
|
||||||
--state-icon-color: #44739e;
|
|
||||||
--state-icon-active-color: #FDD835;
|
|
||||||
|
|
||||||
/* background and sidebar */
|
/* background and sidebar */
|
||||||
--card-background-color: #ffffff;
|
--card-background-color: #ffffff;
|
||||||
--primary-background-color: #fafafa;
|
--primary-background-color: #fafafa;
|
||||||
@ -60,6 +56,32 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--label-badge-green: #0DA035;
|
--label-badge-green: #0DA035;
|
||||||
--label-badge-yellow: #f4b400;
|
--label-badge-yellow: #f4b400;
|
||||||
|
|
||||||
|
/* states and badges */
|
||||||
|
--state-icon-color: #44739e;
|
||||||
|
/* an active state is anything that would require attention */
|
||||||
|
--state-icon-active-color: #FDD835;
|
||||||
|
/* an error state is anything that would be considered an error */
|
||||||
|
/* --state-icon-error-color: #db4437; derived from error-color */
|
||||||
|
|
||||||
|
--state-on-color: #66a61e;
|
||||||
|
--state-off-color: #ff0029;
|
||||||
|
--state-home-color: #66a61e;
|
||||||
|
--state-not_home-color: #ff0029;
|
||||||
|
/* --state-unavailable-color: #a0a0a0; derived from disabled-text-color */
|
||||||
|
--state-unknown-color: #606060;
|
||||||
|
--state-idle-color: #377eb8;
|
||||||
|
|
||||||
|
/* climate state colors */
|
||||||
|
--state-climate-auto-color: #008000;
|
||||||
|
--state-climate-eco-color: #00ff7f;
|
||||||
|
--state-climate-cool-color: #2b9af9;
|
||||||
|
--state-climate-heat-color: #ff8100;
|
||||||
|
--state-climate-manual-color: #44739e;
|
||||||
|
--state-climate-off-color: #8a8a8a;
|
||||||
|
--state-climate-fan_only-color: #8a8a8a;
|
||||||
|
--state-climate-dry-color: #efbd07;
|
||||||
|
--state-climate-idle-color: #8a8a8a;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Paper-styles color.html dependency is stripped on build.
|
Paper-styles color.html dependency is stripped on build.
|
||||||
When a default paper-style color is used, it needs to be copied
|
When a default paper-style color is used, it needs to be copied
|
||||||
|
@ -34,8 +34,9 @@ export const darkStyles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const derivedStyles = {
|
export const derivedStyles = {
|
||||||
"error-state-color": "var(--error-color)",
|
"state-icon-error-color": "var(--error-state-color, var(--error-color))",
|
||||||
"state-icon-unavailable-color": "var(--disabled-text-color)",
|
"state-unavailable-color":
|
||||||
|
"var(--state-icon-unavailable-color, var(--disabled-text-color))",
|
||||||
"sidebar-text-color": "var(--primary-text-color)",
|
"sidebar-text-color": "var(--primary-text-color)",
|
||||||
"sidebar-background-color": "var(--card-background-color)",
|
"sidebar-background-color": "var(--card-background-color)",
|
||||||
"sidebar-selected-text-color": "var(--primary-color)",
|
"sidebar-selected-text-color": "var(--primary-color)",
|
||||||
|
48
yarn.lock
48
yarn.lock
@ -4634,33 +4634,10 @@ chardet@^0.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||||
|
|
||||||
chart.js@^2.9.4:
|
chart.js@^3.3.2:
|
||||||
version "2.9.4"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.3.2.tgz#667f3a0b6371b9719d8949c04a5bcbaec0d8c615"
|
||||||
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
|
integrity sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA==
|
||||||
dependencies:
|
|
||||||
chartjs-color "^2.1.0"
|
|
||||||
moment "^2.10.2"
|
|
||||||
|
|
||||||
chartjs-chart-timeline@^0.4.0:
|
|
||||||
version "0.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/chartjs-chart-timeline/-/chartjs-chart-timeline-0.4.0.tgz#cbd25dc5ddb5c2b34289f8dd7a2a627d71e251e8"
|
|
||||||
integrity sha512-a3iOFgMUXgEK9zyDFXlL7cfhO6z4DkeuGqok1xnNVNg12ciSt/k1jDBFk8JKN+sVNZfoqeGAFBT9zvb++iEWnA==
|
|
||||||
|
|
||||||
chartjs-color-string@^0.6.0:
|
|
||||||
version "0.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
|
||||||
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
|
||||||
dependencies:
|
|
||||||
color-name "^1.0.0"
|
|
||||||
|
|
||||||
chartjs-color@^2.1.0:
|
|
||||||
version "2.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.3.0.tgz#0e7e1e8dba37eae8415fd3db38bf572007dd958f"
|
|
||||||
integrity sha512-hEvVheqczsoHD+fZ+tfPUE+1+RbV6b+eksp2LwAhwRTVXEjCSEavvk+Hg3H6SZfGlPh/UfmWKGIvZbtobOEm3g==
|
|
||||||
dependencies:
|
|
||||||
chartjs-color-string "^0.6.0"
|
|
||||||
color-convert "^0.5.3"
|
|
||||||
|
|
||||||
check-error@^1.0.2:
|
check-error@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -4942,11 +4919,6 @@ collection-visit@^1.0.0:
|
|||||||
map-visit "^1.0.0"
|
map-visit "^1.0.0"
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
color-convert@^0.5.3:
|
|
||||||
version "0.5.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
|
|
||||||
integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
|
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
@ -4966,7 +4938,7 @@ color-name@1.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
color-name@^1.0.0, color-name@~1.1.4:
|
color-name@~1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
@ -5360,6 +5332,11 @@ d@1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
es5-ext "^0.10.9"
|
es5-ext "^0.10.9"
|
||||||
|
|
||||||
|
date-fns@^2.22.1:
|
||||||
|
version "2.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
|
||||||
|
integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
|
||||||
|
|
||||||
dateformat@^1.0.7-1.2.3:
|
dateformat@^1.0.7-1.2.3:
|
||||||
version "1.0.12"
|
version "1.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
|
||||||
@ -9425,11 +9402,6 @@ mocha@^8.4.0:
|
|||||||
yargs-parser "20.2.4"
|
yargs-parser "20.2.4"
|
||||||
yargs-unparser "2.0.0"
|
yargs-unparser "2.0.0"
|
||||||
|
|
||||||
moment@^2.10.2:
|
|
||||||
version "2.24.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
|
||||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user