Plotly interactive Graph Card

Hello,
I need your help for my plotty graph :slight_smile:

I do not understand why plotty merge November and December in ā€œthe month viewā€

type: custom:plotly-graph
hours_to_show: 4920
layout:
  barmode: overlay
  yaxis:
    visible: true
    fixedrange: true
    autorange_after_scroll: true
    rangegmode: tozero
entities:
  - entity: sensor.gaz_total_usage_daily_hp
    points_per_hour: 1
    barmode: overlay
    statistic: state
    period:
      0m: day
      100d: month
      100w: month
    autorange_after_scroll: true
    type: bar
    yaxis: 'y'
    base: 0
    unit_of_measurement: m3
    name: Cette annƩe
    texttemplate: '%{y:0.1f} m3'
    connectgaps: true
    show_value: false
    fill: tozeroy
    filters:
      - filter: i>0
    marker:
      color: rgba(255, 100, 100, 0.6)
  - entity: sensor.gaz_total_usage_daily_hp
    barmode: overlay
    statistic: state
    period:
      0m: day
      100d: month
      100w: month
    points_per_hour: 1
    time_offset: 366d
    autorange_after_scroll: true
    type: bar
    yaxis: 'y'
    base: 0
    unit_of_measurement: m3
    name: L'annĆ©e derniĆØre
    texttemplate: '%{y:0.1f} m3'
    connectgaps: false
    fill: tozeroy
    show_value: false
    zero_y_every: d
    filters:
      - filter: i>0
    marker:
      color: rgba(255, 255, 100, 0.4)
  - entity: ''
    name: Now
    yaxis: y9
    showlegend: false
    line:
      width: 1
      dash: dot
      color: deepskyblue
    x: $ex [Date.now(), Date.now()]
    'y':
      - 0
      - 1

Thanks :slight_smile:

EDIT : One more question Does plotty handle leap year ? Everything is one day shift ā€¦

EDIT 2 :

On July, plotty on month view do not detect any data ( only green bar ). Why ?

Check in the standard statistics card if the monthly statistics are not actually broken in your db

I have only hourly data :

plotly to the ā€œmerge/sumā€

The card just uses the statistics that home assistant delivers

Hi
Maybe something closed to the @max5962 problem.
Iā€™m currently try to convert HA standard graph to plotly.
I have this standard graph:
image

chart_type: bar
period: month
type: statistics-graph
entities:
  - linky_prod:01234567890_revenu
stat_types:
  - change
hide_legend: true
layout_options:
  grid_columns: 4
  grid_rows: 4
title: EDF OA
days_to_show: 90

I created the plotly graph with the following code:

type: custom:plotly-graph
entities:
  - entity: linky_prod:01234567890_revenu
    name: Production
    type: bar
    statistic: sum
    period: month
    unit_of_measurement: Euro
    texttemplate: '%{y:.2f} ā‚¬'
    textfont:
      color: white
    filters:
      - delta
layout:
  plot_bgcolor: rgba(0,0,0,0)
  showlegend: false
  uniformtext:
    minsize: 12
    mode: show
  margin:
    t: 30
    l: 70
    r: 10
  height: 250
  title: Revenus mensuels
  xaxis:
    gridcolor: rgba(238,235,235,0.3)
    showgrid: true
    linewidth: 0
    linecolor: null
  yaxis:
    visible: true
    fixedrange: true
hours_to_show: 4M
refresh_interval: 10

On the created graph, June is missing
image
Juneā€™s data start on the 5th.
Does the data of June must start on the 1st of June?

I added a statistics for may into the database and it works.
@mateine Is there another way to get it works on the card configuration side?

1 Like

If you want a solution that doesnā€™t use plotly, there is a heatmap card available: GitHub - kandsten/ha-heatmap-card: Heatmap custom card for Home Assistant

The default version displays hour blocks, but there is a higher resolution branch on the github that has 5 minute resolution:

Itā€™s not perfect but works reasonably well.

Thank you for the suggestion - its a great card that is simple to use. However there is no option to have date on X axis and remove the key. So I customised the above plotly card as follows:

type: custom:plotly-graph
raw_plotly_config: true
hours_to_show: 504
title: Solar generation hourly
layout:
  yaxis:
    fixedrange: true
  xaxis:
    fixedrange: true
    tick0: $ex hass.states['input_datetime.last_monday_0_week'].state
    dtick: $ex 24*3600*1000
    tickfont:
      size: 8
    tickformat: '%d'
  height: 200
  margin:
    t: 10
    b: 30
    l: 60
    r: 20
