ApexCharts card - A highly customizable graph card

Well it turned out that I did not look deep enough, or actually in the structure. The 2nd list is under a "next" level, so by just adding another data_generator I fixed it kind of - however if anyone has a better solution please let me know. I am still new to this kind of programming.

For reference here is the latest code that gives me what I was looking for:

type: custom:apexcharts-card
experimental:
  color_threshold: true
apex_config:
  legend:
    show: false
  xaxis:
    labels:
      datetimeFormatter:
        hour: HH
  tooltip:
    fixed:
      enabled: true
      position: topLeft
graph_span: 2d
show:
  last_updated: true
header:
  title: Elkostnad och nyttjande
  show: true
  show_states: true
  colorize_states: true
span:
  start: day
now:
  show: true
series:
  - entity: binary_sensor.test_billigaste_tid
    name: Cheapest
    type: area
    curve: stepline
    yaxis_id: kWh
    opacity: 0.6
    stroke_width: 0
    color: lightgreen
    show:
      extremas: false
      in_header: false
    data_generator: |
      let data = [];
      let periods = entity.attributes.list || [];
      periods.forEach((p) => {
        let s = new Date(p.start).getTime();
        let e = new Date(p.end).getTime();
        data.push([s, 1]);    // jump up at start
        data.push([e, 1]);    // stay high until end
        data.push([e, 0]);    // drop straight down
      });
      return data;
  - entity: binary_sensor.test_billigaste_tid
    name: Cheapest
    type: area
    curve: stepline
    yaxis_id: kWh
    opacity: 0.6
    stroke_width: 0
    color: lightgreen
    show:
      extremas: false
      in_header: false
    data_generator: |
      let data = [];
      let periods = entity.attributes.next.list || [];
      periods.forEach((p) => {
        let s = new Date(p.start).getTime();
        let e = new Date(p.end).getTime();
        data.push([s, 1]);    // jump up at start
        data.push([e, 1]);    // stay high until end
        data.push([e, 0]);    // drop straight down
      });
      return data;
  - entity: binary_sensor.cheapest_hours
    name: Cheapest
    type: area
    curve: stepline
    yaxis_id: kWh
    opacity: 0.9
    stroke_width: 0
    color: lightgray
    show:
      extremas: false
      in_header: false
    data_generator: |
      let data = [];
      let periods = entity.attributes.list || [];
      periods.forEach((p) => {
        let s = new Date(p.start).getTime();
        let e = new Date(p.end).getTime();
        data.push([s, 1]);    // jump up at start
        data.push([e, 1]);    // stay high until end
        data.push([e, 0]);    // drop straight down
      });
      return data;
  - entity: binary_sensor.cheapest_hours
    name: Cheapest
    type: area
    curve: stepline
    yaxis_id: kWh
    opacity: 0.9
    stroke_width: 0
    color: lightgray
    show:
      extremas: false
      in_header: false
    data_generator: |
      let data = [];
      let periods = entity.attributes.next.list || [];
      periods.forEach((p) => {
        let s = new Date(p.start).getTime();
        let e = new Date(p.end).getTime();
        data.push([s, 1]);    // jump up at start
        data.push([e, 1]);    // stay high until end
        data.push([e, 0]);    // drop straight down
      });
      return data;
  - entity: sensor.electricity_prices_today
    yaxis_id: SEK
    type: line
    show:
      extremas: false
      in_header: false
    float_precision: 3
    stroke_width: 3
    extend_to: false
    color: green
    color_threshold:
      - value: 1.5
        color: blue
      - value: 2
        color: green
      - value: 2.5
        color: yellow
      - value: 3
        color: orange
      - value: 3.5
        color: red
      - value: 4
        color: pink
      - value: 5
        color: "#ffffff"
    data_generator: |
      const data = entity.attributes.data.map((entry) => {
        // Center each 15-minute period correctly
        const start = new Date(entry.start).getTime();
        const end = new Date(entry.end).getTime();
        const midpoint = start + (end - start) / 2;
        return [midpoint, entry.value];
      });
      return data;
  - entity: sensor.electricity_prices_tomorrow
    yaxis_id: SEK
    type: line
    show:
      extremas: false
      in_header: false
    float_precision: 3
    stroke_width: 3
    extend_to: false
    color: green
    color_threshold:
      - value: 1.5
        color: blue
      - value: 2
        color: green
      - value: 2.5
        color: yellow
      - value: 3
        color: orange
      - value: 3.5
        color: red
      - value: 4
        color: pink
      - value: 5
        color: "#ffffff"
    data_generator: |
      const data = entity.attributes.data.map((entry) => {
        // Center each 15-minute period correctly
        const start = new Date(entry.start).getTime();
        const end = new Date(entry.end).getTime();
        const midpoint = start + (end - start) / 2;
        return [midpoint, entry.value];
      });
      return data;    
  - entity: sensor.shellypro3em_ece334eddd28_total_active_power
    yaxis_id: kWh
    name: Power Usage Chart
    color: orange
    opacity: 0.8
    float_precision: 2
    show:
      extremas: false
      in_header: true
    type: line
    curve: stepline
    stroke_width: 1
    extend_to: false
    unit: kW
    transform: return x /1000;
    group_by:
      func: avg
      duration: 15m
  - entity: sensor.electricity_prices_today
    name: Current Price
    yaxis_id: SEK
    float_precision: 3
    show:
      in_header: true
      in_chart: false
    unit: Kr/kWh
    color: green
