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"'); + }); +});