config:
  displaylogo: false
  scrollZoom: true
  modeBarButtonsToRemove:
    - resetScale2d
    - toImage
    - lasso2d
    - select2d
  displayModeBar: false
entities:
  - entity: sensor.myenergi_harvi_14922031_generation_ct2_in_kwh
    showscale: false
    hovertemplate: '%{x|%d %b} %{y}: <b>%{z} kWh</b><extra></extra>'
    filters:
      - resample: 60m
      - delta
    fn: |
      $fn ({ xs, ys, vars }) => {
        const days_to_show = 21;
        const datetoday = new Date();

        const x = [];
        for (let i = 0; i < days_to_show; i++) {
          var helpday = new Date();
          helpday.setDate(datetoday.getDate()-i);
          x.unshift( new Date(helpday.setUTCHours(0,0,0,0)) );
        }

        const y = [];
        for (let i = 0; i < 24; i++) {
          y.push(i.toString().padStart(2,'0')+":00")
        }
        
        const z = y.map((time) => x.map((day) => 0));

        for (let i = 0; i < xs.length; i++) {
          // Shift by -30 minutes
          const date = new Date(xs[i].getTime() - 30*60000)
          
          const time_bucket = Math.floor(date.getHours());
          
          const daysago = Math.round((datetoday.getTime() - date.getTime()) / (1000 * 3600 * 24));
        
          if (daysago <= days_to_show) {
            z[time_bucket][days_to_show - daysago -1] += +ys[i];
          }
        }

        vars.x = x;
        vars.y = y;
        vars.z = z;
        vars.colorscale = [
          ['0', '#230382'],
          ['0.1', '#921C96'],
          ['0.25', '#C93F55'],
          ['0.4', '#DF6D2D'],
          ['0.6', '#EFB03D'],
          ['0.75', '#F9DE52'],
          ['1', '#F5F5D4']
        ];
        
      };
    x: $fn ({ vars }) => vars.x
    'y': $fn ({ vars }) => vars.y
    z: $fn ({ vars }) => vars.z
    colorscale: $fn ({ vars }) => vars.colorscale
    type: heatmap

2 Likes

Hey all. I want to plot ā€œcloud heightā€ as a top-down histogram. On a scale of 0-10000m, so that if there was a 2500m height, the top 75% of the bar would be light grey fill while the bottom 25% would be clear. Is there a toggle or something for inverting a curve fill?

image

You could add another fake entity with a fixed y axis value of 10000 and use fill: tonexty
something like this

entities:
  - entity: sensor.cloud_ceiling
    name: top
    showlegend: false
    filters:
     - map_y: 10000
    fill: tonexty
  - entity: sensor.cloud_ceiling
1 Like

image

Thatā€™s done the job, thanks!

  - entity: sensor.home_cloud_ceiling
    name: top
    showlegend: false
    filters:
     - map_y: 15000
    fill: tonexty
    fillcolor: rgba(10, 0, 0, 0.15)

Hello all,
Iā€™m plotting my solar battery power on this plotly gaph:

when the battery power is 0 it displays a wierd curve on the graph (circled area)
this is how it looks in the home assistant history graph:

this is the code Iā€™m using for it: Can you please suggest a fix for this?

type: custom:plotly-graph
view_layout:
  grid-area: graph
entities:
  - entity: sensor.battery_power
    fill: tozeroy
    name: |
      $fn ({ ys,meta }) =>
        "Battery" + "šŸ”‹" + "(" +ys[ys.length - 1]+"W)"
    line:
      color: rgb(243, 179, 202)
      shape: spline
  - entity: sensor.battery_state_of_charge
    name: |
      $fn ({ ys,meta }) =>
        "SOC" + "āš ļø" + "(" +ys[ys.length - 1]+"%)"
    yaxis: y2
    line:
      color: red
      width: 1
      shape: spline
    fill: none
    filters:
      - sliding_window_moving_average:
          window_size: 1
          extended: true
hours_to_show: 2
refresh_interval: 60
title: Battery
defaults:
  entity:
    show_value: false
  yaxes:
    fixedrange: true
fn: |
  $fn({getFromConfig, vars})=> {
    const range = getFromConfig("visible_range");
    const width = range[1] - range[0];
    vars.scroll = (label, p) => ({
      args: [
        {
          layout: {
            "xaxis.range": [range[0] + width*p, range[1] + width*p],
          }
        }, {
          transition: {
            duration: 150,
          }
        }
      ],
      label,
      method: "animate",
    })
    vars.zoom = (label, h) => ({
      args: [
        {
          layout: {
            "xaxis.range": [Date.now()-1000*60*60*h, Date.now()],
          }
        }
      ],
      label,
      method: "animate",
    })
  }