yaxis:
  - id: SEK
    min: 0
    max: ~6
    opposite: true
    apex_config:
      tickAmount: 6
      forceNiceScale: true
      title:
        text: SEK
  - id: kWh
    min: 0
    max: ~4
    apex_config:
      tickAmount: 6
      forceNiceScale: true
      title:
        text: kWh

@RomRider you haven't been updating/working on the project for a LONG while, so I guess you have other priorities.

I forked your repo to GitHub - dewi-ny-je/apexcharts-card: šŸ“ˆ A Lovelace card to display advanced graphs and charts based on ApexChartsJS for Home Assistant Ā· GitHub then I integrated the improvements which were introduced by @vingerha and then I updated apexcharts package to the latest version 5.15.
After doing so, I also went on and I removed duplicated/obsolete/redundant code (which was not few lines, but hundreds of lines), and the card works quite well now.

I wonder how many of the issues raised in your repo (33 open, ranging from 2021 to last week) could be solved already in my fork. Probably quite some.

Anyway, I think it's appropriate to have HACS ship my repo instead of yours, since mine has all you have, plus updates and fixes.
Before contacting HACS I am required to ask for your feedback.

Can you give the green light, or a reasoned statement if declining the proposal? In a week time I will proceed with an official request to HACS with a link to this post.

Thanks for your cooperation/attention.

10 Likes

would be great to see it in hacs

The y-axis min / max auto scaling isn't working for me. Have I got something wrong in the config?

As you can see, the max Y-axis seems to be around 40, but the peak of the series is 84.5, so it just gets clipped to fit in the box.

My code is:

type: custom:apexcharts-card
header:
  title: Electricity prices
  show: true
  show_states: true
  colorize_states: true
  standard_format: true
now:
  show: true
  color: red
  label: Now
span:
  start: day
graph_span: 46.5h
all_series_config:
  float_precision: 4
  extend_to: now
experimental:
  color_threshold: true
