Lovelace: Weather card with chart

check this line in the card:

  computeWind(speed) {
    var calcSpeed = speed /*Math.round(speed * 1000 / 3600);*/
    return calcSpeed;
  }

and un-comment if you need it

Hey folks, I can’t get this to work. Probably a very basic issue I pressume.

First I got an issue with " “custom element doesn’t exist: weather card-chart", but that is resolved given a silly mistake.

Now I don’t get any warnings, so it should be Ok… but the issue is that it does not display anything at all - it’s completely empty, not even a pixel to indicate that something is loaded.

I have restarted, reloaded resources, but still no luck. I’ve used the code for the resource and for my Lovelace card as provided in the instructions.

Any clue?

Can you share where they’re hardcoded? I’ve also spent quite a bit of time trying to track this down and can change many of the other attributes (e.g. fill, borderWidth) but not line/fill color.

Fantastic card by the way!

1 Like

I have a similar issue. I am not experienced with HA am not clear on how to the settings and into which file. I did download the .js file and place in a www folder that I created. However, from there not sure. Appreciate if anyone can provide some basic instructions on where to place everything.

2 Likes

Can anyone help us with change line color?

@Marcus_Jansson, try installing it with HACS, which makes everything sooo much easier. Also note that the resources section has moved to configuration.yaml and is no longer in ui-lovelace.yaml. Nobody seems to be updating their readme’s to reflect that.

lovelace:
  resources:
    - url: /hacsfiles/lovelace-weather-card-chart/weather-card-chart.js
      type: module

This really is the best-looking weather card.

What is that card named in hacs? Can’t find it.

Never mind, I need to add a custom repository first.

You have to add it manually into hacs. Goto “Settings” and enter the git url as plugin

I have installed your edited card but receive an error related to the forecast_max_column line. If i comment out this line the card works with the basic details, but it’s the graph integration i’m most interested in as i already had the custom weather card. mode: daily was already in my dark_sky config.

Edit-so I restarted HA a couple of time’s since and the graph appears now. The forecast_max_col line still causes it to error but if I remove the line with code editor it’s fixed. The UI says it is optional but no value I enter removes it, any chance of a value being entered in UI removing instead or having it as a switch like other options?

It’s possible to change the standard icon, with animated icon?

yes it is:

well, at least to forecast icons, didn’t check for the main icon

wait, yes it is :wink:

will up the size a bit but yes, rather easily in fact.

Could you please explain to me how to change the icons?

sure, this is what I use now.

May-06-2020 20-59-29
most important changes:

add:
this.animatedIcons in line 185,
and use these in the card rendering line 114 for the main icon and 142 for the forecast icons