layout:
  height: 500
  updatemenus:
    - buttons:
        - $fn({vars}) => vars.zoom('3 h', 3)
        - $fn({vars}) => vars.zoom('6 h', 6)
        - $fn({vars}) => vars.zoom('12 h', 12)
        - $fn({vars}) => vars.zoom('24 h', 24)
        - $fn({vars}) => vars.zoom('2 D', 24 * 2)
        - $fn({vars}) => vars.scroll('<', -0.9)
        - $fn({vars}) => vars.scroll('>', 0.9)
      direction: left
      pad:
        r: 10
        t: 10
      showactive: true
      type: buttons
      x: 1
      xanchor: right
      'y': 1.2
      yanchor: top
  legend:
    bgcolor: rgba(0,0,0,0)
    itemsizing: constant
    font:
      size: 11
  yaxis2:
    range:
      - 20
      - 105
    fixedrange: true
  yaxis:
    tickmode: linear
    dtick: 200
config:
  scrollZoom: false

Thatā€™s because you have line: spline, remove that line and it will look exactly like the history card

Hi,

Iā€™m trying to set a fixed range of the y axis, say 0-10. When I add

  yaxis:
    range:
      - 0
      - 10

the y axis scaling stays unaltered. When I plot another entity on yaxis 2 and add

  yaxis2:
    range:
      - 0
      - 10

it works as expected, the range on the right y axis runs from 0 to 10. Why isnā€™t it working for the left y axis? Is this a bug, or am I doing something wrong?

My workaround :smile: at the moment is

  • Plot my entity on yaxis1 and yaxis2
  • Set yaxis2 appropriately
  • Set yaxis1 as follows, making it vanish completely:
  yaxis:
    color: transparent
    tickcolor: transparent
    gridcolor: transparent

Still leaves me with a single y axis on the right side, which is of course a bit uncommonā€¦

Hello, this card is really amazing! :star_struck: Thank you for all the hard work.

Iā€™m struggling a bit to style the updatemenus buttons. Iā€™m trying to change the padding between the buttons and also make them smaller. I like the way the rangeselector looks (bottom), but the back and forth buttons on the updatemenus are a nice touch. Also, not sure if the concept of activecolor exists on the updatemenus buttons, similar to the way I have it in the rangeselector?

Iā€™ve been Googling, but so far in all the examples Iā€™ve seen, the buttons are right next to each other. Iā€™m able to change the font, font-color, background color, and borders so far.

What other styling options are supported?

type: custom:plotly-graph
config:
  displaylogo: false
  scrollZoom: true
  displayModeBar: true
entities:
  - entity: sensor.attic_temp_sensor_attic_temperature
    name: Attic Ā°F
    line:
      color: darkorange
      width: 2
      shape: spline
hours_to_show: 6
refresh_interval: 10
logarithmic_scale: false
fn: |
  $fn({getFromConfig, vars})=> {
    const range = getFromConfig("visible_range");
    const width = range[1] - range[0];
    vars.scroll = (label, p) => ({
      args: [
        {
          layout: {
            "xaxis.range": [range[0] + width*p, range[1] + width*p],
          }
        }, {
          transition: {
            duration: 150,
          }
        }
      ],
      label,
      method: "animate",
    })
    vars.zoom = (label, h) => ({
      args: [
        {
          layout: {
            "xaxis.range": [Date.now()-1000*60*60*h, Date.now()],
          }
        }
      ],
      label,
      method: "animate",
    })
  }