series:
  - entity: sensor.octopus_energy_electricity_current_rate
    name: Current price
    unit: p/kWh
    transform: return Number(x)*100
    show:
      in_chart: false
      header_color_threshold: true
      name_in_header: true
    yaxis_id: rate
    color_threshold:
      - value: -100
        color: cyan
      - value: 0
        color: lightblue
      - value: 20
        color: "#00E396"
      - value: 30
        color: "#FEB019"
      - value: 40
        color: "#FF4560"
  - entity: sensor.electricity_daily_average_rate
    name: Today's average so far
    unit: p/kWh
    transform: if (x <= 1.0) return Number(x)*100
    show:
      header_color_threshold: true
      name_in_header: true
      in_chart: false
    yaxis_id: rate
    color_threshold:
      - value: -100
        color: cyan
      - value: 0
        color: lightblue
      - value: 20
        color: "#00E396"
      - value: 30
        color: "#FEB019"
      - value: 40
        color: "#FF4560"
  - entity: sensor.electricity_daily_average_rate
    name: Today's average so far
    unit: p/kWh
    transform: if (x <= 1.0) return Number(x)*100
    show:
      in_header: false
      legend_value: false
    color: orange
    stroke_width: 2
    yaxis_id: rate
  - entity: sensor.agile_rates_prediction
    type: area
    stroke_width: 2
    name: Prediction
    unit: p/kWh
    curve: stepline
    data_generator: |
      var predictions = entity.attributes.rates.filter((entry) => {
        return entry.agileRate.result.prediction;
      });
      var all = predictions.map((entry) => {
        return [new Date(entry.deliveryStart), entry.agileRate.result.rate];
      }); if (all.length > 0) {
        var last = all[all.length - 1];
        var new_date = new Date(last[0].getTime() + 1800000);
        all.push([new_date, last[1]]);
      } return all;
    show:
      in_header: false
      legend_value: false
    color: "#557e8a"
    yaxis_id: rate
  - entity: >-
      event.octopus_energy_electricity_current_day_rates
    type: area
    stroke_width: 2
    name: Prices
    unit: p/kWh
    curve: stepline
    data_generator: >
      var series1 = entity.attributes.rates.map((entry) => {
        return [new Date(entry.start), entry.value_inc_vat*100];
      });

      var series2 =
      hass.states['event.octopus_energy_electricity_next_day_rates'].attributes.rates.map(entry
      => {
        return [new Date(entry.start), entry.value_inc_vat*100];
      });

      var all = series1.concat(series2);

      if (all.length > 0) {
        var last = all[all.length - 1];
        var new_date = new Date(last[0].getTime() + 1800000);
        all.push([new_date, last[1]]);
      }

      return all;
    show:
      in_header: false
      legend_value: false
    color: lightblue
    yaxis_id: rate
apex_config:
  legend:
    show: true
  annotations:
    yaxis:
      - "y": 0
        strokeDashArray: 0
        borderColor: "#FFFFFF"
        label:
          borderColor: "#FFFF"
      - "y": 20
        strokeDashArray: 0
        borderColor: "#00E396"
        label:
          borderColor: "#00E396"
          style:
            color: "#fff"
            background: "#00E396"
          text: Low
      - "y": 30
        strokeDashArray: 0
        borderColor: "#FEB019"
        label:
          borderColor: "#FEB019"
          style:
            color: "#fff"
            background: "#FEB019"
          text: Moderate
      - "y": 40
        strokeDashArray: 0
        borderColor: "#FF4560"
        label:
          borderColor: "#FF4560"
          style:
            color: "#fff"
            background: "#FF4560"
          text: High
yaxis:
  - id: rate
    min: ~0
    max: ~40
    decimals: 0

According to the docs, setting max to "~40" should set a soft limit that is overridden if the series contains values above that, but that isn't working.

If I set max to "auto" then it autoscales to a max of 29 (which is even more wrong) and "|+10|" autoscales to 39, so it seems clear that the problem is that the value calculated for auto-scaling is wrong, and both "~40" and "|+10|" rely on that value, so neither works correctly.

Have you tried asking a LLM about it, providing the full YAML and the link to the apexcharts card? Be sure to use more than the basic model (free tier gpt-4.1 or gemini-flash are not that good) and to enable "web search" if applicable.

Also, it would be good for a support request like yours to actually take a screenshot where the tooltip shows the peak value is shown to be higher than the Ymax...

stepSize seems to be not working when using soft bound for the max value. At first I tought it would generally not work, but then I found, that it just the upper soft bound that fives the problem.

