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: Native multi y-axis support with auto-scale #160

Merged
merged 3 commits into from
May 24, 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
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[];
}