layout:
  updatemenus:
    - buttons:
        - $fn({vars}) => vars.scroll( '<', -.5)
        - $fn({vars}) => vars.scroll( '>', .5)
      direction: right
      active: -1
      type: buttons
      x: 1
      xanchor: right
      'y': 1
      yanchor: top
      font:
        size: 12
        color: white
        weight: bold
      bgcolor: black
      bordercolor: blue
      borderwidth: 1
    - buttons:
        - $fn({vars}) => vars.zoom( '30m', 0.5)
        - $fn({vars}) => vars.zoom( '1h', 1)
        - $fn({vars}) => vars.zoom( '6h', 6)
        - $fn({vars}) => vars.zoom( '12h', 12)
        - $fn({vars}) => vars.zoom( '1d', 24)
        - $fn({vars}) => vars.zoom( '7d', 24*7)
      buttonpadding:
        l: 2
        r: 2
      font:
        size: 12
        color: white
        weight: bold
      bgcolor: black
      bordercolor: blue
      borderwidth: 1
      activecolor: darkgreen
      direction: right
      active: -1
      type: buttons
      x: 0
      xanchor: left
      'y': 1
      yanchor: top
  showlegend: true
  yaxis:
    title: Temperature (Ā°F)
    range:
      - 80
      - 110
    dtick: 2
    hoverformat: .2f
  xaxis:
    dtick: 1800000
    tickformat: '%H:%M'
    hoverformat: '%Y-%m-%d %H:%M'
    rangeselector:
      activecolor: darkgreen
      bordercolor: white
      borderwidth: 1
      bgcolor: black
      'y': 0
      buttons:
        - count: 30
          step: minute
          label: 30m
        - count: 1
          step: hour
          label: 1h
        - count: 6
          step: hour
          label: 6h
        - count: 12
          label: 12h
          step: hour
        - count: 1
          step: day
          label: 1d
        - count: 7
          step: day
          label: 7d
        - count: 1
          step: month
          label: 1M
      pad:
        r: 0
        t: 10
      buttonpadding:
        l: 5
        r: 5
      font:
        color: white
        size: 12
        weight: 1000
  legend:
    font:
      size: 16
    itemsizing: constant
  hovermode: x unified
  paper_bgcolor: black
  plot_bgcolor: black
  font:
    color: white

Thanks mate. It looks good now!

See the plotlyjs docs here: Layout.updatemenus in JavaScript

Hey! Thank you for this great card. I liked using plotly for python visualizations in the past, therefore finding this for HA is a great plus! I do have one issue, that seemed to be addressed in the past, but I was wanting to make sure whether the problem still persists:

I have the following graph:
Screenshot 2024-09-10 at 10.19.45

However, my dashboard on the wall does not update the drawn integrals but does update the current production (green text on the right hand side). Over time, the graph slowly scrolls to the left and a gap between the last refresh of the dashboard and the current time grows.

If the problem is still related to homeassistant handling states different from statistics ā€“ I believe that was/is the underlying issue ā€“ what would be best practices for regular refreshes of the card?

Thank you!

Code for the card, in case some of you are interested:

type: custom:plotly-graph
hours_to_show: 12
refresh_interval: auto
entities:
  - entity: sensor.total_pv_yield
    name: PV Production
    period: auto
    legendgroup: 1
    filters:
      - derivate
      - store_var: pv_ac_power
    line:
      width: 1
      color: palegreen
      shape: spline
    fill: tonexty
  - entity: sensor.power_meter_exported
    name: Self Consumption
    statistic: state
    legendgroup: 2
    period: auto
    fill: tozeroy
    filters:
      - derivate
      - map_y: >
          (parseFloat(vars.pv_ac_power.ys[i]) < parseFloat(ys[i])) ? 0 :
          parseFloat(vars.pv_ac_power.ys[i]) - parseFloat(ys[i])
      - store_var: selfcons
    line:
      width: 1
      color: dodgerblue
      shape: spline
  - entity: sensor.power_meter_consumption
    name: Import
    statistic: state
    legendgroup: 3
    period: auto
    filters:
      - derivate
      - map_y: >
          (parseFloat(y[i-1]) === parseFloat(ys[i])) ? 0 :
          parseFloat(vars.selfcons.ys[i]) + parseFloat(ys[i])
    line:
      width: 0
      color: lightsalmon
      shape: spline
    fill: tonexty
  - entity: sensor.total_active_pv_power
    show_value: true
    showlegend: false
    line:
      width: 0
      color: green
      shape: spline
    marker:
      show: true
      color: green
      size: 10
      symbol: circle

Iā€™ve noticed this too although it has been a bit elusive. I think home assistant eventually stops sending updates via web socket at some point.

Does refresh_interval: 10s fix it? Setting it like this forces an update via requests every 10 seconds.

Did you find solution ?

Hi,
taking inspiration from this topic and various models online, I have created two charts that ā€œshouldā€ show me the thermostatā€™s operating time.
Unfortunately, they donā€™t work as I would like.

The first chart should display the current monthā€™s days from 1 to 31, with a bar for each day. I want to be able to move forward and backward through the month using the arrows on top. Initially, it seems to work, but when I start moving the cursor, it no longer functions correctly, and the months are displayed all wrong. Each time I click, the starting day changes.

