From c7324e04f7d1b196ccf9f7437bc2f44671f1ba84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wiedemann?= Date: Sun, 24 Jan 2021 19:03:37 +0000 Subject: [PATCH] feat(group_by): Add more functions and fix buckets --- .devcontainer/ui-lovelace.yaml | 35 ++++++++++- src/apex-layouts.ts | 21 ++++--- src/apexcharts-card.ts | 11 +++- src/const.ts | 17 ++++++ src/graphEntry.ts | 107 +++++++++++++++++++++++++++------ src/types-config-ti.ts | 6 +- src/types-config.ts | 5 +- src/types.ts | 5 +- 8 files changed, 173 insertions(+), 34 deletions(-) diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index cd4c32e..a259715 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -69,11 +69,40 @@ views: curve: straight - type: custom:apexcharts-card - hours_to_show: 24 + hours_to_show: 48 series: - - entity: sensor.humidity + - entity: sensor.random0_100 + name: AVG curve: smooth type: area group_by: - duration: 5h + duration: 1h func: avg + - entity: sensor.random0_100 + curve: smooth + name: MIN + type: area + group_by: + duration: 1h + func: min + - entity: sensor.random0_100 + curve: smooth + name: MAX + type: area + group_by: + duration: 1h + func: max + - entity: sensor.random0_100 + curve: smooth + name: LAST + type: area + group_by: + duration: 3h + func: last + - entity: sensor.random0_100 + curve: smooth + name: FIRST + type: area + group_by: + duration: 3h + func: first diff --git a/src/apex-layouts.ts b/src/apex-layouts.ts index d3ffaf5..9fe7243 100644 --- a/src/apex-layouts.ts +++ b/src/apex-layouts.ts @@ -1,5 +1,5 @@ import { HomeAssistant } from 'custom-card-helpers'; -import { moment } from './const'; +import { DEFAULT_COLORS, moment } from './const'; import { ChartCardConfig } from './types'; import { computeName, computeUom, mergeDeep } from './utils'; @@ -17,6 +17,7 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u show: false, }, }, + colors: DEFAULT_COLORS, grid: { strokeDashArray: 3, }, @@ -63,12 +64,18 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u return [ computeName(opts.seriesIndex, conf, undefined, hass2?.states[conf.series[opts.seriesIndex].entity]), ' - ', - `${opts.w.globals.series[opts.seriesIndex].slice(-1)}${computeUom( - opts.seriesIndex, - conf, - undefined, - hass2?.states[conf.series[opts.seriesIndex].entity], - )}`, + `${ + opts.w.globals.series[opts.seriesIndex].slice(-1).length !== 0 + ? opts.w.globals.series[opts.seriesIndex].slice(-1)[0].toFixed(1) + : opts.w.globals.series[opts.seriesIndex].slice(-1) + } + ${computeUom( + opts.seriesIndex, + conf, + undefined, + hass2?.states[conf.series[opts.seriesIndex].entity], + )} + `, ]; }, }, diff --git a/src/apexcharts-card.ts b/src/apexcharts-card.ts index 78ccd68..f3f54a8 100644 --- a/src/apexcharts-card.ts +++ b/src/apexcharts-card.ts @@ -13,7 +13,13 @@ import GraphEntry from './graphEntry'; import { createCheckers } from 'ts-interface-checker'; import { ChartCardExternalConfig } from './types-config'; import exportedTypeSuite from './types-config-ti'; -import { DEFAULT_DURATION, DEFAULT_FUNC, DEFAULT_HOURS_TO_SHOW, DEFAULT_SERIE_TYPE } from './const'; +import { + DEFAULT_DURATION, + DEFAULT_FUNC, + DEFAULT_GROUP_BY_FILL, + DEFAULT_HOURS_TO_SHOW, + DEFAULT_SERIE_TYPE, +} from './const'; import parse from 'parse-duration'; /* eslint no-console: 0 */ @@ -122,10 +128,11 @@ class ChartsCard extends LitElement { serie.extend_to_end = serie.extend_to_end !== undefined ? serie.extend_to_end : true; serie.type = serie.type || DEFAULT_SERIE_TYPE; if (!serie.group_by) { - serie.group_by = { duration: DEFAULT_DURATION, func: DEFAULT_FUNC }; + serie.group_by = { duration: DEFAULT_DURATION, func: DEFAULT_FUNC, fill: DEFAULT_GROUP_BY_FILL }; } else { serie.group_by.duration = serie.group_by.duration || DEFAULT_DURATION; serie.group_by.func = serie.group_by.func || DEFAULT_FUNC; + serie.group_by.fill = serie.group_by.fill || DEFAULT_GROUP_BY_FILL; } if (!parse(serie.group_by.duration)) { throw `Can't parse 'group_by' duration: '${serie.group_by.duration}'`; diff --git a/src/const.ts b/src/const.ts index 072738e..d660039 100644 --- a/src/const.ts +++ b/src/const.ts @@ -8,3 +8,20 @@ export const DEFAULT_HOURS_TO_SHOW = 24; export const DEFAULT_SERIE_TYPE = 'line'; export const DEFAULT_DURATION = '1h'; export const DEFAULT_FUNC = 'raw'; +export const DEFAULT_GROUP_BY_FILL = 'last'; + +export const DEFAULT_COLORS = [ + 'var(--accent-color)', + '#3498db', + '#e74c3c', + '#9b59b6', + '#f1c40f', + '#2ecc71', + '#1abc9c', + '#34495e', + '#e67e22', + '#7f8c8d', + '#27ae60', + '#2980b9', + '#8e44ad', +]; diff --git a/src/graphEntry.ts b/src/graphEntry.ts index ecbea68..197a4cb 100644 --- a/src/graphEntry.ts +++ b/src/graphEntry.ts @@ -43,6 +43,11 @@ export default class GraphEntry { constructor(entity: string, index: number, hoursToShow: number, cache: boolean, config: ChartCardSeriesConfig) { const aggregateFuncMap = { avg: this._average, + max: this._maximum, + min: this._minimum, + first: this._first, + last: this._last, + sum: this._sum, }; this._index = index; this._cache = cache; @@ -197,34 +202,100 @@ export default class GraphEntry { const ranges = Array.from(this._timeRange.reverseBy('milliseconds', { step: this._groupByDurationMs })).reverse(); // const res: EntityCachePoints[] = [[]]; const buckets: HistoryBuckets = []; - ranges.forEach((range) => { - buckets.push({ timestamp: range.valueOf(), data: [] }); + ranges.forEach((range, index) => { + buckets[index] = { timestamp: range.valueOf(), data: [] }; }); + let lastNotNullValue: number | null = null; this._history?.data.forEach((entry) => { - buckets.forEach((bucket, index) => { - if (bucket.timestamp > entry![0] && index > 0) { - buckets[index - 1].data.push(entry); + let properEntry = entry; + // Fill null values + if (properEntry[1] === null) { + if (this._config.group_by.fill === 'last') { + properEntry = [entry[0], lastNotNullValue]; + } else if (this._config.group_by.fill === 'zero') { + properEntry = [entry[0], 0]; } + } else { + lastNotNullValue = properEntry[1]; + } + + buckets.some((bucket, index) => { + if (bucket.timestamp > properEntry![0] && index > 0) { + buckets[index - 1].data.push(properEntry); + return true; + } + return false; }); }); + let lastNonNullBucketValue: number | null = null; + buckets.forEach((bucket) => { + if (bucket.data.length === 0) { + if (this._config.group_by.fill === 'last') { + bucket.data[0] = [bucket.timestamp, lastNonNullBucketValue]; + } else if (this._config.group_by.fill === 'zero') { + bucket.data[0] = [bucket.timestamp, 0]; + } else if (this._config.group_by.fill === 'null') { + bucket.data[0] = [bucket.timestamp, null]; + } + } else { + lastNonNullBucketValue = bucket.data.slice(-1)[0][1]; + } + }); buckets.pop(); return buckets; } + private _sum(items: EntityCachePoints): number { + if (items.length === 0) return 0; + let lastIndex = 0; + return items.reduce((sum, entry, index) => { + let val = 0; + if (entry && entry[1] === null) { + val = items[lastIndex][1]!; + } else { + val = entry[1]!; + lastIndex = index; + } + return sum + val; + }, 0); + } + private _average(items: EntityCachePoints): number | null { if (items.length === 0) return null; - let lastIndex = 0; - return ( - items.reduce((sum, entry, index) => { - let val = 0; - if (entry && entry[1] === null) { - val = items[lastIndex]![1]!; - } else { - val = entry![1]!; - lastIndex = index; - } - return sum + val; - }, 0) / items.length - ); + return this._sum(items) / items.length; + } + + private _minimum(items: EntityCachePoints): number | null { + let min: number | null = null; + items.forEach((item) => { + if (item[1] !== null) + if (min === null) min = item[1]; + else min = Math.min(item[1], min); + }); + return min; + } + + private _maximum(items: EntityCachePoints): number | null { + let max: number | null = null; + items.forEach((item) => { + if (item[1] !== null) + if (max === null) max = item[1]; + else max = Math.max(item[1], max); + }); + return max; + } + + private _last(items: EntityCachePoints): number | null { + if (items.length === 0) return null; + return items.slice(-1)[0][1]; + } + + private _first(items: EntityCachePoints): number | null { + if (items.length === 0) return null; + return items[0][1]; + } + + private _filterNulls(items: EntityCachePoints): EntityCachePoints { + return items.filter((item) => item[1] !== null); } } diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index d915e81..78c7712 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -28,10 +28,13 @@ export const ChartCardSeriesExternalConfig = t.iface([], { "group_by": t.opt(t.iface([], { "duration": t.opt("string"), "func": t.opt("GroupByFunc"), + "fill": t.opt("GroupByFill"), })), }); -export const GroupByFunc = t.union(t.lit('raw'), t.lit('avg')); +export const GroupByFill = t.union(t.lit('null'), t.lit('last'), t.lit('zero')); + +export const GroupByFunc = t.union(t.lit('raw'), t.lit('avg'), t.lit('min'), t.lit('max'), t.lit('last'), t.lit('first'), t.lit('sum')); export const ChartCardHeaderExternalConfig = t.iface([], { "show": t.opt("boolean"), @@ -41,6 +44,7 @@ export const ChartCardHeaderExternalConfig = t.iface([], { const exportedTypeSuite: t.ITypeSuite = { ChartCardExternalConfig, ChartCardSeriesExternalConfig, + GroupByFill, GroupByFunc, ChartCardHeaderExternalConfig, }; diff --git a/src/types-config.ts b/src/types-config.ts index 0bd0ace..9d49137 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -23,10 +23,13 @@ export interface ChartCardSeriesExternalConfig { group_by?: { duration?: string; func?: GroupByFunc; + fill?: GroupByFill; }; } -export type GroupByFunc = 'raw' | 'avg'; +export type GroupByFill = 'null' | 'last' | 'zero'; + +export type GroupByFunc = 'raw' | 'avg' | 'min' | 'max' | 'last' | 'first' | 'sum'; export interface ChartCardHeaderExternalConfig { show?: boolean; diff --git a/src/types.ts b/src/types.ts index 1e6a984..e0d6b5d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { ApexOptions } from 'apexcharts'; -import { ChartCardExternalConfig, ChartCardSeriesExternalConfig, GroupByFunc } from './types-config'; +import { ChartCardExternalConfig, ChartCardSeriesExternalConfig, GroupByFill, GroupByFunc } from './types-config'; export interface ChartCardConfig extends ChartCardExternalConfig { series: ChartCardSeriesConfig[]; @@ -14,6 +14,7 @@ export interface ChartCardSeriesConfig extends ChartCardSeriesExternalConfig { group_by: { duration: string; func: GroupByFunc; + fill: GroupByFill; }; } @@ -23,7 +24,7 @@ export interface EntityEntryCache { data: EntityCachePoints; } -export type EntityCachePoints = Array<[number, number | null] | undefined>; +export type EntityCachePoints = Array<[number, number | null]>; export type HassHistory = Array<[HassHistoryEntry] | undefined>;