From 0792a21b747a7b8652443d9606756c2ad3e77200 Mon Sep 17 00:00:00 2001 From: tempus2016 Date: Thu, 29 Oct 2026 09:19:02 +0000 Subject: [PATCH] feat: column chart of totals per hour, day, week or month A new history style, `column`: vertical columns over time, for energy used per day, rain per hour or hours of heating per day. It uses the shared history frame, so periods, previous/next, statistics, axes, the tooltip and the compact layout all work as on the line chart. It is set up to be right with no options. With no period it shows the last 7 days; with no `group_by` it picks a grouping for the span (hours up to two days, days up to two months, then weeks, then months; today by hour, a week or month by day, a year by month); and with no `aggregate` it shows how much a meter went up (`change`) when every entity is a meter (`state_class` total or total_increasing), else the average. So an energy meter on its own draws a column of kWh per day. `group_by: none` draws one column per reading. - Rounded outer ends (`rounded`, on by default). Columns are drawn as HTML over the plot rather than in its stretched SVG, so corners stay round. - `labels` puts each column's value on top (a stack's total), and hides them when the columns are too narrow to fit them. - `stack: normal` piles series up; `stack: percent` scales each column to 100 %. Positive values stack up from zero, negative ones down. - Negative values hang below zero, and the axis always includes zero. - `color_by: bands` colours each column by its value's colour band. - `range: true` draws each column from the group's lowest to its highest, with the average marked across it. From statistics it asks the recorder for min and max; from history it takes the spread of the values held in each group. The shared history frame gained an opt-in for this, so other charts are unchanged. - Several entities sit side by side in each group. - An entity's `target` (from the Limits section) is marked across each of its columns. - When grouped, the time axis puts one label under each set of columns (Mon, Tue... or 26 Oct, Oct, Nov), every second or third when they would not fit, and the tooltip names the whole group ("26 Oct - 1 Nov") and highlights it. Editor: Chart section has the period (7 days by default), Group readings by (with Automatic and Don't group), Combine with, Stack, Colour, Range, Show values on the columns and Rounded tops, then axes and layout. Each entity gets its own Colour. Closes #11 --- dist/chartdeck-card.js | 650 +++++++++++++++++++++++------------------ src/chart/columns.ts | 152 ++++++++++ src/chart/frame.ts | 16 +- src/chart/grid.ts | 4 +- src/history/group.ts | 45 ++- src/styles/column.ts | 564 +++++++++++++++++++++++++++++++++++ src/styles/index.ts | 2 + src/types.ts | 15 +- test/column.test.ts | 303 +++++++++++++++++++ 9 files changed, 1460 insertions(+), 291 deletions(-) create mode 100644 src/chart/columns.ts create mode 100644 src/styles/column.ts create mode 100644 test/column.test.ts diff --git a/dist/chartdeck-card.js b/dist/chartdeck-card.js index 5721369..dc4193f 100644 --- a/dist/chartdeck-card.js +++ b/dist/chartdeck-card.js @@ -3,88 +3,88 @@ * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),n=new WeakMap;let o=class{constructor(t,e,n){if(this._$cssResult$=!0,n!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const i=this.t;if(e&&void 0===t){const e=void 0!==i&&1===i.length;e&&(t=n.get(i)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&n.set(i,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const n=1===t.length?t[0]:e.reduce((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1],t[0]);return new o(n,t,i)},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new o("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:s,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:d,getOwnPropertySymbols:h,getPrototypeOf:u}=Object,p=globalThis,m=p.trustedTypes,f=m?m.emptyScript:"",g=p.reactiveElementPolyfillSupport,_=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},v=(t,e)=>!s(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:v}; +const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,n=Symbol(),o=new WeakMap;let i=class{constructor(t,e,o){if(this._$cssResult$=!0,o!==n)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const n=this.t;if(e&&void 0===t){const e=void 0!==n&&1===n.length;e&&(t=o.get(n)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o.set(n,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const o=1===t.length?t[0]:e.reduce((e,n,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+t[o+1],t[0]);return new i(o,t,n)},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return(t=>new i("string"==typeof t?t:t+"",void 0,n))(e)})(t):t,{is:s,defineProperty:l,getOwnPropertyDescriptor:c,getOwnPropertyNames:d,getOwnPropertySymbols:h,getPrototypeOf:u}=Object,p=globalThis,m=p.trustedTypes,f=m?m.emptyScript:"",g=p.reactiveElementPolyfillSupport,y=(t,e)=>t,_={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let n=t;switch(e){case Boolean:n=null!==t;break;case Number:n=null===t?null:Number(t);break;case Object:case Array:try{n=JSON.parse(t)}catch(t){n=null}}return n}},v=(t,e)=>!s(t,e),b={attribute:!0,type:String,converter:_,reflect:!1,useDefault:!1,hasChanged:v}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let $=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),n=this.getPropertyDescriptor(t,i,e);void 0!==n&&l(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){const{get:n,set:o}=c(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:n,set(e){const r=n?.call(this);o?.call(this,e),this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(_("elementProperties")))return;const t=u(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(_("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(_("properties"))){const t=this.properties,e=[...d(t),...h(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[e,i]of this.elementProperties){const t=this._$Eu(e,i);void 0!==t&&this._$Eh.set(t,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,n)=>{if(e)i.adoptedStyleSheets=n.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of n){const n=document.createElement("style"),o=t.litNonce;void 0!==o&&n.setAttribute("nonce",o),n.textContent=e.cssText,i.appendChild(n)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),n=this.constructor._$Eu(t,i);if(void 0!==n&&!0===i.reflect){const o=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==o?this.removeAttribute(n):this.setAttribute(n,o),this._$Em=null}}_$AK(t,e){const i=this.constructor,n=i._$Eh.get(t);if(void 0!==n&&this._$Em!==n){const t=i.getPropertyOptions(n),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=n;const r=o.fromAttribute(e,t.type);this[n]=r??this._$Ej?.get(n)??r,this._$Em=null}}requestUpdate(t,e,i,n=!1,o){if(void 0!==t){const r=this.constructor;if(!1===n&&(o=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??v)(o,e)||i.useDefault&&i.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:n,wrapped:o},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??e??this[t]),!0!==o||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===n&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,n=this[e];!0!==t||this._$AL.has(e)||void 0===n||this.C(e,void 0,i,n)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};$.elementStyles=[],$.shadowRootOptions={mode:"open"},$[_("elementProperties")]=new Map,$[_("finalized")]=new Map,g?.({ReactiveElement:$}),(p.reactiveElementVersions??=[]).push("2.1.2"); + */Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let $=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const n=Symbol(),o=this.getPropertyDescriptor(t,n,e);void 0!==o&&l(this.prototype,t,o)}}static getPropertyDescriptor(t,e,n){const{get:o,set:i}=c(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const r=o?.call(this);i?.call(this,e),this.requestUpdate(t,r,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(y("elementProperties")))return;const t=u(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(y("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(y("properties"))){const t=this.properties,e=[...d(t),...h(t)];for(const n of e)this.createProperty(n,t[n])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,n]of e)this.elementProperties.set(t,n)}this._$Eh=new Map;for(const[e,n]of this.elementProperties){const t=this._$Eu(e,n);void 0!==t&&this._$Eh.set(t,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const n=new Set(t.flat(1/0).reverse());for(const t of n)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const n of e.keys())this.hasOwnProperty(n)&&(t.set(n,this[n]),delete this[n]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const n=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((n,o)=>{if(e)n.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of o){const o=document.createElement("style"),i=t.litNonce;void 0!==i&&o.setAttribute("nonce",i),o.textContent=e.cssText,n.appendChild(o)}})(n,this.constructor.elementStyles),n}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,n){this._$AK(t,n)}_$ET(t,e){const n=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,n);if(void 0!==o&&!0===n.reflect){const i=(void 0!==n.converter?.toAttribute?n.converter:_).toAttribute(e,n.type);this._$Em=t,null==i?this.removeAttribute(o):this.setAttribute(o,i),this._$Em=null}}_$AK(t,e){const n=this.constructor,o=n._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=n.getPropertyOptions(o),i="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:_;this._$Em=o;const r=i.fromAttribute(e,t.type);this[o]=r??this._$Ej?.get(o)??r,this._$Em=null}}requestUpdate(t,e,n,o=!1,i){if(void 0!==t){const r=this.constructor;if(!1===o&&(i=this[t]),n??=r.getPropertyOptions(t),!((n.hasChanged??v)(i,e)||n.useDefault&&n.reflect&&i===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,n))))return;this.C(t,e,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:n,reflect:o,wrapped:i},r){n&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??e??this[t]),!0!==i||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||n||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,n]of t){const{wrapped:t}=n,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,n,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};$.elementStyles=[],$.shadowRootOptions={mode:"open"},$[y("elementProperties")]=new Map,$[y("finalized")]=new Map,g?.({ReactiveElement:$}),(p.reactiveElementVersions??=[]).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const x=globalThis,w=t=>t,k=x.trustedTypes,A=k?k.createPolicy("lit-html",{createHTML:t=>t}):void 0,M="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,S="?"+C,E=`<${S}>`,O=document,T=()=>O.createComment(""),L=t=>null===t||"object"!=typeof t&&"function"!=typeof t,N=Array.isArray,U="[ \t\n\f\r]",P=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,z=/-->/g,j=/>/g,H=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),F=/'/g,D=/"/g,R=/^(?:script|style|textarea|title)$/i,B=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),V=B(1),I=B(2),W=Symbol.for("lit-noChange"),q=Symbol.for("lit-nothing"),Z=new WeakMap,G=O.createTreeWalker(O,129);function Y(t,e){if(!N(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==A?A.createHTML(e):e}class X{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let o=0,r=0;const a=t.length-1,s=this.parts,[l,c]=((t,e)=>{const i=t.length-1,n=[];let o,r=2===e?"":3===e?"":"",a=P;for(let s=0;s"===l[0]?(a=o??P,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,i=l[1],a=void 0===l[3]?H:'"'===l[3]?D:F):a===D||a===F?a=H:a===z||a===j?a=P:(a=H,o=void 0);const h=a===H&&t[s+1].startsWith("/>")?" ":"";r+=a===P?e+E:c>=0?(n.push(i),e.slice(0,c)+M+e.slice(c)+C+h):e+C+(-2===c?s:h)}return[Y(t,r+(t[i]||"")+(2===e?"":3===e?"":"")),n]})(t,e);if(this.el=X.createElement(l,i),G.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(n=G.nextNode())&&s.length0){n.textContent=k?k.emptyScript:"";for(let i=0;iN(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==q&&L(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,n="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===n)this._$AH.p(e);else{const t=new K(n,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=Z.get(t.strings);return void 0===e&&Z.set(t.strings,e=new X(t)),e}k(t){N(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,n=0;for(const o of t)n===e.length?e.push(i=new Q(this.O(T()),this.O(T()),this,this.options)):i=e[n],i._$AI(o),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=q}_$AI(t,e=this,i,n){const o=this.strings;let r=!1;if(void 0===o)t=J(this,t,e,0),r=!L(t)||t!==this._$AH&&t!==W,r&&(this._$AH=t);else{const n=t;let a,s;for(t=o[0],a=0;at,k=x.trustedTypes,M=k?k.createPolicy("lit-html",{createHTML:t=>t}):void 0,A="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,S="?"+C,E=`<${S}>`,O=document,T=()=>O.createComment(""),L=t=>null===t||"object"!=typeof t&&"function"!=typeof t,N=Array.isArray,U="[ \t\n\f\r]",j=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,z=/-->/g,P=/>/g,H=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),F=/'/g,D=/"/g,R=/^(?:script|style|textarea|title)$/i,V=t=>(e,...n)=>({_$litType$:t,strings:e,values:n}),B=V(1),I=V(2),W=Symbol.for("lit-noChange"),Z=Symbol.for("lit-nothing"),q=new WeakMap,Y=O.createTreeWalker(O,129);function G(t,e){if(!N(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==M?M.createHTML(e):e}class X{constructor({strings:t,_$litType$:e},n){let o;this.parts=[];let i=0,r=0;const a=t.length-1,s=this.parts,[l,c]=((t,e)=>{const n=t.length-1,o=[];let i,r=2===e?"":3===e?"":"",a=j;for(let s=0;s"===l[0]?(a=i??j,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,n=l[1],a=void 0===l[3]?H:'"'===l[3]?D:F):a===D||a===F?a=H:a===z||a===P?a=j:(a=H,i=void 0);const h=a===H&&t[s+1].startsWith("/>")?" ":"";r+=a===j?e+E:c>=0?(o.push(n),e.slice(0,c)+A+e.slice(c)+C+h):e+C+(-2===c?s:h)}return[G(t,r+(t[n]||"")+(2===e?"":3===e?"":"")),o]})(t,e);if(this.el=X.createElement(l,n),Y.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=Y.nextNode())&&s.length0){o.textContent=k?k.emptyScript:"";for(let n=0;nN(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==Z&&L(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:n}=t,o="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=X.createElement(G(n.h,n.h[0]),this.options)),n);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new K(o,this),n=t.u(this.options);t.p(e),this.T(n),this._$AH=t}}_$AC(t){let e=q.get(t.strings);return void 0===e&&q.set(t.strings,e=new X(t)),e}k(t){N(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let n,o=0;for(const i of t)o===e.length?e.push(n=new Q(this.O(T()),this.O(T()),this,this.options)):n=e[o],n._$AI(i),o++;o2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=Z}_$AI(t,e=this,n,o){const i=this.strings;let r=!1;if(void 0===i)t=J(this,t,e,0),r=!L(t)||t!==this._$AH&&t!==W,r&&(this._$AH=t);else{const o=t;let a,s;for(t=i[0],a=0;a{const n=i?.renderBefore??e;let o=n._$litPart$;if(void 0===o){const t=i?.renderBefore??null;n._$litPart$=o=new Q(e.insertBefore(T(),t),t,void 0,i??{})}return o._$AI(t),o})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return W}};lt._$litElement$=!0,lt.finalized=!0,st.litElementHydrateSupport?.({LitElement:lt});const ct=st.litElementPolyfillSupport;ct?.({LitElement:lt}),(st.litElementVersions??=[]).push("4.2.2"); + */let lt=class extends ${constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=((t,e,n)=>{const o=n?.renderBefore??e;let i=o._$litPart$;if(void 0===i){const t=n?.renderBefore??null;o._$litPart$=i=new Q(e.insertBefore(T(),t),t,void 0,n??{})}return i._$AI(t),i})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return W}};lt._$litElement$=!0,lt.finalized=!0,st.litElementHydrateSupport?.({LitElement:lt});const ct=st.litElementPolyfillSupport;ct?.({LitElement:lt}),(st.litElementVersions??=[]).push("4.2.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const dt=1,ht=2,ut=3,pt=4,mt=t=>(...e)=>({_$litDirective$:t,values:e});let ft=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}; +const dt=1,ht=2,ut=3,pt=4,mt=t=>(...e)=>({_$litDirective$:t,values:e});let ft=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,n){this._$Ct=t,this._$AM=e,this._$Ci=n}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}; /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */const{I:gt}=rt,_t=t=>t,yt=()=>document.createComment(""),vt=(t,e,i)=>{const n=t._$AA.parentNode,o=void 0===e?t._$AB:e._$AA;if(void 0===i){const e=n.insertBefore(yt(),o),r=n.insertBefore(yt(),o);i=new gt(e,r,t,t.options)}else{const e=i._$AB.nextSibling,r=i._$AM,a=r!==t;if(a){let e;i._$AQ?.(t),i._$AM=t,void 0!==i._$AP&&(e=t._$AU)!==r._$AU&&i._$AP(e)}if(e!==o||a){let t=i._$AA;for(;t!==e;){const e=_t(t).nextSibling;_t(n).insertBefore(t,o),t=e}}}return i},bt=(t,e,i=t)=>(t._$AI(e,i),t),$t={},xt=(t,e=$t)=>t._$AH=e,wt=t=>{t._$AR(),t._$AA.remove()},kt=(t,e,i)=>{const n=new Map;for(let o=e;o<=i;o++)n.set(t[o],o);return n},At=mt(class extends ft{constructor(t){if(super(t),t.type!==ht)throw Error("repeat() can only be used in text expressions")}dt(t,e,i){let n;void 0===i?i=e:void 0!==e&&(n=e);const o=[],r=[];let a=0;for(const s of t)o[a]=n?n(s,a):a,r[a]=i(s,a),a++;return{values:r,keys:o}}render(t,e,i){return this.dt(t,e,i).values}update(t,[e,i,n]){const o=(t=>t._$AH)(t),{values:r,keys:a}=this.dt(e,i,n);if(!Array.isArray(o))return this.ut=a,r;const s=this.ut??=[],l=[];let c,d,h=0,u=o.length-1,p=0,m=r.length-1;for(;h<=u&&p<=m;)if(null===o[h])h++;else if(null===o[u])u--;else if(s[h]===a[p])l[p]=bt(o[h],r[p]),h++,p++;else if(s[u]===a[m])l[m]=bt(o[u],r[m]),u--,m--;else if(s[h]===a[m])l[m]=bt(o[h],r[m]),vt(t,l[m+1],o[h]),h++,m--;else if(s[u]===a[p])l[p]=bt(o[u],r[p]),vt(t,o[h],o[u]),u--,p++;else if(void 0===c&&(c=kt(a,p,m),d=kt(s,h,u)),c.has(s[h]))if(c.has(s[u])){const e=d.get(a[p]),i=void 0!==e?o[e]:null;if(null===i){const e=vt(t,o[h]);bt(e,r[p]),l[p]=e}else l[p]=bt(i,r[p]),vt(t,o[h],i),o[e]=null;p++}else wt(o[u]),u--;else wt(o[h]),h++;for(;p<=m;){const e=vt(t,l[m+1]);bt(e,r[p]),l[p++]=e}for(;h<=u;){const t=o[h++];null!==t&&wt(t)}return this.ut=a,xt(t,l),W}}),Mt=t=>!!t&&"none"!==t.action; + */const{I:gt}=rt,yt=t=>t,_t=()=>document.createComment(""),vt=(t,e,n)=>{const o=t._$AA.parentNode,i=void 0===e?t._$AB:e._$AA;if(void 0===n){const e=o.insertBefore(_t(),i),r=o.insertBefore(_t(),i);n=new gt(e,r,t,t.options)}else{const e=n._$AB.nextSibling,r=n._$AM,a=r!==t;if(a){let e;n._$AQ?.(t),n._$AM=t,void 0!==n._$AP&&(e=t._$AU)!==r._$AU&&n._$AP(e)}if(e!==i||a){let t=n._$AA;for(;t!==e;){const e=yt(t).nextSibling;yt(o).insertBefore(t,i),t=e}}}return n},bt=(t,e,n=t)=>(t._$AI(e,n),t),$t={},xt=(t,e=$t)=>t._$AH=e,wt=t=>{t._$AR(),t._$AA.remove()},kt=(t,e,n)=>{const o=new Map;for(let i=e;i<=n;i++)o.set(t[i],i);return o},Mt=mt(class extends ft{constructor(t){if(super(t),t.type!==ht)throw Error("repeat() can only be used in text expressions")}dt(t,e,n){let o;void 0===n?n=e:void 0!==e&&(o=e);const i=[],r=[];let a=0;for(const s of t)i[a]=o?o(s,a):a,r[a]=n(s,a),a++;return{values:r,keys:i}}render(t,e,n){return this.dt(t,e,n).values}update(t,[e,n,o]){const i=(t=>t._$AH)(t),{values:r,keys:a}=this.dt(e,n,o);if(!Array.isArray(i))return this.ut=a,r;const s=this.ut??=[],l=[];let c,d,h=0,u=i.length-1,p=0,m=r.length-1;for(;h<=u&&p<=m;)if(null===i[h])h++;else if(null===i[u])u--;else if(s[h]===a[p])l[p]=bt(i[h],r[p]),h++,p++;else if(s[u]===a[m])l[m]=bt(i[u],r[m]),u--,m--;else if(s[h]===a[m])l[m]=bt(i[h],r[m]),vt(t,l[m+1],i[h]),h++,m--;else if(s[u]===a[p])l[p]=bt(i[u],r[p]),vt(t,i[h],i[u]),u--,p++;else if(void 0===c&&(c=kt(a,p,m),d=kt(s,h,u)),c.has(s[h]))if(c.has(s[u])){const e=d.get(a[p]),n=void 0!==e?i[e]:null;if(null===n){const e=vt(t,i[h]);bt(e,r[p]),l[p]=e}else l[p]=bt(n,r[p]),vt(t,i[h],n),i[e]=null;p++}else wt(i[u]),u--;else wt(i[h]),h++;for(;p<=m;){const e=vt(t,l[m+1]);bt(e,r[p]),l[p++]=e}for(;h<=u;){const t=i[h++];null!==t&&wt(t)}return this.ut=a,xt(t,l),W}}),At=t=>!!t&&"none"!==t.action; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function Ct(t,e){let i,n,o=0,r=0,a=!1,s=!1;const l=i=>{const n=e(),o="tap"===i?n.tap_action:"hold"===i?n.hold_action:n.double_tap_action;Mt(o)&&t.dispatchEvent(new CustomEvent("hass-action",{bubbles:!0,composed:!0,detail:{config:n,action:i}}))},c=()=>{void 0!==i&&clearTimeout(i),i=void 0},d=t=>{const n=t;n.button>0||(a=!0,s=!1,o=n.clientX,r=n.clientY,c(),Mt(e().hold_action)&&(i=setTimeout(()=>{i=void 0,s=!0,l("hold")},500)))},h=t=>{if(!a)return;const e=t;Math.hypot(e.clientX-o,e.clientY-r)>10&&(c(),a=!1)},u=()=>{c(),a=!1},p=()=>{const t=a;c(),a=!1,t&&!s?Mt(e().double_tap_action)?void 0!==n?(clearTimeout(n),n=void 0,l("double_tap")):n=setTimeout(()=>{n=void 0,l("tap")},250):l("tap"):s=!1},m=t=>{const e=t;e.repeat||"Enter"!==e.key&&" "!==e.key||(" "===e.key&&e.preventDefault(),l("tap"))},f=t=>{Mt(e().hold_action)&&t.preventDefault()};return t.addEventListener("pointerdown",d),t.addEventListener("pointermove",h),t.addEventListener("pointerup",p),t.addEventListener("pointercancel",u),t.addEventListener("pointerleave",u),t.addEventListener("keydown",m),t.addEventListener("contextmenu",f),()=>{c(),void 0!==n&&clearTimeout(n),n=void 0,t.removeEventListener("pointerdown",d),t.removeEventListener("pointermove",h),t.removeEventListener("pointerup",p),t.removeEventListener("pointercancel",u),t.removeEventListener("pointerleave",u),t.removeEventListener("keydown",m),t.removeEventListener("contextmenu",f)}}const St={primary:{variable:"--primary-color",fallback:"#2f6fe4"},green:{variable:"--success-color",fallback:"#1fae7a"},amber:{variable:"--warning-color",fallback:"#f59e0b"},red:{variable:"--error-color",fallback:"#ef3b5a"},blue:{variable:"--info-color",fallback:"#2f6fe4"},purple:{variable:"--purple-color",fallback:"#8b6cff"},grey:{variable:"--grey-color",fallback:"#8e919a"}};function Et(t){if(Object.prototype.hasOwnProperty.call(St,t)){const{variable:e,fallback:i}=St[t];return`var(${e}, ${i})`}return t}const Ot=new Set(["amber"]);function Tt(t){if(Object.prototype.hasOwnProperty.call(St,t))return Ot.has(t)?"dark":"light";const e=function(t){const e=t.trim().toLowerCase(),i=/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(e);if(i){const t=i[1].length<=4?[...i[1]].map(t=>t+t).join(""):i[1],[e,n,o]=[0,2,4].map(e=>parseInt(t.slice(e,e+2),16));return[e,n,o,8===t.length?parseInt(t.slice(6,8),16)/255:1]}const n=/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+)(%?)\s*)?\)$/.exec(e);if(n){const[t,e,i]=[n[1],n[2],n[3]].map(t=>Math.min(255,Number(t))),o=void 0===n[4]?1:Number(n[4])/(n[5]?100:1);return[t,e,i,Math.min(1,o)]}}(t);return e?e[3]<.5?"theme":function([t,e,i]){const n=t=>{const e=t/255;return e<=.03928?e/12.92:((e+.055)/1.055)**2.4};return.2126*n(t)+.7152*n(e)+.0722*n(i)}([e[0],e[1],e[2]])>.4?"dark":"light":"light"}function Lt(t,e){let i=e[0];for(const n of e){if(!(n.from<=t))break;i=n}return i}const Nt={battery:[{from:0,color:"red",label:"Low"},{from:20,color:"amber"},{from:50,color:"green"}],usage:[{from:0,color:"green"},{from:60,color:"amber"},{from:85,color:"red",label:"High"}],temperature:[{from:-50,color:"blue",label:"Cold"},{from:16,color:"green"},{from:24,color:"amber"},{from:28,color:"red",label:"Hot"}],none:void 0},Ut=["label","area","floor","device_class","domain","entity"];function Pt(t,e){if(null==t)return;if("object"!=typeof t||Array.isArray(t))throw new Error(`"${e}" must list what to match, such as "area" or "device_class"`);const i="include"===e?Ut.filter(t=>"entity"!==t):Ut,n={};for(const[o,r]of Object.entries(t)){if(!i.includes(o))throw new Error(`"${e}" can't match on "${o}". Use: ${i.join(", ")}`);const t=(Array.isArray(r)?r:[r]).filter(t=>null!=t&&""!==t);if(t.some(t=>"string"!=typeof t))throw new Error(`"${e}.${o}" must be text or a list of text`);t.length&&(n[o]=t)}return Object.keys(n).length?n:void 0}function zt(t){const e=t.state;return"unavailable"===e||"unknown"===e||""!==e.trim()&&Number.isFinite(Number(e))}function jt(t,e){const i=t.entity_id,n=e.entities?.[i],o=n?.device_id?e.devices?.[n.device_id]:void 0,r=n?.area_id??o?.area_id??void 0,a=t.attributes?.device_class;return{label:[...n?.labels??[],...o?.labels??[]],area:r,floor:r?e.areas?.[r]?.floor_id??void 0:void 0,device_class:"string"==typeof a?a:void 0,domain:i.slice(0,i.indexOf(".")),entity:i}}function Ht(t,e,i){const n=t[e],o=i[e];return Array.isArray(o)?o.some(t=>n.includes(t)):void 0!==o&&n.includes(o)}function Ft(t,e){return Object.keys(t).every(i=>Ht(t,i,e))}function Dt(t,e){return!!t&&Object.keys(t).some(i=>Ht(t,i,e))}const Rt=new WeakMap;const Bt=["bar","thin","segmented","tank","ring","needle","arc","line","area"],Vt=[{from:-1/0,color:"primary"}],It=["none","ascending","descending"];function Wt(t){if("number"==typeof t)return Number.isFinite(t)?t:void 0;if("string"==typeof t&&""!==t.trim()){const e=Number(t);return Number.isFinite(e)?e:void 0}}function qt(t,e){if(null==t||""===t)return;const i=Wt(t);if(void 0!==i)return i;if("string"==typeof t)return t;throw new Error(`"${e}" must be a number or an entity id`)}function Zt(t,e,i,n=1/0){if(null==t||""===t)return;const o=Wt(t);if(void 0===o||!Number.isInteger(o)||on){throw new Error(`"${e}" must be a whole number ${n===1/0?`of at least ${i}`:`from ${i} to ${n}`}`)}return o}function Gt(t){if(null==t)return;if(!Array.isArray(t))throw new Error('"severity" must be a list of bands');if(0===t.length)return;const e=t.map((t,e)=>{let i;try{i=qt(t?.from,"from")}catch{i=void 0}if(void 0===i)throw new Error(`Severity band ${e+1}: "from" must be a number or an entity id`);if("string"!=typeof t.color||!t.color)throw new Error(`Severity band ${e+1}: "color" is required`);return{...t,from:i}});return e.every(t=>"number"==typeof t.from)?e.sort((t,e)=>t.from-e.from):e}function Yt(t,e,i){if(null==t)return{action:e};if("object"!=typeof t||"string"!=typeof t.action)throw new Error(`"${i}" must have an "action"`);return t}function Xt(t,e){return void 0!==t?t:e}function Jt(t,e,i){const n="string"==typeof t?{entity:t}:t;if(!n||"object"!=typeof n||"string"!=typeof n.entity||!n.entity)throw new Error(`Entity ${i+1} needs an "entity" id`);if(void 0!==n.attribute&&null!==n.attribute&&"string"!=typeof n.attribute)throw new Error(`Entity ${i+1}: "attribute" must be an attribute name`);const o=Xt(qt(n.min,"min"),qt(e.min,"min"))??0,r=Xt(qt(n.max,"max"),qt(e.max,"max"))??100;if("number"==typeof o&&"number"==typeof r&&o>=r)throw new Error(`"max" (${r}) must be greater than "min" (${o})`);const a={...n,entity:n.entity,min:o,max:r,severity:Gt(n.severity)??Gt(e.severity)??Vt,tap_action:Yt(Xt(n.tap_action,e.tap_action),"more-info","tap_action"),hold_action:Yt(Xt(n.hold_action,e.hold_action),"none","hold_action"),double_tap_action:Yt(Xt(n.double_tap_action,e.double_tap_action),"none","double_tap_action")},s=Xt(n.name,e.name),l=Xt(n.icon,e.icon),c=Xt(n.unit,e.unit),d=Xt(qt(n.target,"target"),qt(e.target,"target")),h=Xt(Zt(n.decimals,"decimals",0,20),Zt(e.decimals,"decimals",0,20));return void 0!==s&&(a.name=s),void 0!==l&&(a.icon=l),void 0!==c&&(a.unit=c),void 0!==d&&(a.target=d),void 0!==h&&(a.decimals=h),n.attribute||delete a.attribute,a} + */function Ct(t,e){let n,o,i=0,r=0,a=!1,s=!1;const l=n=>{const o=e(),i="tap"===n?o.tap_action:"hold"===n?o.hold_action:o.double_tap_action;At(i)&&t.dispatchEvent(new CustomEvent("hass-action",{bubbles:!0,composed:!0,detail:{config:o,action:n}}))},c=()=>{void 0!==n&&clearTimeout(n),n=void 0},d=t=>{const o=t;o.button>0||(a=!0,s=!1,i=o.clientX,r=o.clientY,c(),At(e().hold_action)&&(n=setTimeout(()=>{n=void 0,s=!0,l("hold")},500)))},h=t=>{if(!a)return;const e=t;Math.hypot(e.clientX-i,e.clientY-r)>10&&(c(),a=!1)},u=()=>{c(),a=!1},p=()=>{const t=a;c(),a=!1,t&&!s?At(e().double_tap_action)?void 0!==o?(clearTimeout(o),o=void 0,l("double_tap")):o=setTimeout(()=>{o=void 0,l("tap")},250):l("tap"):s=!1},m=t=>{const e=t;e.repeat||"Enter"!==e.key&&" "!==e.key||(" "===e.key&&e.preventDefault(),l("tap"))},f=t=>{At(e().hold_action)&&t.preventDefault()};return t.addEventListener("pointerdown",d),t.addEventListener("pointermove",h),t.addEventListener("pointerup",p),t.addEventListener("pointercancel",u),t.addEventListener("pointerleave",u),t.addEventListener("keydown",m),t.addEventListener("contextmenu",f),()=>{c(),void 0!==o&&clearTimeout(o),o=void 0,t.removeEventListener("pointerdown",d),t.removeEventListener("pointermove",h),t.removeEventListener("pointerup",p),t.removeEventListener("pointercancel",u),t.removeEventListener("pointerleave",u),t.removeEventListener("keydown",m),t.removeEventListener("contextmenu",f)}}const St={primary:{variable:"--primary-color",fallback:"#2f6fe4"},green:{variable:"--success-color",fallback:"#1fae7a"},amber:{variable:"--warning-color",fallback:"#f59e0b"},red:{variable:"--error-color",fallback:"#ef3b5a"},blue:{variable:"--info-color",fallback:"#2f6fe4"},purple:{variable:"--purple-color",fallback:"#8b6cff"},grey:{variable:"--grey-color",fallback:"#8e919a"}};function Et(t){if(Object.prototype.hasOwnProperty.call(St,t)){const{variable:e,fallback:n}=St[t];return`var(${e}, ${n})`}return t}const Ot=new Set(["amber"]);function Tt(t){if(Object.prototype.hasOwnProperty.call(St,t))return Ot.has(t)?"dark":"light";const e=function(t){const e=t.trim().toLowerCase(),n=/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(e);if(n){const t=n[1].length<=4?[...n[1]].map(t=>t+t).join(""):n[1],[e,o,i]=[0,2,4].map(e=>parseInt(t.slice(e,e+2),16));return[e,o,i,8===t.length?parseInt(t.slice(6,8),16)/255:1]}const o=/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+)(%?)\s*)?\)$/.exec(e);if(o){const[t,e,n]=[o[1],o[2],o[3]].map(t=>Math.min(255,Number(t))),i=void 0===o[4]?1:Number(o[4])/(o[5]?100:1);return[t,e,n,Math.min(1,i)]}}(t);return e?e[3]<.5?"theme":function([t,e,n]){const o=t=>{const e=t/255;return e<=.03928?e/12.92:((e+.055)/1.055)**2.4};return.2126*o(t)+.7152*o(e)+.0722*o(n)}([e[0],e[1],e[2]])>.4?"dark":"light":"light"}function Lt(t,e){let n=e[0];for(const o of e){if(!(o.from<=t))break;n=o}return n}const Nt={battery:[{from:0,color:"red",label:"Low"},{from:20,color:"amber"},{from:50,color:"green"}],usage:[{from:0,color:"green"},{from:60,color:"amber"},{from:85,color:"red",label:"High"}],temperature:[{from:-50,color:"blue",label:"Cold"},{from:16,color:"green"},{from:24,color:"amber"},{from:28,color:"red",label:"Hot"}],none:void 0},Ut=["label","area","floor","device_class","domain","entity"];function jt(t,e){if(null==t)return;if("object"!=typeof t||Array.isArray(t))throw new Error(`"${e}" must list what to match, such as "area" or "device_class"`);const n="include"===e?Ut.filter(t=>"entity"!==t):Ut,o={};for(const[i,r]of Object.entries(t)){if(!n.includes(i))throw new Error(`"${e}" can't match on "${i}". Use: ${n.join(", ")}`);const t=(Array.isArray(r)?r:[r]).filter(t=>null!=t&&""!==t);if(t.some(t=>"string"!=typeof t))throw new Error(`"${e}.${i}" must be text or a list of text`);t.length&&(o[i]=t)}return Object.keys(o).length?o:void 0}function zt(t){const e=t.state;return"unavailable"===e||"unknown"===e||""!==e.trim()&&Number.isFinite(Number(e))}function Pt(t,e){const n=t.entity_id,o=e.entities?.[n],i=o?.device_id?e.devices?.[o.device_id]:void 0,r=o?.area_id??i?.area_id??void 0,a=t.attributes?.device_class;return{label:[...o?.labels??[],...i?.labels??[]],area:r,floor:r?e.areas?.[r]?.floor_id??void 0:void 0,device_class:"string"==typeof a?a:void 0,domain:n.slice(0,n.indexOf(".")),entity:n}}function Ht(t,e,n){const o=t[e],i=n[e];return Array.isArray(i)?i.some(t=>o.includes(t)):void 0!==i&&o.includes(i)}function Ft(t,e){return Object.keys(t).every(n=>Ht(t,n,e))}function Dt(t,e){return!!t&&Object.keys(t).some(n=>Ht(t,n,e))}const Rt=new WeakMap;const Vt=["bar","thin","segmented","tank","ring","needle","arc","line","column","area"],Bt=[{from:-1/0,color:"primary"}],It=["none","ascending","descending"];function Wt(t){if("number"==typeof t)return Number.isFinite(t)?t:void 0;if("string"==typeof t&&""!==t.trim()){const e=Number(t);return Number.isFinite(e)?e:void 0}}function Zt(t,e){if(null==t||""===t)return;const n=Wt(t);if(void 0!==n)return n;if("string"==typeof t)return t;throw new Error(`"${e}" must be a number or an entity id`)}function qt(t,e,n,o=1/0){if(null==t||""===t)return;const i=Wt(t);if(void 0===i||!Number.isInteger(i)||io){throw new Error(`"${e}" must be a whole number ${o===1/0?`of at least ${n}`:`from ${n} to ${o}`}`)}return i}function Yt(t){if(null==t)return;if(!Array.isArray(t))throw new Error('"severity" must be a list of bands');if(0===t.length)return;const e=t.map((t,e)=>{let n;try{n=Zt(t?.from,"from")}catch{n=void 0}if(void 0===n)throw new Error(`Severity band ${e+1}: "from" must be a number or an entity id`);if("string"!=typeof t.color||!t.color)throw new Error(`Severity band ${e+1}: "color" is required`);return{...t,from:n}});return e.every(t=>"number"==typeof t.from)?e.sort((t,e)=>t.from-e.from):e}function Gt(t,e,n){if(null==t)return{action:e};if("object"!=typeof t||"string"!=typeof t.action)throw new Error(`"${n}" must have an "action"`);return t}function Xt(t,e){return void 0!==t?t:e}function Jt(t,e,n){const o="string"==typeof t?{entity:t}:t;if(!o||"object"!=typeof o||"string"!=typeof o.entity||!o.entity)throw new Error(`Entity ${n+1} needs an "entity" id`);if(void 0!==o.attribute&&null!==o.attribute&&"string"!=typeof o.attribute)throw new Error(`Entity ${n+1}: "attribute" must be an attribute name`);const i=Xt(Zt(o.min,"min"),Zt(e.min,"min"))??0,r=Xt(Zt(o.max,"max"),Zt(e.max,"max"))??100;if("number"==typeof i&&"number"==typeof r&&i>=r)throw new Error(`"max" (${r}) must be greater than "min" (${i})`);const a={...o,entity:o.entity,min:i,max:r,severity:Yt(o.severity)??Yt(e.severity)??Bt,tap_action:Gt(Xt(o.tap_action,e.tap_action),"more-info","tap_action"),hold_action:Gt(Xt(o.hold_action,e.hold_action),"none","hold_action"),double_tap_action:Gt(Xt(o.double_tap_action,e.double_tap_action),"none","double_tap_action")},s=Xt(o.name,e.name),l=Xt(o.icon,e.icon),c=Xt(o.unit,e.unit),d=Xt(Zt(o.target,"target"),Zt(e.target,"target")),h=Xt(qt(o.decimals,"decimals",0,20),qt(e.decimals,"decimals",0,20));return void 0!==s&&(a.name=s),void 0!==l&&(a.icon=l),void 0!==c&&(a.unit=c),void 0!==d&&(a.target=d),void 0!==h&&(a.decimals=h),o.attribute||delete a.attribute,a} /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt&&t.type!==pt)throw Error("The `live` directive is not allowed on child or event bindings");if(!(t=>void 0===t.strings)(t))throw Error("`live` bindings can only contain a single expression")}render(t){return t}update(t,[e]){if(e===W||e===q)return e;const i=t.element,n=t.name;if(t.type===ut){if(e===i[n])return W}else if(t.type===pt){if(!!e===i.hasAttribute(n))return W}else if(t.type===dt&&i.getAttribute(n)===e+"")return W;return xt(t),e}}),Qt={en:{entity_not_found:"Entity not found",unavailable:"Unavailable",no_entities:"Add an entity in the editor to get started.",no_matches:"No entities match right now.",none_available:"None of these entities are available.",editor_section_entities:"Entities",editor_section_style:"Style",editor_section_chart:"Chart",editor_group_bars:"Bars",editor_group_gauges:"Gauges",editor_group_history:"History charts",editor_group_totals:"Parts of a total",editor_group_compare:"Comparisons",editor_group_distribution:"Distributions",editor_period_last:"Show the last",editor_period_unit:"Unit",editor_period_unit_hour:"Hours",editor_period_unit_day:"Days",editor_period_unit_week:"Weeks",editor_period_unit_month:"Months",editor_source:"Readings from",editor_source_history:"History (every change)",editor_source_statistics:"Long-term statistics",editor_curve:"Line shape",editor_curve_smooth:"Smooth",editor_curve_straight:"Straight",editor_curve_step:"Steps",editor_color_by:"Colour",editor_color_by_series:"One colour per entity",editor_color_by_bands:"By colour band",editor_labels:"Show values on the points",editor_series_color:"Line colour",editor_dash:"Dashed",editor_section_limits:"Limits",editor_section_colours:"Colours",editor_section_display:"Display",editor_section_actions:"Actions",editor_title:"Title",editor_style_bar:"Bar",editor_style_thin:"Thin",editor_style_segmented:"Segmented",editor_style_tank:"Tank",editor_style_ring:"Ring",editor_style_needle:"Needle",editor_style_arc:"Arc",editor_show_name:"Show name",editor_show_value:"Show value",editor_show_icon:"Show icon",editor_show_target:"Show target",editor_show_legend:"Show legend",editor_columns:"Columns",editor_sort:"Sort",editor_sort_none:"As listed",editor_sort_ascending:"Lowest first",editor_sort_descending:"Highest first",editor_decimals:"Decimals",editor_unit:"Unit",editor_animate:"Animate",editor_hide_unavailable:"Hide unavailable entities",editor_limit:"Show at most",editor_include_heading:"Also show every entity that matches",editor_include_help:"New devices appear without editing the card. Pick several of one kind to match any of them; different kinds must all match.",editor_exclude_heading:"Leave out",editor_exclude_help:"Matched entities are left out if they match any of these.",editor_rule_device_class:"Device class",editor_rule_area:"Areas",editor_rule_floor:"Floors",editor_rule_label:"Labels",editor_rule_domain:"Domains",editor_rule_entity:"Entities",editor_tap_action:"Tap action",editor_hold_action:"Hold action",editor_double_tap_action:"Double tap action",editor_attribute:"Attribute (instead of the state)",editor_name:"Name",editor_icon:"Icon",editor_min:"Minimum",editor_max:"Maximum",editor_target:"Target",editor_none:"None",editor_card_setting:"card setting",editor_limit_default:"Default ({value})",editor_limit_number:"Number",editor_limit_entity:"Entity",editor_preset:"Preset",editor_preset_battery:"Battery (low is bad)",editor_preset_usage:"Usage (high is bad)",editor_preset_temperature:"Temperature",editor_preset_none:"None",editor_preset_custom:"Custom",editor_band_from:"From",editor_band_from_entity:"Start from an entity",editor_band_from_number:"Start from a number",editor_band_from_entity_label:"Starts at this entity",editor_band_color:"Colour",editor_band_custom_color:"CSS colour",editor_band_label:"Label",editor_band_icon:"Icon",editor_color_icon:"Colour icons to match their band",editor_color_primary:"Primary",editor_color_green:"Green",editor_color_amber:"Amber",editor_color_red:"Red",editor_color_blue:"Blue",editor_color_purple:"Purple",editor_color_grey:"Grey",editor_color_custom:"Custom…",editor_add_band:"Add band",editor_remove_band:"Remove band",editor_add_entity:"Add entity",editor_move_up:"Move up",editor_move_down:"Move down",editor_remove_entity:"Remove entity",editor_show_options:"Show options",editor_hide_options:"Hide options",editor_not_found:"{entity} (not found)",editor_severity:"Colours"}};function te(t,e){Qt[t]={...Qt[t],...e}}function ee(t){return(t?.locale?.language??t?.language??"en").split("-")[0]}function ie(t,e,i){const n=ee(t),o=Qt[n]?.[e]??Qt.en[e]??e;return i?o.replace(/\{(\w+)\}/g,(t,e)=>e in i?String(i[e]):t):o}function ne(t){return null==t||""===t}function oe(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function re(t,e,i){const[n,...o]=e,r=function(t,e){return Array.isArray(t)?[...t]:oe(t)?{...t}:"number"==typeof e?[]:{}}(t,n),a=r,s=o.length?re(a[n],o,i):i,l=o.length>0&&oe(s)&&0===Object.keys(s).length;return ne(s)?Array.isArray(r)&&"number"==typeof n?r.splice(n,1):delete a[n]:l&&oe(r)?delete a[n]:a[n]=s,r}function ae(t,e,i){return 0===e.length||ne(i)&&void 0===function(t,e){let i=t;for(const n of e){if("object"!=typeof i||null===i)return;i=i[n]}return i}(t,e)?t:re(t,e,i)}function se(t,e){const i=t;return{width:i.line_width??e.line_width??2.5,dots:i.dots??!0===e.dots,onTop:!0===i.on_top}}function le(t,e){return[...t.filter(t=>!e(t)),...t.filter(e)]}function ce(t){return t.map(([t,e])=>`M${Math.round(100*t)/100},${Math.round(100*e)/100}h0`).join("")}function de(t){return Math.round(10*(t+3.5))/10}function he(t,e){if(void 0!==t&&("number"!=typeof t||!Number.isFinite(t)||t<.5||t>12))throw new Error(`${e} must be a number from 0.5 to 12`)}function ue(t,e){if(void 0!==t&&"boolean"!=typeof t)throw new Error(`${e} must be true or false`)}function pe(t){he(t.line_width,'"line_width"'),ue(t.dots,'"dots"');for(const e of t.entities??[])"object"==typeof e&&(he(e.line_width,`"line_width" of ${e.entity}`),ue(e.dots,`"dots" of ${e.entity}`),ue(e.on_top,`"on_top" of ${e.entity}`))}const me=[{name:"title",path:["title"]}],fe=[{name:"show_name",path:["show","name"],default:!0},{name:"show_value",path:["show","value"],default:!0},{name:"show_icon",path:["show","icon"],default:!0},{name:"show_target",path:["show","target"],default:!0},{name:"show_legend",path:["show","legend"],default:!1},{name:"animate",path:["animate"],default:!0},{name:"hide_unavailable",path:["hide_unavailable"],default:!1}],ge=[{name:"color_icon",path:["color_icon"],default:!1}],_e=[{name:"columns",path:["columns"]},{name:"sort",path:["sort"],default:"none"},{name:"limit",path:["limit"]},{name:"decimals",path:["decimals"]},{name:"unit",path:["unit"]}],ye=[{name:"tap_action",path:["tap_action"],default:{action:"more-info"}},{name:"hold_action",path:["hold_action"],default:{action:"none"}},{name:"double_tap_action",path:["double_tap_action"],default:{action:"none"}}],ve=[{name:"name",path:["name"]},{name:"unit",path:["unit"]},{name:"decimals",path:["decimals"]}],be=[{name:"attribute",path:["attribute"]}],$e=[{name:"series_color",path:["color"]},{name:"curve",path:["curve"]},{name:"line_width",path:["line_width"]},{name:"dash",path:["dash"],default:!1},{name:"on_top",path:["on_top"],default:!1}],xe={number:{min:.5,max:12,step:.5,mode:"box",unit_of_measurement:"px"}},we=new Map;function ke(t){let e=we.get(t);return e||(e=[...$e,{name:"dots",path:["dots"],default:t}],we.set(t,e)),e}const Ae=ye.map(({name:t,path:e})=>({name:t,path:e})),Me=[{name:"title",selector:{text:{}}}],Ce=ye.map(({name:t})=>({name:t,selector:{ui_action:{}}})),Se=[{type:"grid",name:"",schema:[{name:"name",selector:{text:{}}},{name:"unit",selector:{text:{}}},{name:"decimals",selector:{number:{min:0,max:20,mode:"box"}}}]}],Ee=new Map;let Oe;function Te(t){const e=ee(t);if(Oe?.language!==e){const i=e=>ie(t,e);Oe={language:e,schema:[{type:"grid",name:"",schema:[{name:"series_color",selector:{text:{}}},{name:"curve",selector:{select:{mode:"dropdown",options:["smooth","straight","step"].map(t=>({value:t,label:i(`editor_curve_${t}`)}))}}}]},{type:"grid",name:"",schema:[{name:"line_width",selector:xe},{name:"dash",selector:{boolean:{}}},{name:"dots",selector:{boolean:{}}},{name:"on_top",selector:{boolean:{}}}]}]}}return Oe.schema}function Le(t){return[{type:"grid",name:"",schema:[{name:"columns",selector:{number:{min:1,max:6,mode:"box"}}},{name:"sort",selector:{select:{mode:"dropdown",options:["none","ascending","descending"].map(e=>({value:e,label:ie(t,`editor_sort_${e}`)}))}}},{name:"limit",selector:{number:{min:1,mode:"box"}}},{name:"decimals",selector:{number:{min:0,max:20,mode:"box"}}},{name:"unit",selector:{text:{}}}]}]}const Ne={include:["device_class","area","floor","label","domain"],exclude:["entity","device_class","area","floor","label","domain"]};function Ue(t,e){const i=t=>({select:{multiple:!0,custom_value:!0,mode:"dropdown",options:t}});switch(t){case"device_class":return i(e.deviceClasses);case"domain":return i(e.domains);case"entity":return{entity:{multiple:!0}};default:return{[t]:{multiple:!0}}}}const Pe=new Map;function ze(t,e){const i=function(t){const e=new Set,i=new Set;for(const n of Object.values(t?.states??{})){const t=n.attributes?.device_class;"string"==typeof t&&t&&e.add(t),i.add(n.entity_id.slice(0,n.entity_id.indexOf(".")))}return{deviceClasses:[...e].sort(),domains:[...i].sort()}}(e),n=`${i.deviceClasses.join()}|${i.domains.join()}`,o=Pe.get(t);if(o?.key===n)return o.schema;const r=Ne[t].map(e=>({name:`${t}_${e}`,selector:Ue(e,i)}));return Pe.set(t,{key:n,schema:r}),r}let je;function He(t,e){let i=t;for(const n of e){if("object"!=typeof i||null===i)return;i=i[n]}return i}function Fe(t,e){if(t===e)return!0;if("object"!=typeof t||"object"!=typeof e||null===t||null===e)return!1;if(Array.isArray(t)!==Array.isArray(e))return!1;const i=Object.keys(t),n=Object.keys(e);return i.length===n.length&&i.every(i=>Fe(t[i],e[i]))}function De(t,e){const i={};for(const n of e){const e=He(t,n.path)??n.default;void 0!==e&&(i[n.name]=e)}return i}function Re(t,e,i){const n=De(t,e);let o=t;for(const r of e){const t=i[r.name];Fe(n[r.name],t)||(o=ae(o,r.path,void 0!==r.default&&Fe(t,r.default)?void 0:t))}return o}const Be="var(--divider-color, rgba(127, 127, 127, 0.3))",Ve="var(--primary-color, #03a9f4)",Ie="var(--secondary-text-color, #727272)";function We(t,e,i,n){const o=(n-90)*Math.PI/180;return[(t+i*Math.cos(o)).toFixed(2),(e+i*Math.sin(o)).toFixed(2)]}function qe(t,e,i,n){return We(t,e,i,n).join(" ")}function Ze(t,e,i,n,o){const r=o-n>180?1:0;return`M ${qe(t,e,i,n)} A ${i} ${i} 0 ${r} 1 ${qe(t,e,i,o)}`}function Ge(t){return V`
- ${a} - ${0!==o?V``:q} -
`}const En=r` + `:Z} + `}const Lo=r` .period-nav { display: flex; align-items: center; @@ -142,16 +142,16 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt border: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent); white-space: nowrap; } -`;function On(t){!function(t){mn.get(t)?.pause()}(t)}function Tn(t,e){const{show:i}=e.config;return i.name||i.value?V`
- ${t.series.map(t=>V`
+`;function No(t){!function(t){yo.get(t)?.pause()}(t)}function Uo(t,e){const{show:n}=e.config;return n.name||n.value?B`
+ ${t.series.map(t=>B`
- ${i.name?V`${t.entity.name}`:q} - ${i.value?V`${t.entity.display}`:q} + ${n.name?B`${t.entity.name}`:Z} + ${n.value?B`${t.entity.display}`:Z}
`)} -
`:q}function Ln(t,e){return"loading"===t.status?V`
${ie(e.hass,"chart_loading")}
`:"error"===t.status?V`
- ${ie(e.hass,"chart_error",{error:t.error??""})} -
`:t.series.every(t=>t.points.every(t=>null===t.v))?V`
${ie(e.hass,"chart_no_data")}
`:void 0}const Nn=r` - ${En} +
`:Z}function jo(t,e){return"loading"===t.status?B`
${ne(e.hass,"chart_loading")}
`:"error"===t.status?B`
+ ${ne(e.hass,"chart_error",{error:t.error??""})} +
`:t.series.every(t=>t.points.every(t=>null===t.v))?B`
${ne(e.hass,"chart_no_data")}
`:void 0}const zo=r` + ${Lo} .series-header { display: flex; flex-wrap: wrap; @@ -239,14 +239,14 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt background: color-mix(in srgb, var(--card-background-color, #fff) 80%, transparent); white-space: nowrap; } -`;function Un(t,e,i){const{locale:n,grouping:o}=function(t){const e=t?.locale?.language??"en";switch(t?.locale?.number_format){case"system":return{locale:void 0,grouping:!0};case"comma_decimal":return{locale:"en-US",grouping:!0};case"decimal_comma":return{locale:"de",grouping:!0};case"space_comma":return{locale:"fr",grouping:!0};case"none":return{locale:"en-US",grouping:!1};default:return{locale:e,grouping:!0}}}(i),r=new Intl.NumberFormat(n,{minimumFractionDigits:e,maximumFractionDigits:e,useGrouping:o});let a=r.format(0===t?0:t);return 0===Number(t.toFixed(e))&&(a=r.format(0)),a}function Pn(t,e){return e?`${t} ${e}`:t}const zn=new WeakMap;function jn(t){zn.get(t)?.observer?.disconnect(),zn.delete(t)}function Hn(t){const e=ri(t.raw);return Math.max(0,function(t){let e=zn.get(t.host);if(!e){const i={width:t.host.clientWidth||400};e=i,zn.set(t.host,i),"undefined"!=typeof ResizeObserver&&(i.observer=new ResizeObserver(e=>{const n=Math.round(e[e.length-1].contentRect.width);Math.round(n/70)!==Math.round(i.width/70)&&(i.width=n,t.requestUpdate())}),i.observer.observe(t.host))}return e.width}(t)-32-(e.y&&"outside"===e.labels?40:0))}function Fn(t,e,i){const n=ri(t.raw),o=li(i,n,4),r=Math.max(2,Math.min(8,Math.floor(Hn(t)/70))),{unit:a,ticks:s}=_i(e,r,mi(yi(t.hass)));return{options:n,value:o,times:s,unit:a,range:e,scales:{x:Xe([e.start,e.end],[0,bn]),y:Xe(o.domain,[$n,0]),domain:o.domain}}}const Dn=t=>Math.round(1e4*t)/100+"%";function Rn({axes:t,ctx:e,content:i,label:n,defs:o=q,overlay:r=q,hover:a}){const{options:s}=t,l="outside"===s.labels,c=s.y&&t.value.ticks.length>0,d=s.x&&t.times.length>0,h=c?function(t,e){const{scales:i,value:n}=t;return n.ticks.map(t=>{const o=i.y(t)/$n;return V`.92?"bottom":""}" style="top:${Dn(o)}" - >${Un(t,n.decimals,e.hass)}`})}(t,e):[],u=c?V`
${h}
`:q,p=d?V`
${function(t,e){const{scales:i,range:n}=t,o=mi(yi(e.hass));return t.times.map(r=>{const a=i.x(r)/bn;return V`.95?"end":""}" style="left:${Dn(a)}" - >${xi(r,t.unit,n.end-n.start,e.hass,o)}`})}(t,e)}
`:q,m=c?V``:q;return V`
- ${l&&c?V`
${m}${h}
`:q} +`;function Po(t,e,n){const{locale:o,grouping:i}=function(t){const e=t?.locale?.language??"en";switch(t?.locale?.number_format){case"system":return{locale:void 0,grouping:!0};case"comma_decimal":return{locale:"en-US",grouping:!0};case"decimal_comma":return{locale:"de",grouping:!0};case"space_comma":return{locale:"fr",grouping:!0};case"none":return{locale:"en-US",grouping:!1};default:return{locale:e,grouping:!0}}}(n),r=new Intl.NumberFormat(o,{minimumFractionDigits:e,maximumFractionDigits:e,useGrouping:i});let a=r.format(0===t?0:t);return 0===Number(t.toFixed(e))&&(a=r.format(0)),a}function Ho(t,e){return e?`${t} ${e}`:t}const Fo=new WeakMap;function Do(t){Fo.get(t)?.observer?.disconnect(),Fo.delete(t)}function Ro(t){const e=an(t.raw);return Math.max(0,function(t){let e=Fo.get(t.host);if(!e){const n={width:t.host.clientWidth||400};e=n,Fo.set(t.host,n),"undefined"!=typeof ResizeObserver&&(n.observer=new ResizeObserver(e=>{const o=Math.round(e[e.length-1].contentRect.width);Math.round(o/70)!==Math.round(n.width/70)&&(n.width=o,t.requestUpdate())}),n.observer.observe(t.host))}return e.width}(t)-32-(e.y&&"outside"===e.labels?40:0))}function Vo(t,e,n){const o=an(t.raw),i=cn(n,o,4),r=Math.max(2,Math.min(8,Math.floor(Ro(t)/70))),{unit:a,ticks:s}=_n(e,r,fn(vn(t.hass)));return{options:o,value:i,times:s,unit:a,range:e,scales:{x:Xe([e.start,e.end],[0,wo]),y:Xe(i.domain,[ko,0]),domain:i.domain}}}const Bo=t=>Math.round(1e4*t)/100+"%";function Io({axes:t,ctx:e,content:n,label:o,defs:i=Z,overlay:r=Z,hover:a}){const{options:s}=t,l="outside"===s.labels,c=s.y&&t.value.ticks.length>0,d=s.x&&t.times.length>0,h=c?function(t,e){const{scales:n,value:o}=t;return o.ticks.map(t=>{const i=n.y(t)/ko;return B`.92?"bottom":""}" style="top:${Bo(i)}" + >${Po(t,o.decimals,e.hass)}`})}(t,e):[],u=c?B`
${h}
`:Z,p=d?B`
${function(t,e){const{scales:n,range:o}=t,i=fn(vn(e.hass));return t.times.map(r=>{const a=n.x(r)/wo;return B`.95?"end":""}" style="left:${Bo(a)}" + >${t.label?t.label(r):Mn(r,t.unit,o.end-o.start,e.hass,i)}`})}(t,e)}
`:Z,m=c?B``:Z;return B`
+ ${l&&c?B`
${m}${h}
`:Z}
- ${function(t,e,i=q,n=q){return V` - + - ${i} + ${n} - ${n} + ${o} ${t} - `}(i,n,o,function(t){const{options:e,scales:i}=t;return I` - ${e.horizontal?t.value.ticks.map(t=>I``):q} - ${e.vertical?t.times.map(t=>I``):q} - `}(t))} ${l?q:V`${u}${p}`} ${r} + `}(n,o,i,function(t){const{options:e,scales:n}=t;return I` + ${e.horizontal?t.value.ticks.map(t=>I``):Z} + ${e.vertical?t.times.map(t=>I``):Z} + `}(t))} ${l?Z:B`${u}${p}`} ${r}
- ${l?p:q} -
`}const Bn=r` + ${l?p:Z} +
`}const Wo=r` .plot-wrap { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -369,9 +369,9 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt border-radius: 2px; background: color-mix(in srgb, var(--card-background-color, #fff) 70%, transparent); } -`,Vn=["above","over","hidden"];function In(t){if(void 0!==t.compact&&"boolean"!=typeof t.compact)throw new Error('"compact" must be true or false');if(void 0!==t.header&&!Vn.includes(t.header))throw new Error(`"header" must be one of: ${Vn.join(", ")}`)}function Wn(t){return{compact:!0===t.compact,header:t.header??"above"}}function qn(t,e,i){return V`
- ${"above"===t.header?e:q}${i} -
`}function Zn(t){return t===q?q:V`
t.stopPropagation()}>${t}
`}const Gn=r` +`,Zo=["above","over","hidden"];function qo(t){if(void 0!==t.compact&&"boolean"!=typeof t.compact)throw new Error('"compact" must be true or false');if(void 0!==t.header&&!Zo.includes(t.header))throw new Error(`"header" must be one of: ${Zo.join(", ")}`)}function Yo(t){return{compact:!0===t.compact,header:t.header??"above"}}function Go(t,e,n){return B`
+ ${"above"===t.header?e:Z}${n} +
`}function Xo(t){return t===Z?Z:B`
t.stopPropagation()}>${t}
`}const Jo=r` .header-over { position: absolute; top: 2px; @@ -424,22 +424,26 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt .frame.compact .axes-outside .x-axis .axis-label { top: 3px; } -`,Yn=["none","normal","percent"];function Xn(t,e){if(0===t.length)return-1;let i=0,n=t.length-1;for(;i>1;t[o].t0&&e-t[i-1].t<=t[i].t-e?i-1:i}const Jn=new WeakMap;function Kn(t){return Jn.get(t)?.t}function Qn(t){const e=Jn.get(t);void 0!==e?.timer&&clearTimeout(e.timer),Jn.delete(t)}function to(t){return!1!==t.tooltip}function eo(t){const e=t.tooltip;if(void 0!==e&&"boolean"!=typeof e)throw new Error('"tooltip" must be true or false')}function io(t,e,i){const n=function(t){let e=Jn.get(t);return e||(e={},Jn.set(t,e)),e}(t.host),o=e=>{e!==n.t&&(n.t=e,t.requestUpdate())},r=()=>{void 0!==n.timer&&clearTimeout(n.timer),n.timer=void 0};return{move(t){r();const n=t.currentTarget.getBoundingClientRect(),a=n.width>0?Math.min(1,Math.max(0,(t.clientX-n.left)/n.width)):0;o(function(t,e){let i;for(const n of t){const t=Xn(n,e);if(t<0)continue;const o=n[t].t;(void 0===i||Math.abs(o-e){n.timer=void 0,o(void 0)},3e3):o(void 0)}}}const no=t=>Math.round(1e4*t)/100+"%";function oo(t,e,i,n,o,r={}){if(void 0===n)return q;const a=i.x(n)/bn;if(!(a>=0&&a<=1))return q;const s=function(t,e){const i=t*e;return i+190<=e?"right":i>=190?"left":t>.5?"pin-left":"pin-right"}(a,o),l=e.map(t=>{const e=Xn(t.points,n);return{s:t,p:e<0?void 0:t.points[e]}});return V``}},Fr={layout:"tiles",group:"bars",height:230,tilesPerRow:4,minColumnWidth:80,styles:r` .style-tank .entity { display: flex; flex-direction: column; @@ -1147,20 +1239,20 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt text-overflow: ellipsis; white-space: nowrap; } - `,render:(t,{show:e,hass:i})=>V` - ${e.icon?V`B` + ${e.icon?B``:q} - ${e.value||!t.available?V`${t.display}`:q} + .icon=${Ri(t)} + >`:Z} + ${e.value||!t.available?B`${t.display}`:Z}
- ${e.target&&null!==t.targetPosition&&t.available?V`
`:q} + ${e.target&&null!==t.targetPosition&&t.available?B`
`:Z}
- ${e.name?V`${t.name}`:q} - `},hr={layout:"rows",group:"bars",height:44,minColumnWidth:140,styles:r` + ${e.name?B`${t.name}`:Z} + `},Dr={layout:"rows",group:"bars",height:44,minColumnWidth:140,styles:r` .style-thin .entity { display: flex; align-items: center; @@ -1226,37 +1318,37 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt background: var(--primary-text-color); transition: left var(--cd-duration) var(--cd-easing); } - `,render(t,{show:e,hass:i}){const n=e.value||!t.available;return V` - ${e.icon?V``:q} + .icon=${Ri(t)} + >`:Z}
- ${e.name||n?V`
- ${e.name?V`${t.name}`:q} - ${n?V`${t.display}`:q} -
`:q} + ${e.name||o?B`
+ ${e.name?B`${t.name}`:Z} + ${o?B`${t.display}`:Z} +
`:Z}
- ${e.target&&null!==t.targetPosition&&t.available?V`
`:q} + ${e.target&&null!==t.targetPosition&&t.available?B`
`:Z}
- `}},ur=["bars","gauges","history","totals","compare","distribution"];function pr(t){return"chart"===t.layout}const mr={bar:Fo,thin:hr,segmented:cr,tank:dr,ring:ar,needle:Qo,arc:sr,line:Bo,area:So},fr="important",gr=" !"+fr,_r=mt(class extends ft{constructor(t){if(super(t),t.type!==dt||"style"!==t.name||t.strings?.length>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce((e,i)=>{const n=t[i];return null==n?e:e+`${i=i.includes("-")?i:i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${n};`},"")}update(t,[e]){const{style:i}=t.element;if(void 0===this.ft)return this.ft=new Set(Object.keys(e)),this.render(e);for(const n of this.ft)null==e[n]&&(this.ft.delete(n),n.includes("-")?i.removeProperty(n):i[n]=null);for(const n in e){const t=e[n];if(null!=t){this.ft.add(n);const e="string"==typeof t&&t.endsWith(gr);n.includes("-")||e?i.setProperty(n,e?t.slice(0,-11):t,e?fr:""):i[n]=t}}return W}}),yr=Object.keys(Nt),vr=Object.keys(St),br="custom",$r={text:{}},xr={entity:{domain:["sensor","input_number","number"]}}; + `}},Rr=["bars","gauges","history","totals","compare","distribution"];function Vr(t){return"chart"===t.layout}const Br={bar:Yi,thin:Dr,segmented:Hr,tank:Fr,ring:jr,needle:Sr,arc:zr,line:yr,column:mr,area:Pi},Ir="important",Wr=" !"+Ir,Zr=mt(class extends ft{constructor(t){if(super(t),t.type!==dt||"style"!==t.name||t.strings?.length>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(t){return Object.keys(t).reduce((e,n)=>{const o=t[n];return null==o?e:e+`${n=n.includes("-")?n:n.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${o};`},"")}update(t,[e]){const{style:n}=t.element;if(void 0===this.ft)return this.ft=new Set(Object.keys(e)),this.render(e);for(const o of this.ft)null==e[o]&&(this.ft.delete(o),o.includes("-")?n.removeProperty(o):n[o]=null);for(const o in e){const t=e[o];if(null!=t){this.ft.add(o);const e="string"==typeof t&&t.endsWith(Wr);o.includes("-")||e?n.setProperty(o,e?t.slice(0,-11):t,e?Ir:""):n[o]=t}}return W}}),qr=Object.keys(Nt),Yr=Object.keys(St),Gr="custom",Xr={text:{}},Jr={entity:{domain:["sensor","input_number","number"]}}; /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function wr(t){return"string"==typeof t&&""!==t.trim()&&!Number.isFinite(Number(t))}const kr={icon:{}};function Ar(t){return void 0===t?"none":yr.find(e=>Nt[e]&&Fe(Nt[e],t))??"custom"}function Mr(t){return vr.includes(t)}class Cr extends lt{constructor(){super(),this._customOpen=new Set,this._entityOpen=new Set}willUpdate(t){t.has("bands")&&(this._customOpen.size&&(this._customOpen=new Set),this._entityOpen.size&&(this._entityOpen=new Set))}_entityValue(t){const e=this.hass?.states[t]?.state,i=void 0===e||""===e.trim()?NaN:Number(e);return Number.isFinite(i)?i:void 0}_t(t){return ie(this.hass,t)}_getSelectors(){const t=`${ee(this.hass)}|${this.unit??""}`;if(this._selectors?.key===t)return this._selectors;const e=yr.map(t=>({value:t,label:this._t(`editor_preset_${t}`)})),i=t=>({select:{mode:"dropdown",options:t}});return this._selectors={key:t,preset:i(e),presetCustom:i([...e,{value:br,label:this._t("editor_preset_custom")}]),color:i([...vr,br].map(t=>({value:t,label:this._t(`editor_color_${t}`)}))),from:{number:{mode:"box",step:"any",...this.unit?{unit_of_measurement:this.unit}:{}}}},this._selectors}_emit(t){this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t&&t.length?t:void 0}}))}_presetChanged(t){t.stopPropagation();const e=t.detail?.value;if(!e||e===br||!yr.includes(e))return;if(e===Ar(this.bands))return;const i=Nt[e];this._emit(i?structuredClone(i):void 0)}_updateBand(t,e,i){const n=[...this.bands??[]],o={...n[t]};null==i||""===i?delete o[e]:o[e]=i,n[t]=o,this._emit(n)}_fromChanged(t,e){e.stopPropagation();const i=Number(e.detail?.value);""!==e.detail?.value&&null!=e.detail?.value&&Number.isFinite(i)&&this._updateBand(t,"from",i)}_fromEntityChanged(t,e){e.stopPropagation();const i=e.detail?.value;"string"==typeof i&&""!==i&&this._updateBand(t,"from",i)}_toggleFromMode(t){const e=this.bands?.[t]?.from,i=new Set(this._entityOpen);wr(e)?this._updateBand(t,"from",this._entityValue(e)??0):i.delete(t)||i.add(t),this._entityOpen=i}_colorChanged(t,e){e.stopPropagation();const i=e.detail?.value,n=this.bands?.[t]?.color??"primary";if(!i)return;const o=new Set(this._customOpen);i!==br?(o.delete(t),this._customOpen=o,i!==n&&this._updateBand(t,"color",i)):Mr(n)&&(this._customOpen=o.add(t))}_customColorChanged(t,e){e.stopPropagation();const i="string"==typeof e.detail?.value?e.detail.value.trim():"";if(!i)return;const n=new Set(this._customOpen);n.delete(t),this._customOpen=n,this._updateBand(t,"color",i)}_iconChanged(t,e){e.stopPropagation(),this._updateBand(t,"icon",e.detail?.value)}_labelChanged(t,e){e.stopPropagation(),this._updateBand(t,"label",e.detail?.value)}_removeBand(t){const e=[...this.bands??[]];e.splice(t,1),this._emit(e)}_addBand(){const t=[...this.bands??[]],e=t[t.length-1]?.from,i="number"==typeof e?e:wr(e)?this._entityValue(e)??NaN:"string"==typeof e&&""!==e.trim()?Number(e):NaN;t.push({from:Number.isFinite(i)?i+10:0,color:"primary"}),this._emit(t)}_renderBand(t,e,i){const n=!Mr(t.color)||this._customOpen.has(e),o=wr(t.from)?t.from:void 0,r=void 0!==o||this._entityOpen.has(e);return V`
- + */function Kr(t){return"string"==typeof t&&""!==t.trim()&&!Number.isFinite(Number(t))}const Qr={icon:{}};function ta(t){return void 0===t?"none":qr.find(e=>Nt[e]&&Fe(Nt[e],t))??"custom"}function ea(t){return Yr.includes(t)}class na extends lt{constructor(){super(),this._customOpen=new Set,this._entityOpen=new Set}willUpdate(t){t.has("bands")&&(this._customOpen.size&&(this._customOpen=new Set),this._entityOpen.size&&(this._entityOpen=new Set))}_entityValue(t){const e=this.hass?.states[t]?.state,n=void 0===e||""===e.trim()?NaN:Number(e);return Number.isFinite(n)?n:void 0}_t(t){return ne(this.hass,t)}_getSelectors(){const t=`${ee(this.hass)}|${this.unit??""}`;if(this._selectors?.key===t)return this._selectors;const e=qr.map(t=>({value:t,label:this._t(`editor_preset_${t}`)})),n=t=>({select:{mode:"dropdown",options:t}});return this._selectors={key:t,preset:n(e),presetCustom:n([...e,{value:Gr,label:this._t("editor_preset_custom")}]),color:n([...Yr,Gr].map(t=>({value:t,label:this._t(`editor_color_${t}`)}))),from:{number:{mode:"box",step:"any",...this.unit?{unit_of_measurement:this.unit}:{}}}},this._selectors}_emit(t){this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t&&t.length?t:void 0}}))}_presetChanged(t){t.stopPropagation();const e=t.detail?.value;if(!e||e===Gr||!qr.includes(e))return;if(e===ta(this.bands))return;const n=Nt[e];this._emit(n?structuredClone(n):void 0)}_updateBand(t,e,n){const o=[...this.bands??[]],i={...o[t]};null==n||""===n?delete i[e]:i[e]=n,o[t]=i,this._emit(o)}_fromChanged(t,e){e.stopPropagation();const n=Number(e.detail?.value);""!==e.detail?.value&&null!=e.detail?.value&&Number.isFinite(n)&&this._updateBand(t,"from",n)}_fromEntityChanged(t,e){e.stopPropagation();const n=e.detail?.value;"string"==typeof n&&""!==n&&this._updateBand(t,"from",n)}_toggleFromMode(t){const e=this.bands?.[t]?.from,n=new Set(this._entityOpen);Kr(e)?this._updateBand(t,"from",this._entityValue(e)??0):n.delete(t)||n.add(t),this._entityOpen=n}_colorChanged(t,e){e.stopPropagation();const n=e.detail?.value,o=this.bands?.[t]?.color??"primary";if(!n)return;const i=new Set(this._customOpen);n!==Gr?(i.delete(t),this._customOpen=i,n!==o&&this._updateBand(t,"color",n)):ea(o)&&(this._customOpen=i.add(t))}_customColorChanged(t,e){e.stopPropagation();const n="string"==typeof e.detail?.value?e.detail.value.trim():"";if(!n)return;const o=new Set(this._customOpen);o.delete(t),this._customOpen=o,this._updateBand(t,"color",n)}_iconChanged(t,e){e.stopPropagation(),this._updateBand(t,"icon",e.detail?.value)}_labelChanged(t,e){e.stopPropagation(),this._updateBand(t,"label",e.detail?.value)}_removeBand(t){const e=[...this.bands??[]];e.splice(t,1),this._emit(e)}_addBand(){const t=[...this.bands??[]],e=t[t.length-1]?.from,n="number"==typeof e?e:Kr(e)?this._entityValue(e)??NaN:"string"==typeof e&&""!==e.trim()?Number(e):NaN;t.push({from:Number.isFinite(n)?n+10:0,color:"primary"}),this._emit(t)}_renderBand(t,e,n){const o=!ea(t.color)||this._customOpen.has(e),i=Kr(t.from)?t.from:void 0,r=void 0!==i||this._entityOpen.has(e);return B`
+ this._fromChanged(e,t)} > @@ -1265,8 +1357,8 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt data-role="color" .hass=${this.hass} .label=${this._t("editor_band_color")} - .selector=${i.color} - .value=${n?br:t.color} + .selector=${n.color} + .value=${o?Gr:t.color} @value-changed=${t=>this._colorChanged(e,t)} > this._labelChanged(e,t)} > @@ -1283,7 +1375,7 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt data-role="icon" .hass=${this.hass} .label=${this._t("editor_band_icon")} - .selector=${kr} + .selector=${Qr} .value=${t.icon??""} @value-changed=${t=>this._iconChanged(e,t)} > @@ -1300,37 +1392,37 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt .label=${this._t("editor_remove_band")} @click=${()=>this._removeBand(e)} > - ${r?V`this._fromEntityChanged(e,t)} - >`:q} - ${n?V``:Z} + ${o?B`this._customColorChanged(e,t)} - >`:q} -
`}render(){const t=this._getSelectors(),e=Ar(this.bands);return V``:Z} +
`}render(){const t=this._getSelectors(),e=ta(this.bands);return B` - ${(this.bands??[]).map((e,i)=>this._renderBand(e,i,t))} - `}}Cr.properties={hass:{attribute:!1},bands:{attribute:!1},unit:{attribute:!1},_customOpen:{state:!0},_entityOpen:{state:!0}},Cr.styles=r` + ${(this.bands??[]).map((e,n)=>this._renderBand(e,n,t))} + `}}na.properties={hass:{attribute:!1},bands:{attribute:!1},unit:{attribute:!1},_customOpen:{state:!0},_entityOpen:{state:!0}},na.styles=r` :host { display: block; } @@ -1397,7 +1489,7 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } - `,customElements.get("cd-bands-editor")||customElements.define("cd-bands-editor",Cr);const Sr={number:{mode:"box",step:"any"}},Er={entity:{domain:["sensor","input_number","number"]}};function Or(t){return null==t||""===t?"default":"number"==typeof t||""!==t.trim()&&Number.isFinite(Number(t))?"number":"entity"}class Tr extends lt{willUpdate(t){t.has("value")&&"default"!==Or(this.value)&&(this._mode=void 0)}_modeSelectorFor(){const t=(t,e)=>ie(this.hass,t,e),e=[{value:"default",label:t("editor_limit_default",{value:this.defaultLabel??""})},{value:"number",label:t("editor_limit_number")},{value:"entity",label:t("editor_limit_entity")}],i=e.map(t=>t.label).join("|");return this._modeSelector?.key!==i&&(this._modeSelector={key:i,selector:{select:{mode:"dropdown",options:e}}}),this._modeSelector.selector}get _currentMode(){return this._mode??Or(this.value)}_emit(t){this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t}}))}_modeChanged(t){t.stopPropagation();const e=t.detail?.value;e&&e!==this._currentMode&&(this._mode=e,void 0!==this.value&&this._emit(void 0))}_numberChanged(t){t.stopPropagation();const e=t.detail?.value,i="number"==typeof e?e:""===e||null==e?NaN:Number(e);this._mode="number",this._emit(Number.isFinite(i)?i:void 0)}_entityChanged(t){t.stopPropagation();const e=t.detail?.value;this._mode="entity",this._emit("string"==typeof e&&""!==e?e:void 0)}render(){const t=t=>ie(this.hass,t),e=this._currentMode;return V`ne(this.hass,t,e),e=[{value:"default",label:t("editor_limit_default",{value:this.defaultLabel??""})},{value:"number",label:t("editor_limit_number")},{value:"entity",label:t("editor_limit_entity")}],n=e.map(t=>t.label).join("|");return this._modeSelector?.key!==n&&(this._modeSelector={key:n,selector:{select:{mode:"dropdown",options:e}}}),this._modeSelector.selector}get _currentMode(){return this._mode??ra(this.value)}_emit(t){this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t}}))}_modeChanged(t){t.stopPropagation();const e=t.detail?.value;e&&e!==this._currentMode&&(this._mode=e,void 0!==this.value&&this._emit(void 0))}_numberChanged(t){t.stopPropagation();const e=t.detail?.value,n="number"==typeof e?e:""===e||null==e?NaN:Number(e);this._mode="number",this._emit(Number.isFinite(n)?n:void 0)}_entityChanged(t){t.stopPropagation();const e=t.detail?.value;this._mode="entity",this._emit("string"==typeof e&&""!==e?e:void 0)}render(){const t=t=>ne(this.hass,t),e=this._currentMode;return B` - ${"number"===e?V``:"entity"===e?V``:"entity"===e?B``:q}`}}Tr.properties={hass:{attribute:!1},label:{attribute:!1},value:{attribute:!1},defaultLabel:{attribute:!1},_mode:{state:!0}},Tr.styles=r` + >`:Z}`}}aa.properties={hass:{attribute:!1},label:{attribute:!1},value:{attribute:!1},defaultLabel:{attribute:!1},_mode:{state:!0}},aa.styles=r` :host { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; align-items: start; } - `,customElements.get("cd-limit-field")||customElements.define("cd-limit-field",Tr);const Lr=["min","max","target"];function Nr(t,e,i){const n=e&&!i?t?.states[e]?.attributes?.unit_of_measurement:void 0;return"string"==typeof n&&n?n:void 0}class Ur extends lt{constructor(){super(),this._computeLabel=t=>this._t(`editor_${t.name}`),this._expanded=new Set}_t(t,e){return ie(this.hass,t,e)}get _asConfig(){return{type:"",entities:this.entities??[]}}_emit(t){t.entities!==this.entities&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t.entities??[]}}))}_set(t,e,i){this._emit(function(t,e,i,n){if("entity"===i&&(ne(n)||"string"!=typeof n))return t;const o=[...t.entities??[]],r=o[e];if(void 0===r)return t;const a="string"==typeof r?{entity:r}:{...r};ne(n)?delete a[i]:a[i]=n;const s=Object.keys(a);return o[e]=1===s.length&&"entity"===s[0]&&"string"==typeof a.entity?a.entity:a,{...t,entities:o}}(this._asConfig,t,e,i))}_move(t,e){const i=new Set;for(const n of this._expanded)i.add(n===t?e:n===e?t:n);this._expanded=i,this._emit(function(t,e,i){const n=[...t.entities??[]];if(e<0||e>=n.length||e===i)return t;const[o]=n.splice(e,1);return n.splice(Math.max(0,Math.min(i,n.length)),0,o),{...t,entities:n}}(this._asConfig,t,e))}_remove(t){const e=new Set;for(const i of this._expanded)i!==t&&e.add(i>t?i-1:i);this._expanded=e,this._emit(function(t,e){const i=[...t.entities??[]];return e<0||e>=i.length?t:(i.splice(e,1),{...t,entities:i})}(this._asConfig,t))}_toggle(t){const e=new Set(this._expanded);e.delete(t)||e.add(t),this._expanded=e}_add(t){t.stopPropagation();const e=t.detail?.value;"string"==typeof e&&""!==e&&(t.target.value="",this._emit(function(t,e){return{...t,entities:[...t.entities??[],e]}}(this._asConfig,e)))}_formChanged(t,e,i){i.stopPropagation();const n=this.entities?.[t];if(void 0===n)return;const o="string"==typeof n?{entity:n}:n,r=Re(o,e,i.detail?.value??{});if(r===o)return;const a=[...this.entities??[]],s=Object.keys(r);a[t]=1===s.length&&"entity"===s[0]?r.entity:r,this._emit({type:"",entities:a})}_rowLabel(t){const e=this.hass?.states[t];return e?{text:e.attributes.friendly_name||t,missing:!1}:this.hass?{text:this._t("editor_not_found",{entity:t}),missing:!0}:{text:t,missing:!1}}_form(t,e,i,n,o){return V`this._t(`editor_${t.name}`),this._expanded=new Set}_t(t,e){return ne(this.hass,t,e)}get _asConfig(){return{type:"",entities:this.entities??[]}}_emit(t){t.entities!==this.entities&&this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:t.entities??[]}}))}_set(t,e,n){this._emit(function(t,e,n,o){if("entity"===n&&(oe(o)||"string"!=typeof o))return t;const i=[...t.entities??[]],r=i[e];if(void 0===r)return t;const a="string"==typeof r?{entity:r}:{...r};oe(o)?delete a[n]:a[n]=o;const s=Object.keys(a);return i[e]=1===s.length&&"entity"===s[0]&&"string"==typeof a.entity?a.entity:a,{...t,entities:i}}(this._asConfig,t,e,n))}_move(t,e){const n=new Set;for(const o of this._expanded)n.add(o===t?e:o===e?t:o);this._expanded=n,this._emit(function(t,e,n){const o=[...t.entities??[]];if(e<0||e>=o.length||e===n)return t;const[i]=o.splice(e,1);return o.splice(Math.max(0,Math.min(n,o.length)),0,i),{...t,entities:o}}(this._asConfig,t,e))}_remove(t){const e=new Set;for(const n of this._expanded)n!==t&&e.add(n>t?n-1:n);this._expanded=e,this._emit(function(t,e){const n=[...t.entities??[]];return e<0||e>=n.length?t:(n.splice(e,1),{...t,entities:n})}(this._asConfig,t))}_toggle(t){const e=new Set(this._expanded);e.delete(t)||e.add(t),this._expanded=e}_add(t){t.stopPropagation();const e=t.detail?.value;"string"==typeof e&&""!==e&&(t.target.value="",this._emit(function(t,e){return{...t,entities:[...t.entities??[],e]}}(this._asConfig,e)))}_formChanged(t,e,n){n.stopPropagation();const o=this.entities?.[t];if(void 0===o)return;const i="string"==typeof o?{entity:o}:o,r=Re(i,e,n.detail?.value??{});if(r===i)return;const a=[...this.entities??[]],s=Object.keys(r);a[t]=1===s.length&&"entity"===s[0]?r.entity:r,this._emit({type:"",entities:a})}_rowLabel(t){const e=this.hass?.states[t];return e?{text:e.attributes.friendly_name||t,missing:!1}:this.hass?{text:this._t("editor_not_found",{entity:t}),missing:!0}:{text:t,missing:!1}}_form(t,e,n,o,i){return B`this._formChanged(e,o,t)} - >`}_renderOverrides(t,e){const i=t=>e=>{e.stopPropagation(),t(e.detail?.value)};return V`
+ @value-changed=${t=>this._formChanged(e,i,t)} + >`}_renderOverrides(t,e){const n=t=>e=>{e.stopPropagation(),t(e.detail?.value)};return B`
${this._form("entity-attribute",e,t,function(t){let e=Ee.get(t);return e||(e=[{name:"attribute",selector:{attribute:{entity_id:t}}}],Ee.set(t,e)),e}(t.entity),be)} ${this._form("entity-text",e,t,Se,ve)} - ${this.chartFields?this._form("entity-chart",e,t,this.chartFields.schema,this.chartFields.fields):q} + ${this.chartFields?this._form("entity-chart",e,t,this.chartFields.schema,this.chartFields.fields):Z} this._set(e,"icon",t))} + @value-changed=${n(t=>this._set(e,"icon",t))} > - ${Lr.map(n=>V`B`this._set(e,n,t))} + @value-changed=${n(t=>this._set(e,o,t))} >`)} this._set(e,"severity",t))} + .unit=${t.unit??this.unit??la(this.hass,t.entity,t.attribute)} + @value-changed=${n(t=>this._set(e,"severity",t))} > - ${this._form("entity-actions",e,t,Ce,Ae)} -
`}_renderRow(t,e,i){const n=function(t){return"string"==typeof t?t:t.entity}(t),o="string"==typeof t?{entity:t}:t,r=this._rowLabel(n),a=this._expanded.has(e);return V`
+ ${this._form("entity-actions",e,t,Ce,Me)} +
`}_renderRow(t,e,n){const o=function(t){return"string"==typeof t?t:t.entity}(t),i="string"==typeof t?{entity:t}:t,r=this._rowLabel(o),a=this._expanded.has(e);return B`
${r.text}
{t.stopPropagation(),this._set(e,"entity",t.detail?.value),this.requestUpdate()}} > @@ -1481,7 +1573,7 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt class="down" .path=${"M11,4H13V16L18.5,10.5L19.92,11.92L12,19.84L4.08,11.92L5.5,10.5L11,16V4Z"} .label=${this._t("editor_move_down")} - .disabled=${e===i-1} + .disabled=${e===n-1} @click=${()=>this._move(e,e+1)} > this._toggle(e)} >
- ${a?this._renderOverrides(o,e):q} -
`}render(){const t=this.entities??[];return V`${t.map((e,i)=>this._renderRow(e,i,t.length))} + ${a?this._renderOverrides(i,e):Z} +
`}render(){const t=this.entities??[];return B`${t.map((e,n)=>this._renderRow(e,n,t.length))} `}}Ur.properties={hass:{attribute:!1},entities:{attribute:!1},unit:{attribute:!1},chartFields:{attribute:!1},_expanded:{state:!0}},Ur.styles=r` + >`}}ca.properties={hass:{attribute:!1},entities:{attribute:!1},unit:{attribute:!1},chartFields:{attribute:!1},_expanded:{state:!0}},ca.styles=r` :host { display: block; } @@ -1559,23 +1651,23 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt display: block; margin-top: 8px; } - `,customElements.get("cd-entities-editor")||customElements.define("cd-entities-editor",Ur);class Pr extends lt{constructor(){super(...arguments),this._label=t=>ie(this.hass,t),this._computeLabel=t=>ie(this.hass,`editor_${t.name}`),this._computeRuleLabel=t=>ie(this.hass,`editor_rule_${t.name.slice(t.name.indexOf("_")+1)}`)}setConfig(t){this._config=t}connectedCallback(){super.connectedCallback(),async function(){if(!customElements.get("ha-entity-picker")&&"function"==typeof window.loadCardHelpers)try{const t=await window.loadCardHelpers(),e=await t.createCardElement({type:"entities",entities:[]}),i=e?.constructor;await(i?.getConfigElement?.())}catch{}}()}_commit(t){this._config=t,function(t,e){t.dispatchEvent(new CustomEvent("config-changed",{bubbles:!0,composed:!0,detail:{config:e}}))}(this,t)}_formChanged(t,e){if(e.stopPropagation(),!this._config)return;const i=e.detail?.value??{};this._commit(Re(this._config,t,i))}_form(t,e,i){return V`ne(this.hass,t),this._computeLabel=t=>ne(this.hass,`editor_${t.name}`),this._computeRuleLabel=t=>ne(this.hass,`editor_rule_${t.name.slice(t.name.indexOf("_")+1)}`)}setConfig(t){this._config=t}connectedCallback(){super.connectedCallback(),async function(){if(!customElements.get("ha-entity-picker")&&"function"==typeof window.loadCardHelpers)try{const t=await window.loadCardHelpers(),e=await t.createCardElement({type:"entities",entities:[]}),n=e?.constructor;await(n?.getConfigElement?.())}catch{}}()}_commit(t){this._config=t,function(t,e){t.dispatchEvent(new CustomEvent("config-changed",{bubbles:!0,composed:!0,detail:{config:e}}))}(this,t)}_formChanged(t,e){if(e.stopPropagation(),!this._config)return;const n=e.detail?.value??{};this._commit(Re(this._config,t,n))}_form(t,e,n){return B`this._formChanged(i,t)} - >`}_panel(t,e){return V` + @value-changed=${t=>this._formChanged(n,t)} + >`}_panel(t,e){return B`
${e}
-
`}_subChanged(t,e){e.stopPropagation(),this._config&&this._commit(ae(this._config,t,e.detail?.value))}_chartFields(){const t=mr[this._config?.style??"bar"];return t&&pr(t)&&this._config?t.entityFields?.(this.hass,this._config):void 0}renderEntities(){return V``}_subChanged(t,e){e.stopPropagation(),this._config&&this._commit(ae(this._config,t,e.detail?.value))}_chartFields(){const t=Br[this._config?.style??"bar"];return t&&Vr(t)&&this._config?t.entityFields?.(this.hass,this._config):void 0}renderEntities(){return B`this._subChanged(["entities"],t)} > - ${this._ruleForm("include")} ${this._ruleForm("exclude")}`}_ruleData(t){const e=this._config?.[t],i={};if(!e||"object"!=typeof e)return i;for(const[n,o]of Object.entries(e))i[`${t}_${n}`]=Array.isArray(o)?o:[o];return i}_ruleChanged(t,e){if(e.stopPropagation(),!this._config)return;const i=e.detail?.value??{},n=this._ruleData(t);let o=this._config;for(const r of Ne[t]){const e=`${t}_${r}`,a=Array.isArray(i[e])?i[e]:[];Fe(n[e]??[],a)||(o=ae(o,[t,r],a.length?a:void 0))}o!==this._config&&this._commit(o)}_ruleForm(t){return V`
${this._label(`editor_${t}_heading`)}
+ ${this._ruleForm("include")} ${this._ruleForm("exclude")}`}_ruleData(t){const e=this._config?.[t],n={};if(!e||"object"!=typeof e)return n;for(const[o,i]of Object.entries(e))n[`${t}_${o}`]=Array.isArray(i)?i:[i];return n}_ruleChanged(t,e){if(e.stopPropagation(),!this._config)return;const n=e.detail?.value??{},o=this._ruleData(t);let i=this._config;for(const r of Ne[t]){const e=`${t}_${r}`,a=Array.isArray(n[e])?n[e]:[];Fe(o[e]??[],a)||(i=ae(i,[t,r],a.length?a:void 0))}i!==this._config&&this._commit(i)}_ruleForm(t){return B`
${this._label(`editor_${t}_heading`)}

${this._label(`editor_${t}_help`)}

this._ruleChanged(t,e)} - >`}renderLimits(){return[["min","0"],["max","100"],["target",this._label("editor_none")]].map(([t,e])=>V`
`}renderLimits(){return[["min","0"],["max","100"],["target",this._label("editor_none")]].map(([t,e])=>B`this._subChanged([t],e)} - >`)}renderColours(){return V`${this._toggles(ge)}`)}renderColours(){return B`${this._toggles(ge)}this._subChanged(["severity"],t)} - >`}_firstEntityUnit(){const t=this._config?.entities?.[0];return"string"==typeof t?Nr(this.hass,t):t?.unit??Nr(this.hass,t?.entity,t?.attribute)}_selectStyle(t){this._config&&this._commit(ae(this._config,["style"],"bar"===t?void 0:t))}renderStyle(){const t=this._config?.style??"bar";return V`${ur.map(e=>{const i=Bt.filter(t=>mr[t].group===e);return i.length?V`
${this._label(`editor_group_${e}`)}
+ >
`}_firstEntityUnit(){const t=this._config?.entities?.[0];return"string"==typeof t?la(this.hass,t):t?.unit??la(this.hass,t?.entity,t?.attribute)}_selectStyle(t){this._config&&this._commit(ae(this._config,["style"],"bar"===t?void 0:t))}renderStyle(){const t=this._config?.style??"bar";return B`${Rr.map(e=>{const n=Vt.filter(t=>Br[t].group===e);return n.length?B`
${this._label(`editor_group_${e}`)}
- ${i.map(e=>{const i=mr[e],n=pr(i)?V``:Ge(e);return V``})} -
`:q})}`}renderChart(){const t=mr[this._config?.style??"bar"];return t&&pr(t)&&t.editor&&this._config?this._panel("chart",t.editor({hass:this.hass,config:this._config,commit:t=>this._commit(t)})):q}renderDisplay(){return V`${this._toggles(fe)} - ${this._form("display",function(t){const e=ee(t);return je?.language!==e&&(je={language:e,schema:Le(t)}),je.schema}(this.hass),_e)}`}_toggles(t){const e=De(this._config,t);return V`
- ${t.map(({name:i})=>{const n=this._label(`editor_${i}`),o=!0===e[i];return V`
- this._toggle(t,i,!o)}>${n}`:Z})}`}renderChart(){const t=Br[this._config?.style??"bar"];return t&&Vr(t)&&t.editor&&this._config?this._panel("chart",t.editor({hass:this.hass,config:this._config,commit:t=>this._commit(t)})):Z}renderDisplay(){return B`${this._toggles(fe)} + ${this._form("display",function(t){const e=ee(t);return Pe?.language!==e&&(Pe={language:e,schema:Le(t)}),Pe.schema}(this.hass),ye)}`}_toggles(t){const e=De(this._config,t);return B`
+ ${t.map(({name:n})=>{const o=this._label(`editor_${n}`),i=!0===e[n];return B`
+ this._toggle(t,n,!i)}>${o}this._toggle(t,i,e.target.checked)} + data-name=${n} + aria-label=${o} + .checked=${Kt(i)} + @change=${e=>this._toggle(t,n,e.target.checked)} >
`})} -
`}_toggle(t,e,i){if(!this._config)return;const n=De(this._config,t);this._commit(Re(this._config,t,{...n,[e]:i}))}renderActions(){return this._form("actions",Ce,ye)}render(){return this._config?V`
${this._form("title",Me,me)}
+
`}_toggle(t,e,n){if(!this._config)return;const o=De(this._config,t);this._commit(Re(this._config,t,{...o,[e]:n}))}renderActions(){return this._form("actions",Ce,_e)}render(){return this._config?B`
${this._form("title",Ae,me)}
${this._panel("entities",this.renderEntities())} ${this._panel("style",this.renderStyle())} ${this.renderChart()} ${this._panel("limits",this.renderLimits())} ${this._panel("colours",this.renderColours())} ${this._panel("display",this.renderDisplay())} - ${this._panel("actions",this.renderActions())}`:q}}Pr.properties={hass:{attribute:!1},_config:{state:!0}},Pr.styles=r` + ${this._panel("actions",this.renderActions())}`:Z}}da.properties={hass:{attribute:!1},_config:{state:!0}},da.styles=r` :host { display: block; } @@ -1704,7 +1796,7 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt outline-offset: -1px; border-color: transparent; } - `,customElements.get("chartdeck-card-editor")||customElements.define("chartdeck-card-editor",Pr);const zr=r` + `,customElements.get("chartdeck-card-editor")||customElements.define("chartdeck-card-editor",da);const ha=r` :host { display: block; --cd-duration: 900ms; @@ -1807,35 +1899,35 @@ const Kt=mt(class extends ft{constructor(t){if(super(t),t.type!==ut&&t.type!==dt border-radius: 50%; flex: none; } -`;console.info("%c CHARTDECK %c 1.2.0 ","color:#fff;background:#3b8bff;font-weight:700","color:#3b8bff");const jr={rows:16,tiles:8};function Hr(t){const e=jr[t.layout];return Math.max(1,Math.floor((368+e)/((t.minColumnWidth??0)+e)))}class Fr extends lt{constructor(){super(...arguments),this._watched=new Set,this._matched=[],this._items=[],this._detachers=new Map,this._readyScheduled=!1,this._readyAt=-1/0}static getStubConfig(t){const e=Object.values(t?.states??{}).filter(t=>t.entity_id.startsWith("sensor.")&&""!==t.state.trim()&&Number.isFinite(Number(t.state))),i=t=>"battery"===t.attributes?.device_class;return e.sort((t,e)=>i(t)!==i(e)?i(t)?-1:1:t.entity_ide.entity_id?1:0),{type:"custom:chartdeck-card",style:"bar",entities:e.slice(0,3).map(t=>t.entity_id)}}static getConfigElement(){return document.createElement("chartdeck-card-editor")}setConfig(t){const e=function(t){if(!t||"object"!=typeof t)throw new Error("Invalid configuration");const e=t.style??"bar";if(!Bt.includes(e))throw new Error(`Unknown style "${String(e)}". Use one of: ${Bt.join(", ")}`);const i=t.sort??"none";if(!It.includes(i))throw new Error(`Unknown sort "${String(i)}". Use one of: ${It.join(", ")}`);if(void 0!==t.entities&&!Array.isArray(t.entities))throw new Error('"entities" must be a list');const n=(t.entities??[]).map((e,i)=>Jt(e,t,i)),o={name:!0,value:!0,icon:!0,target:!0,legend:!1,...t.show},r=Zt(t.columns,"columns",1),a=Zt(t.limit,"limit",1),s=Pt(t.include,"include"),l=Pt(t.exclude,"exclude");s&&Jt("sensor.chartdeck_check",t,0);const c={style:e,entities:n,hideUnavailable:!0===t.hide_unavailable,cardOptions:t,show:o,sort:i,animate:!1!==t.animate,colorIcon:!0===t.color_icon,legendBands:Gt(t.severity)??Vt};return void 0!==t.title&&(c.title=t.title),void 0!==r&&(c.columns=r),void 0!==a&&(c.limit=a),s&&(c.include=s),l&&(c.exclude=l),c}(t),i=mr[e.style];if(pr(i)&&i.validate?.(t),this._watched=function(t){const e=new Set;for(const i of t.entities){e.add(i.entity);for(const t of[i.min,i.max,i.target,...i.severity.map(t=>t.from)])"string"==typeof t&&""!==t&&e.add(t)}for(const i of t.legendBands)"string"==typeof i.from&&e.add(i.from);return e}(e),pr(i))for(const n of i.watch?.(t)??[])this._watched.add(n);this._config&&this._config.style!==e.style&&this._disconnectStyle(),this._config=e}get _styleName(){return this._config?.style??"bar"}get _style(){return mr[this._styleName]}_legend(t){const e=this._config;if(!e?.show.legend||0===this._count())return[];const i=t?.decimals??1;return function(t,e,i){const n=i?` ${i}`:"",o=[];return t.forEach((i,r)=>{const a=t[r+1],s=Et(i.color);i.label?o.push({color:s,text:i.label}):Number.isFinite(i.from)?a?o.push({color:s,text:`${e(i.from)}–${e(a.from)}${n}`}):o.push({color:s,text:`${e(i.from)}${n}+`}):a&&o.push({color:s,text:`< ${e(a.from)}${n}`})}),o}(zo(e.legendBands,this.hass),t=>Un(t,i,this.hass),t?.unit??"")}_count(){return this.hasUpdated&&this.hass?this._items.length:this._config?.entities.length??0}_contentHeight(){const t=this._config;if(!t)return 0;const e=this._style,i=16+(t.title?73:0)+(this._legend().length>0?32:0);if(pr(e))return i+e.height;const n=this._count(),o=void 0!==t.columns?Math.min(t.columns,Hr(e)):"rows"===e.layout?1:Math.min(n,e.tilesPerRow??3,Hr(e));return i+(0===n?0:Math.ceil(n/Math.max(1,o)))*e.height}getCardSize(){return Math.max(1,Math.ceil(this._contentHeight()/50))}getGridOptions(){const t=this._count();if(pr(this._style))return{columns:12,rows:"auto",min_columns:6,min_rows:2};const e="rows"===this._style.layout;return{columns:e||t<=1?6:12,rows:"auto",min_columns:e?6:3,min_rows:1}}shouldUpdate(t){if(0===t.size)return!0;if([...t.keys()].some(t=>"hass"!==t))return!0;if(!t.has("hass"))return!1;const e=t.get("hass"),i=this.hass;if(!e||!i)return!0;for(const n of this._watched)if(e.states[n]!==i.states[n])return!0;if(this._config?.include){const t=Ho(this._config,i);if(t.length!==this._matched.length||t.some((t,e)=>t!==this._matched[e]))return!0;for(const n of t)if(e.states[n]!==i.states[n])return!0}return e.themes?.darkMode!==i.themes?.darkMode||e.themes?.theme!==i.themes?.theme||e.locale!==i.locale}willUpdate(){const t=!1===this._config?.animate||"function"==typeof window.matchMedia&&!!window.matchMedia("(prefers-reduced-motion: reduce)")?.matches;this.toggleAttribute("static",t),(t||this.preview)&&this.setAttribute("ready","")}_scheduleReady(){if(this._readyScheduled||this.hasAttribute("ready")||0===this._items.length)return;this._readyScheduled=!0;const t=()=>{this.shadowRoot?.querySelector(".entity")?.getBoundingClientRect(),this.setAttribute("ready",""),this._readyAt=performance.now()};"function"==typeof requestAnimationFrame?requestAnimationFrame(t):setTimeout(t,16)}updated(){this._scheduleReady(),this._syncActions()}connectedCallback(){if(super.connectedCallback(),!this.hasUpdated)return;const t=this._config?mr[this._config.style]:void 0;t&&pr(t)&&this.requestUpdate(),this._syncActions(),this.hasAttribute("ready")&&!this.hasAttribute("static")&&performance.now()-this._readyAt<1e3&&(this.removeAttribute("ready"),this._readyScheduled=!1,this._scheduleReady())}_disconnectStyle(){const t=this._config?mr[this._config.style]:void 0;t&&pr(t)&&t.disconnect?.(this)}disconnectedCallback(){super.disconnectedCallback(),this._disconnectStyle();for(const t of this._detachers.values())t();this._detachers.clear()}_syncActions(){const t=this.shadowRoot;if(!t)return;const e=new Set(t.querySelectorAll(".entity"));for(const[i,n]of this._detachers)e.has(i)||(n(),this._detachers.delete(i));for(const i of e)this._detachers.has(i)||this._detachers.set(i,Ct(i,()=>this._items[Number(i.dataset.index)].config))}_renderEntity(t,e,i,n){const o=this._config,r=(a=t.config,Mt(a.tap_action)||Mt(a.hold_action)||Mt(a.double_tap_action));var a;const s=function(t){const e=Et(t);return{start:e,end:`color-mix(in srgb, ${e} 80%, #fff)`}}(t.band.color),l=`--start:${t.span[0]};--end:${t.span[1]};--pos:${t.position};--target:${t.targetPosition??0};--c-start:${s.start};--c-end:${s.end}`;return V`
t.entity_id.startsWith("sensor.")&&""!==t.state.trim()&&Number.isFinite(Number(t.state))),n=t=>"battery"===t.attributes?.device_class;return e.sort((t,e)=>n(t)!==n(e)?n(t)?-1:1:t.entity_ide.entity_id?1:0),{type:"custom:chartdeck-card",style:"bar",entities:e.slice(0,3).map(t=>t.entity_id)}}static getConfigElement(){return document.createElement("chartdeck-card-editor")}setConfig(t){const e=function(t){if(!t||"object"!=typeof t)throw new Error("Invalid configuration");const e=t.style??"bar";if(!Vt.includes(e))throw new Error(`Unknown style "${String(e)}". Use one of: ${Vt.join(", ")}`);const n=t.sort??"none";if(!It.includes(n))throw new Error(`Unknown sort "${String(n)}". Use one of: ${It.join(", ")}`);if(void 0!==t.entities&&!Array.isArray(t.entities))throw new Error('"entities" must be a list');const o=(t.entities??[]).map((e,n)=>Jt(e,t,n)),i={name:!0,value:!0,icon:!0,target:!0,legend:!1,...t.show},r=qt(t.columns,"columns",1),a=qt(t.limit,"limit",1),s=jt(t.include,"include"),l=jt(t.exclude,"exclude");s&&Jt("sensor.chartdeck_check",t,0);const c={style:e,entities:o,hideUnavailable:!0===t.hide_unavailable,cardOptions:t,show:i,sort:n,animate:!1!==t.animate,colorIcon:!0===t.color_icon,legendBands:Yt(t.severity)??Bt};return void 0!==t.title&&(c.title=t.title),void 0!==r&&(c.columns=r),void 0!==a&&(c.limit=a),s&&(c.include=s),l&&(c.exclude=l),c}(t),n=Br[e.style];if(Vr(n)&&n.validate?.(t),this._watched=function(t){const e=new Set;for(const n of t.entities){e.add(n.entity);for(const t of[n.min,n.max,n.target,...n.severity.map(t=>t.from)])"string"==typeof t&&""!==t&&e.add(t)}for(const n of t.legendBands)"string"==typeof n.from&&e.add(n.from);return e}(e),Vr(n))for(const o of n.watch?.(t)??[])this._watched.add(o);this._config&&this._config.style!==e.style&&this._disconnectStyle(),this._config=e}get _styleName(){return this._config?.style??"bar"}get _style(){return Br[this._styleName]}_legend(t){const e=this._config;if(!e?.show.legend||0===this._count())return[];const n=t?.decimals??1;return function(t,e,n){const o=n?` ${n}`:"",i=[];return t.forEach((n,r)=>{const a=t[r+1],s=Et(n.color);n.label?i.push({color:s,text:n.label}):Number.isFinite(n.from)?a?i.push({color:s,text:`${e(n.from)}–${e(a.from)}${o}`}):i.push({color:s,text:`${e(n.from)}${o}+`}):a&&i.push({color:s,text:`< ${e(a.from)}${o}`})}),i}(Wi(e.legendBands,this.hass),t=>Po(t,n,this.hass),t?.unit??"")}_count(){return this.hasUpdated&&this.hass?this._items.length:this._config?.entities.length??0}_contentHeight(){const t=this._config;if(!t)return 0;const e=this._style,n=16+(t.title?73:0)+(this._legend().length>0?32:0);if(Vr(e))return n+e.height;const o=this._count(),i=void 0!==t.columns?Math.min(t.columns,pa(e)):"rows"===e.layout?1:Math.min(o,e.tilesPerRow??3,pa(e));return n+(0===o?0:Math.ceil(o/Math.max(1,i)))*e.height}getCardSize(){return Math.max(1,Math.ceil(this._contentHeight()/50))}getGridOptions(){const t=this._count();if(Vr(this._style))return{columns:12,rows:"auto",min_columns:6,min_rows:2};const e="rows"===this._style.layout;return{columns:e||t<=1?6:12,rows:"auto",min_columns:e?6:3,min_rows:1}}shouldUpdate(t){if(0===t.size)return!0;if([...t.keys()].some(t=>"hass"!==t))return!0;if(!t.has("hass"))return!1;const e=t.get("hass"),n=this.hass;if(!e||!n)return!0;for(const o of this._watched)if(e.states[o]!==n.states[o])return!0;if(this._config?.include){const t=qi(this._config,n);if(t.length!==this._matched.length||t.some((t,e)=>t!==this._matched[e]))return!0;for(const o of t)if(e.states[o]!==n.states[o])return!0}return e.themes?.darkMode!==n.themes?.darkMode||e.themes?.theme!==n.themes?.theme||e.locale!==n.locale}willUpdate(){const t=!1===this._config?.animate||"function"==typeof window.matchMedia&&!!window.matchMedia("(prefers-reduced-motion: reduce)")?.matches;this.toggleAttribute("static",t),(t||this.preview)&&this.setAttribute("ready","")}_scheduleReady(){if(this._readyScheduled||this.hasAttribute("ready")||0===this._items.length)return;this._readyScheduled=!0;const t=()=>{this.shadowRoot?.querySelector(".entity")?.getBoundingClientRect(),this.setAttribute("ready",""),this._readyAt=performance.now()};"function"==typeof requestAnimationFrame?requestAnimationFrame(t):setTimeout(t,16)}updated(){this._scheduleReady(),this._syncActions()}connectedCallback(){if(super.connectedCallback(),!this.hasUpdated)return;const t=this._config?Br[this._config.style]:void 0;t&&Vr(t)&&this.requestUpdate(),this._syncActions(),this.hasAttribute("ready")&&!this.hasAttribute("static")&&performance.now()-this._readyAt<1e3&&(this.removeAttribute("ready"),this._readyScheduled=!1,this._scheduleReady())}_disconnectStyle(){const t=this._config?Br[this._config.style]:void 0;t&&Vr(t)&&t.disconnect?.(this)}disconnectedCallback(){super.disconnectedCallback(),this._disconnectStyle();for(const t of this._detachers.values())t();this._detachers.clear()}_syncActions(){const t=this.shadowRoot;if(!t)return;const e=new Set(t.querySelectorAll(".entity"));for(const[n,o]of this._detachers)e.has(n)||(o(),this._detachers.delete(n));for(const n of e)this._detachers.has(n)||this._detachers.set(n,Ct(n,()=>this._items[Number(n.dataset.index)].config))}_renderEntity(t,e,n,o){const i=this._config,r=(a=t.config,At(a.tap_action)||At(a.hold_action)||At(a.double_tap_action));var a;const s=function(t){const e=Et(t);return{start:e,end:`color-mix(in srgb, ${e} 80%, #fff)`}}(t.band.color),l=`--start:${t.span[0]};--end:${t.span[1]};--pos:${t.position};--target:${t.targetPosition??0};--c-start:${s.start};--c-end:${s.end}`;return B`
- ${i.render(t,{show:o.show,hass:n})} -
`}render(){const t=this._config;if(!t)return q;const e=this.hass,i=this._style;let n,o,r;const a=e?function(t,e){const i=Ho(t,e).map((e,i)=>Jt(e,t.cardOptions,i)),n=[...t.entities,...i].map(t=>jo(t,e));return t.hideUnavailable?n.filter(t=>t.available):n}(t,e):[];if(this._matched=e?Ho(t,e):[],0!==t.entities.length||t.include)if(e)if(0===a.length)this._items=[],n=V`
${ie(e,t.include?"no_matches":"none_available")}
`;else{o=a[0];const s=new Map,l=new Set;a.forEach((t,e)=>{s.set(t,l.has(t.entityId)?`${t.entityId}#${e}`:t.entityId),l.add(t.entityId)}),this._items=function(t,e){if("none"===e)return[...t];const i="ascending"===e?1:-1;return t.map((t,e)=>({item:t,index:e})).sort((t,e)=>{if(t.item.available!==e.item.available)return t.item.available?-1:1;if(t.item.available){const n=(t.item.position-e.item.position)*i;if(0!==n)return n}return t.index-e.index}).map(t=>t.item)}(a,t.sort).slice(0,t.limit);const c=this._items.filter(t=>t.available).map(t=>t.display.length);if(r=c.length?Math.max(...c):void 0,pr(i))n=V`
- ${i.render({items:this._items,config:t,raw:t.cardOptions,hass:e,requestUpdate:()=>this.requestUpdate(),host:this})} -
`;else{const o="tiles"===i.layout,r=function(t,e){if(void 0===e)return"rows"===t.layout?"grid-template-columns: repeat(1, minmax(0, 1fr))":"";const i=(e-1)*jr[t.layout];return`grid-template-columns: repeat(auto-fill, minmax(max(min(${t.minColumnWidth??0}px, 100%), calc((100% - ${i}px) / ${e} - 0.5px)), 1fr))`}(i,t.columns);n=V`
`}render(){const t=this._config;if(!t)return Z;const e=this.hass,n=this._style;let o,i,r;const a=e?function(t,e){const n=qi(t,e).map((e,n)=>Jt(e,t.cardOptions,n)),o=[...t.entities,...n].map(t=>Zi(t,e));return t.hideUnavailable?o.filter(t=>t.available):o}(t,e):[];if(this._matched=e?qi(t,e):[],0!==t.entities.length||t.include)if(e)if(0===a.length)this._items=[],o=B`
${ne(e,t.include?"no_matches":"none_available")}
`;else{i=a[0];const s=new Map,l=new Set;a.forEach((t,e)=>{s.set(t,l.has(t.entityId)?`${t.entityId}#${e}`:t.entityId),l.add(t.entityId)}),this._items=function(t,e){if("none"===e)return[...t];const n="ascending"===e?1:-1;return t.map((t,e)=>({item:t,index:e})).sort((t,e)=>{if(t.item.available!==e.item.available)return t.item.available?-1:1;if(t.item.available){const o=(t.item.position-e.item.position)*n;if(0!==o)return o}return t.index-e.index}).map(t=>t.item)}(a,t.sort).slice(0,t.limit);const c=this._items.filter(t=>t.available).map(t=>t.display.length);if(r=c.length?Math.max(...c):void 0,Vr(n))o=B`
+ ${n.render({items:this._items,config:t,raw:t.cardOptions,hass:e,requestUpdate:()=>this.requestUpdate(),host:this})} +
`;else{const i="tiles"===n.layout,r=function(t,e){if(void 0===e)return"rows"===t.layout?"grid-template-columns: repeat(1, minmax(0, 1fr))":"";const n=(e-1)*ua[t.layout];return`grid-template-columns: repeat(auto-fill, minmax(max(min(${t.minColumnWidth??0}px, 100%), calc((100% - ${n}px) / ${e} - 0.5px)), 1fr))`}(n,t.columns);o=B`
- ${At(this._items,t=>s.get(t),(t,n)=>this._renderEntity(t,n,i,e))} -
`}}else this._items=[],n=q;else this._items=[],n=V`
${ie(e,"no_entities")}
`;const s=this._legend(o);return V` + ${Mt(this._items,t=>s.get(t),(t,o)=>this._renderEntity(t,o,n,e))} +
`}}else this._items=[],o=Z;else this._items=[],o=B`
${ne(e,"no_entities")}
`;const s=this._legend(i);return B`
- ${n} + ${o}
- ${s.length?V`
- ${s.map(t=>V` + ${s.map(t=>B`${t.text}`)} -
`:q} -
`}}Fr.properties={hass:{attribute:!1},preview:{type:Boolean},_config:{state:!0}},Fr.styles=[zr,...Object.values(mr).map(t=>t.styles)],customElements.get("chartdeck-card")||customElements.define("chartdeck-card",Fr);const Dr=window.customCards??=[];Dr.some(t=>"chartdeck-card"===t.type)||Dr.push({type:"chartdeck-card",name:"Chartdeck",description:"Bars, gauges and charts, set up entirely from the visual editor.",preview:!0,documentationURL:"https://tempus2016.github.io/chartdeck/"});export{Fr as ChartdeckCard}; +
`:Z} + `}}ma.properties={hass:{attribute:!1},preview:{type:Boolean},_config:{state:!0}},ma.styles=[ha,...Object.values(Br).map(t=>t.styles)],customElements.get("chartdeck-card")||customElements.define("chartdeck-card",ma);const fa=window.customCards??=[];fa.some(t=>"chartdeck-card"===t.type)||fa.push({type:"chartdeck-card",name:"Chartdeck",description:"Bars, gauges and charts, set up entirely from the visual editor.",preview:!0,documentationURL:"https://tempus2016.github.io/chartdeck/"});export{ma as ChartdeckCard}; diff --git a/src/chart/columns.ts b/src/chart/columns.ts new file mode 100644 index 0000000..afa0aba --- /dev/null +++ b/src/chart/columns.ts @@ -0,0 +1,152 @@ +import type { ResolvedEntity } from '../resolve'; +import { parsePeriod } from '../history/period'; +import type { Period } from '../history/period'; +import type { GroupUnit } from '../history/group'; +import type { Point } from '../history/types'; +import type { CardConfig } from '../types'; +import { valueAt } from './stack'; + +/** + * Column maths: which grouping a column chart uses when none is set, how readings line up into + * slots (one per group, or per reading), and where each series' column sits in its slot. + */ + +/** Grouping choices a column chart offers: `auto` picks one for the span, `none` draws each reading. */ +export const COLUMN_GROUPS = ['auto', 'none', 'hour', 'day', 'week', 'month'] as const; +export const COLUMN_AGGREGATES = ['auto', 'mean', 'sum', 'min', 'max', 'first', 'last', 'change'] as const; + +/** A column chart with no period shows the last 7 days: with automatic grouping, a column a day. */ +export const COLUMN_PERIOD = { last: 7, unit: 'day' } as const; + +const PERIOD_DAYS = { hour: 1 / 24, day: 1, week: 7, month: 30 } as const; + +/** + * The group a span reads best in: hours for up to two days, days up to about two months, weeks + * up to half a year, then months. Calendar spans: today by hour, a week or month by day, a year + * by month. + */ +export function autoGroupUnit(period: Period): GroupUnit { + if (period.calendar) return ({ day: 'hour', week: 'day', month: 'day', year: 'month' } as const)[period.calendar]; + const days = period.last * PERIOD_DAYS[period.unit]; + if (days <= 2) return 'hour'; + if (days <= 62) return 'day'; + if (days <= 190) return 'week'; + return 'month'; +} + +/** A meter that counts up (energy, water, gas): its columns should show how much it went up. */ +export function isMeter(e: ResolvedEntity): boolean { + if (e.config.attribute) return false; + const stateClass = e.stateObj?.attributes?.state_class; + return stateClass === 'total_increasing' || stateClass === 'total'; +} + +/** + * The card config the shared history frame should read: the column chart's own defaults filled + * in. No period means the last 7 days; `group_by` auto (or unset) picks a unit for the span, and + * `none` draws one column per reading; `aggregate` auto (or unset) is the change of meters when + * every entity is one (energy per day), else the average. Range columns always use the average. + */ +export function columnRaw(raw: CardConfig, items: readonly ResolvedEntity[]): CardConfig { + const out: Record = { ...raw, period: raw.period ?? { ...COLUMN_PERIOD } }; + if (raw.group_by === 'none') { + delete out.group_by; + } else if (raw.group_by === undefined || raw.group_by === 'auto') { + out.group_by = autoGroupUnit(parsePeriod(out.period as CardConfig['period'])); + } + if (raw.aggregate === undefined || raw.aggregate === 'auto') { + out.aggregate = raw.range !== true && items.length > 0 && items.every(isMeter) ? 'change' : 'mean'; + } + return out as CardConfig; +} + +/** Every series' readings at one shared set of slots. */ +export interface ColumnTable { + /** Slot starts, in order. */ + times: number[]; + /** Where each slot ends (the next slot's start, a group's end, or the range's end). */ + ends: number[]; + /** Per series, its value in each slot: its reading then, or the one it held. */ + values: (number | null)[][]; + /** Per series, each slot's lowest and highest (statistics, or a grouped range); null if unknown. */ + lows: (number | null)[][]; + highs: (number | null)[][]; +} + +/** + * Line series up into slots. Grouped points already share their times; readings that don't are + * read at every time any series has one (a value holds until it changes). A slot ends at its + * group's end, else at the next slot, else at `end`; one with no width (the reading carried to + * the right edge) is dropped. + */ +export function columnTable(series: readonly (readonly Point[])[], end: number): ColumnTable { + const all = [...new Set(series.flatMap((points) => points.map((p) => p.t)))].sort((a, b) => a - b); + const exact = series.map((points) => new Map(points.map((p) => [p.t, p]))); + const groupEnd = new Map(); + for (const points of series) for (const p of points) if (p.end !== undefined) groupEnd.set(p.t, p.end); + const table: ColumnTable = { + times: [], + ends: [], + values: series.map(() => []), + lows: series.map(() => []), + highs: series.map(() => []), + }; + all.forEach((t, i) => { + const slotEnd = Math.min(groupEnd.get(t) ?? all[i + 1] ?? end, end); + if (!(slotEnd > t)) return; + table.times.push(t); + table.ends.push(slotEnd); + series.forEach((points, k) => { + const p = exact[k].get(t); + table.values[k].push(p ? p.v : valueAt(points, t)); + table.lows[k].push(p?.min ?? null); + table.highs[k].push(p?.max ?? null); + }); + }); + return table; +} + +/** Share of a slot left empty, split either side, so neighbouring slots don't touch. */ +export const SLOT_GAP = 0.2; +/** Share of a series' column width left between columns side by side in one slot. */ +export const SERIES_GAP = 0.12; + +/** + * Where column `k` of `n` side by side sits in a slot from `x0` to `x1` (plot units): its left + * edge and width. One column (or a stack) takes the slot's middle 80 %. + */ +export function columnBox(x0: number, x1: number, k: number, n: number): [left: number, width: number] { + const w = x1 - x0; + const inner = w * (1 - SLOT_GAP); + const each = inner / n; + const pad = n > 1 ? each * SERIES_GAP : 0; + return [x0 + (w * SLOT_GAP) / 2 + k * each + pad / 2, each - pad]; +} + +/** + * Value-axis ends to fix so columns stand on zero: readings all at or above zero start the axis + * at zero, all at or below end it there. Ends the user set always win. + */ +export function zeroBased(data: [number, number], raw: CardConfig): CardConfig { + const out: Record = { ...raw }; + if (raw.y_min === undefined && data[0] >= 0) out.y_min = 0; + if (raw.y_max === undefined && data[1] <= 0) out.y_max = 0; + return out as CardConfig; +} + +/** Lowest and highest of some numbers (nulls skipped), with zero in unless `zero` is false. */ +export function spread(values: Iterable, zero = true): [number, number] | undefined { + let lo = zero ? 0 : Infinity; + let hi = zero ? 0 : -Infinity; + for (const v of values) { + if (v === null || !Number.isFinite(v)) continue; + lo = Math.min(lo, v); + hi = Math.max(hi, v); + } + return lo === Infinity ? undefined : [lo, hi]; +} + +/** The time labels to keep so about `fit` of `count` show: every first, every second, … */ +export function labelEvery(count: number, fit: number): number { + return Math.max(1, Math.ceil(count / Math.max(1, fit))); +} diff --git a/src/chart/frame.ts b/src/chart/frame.ts index 7700f43..ebca92f 100644 --- a/src/chart/frame.ts +++ b/src/chart/frame.ts @@ -90,8 +90,14 @@ export function validateHistoryOptions(raw: CardConfig): void { } } +/** What a chart asks of its history beyond the readings. */ +export interface FrameOptions { + /** Keep each group's lowest and highest (`min`, `max` on its point), for range charts. */ + range?: boolean; +} + /** Load (or reuse) the card's history and pair it with its entities. */ -export function historyFrame(ctx: ChartContext, now = Date.now()): HistoryFrame { +export function historyFrame(ctx: ChartContext, now = Date.now(), options: FrameOptions = {}): HistoryFrame { const period = parsePeriod(ctx.raw.period as CardConfig['period']); const source: Source = (ctx.raw.source as Source | undefined) ?? 'history'; const zone = zoneFor(ctx.hass); @@ -102,7 +108,9 @@ export function historyFrame(ctx: ChartContext, now = Date.now()): HistoryFrame const requests = ctx.items.map((e) => ({ entity: e.entityId, attribute: e.config.attribute })); const store = storeFor(ctx.host, ctx.requestUpdate); const statistics = - group && source === 'statistics' ? statisticsQueryFor(group, zone, ctx.hass.config?.time_zone) : undefined; + group && source === 'statistics' + ? statisticsQueryFor(group, zone, ctx.hass.config?.time_zone, options.range === true) + : undefined; // Statistics groups load from one row early: the row before a group gives its `first`. const load = statistics ? { start: range.start - PERIOD_MS[statistics.period], end: range.end } : range; const loaded = store.get(ctx.hass, requests, load, source, { live: span.live, statistics }); @@ -117,8 +125,8 @@ export function historyFrame(ctx: ChartContext, now = Date.now()): HistoryFrame if (group) { points = source === 'statistics' - ? groupStatistics(points, bounds, group.aggregate, until) - : groupHistory(points, bounds, group.aggregate, until); + ? groupStatistics(points, bounds, group.aggregate, until, options.range === true) + : groupHistory(points, bounds, group.aggregate, until, options.range === true); } else if (source === 'history' && last && last.v !== null && last.t < until) { // A sensor holds its last reading until it changes, so carry it on to the right edge. points = [...points, { t: until, v: last.v }]; diff --git a/src/chart/grid.ts b/src/chart/grid.ts index 6f84e93..a4de65d 100644 --- a/src/chart/grid.ts +++ b/src/chart/grid.ts @@ -72,6 +72,8 @@ export interface PlotAxes { unit: TimeUnit; range: Range; scales: PlotScales; + /** The text under each time label, when a chart names its own (a column's day); else the time's. */ + label?: (t: number) => string; } /** Work out the axes for readings spanning `data` over `range`, sized to the card. */ @@ -126,7 +128,7 @@ function timeLabels(axes: PlotAxes, ctx: ChartContext) { // Labels near an edge line up with it instead of centring off the chart. const edge = left < 0.05 ? 'start' : left > 0.95 ? 'end' : ''; return html`${timeLabel(t, axes.unit, range.end - range.start, ctx.hass, offset)}${axes.label ? axes.label(t) : timeLabel(t, axes.unit, range.end - range.start, ctx.hass, offset)}`; }); } diff --git a/src/history/group.ts b/src/history/group.ts index b131f5e..5539bd1 100644 --- a/src/history/group.ts +++ b/src/history/group.ts @@ -78,7 +78,13 @@ export function increments(points: Point[]): (number | null)[] { * * A group with no value held at any time in it is a gap. For sum, a group with no readings is. */ -export function groupHistory(points: Point[], bounds: number[], aggregate: Aggregate, until: number): Point[] { +export function groupHistory( + points: Point[], + bounds: number[], + aggregate: Aggregate, + until: number, + range = false, +): Point[] { const ups = aggregate === 'change' ? increments(points) : []; const out: Point[] = []; // Index of the first point at or after the group's start. @@ -112,6 +118,11 @@ export function groupHistory(points: Point[], bounds: number[], aggregate: Aggre }[]; if (known.length === 0) continue; const values = known.map((h) => h.v); + if (range) { + // The group's spread, for range charts: every value that held in it. + group.min = Math.min(...values); + group.max = Math.max(...values); + } switch (aggregate) { case 'mean': { const time = known.reduce((a, h) => a + h.dt, 0); @@ -153,7 +164,13 @@ export function groupHistory(points: Point[], bounds: number[], aggregate: Aggre * reading at the group's start), or the group's first row when there is none. A group that starts * after `until` (now) is a gap whatever the server sent. */ -export function groupStatistics(rows: Point[], bounds: number[], aggregate: Aggregate, until = Infinity): Point[] { +export function groupStatistics( + rows: Point[], + bounds: number[], + aggregate: Aggregate, + until = Infinity, + range = false, +): Point[] { const out: Point[] = []; let j = 0; for (let i = 0; i + 1 < bounds.length; i++) { @@ -162,8 +179,16 @@ export function groupStatistics(rows: Point[], bounds: number[], aggregate: Aggr while (j < rows.length && rows[j].t < start) j++; let k = j; const values: number[] = []; + const lows: number[] = []; + const highs: number[] = []; // A row moved to the range's start is the previous period's: only `first` wants it. - for (; k < rows.length && rows[k].t < end; k++) if (rows[k].v !== null && !rows[k].carried) values.push(rows[k].v!); + for (; k < rows.length && rows[k].t < end; k++) { + const row = rows[k]; + if (row.v === null || row.carried) continue; + values.push(row.v); + lows.push(row.min ?? row.v); + highs.push(row.max ?? row.v); + } let v: number | null = null; if (values.length && start < until) { if (aggregate === 'mean') v = values.reduce((a, x) => a + x, 0) / values.length; @@ -180,7 +205,13 @@ export function groupStatistics(rows: Point[], bounds: number[], aggregate: Aggr v = (prior ?? carried)?.v ?? values[0]; } } - out.push({ t: start, end, v }); + const group: Point = { t: start, end, v }; + // The group's spread, for range charts: lowest of the rows' lows to highest of their highs. + if (range && v !== null) { + group.min = Math.min(...lows); + group.max = Math.max(...highs); + } + out.push(group); } return out; } @@ -216,8 +247,12 @@ export function statisticsQueryFor( grouping: Grouping, zone: Zone, serverTimeZone: string | undefined, + range = false, ): StatisticsQuery { const sameCalendar = zone.timeZone === serverTimeZone && (grouping.unit !== 'week' || zone.firstDay === 1); const period: StatisticsPeriod = grouping.unit === 'hour' || !sameCalendar ? 'hour' : grouping.unit; - return { period, ...COLUMNS[grouping.aggregate] }; + const { types, value } = COLUMNS[grouping.aggregate]; + // A range chart also wants each row's lowest and highest. + const wanted: StatisticField[] = range ? [...new Set([...types, 'min', 'max'])] : types; + return { period, types: wanted, value }; } diff --git a/src/styles/column.ts b/src/styles/column.ts new file mode 100644 index 0000000..06fdb42 --- /dev/null +++ b/src/styles/column.ts @@ -0,0 +1,564 @@ +import { css, html, nothing, svg } from 'lit'; +import type { TemplateResult } from 'lit'; +import { baseColor, bandFor } from '../bands'; +import { formatDateTime, formatTime, timeLabel, timeZoneOf, validateAxisOptions } from '../chart/axes'; +import type { TimeUnit } from '../chart/axes'; +import { + COLUMN_AGGREGATES, + COLUMN_GROUPS, + COLUMN_PERIOD, + columnBox, + columnRaw, + columnTable, + labelEvery, + spread, + zeroBased, +} from '../chart/columns'; +import type { ColumnTable } from '../chart/columns'; +import { + VIEW_H, + VIEW_W, + disconnectHistory, + frameMessage, + frameStyles, + historyFrame, + periodNav, + seriesHeader, + validateHistoryOptions, +} from '../chart/frame'; +import type { ChartSeries, HistoryFrame } from '../chart/frame'; +import { chartArea, gridStyles, plotAxes, plotWidth, stopChartWidth } from '../chart/grid'; +import type { PlotAxes, PlotScales } from '../chart/grid'; +import { chartFrame, chartLayout, headerOver, layoutStyles, validateLayout } from '../chart/layout'; +import { STACK_MODES, stackExtent, stackValues, totals, validateStack } from '../chart/stack'; +import type { Layer, StackMode } from '../chart/stack'; +import { + hoverHandlers, + hoverTime, + sharedUnit, + stopHover, + tooltipEnabled, + tooltipLayer, + tooltipStyles, + totalRow, + validateTooltipOptions, +} from '../chart/tooltip'; +import type { TooltipOptions } from '../chart/tooltip'; +import { + AXES_FIELDS, + HISTORY_FIELDS, + LAYOUT_FIELDS, + axesSchema, + cachedSchema, + chartForm, + choice, + historySchema, + historyVariant, + layoutSchema, +} from '../editor/chart-editors'; +import type { FieldBinding } from '../editor/schemas'; +import { formatNumber, withUnit } from '../format'; +import type { GroupUnit } from '../history/group'; +import type { Point } from '../history/types'; +import { localize, registerStrings } from '../localize'; +import type { CardConfig, HomeAssistant } from '../types'; +import type { ChartContext, ChartEditorContext, ChartStyleModule } from './index'; + +/** Above this many columns, none get value labels: they would overlap. */ +const MAX_LABELS = 60; +/** A column narrower than this (px) has no room for its value label. */ +const LABEL_PX = 24; +/** Room each group label needs under the chart, in px: clock times are the widest. */ +const GROUP_LABEL_PX: Record = { hour: 66, day: 52, week: 52, month: 60 }; +/** Corner radius of a column's outer end, in px, before narrow columns scale it down. */ +const RADIUS_PX = 4; + +registerStrings('en', { + editor_style_column: 'Column', + editor_group_by_auto: 'Automatic', + editor_aggregate_auto: 'Automatic (change for meters, else average)', + editor_range: 'Lowest to highest in each column', + editor_rounded: 'Rounded tops', + editor_column_labels: 'Show values on the columns', + editor_column_color: 'Colour', +}); + +const isColumnGroup = (v: unknown) => COLUMN_GROUPS.includes(v as (typeof COLUMN_GROUPS)[number]); + +/** Check the column chart's options, with its own grouping choices taken out first. */ +function validate(raw: CardConfig): void { + if (raw.group_by !== undefined && !isColumnGroup(raw.group_by)) { + throw new Error(`"group_by" must be one of: ${COLUMN_GROUPS.join(', ')}`); + } + if (raw.aggregate !== undefined && !COLUMN_AGGREGATES.includes(raw.aggregate as (typeof COLUMN_AGGREGATES)[number])) { + throw new Error(`"aggregate" must be one of: ${COLUMN_AGGREGATES.join(', ')}`); + } + const shared: Record = { ...raw }; + if (raw.group_by === 'auto' || raw.group_by === 'none') delete shared.group_by; + if (raw.aggregate === 'auto') delete shared.aggregate; + validateHistoryOptions(shared as CardConfig); + validateAxisOptions(raw); + validateTooltipOptions(raw); + validateLayout(raw); + validateStack(raw); + for (const k of ['range', 'rounded', 'labels']) { + if (raw[k] !== undefined && typeof raw[k] !== 'boolean') throw new Error(`"${k}" must be true or false`); + } + if (raw.color_by !== undefined && raw.color_by !== 'series' && raw.color_by !== 'bands') { + throw new Error('"color_by" must be one of: series, bands'); + } +} + +/** The time unit whose labels name a group: a week is named by its first day. */ +const LABEL_UNIT: Record = { hour: 'hour', day: 'day', week: 'day', month: 'month' }; + +/** A group's full name for the tooltip: "Tue 29 Oct, 14:00 – 15:00", "Tue 29 Oct", "26 Oct – 1 Nov", "October 2026". */ +export function groupHeading(start: number, end: number, unit: GroupUnit, hass: HomeAssistant | undefined): string { + if (unit === 'hour') return `${formatDateTime(start, hass)} – ${formatTime(end, hass)}`; + const options: Intl.DateTimeFormatOptions = + unit === 'day' + ? { weekday: 'short', day: 'numeric', month: 'short' } + : unit === 'week' + ? { day: 'numeric', month: 'short' } + : { month: 'long', year: 'numeric' }; + let format: Intl.DateTimeFormat; + try { + format = new Intl.DateTimeFormat(hass?.locale?.language ?? 'en', { ...options, timeZone: timeZoneOf(hass) }); + } catch { + format = new Intl.DateTimeFormat('en', options); + } + return unit === 'week' ? format.formatRange(start, end - 1) : format.format(start); +} + +const pct = (n: number, of: number) => `${Math.round((n / of) * 10000) / 100}%`; +const clampY = (y: number) => Math.min(VIEW_H, Math.max(0, y)); + +interface Drawn { + columns: TemplateResult[]; + labels: TemplateResult[]; + targets: TemplateResult[]; +} + +/** A column from `lo` to `hi` (values), `left`/`width` in plot units, with its outer end(s) rounded. */ +function column( + scales: PlotScales, + left: number, + width: number, + lo: number, + hi: number, + color: string, + cap: 'top' | 'bottom' | 'both' | '', + series: number, +) { + const top = clampY(scales.y(hi)); + const bottom = clampY(scales.y(lo)); + return html`
`; +} + +function valueLabel(ctx: ChartContext, x: number, y: number, v: number, decimals: number, below: boolean) { + return html`${formatNumber(v, decimals, ctx.hass)}`; +} + +function targetMark(scales: PlotScales, left: number, width: number, target: number) { + const y = scales.y(target); + if (y < 0 || y > VIEW_H) return html``; + return html`
`; +} + +/** Everything drawn in the plot: columns, their labels and target marks. */ +function drawColumns( + ctx: ChartContext, + frame: HistoryFrame, + table: ColumnTable, + scales: PlotScales, + mode: StackMode, + layers: Layer[][] | undefined, +): Drawn { + const raw = ctx.raw; + const series = frame.series; + const n = series.length; + const range = raw.range === true; + const byBands = raw.color_by === 'bands'; + const px = plotWidth(ctx) / VIEW_W; + const showLabels = raw.labels === true && mode !== 'percent' && table.times.length * (layers ? 1 : n) <= MAX_LABELS; + const showTargets = ctx.config.show.target && mode !== 'percent'; + const baseline = Math.min(Math.max(0, scales.domain[0]), scales.domain[1]); + const colorOf = (s: ChartSeries, v: number) => (byBands ? baseColor(bandFor(v, s.entity.bands).color) : s.color); + const out: Drawn = { columns: [], labels: [], targets: [] }; + const sums = layers ? totals(table.values) : []; + table.times.forEach((t, i) => { + const x0 = scales.x(t); + const x1 = scales.x(table.ends[i]); + if (layers) { + const [left, width] = columnBox(x0, x1, 0, 1); + // The outermost slice each way gets the rounded end. + let up = -1; + let down = -1; + layers.forEach((l, k) => { + const layer = l[i]; + if (layer && layer[1] > layer[0]) up = k; + if (layer && layer[1] < layer[0]) down = k; + }); + layers.forEach((l, k) => { + const layer = l[i]; + if (!layer || layer[0] === layer[1]) return; + const v = table.values[k][i] as number; + const cap = k === up ? 'top' : k === down ? 'bottom' : ''; + out.columns.push( + column(scales, left, width, Math.min(...layer), Math.max(...layer), colorOf(series[k], v), cap, k), + ); + }); + const sum = sums[i]; + if (showLabels && sum !== null && width * px >= LABEL_PX) { + const end = + sum >= 0 + ? Math.max(0, ...layers.map((l) => l[i]?.[1] ?? 0)) + : Math.min(0, ...layers.map((l) => l[i]?.[1] ?? 0)); + const decimals = Math.max(...series.map((s) => s.entity.decimals)); + out.labels.push(valueLabel(ctx, left + width / 2, scales.y(end), sum, decimals, sum < 0)); + } + const target = series[0].entity.target; + if (showTargets && target !== null) out.targets.push(targetMark(scales, left, width, target)); + return; + } + series.forEach((s, k) => { + const v = table.values[k][i]; + if (v === null) return; + const [left, width] = columnBox(x0, x1, k, n); + if (range) { + const lo = table.lows[k][i] ?? v; + const hi = table.highs[k][i] ?? v; + out.columns.push(column(scales, left, width, lo, hi, colorOf(s, v), 'both', k)); + out.columns.push( + html`
`, + ); + } else { + out.columns.push( + column( + scales, + left, + width, + Math.min(baseline, v), + Math.max(baseline, v), + colorOf(s, v), + v >= baseline ? 'top' : 'bottom', + k, + ), + ); + if (showLabels && width * px >= LABEL_PX) { + out.labels.push(valueLabel(ctx, left + width / 2, scales.y(v), v, s.entity.decimals, v < baseline)); + } + } + if (showTargets && s.entity.target !== null) out.targets.push(targetMark(scales, left, width, s.entity.target)); + }); + }); + return out; +} + +function render(ctx0: ChartContext): TemplateResult { + // The column chart's defaults (7 days, grouped to fit) filled in, for the shared frame to read. + const ctx = { ...ctx0, raw: columnRaw(ctx0.raw, ctx0.items) }; + const raw = ctx.raw; + const range = raw.range === true; + const frame = historyFrame(ctx, Date.now(), { range }); + const nav = periodNav(frame, ctx); + const names = seriesHeader(frame, ctx); + const header = nav === nothing && names === nothing ? nothing : html`${nav}${names}`; + const message = frameMessage(frame, ctx); + const layout = chartLayout(raw); + if (message) return chartFrame(layout.header === 'over' ? { ...layout, header: 'above' } : layout, header, message); + const mode: StackMode = range ? 'none' : ((raw.stack as StackMode | undefined) ?? 'none'); + const group = frame.group; + // Grouped, every slot is a whole group: today's column is as wide as yesterday's, and the time + // axis runs on to the end of the last group. + const table = columnTable( + frame.series.map((s) => s.points), + group ? Infinity : frame.range.end, + ); + const xRange = + group && table.ends.length ? { ...frame.range, end: Math.max(frame.range.end, ...table.ends) } : frame.range; + const layers = mode === 'none' ? undefined : stackValues(table.values, mode); + const targets = frame.series.map((s) => s.entity.target).filter((t): t is number => t !== null); + let data: [number, number] | undefined; + if (layers) data = stackExtent(layers); + else if (range) + data = spread([...table.lows.flat(), ...table.highs.flat(), ...table.values.flat(), ...targets], false); + else data = spread([...table.values.flat(), ...(ctx.config.show.target ? targets : [])]); + let fixed = raw; + if (mode === 'percent') fixed = { y_min: data![0] < 0 ? -100 : 0, y_max: data![1] > 0 ? 100 : 0, ...raw }; + else if (!range && data) fixed = zeroBased(data, raw); + let axes: PlotAxes = plotAxes({ ...ctx, raw: fixed }, xRange, data); + const { scales } = axes; + const centres = table.times.map((t, i) => (t + table.ends[i]) / 2); + const slotOf = new Map(centres.map((c, i) => [c, i])); + if (group && centres.length) { + // One label under each set of columns, every second or third one when they won't all fit. + const every = labelEvery(centres.length, plotWidth(ctx) / GROUP_LABEL_PX[group.unit]); + const unit = LABEL_UNIT[group.unit]; + // A week's worth of days goes by its day names. + const span = group.unit === 'day' && centres.length <= 8 ? 7 * 86_400_000 : frame.range.end - frame.range.start; + axes = { + ...axes, + unit, + times: centres.filter((_, i) => i % every === 0), + label: (c) => timeLabel(table.times[slotOf.get(c)!], unit, span, ctx.hass), + }; + } + const drawn = drawColumns(ctx, frame, table, scales, mode, layers); + // The tooltip reads each series at the middle of each slot. + const shown: ChartSeries[] = frame.series.map((s, k) => ({ + ...s, + points: centres.map((t, i): Point => ({ t, v: table.values[k][i], min: table.lows[k][i], max: table.highs[k][i] })), + })); + const tip: TooltipOptions = { + dotY: () => null, + heading: (c) => { + const i = slotOf.get(c); + if (i === undefined) return formatDateTime(c, ctx.hass); + return group + ? groupHeading(table.times[i], table.ends[i], group.unit, ctx.hass) + : formatDateTime(table.times[i], ctx.hass); + }, + }; + const fmt = (v: number, s: ChartSeries) => formatNumber(v, s.entity.decimals, ctx.hass); + if (range) { + tip.text = (s, p) => { + if (!p || p.v === null) return '—'; + const lo = p.min ?? p.v; + const hi = p.max ?? p.v; + return withUnit(`${fmt(lo, s)} – ${fmt(hi, s)}`, s.entity.unit); + }; + } + if (layers) { + const sums = totals(table.values); + const unit = sharedUnit(shown); + const decimals = Math.max(...shown.map((s) => s.entity.decimals)); + tip.footer = (rows) => totalRow(ctx, sums[slotOf.get(rows[0]?.p?.t ?? NaN) ?? -1] ?? null, unit, decimals); + if (mode === 'percent') { + tip.text = (s, p) => { + const layer = p ? layers[s.index][slotOf.get(p.t) ?? -1] : null; + if (!p || p.v === null || !layer) return '—'; + return `${withUnit(fmt(p.v, s), s.entity.unit)} · ${Math.round(Math.abs(layer[1] - layer[0]))} %`; + }; + } + } + const tooltip = tooltipEnabled(raw); + const hovered = tooltip ? hoverTime(ctx.host) : undefined; + const at = hovered === undefined ? undefined : slotOf.get(hovered); + const highlight = + at === undefined + ? nothing + : html`
`; + const label = localize(ctx.hass, 'chart_aria', { + style: localize(ctx.hass, 'editor_style_column'), + names: frame.series.map((s) => s.entity.name).join(', '), + }); + const radius = raw.rounded === false ? 0 : RADIUS_PX; + return chartFrame( + layout, + header, + chartArea({ + axes, + ctx, + content: svg``, + label, + overlay: html`
${highlight}${drawn.columns}${drawn.targets}
+
${drawn.labels}
+ ${layout.header === 'over' ? headerOver(header) : nothing} + ${tooltip ? tooltipLayer(ctx, shown, scales, hovered, plotWidth(ctx), tip) : nothing}`, + hover: tooltip + ? hoverHandlers( + ctx, + xRange, + shown.map((s) => s.points), + ) + : undefined, + }), + ); +} + +/** The shared history fields with the column chart's defaults: 7 days, grouping chosen for you. */ +const COLUMN_HISTORY_FIELDS: FieldBinding[] = HISTORY_FIELDS.map((f) => { + if (f.name === 'period_last') return { ...f, default: COLUMN_PERIOD.last }; + if (f.name === 'period_unit') return { ...f, default: COLUMN_PERIOD.unit }; + if (f.name === 'group_by' || f.name === 'aggregate') return { ...f, default: 'auto' }; + return f; +}); + +const COLUMN_FIELDS: FieldBinding[] = [ + ...COLUMN_HISTORY_FIELDS, + { name: 'stack', path: ['stack'], default: 'none' }, + { name: 'color_by', path: ['color_by'], default: 'series' }, + { name: 'range', path: ['range'], default: false }, + { name: 'column_labels', path: ['labels'], default: false }, + { name: 'rounded', path: ['rounded'], default: true }, + ...AXES_FIELDS, + ...LAYOUT_FIELDS, +]; + +/** + * A rolling period edited here keeps both its length and unit in the config: the column chart's + * default (7 days) differs from other charts', so a lone `last: 24` would read as 24 hours. + */ +export function fullPeriod(config: CardConfig): CardConfig { + const period = config.period as Record | undefined; + if (!period || typeof period !== 'object' || period.calendar !== undefined) return config; + if (period.last !== undefined && period.unit !== undefined) return config; + return { ...config, period: { last: COLUMN_PERIOD.last, unit: COLUMN_PERIOD.unit, ...period } }; +} + +export function columnEditor(ctx: ChartEditorContext): TemplateResult { + const ungrouped = ctx.config.group_by === 'none'; + // The shared period fields without their grouping row: the column chart has its own choices. + const period = { ...ctx.config, group_by: undefined }; + const schema = cachedSchema(`column|${historyVariant(period)}|${ungrouped}`, ctx.hass, () => [ + ...historySchema(ctx.hass, period).slice(0, -1), + { + type: 'grid', + name: '', + schema: [ + { name: 'group_by', selector: choice(ctx.hass, 'group_by', COLUMN_GROUPS) }, + ...(ungrouped ? [] : [{ name: 'aggregate', selector: choice(ctx.hass, 'aggregate', COLUMN_AGGREGATES) }]), + ], + }, + { + type: 'grid', + name: '', + schema: [ + { name: 'stack', selector: choice(ctx.hass, 'stack', STACK_MODES) }, + { name: 'color_by', selector: choice(ctx.hass, 'color_by', ['series', 'bands']) }, + ], + }, + { + type: 'grid', + name: '', + schema: [ + { name: 'range', selector: { boolean: {} } }, + { name: 'column_labels', selector: { boolean: {} } }, + { name: 'rounded', selector: { boolean: {} } }, + ], + }, + ...axesSchema(ctx.hass), + ...layoutSchema(ctx.hass), + ]); + return chartForm({ ...ctx, commit: (c) => ctx.commit(fullPeriod(c)) }, 'chart', schema, COLUMN_FIELDS); +} + +const ENTITY_FIELDS: FieldBinding[] = [{ name: 'column_color', path: ['color'] }]; + +export const columnStyles = css` + .columns { + position: absolute; + inset: 0; + pointer-events: none; + } + .col { + position: absolute; + box-sizing: border-box; + transform-origin: 50% 100%; + animation: cd-col-grow var(--cd-duration) var(--cd-easing); + } + .col.down { + transform-origin: 50% 0; + } + .col.cap-top { + border-radius: var(--r) var(--r) 0 0; + } + .col.cap-bottom { + border-radius: 0 0 var(--r) var(--r); + } + .col.cap-both { + border-radius: var(--r); + opacity: 0.75; + } + .col-mark { + position: absolute; + height: 0; + border-top: 2px solid var(--card-background-color, #fff); + transform: translateY(-1px); + } + .target-mark { + position: absolute; + height: 0; + border-top: 2px solid var(--primary-text-color); + transform: translateY(-1px); + border-radius: 1px; + } + .slot-hover { + position: absolute; + top: 0; + bottom: 0; + background: color-mix(in srgb, var(--primary-text-color) 7%, transparent); + } + .point-label.col-label.below { + transform: translate(-50%, 4px); + } + :host([static]) .col { + animation: none; + } + @keyframes cd-col-grow { + from { + transform: scaleY(0); + } + } + /* Labels over the plot stay readable above the columns. */ + .axes-inside .y-axis, + .axes-inside .x-axis { + z-index: 1; + } +`; + +export const columnChart: ChartStyleModule = { + layout: 'chart', + group: 'history', + height: 240, + styles: css` + ${frameStyles} + ${gridStyles} + ${tooltipStyles} + ${layoutStyles} + ${columnStyles} + `, + thumbnail: () => svg` + + + + + + `, + validate, + editor: columnEditor, + entityFields: (hass) => ({ + schema: cachedSchema('column-entity', hass, () => [{ name: 'column_color', selector: { text: {} } }]), + fields: ENTITY_FIELDS, + }), + disconnect(host) { + disconnectHistory(host); + stopChartWidth(host); + stopHover(host); + }, + render, +}; diff --git a/src/styles/index.ts b/src/styles/index.ts index ea77cab..f74dbda 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4,6 +4,7 @@ import type { ResolvedEntity } from '../resolve'; import type { CardConfig, HomeAssistant, NormalizedConfig, ShowOptions, StyleName } from '../types'; import { area } from './area'; import { bar } from './bar'; +import { columnChart } from './column'; import { line } from './line'; import { needle } from './needle'; import { arc, ring } from './radial'; @@ -107,5 +108,6 @@ export const STYLES: Record = { needle, arc, line, + column: columnChart, area, }; diff --git a/src/types.ts b/src/types.ts index 7c12cbf..3450938 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,17 @@ import type { Rule } from './fill'; -export type StyleName = 'bar' | 'thin' | 'segmented' | 'tank' | 'ring' | 'needle' | 'arc' | 'line' | 'area'; -export const STYLE_NAMES: StyleName[] = ['bar', 'thin', 'segmented', 'tank', 'ring', 'needle', 'arc', 'line', 'area']; +export type StyleName = 'bar' | 'thin' | 'segmented' | 'tank' | 'ring' | 'needle' | 'arc' | 'line' | 'column' | 'area'; +export const STYLE_NAMES: StyleName[] = [ + 'bar', + 'thin', + 'segmented', + 'tank', + 'ring', + 'needle', + 'arc', + 'line', + 'column', + 'area', +]; export type SortMode = 'none' | 'ascending' | 'descending'; /** A number, or a string naming an entity whose state supplies the number. */ export type Limit = number | string; diff --git a/test/column.test.ts b/test/column.test.ts new file mode 100644 index 0000000..3d42b06 --- /dev/null +++ b/test/column.test.ts @@ -0,0 +1,303 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import '../src/chartdeck-card'; +import { + autoGroupUnit, + columnBox, + columnRaw, + columnTable, + isMeter, + labelEvery, + spread, + zeroBased, +} from '../src/chart/columns'; +import { groupHistory, groupStatistics, statisticsQueryFor } from '../src/history/group'; +import { parsePeriod } from '../src/history/period'; +import type { ResolvedEntity } from '../src/resolve'; +import { fullPeriod, groupHeading } from '../src/styles/column'; +import type { CardConfig } from '../src/types'; +import { H, ago, chartHass, mountCard, mountEditor, pointAt, q, qa, tooltipRows } from './helpers/chart'; +import type { Card } from './helpers/chart'; + +const now = Date.now(); +const sec = (ms: number) => ago(now, ms); +const D = 24 * H; + +const states = { + 'sensor.energy': ['1000', { friendly_name: 'Energy', unit_of_measurement: 'kWh', state_class: 'total_increasing' }], + 'sensor.solar': ['40', { friendly_name: 'Solar', unit_of_measurement: 'kWh', state_class: 'total_increasing' }], + 'sensor.temp': ['20', { friendly_name: 'Temp', unit_of_measurement: '°C', state_class: 'measurement' }], + 'sensor.grid': ['-200', { friendly_name: 'Grid', unit_of_measurement: 'W' }], +} as const; + +/** A meter that goes up 10 a day for the last 10 days. */ +const meter = (perDay: number) => + Array.from({ length: 11 }, (_, i) => ({ s: String(1000 + perDay * i), lu: sec((10 - i) * D + H) })); + +const history = { + 'sensor.energy': meter(10), + 'sensor.solar': meter(4), + 'sensor.temp': [ + { s: '15', lu: sec(30 * H) }, + { s: '25', lu: sec(20 * H) }, + { s: '18', lu: sec(5 * H) }, + ], + 'sensor.grid': [ + { s: '400', lu: sec(20 * H) }, + { s: '-600', lu: sec(10 * H) }, + { s: 'unavailable', lu: sec(5 * H) }, + ], +}; + +const mount = (config: Record, hass = chartHass(states, history)) => + mountCard({ style: 'column', entities: ['sensor.energy'], ...config } as Partial, hass); + +const entity = (attrs: Record, attribute?: string) => + ({ config: { entity: 'sensor.x', attribute }, stateObj: { attributes: attrs } }) as unknown as ResolvedEntity; + +afterEach(() => { + document.body.innerHTML = ''; +}); + +describe('column maths', () => { + it('picks a grouping for the span', () => { + expect(autoGroupUnit(parsePeriod({ last: 24, unit: 'hour' }))).toBe('hour'); + expect(autoGroupUnit(parsePeriod({ last: 7, unit: 'day' }))).toBe('day'); + expect(autoGroupUnit(parsePeriod({ last: 12, unit: 'week' }))).toBe('week'); + expect(autoGroupUnit(parsePeriod({ last: 12, unit: 'month' }))).toBe('month'); + expect(autoGroupUnit(parsePeriod({ calendar: 'day' }))).toBe('hour'); + expect(autoGroupUnit(parsePeriod({ calendar: 'year' }))).toBe('month'); + }); + + it('fills in 7 days, automatic grouping, and the change for meters', () => { + const meters = [entity({ state_class: 'total_increasing' }), entity({ state_class: 'total' })]; + const raw = { type: 'x' } as CardConfig; + expect(columnRaw(raw, meters)).toMatchObject({ + period: { last: 7, unit: 'day' }, + group_by: 'day', + aggregate: 'change', + }); + expect(columnRaw({ ...raw, range: true }, meters).aggregate).toBe('mean'); + expect(columnRaw(raw, [...meters, entity({ state_class: 'measurement' })]).aggregate).toBe('mean'); + expect(columnRaw({ ...raw, group_by: 'none', aggregate: 'max' }, meters)).not.toHaveProperty('group_by'); + expect(columnRaw({ ...raw, group_by: 'week', period: { calendar: 'day' } }, meters).group_by).toBe('week'); + expect(isMeter(entity({ state_class: 'total_increasing' }, 'reading'))).toBe(false); + }); + + it('lines readings up into slots, holding values and dropping the empty last one', () => { + const t = columnTable( + [ + [ + { t: 0, v: 1 }, + { t: 20, v: 2 }, + { t: 30, v: 2 }, + ], + [{ t: 10, v: 5, min: 4, max: 6 }], + ], + 30, + ); + expect(t.times).toEqual([0, 10, 20]); + expect(t.ends).toEqual([10, 20, 30]); + expect(t.values).toEqual([ + [1, 1, 2], + [null, 5, 5], + ]); + expect(t.lows[1]).toEqual([null, 4, null]); + const grouped = columnTable([[{ t: 0, end: 24, v: 3 }]], Infinity); + expect(grouped.ends).toEqual([24]); + }); + + it('places columns side by side in a slot, and fits the axis to zero', () => { + expect(columnBox(0, 100, 0, 1)).toEqual([10, 80]); + const [l0, w0] = columnBox(0, 100, 0, 2); + const [l1] = columnBox(0, 100, 1, 2); + expect(l1 - l0).toBeCloseTo(40); + expect(w0).toBeCloseTo(40 * 0.88); + const raw = { type: 'x' } as CardConfig; + expect(zeroBased([5, 10], raw).y_min).toBe(0); + expect(zeroBased([-5, -1], raw).y_max).toBe(0); + expect(zeroBased([5, 10], { ...raw, y_min: 3 }).y_min).toBe(3); + expect(spread([3, null, 7])).toEqual([0, 7]); + expect(spread([3, 7], false)).toEqual([3, 7]); + expect(spread([null], false)).toBeUndefined(); + expect(labelEvery(30, 8)).toBe(4); + expect(labelEvery(3, 0)).toBe(3); + }); + + it('keeps each group’s lowest and highest when asked', () => { + const pts = [ + { t: 0, v: 5 }, + { t: 10, v: 9 }, + { t: 20, v: 1 }, + ]; + const [g] = groupHistory(pts, [0, 30], 'mean', 30, true); + expect([g.min, g.max]).toEqual([1, 9]); + expect(groupHistory(pts, [0, 30], 'mean', 30)[0].min).toBeUndefined(); + const rows = [ + { t: 0, v: 5, min: 2, max: 8 }, + { t: 10, v: 6, min: null, max: null }, + ]; + const [s] = groupStatistics(rows, [0, 20], 'mean', Infinity, true); + expect([s.min, s.max]).toEqual([2, 8]); + const zone = { timeZone: 'UTC', firstDay: 1 }; + expect(statisticsQueryFor({ unit: 'day', aggregate: 'mean' }, zone, 'UTC', true).types).toEqual([ + 'mean', + 'min', + 'max', + ]); + expect(statisticsQueryFor({ unit: 'day', aggregate: 'mean' }, zone, 'UTC').types).toEqual(['mean']); + }); + + it('names a group in the tooltip', () => { + const start = Date.UTC(2026, 9, 26); + expect(groupHeading(start, start + D, 'day', undefined)).toMatch(/26/); + expect(groupHeading(start, start + 7 * D, 'week', undefined)).toMatch(/26.*1|Oct.*Nov/); + expect(groupHeading(start, start + 31 * D, 'month', undefined)).toMatch(/2026/); + expect(groupHeading(start, start + H, 'hour', undefined)).toMatch(/–/); + }); + + it('keeps a rolling period whole when the editor writes it', () => { + expect(fullPeriod({ type: 'x', period: { last: 24 } })).toEqual({ type: 'x', period: { last: 24, unit: 'day' } }); + expect(fullPeriod({ type: 'x', period: { calendar: 'week' } })).toEqual({ + type: 'x', + period: { calendar: 'week' }, + }); + expect(fullPeriod({ type: 'x' })).toEqual({ type: 'x' }); + }); +}); + +describe('column chart', () => { + it('shows a meter as daily totals over the last 7 days with no setup', async () => { + const card = await mount({ labels: true }); + const cols = qa(card, '.col'); + expect(cols.length).toBeGreaterThanOrEqual(7); + expect(cols.length).toBeLessThanOrEqual(8); + expect(cols.every((c) => c.classList.contains('cap-top'))).toBe(true); + expect(qa(card, '.col-label').some((l) => l.textContent === '10.0')).toBe(true); + // A label under every day, or every other one on a narrow card. + expect(qa(card, '.x-axis .axis-label').length).toBeGreaterThanOrEqual(4); + expect(q(card, 'svg.plot')!.getAttribute('aria-label')).toBe('Column chart of Energy'); + const area = q(card, '.plot-area')!; + pointAt(area, 'pointermove', 0.5); + await card.updateComplete; + expect(tooltipRows(card)[0]).toMatch(/^Energy 10\.0 kWh$/); + expect(q(card, '.slot-hover')).not.toBeNull(); + expect(qa(card, '.hover-dot')).toHaveLength(0); + }); + + it('draws series side by side, coloured by band, with a target on each column', async () => { + const card = await mount({ + entities: [{ entity: 'sensor.energy', target: 8 }, 'sensor.solar'], + color_by: 'bands', + severity: [ + { from: 0, color: 'green' }, + { from: 5, color: 'red' }, + ], + }); + const a = qa(card, '.col[data-series="0"]'); + const b = qa(card, '.col[data-series="1"]'); + expect(a.length).toBe(b.length); + expect(a[1].getAttribute('style')).toContain('--error-color'); + expect(b[1].getAttribute('style')).toContain('--success-color'); + expect(qa(card, '.target-mark').length).toBe(a.length); + }); + + it('stacks columns with a total, and to 100 %', async () => { + const card = await mount({ stack: 'normal', labels: true, entities: ['sensor.energy', 'sensor.solar'] }); + expect(qa(card, '.col.cap-top').every((c) => c.getAttribute('data-series') === '1')).toBe(true); + expect(qa(card, '.col-label').some((l) => l.textContent === '14.0')).toBe(true); + pointAt(q(card, '.plot-area')!, 'pointermove', 0.5); + await card.updateComplete; + expect(tooltipRows(card)).toEqual(['Energy 10.0 kWh', 'Solar 4.0 kWh', 'Total 14.0 kWh']); + const pct = await mount({ stack: 'percent', entities: ['sensor.energy', 'sensor.solar'] }); + expect(qa(pct, '.y-axis .axis-label').at(-1)!.textContent).toBe('100'); + pointAt(q(pct, '.plot-area')!, 'pointermove', 0.5); + await pct.updateComplete; + expect(tooltipRows(pct)[0]).toBe('Energy 10.0 kWh · 71 %'); + }); + + it('hangs negative columns below zero, and leaves a gap while unavailable', async () => { + const card = await mount({ entities: ['sensor.grid'], period: { last: 24, unit: 'hour' }, labels: true }); + expect(qa(card, '.col.cap-bottom').length).toBeGreaterThan(0); + expect(qa(card, '.col.down').length).toBeGreaterThan(0); + expect(qa(card, '.col').length).toBeLessThan(24); + }); + + it('draws one column per reading when not grouped', async () => { + const card = await mount({ entities: ['sensor.temp'], group_by: 'none', period: { last: 2, unit: 'day' } }); + expect(qa(card, '.col')).toHaveLength(3); + }); + + it('draws range columns from each group’s lowest to highest, from statistics', async () => { + const stats = { + 'sensor.temp': [ + { start: now - 2 * D, mean: 18, min: 12, max: 24 }, + { start: now - D, mean: 20, min: 15, max: 26 }, + ], + }; + const hass = chartHass(states, history, stats); + const card = await mount({ entities: ['sensor.temp'], range: true, source: 'statistics', group_by: 'day' }, hass); + expect(qa(card, '.col.cap-both').length).toBeGreaterThan(0); + expect(qa(card, '.col-mark').length).toBe(qa(card, '.col.cap-both').length); + const call = (hass.callWS as unknown as { mock: { calls: [{ types: string[] }][] } }).mock.calls[0][0]; + expect(call.types).toEqual(expect.arrayContaining(['mean', 'min', 'max'])); + pointAt(q(card, '.plot-area')!, 'pointermove', 0.8); + await card.updateComplete; + expect(tooltipRows(card)[0]).toMatch(/Temp \d+\.\d – \d+\.\d °C/); + }); + + it('checks its options', () => { + const card = document.createElement('chartdeck-card') as Card; + const set = (extra: Record) => () => + card.setConfig({ type: 'x', style: 'column', entities: ['sensor.energy'], ...extra } as CardConfig); + expect(set({ group_by: 'year' })).toThrow(/group_by/); + expect(set({ aggregate: 'median' })).toThrow(/aggregate/); + expect(set({ range: 'yes' })).toThrow(/range/); + expect(set({ color_by: 'mood' })).toThrow(/color_by/); + expect(set({ stack: 'up' })).toThrow(/stack/); + expect(set({ group_by: 'auto', aggregate: 'auto', rounded: false })).not.toThrow(); + expect(set({ group_by: 'none' })).not.toThrow(); + }); +}); + +describe('column chart editor', () => { + it('defaults to 7 days with automatic grouping and writes its own options', async () => { + const raw = { type: 'custom:chartdeck-card', style: 'column', entities: ['sensor.energy'] } as CardConfig; + const { form, change, events, entityFields } = await mountEditor(raw, chartHass(states, {})); + expect(form().data).toMatchObject({ + period_last: 7, + period_unit: 'day', + group_by: 'auto', + aggregate: 'auto', + stack: 'none', + range: false, + column_labels: false, + rounded: true, + color_by: 'series', + }); + change({ period_last: 24 }); + expect(events.at(-1)).toEqual({ ...raw, period: { last: 24, unit: 'day' } }); + change({ stack: 'percent', column_labels: true, rounded: false, range: true, group_by: 'none' }); + expect(events.at(-1)).toEqual({ + ...raw, + stack: 'percent', + labels: true, + rounded: false, + range: true, + group_by: 'none', + }); + const schema = JSON.stringify(form().schema); + expect(schema).toContain('"aggregate"'); + expect(entityFields().fields.map((f) => f.name)).toEqual(['column_color']); + }); + + it('hides the combine choice when each reading is its own column', async () => { + const raw = { + type: 'custom:chartdeck-card', + style: 'column', + group_by: 'none', + entities: ['sensor.energy'], + } as CardConfig; + const { form } = await mountEditor(raw, chartHass(states, {})); + expect(JSON.stringify(form().schema)).not.toContain('"aggregate"'); + }); +});