Skip to content

Commit c8dedf7

Browse files
authored
feat(brush): Support for a timeframe selection chart (#102)
* Initial brush support * working brush * Update doc * refactor locales
1 parent 3a441f9 commit c8dedf7

File tree

12 files changed

+455
-142
lines changed

12 files changed

+455
-142
lines changed

.devcontainer/ui-lovelace.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,3 +834,30 @@ views:
834834
group_by:
835835
func: sum
836836
duration: 120min
837+
838+
- type: custom:apexcharts-card
839+
experimental:
840+
color_threshold: true
841+
brush: true
842+
graph_span: 2h
843+
brush:
844+
selection_span: 10m
845+
series:
846+
- entity: sensor.random0_100
847+
color: blue
848+
type: area
849+
stroke_width: 1
850+
color_threshold:
851+
- value: 0
852+
color: red
853+
- value: 50
854+
color: yellow
855+
- value: 100
856+
color: green
857+
- entity: sensor.random0_100
858+
color: blue
859+
stroke_width: 1
860+
float_precision: 0
861+
show:
862+
in_brush: true
863+
in_chart: false

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ However, some things might be broken :grin:
4949
- [Configuration options](#configuration-options)
5050
- [`color_threshold` experimental feature](#color_threshold-experimental-feature)
5151
- [`hidden_by_default` experimental feature](#hidden_by_default-experimental-feature)
52+
- [`brush` experimental feature](#brush-experimental-feature)
5253
- [Known issues](#known-issues)
5354
- [Roadmap](#roadmap)
5455
- [Examples](#examples)
@@ -142,6 +143,7 @@ The card stricly validates all the options available (but not for the `apex_conf
142143
| `apex_config`| object | | v1.0.0 | Apexcharts API 1:1 mapping. You call see all the options [here](https://apexcharts.com/docs/installation/) --> `Options (Reference)` in the Menu. See [Apex Charts](#apex-charts-options-example) |
143144
| `experimental` | object | | v1.6.0 | See [experimental](#experimental-features) |
144145
| `locale` | string | | v1.7.0 | Default is to inherit from Home-Assistant's user configuration. This overrides it and forces the locale. Eg: `en`, or `fr`. Reverts to `en` if locale is unknown. |
146+
| `brush` | object | | NEXT_VERSION | See [brush](#brush-experimental-feature) |
145147

146148

147149

@@ -182,6 +184,7 @@ The card stricly validates all the options available (but not for the `apex_conf
182184
| `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 v1.7.0), it will display the stacked total value (only works when `stacked: true`) |
183185
| `hidden_by_default` | boolean | `false` | v1.6.0 | See [experimental](#hidden_by_default-experimental-feature) |
184186
| `extremas` | boolean or string | `false` | v1.7.0 | If `true`, will show the min and the max of the serie in the chart. If the value is `time`, it will display also the time of the min/max value on top of the value. This feature doesn't work with `stacked: true`. |
187+
| `in_brush` | boolean | `false` | NEXT_VERSION | See [brush](#brush-experimental-feature) |
185188

186189

187190
### Main `show` Options
@@ -580,6 +583,7 @@ Generates the same result as repeating the configuration in each series:
580583
| `color_threshold` | boolean | `false` | v1.6.0 | Will enable the color threshold feature. See [color_threshold](#color_threshold-experimental-feature) |
581584
| `disable_config_validation` | boolean | `false` | v1.6.0 | If `true`, will disable the config validation. Useful if you have cards adding parameters to this one. Use at your own risk. |
582585
| `hidden_by_default` | boolean | `false` | v1.6.0 | Will allow you to use the `hidden_by_default` option. See [hidden_by_default](#hidden_by_default-experimental-feature) |
586+
| `brush` | boolean | `false` | NEXT_VERSION | Will display a brush which allows you to select a portion time to display on the main chart. See [brush](#brush-experimental-feature) |
583587

584588
### `color_threshold` experimental feature
585589

@@ -646,6 +650,58 @@ series:
646650
- entity: sensor.temperature_office
647651
```
648652

653+
### `brush` experimental feature
654+
655+
`brush` will allow you to display a small chart on the bottom of the card to select a time frame to display on the main chart.
656+
657+
![brush_image](docs/brush.png)
658+
659+
Things to know:
660+
* You might have some glitches if you are using colums in either the top or the bottom of the chart. There's nothing I can do about it.
661+
* All the features from normal series can be applied to the brush series
662+
* You can configure the bottom chart the way you want with another specific `apex_config` also
663+
* It might be compute heavy and slow with a lot of history data points
664+
* It is recommended to not show too much data on the bottom chart for the sake of lisibility
665+
666+
Here is how to use it (this represents the chart above):
667+
```yaml
668+
type: custom:apexcharts-card
669+
experimental:
670+
color_threshold: true
671+
brush: true # This is required
672+
graph_span: 2h # This will represent the span of the brush
673+
brush:
674+
# selection_span: optional
675+
# defines the default selected span in the brush
676+
# Defaults to 1/4 of the `graph_span`
677+
selection_span: 10m
678+
# apex_config: optional
679+
apex_config:
680+
# Any ApexCharts settings you want to apply to the brush
681+
# Same as the standard apex_config
682+
series:
683+
- entity: sensor.random0_100
684+
color: blue
685+
type: area
686+
stroke_width: 1
687+
color_threshold:
688+
- value: 0
689+
color: red
690+
- value: 50
691+
color: yellow
692+
- value: 100
693+
color: green
694+
- entity: sensor.random0_100
695+
color: blue
696+
stroke_width: 1
697+
float_precision: 0
698+
show:
699+
# in_brush: set it to true and the serie will show up in the brush
700+
in_brush: true
701+
# add this also if you want your serie to only show up in the brush
702+
in_chart: false
703+
```
704+
649705
## Known issues
650706
651707
* Sometimes, if `smoothing` is used alongside `area` and there is missing data in the chart, the background will be glitchy. See [apexcharts.js/#2180](https://github.com/apexcharts/apexcharts.js/issues/2180)

docs/brush.png

53.2 KB
Loading

rollup.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,8 @@ export default [
6060
watch: {
6161
exclude: 'node_modules/**',
6262
},
63+
globals: {
64+
apexcharts: 'ApexCharts',
65+
},
6366
},
6467
];

src/apex-layouts.ts

Lines changed: 98 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,77 +12,15 @@ import {
1212
import { ChartCardConfig } from './types';
1313
import { computeName, computeUom, is12Hour, mergeDeep, prettyPrintTime, truncateFloat } from './utils';
1414
import { layoutMinimal } from './layouts/minimal';
15-
import * as ca from 'apexcharts/dist/locales/ca.json';
16-
import * as cs from 'apexcharts/dist/locales/cs.json';
17-
import * as de from 'apexcharts/dist/locales/de.json';
18-
import * as el from 'apexcharts/dist/locales/el.json';
19-
import * as en from 'apexcharts/dist/locales/en.json';
20-
import * as es from 'apexcharts/dist/locales/es.json';
21-
import * as fi from 'apexcharts/dist/locales/fi.json';
22-
import * as fr from 'apexcharts/dist/locales/fr.json';
23-
import * as he from 'apexcharts/dist/locales/he.json';
24-
import * as hi from 'apexcharts/dist/locales/hi.json';
25-
import * as hr from 'apexcharts/dist/locales/hr.json';
26-
import * as hy from 'apexcharts/dist/locales/hy.json';
27-
import * as id from 'apexcharts/dist/locales/id.json';
28-
import * as it from 'apexcharts/dist/locales/it.json';
29-
import * as ka from 'apexcharts/dist/locales/ka.json';
30-
import * as ko from 'apexcharts/dist/locales/ko.json';
31-
import * as lt from 'apexcharts/dist/locales/lt.json';
32-
import * as nb from 'apexcharts/dist/locales/nb.json';
33-
import * as nl from 'apexcharts/dist/locales/nl.json';
34-
import * as pl from 'apexcharts/dist/locales/pl.json';
35-
import * as pt_br from 'apexcharts/dist/locales/pt-br.json';
36-
import * as pt from 'apexcharts/dist/locales/pt.json';
37-
import * as rs from 'apexcharts/dist/locales/rs.json';
38-
import * as ru from 'apexcharts/dist/locales/ru.json';
39-
import * as se from 'apexcharts/dist/locales/se.json';
40-
import * as sk from 'apexcharts/dist/locales/sk.json';
41-
import * as sl from 'apexcharts/dist/locales/sl.json';
42-
import * as sq from 'apexcharts/dist/locales/sq.json';
43-
import * as th from 'apexcharts/dist/locales/th.json';
44-
import * as tr from 'apexcharts/dist/locales/tr.json';
45-
import * as ua from 'apexcharts/dist/locales/ua.json';
46-
import * as zh_cn from 'apexcharts/dist/locales/zh-cn.json';
15+
import { getLocales, getDefaultLocale } from './locales';
4716

4817
export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | undefined = undefined): unknown {
49-
const locales = {
50-
ca: ca,
51-
cs: cs,
52-
de: de,
53-
el: el,
54-
en: en,
55-
es: es,
56-
fi: fi,
57-
fr: fr,
58-
he: he,
59-
hi: hi,
60-
hr: hr,
61-
hy: hy,
62-
id: id,
63-
it: it,
64-
ka: ka,
65-
ko: ko,
66-
lt: lt,
67-
nb: nb,
68-
nl: nl,
69-
pl: pl,
70-
'pt-br': pt_br,
71-
pt: pt,
72-
rs: rs,
73-
ru: ru,
74-
se: se,
75-
sk: sk,
76-
sl: sl,
77-
sq: sq,
78-
th: th,
79-
tr: tr,
80-
ua: ua,
81-
'zh-cn': zh_cn,
82-
};
18+
const locales = getLocales();
8319
const def = {
8420
chart: {
85-
locales: [(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || en],
21+
locales: [
22+
(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || getDefaultLocale(),
23+
],
8624
defaultLocale:
8725
(config.locale && locales[config.locale] && config.locale) ||
8826
(hass?.language && locales[hass.language] && hass.language) ||
@@ -102,10 +40,10 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
10240
strokeDashArray: 3,
10341
},
10442
fill: {
105-
opacity: getFillOpacity(config),
106-
type: getFillType(config),
43+
opacity: getFillOpacity(config, false),
44+
type: getFillType(config, false),
10745
},
108-
series: getSeries(config, hass),
46+
series: getSeries(config, hass, false),
10947
labels: getLabels(config, hass),
11048
xaxis: getXAxis(config, hass),
11149
yaxis: getYAxis(config),
@@ -131,11 +69,11 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
13169
formatter: getLegendFormatter(config, hass),
13270
},
13371
stroke: {
134-
curve: getStrokeCurve(config),
72+
curve: getStrokeCurve(config, false),
13573
lineCap: config.chart_type === 'radialBar' ? 'round' : 'butt',
13674
colors:
13775
config.chart_type === 'pie' || config.chart_type === 'donut' ? ['var(--card-background-color)'] : undefined,
138-
width: getStrokeWidth(config),
76+
width: getStrokeWidth(config, false),
13977
},
14078
markers: {
14179
showNullDataPoints: false,
@@ -158,17 +96,90 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
15896
return config.apex_config ? mergeDeep(mergeDeep(def, conf), config.apex_config) : mergeDeep(def, conf);
15997
}
16098

161-
function getFillOpacity(config: ChartCardConfig): number[] {
162-
return config.series_in_graph.map((serie) => {
99+
export function getBrushLayoutConfig(
100+
config: ChartCardConfig,
101+
hass: HomeAssistant | undefined = undefined,
102+
id: string,
103+
): unknown {
104+
const locales = getLocales();
105+
const def = {
106+
chart: {
107+
locales: [
108+
(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || getDefaultLocale(),
109+
],
110+
defaultLocale:
111+
(config.locale && locales[config.locale] && config.locale) ||
112+
(hass?.language && locales[hass.language] && hass.language) ||
113+
'en',
114+
type: config.chart_type || DEFAULT_SERIE_TYPE,
115+
stacked: config?.stacked,
116+
foreColor: 'var(--primary-text-color)',
117+
width: '100%',
118+
height: '120px',
119+
zoom: {
120+
enabled: false,
121+
},
122+
toolbar: {
123+
show: false,
124+
},
125+
id: Math.random().toString(36).substring(7),
126+
brush: {
127+
target: id,
128+
enabled: true,
129+
},
130+
},
131+
grid: {
132+
strokeDashArray: 3,
133+
},
134+
fill: {
135+
opacity: getFillOpacity(config, true),
136+
type: getFillType(config, true),
137+
},
138+
series: getSeries(config, hass, true),
139+
xaxis: getXAxis(config, hass),
140+
yaxis: {
141+
tickAmount: 2,
142+
decimalsInFloat: DEFAULT_FLOAT_PRECISION,
143+
},
144+
tooltip: {
145+
enabled: false,
146+
},
147+
dataLabels: {
148+
enabled: false,
149+
},
150+
legend: {
151+
show: false,
152+
},
153+
stroke: {
154+
curve: getStrokeCurve(config, true),
155+
lineCap: config.chart_type === 'radialBar' ? 'round' : 'butt',
156+
colors:
157+
config.chart_type === 'pie' || config.chart_type === 'donut' ? ['var(--card-background-color)'] : undefined,
158+
width: getStrokeWidth(config, true),
159+
},
160+
markers: {
161+
showNullDataPoints: false,
162+
},
163+
noData: {
164+
text: 'Loading...',
165+
},
166+
};
167+
return config.brush?.apex_config ? mergeDeep(def, config.brush.apex_config) : def;
168+
}
169+
170+
function getFillOpacity(config: ChartCardConfig, brush: boolean): number[] {
171+
const series = brush ? config.series_in_brush : config.series_in_graph;
172+
return series.map((serie) => {
163173
return serie.opacity !== undefined ? serie.opacity : serie.type === 'area' ? DEFAULT_AREA_OPACITY : 1;
164174
});
165175
}
166176

167-
function getSeries(config: ChartCardConfig, hass: HomeAssistant | undefined) {
177+
function getSeries(config: ChartCardConfig, hass: HomeAssistant | undefined, brush: boolean) {
178+
const series = brush ? config.series_in_brush : config.series_in_graph;
168179
if (TIMESERIES_TYPES.includes(config.chart_type)) {
169-
return config?.series_in_graph.map((serie, index) => {
180+
return series.map((serie, index) => {
170181
return {
171-
name: computeName(index, config.series_in_graph, undefined, hass?.states[serie.entity]),
182+
name: computeName(index, series, undefined, hass?.states[serie.entity]),
172183
type: serie.type,
173184
data: [],
174185
};
@@ -365,8 +376,9 @@ function getLegendFormatter(config: ChartCardConfig, hass: HomeAssistant | undef
365376
};
366377
}
367378

368-
function getStrokeCurve(config: ChartCardConfig) {
369-
return config.series_in_graph.map((serie) => {
379+
function getStrokeCurve(config: ChartCardConfig, brush: boolean) {
380+
const series = brush ? config.series_in_brush : config.series_in_graph;
381+
return series.map((serie) => {
370382
return serie.curve || 'smooth';
371383
});
372384
}
@@ -377,22 +389,24 @@ function getDataLabels_enabledOnSeries(config: ChartCardConfig) {
377389
});
378390
}
379391

380-
function getStrokeWidth(config: ChartCardConfig) {
392+
function getStrokeWidth(config: ChartCardConfig, brush: boolean) {
381393
if (config.chart_type !== undefined && config.chart_type !== 'line')
382394
return config.apex_config?.stroke?.width === undefined ? 3 : config.apex_config?.stroke?.width;
383-
return config.series_in_graph.map((serie) => {
395+
const series = brush ? config.series_in_brush : config.series_in_graph;
396+
return series.map((serie) => {
384397
if (serie.stroke_width !== undefined) {
385398
return serie.stroke_width;
386399
}
387400
return [undefined, 'line', 'area'].includes(serie.type) ? 5 : 0;
388401
});
389402
}
390403

391-
function getFillType(config: ChartCardConfig) {
404+
function getFillType(config: ChartCardConfig, brush: boolean) {
392405
if (!config.experimental?.color_threshold) {
393-
return config.apex_config?.fill?.type || 'solid';
406+
return brush ? config.brush?.apex_config?.fill?.type || 'solid' : config.apex_config?.fill?.type || 'solid';
394407
} else {
395-
return config.series_in_graph.map((serie) => {
408+
const series = brush ? config.series_in_brush : config.series_in_graph;
409+
return series.map((serie) => {
396410
if (
397411
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
398412
!PLAIN_COLOR_TYPES.includes(config.chart_type!) &&

0 commit comments

Comments
 (0)