Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(brush): Support for a timeframe selection chart #102

Merged
merged 4 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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