From af8ba8156452054c38b4b65bb843bb16fbfdf6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Fri, 29 Jan 2021 19:21:58 +0100 Subject: [PATCH] feat(series.offset): Compare data from another timeframe with the current timeframe (#19) Fixes #18 --- .devcontainer/ui-lovelace.yaml | 22 ++++++++++++++++++++++ README.md | 19 +++++++++++++++++++ src/apexcharts-card.ts | 8 ++++++++ src/graphEntry.ts | 31 ++++++++++++++++++++++++++----- src/types-config-ti.ts | 1 + src/types-config.ts | 1 + src/utils.ts | 10 +++++----- 7 files changed, 82 insertions(+), 10 deletions(-) diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index b468c45..fe43aa9 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -25,6 +25,7 @@ views: - entity: sensor.random_0_1000 name: Sensor 2 type: area + offset: -1d graph_span: 15min cache: true layout: minimal @@ -33,6 +34,8 @@ views: - type: custom:apexcharts-card stacked: true + span: + offset: -1d series: - entity: sensor.random_0_1000 name: RAM Usage @@ -228,3 +231,22 @@ views: const value = entity.attributes[attr]; return [moment().startOf('day').hours(hour).valueOf(), value]; }) + + - type: custom:apexcharts-card + graph_span: 8d + span: + start: hour + # apex_config: + # dataLabels: + # enabled: true + header: + show: true + title: Precipitation Forecast + series: + - entity: weather.openweathermap + type: area + extend_to_end: false + data_generator: | + return entity.attributes.forecast.map((entry) => { + return [new Date(entry.datetime), entry.temperature]; + }); diff --git a/README.md b/README.md index ef1eace..9f898b3 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ However, some things might be broken :grin: - [Simple graph](#simple-graph) - [Multiple Types of Graphs](#multiple-types-of-graphs) - [Aggregating data](#aggregating-data) + - [Compare data from today with yesterday](#compare-data-from-today-with-yesterday) ## Installation @@ -121,6 +122,7 @@ The card stricly validates all the options available (but not for the `apex_conf | `group_by` | object | | v1.0.0 | See [group_by](#group_by-options) | | `invert` | boolean | `false` | v1.2.0 | Negates the data (`1` -> `-1`). Usefull to display opposites values like network in (standard)/out (inverted) | | `data_generator` | string | | v1.2.0 | See [data_generator](#data_generator-option) | +| `offset` | string | | NEXT_VERSION | This is different from the main `offset` parameter. This is at the series level. It is only usefull if you want to display data from for eg. yesterday on top of the data from today for the same sensor and compare the data. The time displayed in the tooltip will be wrong as will the x axis information. Valid values are any negative time string, eg: `-1h`, `-12min`, `-1d`, `-1h25`, `-10sec`, ... | ### `show` Options @@ -416,3 +418,20 @@ series: duration: 10min func: first ``` + +### Compare data from today with yesterday + +```yaml +type: custom:apexcharts-card +graph_span: 1d +span: + start: day +header: + show: false +series: + # data from today + - entity: sensor.temperature + # data from yesterday offsetted to be displayed today + - entity: sensor.temperature + offset: -1d +``` \ No newline at end of file diff --git a/src/apexcharts-card.ts b/src/apexcharts-card.ts index 018b021..d784c7a 100644 --- a/src/apexcharts-card.ts +++ b/src/apexcharts-card.ts @@ -95,6 +95,8 @@ class ChartsCard extends LitElement { private _dataLoaded = false; + private _seriesOffset: number[] = []; + public connectedCallback() { super.connectedCallback(); if (this._config && this._hass && !this._loaded) { @@ -194,6 +196,11 @@ class ChartsCard extends LitElement { if (config.span?.end && config.span?.start) { throw new Error(`span: Only one of 'start' or 'end' is allowed.`); } + config.series.forEach((serie, index) => { + if (serie.offset) { + this._seriesOffset[index] = validateOffset(serie.offset, `series[${index}].offset`); + } + }); this._config = mergeDeep( { @@ -231,6 +238,7 @@ class ChartsCard extends LitElement { this._config!.cache, serie, this._config?.span, + this._seriesOffset[index] || 0, ); } return undefined; diff --git a/src/graphEntry.ts b/src/graphEntry.ts index e30e279..e6c90e7 100644 --- a/src/graphEntry.ts +++ b/src/graphEntry.ts @@ -1,7 +1,7 @@ import { HomeAssistant } from 'custom-card-helpers'; import { ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache, HassHistory, HistoryBuckets } from './types'; import { compress, decompress, log } from './utils'; -import localForage from 'localforage'; +import localForage, { config } from 'localforage'; import { HassEntity } from 'home-assistant-js-websocket'; import { DateRange } from 'moment-range'; import { HOUR_24, moment } from './const'; @@ -47,12 +47,15 @@ export default class GraphEntry { private _md5Config: string; + private _offset = 0; + constructor( index: number, graphSpan: number, cache: boolean, config: ChartCardSeriesConfig, span: ChartCardSpanExtConfig | undefined, + offset = 0, ) { const aggregateFuncMap = { avg: this._average, @@ -70,6 +73,7 @@ export default class GraphEntry { this._history = undefined; this._graphSpan = graphSpan; this._config = config; + this._offset = offset; const now = new Date(); const now2 = new Date(now); this._func = aggregateFuncMap[config.group_by.func]; @@ -89,7 +93,7 @@ export default class GraphEntry { } get history(): EntityCachePoints { - return this._computedHistory || this._history?.data || []; + return this._offsetData(this._computedHistory || this._history?.data || []); } get index(): number { @@ -104,6 +108,17 @@ export default class GraphEntry { return this._realEnd; } + private _offsetData(data: EntityCachePoints) { + if (this._offset) { + const lData = JSON.parse(JSON.stringify(data)); + lData.forEach((entry) => { + entry[0] = entry[0] - this._offset; + }); + return lData; + } + return data; + } + private async _getCache(key: string, compressed: boolean): Promise { const data: EntityEntryCache | undefined | null = await localForage.getItem( `${key}_${this._md5Config}${compressed ? '' : '-raw'}`, @@ -136,8 +151,14 @@ export default class GraphEntry { if (this._config.data_generator) { this._history = this._generateData(start, end); } else { - this._realStart = start; - this._realEnd = end; + this._realStart = new Date(start); + this._realEnd = new Date(end); + const endHistory = new Date(end); + + if (this._offset) { + startHistory.setTime(startHistory.getTime() + this._offset); + endHistory.setTime(endHistory.getTime() + this._offset); + } let skipInitialState = false; @@ -165,7 +186,7 @@ export default class GraphEntry { ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion new Date(history.data.slice(-1)[0]![0] + 1) : startHistory, - end, + endHistory, skipInitialState, ); if (newHistory && newHistory[0] && newHistory[0].length > 0) { diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index 20ada26..8f278b8 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -39,6 +39,7 @@ export const ChartCardSeriesExternalConfig = t.iface([], { "invert": t.opt("boolean"), "data_generator": t.opt("string"), "float_precision": t.opt("number"), + "offset": t.opt("string"), "group_by": t.opt(t.iface([], { "duration": t.opt("string"), "func": t.opt("GroupByFunc"), diff --git a/src/types-config.ts b/src/types-config.ts index 600774d..7ccc48f 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -35,6 +35,7 @@ export interface ChartCardSeriesExternalConfig { invert?: boolean; data_generator?: string; float_precision?: number; + offset?: string; group_by?: { duration?: string; func?: GroupByFunc; diff --git a/src/utils.ts b/src/utils.ts index 9199973..4478c5d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -63,17 +63,17 @@ export function computeName( entity: HassEntity | undefined = undefined, ): string { if (!config || (!entities && !entity)) return ''; + let name = ''; if (entity) { - return config.series[index].name || entity.attributes?.friendly_name || entity.entity_id || ''; + name = config.series[index].name || entity.attributes?.friendly_name || entity.entity_id || ''; } else if (entities) { - return ( + name = config.series[index].name || entities[index]?.attributes?.friendly_name || entities[entities[index]]?.entity_id || - '' - ); + ''; } - return ''; + return name + (config.series[index].offset ? ` (${config.series[index].offset})` : ''); } export function computeUom(