ApexCharts card - A highly customizable graph card

Thanks for this, worked a treat, been looking for a way to do this for ages, also used it to changed the Now colour based on a state.

now:
      show: true
      label: >-
        ${states['climate.immersion_heater_thermostat'].attributes.current_temperature + '°'}
      color: >-
        ${states['binary_sensor.immersion_heater_heating'].state === 'on' ? 'red' : 'cyan'}
1 Like

trying to figure out the data_generator, but need some help, please…

if this is my sensor and attributes list:

how would I need to change this config, so it actually renders anything…?

  - type: custom:apexcharts-card
    graph_span: 48h
    update_interval: 1h
    header:
      show: true
      title: Solar Forecast
    span:
      start: day
    now:
      show: true
      label: Now
    yaxis:
      - id: power
        decimals: 0
    apex_config:
      plotOptions:
        bar:
          columnWidth: 100%
      stroke:
        width: 2
    series:
      - entity: sensor.solar_forecast_estimate_watts
        name: Solar power
        yaxis_id: power
        data_generator: |
          return entity.attributes.watts.map((entry) => {
            return [new Date(entry).getTime(), entry];
          });

        type: column
        show:
          extremas: true
        opacity: 1
        float_precision: 3

I’ve copied the above form some other sensor attributes for price, but they contain an actual named KVP like

and use:

    series:
      - entity: sensor.energyzero_today_energy_average_price
        name: Price this hour
        yaxis_id: price
        data_generator: |
          return entity.attributes.today.map((entry) => {
            return [new Date(entry.timestamp).getTime(), entry.price];
          });

that wont work now obviously,

but I fail to see how I should change to fix it.

please have a look? thanks!

How does this look like, if you paste the sensor in Dev-tools/state ?

Assuming that is white space before the value, you could try this.

        data_generator: |
          return entity.attributes.watts.map((entry) => {
          const myArray = Watts.split(" ");
            return [new Date(myArray[0]).getTime(), myArray[1]];
          });

thanks for your reply, unfortunately that throws an error:

