100% Stacked Bar Graph with ApexCharts Styling

So for the longest time I’ve been trying to find a way to display a decent 100% stacked bar graph showing the distribution of a quantity over several categories with no time dimension. custom:apexcharts-card is my go-to for graphing in Home Assistant but it has not implemented bar graphs and the only way it provides to achieve this is the radial graph:


This takes up a lot of space on a dashboard, especially as we only need one dimension (length) to express the relative proportions of the categories. Alternatives like custom:bar-card can’t handle more than two categories.

So I hacked together an alternative using the venerable custom:button-card!

First, to facilitate code reuse when you need more than one stacked bar chart, declare some button card templates:

button_card_templates:
  bar_chart: # for the overall custom:button-card containing the chart
    styles:
      card:
        - padding: 0px # to make the chart take up minimal space
  bar_chart_segment: # for each chart segment, one per category
    variables:
      percent: >- # for displaying a data label for the proportion taken up by this category
        [[[ return Math.round(parseInt(entity.state) / variables.chart_total *
        100) ]]]
    name: |
      [[[ 
        if (variables.percent > 5) {
          return variables.percent.toString() + '%'
        } else {
          return ''
        }
      ]]]
    show_icon: false
    styles:
      card: # for a modern look based on the Tile Card
        - height: 56px 
        - border-radius: 0px
      name:
        - font-size: 14px
        - font-weight: 500

Then, wherever you need a stacked bar chart, use custom:button-card:

type: custom:button-card
template: bar_chart
variables:
  all_total: >- # sum of quantities over all categories
    [[[ return parseInt(states['sensor.immich_photos'].state) +
    parseInt(states['sensor.immich_videos'].state) ]]]
  bar_segment_photos_percent: >- # proportion for first category
    [[[ return
    parseInt(states['sensor.immich_photos'].state)/variables.all_total * 100 ]]]
  bar_segment_videos_percent: >- # proportion for second category.  Keep adding bar_segment_xxx_percent for each category.
    [[[ return
    parseInt(states['sensor.immich_videos'].state)/variables.all_total * 100 ]]]
custom_fields: # create a custom_field containing another custom:button-card for each category.
  bar_segment_photos:
    card:
      type: custom:button-card
      variables:
        percent: "[[[ return Math.round(variables.bar_segment_photos_percent) ]]]"
      entity: sensor.immich_photos
      template: bar_chart_segment
      styles:
        card:
          - background: "#FF9800"  # set colors to match custom:apexcharts-card
  bar_segment_videos:
    card:
      type: custom:button-card
      variables:
        percent: "[[[ return Math.round(variables.bar_segment_videos_percent) ]]]"
      entity: sensor.immich_videos
      template: bar_chart_segment
      styles:
        card:
          - background: "#3498DB"
styles:
  grid:
    - grid-template-areas: "\"bar_segment_photos bar_segment_videos\"" # ensure all segments are listed in one row in the correct order
    - grid-template-columns: >- # using the CSS grid, column widths for each custom field is set to the category proportion
        [[[ return variables.bar_segment_photos_percent.toString() + '% ' +
        variables.bar_segment_videos_percent.toString() + '%' ]]]
    - grid-template-rows: min-content

Finally, if you still want a header in the style of custom:apexcharts-card (which looks quite nice IMO), create a card containing only the header. To facilitate code reuse:

apexcharts_card_templates:
  header_only:
    header:
      show: true
      show_states: true
      colorize_states: true
    apex_config:
      chart:
        height: 0 # hides the chart area
      tooltip:
        enabled: false # disables the tooltips, otherwise hovering over the header could confusingly show tooltips
    graph_span: 1min # optional - minimise the load on the card to compile a chart that won't be displayed

Then to create the header itself:

type: custom:apexcharts-card
config_templates: header_only
header:
  title: Assets
all_series_config:
  unit: " "
series:
  - entity: sensor.immich_photos
    name: Photos
  - entity: sensor.immich_videos
    name: Videos

Put the header and the bar chart in a vertical stack to get:

This can handle any number of categories, but it is rather janky as the number of custom fields and number of columns in the CSS grid will increase by the number of categories, and you need to manually ensure that the completeness and order of the custom fields in the CSS grid is correct.

If you’ve got any suggestions on how to improve this, do let me know! For example:

  • How to embed the header and the bar chart into a single custom:button-card so that the header and the chart appear as one card. This can work in principle but the button card seems to interfere with the justification of the text in the header, making them all centre justified when they should be left justified
  • How to pass entity IDs from a parent custom:button-card to other custom:button-cards specified in the custom_fields, and how to enforce the order of the custom_fields in the grid_template_rows, which allow for the entity IDs for the categories to be declared once and minimise the risk of copy-pasta.

