diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index 5131208..13af149 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -1,273 +1,337 @@ views: - title: Main + panel: true cards: - - type: custom:apexcharts-card - apex_config: - chart: - toolbar: - show: true - tools: - zoom: true - zoomin: true - zoomout: true - pan: true - reset: true - zoom: - enabled: true - update_interval: 5sec - stacked: true - series: - - entity: sensor.random_0_1000 - name: Sensor 1 - type: area - curve: straight - color: yellow - - entity: sensor.random_0_1000 - name: Sensor 2 - type: area - offset: -1d - graph_span: 15min - cache: true - layout: minimal - header: - show: false + - type: grid + columns: 3 + cards: + - type: custom:apexcharts-card + apex_config: + chart: + toolbar: + show: true + tools: + zoom: true + zoomin: true + zoomout: true + pan: true + reset: true + zoom: + enabled: true + update_interval: 5sec + stacked: true + series: + - entity: sensor.random_0_1000 + name: Sensor 1 + type: area + curve: straight + color: yellow + - entity: sensor.random_0_1000 + name: Sensor 2 + type: area + offset: -1d + graph_span: 15min + cache: true + layout: minimal + header: + show: false - - type: custom:apexcharts-card - stacked: true - span: - offset: -1d - series: - - entity: sensor.random_0_1000 - name: RAM Usage - unit: Mb - type: area - curve: smooth - - entity: sensor.random_0_1000 - name: Ram Free - type: area - graph_span: 15min - cache: true - layout: minimal - header: - floating: true + - type: custom:apexcharts-card + stacked: true + span: + offset: -1d + series: + - entity: sensor.random_0_1000 + name: RAM Usage + unit: Mb + type: area + curve: smooth + - entity: sensor.random_0_1000 + name: Ram Free + type: area + graph_span: 15min + cache: true + layout: minimal + header: + floating: true - - type: custom:apexcharts-card - stacked: false - series: - - entity: sensor.random_0_1000 - name: test1 - type: column - - entity: sensor.random_0_1000 - name: test2 - type: column - graph_span: 20min - cache: true - - type: custom:apexcharts-card - graph_span: 200h - series: - - entity: sensor.humidity - curve: straight + - type: custom:apexcharts-card + stacked: false + series: + - entity: sensor.random_0_1000 + name: test1 + type: column + - entity: sensor.random_0_1000 + name: test2 + type: column + graph_span: 20min + cache: true + - type: custom:apexcharts-card + graph_span: 200h + series: + - entity: sensor.humidity + curve: straight - - type: custom:apexcharts-card - graph_span: 1h - header: - title: Test Aggregate - show: true - show_states: true - colorize_states: true - series: - - entity: sensor.random0_100 - name: AVG - curve: smooth - type: line - show: - as_duration: millisecond - group_by: - duration: 10min - func: avg - - entity: sensor.random0_100 - name: AVG - curve: smooth - type: line - show: - as_duration: minute - group_by: - duration: 10min - func: avg - - entity: sensor.random0_100 - curve: smooth - name: MIN - type: line - show: - as_duration: hour - group_by: - duration: 10min - func: min - - entity: sensor.random0_100 - curve: smooth - name: MAX - type: line - show: - as_duration: day - group_by: - duration: 10min - func: max - - entity: sensor.random0_100 - curve: smooth - name: LAST - type: line - show: - as_duration: month - group_by: - duration: 10min - func: last - - entity: sensor.random0_100 - curve: smooth - name: FIRST - type: line - show: - as_duration: year - group_by: - duration: 10min - func: first + - type: custom:apexcharts-card + graph_span: 1h + header: + title: Test Aggregate + show: true + show_states: true + colorize_states: true + series: + - entity: sensor.random0_100 + name: AVG + curve: smooth + type: line + show: + as_duration: millisecond + group_by: + duration: 10min + func: avg + - entity: sensor.random0_100 + name: AVG + curve: smooth + type: line + show: + as_duration: minute + group_by: + duration: 10min + func: avg + - entity: sensor.random0_100 + curve: smooth + name: MIN + type: line + show: + as_duration: hour + group_by: + duration: 10min + func: min + - entity: sensor.random0_100 + curve: smooth + name: MAX + type: line + show: + as_duration: day + group_by: + duration: 10min + func: max + - entity: sensor.random0_100 + curve: smooth + name: LAST + type: line + show: + as_duration: month + group_by: + duration: 10min + func: last + - entity: sensor.random0_100 + curve: smooth + name: FIRST + type: line + show: + as_duration: year + group_by: + duration: 10min + func: first - - type: custom:apexcharts-card - graph_span: 4h - header: - title: Test - series: - - entity: sensor.humidity - type: area - name: Outside Humidity - group_by: - func: avg - duration: 1h - - entity: sensor.random0_100 - type: area - name: Office Humidity - group_by: - func: avg - duration: 1h - - type: sensor - entity: sensor.humidity - graph: line + - type: custom:apexcharts-card + graph_span: 4h + header: + title: Test + series: + - entity: sensor.humidity + type: area + name: Outside Humidity + group_by: + func: avg + duration: 1h + - entity: sensor.random0_100 + type: area + name: Office Humidity + group_by: + func: avg + duration: 1h + - type: sensor + entity: sensor.humidity + graph: line - - type: custom:apexcharts-card - graph_span: 6h - header: - show: false - series: - - entity: sensor.humidity - type: line - name: Outside Humidity - group_by: - func: avg - duration: 30min - - entity: sensor.random0_100 - type: column - name: Office Humidity - group_by: - func: avg - duration: 30min + - type: custom:apexcharts-card + graph_span: 6h + header: + show: false + series: + - entity: sensor.humidity + type: line + name: Outside Humidity + group_by: + func: avg + duration: 30min + - entity: sensor.random0_100 + type: column + name: Office Humidity + group_by: + func: avg + duration: 30min - - type: custom:apexcharts-card - header: - show: true - title: Start of day - graph_span: 24h - span: - start: day - apex_config: - dataLabels: - enabled: true - dropShadow: - enabled: true - series: - - entity: sensor.random0_100 - extend_to_end: false - type: column - group_by: - func: avg - fill: 'null' + - type: custom:apexcharts-card + header: + show: true + title: Start of day + graph_span: 24h + span: + start: day + apex_config: + dataLabels: + enabled: true + dropShadow: + enabled: true + series: + - entity: sensor.random0_100 + extend_to_end: false + type: column + group_by: + func: avg + fill: 'null' - - type: custom:apexcharts-card - header: - show: true - title: Last 8h - graph_span: 8h - span: - end: hour - apex_config: - dataLabels: - enabled: true - dropShadow: - enabled: true - yaxis: - - title: - text: test1 + - type: custom:apexcharts-card + header: show: true - opposite: true - decimalsInFloat: 1 - - title: - text: Test2 + title: Last 8h + graph_span: 8h + span: + end: hour + apex_config: + dataLabels: + enabled: true + dropShadow: + enabled: true + yaxis: + - title: + text: test1 + show: true + opposite: true + decimalsInFloat: 1 + - title: + text: Test2 + show: true + # opposite: true + decimalsInFloat: 1 + series: + - entity: sensor.random0_100 + extend_to_end: false + type: column + invert: true + group_by: + func: avg + fill: 'last' + show: + legend_value: false + - entity: sensor.random0_100 + extend_to_end: false + type: column + group_by: + func: avg + fill: 'last' + - entity: sensor.random0_100 + extend_to_end: false + type: column + group_by: + func: avg + fill: 'last' + + - type: custom:apexcharts-card + span: + start: day + y_axis_precision: 5 + header: show: true - # opposite: true - decimalsInFloat: 1 - series: - - entity: sensor.random0_100 - extend_to_end: false - type: column - invert: true - group_by: - func: avg - fill: 'last' - show: - legend_value: false - - entity: sensor.random0_100 - extend_to_end: false - type: column - group_by: - func: avg - fill: 'last' - - entity: sensor.random0_100 - extend_to_end: false - type: column - group_by: - func: avg - fill: 'last' + show_states: true + series: + - entity: sensor.pvpc + float_precision: 5 + data_generator: | + return [...Array(22).keys()].map((hour) => { + const attr = 'price_' + `${hour}`.padStart(2, '0') + 'h'; + const value = entity.attributes[attr]; + return [moment().startOf('day').hours(hour).valueOf(), value]; + }) - - type: custom:apexcharts-card - span: - start: day - y_axis_precision: 5 - header: - show: true - show_states: true - series: - - entity: sensor.pvpc - float_precision: 5 - data_generator: | - return [...Array(22).keys()].map((hour) => { - const attr = 'price_' + `${hour}`.padStart(2, '0') + 'h'; - const value = entity.attributes[attr]; - return [moment().startOf('day').hours(hour).valueOf(), value]; - }) + - type: custom:apexcharts-card + graph_span: 8d + span: + start: hour + # apex_config: + # dataLabels: + # enabled: true + header: + show: true + title: Precipitation Forecast + series: + - entity: weather.openweathermap + type: area + extend_to_end: false + data_generator: | + return entity.attributes.forecast.map((entry) => { + return [new Date(entry.datetime), entry.temperature]; + }); - - type: custom:apexcharts-card - graph_span: 8d - span: - start: hour - # apex_config: - # dataLabels: - # enabled: true - header: - show: true - title: Precipitation Forecast - series: - - entity: weather.openweathermap - type: area - extend_to_end: false - data_generator: | - return entity.attributes.forecast.map((entry) => { - return [new Date(entry.datetime), entry.temperature]; - }); + - type: custom:apexcharts-card + header: + show: true + title: radialBar + chart_type: radialBar + series: + - entity: sensor.random0_100 + - entity: sensor.random_0_1000 + min: 0 + max: 1000 + - type: custom:apexcharts-card + chart_type: pie + header: + show: true + title: pie + series: + - entity: sensor.random0_100 + - entity: sensor.random_0_1000 + - type: custom:apexcharts-card + chart_type: donut + header: + show: true + title: donut + series: + - entity: sensor.random0_100 + - entity: sensor.random_0_1000 + - type: custom:apexcharts-card + chart_type: line + graph_span: 15m + header: + show: true + title: line + series: + - entity: sensor.random0_100 + - entity: sensor.random_0_1000 + - type: custom:apexcharts-card + header: + show: true + title: Scatter + chart_type: scatter + apex_config: + responsive: + - breakpoint: 400 + options: + chart: + height: 500px + - breakpoint: 5000 + options: + chart: + height: auto + series: + - entity: sensor.random0_100 + group_by: + func: sum + duration: 20min + - entity: sensor.random_0_1000 + group_by: + func: sum + duration: 20min diff --git a/README.md b/README.md index 38a976d..9f5440a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ However, some things might be broken :grin: - [`header` Options](#header-options) - [`group_by` Options](#group_by-options) - [`func` Options](#func-options) + - [`chart_type` Options](#chart_type-options) - [`span` Options](#span-options) - [`data_generator` Option](#data_generator-option) - [Apex Charts Options Example](#apex-charts-options-example) @@ -103,6 +104,7 @@ The card stricly validates all the options available (but not for the `apex_conf | ---- | :--: | :-----: | :---: | ----------- | | :white_check_mark: `type` | string | | v1.0.0 | `custom:apexcharts-card` | | :white_check_mark: `series` | array | | v1.0.0 | See [series](#series-options) | +| `chart_type` | string | `line` | NEXT_VERSION | See [chart_type](#chart_type-options) | | `update_interval` | string | | v1.1.0 | By default the card updates on every state change. Setting this overrides the behaviour. Valid values are any time string, eg: `1h`, `12min`, `1d`, `1h25`, `10sec`, ... | | `graph_span` | string | `24h` | v1.1.0 | The span of the graph as a time interval. Valid values are any time string, eg: `1h`, `12min`, `1d`, `1h25`, `10sec`, ... | | `span` | object | | v1.2.0 | See [span](#span-options) | @@ -132,7 +134,8 @@ The card stricly validates all the options available (but not for the `apex_conf | `invert` | boolean | `false` | v1.2.0 | Negates the data (`1` -> `-1`). Usefull to display opposites values like network in (standard)/out (inverted) | | `data_generator` | string | | v1.2.0 | See [data_generator](#data_generator-option) | | `offset` | string | | v1.3.0 | This is different from the main `offset` parameter. This is at the series level. It is only usefull if you want to display data from for eg. yesterday on top of the data from today for the same sensor and compare the data. The time displayed in the tooltip will be wrong as will the x axis information. Valid values are any negative time string, eg: `-1h`, `-12min`, `-1d`, `-1h25`, `-10sec`, ... | - +| `min` | number | `0` | NEXT_VERSION | 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` | NEXT_VERSION | 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 | ### `series.show` Options @@ -180,6 +183,18 @@ The card stricly validates all the options available (but not for the `apex_conf | `median` | v1.0.0 | Will return the median of all the states in each bucket | | `delta` | v1.0.0 | Will return the delta between the biggest and smallest state in each bucket | +### `chart_type` Options + +| Name | Since | Description | +| ---- | :---: | ----------- | +| `line` | v1.0.0 | This is the default and will show a timeline. It is compatible with `series.type` = `column`, `line` and `area` | +| `scatter` | NEXT_VERSION | Displays a cloud of points without a line between the values | +| `pie` | NEXT_VERSION | This will display a pie chart with the last value computed of each sensor | +| `donut` | NEXT_VERSION | This will display a donut chart with the last value computed of each sensor, same as pie but with a hole in the center | +| `radialBar` | NEXT_VERSION | This will display a radial bar chart with the last value computed of each sensor. The value is represented in percentage only. It is required to provide `min` and `max` for each series displayed as it requires to convert the value into percentage. The default value for `min` is `0` and for `max` it is `100`. This graph works well if you want to display sensors natively in percentages | + +![Charts Type](docs/charts_type.png) + ### `span` Options | Name | Since | Description | @@ -346,7 +361,7 @@ For code junkies, you'll find the default options I use in [`src/apex-layouts.ts Not ordered by priority: -* [ ] Support more types of charts (pie, radial, polar area at least) +* [X] ~~Support more types of charts (pie, radial, polar area at least)~~ * [ ] Support for `binary_sensors` * [X] ~~Support for aggregating data with exact boundaries (ex: aggregating data with `1h` could aggregate from `2:00:00am` to `2:59:59am` then `3:00:00am` to `3:59:59` exactly, etc...)~~ * [X] ~~Display the graph from start of day, week, month, ... with support for "up to now" or until the "end of the period"~~ diff --git a/docs/charts_type.png b/docs/charts_type.png new file mode 100644 index 0000000..09605a7 Binary files /dev/null and b/docs/charts_type.png differ diff --git a/src/apex-layouts.ts b/src/apex-layouts.ts index 6ca4ce5..befde2d 100644 --- a/src/apex-layouts.ts +++ b/src/apex-layouts.ts @@ -1,12 +1,13 @@ import { HomeAssistant } from 'custom-card-helpers'; import parse from 'parse-duration'; -import { DEFAULT_FLOAT_PRECISION, HOUR_24, moment, NO_VALUE } from './const'; +import { DEFAULT_FLOAT_PRECISION, DEFAULT_SERIE_TYPE, HOUR_24, moment, NO_VALUE, TIMESERIES_TYPES } from './const'; import { ChartCardConfig } from './types'; import { computeName, computeUom, mergeDeep, prettyPrintTime } from './utils'; export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | undefined = undefined): unknown { const def = { chart: { + type: config.chart_type || DEFAULT_SERIE_TYPE, stacked: config?.stacked, // type: 'line', foreColor: 'var(--primary-text-color)', @@ -26,20 +27,29 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u return serie.type === 'area' ? 0.7 : 1; }), }, - series: config?.series.map((serie, index) => { - return { - name: computeName(index, config, undefined, hass?.states[serie.entity]), - type: serie.type, - data: [], - }; - }), - xaxis: { - type: 'datetime', - // range: getMilli(config.hours_to_show), - labels: { - datetimeUTC: false, - }, - }, + series: TIMESERIES_TYPES.includes(config.chart_type) + ? config?.series.map((serie, index) => { + return { + name: computeName(index, config, undefined, hass?.states[serie.entity]), + type: serie.type, + data: [], + }; + }) + : [], + labels: TIMESERIES_TYPES.includes(config.chart_type) + ? [] + : config.series.map((serie, index) => { + return computeName(index, config, undefined, hass?.states[serie.entity]); + }), + xaxis: TIMESERIES_TYPES.includes(config.chart_type) + ? { + type: 'datetime', + // range: getMilli(config.hours_to_show), + labels: { + datetimeUTC: false, + }, + } + : {}, yaxis: Array.isArray(config.apex_config?.yaxis) ? undefined : { @@ -98,7 +108,19 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u return lValue; }, }, + plotOptions: { + radialBar: + config.chart_type === 'radialBar' + ? { + track: { + background: 'rgba(128, 128, 128, 0.2)', + }, + } + : {}, + }, legend: { + position: 'bottom', + show: true, formatter: function (_, opts, conf = config, hass2 = hass) { const name = computeName( opts.seriesIndex, @@ -109,7 +131,9 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u if (!conf.series[opts.seriesIndex].show.legend_value) { return [name]; } else { - let value = opts.w.globals.series[opts.seriesIndex].slice(-1)[0]; + let value = TIMESERIES_TYPES.includes(config.chart_type) + ? opts.w.globals.series[opts.seriesIndex].slice(-1)[0] + : opts.w.globals.series[opts.seriesIndex]; if ( value !== null && typeof value === 'number' && @@ -150,7 +174,9 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u curve: config.series.map((serie) => { return serie.curve || 'smooth'; }), - lineCap: 'butt', + lineCap: config.chart_type === 'radialBar' ? 'round' : 'butt', + colors: + config.chart_type === 'pie' || config.chart_type === 'donut' ? ['var(--card-background-color)'] : undefined, }, markers: { showNullDataPoints: false, diff --git a/src/apexcharts-card.ts b/src/apexcharts-card.ts index 3df5d1a..345d1c0 100644 --- a/src/apexcharts-card.ts +++ b/src/apexcharts-card.ts @@ -9,6 +9,7 @@ import { computeName, computeUom, decompress, + getPercentFromValue, log, mergeDeep, offsetData, @@ -24,7 +25,7 @@ import GraphEntry from './graphEntry'; import { createCheckers } from 'ts-interface-checker'; import { ChartCardExternalConfig } from './types-config'; import exportedTypeSuite from './types-config-ti'; -import { DEFAULT_FLOAT_PRECISION, DEFAULT_SHOW_LEGEND_VALUE, moment, NO_VALUE } from './const'; +import { DEFAULT_FLOAT_PRECISION, DEFAULT_SHOW_LEGEND_VALUE, moment, NO_VALUE, TIMESERIES_TYPES } from './const'; import { DEFAULT_COLORS, DEFAULT_DURATION, @@ -222,7 +223,8 @@ class ChartsCard extends LitElement { this._colors![index] = serie.color; } serie.extend_to_end = serie.extend_to_end !== undefined ? serie.extend_to_end : true; - serie.type = serie.type || DEFAULT_SERIE_TYPE; + serie.type = this._config?.chart_type ? undefined : serie.type || DEFAULT_SERIE_TYPE; + serie.unit = this._config?.chart_type === 'radialBar' ? '%' : serie.unit; if (!serie.group_by) { serie.group_by = { duration: DEFAULT_DURATION, func: DEFAULT_FUNC, fill: DEFAULT_GROUP_BY_FILL }; } else { @@ -372,49 +374,73 @@ class ChartsCard extends LitElement { ), ); await Promise.all(promise); - const graphData = { - series: this._graphs.map((graph) => { - if (!graph || graph.history.length === 0) return { data: [] }; - const index = graph.index; - if (graph.history.length > 0) { - this._lastState[index] = graph.history[graph.history.length - 1][1]; - if ( - this._lastState[index] !== null && - typeof this._lastState[index] === 'number' && - !Number.isInteger(this._lastState[index]) - ) { - const precision = - this._config?.series[index].float_precision === undefined - ? DEFAULT_FLOAT_PRECISION - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._config.series[index].float_precision!; - this._lastState[index] = (this._lastState[index] as number).toFixed(precision); + let graphData: unknown = {}; + if (TIMESERIES_TYPES.includes(this._config.chart_type)) { + graphData = { + series: this._graphs.map((graph, index) => { + if (!graph || graph.history.length === 0) return { data: [] }; + this._lastState[index] = this._computeLastState(graph.history[graph.history.length - 1][1], index); + let data: EntityCachePoints = []; + if (this._config?.series[index].extend_to_end && this._config?.series[index].type !== 'column') { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + data = [...graph.history, ...([[end.getTime(), graph.history.slice(-1)[0]![1]]] as EntityCachePoints)]; + } else { + data = graph.history; } - } - let data: EntityCachePoints = []; - if (this._config?.series[index].extend_to_end && this._config?.series[index].type !== 'column') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - data = [...graph.history, ...([[end.getTime(), graph.history.slice(-1)[0]![1]]] as EntityCachePoints)]; - } else { - data = graph.history; - } - data = offsetData(data, this._seriesOffset[index]); - return this._config?.series[index].invert ? { data: this._invertData(data) } : { data }; - }), - xaxis: { - min: start.getTime(), - max: this._findEndOfChart(end), - }, - colors: computeColors(this._colors), - }; + data = offsetData(data, this._seriesOffset[index]); + return this._config?.series[index].invert ? { data: this._invertData(data) } : { data }; + }), + xaxis: { + min: start.getTime(), + max: this._findEndOfChart(end), + }, + colors: computeColors(this._colors), + }; + } else { + // No timeline charts + graphData = { + series: this._graphs.map((graph, index) => { + if (!graph || graph.history.length === 0) return; + const lastState = graph.history[graph.history.length - 1][1]; + this._lastState[index] = this._computeLastState(lastState, index); + if (lastState === null) { + return; + } else { + if (this._config?.chart_type === 'radialBar') { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return getPercentFromValue(lastState, this._config.series[index].min, this._config.series[index].max); + } else { + return lastState; + } + } + }), + colors: computeColors(this._colors), + }; + } this._lastState = [...this._lastState]; - this._apexChart?.updateOptions(graphData, false, false); + this._apexChart?.updateOptions( + graphData, + false, + TIMESERIES_TYPES.includes(this._config.chart_type) ? false : true, + ); } catch (err) { log(err); } this._updating = false; } + private _computeLastState(value: number | null, index: number): string | number | null { + if (value !== null && typeof value === 'number' && !Number.isInteger(value)) { + const precision = + this._config?.series[index].float_precision === undefined + ? DEFAULT_FLOAT_PRECISION + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._config.series[index].float_precision!; + return (value as number).toFixed(precision); + } + return value; + } + /* Makes the chart end at the last timestamp of the data when everything displayed is a column and group_by is enabled for every serie diff --git a/src/const.ts b/src/const.ts index cb83d91..88bed2d 100644 --- a/src/const.ts +++ b/src/const.ts @@ -33,3 +33,7 @@ export const DEFAULT_COLORS = [ ]; export const NO_VALUE = 'N/A'; +export const TIMESERIES_TYPES = ['line', 'scatter', undefined]; + +export const DEFAULT_MIN = 0; +export const DEFAULT_MAX = 100; diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index f29ca3f..037536d 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -6,6 +6,7 @@ import * as t from "ts-interface-checker"; export const ChartCardExternalConfig = t.iface([], { "type": t.lit('custom:apexcharts-card'), + "chart_type": t.opt(t.union(t.lit('line'), t.lit('scatter'), t.lit('pie'), t.lit('donut'), t.lit('radialBar'))), "update_interval": t.opt("string"), "series": t.array("ChartCardSeriesExternalConfig"), "graph_span": t.opt("string"), @@ -39,6 +40,8 @@ export const ChartCardSeriesExternalConfig = t.iface([], { "invert": t.opt("boolean"), "data_generator": t.opt("string"), "float_precision": t.opt("number"), + "min": t.opt("number"), + "max": t.opt("number"), "offset": t.opt("string"), "show": t.opt(t.iface([], { "as_duration": t.opt("ChartCardPrettyTime"), diff --git a/src/types-config.ts b/src/types-config.ts index dbe5309..b89ff2b 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -1,5 +1,6 @@ export interface ChartCardExternalConfig { type: 'custom:apexcharts-card'; + chart_type?: 'line' | 'scatter' | 'pie' | 'donut' | 'radialBar'; update_interval?: string; series: ChartCardSeriesExternalConfig[]; graph_span?: string; @@ -35,6 +36,8 @@ export interface ChartCardSeriesExternalConfig { invert?: boolean; data_generator?: string; float_precision?: number; + min?: number; + max?: number; offset?: string; show?: { as_duration?: ChartCardPrettyTime; diff --git a/src/utils.ts b/src/utils.ts index ff37a91..536b7ef 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,7 @@ import { ChartCardConfig, EntityCachePoints } from './types'; import { TinyColor } from '@ctrl/tinycolor'; import parse from 'parse-duration'; import { ChartCardPrettyTime } from './types-config'; -import { moment, NO_VALUE } from './const'; +import { DEFAULT_MAX, DEFAULT_MIN, moment, NO_VALUE } from './const'; export function compress(data: unknown): string { return lzStringCompress(JSON.stringify(data)); @@ -142,3 +142,9 @@ export function prettyPrintTime(value: string | number | null, unit: ChartCardPr if (value === null) return NO_VALUE; return moment.duration(value, unit).format('y[y] d[d] h[h] m[m] s[s] S[ms]', { trim: 'both' }); } + +export function getPercentFromValue(value: number, min: number | undefined, max: number | undefined): number { + const lMin = min === undefined ? DEFAULT_MIN : min; + const lMax = max === undefined ? DEFAULT_MAX : max; + return ((value - lMin) * 100) / (lMax - lMin); +}