Skip to content

Commit

Permalink
feat: Native multi y-axis support with auto-scale (#160)
Browse files Browse the repository at this point in the history
* feat: initial multi-y axis support

* doc: Add documentation fo yaxis

* fix: Remove unneeded types

Fixes #158
  • Loading branch information
RomRider authored May 24, 2021
1 parent 524dcae commit e08aa14
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 5 deletions.
25 changes: 25 additions & 0 deletions .devcontainer/ui-lovelace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -991,3 +991,28 @@ views:
}
series:
- entity: sensor.random0_100

- type: custom:apexcharts-card
graph_span: 20min
all_series_config:
stroke_width: 2
series:
- entity: sensor.random0_100
yaxis_id: first
- entity: sensor.random_0_1000
yaxis_id: second
- entity: sensor.random0_100
yaxis_id: first
transform: 'return Number(x) + 30;'
- entity: sensor.random0_100
yaxis_id: first
transform: 'return Number(x) - 30;'
yaxis:
- id: first
apex_config:
tickAmount: 4
- id: second
opposite: true
show: true
apex_config:
tickAmount: 4
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ However, some things might be broken :grin:
- [Using the card](#using-the-card)
- [Main Options](#main-options)
- [`series` Options](#series-options)
- [`series.show` Options](#seriesshow-options)
- [series' `show` Options](#series-show-options)
- [Main `show` Options](#main-show-options)
- [`header` Options](#header-options)
- [`now` Options](#now-options)
Expand All @@ -40,6 +40,7 @@ However, some things might be broken :grin:
- [`span` Options](#span-options)
- [`transform` Option](#transform-option)
- [`data_generator` Option](#data_generator-option)
- [`yaxis` Options. Multi-Y axis](#yaxis-options-multi-y-axis)
- [Apex Charts Options Example](#apex-charts-options-example)
- [Layouts](#layouts)
- [Configuration Templates](#configuration-templates)
Expand Down Expand Up @@ -139,7 +140,8 @@ The card stricly validates all the options available (but not for the `apex_conf
| `layout` | string | | v1.0.0 | See [layouts](#layouts) |
| `header` | object | | v1.0.0 | See [header](#header-options) |
| `now` | object | | v1.5.0 | See [now](#now-options) |
| `y_axis_precision` | numnber | `1` | v1.2.0 | The float precision used to display numbers on the Y axis |
| `y_axis_precision` | number | `1` | v1.2.0 | The float precision used to display numbers on the Y axis. Only works if `yaxis` is undefined. |
| `yaxis` | array | | NEXT_VERSION | See [yaxis](#yaxis-options-multi-y-axis) |
| `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. |
Expand Down Expand Up @@ -171,8 +173,10 @@ The card stricly validates all the options available (but not for the `apex_conf
| `min` | number | `0` | v1.4.0 | Only used when `chart_type = radialBar`, see [chart_type](#chart_type-options). Used to convert the value into a percentage. Minimum value of the sensor |
| `max` | number | `100` | v1.4.0 | Only used when `chart_type = radialBar`, see [chart_type](#chart_type-options). Used to convert the value into a percentage. Maximum value of the sensor |
| `color_threshold` | object | | v1.6.0 | See [experimental](#experimental-features) |
| `yaxis_id` | string | | NEXT_VERSION | The identification name of the y-axis which this serie should be associated to. See [yaxis](#yaxis-options-multi-y-axis) |
| `show` | object | | v1.3.0 | See [serie's show options](#series-show-options) |

### `series.show` Options
### series' `show` Options

| Name | Type | Default | Since | Description |
| ---- | :--: | :-----: | :---: | ----------- |
Expand Down Expand Up @@ -419,6 +423,55 @@ Let's take this example:

* And this is all you need :tada:

### `yaxis` Options. Multi-Y axis

:warning: If this option is used, you can't define `yaxis` in the main `apex_config` option as it will be overriden.

You can have as many y-axis as there are series defined in your configuration or less.

| Name | Type | Default | Since | Description |
| ---- | :--: | :-----: | :---: | ----------- |
| :white_check_mark: `id` | string | | NEXT_VERSION | The identification name of the yaxis used to map it to a serie. Needs to be unique. |
| `show` | boolean | `true` | NEXT_VERSION | Whether to show or not the axis on the chart |
| `opposite` | boolean | `false` | NEXT_VERSION | If `true`, the axis will be shown on the right side of the chart |
| `min` | `auto` or number | `auto` | NEXT_VERSION | If undefined or `auto`, the `min` of the yaxis will be automatically calculated based on the min value of all the series associated to this axis. If a number is set, the min will be forced to this number |
| `max` | `auto` or number | `auto` | NEXT_VERSION | If undefined or `auto`, the `min` of the yaxis will be automatically calculated based on the max value of all the series associated to this axis. If a number is set, the max will be forced to this number |
| `apex_config` | object | | NEXT_VERSION | Any configuration from https://apexcharts.com/docs/options/yaxis/, except `min`, `max`, `show` and `opposite` |

In this example, we have 2 sensors:
* `sensor.random0_100`: goes from `0` to `100`
* `sensor.random_0_1000`: goes from `0` to `1000`

The `min` and `max` of both y-axis are auto calculated based on the spread of the data associated with each axis.

![multi_y_axis](docs/multi_y_axis.png)

```yaml
type: custom:apexcharts-card
graph_span: 20min
yaxis:
- id: first # identification name of the first y-axis
apex_config:
tickAmount: 4
- id: second # identification name of the second y-axis
opposite: true # make it show on the right side
apex_config:
tickAmount: 4
all_series_config:
stroke_width: 2
series:
- entity: sensor.random0_100
yaxis_id: first # this serie will be associated to the 'id: first' axis.
- entity: sensor.random_0_1000
yaxis_id: second # this serie will be associated to the 'id: second' axis.
- entity: sensor.random0_100
yaxis_id: first # this serie will be associated to the 'id: first' axis.
transform: 'return Number(x) + 30;' # We make it go fom 30 to 130
- entity: sensor.random0_100
yaxis_id: first # this serie will be associated to the 'id: first' axis.
transform: 'return Number(x) - 30;' # We make it go from -30 to 70
```

### Apex Charts Options Example

This is how you could change some options from ApexCharts as described on the [`Options (Reference)` menu entry](https://apexcharts.com/docs/installation/).
Expand Down
Binary file added docs/multi_y_axis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/apex-layouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ function getXAxis(config: ChartCardConfig, hass: HomeAssistant | undefined) {
}

function getYAxis(config: ChartCardConfig) {
return Array.isArray(config.apex_config?.yaxis)
return Array.isArray(config.apex_config?.yaxis) || config.yaxis
? undefined
: {
decimalsInFloat: config.y_axis_precision === undefined ? DEFAULT_FLOAT_PRECISION : config.y_axis_precision,
Expand Down
116 changes: 115 additions & 1 deletion src/apexcharts-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import 'array-flat-polyfill';
import { LitElement, html, customElement, property, TemplateResult, CSSResult, PropertyValues } from 'lit-element';
import { ifDefined } from 'lit-html/directives/if-defined';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import { ChartCardConfig, ChartCardSeriesConfig, EntityCachePoints, EntityEntryCache, HistoryPoint } from './types';
import {
ChartCardConfig,
ChartCardSeriesConfig,
ChartCardYAxis,
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 @@ -137,6 +144,8 @@ class ChartsCard extends LitElement {

private _brushSelectionSpan = 0;

private _yAxisConfig?: ChartCardYAxis[];

@property({ type: Boolean }) private _warning = false;

public connectedCallback() {
Expand Down Expand Up @@ -331,6 +340,25 @@ class ChartsCard extends LitElement {

const defColors = this._config?.color_list || DEFAULT_COLORS;
if (this._config) {
if (this._config.yaxis && this._config.yaxis.length > 1) {
if (
this._config.series.some((serie) => {
return !serie.yaxis_id;
})
) {
throw new Error(`Multiple yaxis detected: Some series are missing the 'yaxis_id' configuration.`);
}
}
if (this._config.yaxis) {
const yAxisConfig = this._generateYAxisConfig(this._config);
if (this._config.apex_config) {
this._config.apex_config.yaxis = yAxisConfig;
} else {
this._config.apex_config = {
yaxis: yAxisConfig,
};
}
}
this._graphs = this._config.series.map((serie, index) => {
serie.index = index;
if (!this._headerColors[index]) {
Expand Down Expand Up @@ -421,6 +449,47 @@ class ChartsCard extends LitElement {
this._reset();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _generateYAxisConfig(config: ChartCardConfig): any {
if (!config.yaxis) return undefined;
const burned: boolean[] = [];
this._yAxisConfig = JSON.parse(JSON.stringify(config.yaxis));
const yaxisConfig = config.series.map((serie, serieIndex) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const idx = config.yaxis!.findIndex((yaxis) => {
return yaxis.id === serie.yaxis_id;
});
if (idx < 0) {
throw new Error(`yaxis_id: ${serie.yaxis_id} doesn't exist.`);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let yAxisDup: ChartCardYAxis = JSON.parse(JSON.stringify(config.yaxis![idx]));
delete yAxisDup.apex_config;
if (this._yAxisConfig?.[idx].series_id) {
this._yAxisConfig?.[idx].series_id?.push(serieIndex);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._yAxisConfig![idx].series_id! = [serieIndex];
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (config.yaxis![idx].apex_config) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
yAxisDup = mergeDeep(JSON.parse(JSON.stringify(config.yaxis![idx])), config.yaxis![idx].apex_config);
}
if (typeof yAxisDup.min !== 'number') delete yAxisDup.min;
if (typeof yAxisDup.max !== 'number') delete yAxisDup.max;
if (burned[idx]) {
yAxisDup.show = false;
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
yAxisDup.show = config.yaxis![idx].show === undefined ? true : config.yaxis![idx].show;
burned[idx] = true;
}
return yAxisDup;
});
return yaxisConfig;
}

static get styles(): CSSResult {
return styles;
}
Expand Down Expand Up @@ -614,6 +683,9 @@ class ChartsCard extends LitElement {
return;
});
graphData.annotations = this._computeAnnotations(start, end, now);
if (this._yAxisConfig) {
graphData.yaxis = this._computeYAxisAutoMinMax(start, end);
}
if (!this._apexBrush) {
graphData.xaxis = {
min: start.getTime(),
Expand Down Expand Up @@ -882,6 +954,48 @@ class ChartsCard extends LitElement {
return {};
}

private _computeYAxisAutoMinMax(start: Date, end: Date) {
if (!this._config) return;
this._yAxisConfig?.map((yaxis) => {
if (typeof yaxis.min !== 'number' || typeof yaxis.max !== 'number') {
const minMax = yaxis.series_id?.map((id) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const lMinMax = this._graphs![id]?.minMaxWithTimestamp(start.getTime(), end.getTime());
if (!lMinMax) return undefined;
if (this._config?.series[id].invert && lMinMax.min[1] !== null) {
lMinMax.min[1] = -lMinMax.min[1];
}
if (this._config?.series[id].invert && lMinMax.max[1] !== null) {
lMinMax.min[1] = -lMinMax.max[1];
}
return lMinMax;
});
let min: number | null = 0;
let max: number | null = 0;
minMax?.forEach((elt) => {
if (!elt) return;
if (min === undefined || min === null) {
min = elt.min[1];
} else if (elt.min[1] !== null && min > elt.min[1]) {
min = elt.min[1];
}
if (max === undefined || max === null) {
max = elt.max[1];
} else if (elt.max[1] !== null && max < elt.max[1]) {
max = elt.max[1];
}
});
yaxis.series_id?.forEach((id) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (yaxis.min === undefined || yaxis.min === 'auto') this._config!.apex_config!.yaxis![id].min = min;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (yaxis.max === undefined || yaxis.max === 'auto') this._config!.apex_config!.yaxis![id].max = max;
});
}
});
return this._config?.apex_config?.yaxis;
}

private _computeChartColors(brush: boolean): (string | (({ value }) => string))[] {
const defaultColors: (string | (({ value }) => string))[] = computeColors(brush ? this._brushColors : this._colors);
const series = brush ? this._config?.series_in_brush : this._config?.series_in_graph;
Expand Down
2 changes: 2 additions & 0 deletions src/graphEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export default class GraphEntry {

public minMaxWithTimestamp(start: number, end: number): { min: HistoryPoint; max: HistoryPoint } | undefined {
if (!this._computedHistory || this._computedHistory.length === 0) return undefined;
if (this._computedHistory.length === 1)
return { min: [start, this._computedHistory[0][1]], max: [end, this._computedHistory[0][1]] };
return this._computedHistory.reduce(
(acc: { min: HistoryPoint; max: HistoryPoint }, point) => {
if (point[1] === null) return acc;
Expand Down
12 changes: 12 additions & 0 deletions src/types-config-ti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const ChartCardExternalConfig = t.iface([], {
"index": t.opt("number"),
"view_index": t.opt("number"),
"brush": t.opt("ChartCardBrushExtConfig"),
"yaxis": t.opt(t.array("ChartCardYAxisExternal")),
});

export const ChartCardChartType = t.union(t.lit('line'), t.lit('scatter'), t.lit('pie'), t.lit('donut'), t.lit('radialBar'));
Expand Down Expand Up @@ -87,6 +88,7 @@ export const ChartCardAllSeriesExternalConfig = t.iface([], {
})),
"transform": t.opt("string"),
"color_threshold": t.opt(t.array("ChartCardColorThreshold")),
"yaxis_id": t.opt("string"),
});

export const ChartCardSeriesShowConfigExt = t.iface([], {
Expand Down Expand Up @@ -128,6 +130,15 @@ export const ChartCardColorThreshold = t.iface([], {
"opacity": t.opt("number"),
});

export const ChartCardYAxisExternal = t.iface([], {
"id": "string",
"show": t.opt("boolean"),
"opposite": t.opt("boolean"),
"min": t.opt(t.union(t.lit('auto'), "number", "string")),
"max": t.opt(t.union(t.lit('auto'), "number", "string")),
"apex_config": t.opt("any"),
});

const exportedTypeSuite: t.ITypeSuite = {
ChartCardExternalConfig,
ChartCardChartType,
Expand All @@ -142,5 +153,6 @@ const exportedTypeSuite: t.ITypeSuite = {
GroupByFunc,
ChartCardHeaderExternalConfig,
ChartCardColorThreshold,
ChartCardYAxisExternal,
};
export default exportedTypeSuite;
12 changes: 12 additions & 0 deletions src/types-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface ChartCardExternalConfig {
index?: number;
view_index?: number;
brush?: ChartCardBrushExtConfig;
yaxis?: ChartCardYAxisExternal[];
}

export type ChartCardChartType = 'line' | 'scatter' | 'pie' | 'donut' | 'radialBar';
Expand Down Expand Up @@ -87,6 +88,7 @@ export interface ChartCardAllSeriesExternalConfig {
};
transform?: string;
color_threshold?: ChartCardColorThreshold[];
yaxis_id?: string;
}

export interface ChartCardSeriesShowConfigExt {
Expand Down Expand Up @@ -127,3 +129,13 @@ export interface ChartCardColorThreshold {
color?: string;
opacity?: number;
}

export interface ChartCardYAxisExternal {
id: string;
show?: boolean;
opposite?: boolean;
min?: 'auto' | number;
max?: 'auto' | number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apex_config?: any;
}
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChartCardExternalConfig,
ChartCardSeriesExternalConfig,
ChartCardSeriesShowConfigExt,
ChartCardYAxisExternal,
GroupByFill,
GroupByFunc,
} from './types-config';
Expand All @@ -15,6 +16,7 @@ export interface ChartCardConfig extends ChartCardExternalConfig {
cache: boolean;
useCompress: boolean;
apex_config?: ApexOptions;
yaxis?: ChartCardYAxis[];
}

export interface ChartCardSeriesConfig extends ChartCardSeriesExternalConfig {
Expand Down Expand Up @@ -63,3 +65,7 @@ export interface HistoryBucket {
}

export type HistoryBuckets = Array<HistoryBucket>;

export interface ChartCardYAxis extends ChartCardYAxisExternal {
series_id?: number[];
}

0 comments on commit e08aa14

Please sign in to comment.