Error: TypeError: entity.attributes.watts.map is not a function in 'return entity.attributes.watts.map((entry) => {
const myArray = Watts.split(" ");
  return [new Da...'

is there a way to test these data generators? given the fact its JS I cant get my head around any online tool with this, we need the frontend info?

to be precise, the output of:

{{state_attr('sensor.solar_forecast_estimate_watts','watts')}}

is:

{'2023-03-24T06:34:00+01:00': 0, '2023-03-24T07:00:00+01:00': 145, '2023-03-24T08:00:00+01:00': 222, '2023-03-24T09:00:00+01:00': 329, '2023-03-24T10:00:00+01:00': 414, '2023-03-24T11:00:00+01:00': 473, '2023-03-24T12:00:00+01:00': 515, '2023-03-24T13:00:00+01:00': 577, '2023-03-24T14:00:00+01:00': 1175, '2023-03-24T15:00:00+01:00': 1395, '2023-03-24T16:00:00+01:00': 1054, '2023-03-24T17:00:00+01:00': 627, '2023-03-24T18:00:00+01:00': 258, '2023-03-24T19:00:00+01:00': 70, '2023-03-24T19:02:00+01:00': 0, '2023-03-25T06:31:00+01:00': 0, '2023-03-25T07:00:00+01:00': 187, '2023-03-25T08:00:00+01:00': 329, '2023-03-25T09:00:00+01:00': 413, '2023-03-25T10:00:00+01:00': 480, '2023-03-25T11:00:00+01:00': 594, '2023-03-25T12:00:00+01:00': 689, '2023-03-25T13:00:00+01:00': 1207, '2023-03-25T14:00:00+01:00': 2140, '2023-03-25T15:00:00+01:00': 2188, '2023-03-25T16:00:00+01:00': 1808, '2023-03-25T17:00:00+01:00': 969, '2023-03-25T18:00:00+01:00': 360, '2023-03-25T19:00:00+01:00': 108, '2023-03-25T19:04:00+01:00': 0}

maybe I need the .items() here too, like in the jinja templates for template-entity-row I use:

      {% if states[config.entity] is not none %}
        {% set attr =
            state_attr(config.entity,'watts').items()
            |sort(attribute='1',reverse=True) %}
        {% set peak = (attr|first| default(('Unknown',0))) %}
         {{peak[1]}} W op {{as_datetime(peak[0]).strftime('%-d-%m')}} om {{as_datetime(peak[0]).strftime('%-H')}} uur
      {% else %} Initializing
      {% endif %}

or is that what the .map((entry) does in the data_generator?

maybe its no a float? figured it is, because of no quotes in the KVP

checking the objects doc on W3, doesnt help me a lot…

I have reached a similar solution a while back (lots of searching)

      - entity: sensor.solar_forecast
        show:
          in_header: false
          legend_value: false
        name: today
        yaxis_id: value
        color: '#ffa600'
        opacity: 1
        stroke_width: 1
        float_precision: 1
        unit: kWh
        offset: '-1h'
        data_generator: >
          let res = []; for (const [key, value] of
          Object.entries(entity.attributes.forecast.result)) {
            res.push([new Date(key).getTime(), value/1000]);
          } return res

in my graph (this is a combined card obviously)
image

4 Likes

EDIT:
vingerha already replied, this is the explination why.

The map function only works on ‘lists’, like arrays, you can recognize them by the square brackets [].
“entity.attributes.watts” is a dict, like objects, recognized by the curly brackets.

To convert an dict to a list, you could indeed use the items() function in jinja templates. But the data_gerenator is uses javascript. So then you use the function Object.entries().
Can you test the data_generator like this:

        data_generator: |
          return Object.entries(entity.attributes.watts).map((entry) => {
             ...
2 Likes

thanks! I figured that to be the case, but fail to fix the generator…

        data_generator: |
          return Object.entries(entity.attributes.watts).map((entry) => {
          const myArray = Watts.split(" ");
            return [new Date(myArray[0]).getTime(), myArray[1]];
          });

should I completely do away with the const myArray, and use the .push construction?

because I did test:

        data_generator: |
          let res = []; for (const [key, value] of
            Object.entries(entity.attributes.watts).map((entry) => {
            res.push([new Date(key).getTime(), value]);
            } return res;

and still it does not render

wait, there more to change:

        data_generator: |
          let res = []; for (const [key, value] of
            Object.entries(entity.attributes.watts)) {
            res.push([new Date(key).getTime(), value]);
            } return res;

now shows!

yeas!!. Now time for some formatting :wink: maybe even color_thresholds…
cool, thx @studioIngrid and @vingerha , much appreciated!

so, full config now:

Why is there a 0 in the legend…?

  - type: custom:apexcharts-card
    graph_span: 48h
    update_interval: 1h
    header:
      show: true
      title: Solar Forecast
    experimental:
      color_threshold: true
    span:
      start: day
    now:
      show: true
      label: Now
    yaxis:
      - id: estimate
      - id: actueel
        show: false
    apex_config:
      plotOptions:
        bar:
          columnWidth: 100%
    series:
      - entity: sensor.solar_forecast_estimate_watts
        name: Solar power
        yaxis_id: estimate
        data_generator: |
          let res = []; for (const [key, value] of
            Object.entries(entity.attributes.watts)) {
            res.push([new Date(key).getTime(), value]);
            } return res;
        color_threshold:
          - value: 0
            color: cornsilk
          - value: 500
            color: papayawhip
          - value: 1000
            color: yellow
          - value: 1500
            color: gold
          - value: 2000
            color: orange
          - value: 2500
            color: darkorange
          - value: 3000
            color: tomato
          - value: 3500
            color: sandybrown
          - value: 4000
            color: peru
          - value: 4500
            color: orangered
          - value: 5000
            color: maroon
          - value: 5500
            color: purple
          - value: 6000
            color: darkred
        type: column
        show:
          extremas: true
        opacity: 1
        float_precision: 0
        stroke_width: 5

      - entity: sensor.zp_actuele_opbrengst
        yaxis_id: actueel
#         transform: return x /1000;
#         unit: kW
        type: line
        name: Actueel
        stroke_width: 2
        extend_to: false
        color: grey
        group_by:
          func: min
          duration: 60m
        show:
          legend_value: true
        float_precision: 0

aside form the colors threshold colors (the lower colors are too light), id love to have those bars we wider, and leave less whitespace. Is there some default config setting for that, or do I simply need to set the stroke_width to a higher number.
figured the columnWidth: 100% should be doing that, but apparently not.

I can set the
stroke:
width:

global option, because then it also makes the secondary line fat and I want that to be a thinner line. like in

more like this, whihc I copied at first, but still showed very thin bars…

Try with:

group_by:
   func: last ( or raw )

maybe also “duration: 1month” beneath “func”

Try this “Global”(by this you can control columWidth), and then you can stil add on a individual line-serie “stroke_width: 5”


    all_series_config:
      stroke_width: 1
      group_by:
        func: min
        duration: 1h

PS: No need for “stroke_width” on/inside column-serie

1 Like

thanks, that works beautifully. And not needing an individual stroke_width now:

type: custom:apexcharts-card
graph_span: 48h
update_interval: 1h
header:
  show: true
  title: Solar Forecast
experimental:
  color_threshold: true

all_series_config:
  stroke_width: 1
  group_by:
    func: min
    duration: 1h

span:
  start: day
now:
  show: true
  label: Now
yaxis:
  - id: estimate
    decimals: 0
  - id: actueel
    decimals: 0
    show: false
apex_config:
  plotOptions:
    bar:
      columnWidth: 100%
series:
  - entity: sensor.solar_forecast_estimate_watts
    name: Solar power
    yaxis_id: estimate
    data_generator: |
      let res = []; for (const [key,value] of
        Object.entries(entity.attributes.watts)) {
        res.push([new Date(key).getTime(),value]);
        } return res;
    color_threshold:
      - value: 0
        color: moccasin
      - value: 500
        color: khaki
      - value: 1000
        color: yellow
      - value: 1500
        color: gold
      - value: 2000
        color: orange
      - value: 2500
        color: darkorange
      - value: 3000
        color: tomato
      - value: 3500
        color: sandybrown
      - value: 4000
        color: peru
      - value: 4500
        color: orangered
      - value: 5000
        color: maroon
      - value: 5500
        color: purple
      - value: 6000
        color: darkred
    type: column
    show:
      extremas: true
#       legend_value: true
#         opacity: 1
#         float_precision: 0
#     stroke_width: 8

  - entity: sensor.zp_actuele_opbrengst
    yaxis_id: actueel
#         transform: return x /1000;
#         unit: kW
    type: line
    name: Actueel
#     stroke_width: 1
    extend_to: false
    color: grey

shows as:

still dont get the legend to be 0 on Solar power though. does this not find the value in the data object maybe?
btw, can we set the left Y-axis scale to use the same numbers as I set in the color_thresholds, this is a bit random somehow…

If you are interested:

Did not work, because the variable “Watts” was never defined.

This is a little more complicated. The map function is essentially shorthand for the “for” function. So here you try to do a double “for”.

Good luck with your next coding adventure.

yeah, the second of your post above I get :wink:
and I had already seen and changed that into:

    data_generator: |
      let res = []; for (const [key,value] of
        Object.entries(entity.attributes.watts)) {
        res.push([new Date(key).getTime(),value]);
        } return res;

however, on your first remark on the variable not being defined in

        data_generator: |
          return Object.entries(entity.attributes.watts).map((entry) => {
          const myArray = Watts.split(" ");
            return [new Date(myArray[0]).getTime(), myArray[1]];
          });

how could I fix that (so define Watts)? simply by adding

let Watts = [];

?

cant test right now, because forecast solar is out for the moment

Hi Marius,

The line const myArray = Watts.split(" ");. means, take the string (it only works on strings) from Watts and split in into a list on every space.

You are correct let Watts would define the variable, and let Watts = [] would fill that variable with an empty list. But there isn’t any actual data there, only an empty list. So you would need a function to fill the variable Watts with a string of data.

The function list.map(x)=>{}, it is the same as a for loop: for x in list. Within the {} you can use the value x.

We know the value x will consist of two fields: a time and a value. Thats what the const [key, value] means. It says to not return the values as one variable x, but as two separate variables a key and a value.

Is there anything you would still like to accomplice?

I’ve got a ApexChart working fine for most devices:

type: custom:apexcharts-card
graph_span: 24h
span:
  start: day
now:
  show: true
  label: Nu
header:
  show: true
  title: Stroomprijs vandaag (€/kWh)
series:
  - entity: sensor.nextenergy_average_electricity_price_today
    stroke_width: 2
    float_precision: 3
    type: column
    opacity: 1
    color: ''
    data_generator: |
      return entity.attributes.prices.map((record, index) => {
        return [record.time, record.price];
      });
yaxis:
  - id: Prijs
    decimals: 2
    min: 0

For some reason this one stays completely empty on my wallmount iPad Air 2 (ios maxed at 15.7.3). Other ApexCharts do show. Is this known or is there something wrong in my code?

For reference, the chart does show on iphone devices with ios 16 and Windows / MacOS devices.

well first of all, thanks.

I guess I would like to use the simplest, of, rephrase, most economical of generators. If any difference at all.

since I can only get the

    data_generator: |
      let res = []; for (const [key,value] of
        Object.entries(entity.attributes.watts)) {
        res.push([new Date(key).getTime(),value]);
        } return res;

to do what is required, I seem to have no choice, but, you might yet see a better way of converting those KVP and feeding them to the apex-chart.
If that were the case, Id be very interested.

one other detail:
my data refreshes each hour, but the secondary (now) entity is a life updating entity. Updating the apex-chart per hour as I do, might require some further customizing of the handling of that entity in the chart?

always check the repo for issues: Graph not displayed on iPad/ iPhone companion app or Safari IOS · Issue #434 · RomRider/apexcharts-card · GitHub
seems identical?

Sure looks like the same issue. I’ll add my experiences to this issue. thanks.

The issue has been marked closed because of inactivity. So I won’t get my hopes up.

I know I can use tickAmount to set a fixed amount of ticks, but depending on the series values I often end up like below. What I would like to have is in the exmaple below even “5 ticks”, so for the right series “0, 5, 10, 15…”. Is that possible somehow? I can change tickAmount and get it to look good, but when sensors increase/decrease this all changes again. Not sure what to search for:)

type: custom:apexcharts-card
header:
  title: ''
  show: false
  show_states: true
graph_span: 7day
update_interval: 1min
apex_config:
  title:
    text: Luftfuktighet & utetemp
    align: left
    margin: 0
    offsetX: 0
    offsetY: 10
    floating: false
    style:
      fontSize: 15px
      fontWeight: 200
      fontFamily: Segoe UI Light
      color: '#FFFFFF'
  tooltip:
    enabled: true
    x:
      format: HH:MM
  xaxis:
    tooltip:
      enabled: false
    axisBorder:
      show: false
    labels:
      style:
        colors: white
        fontSize: 9px
        fontFamily: Arial
  chart:
    height: 225
  grid:
    show: true
    borderColor: rgba(220, 220, 220, 0.1)
    strokeDashArray: 4
    position: back
  legend:
    show: false
  plotOptions:
    bar:
      borderRadius: 1
yaxis:
  - id: left
    min: 25
    max: ~50
    apex_config:
      tickAmount: 5
      forceNiceScale: true
      decimalsInFloat: 0
      labels:
        style:
          colors: '#ffffff'
          fontSize: 9px
          fontFamily: Arial
  - id: right
    min: ~0
    max: ~15
    opposite: true
    apex_config:
      tickAmount: 6
      forceNiceScale: true
      decimalsInFloat: 0
      labels:
        style:
          colors: '#ffffff'
          fontSize: 9px
          fontFamily: Arial
series:
  - entity: sensor.arbetsrummet_humidity
    name: Arbetsrummet
    yaxis_id: left
    type: line
    stroke_width: 1.5
    color: 2eb9ff
    group_by:
      func: avg
      duration: 120min
  - entity: sensor.sovrummet_luftfuktighet
    name: Sovrummet
    yaxis_id: left
    type: line
    stroke_width: 1
    color: rgba(240, 240, 240, 0.7)
    group_by:
      func: avg
      duration: 120min
  - entity: sensor.nibe_105703_40004
    name: Utetemperatur
    yaxis_id: right
    type: column
    stroke_width: 1
    color: 1b6d96
    group_by:
      func: avg
      duration: 120min

1 Like