Skip to content

Commit f64169a

Browse files
authored
feat(series.show): Display your serie's extremas on the chart (#85)
1 parent f2f91a9 commit f64169a

File tree

8 files changed

+118
-25
lines changed

8 files changed

+118
-25
lines changed

.devcontainer/ui-lovelace.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ views:
5151
type: area
5252
curve: straight
5353
color: yellow
54+
show:
55+
extremas: true
5456
- entity: sensor.random_0_1000
5557
name: Sensor 2
5658
type: area
@@ -130,6 +132,8 @@ views:
130132
series:
131133
- entity: sensor.humidity
132134
curve: straight
135+
show:
136+
extremas: true
133137

134138
- type: custom:apexcharts-card
135139
graph_span: 1h
@@ -339,6 +343,8 @@ views:
339343
series:
340344
- entity: sensor.pvpc
341345
float_precision: 5
346+
show:
347+
extremas: true
342348
data_generator: |
343349
return [...Array(22).keys()].map((hour) => {
344350
const attr = 'price_' + `${hour}`.padStart(2, '0') + 'h';
@@ -441,6 +447,8 @@ views:
441447
title: line
442448
series:
443449
- entity: sensor.random0_100
450+
show:
451+
extremas: true
444452
color_threshold:
445453
- value: 0
446454
color: '#0000ff'
@@ -449,6 +457,8 @@ views:
449457
- value: 66
450458
color: '#ff0000'
451459
- entity: sensor.random_0_1000
460+
show:
461+
extremas: true
452462
- type: custom:apexcharts-card
453463
header:
454464
show: true

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ The card stricly validates all the options available (but not for the `apex_conf
180180
| `in_chart` | boolean | `true` | v1.4.0 | If `false`, hides the serie from the chart |
181181
| `datalabels` | boolean or string | `false` | v1.5.0 | If `true` will show the value of each point for this serie directly in the chart. Don't use it if you have a lot of points displayed, it will be a mess. If you set it to `total` (introduced in NEXT_VERSION), it will display the stacked total value (only works when `stacked: true`) |
182182
| `hidden_by_default` | boolean | `false` | v1.6.0 | See [experimental](#hidden_by_default-experimental-feature) |
183+
| `extremas` | boolean | `false` | NEXT_VERSION | If enabled, will show the min and the max of the serie in the chart |
183184

184185

185186
### Main `show` Options

rollup.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import babel from '@rollup/plugin-babel';
55
import { terser } from 'rollup-plugin-terser';
66
import serve from 'rollup-plugin-serve';
77
import json from '@rollup/plugin-json';
8-
// import cleanup from 'rollup-plugin-cleanup';
98

109
// eslint-disable-next-line no-undef
1110
const dev = process.env.ROLLUP_WATCH;
@@ -37,7 +36,6 @@ const plugins = [
3736
},
3837
],
3938
}),
40-
// cleanup({ comments: 'none' }),
4139
dev && serve(serveopts),
4240
!dev &&
4341
terser({

src/apexcharts-card.ts

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'array-flat-polyfill';
22
import { LitElement, html, customElement, property, TemplateResult, CSSResult, PropertyValues } from 'lit-element';
33
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
4-
import { ChartCardConfig, ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache } from './types';
4+
import { ChartCardConfig, ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache, HistoryPoint } from './types';
55
import { getLovelace, HomeAssistant } from 'custom-card-helpers';
66
import localForage from 'localforage';
77
import * as pjson from '../package.json';
@@ -307,6 +307,7 @@ class ChartsCard extends LitElement {
307307
const defColors = this._config?.color_list || DEFAULT_COLORS;
308308
if (this._config) {
309309
this._graphs = this._config.series.map((serie, index) => {
310+
serie.index = index;
310311
if (!this._headerColors[index]) {
311312
this._headerColors[index] = defColors[index % defColors.length];
312313
}
@@ -540,28 +541,8 @@ class ChartsCard extends LitElement {
540541
min: start.getTime(),
541542
max: this._findEndOfChart(end),
542543
},
544+
annotations: this._computeAnnotations(start, end),
543545
};
544-
if (this._config.now?.show) {
545-
const color = computeColor(this._config.now.color || 'var(--primary-color)');
546-
const textColor = computeTextColor(color);
547-
graphData.annotations = {
548-
xaxis: [
549-
{
550-
x: new Date().getTime(),
551-
strokeDashArray: 3,
552-
label: {
553-
text: this._config.now.label,
554-
borderColor: color,
555-
style: {
556-
color: textColor,
557-
background: color,
558-
},
559-
},
560-
borderColor: color,
561-
},
562-
],
563-
};
564-
}
565546
} else {
566547
// No timeline charts
567548
graphData = {
@@ -633,6 +614,87 @@ class ChartsCard extends LitElement {
633614
this._updating = false;
634615
}
635616

617+
private _computeAnnotations(start: Date, end: Date) {
618+
return {
619+
...this._computeMinMaxPointsAnnotations(start, end),
620+
...this._computeNowAnnotation(),
621+
};
622+
}
623+
624+
private _computeMinMaxPointsAnnotations(start: Date, end: Date) {
625+
return {
626+
points: this._config?.series_in_graph.flatMap((serie, index) => {
627+
if (serie.show.extremas) {
628+
const { min, max } = this._graphs?.[serie.index]?.minMaxWithTimestamp(start.getTime(), end.getTime()) || {
629+
min: [0, null],
630+
max: [0, null],
631+
};
632+
const bgColor = computeColor(this._colors[index]);
633+
const txtColor = computeTextColor(bgColor);
634+
if (!min[0] || !max[0]) return [];
635+
return [
636+
this._getPointAnnotationStyle(min, bgColor, txtColor, serie, index),
637+
this._getPointAnnotationStyle(max, bgColor, txtColor, serie, index),
638+
];
639+
} else {
640+
return [];
641+
}
642+
}),
643+
};
644+
}
645+
646+
private _getPointAnnotationStyle(
647+
value: HistoryPoint,
648+
bgColor: string,
649+
txtColor: string,
650+
serie: ChartCardSeriesConfig,
651+
index: number,
652+
) {
653+
return {
654+
x: value[0],
655+
y: value[1],
656+
seriesIndex: index,
657+
marker: {
658+
strokeColor: bgColor,
659+
fillColor: 'var(--card-background-color)',
660+
},
661+
label: {
662+
text: truncateFloat(value[1], serie.float_precision)?.toString(),
663+
borderColor: 'var(--card-background-color)',
664+
borderWidth: 2,
665+
style: {
666+
background: bgColor,
667+
color: txtColor,
668+
},
669+
},
670+
};
671+
}
672+
673+
private _computeNowAnnotation() {
674+
if (this._config?.now?.show) {
675+
const color = computeColor(this._config.now.color || 'var(--primary-color)');
676+
const textColor = computeTextColor(color);
677+
return {
678+
xaxis: [
679+
{
680+
x: new Date().getTime(),
681+
strokeDashArray: 3,
682+
label: {
683+
text: this._config.now.label,
684+
borderColor: color,
685+
style: {
686+
color: textColor,
687+
background: color,
688+
},
689+
},
690+
borderColor: color,
691+
},
692+
],
693+
};
694+
}
695+
return {};
696+
}
697+
636698
private _computeChartColors(): (string | (({ value }) => string))[] {
637699
const defaultColors: (string | (({ value }) => string))[] = computeColorsWithAlpha(
638700
this._colors,

src/graphEntry.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
HassHistory,
77
HassHistoryEntry,
88
HistoryBuckets,
9+
HistoryPoint,
910
} from './types';
1011
import { compress, decompress, log } from './utils';
1112
import localForage from 'localforage';
@@ -117,6 +118,20 @@ export default class GraphEntry {
117118
return Math.max(...this._computedHistory.flatMap((item) => (item[1] === null ? [] : [item[1]])));
118119
}
119120

121+
public minMaxWithTimestamp(start: number, end: number): { min: HistoryPoint; max: HistoryPoint } | undefined {
122+
if (!this._computedHistory || this._computedHistory.length === 0) return undefined;
123+
return this._computedHistory.reduce(
124+
(acc: { min: HistoryPoint; max: HistoryPoint }, point) => {
125+
if (point[1] === null) return acc;
126+
if (point[0] > end || point[0] < start) return acc;
127+
if (acc.max[1] === null || acc.max[1] < point[1]) acc.max = point;
128+
if (acc.min[1] === null || (point[1] !== null && acc.min[1] > point[1])) acc.min = point;
129+
return acc;
130+
},
131+
{ min: [0, null], max: [0, null] },
132+
);
133+
}
134+
120135
private async _getCache(key: string, compressed: boolean): Promise<EntityEntryCache | undefined> {
121136
const data: EntityEntryCache | undefined | null = await localForage.getItem(
122137
`${key}_${this._md5Config}${compressed ? '' : '-raw'}`,

src/types-config-ti.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const ChartCardAllSeriesExternalConfig = t.iface([], {
6868
"in_chart": t.opt("boolean"),
6969
"datalabels": t.opt(t.union("boolean", t.lit('total'))),
7070
"hidden_by_default": t.opt("boolean"),
71+
"extremas": t.opt("boolean"),
7172
})),
7273
"group_by": t.opt(t.iface([], {
7374
"duration": t.opt("string"),
@@ -103,6 +104,7 @@ export const ChartCardSeriesExternalConfig = t.iface([], {
103104
"in_chart": t.opt("boolean"),
104105
"datalabels": t.opt(t.union("boolean", t.lit('total'))),
105106
"hidden_by_default": t.opt("boolean"),
107+
"extremas": t.opt("boolean"),
106108
})),
107109
"group_by": t.opt(t.iface([], {
108110
"duration": t.opt("string"),

src/types-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface ChartCardAllSeriesExternalConfig {
6565
in_chart?: boolean;
6666
datalabels?: boolean | 'total';
6767
hidden_by_default?: boolean;
68+
extremas?: boolean;
6869
};
6970
group_by?: {
7071
duration?: string;
@@ -104,6 +105,7 @@ export interface ChartCardSeriesExternalConfig {
104105
in_chart?: boolean;
105106
datalabels?: boolean | 'total';
106107
hidden_by_default?: boolean;
108+
extremas?: boolean;
107109
};
108110
group_by?: {
109111
duration?: string;

src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ChartCardSeriesConfig extends ChartCardSeriesExternalConfig {
3030
in_chart: boolean;
3131
datalabels?: boolean | 'total';
3232
hidden_by_default?: boolean;
33+
extremas?: boolean;
3334
};
3435
}
3536

@@ -40,7 +41,9 @@ export interface EntityEntryCache {
4041
data: EntityCachePoints;
4142
}
4243

43-
export type EntityCachePoints = Array<[number, number | null]>;
44+
export type EntityCachePoints = Array<HistoryPoint>;
45+
46+
export type HistoryPoint = [number, number | null];
4447

4548
export type HassHistory = Array<[HassHistoryEntry] | undefined>;
4649

0 commit comments

Comments
 (0)