These do not work (just give random y axis labels as if nothing is configured)

yaxis:
  min: ~-30
  max: ~12
  apex_config:
    stepSize: 3
yaxis:
  min: -30
  max: ~12
  apex_config:
    stepSize: 3

These work fine

yaxis:
  min: -30
  max: 12
  apex_config:
    stepSize: 3
yaxis:
  min: ~-30
  max: 12
  apex_config:
    stepSize: 3

Is there anything I can do to make this work with an upper soft bound?
FYI: Using forceNiceScale: true changes nothing

thanks
Thomas

hi,
great you are working on this resource, and developing it beyond its current capabilities

personally I find your request a bit unnecessarily blunt to Romrider, as he has no reason at all to retract his resource, simply because not having updated for a while.
It’s how things are.

a deadline like this is even more upsetting to me, let alone the author. In a week? Come on, let’s all be reasonable here.

besides, what do you think is the round time of a FR in HACS…

You are perfectly free to also upload your version, and proceed from there. I believe the community could welcome development of the apexcharts card indeed

this of course has to be seen, so why not just jump in and try to do so.

Id welcome the new card for sure, if indeed it would fix those few issues/fr’s.
I raised a few myself, and would love to get those fixed, but that is not my point for this post.

1 Like

@thh @FireFury @deluxestyle @Lars_IH

2 Likes

WOW! This is IT. The card we’ve been waiting for. Brilliant! :grinning_face: THANK YOU! @cataseven
I cannot believe that nobody have seen the need for this type of card … I thought ā€œit’s me.. and not youā€ :face_with_thermometer:

Only had a quick look and browse through the documentation. Need to test this properly. Trying to fugure och if there is an opton to use a ā€œdata generatorā€ to grab from InfluxDB…

1 Like

I’m having issues displaying the labels for my weekly cost card. The first is nearly right, except the last bucket is missing the label. While the second attempt brings back the label but as the location is offset, they don’t sit right under the bars. Any help please.

type: custom:apexcharts-card
experimental:
  color_threshold: true
header:
  show: true
  title: Weekly Net Cost (Last 8 Weeks)
  show_states: true
  colorize_states: true
graph_span: 8w
span:
  start: week
  offset: -7w
all_series_config:
  type: column
  extend_to: false
  float_precision: 2
series:
  - entity: input_number.energy_net_cost_8_week_average
    name: 8-wk Avg
    unit: $
    show:
      in_header: raw
      header_color_threshold: true
      in_chart: false
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
  - entity: sensor.energy_net_cost_weekly
    name: Net Cost
    unit: $
    show:
      in_header: raw
      header_color_threshold: true
      in_chart: false
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
  - entity: sensor.energy_net_cost_total
    name: Net Cost bars
    statistics:
      type: change
      period: week
      align: start
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
    show:
      in_header: false
      header_color_threshold: true
      datalabels: true
    group_by:
      func: last
      duration: 7 day
apex_config:
  xaxis:
    labels:
      rotate: 0
      offsetX: 0
      style:
        fontSize: 10px
      formatter: >-
        EVAL: function(value, timestamp, opts) { if (opts.i > 7) return ''; var
        d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() -
        ((d.getDay() + 6) % 7) - 7 * (7 - opts.i)); return d.getDate() + '/' +
        (d.getMonth() + 1); }
    tickAmount: 7
    axisTicks:
      offsetX: 1
  chart:
    height: 150px
  yaxis:
    - forceNiceScale: true
      decimalsInFloat: 2
  tooltip:
    x:
      formatter: >-
        EVAL: function(value, opts) { var i = opts && typeof opts.dataPointIndex
        === 'number' ? opts.dataPointIndex : 7; var d = new Date();
        d.setHours(0,0,0,0); d.setDate(d.getDate() - ((d.getDay() + 6) % 7) - 7
        * (7 - i)); return 'Week of ' + d.getDate() + '/' + (d.getMonth() + 1);
        }
    'y': {}
  plotOptions:
    bar:
      columnWidth: 80%
      borderRadius: 2
      dataLabels:
        position: top
  legend:
    show: false
  dataLabels:
    enabled: true
    formatter: |-
      EVAL: function (value) {
        if (value === null || value === undefined) return '';
        return value < 0 ? '-$' + Math.abs(value).toFixed(2) : '$' + value.toFixed(2);
      }
    offsetY: -6
    style:
      fontSize: 10px
      fontWeight: 600
      colors:
        - '#4a4a4a'
    background:
      enabled: false
  grid:
    padding:
      top: 14