no use of ‘px’ in the sizes, (was for icons, these are images using https://www.w3schools.com/tags/tag_img.asp)

could be I have made some other changes in the past, so backup your current file first please :wink:

note the forecast icons could be outlined better, using a smaller size looks a bit better imo, see the img in the post above. Still, I do like the animated icons to be just that bit larger, so its a trade-off :wink:

const locale = {
  da: {
    tempHi: "Temperatur",
    tempLo: "Temperatur nat",
    precip: "Nedbør",
    uPress: "hPa",
    uSpeed: "m/s",
    uPrecip: "mm",
    cardinalDirections: [
      'N', 'N-NØ', 'NØ', 'Ø-NØ', 'Ø', 'Ø-SØ', 'SØ', 'S-SØ',
      'S', 'S-SV', 'SV', 'V-SV', 'V', 'V-NV', 'NV', 'N-NV', 'N'
    ]
  },
  en: {
    tempHi: "Temperature",
    tempLo: "Temperature night",
    precip: "Precipitations",
    uPress: "hPa",
    uSpeed: "km/h",
    uPrecip: "mm",
    cardinalDirections: [
      'N', 'N-NE', 'NE', 'E-NE', 'E', 'E-SE', 'SE', 'S-SE',
      'S', 'S-SW', 'SW', 'W-SW', 'W', 'W-NW', 'NW', 'N-NW', 'N'
    ]
  },
  fr: {
    tempHi: "Température",
    tempLo: "Température nuit",
    precip: "Précipitations",
    uPress: "hPa",
    uSpeed: "m/s",
    uPrecip: "mm",
    cardinalDirections: [
      'N', 'N-NE', 'NE', 'E-NE', 'E', 'E-SE', 'SE', 'S-SE',
      'S', 'S-SO', 'SO', 'O-SO', 'O', 'O-NO', 'NO', 'N-NO', 'N'
    ]
  },
  nl: {
    tempHi: "Maximum temperatuur",
    tempLo: "Minimum temperatuur",
    precip: "Neerslag",
    uPress: "hPa",
    uSpeed: "km/u",
    uPrecip: "mm",
    cardinalDirections: [
      'N', 'N-NO', 'NO', 'O-NO', 'O', 'O-ZO', 'ZO', 'Z-ZO',
      'Z', 'Z-ZW', 'ZW', 'W-ZW', 'W', 'W-NW', 'NW', 'N-NW', 'N'
    ]
  },
  ru: {
    tempHi: "Температура",
    tempLo: "Температура ночью",
    precip: "Осадки",
    uPress: "гПа",
    uSpeed: "м/с",
    uPrecip: "мм",
    cardinalDirections: [
      'С', 'С-СВ', 'СВ', 'В-СВ', 'В', 'В-ЮВ', 'ЮВ', 'Ю-ЮВ',
      'Ю', 'Ю-ЮЗ', 'ЮЗ', 'З-ЮЗ', 'З', 'З-СЗ', 'СЗ', 'С-СЗ', 'С'
    ]
  }
};

class WeatherCardChart extends Polymer.Element {

  static get template() {
    return Polymer.html`
      <style>
        ha-icon {
          color: var(--paper-item-icon-color);
        }
        .card {
          padding: 0 18px 18px 18px;
        }
        .main {
          display: flex;
          align-items: center;
          font-size: 60px;
          font-weight: 350;
          margin-top: -10px;
        }
        .main ha-icon {
          --iron-icon-height: 74px;
          --iron-icon-width: 74px;
          margin-right: 20px;
        }
        .main div {
          cursor: pointer;
          margin-top: -11px;
        }
        .main sup {
          font-size: 32px;
        }
        .attributes {
          cursor: pointer;
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin: 10px 0px 10px 0px;
        }
        .attributes div {
          text-align: left;
        }
        .conditions {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin: 0px 3px 0px 16px;
        }
      </style>
      <ha-card header="[[title]]">
        <div class="card">
          <div class="main">
            <img src="[[getAnimatedIcon(weatherObj.state)]]" height="100" width="100>" ></img src>
            <template is="dom-if" if="[[tempObj]]">
              <div on-click="_tempAttr">[[roundNumber(tempObj.state)]]<sup>[[getUnit('temperature')]]</sup></div>
            </template>
            <template is="dom-if" if="[[!tempObj]]">
              <div on-click="_weatherAttr">[[roundNumber(weatherObj.attributes.temperature)]]<sup>[[getUnit('temperature')]]</sup></div>
            </template>
          </div>
          <div class="attributes" on-click="_weatherAttr">
            <div>
              <ha-icon icon="hass:water-percent"></ha-icon> [[roundNumber(weatherObj.attributes.humidity)]] %<br>
              <ha-icon icon="hass:gauge"></ha-icon> [[roundNumber(weatherObj.attributes.pressure)]] [[ll('uPress')]]
            </div>
            <div>
              <template is="dom-if" if="[[sunObj]]">
                <ha-icon icon="mdi:weather-sunset-up"></ha-icon> [[computeTime(sunObj.attributes.next_rising)]]<br>
                <ha-icon icon="mdi:weather-sunset-down"></ha-icon> [[computeTime(sunObj.attributes.next_setting)]]
              </template>
            </div>
            <div>
              <ha-icon icon="hass:[[getWindDirIcon(windBearing)]]"></ha-icon> [[getWindDir(windBearing)]]<br>
              <ha-icon icon="hass:weather-windy"></ha-icon> [[computeWind(weatherObj.attributes.wind_speed)]] [[ll('uSpeed')]]
            </div>
          </div>
          <ha-chart-base data="[[ChartData]]"></ha-chart-base>
          <div class="conditions">
            <template is="dom-repeat" items="[[forecast]]">
              <div>
                <img src="[[getAnimatedIcon(item.condition)]]" width="60" height="60"></img src>
              </div>
            </template>
          </div>
        </div>
      </ha-card>
    `;
  }

  static get properties() {
    return {
      config: Object,
      sunObj: Object,
      tempObj: Object,
      mode: String,
      weatherObj: {
        type: Object,
        observer: 'dataChanged',
      },
    };
  }


  constructor() {
    super();
    this.mode = 'daily';
    this.weatherIcons = {
      'clear-night': 'hass:weather-night',
      'cloudy': 'hass:weather-cloudy',
      'fog': 'hass:weather-fog',
      'hail': 'hass:weather-hail',
      'lightning': 'hass:weather-lightning',
      'lightning-rainy': 'hass:weather-lightning-rainy',
      'partlycloudy': 'hass:weather-partly-cloudy',
      'pouring': 'hass:weather-pouring',
      'rainy': 'hass:weather-rainy',
      'snowy': 'hass:weather-snowy',
      'snowy-rainy': 'hass:weather-snowy-rainy',
      'sunny': 'hass:weather-sunny',
      'windy': 'hass:weather-windy',
      'windy-variant': 'hass:weather-windy-variant'
    };

    this.animatedIcons = {
      'clear-night': "/local/weather/animated/night.svg",
      'cloudy': "/local/weather/animated/cloudy.svg",
      'fog': "/local/weather/animated/cloudy.svg",
      'hail': "/local/weather/animated/rainy-7.svg",
      'lightning': "/local/weather/animated/thunder.svg",
      'lightning-rainy': "/local/weather/animated/rainy-5.svg",
      'partlycloudy': "/local/weather/animated/partly-cloudy-day.svg",
      'pouring': "/local/weather/animated/rainy-6.svg",
      'rainy': "/local/weather/animated/rainy-4.svg",
      'snowy': "/local/weather/animated/snowy-5.svg",
      'snowy-rainy': "/local/weather/animated/snowy-4.svg",
      'sunny': "/local/weather/animated/day.svg",
      'windy': 'hass:weather-windy',
      'windy-variant': 'hass:weather-windy-variant'
    };

    this.cardinalDirectionsIcon = [
      'mdi:arrow-down', 'mdi:arrow-bottom-left', 'mdi:arrow-left',
      'mdi:arrow-top-left', 'mdi:arrow-up', 'mdi:arrow-top-right',
      'mdi:arrow-right', 'mdi:arrow-bottom-right', 'mdi:arrow-down'
    ];
  }

  setConfig(config) {
    this.config = config;
    this.title = config.title;
    this.weatherObj = config.weather;
    this.tempObj = config.temp;
    this.mode = config.mode;
    if (!config.weather) {
      throw new Error('Please define "weather" entity in the card config');
    }
  }

  set hass(hass) {
    this._hass = hass;
    this.lang = this._hass.selectedLanguage || this._hass.language;
    this.weatherObj = this.config.weather in hass.states ? hass.states[this.config.weather] : null;
    this.sunObj = 'sun.sun' in hass.states ? hass.states['sun.sun'] : null;
    this.tempObj = this.config.temp in hass.states ? hass.states[this.config.temp] : null;
    this.forecast = this.weatherObj.attributes.forecast.slice(0,9);
    this.windBearing = this.weatherObj.attributes.wind_bearing;
  }

  dataChanged() {
    this.drawChart();
  }

  roundNumber(number) {
    var rounded = Math.round(number);
    return rounded;
  }

  ll(str) {
    if (locale[this.lang] === undefined)
      return locale.en[str];
    return locale[this.lang][str];
  }

  computeTime(time) {
    const date = new Date(time);
    return date.toLocaleTimeString(this.lang,
      { hour:'2-digit', minute:'2-digit' }
    );
  }

  computeWind(speed) {
    var calcSpeed = speed /*Math.round(speed * 1000 / 3600);*/
    return calcSpeed;
  }

  getCardSize() {
    return 4;
  }

  getUnit(unit) {
    return this._hass.config.unit_system[unit] || '';
  }

  getWeatherIcon(condition) {
    return this.weatherIcons[condition];
  }

  getAnimatedIcon(condition) {
    return this.animatedIcons[condition];
  }

  getWindDirIcon(degree) {
    return this.cardinalDirectionsIcon[parseInt((degree + 22.5) / 45.0)];
  }

  getWindDir(deg) {
    if (locale[this.lang] === undefined)
      return locale.en.cardinalDirections[parseInt((deg + 11.25) / 22.5)];
    return locale[this.lang]['cardinalDirections'][parseInt((deg + 11.25) / 22.5)];
  }

  drawChart() {
    var data = this.weatherObj.attributes.forecast.slice(0,9);
    var locale = this._hass.selectedLanguage || this._hass.language;
    var tempUnit = this._hass.config.unit_system.temperature;
    var lengthUnit = this._hass.config.unit_system.length;
    var precipUnit = lengthUnit === 'km' ? this.ll('uPrecip') : 'in';
    var mode = this.mode;
    var i;
    if (!this.weatherObj.attributes.forecast) {
      return [];
    }
    var dateTime = [];
    var tempHigh = [];
    var tempLow = [];
    var precip = [];
    for (i = 0; i < data.length; i++) {
      var d = data[i];
      dateTime.push(new Date(d.datetime));
      tempHigh.push(d.temperature);
      tempLow.push(d.templow);
      precip.push(d.precipitation);
    }
    var style = getComputedStyle(document.body);
    var textColor = style.getPropertyValue('--primary-text-color');
    var dividerColor = style.getPropertyValue('--divider-color');
    const chartOptions = {
      type: 'bar',
      data: {
        labels: dateTime,
        datasets: [
          {
            label: this.ll('tempHi'),
            type: 'line',
            data: tempHigh,
            yAxisID: 'TempAxis',
            borderWidth: 2.0,
            lineTension: 0.4,
            pointRadius: 0.0,
            pointHitRadius: 5.0,
            fill: false,
          },
          {
            label: this.ll('tempLo'),
            type: 'line',
            data: tempLow,
            yAxisID: 'TempAxis',
            borderWidth: 2.0,
            lineTension: 0.4,
            pointRadius: 0.0,
            pointHitRadius: 5.0,
            fill: false,
          },
          {
            label: this.ll('precip'),
            type: 'bar',
            data: precip,
            yAxisID: 'PrecipAxis',
          },
        ]
      },
      options: {
        animation: {
          duration: 300,
          easing: 'linear',
          onComplete: function () {
            var chartInstance = this.chart,
              ctx = chartInstance.ctx;
            ctx.fillStyle = textColor;
            var fontSize = 10;
            var fontStyle = 'normal';
            var fontFamily = 'Roboto';
            ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
            ctx.textAlign = 'center';
            ctx.textBaseline = 'bottom';
            var meta = chartInstance.controller.getDatasetMeta(2);
            meta.data.forEach(function (bar, index) {
              var data = (Math.round((chartInstance.data.datasets[2].data[index]) * 10) / 10).toFixed(1);
              ctx.fillText(data, bar._model.x, bar._model.y - 5);
            });
          },
        },
        legend: {
          display: false,
        },
        scales: {
          xAxes: [{
            type: 'time',
            maxBarThickness: 15,
            display: false,
            ticks: {
              display: false,
            },
            gridLines: {
              display: false,
            },
          },
          {
            id: 'DateAxis',
            position: 'top',
            gridLines: {
              display: true,
              drawBorder: false,
              color: dividerColor,
            },
            ticks: {
              display: true,
              source: 'labels',
              autoSkip: true,
              fontColor: textColor,
              maxRotation: 0,
              callback: function(value, index, values) {
                var data = new Date(value).toLocaleDateString(locale,
                  { weekday: 'short' });
                var time = new Date(value).toLocaleTimeString(locale,
                  { hour: 'numeric' });
                if (mode == 'hourly') {
                  return time;
                }
                return data;
              },
            },
          }],
          yAxes: [{
            id: 'TempAxis',
            position: 'left',
            gridLines: {
              display: true,
              drawBorder: false,
              color: dividerColor,
              borderDash: [1,3],
            },
            ticks: {
              display: true,
              fontColor: textColor,
            },
            afterFit: function(scaleInstance) {
              scaleInstance.width = 28;
            },
          },
          {
            id: 'PrecipAxis',
            position: 'right',
            gridLines: {
              display: false,
              drawBorder: false,
              color: dividerColor,
            },
            ticks: {
              display: false,
              min: 0,
              suggestedMax: 20,
              fontColor: textColor,
            },
            afterFit: function(scaleInstance) {
              scaleInstance.width = 15;
            },
          }],
        },
        tooltips: {
          mode: 'index',
          callbacks: {
            title: function (items, data) {
              const item = items[0];
              const date = data.labels[item.index];
              return new Date(date).toLocaleDateString(locale, {
                month: 'long',
                day: 'numeric',
                weekday: 'long',
                hour: 'numeric',
                minute: 'numeric',
              });
            },
            label: function(tooltipItems, data) {
              var label = data.datasets[tooltipItems.datasetIndex].label || '';
              if (data.datasets[2].label == label) {
                return label + ': ' + (tooltipItems.yLabel ?
                  (tooltipItems.yLabel + ' ' + precipUnit) : ('0 ' + precipUnit));
              }
              return label + ': ' + tooltipItems.yLabel + ' ' + tempUnit;
            },
          },
        },
      },
    };
    this.ChartData = chartOptions;
  }

  _fire(type, detail, options) {
    const node = this.shadowRoot;
    options = options || {};
    detail = (detail === null || detail === undefined) ? {} : detail;
    const e = new Event(type, {
      bubbles: options.bubbles === undefined ? true : options.bubbles,
      cancelable: Boolean(options.cancelable),
      composed: options.composed === undefined ? true : options.composed
    });
    e.detail = detail;
    node.dispatchEvent(e);
    return e;
  }

  _tempAttr() {
    this._fire('hass-more-info', { entityId: this.config.temp });
  }

  _weatherAttr() {
    this._fire('hass-more-info', { entityId: this.config.weather });
  }
}

customElements.define('weather-card-chart', WeatherCardChart);

Ok, works perfectly. But, it’s possible have a icon forecast for today more bigger?

just experiment with the width and height in the appropriate line in the card.

<img src="[[getAnimatedIcon(weatherObj.state)]]" height="100" width="100>" ></img src>

I found this size to be the practical max, before the rest of the card started to be out of coherence. That is, without changing other sizes in the config, which you could do too of course. simply try for your self, and reload the resources in between.

I tried changing the values ​​of that line, but then the part of the graph and the icons of humidity and wind pressure at sunrise and sunset moved all down.

yes, as I said, you have to find a value that doesnt interfere too much with the coherence of the rest of the card. Or start experimenting there too of course :wink:

it’s all available, edit the css style codes and immediately see the results.

card looks great, but does anyone know why I cannot get the daily forecast (or green graph line for that matter) to appear?

image

I have installed via HACs, config is the basic:

weather:
  - platform: openweathermap
    api_key: [] 

and lovelace-ui is just:

      - type: custom:weather-card-chart
        title: Weather
        weather: weather.openweathermap

I have tried including “mode: daily” but it makes no difference. Any thoughts?

The openweathermap integration currently only supplies forecasts for a 3hr period of each day. Means it can’t map separate night time temps. Etc. Hopefully, this will improve soon owm has a new api and people are working on t for HA.

Ahh! Thanks, makes sense