Skip to content

Commit

Permalink
feat(group_by): Add more functions and fix buckets
Browse files Browse the repository at this point in the history
  • Loading branch information
RomRider committed Jan 24, 2021
1 parent ec875d5 commit c7324e0
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 34 deletions.
35 changes: 32 additions & 3 deletions .devcontainer/ui-lovelace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 14 additions & 7 deletions src/apex-layouts.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,6 +17,7 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
show: false,
},
},
colors: DEFAULT_COLORS,
grid: {
strokeDashArray: 3,
},
Expand Down Expand Up @@ -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]),
' - ',
`<strong>${opts.w.globals.series[opts.seriesIndex].slice(-1)}${computeUom(
opts.seriesIndex,
conf,
undefined,
hass2?.states[conf.series[opts.seriesIndex].entity],
)}</strong>`,
`<strong>${
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],
)}</strong>
`,
];
},
},
Expand Down
11 changes: 9 additions & 2 deletions src/apexcharts-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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}'`;
Expand Down
17 changes: 17 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
107 changes: 89 additions & 18 deletions src/graphEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
6 changes: 5 additions & 1 deletion src/types-config-ti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -41,6 +44,7 @@ export const ChartCardHeaderExternalConfig = t.iface([], {
const exportedTypeSuite: t.ITypeSuite = {
ChartCardExternalConfig,
ChartCardSeriesExternalConfig,
GroupByFill,
GroupByFunc,
ChartCardHeaderExternalConfig,
};
Expand Down
5 changes: 4 additions & 1 deletion src/types-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -14,6 +14,7 @@ export interface ChartCardSeriesConfig extends ChartCardSeriesExternalConfig {
group_by: {
duration: string;
func: GroupByFunc;
fill: GroupByFill;
};
}

Expand All @@ -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>;

Expand Down

0 comments on commit c7324e0

Please sign in to comment.