Skip to content

Commit

Permalink
feat(brush): Support for a timeframe selection chart (RomRider#102)
Browse files Browse the repository at this point in the history
* Initial brush support

* working brush

* Update doc

* refactor locales
  • Loading branch information
RomRider authored Feb 26, 2021
1 parent 3a441f9 commit c8dedf7
Show file tree
Hide file tree
Showing 12 changed files with 455 additions and 142 deletions.
27 changes: 27 additions & 0 deletions .devcontainer/ui-lovelace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -834,3 +834,30 @@ views:
group_by:
func: sum
duration: 120min

- type: custom:apexcharts-card
experimental:
color_threshold: true
brush: true
graph_span: 2h
brush:
selection_span: 10m
series:
- entity: sensor.random0_100
color: blue
type: area
stroke_width: 1
color_threshold:
- value: 0
color: red
- value: 50
color: yellow
- value: 100
color: green
- entity: sensor.random0_100
color: blue
stroke_width: 1
float_precision: 0
show:
in_brush: true
in_chart: false
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ However, some things might be broken :grin:
- [Configuration options](#configuration-options)
- [`color_threshold` experimental feature](#color_threshold-experimental-feature)
- [`hidden_by_default` experimental feature](#hidden_by_default-experimental-feature)
- [`brush` experimental feature](#brush-experimental-feature)
- [Known issues](#known-issues)
- [Roadmap](#roadmap)
- [Examples](#examples)
Expand Down Expand Up @@ -142,6 +143,7 @@ The card stricly validates all the options available (but not for the `apex_conf
| `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) |
| `experimental` | object | | v1.6.0 | See [experimental](#experimental-features) |
| `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. |
| `brush` | object | | NEXT_VERSION | See [brush](#brush-experimental-feature) |



Expand Down Expand Up @@ -182,6 +184,7 @@ The card stricly validates all the options available (but not for the `apex_conf
| `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`) |
| `hidden_by_default` | boolean | `false` | v1.6.0 | See [experimental](#hidden_by_default-experimental-feature) |
| `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`. |
| `in_brush` | boolean | `false` | NEXT_VERSION | See [brush](#brush-experimental-feature) |


### Main `show` Options
Expand Down Expand Up @@ -580,6 +583,7 @@ Generates the same result as repeating the configuration in each series:
| `color_threshold` | boolean | `false` | v1.6.0 | Will enable the color threshold feature. See [color_threshold](#color_threshold-experimental-feature) |
| `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. |
| `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) |
| `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) |

### `color_threshold` experimental feature

Expand Down Expand Up @@ -646,6 +650,58 @@ series:
- entity: sensor.temperature_office
```

### `brush` experimental feature

`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.

![brush_image](docs/brush.png)

Things to know:
* 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.
* All the features from normal series can be applied to the brush series
* You can configure the bottom chart the way you want with another specific `apex_config` also
* It might be compute heavy and slow with a lot of history data points
* It is recommended to not show too much data on the bottom chart for the sake of lisibility

Here is how to use it (this represents the chart above):
```yaml
type: custom:apexcharts-card
experimental:
color_threshold: true
brush: true # This is required
graph_span: 2h # This will represent the span of the brush
brush:
# selection_span: optional
# defines the default selected span in the brush
# Defaults to 1/4 of the `graph_span`
selection_span: 10m
# apex_config: optional
apex_config:
# Any ApexCharts settings you want to apply to the brush
# Same as the standard apex_config
series:
- entity: sensor.random0_100
color: blue
type: area
stroke_width: 1
color_threshold:
- value: 0
color: red
- value: 50
color: yellow
- value: 100
color: green
- entity: sensor.random0_100
color: blue
stroke_width: 1
float_precision: 0
show:
# in_brush: set it to true and the serie will show up in the brush
in_brush: true
# add this also if you want your serie to only show up in the brush
in_chart: false
```
## Known issues
* 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)
Expand Down
Binary file added docs/brush.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@ export default [
watch: {
exclude: 'node_modules/**',
},
globals: {
apexcharts: 'ApexCharts',
},
},
];
182 changes: 98 additions & 84 deletions src/apex-layouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,77 +12,15 @@ import {
import { ChartCardConfig } from './types';
import { computeName, computeUom, is12Hour, mergeDeep, prettyPrintTime, truncateFloat } from './utils';
import { layoutMinimal } from './layouts/minimal';
import * as ca from 'apexcharts/dist/locales/ca.json';
import * as cs from 'apexcharts/dist/locales/cs.json';
import * as de from 'apexcharts/dist/locales/de.json';
import * as el from 'apexcharts/dist/locales/el.json';
import * as en from 'apexcharts/dist/locales/en.json';
import * as es from 'apexcharts/dist/locales/es.json';
import * as fi from 'apexcharts/dist/locales/fi.json';
import * as fr from 'apexcharts/dist/locales/fr.json';
import * as he from 'apexcharts/dist/locales/he.json';
import * as hi from 'apexcharts/dist/locales/hi.json';
import * as hr from 'apexcharts/dist/locales/hr.json';
import * as hy from 'apexcharts/dist/locales/hy.json';
import * as id from 'apexcharts/dist/locales/id.json';
import * as it from 'apexcharts/dist/locales/it.json';
import * as ka from 'apexcharts/dist/locales/ka.json';
import * as ko from 'apexcharts/dist/locales/ko.json';
import * as lt from 'apexcharts/dist/locales/lt.json';
import * as nb from 'apexcharts/dist/locales/nb.json';
import * as nl from 'apexcharts/dist/locales/nl.json';
import * as pl from 'apexcharts/dist/locales/pl.json';
import * as pt_br from 'apexcharts/dist/locales/pt-br.json';
import * as pt from 'apexcharts/dist/locales/pt.json';
import * as rs from 'apexcharts/dist/locales/rs.json';
import * as ru from 'apexcharts/dist/locales/ru.json';
import * as se from 'apexcharts/dist/locales/se.json';
import * as sk from 'apexcharts/dist/locales/sk.json';
import * as sl from 'apexcharts/dist/locales/sl.json';
import * as sq from 'apexcharts/dist/locales/sq.json';
import * as th from 'apexcharts/dist/locales/th.json';
import * as tr from 'apexcharts/dist/locales/tr.json';
import * as ua from 'apexcharts/dist/locales/ua.json';
import * as zh_cn from 'apexcharts/dist/locales/zh-cn.json';
import { getLocales, getDefaultLocale } from './locales';

export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | undefined = undefined): unknown {
const locales = {
ca: ca,
cs: cs,
de: de,
el: el,
en: en,
es: es,
fi: fi,
fr: fr,
he: he,
hi: hi,
hr: hr,
hy: hy,
id: id,
it: it,
ka: ka,
ko: ko,
lt: lt,
nb: nb,
nl: nl,
pl: pl,
'pt-br': pt_br,
pt: pt,
rs: rs,
ru: ru,
se: se,
sk: sk,
sl: sl,
sq: sq,
th: th,
tr: tr,
ua: ua,
'zh-cn': zh_cn,
};
const locales = getLocales();
const def = {
chart: {
locales: [(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || en],
locales: [
(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || getDefaultLocale(),
],
defaultLocale:
(config.locale && locales[config.locale] && config.locale) ||
(hass?.language && locales[hass.language] && hass.language) ||
Expand All @@ -102,10 +40,10 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
strokeDashArray: 3,
},
fill: {
opacity: getFillOpacity(config),
type: getFillType(config),
opacity: getFillOpacity(config, false),
type: getFillType(config, false),
},
series: getSeries(config, hass),
series: getSeries(config, hass, false),
labels: getLabels(config, hass),
xaxis: getXAxis(config, hass),
yaxis: getYAxis(config),
Expand All @@ -131,11 +69,11 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
formatter: getLegendFormatter(config, hass),
},
stroke: {
curve: getStrokeCurve(config),
curve: getStrokeCurve(config, false),
lineCap: config.chart_type === 'radialBar' ? 'round' : 'butt',
colors:
config.chart_type === 'pie' || config.chart_type === 'donut' ? ['var(--card-background-color)'] : undefined,
width: getStrokeWidth(config),
width: getStrokeWidth(config, false),
},
markers: {
showNullDataPoints: false,
Expand All @@ -158,17 +96,90 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u
return config.apex_config ? mergeDeep(mergeDeep(def, conf), config.apex_config) : mergeDeep(def, conf);
}

function getFillOpacity(config: ChartCardConfig): number[] {
return config.series_in_graph.map((serie) => {
export function getBrushLayoutConfig(
config: ChartCardConfig,
hass: HomeAssistant | undefined = undefined,
id: string,
): unknown {
const locales = getLocales();
const def = {
chart: {
locales: [
(config.locale && locales[config.locale]) || (hass?.language && locales[hass.language]) || getDefaultLocale(),
],
defaultLocale:
(config.locale && locales[config.locale] && config.locale) ||
(hass?.language && locales[hass.language] && hass.language) ||
'en',
type: config.chart_type || DEFAULT_SERIE_TYPE,
stacked: config?.stacked,
foreColor: 'var(--primary-text-color)',
width: '100%',
height: '120px',
zoom: {
enabled: false,
},
toolbar: {
show: false,
},
id: Math.random().toString(36).substring(7),
brush: {
target: id,
enabled: true,
},
},
grid: {
strokeDashArray: 3,
},
fill: {
opacity: getFillOpacity(config, true),
type: getFillType(config, true),
},
series: getSeries(config, hass, true),
xaxis: getXAxis(config, hass),
yaxis: {
tickAmount: 2,
decimalsInFloat: DEFAULT_FLOAT_PRECISION,
},
tooltip: {
enabled: false,
},
dataLabels: {
enabled: false,
},
legend: {
show: false,
},
stroke: {
curve: getStrokeCurve(config, true),
lineCap: config.chart_type === 'radialBar' ? 'round' : 'butt',
colors:
config.chart_type === 'pie' || config.chart_type === 'donut' ? ['var(--card-background-color)'] : undefined,
width: getStrokeWidth(config, true),
},
markers: {
showNullDataPoints: false,
},
noData: {
text: 'Loading...',
},
};
return config.brush?.apex_config ? mergeDeep(def, config.brush.apex_config) : def;
}

function getFillOpacity(config: ChartCardConfig, brush: boolean): number[] {
const series = brush ? config.series_in_brush : config.series_in_graph;
return series.map((serie) => {
return serie.opacity !== undefined ? serie.opacity : serie.type === 'area' ? DEFAULT_AREA_OPACITY : 1;
});
}

function getSeries(config: ChartCardConfig, hass: HomeAssistant | undefined) {
function getSeries(config: ChartCardConfig, hass: HomeAssistant | undefined, brush: boolean) {
const series = brush ? config.series_in_brush : config.series_in_graph;
if (TIMESERIES_TYPES.includes(config.chart_type)) {
return config?.series_in_graph.map((serie, index) => {
return series.map((serie, index) => {
return {
name: computeName(index, config.series_in_graph, undefined, hass?.states[serie.entity]),
name: computeName(index, series, undefined, hass?.states[serie.entity]),
type: serie.type,
data: [],
};
Expand Down Expand Up @@ -365,8 +376,9 @@ function getLegendFormatter(config: ChartCardConfig, hass: HomeAssistant | undef
};
}

function getStrokeCurve(config: ChartCardConfig) {
return config.series_in_graph.map((serie) => {
function getStrokeCurve(config: ChartCardConfig, brush: boolean) {
const series = brush ? config.series_in_brush : config.series_in_graph;
return series.map((serie) => {
return serie.curve || 'smooth';
});
}
Expand All @@ -377,22 +389,24 @@ function getDataLabels_enabledOnSeries(config: ChartCardConfig) {
});
}

function getStrokeWidth(config: ChartCardConfig) {
function getStrokeWidth(config: ChartCardConfig, brush: boolean) {
if (config.chart_type !== undefined && config.chart_type !== 'line')
return config.apex_config?.stroke?.width === undefined ? 3 : config.apex_config?.stroke?.width;
return config.series_in_graph.map((serie) => {
const series = brush ? config.series_in_brush : config.series_in_graph;
return series.map((serie) => {
if (serie.stroke_width !== undefined) {
return serie.stroke_width;
}
return [undefined, 'line', 'area'].includes(serie.type) ? 5 : 0;
});
}

function getFillType(config: ChartCardConfig) {
function getFillType(config: ChartCardConfig, brush: boolean) {
if (!config.experimental?.color_threshold) {
return config.apex_config?.fill?.type || 'solid';
return brush ? config.brush?.apex_config?.fill?.type || 'solid' : config.apex_config?.fill?.type || 'solid';
} else {
return config.series_in_graph.map((serie) => {
const series = brush ? config.series_in_brush : config.series_in_graph;
return series.map((serie) => {
if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
!PLAIN_COLOR_TYPES.includes(config.chart_type!) &&
Expand Down
Loading

0 comments on commit c8dedf7

Please sign in to comment.