You can use it with a combination of different cards, you already have the base template for a button card so, for example, use custom:auto-entities with a template for rendering or decluttering card. And for layouts check custom:layout-card etc., there are many options.

and if you are more experienced, You can fine-tune a lot more in yaml mode… where you can apply other HA options, like splitting card config to several smaller parts.

Wow, thanks a lot for this “hacked” horizontal bar chart. Works super well. I adapted it for a segmented bar chart with absolute values.


Like this I can show the live values of my home’s power consumpton including the info where the power is coming from. Here the code I used

Template:

button_card_templates:
  bar_chart:
    styles:
      card:
        - padding: 0px
  bar_chart_segment:
    variables:
      percent: >-
        [[[ return Math.round(parseInt(entity.state) / variables.chart_total *
        100) ]]]
    name: |
      [[[ 
        if (variables.percent > 7) {
          return parseInt(entity.state).toString() + ' W'
        } else {
          return ''
        }
      ]]]
    show_icon: false
    styles:
      card:
        - height: 46px
        - border-radius: 0px
      name:
        - font-size: 16px
        - font-weight: 500

Chart:

type: vertical-stack
cards:
  - type: custom:mushroom-chips-card
    chips:
      - type: template
        content: >
          Aktueller Stromverbr.  {{ (states('sensor.leistung_netzbezug')
          | float + states('sensor.leistung_pv_verbrauch') | float +
          states('sensor.leistung_batterie_verbrauch') | float) |
          round(0) }} W
      - type: template
        content: aus PV
        icon: mdi:solar-power
        icon_color: '#FB8C00'
        entity: sensor.leistung_pv_verbrauch
        tap_action:
          action: more-info
      - type: template
        content: aus Bat
        icon: mdi:battery
        icon_color: '#42A5F5'
        entity: sensor.leistung_batterie_verbrauch
        tap_action:
          action: more-info
      - type: template
        content: aus Netz
        icon: mdi:transmission-tower
        icon_color: '#66BB6A'
        entity: sensor.leistung_netzbezug
        tap_action:
          action: more-info
  - type: custom:button-card
    template: bar_chart
    variables:
      all_total: |-
        [[[ 
          const pv = parseInt(states['sensor.leistung_pv_verbrauch'].state) || 0;
          const bat = parseInt(states['sensor.leistung_batterie_verbrauch'].state) || 0;
          const netz = parseInt(states['sensor.leistung_netzbezug'].state) || 0;
          const all = pv + bat + netz;
          return all + 1000;
        ]]]
      bar_segment_pv_verbrauch_perc: >-
        [[[  return
        parseInt(states['sensor.leistung_pv_verbrauch'].state)/variables.all_total*100
        ]]]
      bar_segment_bat_verbrauch_perc: >-
        [[[  return
        parseInt(states['sensor.leistung_batterie_verbrauch'].state)/variables.all_total*100
        ]]]
      bar_segment_netzbezug_perc: >-
        [[[  return
        parseInt(states['sensor.leistung_netzbezug'].state)/variables.all_total*100
        ]]]
    custom_fields:
      bar_segment_pv_verbrauch:
        card:
          type: custom:button-card
          variables:
            percent: >-
              [[[ return
              Math.round(variables.bar_segment_pv_verbrauch_perc) ]]]
          entity: sensor.leistung_pv_verbrauch
          template: bar_chart_segment
          styles:
            card:
              - background: '#FB8C00'
      bar_segment_bat_verbrauch:
        card:
          type: custom:button-card
          variables:
            percent: >-
              [[[ return
              Math.round(variables.bar_segment_bat_verbrauch_perc) ]]]
          entity: sensor.leistung_batterie_verbrauch
          template: bar_chart_segment
          styles:
            card:
              - background: '#42A5F5'
      bar_segment_netzbezug:
        card:
          type: custom:button-card
          variables:
            percent: >-
              [[[ return
              Math.round(variables.bar_segment_netzbezug_perc) ]]]
          entity: sensor.leistung_netzbezug
          template: bar_chart_segment
          styles:
            card:
              - background: '#66BB6A'
    styles:
      grid:
        - grid-template-areas: >-
            "bar_segment_pv_verbrauch bar_segment_bat_verbrauch
            bar_segment_netzbezug"
        - grid-template-columns: >-
            [[[ return
            variables.bar_segment_pv_verbrauch_perc.toString() + '% ' +
            variables.bar_segment_bat_verbrauch_perc.toString() + '%' +
            variables.bar_segment_netzbezug_perc.toString() + '%' ]]]
        - grid-template-rows: min-content

With the “return all+1000;” line I achieve that the the bars are smaller and larger when power consumption goes down/up while having larger bars for small values (kind of a dynamic scale)

1 Like