Canvas gauge as a picture element: can't get size right for PC & phone

I’ve got a nice gauge which displays the tidal height with the heights & times of the previous & next tides. To achieve this I’ve overlaid 3 custom:canvas-gauge-cards (high, current, low) using a picture-elements card to stack them on top of each other. I’m then using custom:card_templater to position various state labels to label the tidal heights and times. It looks like this:

and a later version on my phone:

As you can see I haven’t got the vertical position of the labels right, and the reason for this is that the image is quite different viewed on my phone (where the labels are almost right). I suspect it has something to do with the size of the background image (which is simply an all- white picture) and the size specified for the canvas gauges. I’ve spent hours on this and can’t see how to get it the same on phone & PC. (The blank area on the right will be filled with other data on height restrictions for launching our RNLI lifeboat).
Another problem is that is blinks every minute (which is how often the current tide height template refreshes. I’ve got animate: false but that doesn’t prevent it.
My lovelace code is this:

type: custom:card-templater
entities:
  - sensor.height_now
  - sensor.tide_last_h
card:
  type: picture-elements
  image: /local/images/blankscreen.jpg
  elements:
    - type: state-label
      entity: sensor.height_now
      prefix: 'Tide height now: '
      style:
        transform: none
        top: 0%
        left: 10%
        font-weight: bold
    - type: state-label
      entity: sensor.nul
      prefix: 'Tide is '
      suffix_template: '{{ state_attr("sensor.height_now","status") }}'
      style:
        transform: none
        top: 6%
        left: 10%
    - type: state-label
      entity: sensor.nul
      prefix: Last tide
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_last_h")|float+1) * 10)|int|string +"%"}}'
        left: 0%
    - type: state-label
      entity: sensor.nul
      prefix: Next tide
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_next_h")|float+1) * 10)|int|string +"%"}}'
        left: 30%
    - type: state-label
      entity: sensor.nul
      prefix_template: '{{states("sensor.tide_last_h")|round(2)}}'
      suffix: m
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_last_h")|float+0.4) * 10)|int|string +"%"}}'
        left: 0%
    - type: state-label
      entity: sensor.nul
      suffix_template: >-
        {{as_timestamp(states('sensor.tide_last'))|timestamp_custom('%H:%M',true)}}
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_last_h")|float+0.65) * 10)|int|string +"%"}}'
        left: 0%
    - type: state-label
      entity: sensor.nul
      suffix_template: >-
        {{as_timestamp(states('sensor.tide_next'))|timestamp_custom('%H:%M',true)}}
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_next_h")|float+0.75) * 10)|int|string +"%"}}'
        left: 30%
    - type: state-label
      entity: sensor.nul
      prefix_template: '{{states("sensor.tide_next_h")|round(2)}}'
      suffix: m
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_next_h")|float+0.5) * 10)|int|string +"%"}}'
        left: 30%
    - type: custom:canvas-gauge-card
      entity_template: sensor.tide_high
      gauge:
        type: linear-gauge
        width: 150
        height: 620
        minValue: -0.5
        maxValue: 8.5
        majorTicks: none
        strokeTicks: false
        animate: false
        colorPlate: '#ffffff00'
        colorBar: '#fdf6ccff'
        colorBarProgress: '#c2e1b7ff'
        borderShadowWidth: 0
        colorBorderOuter: '#ffffff00'
        colorBorderMiddle: '#ffffff00'
        colorBorderInner: '#ffffff00'
        colorBorderOuterEnd: '#ffffff00'
        colorBorderMiddleEnd: '#ffffff00'
        colorBorderInnerEnd: '#ffffff00'
        borders: false
        needleType: line
        needleWidth: 3
        colorNeedle: red
        tickSide: left
        numberSide: left
        needleSide_template: |
          {% if states('sensor.tide_last_h') > states('sensor.height_now') %}
            left
          {% else %}
           right
          {% endif %} 
        barStrokeWidth: 0
        barBeginCircle: false
        valueBox: false
      style:
        transform: none
        left: 10%
        bottom: 0
    - type: custom:canvas-gauge-card
      entity: sensor.height_now
      gauge:
        type: linear-gauge
        width: 150
        height: 620
        minValue: -0.5
        maxValue: 8.5
        majorTicks:
          - '-0.5'
          - '0.0'
          - '0.5'
          - '1.0'
          - '1.5'
          - '2.0'
          - '2.5'
          - '3.0'
          - '3.5'
          - '4.0'
          - '4.5'
          - '5.0'
          - '5.5'
          - '6.0'
          - '6.5'
          - '7.0'
          - '7.5'
          - '8.0'
          - '8.5'
        minorTicks: 5
        strokeTicks: true
        animate: false
        highlights:
          - color: red
            from: -0.5
            to_template: '{{ states("input_number.tide_low_restrction") }}'
          - color: '#d6cb94'
            from_template: '{{ states("input_number.tide_low_restrction") }}'
            to_template: '{{ states("input_number.tide_height_target") }}'
        colorPlate: '#ffffff00'
        colorBar: '#ffffff00'
        colorBarProgress: ' #9fddf9ff'
        colorNeedle: red
        borderShadowWidth: 0
        colorBorderOuter: '#ffffff00'
        colorBorderMiddle: '#ffffff00'
        colorBorderInner: '#ffffff00'
        colorBorderOuterEnd: '#ffffff00'
        colorBorderMiddleEnd: '#ffffff00'
        colorBorderInnerEnd: '#ffffff00'
        borders: false
        needleType: line
        needleWidth: 5
        tickSide: left
        numberSide: left
        needleSide: both
        barStrokeWidth: 0
        barBeginCircle: false
        valueBox: false
      style:
        transform: none
        left: 10%
        bottom: 0
    - type: custom:canvas-gauge-card
      entity: sensor.tide_low
      gauge:
        type: linear-gauge
        width: 150
        height: 620
        minValue: -0.5
        maxValue: 8.5
        majorTicks: none
        strokeTicks: false
        animate: false
        colorPlate: '#ffffff00'
        colorBar: '#ffffff00'
        colorBarProgress: '#dcf2fd'
        borderShadowWidth: 0
        colorBorderOuter: '#ffffff00'
        colorBorderMiddle: '#ffffff00'
        colorBorderInner: '#ffffff00'
        colorBorderOuterEnd: '#ffffff00'
        colorBorderMiddleEnd: '#ffffff00'
        colorBorderInnerEnd: '#ffffff00'
        borders: false
        needleType: line
        needleWidth: 3
        colorNeedle: red
        tickSide: left
        numberSide: left
        needleSide_template: |
          {% if states('sensor.tide_last_h') < states('sensor.height_now') %}
            left
          {% else %}
           right
          {% endif %} 
        barStrokeWidth: 0
        barBeginCircle: false
        valueBox: false
      style:
        transform: none
        left: 10%
        bottom: 0

1 Like

SInce posting this I’ve spent 'oos of hours trying different solutions with no success.
The oveview is:

  • I’ve got a nice picture-elements card that displays a number of sensors relating to the local tides that help our lifeboat station make decisions about launching boats
  • for ease of comprehension I want to present the current height as a tide gauge which show previous/next tide times & heights
  • I’m using a custom:canvas-gauge to do this as it nicely presents the scale and can use colour to highlight previous/next tide heights etc
  • I then use custom:card-templater within the picture-elements card to place the time & height data at the correct height alongside the canvas-gauge.
    The trouble is that the canvas-gauge must be sized in pixels and this is not scaling correctly on diiferent displays. I am currently using custom:state-switch and mediaquery to detect the browser size and switch between different versions of the canvas-gauge each having a different pixel height. This still fails and presents different heights on different browsers even when they have the almost same browser height/width but differ in whether the sidebar is visible. I’ve got a table of some of the broswer characteristics & whether the gauge scales correctly, but I can’t see a pattern.
    A much simpified version of the card looks like this:
type: custom:card-templater
entities:
  - sensor.height_now
  - sensor.height_later
  - input_number.tide_low_restriction
  - input_select.tide_later_offset
card:
  type: picture-elements
  image: /local/images/blankv.jpg
  title: Appledore tides location dev
  elements:
    - type: state-label
      entity: sensor.nult
      suffix_template: >-
        {{+ states("sensor.height_now")|round(1)~"m
        ("~state_attr("sensor.height_now","status")~")"}}
      prefix: 'Tide height now: '
      style:
        transform: translate(0%,-80%)
        top: 0%
        left: 0%
        font-weight: bold
        color_template: >-
          {{ iif( states('sensor.height_now')|float <=
          states('input_number.tide_low_restriction')|float, 'red', 'black') }}
    - type: state-label
      entity: sensor.nult
      suffix_template: >-
        {{ states('sensor.height_later')|round(1) ~"m in
        "~states('input_select.tide_later_offset')~" mins"}}
      style:
        transform: translate(0%,-50%)
        top: 0%
        right: 0%
        color: black
    - type: state-label
      entity: sensor.nult
      prefix: Last
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_last_h")|float*80/9) + 12.5)|int|string +"%"}}'
        right: 87%
        font-size: 90%
        color: black
    - type: state-label
      entity: sensor.nult
      prefix: Now
      style:
        transform: none
        bottom_template: '{{((states("sensor.height_now")|float*80/9) + 10.5)|int|string +"%"}}'
        right_template: >-
          {% if
          (states("sensor.height_now")|float-states("sensor.tide_last_h")|float)|abs>0.6
          %}
            86%
          {% endif %}
        left_template: >-
          {% if
          (states("sensor.height_now")|float-states("sensor.tide_last_h")|float)|abs<0.6
          %}
            30%
          {% endif %}
        font-weight: bold
        color: black
    - type: state-label
      entity: sensor.nult
      suffix_template: >-
        {{as_timestamp(states('sensor.tide_last'))|timestamp_custom('%H:%M',true)}}
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_last_h")|float*80/9) + 9.5)|int|string +"%"}}'
        right: 87%
        color_template: >
          {% if states('sensor.tide_last_h')|float <=
          states('input_number.tide_low_restriction')|float %}
            red
          {% else %}
           black
          {% endif %} 
    - type: state-label
      entity: sensor.nult
      prefix_template: '{{states("sensor.tide_last_h")|round(1)}}'
      suffix: m
      style:
        transform: none
        font-size: 90%
        bottom_template: '{{((states("sensor.tide_last_h")|float*80/9) + 6.5)|int|string +"%"}}'
        right: 87%
        color: black
    - type: state-label
      entity: sensor.nult
      prefix: Next
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_next_h")|float*80/9) + 12.5)|int|string +"%"}}'
        left: 30%
        font-size: 90%
        color: black
    - type: state-label
      entity: sensor.nult
      suffix: 100-
      style:
        transform: none
        top: 0%
    - type: state-label
      entity: sensor.nult
      suffix: 5-
      style:
        transform: none
        bottom: 5%
    - type: state-label
      entity: sensor.nult
      suffix_template: >-
        {{as_timestamp(states('sensor.tide_next'))|timestamp_custom('%H:%M',true)}}
      style:
        transform: none
        bottom_template: '{{((states("sensor.tide_next_h")|float*80/9) + 9.5)|int|string +"%"}}'
        left: 30%
        color_template: >
          {% if states('sensor.tide_next_h')|float <=
          states('input_number.tide_low_restriction')|float %}
            red
          {% else %}
           black
          {% endif %} 
    - type: state-label
      entity: sensor.nult
      prefix_template: '{{states("sensor.tide_next_h")|round(1)}}'
      suffix: m
      style:
        transform: none
        font-size: 90%
        bottom_template: '{{((states("sensor.tide_next_h")|float*80/9) + 6.5)|int|string +"%"}}'
        left: 30%
        color_template: >
          {% if states('sensor.tide_next_h')|float <=
          states('input_number.tide_low_restriction')|float %}
            red
          {% else %}
           black
          {% endif %} 
    - type: custom:state-switch
      entity: mediaquery
      style:
        transform: none
        left: 7.5%
        bottom: 0%
        height: 100%
      states:
        all:
          type: custom:canvas-gauge-card
          entity: sensor.height_now
          gauge:
            type: linear-gauge
            width: 100
            height: 500
            minValue: -0.5
            maxValue: 8.5
            majorTicks:
              - '-0.5'
              - '0.0'
              - '0.5'
              - '1.0'
              - '1.5'
              - '2.0'
              - '2.5'
              - '3.0'
              - '3.5'
              - '4.0'
              - '4.5'
              - '5.0'
              - '5.5'
              - '6.0'
              - '6.5'
              - '7.0'
              - '7.5'
              - '8.0'
              - '8.5'
            minorTicks: 5
            strokeTicks: true
            animate: false
            highlights:
              - color: red
                from: -0.5
                to_template: '{{ states("input_number.tide_low_restriction") }}'
              - color: '#dcf2fd'
                from: '{{ states("input_number.tide_low_restriction") }}'
                to_template: '{{ states("sensor.tide_low") }}'
              - color: '#fdf6ccff'
                from_template: '{{ states("sensor.tide_low") }}'
                to_template: '{{ states("sensor.tide_high") }}'
              - color: '#c2e1b7'
                from_template: '{{ states("sensor.tide_high") }}'
                to: 8.5
            highlightsWidth: 20
            colorPlate: grey
            colorBar: '#fdf6ccff'
            colorBarProgress: ' #9fddf9ff'
            colorNeedle: red
            borderShadowWidth: 0
            colorBorderOuter: black
            colorBorderMiddle: '#ffffff00'
            colorBorderInner: '#ffffff00'
            colorBorderOuterEnd: black
            colorBorderMiddleEnd: '#ffffff00'
            colorBorderInnerEnd: '#ffffff00'
            borders: false
            needleType: line
            needleWidth: 5
            tickSide: both
            numberSide: left
            needleSide: both
            barStrokeWidth: 0
            barBeginCircle: false
            valueBox: false

Probably because you placed the whole card inside “card-templater” - it redraws an embedded card on every change of monitored entities.
Never used the “card-templater” - but it seems to work like another custom “config-template-card” (CTC) which I am using.
There is one “NEVER” - never place picture-elements (PE) card inside CTC, it will blink on every change of monitored entities (as mentioned above). Instead, place a particular element(s) inside CTC - then only this element will change dynamically. Check CTC github docs how to use CTC with elements.

Next, about using templates.
As far as I can see you need them:
– for “gauge” cards;
– for “state-label” elements: values & position.
There is an alternative way for the “state-label” elements:
– create template sensors & show them instead prefixes;
– define positions dynamically by using CSS variables & card-mod.
You decide yourself which way is easier to implement / test / maintain.

Next, about different screens.
Here I tried to adapt PE card to different viewports, check links in this post.
Check this:
9de761b296ec6b25a0cfc36817e917f6b501c29b_2_690x320
It is possible to position elements properly for every viewport.
The main problem is a size of an element (“scale”). For this you will have to define it dynamically (explained there in my link).

In some cases you may wish to have a different background image for different viewports - like for a normal PC monitor & portrait-oriented smartphone.
Then you will need this approach.

Thanks Ilda, I appreciate your help. There’s a lot in here and I will clearly have to re-read your comprehensive guide which I couldn’t work out how to apply first time around.
Regarding the scaling issue there are two points which immediately spring to mind:-

  • Exactly the same browser & PC can produce a canvas-gauge height that changes depending on whether the sidebar is fully visible or minimised (see images below). The browser height & width don’t change so I can’t see how this change can be detected to adjust either the scale or the PE background image to compensate. I’ve tried using layout-card to control the column width but this just causes other problems on my mobile phone.
  • I have spent some time trying to use card-mod to modify a canvas-gauge with no success at all.

I suspect there is something odd about the behaviour of canvas-gauges e.g. their dimensions being defined only in pixels.
Sidebar minimised; tide gauge (in left column) correct size:


Sidebar full width: tide gauge suddenly ~10% too tall

(I added markers on the left to indicate bottom: 5% & bottom: 100%)

When sidebar is ON/OFF, a column’s width changes.
An image inside PE card resizes (see a picture from my post above).
Since an element’s position is set in %, it stays on same place (relatively to the image borders).
Only a size of the element changes.
For this particular case a difference between scales should be small - then probably try setting smaller scales to fit both cases.

Cannot say more w/o a possibility to reproduce.

P.S. Why the “Compliance dev” card changes it’s height so significantly?

I think I took the screenshot in the middle of a redraw, it nomally looks the same height as the tide gauge.
I’m honestly unclear how to proceed with this despite your kind help. I’m appending the code for a vastly simplified version of the gauge which you should be able to reproduce to see the gauge height problem I’ve described (I put 3 copies on a dashboard to show the effect). You might need to substitute a couple of sensors that exist local to you. I added a card mod block just to prove it worked, although nothing I put in the format section of the gauge works for me.

type: picture-elements
image: /local/images/blankv.jpg
title: Old gauge tester
style: |
  ha-card {
    color: red !important;
    }
elements:
  - type: state-label
    entity: sensor.nult
    suffix: 100-
    style:
      transform: none
      top: 0%
  - type: state-label
    entity: sensor.nult
    suffix: 5-
    style:
      transform: none
      bottom: 5%
  - type: custom:canvas-gauge-card
    entity: sensor.tide_high
    style:
      transform: none
      left: 10%
      bottom: 0%
    gauge:
      type: linear-gauge
      width: 100
      height: 600
      minValue: -0.5
      maxValue: 8.5
      majorTicks: none
      strokeTicks: false
      animate: false
      colorPlate: '#ffffff00'
      colorBar: '#fdf6ccff'
      colorBarProgress: '#c2e1b7ff'
      borderShadowWidth: 0
      colorBorderOuter: black
      colorBorderMiddle: '#ffffff00'
      colorBorderInner: '#ffffff00'
      colorBorderOuterEnd: black
      colorBorderMiddleEnd: '#ffffff00'
      colorBorderInnerEnd: '#ffffff00'
      borders: false
      needleType: line
      needleWidth: 3
      colorNeedle: red
      tickSide: left
      numberSide: left
      needleSide: left
      barStrokeWidth: 0
      barBeginCircle: false
      valueBox: false

The background “image” is a blank 348 by 608 pixels

Thanks for the guidance. I’m well advanced with replacing all the in-card templates with template sensors in order to stop using card-templater. I’ve hit a problem with positioning the labels though. They are currently positioned using a template that calculates a variable top: xx%. I can now get card-mod to position them using a template and have tried 2 techniques, neither gets close to emulating the top: xx%. Using something like this:

  - element: ''
    type: state-label
    entity: sensor.wind_display
    precision: 0
    suffix: '->'
    style:
      font-weight: bold
      left: 0%
      top: 5%
    card_mod:
      style: |
        div {
          transform: translate(50%,{{state_attr('sensor.wind_display','scale')}});
          background-color: #FFFFFF80;
          border-radius: 50px !important;
        }

where state_attr('sensor.wind_display','scale') equals say 500% works on some displays but is badly wrong on others. I’ve also tried making the template sensor return a value in px, but that just changes the scale of the problem - its still display dependent. I’m a bit lost to be honest.

This is not how I suggested to use card-mod.
Go to the tutorial, find “conditional styles”.

I have studied that but can’t find any reference to using a variable to locate a state-label within a P-E card. You explain clearly your approaches to positioning images but I don’t see that is applicable to this.
As card_mod accepts templates I tried putting top: {{template}} inside a card_mod but it had no effect. Using translate did move the label but unreliably as just described.
I don’t mean to sound tetchy- your tutorial is awesome but hard for me to apply!

Answering from iPhone, have no access for any civilian PC these days.

There 2 ways of using card-mod for PE:

  1. A usual way - define a path to some UI element (or any part of it), define css properties. (go to huge card-mod theme - 1st post - link at the bottom - styles for PE )
  2. PE-only way - define variables for some PE-element or whole PE card, then use values of these variable as a part of a native PE “style” option.

You may not use card-mod at all for PE - it has own native “style” option.
But it does not allow templating. So, use card-mod (method 2).
Also, it does not allow to style a particular part of some element - the use card-mod (method 1).

In your case you need to use method 2.
Described in Tutorial - 1st post - link to “conditional styles”.

Well, with a lot more reading around card-mod and Ildar’s tutorial I’ve cracked the problems of positioning a state_label on the PE card. The trick was to use :host not div

      entity: sensor.nult
      prefix: Now
      style:
        transform: none
        font-weight: bold
        color: black
      card_mod:
        style: |
          :host {
            position: relative;
            bottom: {{((states("sensor.height_now")|float*80/9) + 10.5)|int|string +"%"}} !important;
            right: {{ iif((states("sensor.height_now")|float-states("sensor.tide_last_h")|float)|abs > 0.6, '86%')}};
            left: {{ iif((states("sensor.height_now")|float-states("sensor.tide_last_h")|float)|abs <= 0.6, '25%')}};
            background-color: #FFFFFF80;
            border-radius: 50px !important;
          }

So I’ve eliminated all the templates (apart from a few within the canvas-gauge) and the cards no longer blink - thanks Ildar!
Now I need to get back to the issue of sizing the guage in all view-ports/zoomscales!

1 Like

Not exactly.
The trick was to use a native “style” option and dynamically defined variables.

style:
  top: var(—some-var)
  ...
card_mod:
  style: |
    :host {
      —some-var : {{...}}
    }

which is more “official and in some cases may give different results than defining a css property for “:host”.

Sorry I don’t understand that. I thought I had a good solution there!

Very difficult to type on iPhone, I am trying))

Imagine that you do not need a dynamic value.
Then you are supposed to use a native “style” option:

- type: some-element
  …
  styles:
    top: …
    left: …

But you need to define a position dynamically.
Your options:
— place the ELEMENT ( not a whole PE card) inside CTC;
— define a variable dynamically by card-mod - then use it inside “style”.

If you choose the 2nd way - what you can do is:

  1. Either define variables on a PE card level:
type: PE
…
card_mod:
  styles: |
    ha-card {
      define var1
      define var2
    }
elements:
  - type: x1
    …
    style:
      top: var1
      …
  - type: x2
    …
    style:
      top: var2
      …
  1. Or define each variable on a particular element level:
type: PE
…
elements:
  - type: x1
    …
    style:
      top: var1
      …
    card_mod:
      styles: |
        :host {
          define var1
        }
  - type: x2
    …
    style:
      top: var2
      …
    card_mod:
      styles: |
        :host {
          define var2
        }

Note that the native “style” option is obligatory.
Surely you may define any top & bottom - and then override it by card-mod as you did.
But this is not correct.
Also, is some cases (as I already said) defining properties in the native “style” option may give different results than defining same properties via card-mod.

I haven’t got my head around your reply yet Ildar, but I wanted to say that I worked out how to apply your tutorial to the problem of getting my canvas-guages to scale consistently with the PE card. What I have had to do is to define the size of the gauge to be exactly the same as the PE image and then to use scale(0.2,1) to compress the gauge horizontally. This results in the text on it being unreadable but I’ve dispensed with it and used elements in the PE card instead. It’s not as elegant but I’ve got it working and so far it scales on all the device/browser combos I’ve been able to test so far (apart from one which truncates the width).
So my thanks again Ildar. I will get round to looking at what you say about dynamic variables, but for now I’m looking at one of my other problems:
There are some elements of the canvas-gauge that still requite templates (an array of “highlights”). Card-mod can’t address this as it isn’t in a format section. It looks like:

              highlights:
              - color: red
                from: -0.5
                to_template: '{{ states("input_number.tide_low_restriction") }}'
              - color: '#dcf2fd'
                from_template: '{{ states("input_number.tide_low_restriction") }}'
                to_template: '{{ states("sensor.tide_low") }}'

You refer to being able to put specific PE elements inside a custom:card-templater (rather than the whole PE card which then flickers) but I haven’t been able to work out how to do this despite numerous attempts. Can you offer any guidance?

This you might “fix” by setting the “sidebar” to “always hide” in your “Profile”

Another though that comes to my mind, Which type of “View” are you using ?, (grid-layout vs vertical-stack-layout, vs masonry-default behave different ), and why are the “canvas”( in your first code) inside a custom:state-switch ?
Have you tried with a Grid “bottom-layer” inside the PE ?
If you combine Grid-Layout View, with a grid-layout-card as bottom-layer in PE ( or around the canvas ) you might get closer to your goals as you have 2 places to set mediaquery:

Correct, see my example with a car in a garage))).

Place your gauge-card inside CTC.
Check CTC girhub - there is a special “element” option for this case.