diff --git a/src/chart.ts b/src/chart.ts index d2e84b1..7289cde 100644 --- a/src/chart.ts +++ b/src/chart.ts @@ -13,7 +13,7 @@ import { getEntityId, normalizeStateValue, renderError, sortBoxes } from './util import { HassEntities, HassEntity } from 'home-assistant-js-websocket'; import { handleAction } from './handle-actions'; import { filterConfigByZoomEntity } from './zoom'; -import './section'; +import { renderSection } from './section'; @customElement('sankey-chart-base') export class Chart extends LitElement { @@ -473,19 +473,19 @@ export class Chart extends LitElement {
${this.sections.map( - (s, i) => html` `, + (s, i) => renderSection({ + config: this.config, + section: s, + nextSection: this.sections[i + 1], + highlightedEntities: this.highlightedEntities, + statePerPixelY: this.statePerPixelY, + connectionsByParent: this.connectionsByParent, + connectionsByChild: this.connectionsByChild, + onTap: this._handleBoxTap.bind(this), + onDoubleTap: this._handleBoxDoubleTap.bind(this), + onMouseEnter: this._handleMouseEnter.bind(this), + onMouseLeave: this._handleMouseLeave.bind(this), + }) )}
diff --git a/src/section.ts b/src/section.ts index 473e564..c967600 100644 --- a/src/section.ts +++ b/src/section.ts @@ -1,6 +1,5 @@ -import { LitElement, html, svg, SVGTemplateResult } from 'lit'; +import { html, svg, SVGTemplateResult } from 'lit'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { customElement, property } from 'lit/decorators'; import { styleMap } from 'lit/directives/style-map'; import { Box, Config, ConnectionState, EntityConfigInternal, SectionState } from './types'; import { formatState, getChildConnections } from './utils'; @@ -8,157 +7,158 @@ import { stateIcon } from 'custom-card-helpers'; import { HassEntity } from 'home-assistant-js-websocket'; import { MIN_LABEL_HEIGHT } from './const'; -@customElement('sankey-chart-section') -class Section extends LitElement { - @property({ attribute: false }) public config!: Config; - @property({ attribute: false }) public section!: SectionState; - @property({ attribute: false }) public nextSection?: SectionState; - @property({ attribute: false }) public highlightedEntities!: EntityConfigInternal[]; - @property({ attribute: false }) public statePerPixelY!: number; - @property({ attribute: false }) public connectionsByParent: Map = new Map(); - @property({ attribute: false }) public connectionsByChild: Map = new Map(); - @property({ attribute: false }) public onTap!: (config: Box) => void; - @property({ attribute: false }) public onDoubleTap!: (config: Box) => void; - @property({ attribute: false }) public onMouseEnter!: (config: Box) => void; - @property({ attribute: false }) public onMouseLeave!: () => void; - - protected renderBranchConnectors(): SVGTemplateResult[] { - const { boxes } = this.section; - return boxes - .filter(b => b.children.length > 0) - .map(b => { - const children = this.nextSection!.boxes.filter(child => b.children.includes(child.entity_id)); - const connections = getChildConnections(b, children, this.connectionsByParent.get(b.config)).filter((c, i) => { - if (c.state > 0) { - children[i].connections.parents.push(c); - if (children[i].config.type === 'passthrough') { - // @FIXME not sure if this is needed anymore after v1.0.0 - const sumState = - this.connectionsByChild.get(children[i].config)?.reduce((sum, conn) => sum + conn.state, 0) || 0; - if (sumState !== children[i].state) { - // virtual entity that must only pass state to the next section - children[i].state = sumState; - // this could reduce the size of the box moving lower boxes up - // so we have to add spacers and adjust some positions - const newSize = Math.floor(sumState / this.statePerPixelY); - children[i].extraSpacers = (children[i].size - newSize) / 2; - c.endY += children[i].extraSpacers!; - children[i].top += children[i].extraSpacers!; - children[i].size = newSize; - } +export function renderBranchConnectors(props: { + section: SectionState, + nextSection?: SectionState, + statePerPixelY: number, + connectionsByParent: Map, + connectionsByChild: Map, +}): SVGTemplateResult[] { + const { boxes } = props.section; + return boxes + .filter(b => b.children.length > 0) + .map(b => { + const children = props.nextSection!.boxes.filter(child => b.children.includes(child.entity_id)); + const connections = getChildConnections(b, children, props.connectionsByParent.get(b.config)).filter((c, i) => { + if (c.state > 0) { + children[i].connections.parents.push(c); + if (children[i].config.type === 'passthrough') { + // @FIXME not sure if props is needed anymore after v1.0.0 + const sumState = + props.connectionsByChild.get(children[i].config)?.reduce((sum, conn) => sum + conn.state, 0) || 0; + if (sumState !== children[i].state) { + // virtual entity that must only pass state to the next section + children[i].state = sumState; + // props could reduce the size of the box moving lower boxes up + // so we have to add spacers and adjust some positions + const newSize = Math.floor(sumState / props.statePerPixelY); + children[i].extraSpacers = (children[i].size - newSize) / 2; + c.endY += children[i].extraSpacers!; + children[i].top += children[i].extraSpacers!; + children[i].size = newSize; } - return true; } - return false; - }); - return svg` - - ${connections.map( - (c, i) => svg` - - - - - `, - )} - + return true; + } + return false; + }); + return svg` + ${connections.map( (c, i) => svg` - + + + + `, )} - `; - }); - } + + ${connections.map( + (c, i) => svg` + + `, + )} + `; + }); +} - public render() { - const { show_names, show_icons, show_states, show_units } = this.config; - const { - boxes, - spacerH, - config: { min_width: minWidth }, - } = this.section; - const hasChildren = this.nextSection && boxes.some(b => b.children.length > 0); +export function renderSection(props: { + config: Config, + section: SectionState, + nextSection?: SectionState, + highlightedEntities: EntityConfigInternal[], + statePerPixelY: number, + connectionsByParent: Map, + connectionsByChild: Map, + onTap: (config: Box) => void, + onDoubleTap: (config: Box) => void, + onMouseEnter: (config: Box) => void, + onMouseLeave: () => void, +}) { + const { show_names, show_icons, show_states, show_units } = props.config; + const { + boxes, + spacerH, + config: { min_width: minWidth }, + } = props.section; + const hasChildren = props.nextSection && boxes.some(b => b.children.length > 0); - return html` -
- ${hasChildren - ? html`
- - ${this.renderBranchConnectors()} - -
` - : null} - ${boxes.map((box, i) => { - const { entity, extraSpacers } = box; - const formattedState = formatState(box.state, this.config.round); - const isNotPassthrough = box.config.type !== 'passthrough'; - const name = box.config.name || entity.attributes.friendly_name || ''; - const icon = box.config.icon || stateIcon(entity as HassEntity); - const maxLabelH = box.size + spacerH - 1; + return html` +
+ ${hasChildren + ? html`
+ + ${renderBranchConnectors(props)} + +
` + : null} + ${boxes.map((box, i) => { + const { entity, extraSpacers } = box; + const formattedState = formatState(box.state, props.config.round); + const isNotPassthrough = box.config.type !== 'passthrough'; + const name = box.config.name || entity.attributes.friendly_name || ''; + const icon = box.config.icon || stateIcon(entity as HassEntity); + const maxLabelH = box.size + spacerH - 1; - // reduce label size if it doesn't fit - const labelStyle: Record = { lineHeight: MIN_LABEL_HEIGHT + 'px' }; - const nameStyle: Record = {}; - if (maxLabelH < MIN_LABEL_HEIGHT) { - const fontSize = maxLabelH / MIN_LABEL_HEIGHT; - // labelStyle.maxHeight = maxLabelH + 'px'; - labelStyle.fontSize = `${fontSize}em`; - labelStyle.lineHeight = `${fontSize}em`; - } - const numLines = name.split('\n').filter(v => v).length; - if (numLines > 1) { - nameStyle.whiteSpace = 'pre'; - if (labelStyle.fontSize) { - nameStyle.fontSize = `${1 / numLines + 0.1}rem`; - nameStyle.lineHeight = `${1 / numLines + 0.1}rem`; - } else if (maxLabelH < MIN_LABEL_HEIGHT * numLines) { - nameStyle.fontSize = `${(maxLabelH / MIN_LABEL_HEIGHT / numLines) * 1.1}em`; - nameStyle.lineHeight = `${(maxLabelH / MIN_LABEL_HEIGHT / numLines) * 1.1}em`; - } + // reduce label size if it doesn't fit + const labelStyle: Record = { lineHeight: MIN_LABEL_HEIGHT + 'px' }; + const nameStyle: Record = {}; + if (maxLabelH < MIN_LABEL_HEIGHT) { + const fontSize = maxLabelH / MIN_LABEL_HEIGHT; + // labelStyle.maxHeight = maxLabelH + 'px'; + labelStyle.fontSize = `${fontSize}em`; + labelStyle.lineHeight = `${fontSize}em`; + } + const numLines = name.split('\n').filter(v => v).length; + if (numLines > 1) { + nameStyle.whiteSpace = 'pre'; + if (labelStyle.fontSize) { + nameStyle.fontSize = `${1 / numLines + 0.1}rem`; + nameStyle.lineHeight = `${1 / numLines + 0.1}rem`; + } else if (maxLabelH < MIN_LABEL_HEIGHT * numLines) { + nameStyle.fontSize = `${(maxLabelH / MIN_LABEL_HEIGHT / numLines) * 1.1}em`; + nameStyle.lineHeight = `${(maxLabelH / MIN_LABEL_HEIGHT / numLines) * 1.1}em`; } + } - return html` - ${i > 0 ? html`
` : null} - ${extraSpacers - ? html`
` - : null} -
-
this.onTap(box)} - @dblclick=${() => this.onDoubleTap(box)} - @mouseenter=${() => this.onMouseEnter(box)} - @mouseleave=${this.onMouseLeave} - title=${formattedState + box.unit_of_measurement + ' ' + name} - class=${this.highlightedEntities.includes(box.config) ? 'hl' : ''} - > - ${show_icons && isNotPassthrough - ? html`` - : null} -
-
- ${show_states && isNotPassthrough - ? html`${formattedState}${show_units - ? html`${box.unit_of_measurement}` - : null}` - : null} - ${show_names && isNotPassthrough - ? html` ${name}` - : null} -
+ return html` + ${i > 0 ? html`
` : null} + ${extraSpacers + ? html`
` + : null} +
+
props.onTap(box)} + @dblclick=${() => props.onDoubleTap(box)} + @mouseenter=${() => props.onMouseEnter(box)} + @mouseleave=${props.onMouseLeave} + title=${formattedState + box.unit_of_measurement + ' ' + name} + class=${props.highlightedEntities.includes(box.config) ? 'hl' : ''} + > + ${show_icons && isNotPassthrough + ? html`` + : null} +
+
+ ${show_states && isNotPassthrough + ? html`${formattedState}${show_units + ? html`${box.unit_of_measurement}` + : null}` + : null} + ${show_names && isNotPassthrough + ? html` ${name}` + : null}
- ${extraSpacers - ? html`
` - : null} - `; - })} -
- `; - } +
+ ${extraSpacers + ? html`
` + : null} + `; + })} +
+ `; } - -export default Section;