Skip to content

Commit

Permalink
feat(series.show): Display your serie's extremas on the chart (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomRider committed Feb 15, 2021
1 parent f2f91a9 commit f64169a
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 25 deletions.
10 changes: 10 additions & 0 deletions .devcontainer/ui-lovelace.yaml
Expand Up @@ -51,6 +51,8 @@ views:
type: area
curve: straight
color: yellow
show:
extremas: true
- entity: sensor.random_0_1000
name: Sensor 2
type: area
Expand Down Expand Up @@ -130,6 +132,8 @@ views:
series:
- entity: sensor.humidity
curve: straight
show:
extremas: true

- type: custom:apexcharts-card
graph_span: 1h
Expand Down Expand Up @@ -339,6 +343,8 @@ views:
series:
- entity: sensor.pvpc
float_precision: 5
show:
extremas: true
data_generator: |
return [...Array(22).keys()].map((hour) => {
const attr = 'price_' + `${hour}`.padStart(2, '0') + 'h';
Expand Down Expand Up @@ -441,6 +447,8 @@ views:
title: line
series:
- entity: sensor.random0_100
show:
extremas: true
color_threshold:
- value: 0
color: '#0000ff'
Expand All @@ -449,6 +457,8 @@ views:
- value: 66
color: '#ff0000'
- entity: sensor.random_0_1000
show:
extremas: true
- type: custom:apexcharts-card
header:
show: true
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -180,6 +180,7 @@ The card stricly validates all the options available (but not for the `apex_conf
| `in_chart` | boolean | `true` | v1.4.0 | If `false`, hides the serie from the chart |
| `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`) |
| `hidden_by_default` | boolean | `false` | v1.6.0 | See [experimental](#hidden_by_default-experimental-feature) |
| `extremas` | boolean | `false` | NEXT_VERSION | If enabled, will show the min and the max of the serie in the chart |


### Main `show` Options
Expand Down
2 changes: 0 additions & 2 deletions rollup.config.js
Expand Up @@ -5,7 +5,6 @@ import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import serve from 'rollup-plugin-serve';
import json from '@rollup/plugin-json';
// import cleanup from 'rollup-plugin-cleanup';

// eslint-disable-next-line no-undef
const dev = process.env.ROLLUP_WATCH;
Expand Down Expand Up @@ -37,7 +36,6 @@ const plugins = [
},
],
}),
// cleanup({ comments: 'none' }),
dev && serve(serveopts),
!dev &&
terser({
Expand Down
106 changes: 84 additions & 22 deletions src/apexcharts-card.ts
@@ -1,7 +1,7 @@
import 'array-flat-polyfill';
import { LitElement, html, customElement, property, TemplateResult, CSSResult, PropertyValues } from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import { ChartCardConfig, ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache } from './types';
import { ChartCardConfig, ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache, HistoryPoint } from './types';
import { getLovelace, HomeAssistant } from 'custom-card-helpers';
import localForage from 'localforage';
import * as pjson from '../package.json';
Expand Down Expand Up @@ -307,6 +307,7 @@ class ChartsCard extends LitElement {
const defColors = this._config?.color_list || DEFAULT_COLORS;
if (this._config) {
this._graphs = this._config.series.map((serie, index) => {
serie.index = index;
if (!this._headerColors[index]) {
this._headerColors[index] = defColors[index % defColors.length];
}
Expand Down Expand Up @@ -540,28 +541,8 @@ class ChartsCard extends LitElement {
min: start.getTime(),
max: this._findEndOfChart(end),
},
annotations: this._computeAnnotations(start, end),
};
if (this._config.now?.show) {
const color = computeColor(this._config.now.color || 'var(--primary-color)');
const textColor = computeTextColor(color);
graphData.annotations = {
xaxis: [
{
x: new Date().getTime(),
strokeDashArray: 3,
label: {
text: this._config.now.label,
borderColor: color,
style: {
color: textColor,
background: color,
},
},
borderColor: color,
},
],
};
}
} else {
// No timeline charts
graphData = {
Expand Down Expand Up @@ -633,6 +614,87 @@ class ChartsCard extends LitElement {
this._updating = false;
}

private _computeAnnotations(start: Date, end: Date) {
return {
...this._computeMinMaxPointsAnnotations(start, end),
...this._computeNowAnnotation(),
};
}

private _computeMinMaxPointsAnnotations(start: Date, end: Date) {
return {
points: this._config?.series_in_graph.flatMap((serie, index) => {
if (serie.show.extremas) {
const { min, max } = this._graphs?.[serie.index]?.minMaxWithTimestamp(start.getTime(), end.getTime()) || {
min: [0, null],
max: [0, null],
};
const bgColor = computeColor(this._colors[index]);
const txtColor = computeTextColor(bgColor);
if (!min[0] || !max[0]) return [];
return [
this._getPointAnnotationStyle(min, bgColor, txtColor, serie, index),
this._getPointAnnotationStyle(max, bgColor, txtColor, serie, index),
];
} else {
return [];
}
}),
};
}

private _getPointAnnotationStyle(
value: HistoryPoint,
bgColor: string,
txtColor: string,
serie: ChartCardSeriesConfig,
index: number,
) {
return {
x: value[0],
y: value[1],
seriesIndex: index,
marker: {
strokeColor: bgColor,
fillColor: 'var(--card-background-color)',
},
label: {
text: truncateFloat(value[1], serie.float_precision)?.toString(),
borderColor: 'var(--card-background-color)',
borderWidth: 2,
style: {
background: bgColor,
color: txtColor,
},
},
};
}

private _computeNowAnnotation() {
if (this._config?.now?.show) {
const color = computeColor(this._config.now.color || 'var(--primary-color)');
const textColor = computeTextColor(color);
return {
xaxis: [
{
x: new Date().getTime(),
strokeDashArray: 3,
label: {
text: this._config.now.label,
borderColor: color,
style: {
color: textColor,
background: color,
},
},
borderColor: color,
},
],
};
}
return {};
}

private _computeChartColors(): (string | (({ value }) => string))[] {
const defaultColors: (string | (({ value }) => string))[] = computeColorsWithAlpha(
this._colors,
Expand Down
15 changes: 15 additions & 0 deletions src/graphEntry.ts
Expand Up @@ -6,6 +6,7 @@ import {
HassHistory,
HassHistoryEntry,
HistoryBuckets,
HistoryPoint,
} from './types';
import { compress, decompress, log } from './utils';
import localForage from 'localforage';
Expand Down Expand Up @@ -117,6 +118,20 @@ export default class GraphEntry {
return Math.max(...this._computedHistory.flatMap((item) => (item[1] === null ? [] : [item[1]])));
}

public minMaxWithTimestamp(start: number, end: number): { min: HistoryPoint; max: HistoryPoint } | undefined {
if (!this._computedHistory || this._computedHistory.length === 0) return undefined;
return this._computedHistory.reduce(
(acc: { min: HistoryPoint; max: HistoryPoint }, point) => {
if (point[1] === null) return acc;
if (point[0] > end || point[0] < start) return acc;
if (acc.max[1] === null || acc.max[1] < point[1]) acc.max = point;
if (acc.min[1] === null || (point[1] !== null && acc.min[1] > point[1])) acc.min = point;
return acc;
},
{ min: [0, null], max: [0, null] },
);
}

private async _getCache(key: string, compressed: boolean): Promise<EntityEntryCache | undefined> {
const data: EntityEntryCache | undefined | null = await localForage.getItem(
`${key}_${this._md5Config}${compressed ? '' : '-raw'}`,
Expand Down
2 changes: 2 additions & 0 deletions src/types-config-ti.ts
Expand Up @@ -68,6 +68,7 @@ export const ChartCardAllSeriesExternalConfig = t.iface([], {
"in_chart": t.opt("boolean"),
"datalabels": t.opt(t.union("boolean", t.lit('total'))),
"hidden_by_default": t.opt("boolean"),
"extremas": t.opt("boolean"),
})),
"group_by": t.opt(t.iface([], {
"duration": t.opt("string"),
Expand Down Expand Up @@ -103,6 +104,7 @@ export const ChartCardSeriesExternalConfig = t.iface([], {
"in_chart": t.opt("boolean"),
"datalabels": t.opt(t.union("boolean", t.lit('total'))),
"hidden_by_default": t.opt("boolean"),
"extremas": t.opt("boolean"),
})),
"group_by": t.opt(t.iface([], {
"duration": t.opt("string"),
Expand Down
2 changes: 2 additions & 0 deletions src/types-config.ts
Expand Up @@ -65,6 +65,7 @@ export interface ChartCardAllSeriesExternalConfig {
in_chart?: boolean;
datalabels?: boolean | 'total';
hidden_by_default?: boolean;
extremas?: boolean;
};
group_by?: {
duration?: string;
Expand Down Expand Up @@ -104,6 +105,7 @@ export interface ChartCardSeriesExternalConfig {
in_chart?: boolean;
datalabels?: boolean | 'total';
hidden_by_default?: boolean;
extremas?: boolean;
};
group_by?: {
duration?: string;
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Expand Up @@ -30,6 +30,7 @@ export interface ChartCardSeriesConfig extends ChartCardSeriesExternalConfig {
in_chart: boolean;
datalabels?: boolean | 'total';
hidden_by_default?: boolean;
extremas?: boolean;
};
}

Expand All @@ -40,7 +41,9 @@ export interface EntityEntryCache {
data: EntityCachePoints;
}

export type EntityCachePoints = Array<[number, number | null]>;
export type EntityCachePoints = Array<HistoryPoint>;

export type HistoryPoint = [number, number | null];

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

Expand Down

0 comments on commit f64169a

Please sign in to comment.