and second option

type: custom:apexcharts-card
experimental:
  color_threshold: true
header:
  show: true
  title: Weekly Net Cost (Last 8 Weeks)
  show_states: true
  colorize_states: true
graph_span: 8w
span:
  start: week
  offset: -7w
all_series_config:
  type: column
  extend_to: false
  float_precision: 2
series:
  - entity: input_number.energy_net_cost_8_week_average
    name: 8-wk Avg
    unit: $
    show:
      in_header: raw
      header_color_threshold: true
      in_chart: false
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
  - entity: sensor.energy_net_cost_weekly
    name: Net Cost
    unit: $
    show:
      in_header: raw
      header_color_threshold: true
      in_chart: false
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
  - entity: sensor.energy_net_cost_total
    name: Net Cost bars
    statistics:
      type: change
      period: week
      align: start
    color_threshold:
      - value: -0.01
        color: limegreen
      - value: 0.01
        color: tomato
    show:
      in_header: false
      header_color_threshold: true
      datalabels: true
    group_by:
      func: last
      duration: 8 day
apex_config:
  xaxis:
    labels:
      rotate: 0
      offsetX: 0
      style:
        fontSize: 10px
      formatter: >-
        EVAL: function(value, timestamp, opts) { if (opts.i > 7) return ''; var
        d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() -
        ((d.getDay() + 6) % 7) - 7 * (7 - opts.i)); return d.getDate() + '/' +
        (d.getMonth() + 1); }
    tickAmount: 8
    axisTicks:
      offsetX: 1
  chart:
    height: 150px
  yaxis:
    - forceNiceScale: true
      decimalsInFloat: 2
  tooltip:
    x:
      formatter: >-
        EVAL: function(value, opts) { var i = opts && typeof opts.dataPointIndex
        === 'number' ? opts.dataPointIndex : 7; var d = new Date();
        d.setHours(0,0,0,0); d.setDate(d.getDate() - ((d.getDay() + 6) % 7) - 7
        * (7 - i)); return 'Week of ' + d.getDate() + '/' + (d.getMonth() + 1);
        }
    'y': {}
  plotOptions:
    bar:
      columnWidth: 80%
      borderRadius: 2
      dataLabels:
        position: top
  legend:
    show: false
  dataLabels:
    enabled: true
    formatter: |-
      EVAL: function (value) {
        if (value === null || value === undefined) return '';
        return value < 0 ? '-$' + Math.abs(value).toFixed(2) : '$' + value.toFixed(2);
      }
    offsetY: -6
    style:
      fontSize: 10px
      fontWeight: 600
      colors:
        - '#4a4a4a'
    background:
      enabled: false
  grid:
    padding:
      top: 14

If you are a user of HACS and you look for apexcharts card, would you want to find an entry with a card unmaintained for more than 6 months?

I’m happy to help maintain the card, but there was no reply so I forked. Proposing to replace the one in HACS is not meant to be impolite, it’s meant to help the actual users.

Yeah I didn’t take any action.
My card is going forward, but having it on HACS would imply more constant involvement on my part so I’ll do it later when I have more time.

Feel free to switch to my fork (it has nothing less than the original one, only improvements so it’s a safe switch) and then if the issue persists I’ll be happy to give it a look.

However I’m thinking about having a ā€œhomework firstā€ policy: tickets for issues should come with the output of a LLM provided with all the information and asked about potential causes of the issue, and proposals for solutions, to reduce my work.