The second chart should display the months of the year from January to December, with a bar for each month. This one also seems to work, but then, when I move with the sliders, it shifts by two years at a time and then behaves randomly.

Here is the code for both charts if anyone would like to help me.
Alternatively, if anyone has a working code for what Iā€™m trying to achieve, that would be great.
Ideally, to be honest, Iā€™d like to have a page similar to the energy monitoring page, but I havenā€™t found anything like that.

type: custom:plotly-graph
title: Accensione Giornaliera
entities:
  - entity: sensor.time_on_termostato_soggiorno_g
    name: Accensione Giornaliera
    statistic: state
    period: day
    type: bar
    texttemplate: '%{y}'
hours_to_show: current_month
fn: |
  $fn({getFromConfig, vars}) => {
    const range = getFromConfig("visible_range");
    const width = range[1] - range[0];
    vars.xrange = (h) => {
      if (h === null) h = (width)/1000/60/60;
      let start = new Date((range[0] + range[1])/2.0);
      start.setHours(12, 0, 0, 0);
      if (h >= 24*7) start.setFullYear(start.getFullYear(), start.getMonth(), start.getDate()-start.getDay());
      if (h >= 24*30) start.setDate(0);
      if (h >= 24*365) start.setMonth(0);
      return [start.getTime(), start.getTime() + 1000*60*60*h];
    }
    vars.scroll = (label, p) => ({
      args: [
        {
          layout: {
            "xaxis.range": [range[0] + width*p, range[1] + width*p],
          }
        }, {
          transition: {
            duration: 150,
          }
        }
      ],
      label,
      method: "animate",
    })
    
    vars.zoom = (label, h) => ({
      args: [
        {
          layout: {  
            "xaxis.range": vars.xrange(h),
          }
        }
      ],
      label,
      method: "animate",
    })
  }
layout:
  plot_bgcolor: black
  height: 400
  margin:
    r: 10
    l: 40
    b: 70
    t: 40
  xaxis:
    range: $fn({vars}) => vars.xrange(null)
    tickangle: -45
    tickformat: '%d %b'
    nticks: 20
    fixedrange: true
  yaxis:
    fixedrange: true
  updatemenus:
    - buttons:
        - $fn({vars}) => vars.scroll( '<', -1.0)
        - $fn({vars}) => vars.scroll( '>', 1.0)
      direction: right
      active: -1
      pad:
        t: -40
        r: 1
      type: buttons
      xanchor: right
      x: 1
type: custom:plotly-graph
title: Accensione Mensile
entities:
  - entity: sensor.time_on_termostato_soggiorno_m
    statistic: state
    period: month
    type: bar
    texttemplate: '%{y}'
hours_to_show: current_year
fn: |
  $fn({getFromConfig, vars}) => {
    const range = getFromConfig("visible_range");
    const width = range[1] - range[0];
    vars.xrange = (h) => {
      if (h === null) h = (width)/1000/60/60;
      let start = new Date((range[0] + range[1])/2.0);
      start.setHours(0, 0, 0, 0);
      if (h >= 24*7) start.setFullYear(start.getFullYear(), start.getMonth(), start.getDate()-start.getDay());
      if (h >= 24*30) start.setDate(1);
      if (h >= 24*365) start.setMonth(-1);
      return [start.getTime(), start.getTime() + 1100*60*60*h];
    }
    vars.scroll = (label, p) => ({
      args: [
        {
          layout: {
            "xaxis.range": [range[0] + width*p, range[1] + width*p],
          }
        }, {
          transition: {
            duration: 150,
          }
        }
      ],
      label,
      method: "animate",
    })
    
    vars.zoom = (label, h) => ({
      args: [
        {
          layout: {  
            "xaxis.range": vars.xrange(h),
          }
        }
      ],
      label,
      method: "animate",
    })
  }
layout:
  plot_bgcolor: black
  height: 400
  margin:
    r: 10
    l: 40
    b: 70
    t: 40
  xaxis:
    range: $fn({vars}) => vars.xrange(null)
    tickangle: -45
    nticks: 14
    fixedrange: true
  yaxis:
    fixedrange: true
  updatemenus:
    - buttons:
        - $fn({vars}) => vars.scroll( '<', -1.0)
        - $fn({vars}) => vars.scroll( '>', 1.0)
      direction: right
      active: -1
      pad:
        t: -40
        r: 1
      type: buttons
      xanchor: right
      x: 1

thanks