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>;