diff --git a/src/chart.ts b/src/chart.ts
index 4e2c14f..d2e84b1 100644
--- a/src/chart.ts
+++ b/src/chart.ts
@@ -1,19 +1,19 @@
-import { LitElement, html, svg, TemplateResult, SVGTemplateResult, PropertyValues, CSSResultGroup } from 'lit';
+import { LitElement, html, TemplateResult, PropertyValues, CSSResultGroup } from 'lit';
import { styleMap } from 'lit/directives/style-map';
import { classMap } from 'lit/directives/class-map';
import { until } from 'lit/directives/until.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { customElement, property, state } from 'lit/decorators';
-import { HomeAssistant, stateIcon } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types. https://github.com/custom-cards/custom-card-helpers
+import { HomeAssistant } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types. https://github.com/custom-cards/custom-card-helpers
import type { Config, SectionState, Box, ConnectionState, EntityConfigInternal, NormalizedState } from './types';
-import { MIN_LABEL_HEIGHT } from './const';
import { localize } from './localize/localize';
import styles from './styles';
-import { formatState, getChildConnections, getEntityId, normalizeStateValue, renderError } from './utils';
+import { getEntityId, normalizeStateValue, renderError, sortBoxes } from './utils';
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
import { handleAction } from './handle-actions';
import { filterConfigByZoomEntity } from './zoom';
+import './section';
@customElement('sankey-chart-base')
export class Chart extends LitElement {
@@ -279,10 +279,11 @@ export class Chart extends LitElement {
const calcResults = this._calcBoxHeights(boxes, availableHeight, total);
const parentBoxes = section.sort_group_by_parent ? sectionsStates[sectionsStates.length - 1]?.boxes || [] : [];
sectionsStates.push({
- boxes: this._sortBoxes(parentBoxes, calcResults.boxes, section.sort_by, section.sort_dir),
+ boxes: sortBoxes(parentBoxes, calcResults.boxes, section.sort_by, section.sort_dir),
total,
statePerPixelY: calcResults.statePerPixelY,
spacerH: 0,
+ config: section,
});
});
@@ -323,23 +324,6 @@ export class Chart extends LitElement {
});
}
- private _sortBoxes(parentBoxes: Box[], boxes: Box[], sort?: string, dir = 'desc') {
- if (sort === 'state') {
- const sortByParent = (a: Box, b: Box, realSort: (a: Box, b: Box) => number) => {
- const parentIndexA = parentBoxes.findIndex(p => p.children.includes(a.entity_id));
- const parentIndexB = parentBoxes.findIndex(p => p.children.includes(b.entity_id));
- return parentIndexA < parentIndexB ? -1 : parentIndexA > parentIndexB ? 1 : realSort(a, b);
- };
-
- if (dir === 'desc') {
- boxes.sort((a, b) => sortByParent(a, b, (a, b) => (a.state > b.state ? -1 : a.state < b.state ? 1 : 0)));
- } else {
- boxes.sort((a, b) => sortByParent(a, b, (a, b) => (a.state < b.state ? -1 : a.state > b.state ? 1 : 0)));
- }
- }
- return boxes;
- }
-
private _calcBoxHeights(
boxes: Box[],
availableHeight: number,
@@ -460,143 +444,6 @@ export class Chart extends LitElement {
return styles;
}
- protected renderSection(index: number): TemplateResult {
- const { show_names, show_icons, show_states, show_units } = this.config;
- const section = this.sections[index];
- const { boxes, spacerH } = section;
- const hasChildren = index < this.sections.length - 1 && boxes.some(b => b.children.length > 0);
- const { min_width: minWidth } = this.config.sections[index];
-
- return html`
-
- ${hasChildren
- ? html`
-
-
`
- : 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;
-
- // 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._handleBoxTap(box)}
- @dblclick=${() => this._handleBoxDoubleTap(box)}
- @mouseenter=${() => this._handleMouseEnter(box)}
- @mouseleave=${this._handleMouseLeave}
- 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}
-
-
- ${extraSpacers
- ? html``
- : null}
- `;
- })}
-
- `;
- }
-
- protected renderBranchConnectors(index: number): SVGTemplateResult[] {
- const section = this.sections[index];
- const { boxes } = section;
- return boxes
- .filter(b => b.children.length > 0)
- .map(b => {
- const children = this.sections[index + 1].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;
- }
- }
- return true;
- }
- return false;
- });
- return svg`
-
- ${connections.map(
- (c, i) => svg`
-
-
-
-
- `,
- )}
-
- ${connections.map(
- (c, i) => svg`
-
- `,
- )}
- `;
- });
- }
-
// https://lit.dev/docs/components/rendering/
protected render(): TemplateResult | void {
try {
@@ -625,7 +472,21 @@ export class Chart extends LitElement {
return html`
- ${this.sections.map((s, i) => this.renderSection(i))}
+ ${this.sections.map(
+ (s, i) => html` `,
+ )}
`;
diff --git a/src/editor.ts.old b/src/editor.ts.old
deleted file mode 100644
index e69de29..0000000
diff --git a/src/editor/index.ts b/src/editor/index.ts
index f6e4af6..9f714a2 100644
--- a/src/editor/index.ts
+++ b/src/editor/index.ts
@@ -5,7 +5,7 @@ import { HomeAssistant, fireEvent, LovelaceCardEditor, LovelaceConfig } from 'cu
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { customElement, property, state } from 'lit/decorators';
import { repeat } from 'lit/directives/repeat';
-import { EntityConfig, SankeyChartConfig, SectionConfig } from '../types';
+import { SankeyChartConfig, SectionConfig } from '../types';
import { localize } from '../localize/localize';
import { getEntityId, normalizeConfig } from '../utils';
import './entity';
@@ -132,7 +132,7 @@ export class SankeyChartEditor extends LitElement implements LovelaceCardEditor
this._entityConfig = { sectionIndex, entityIndex, entity: sections[sectionIndex].entities[entityIndex] };
}
- private _handleEntityConfig = (entityConf: EntityConfig): void => {
+ private _handleEntityConfig = (entityConf: EntityConfigOrStr): void => {
this._editEntity({
detail: { value: entityConf },
target: { section: this._entityConfig?.sectionIndex, index: this._entityConfig?.entityIndex },
diff --git a/src/energy.ts b/src/energy.ts
index 2ea6d6c..da6a67a 100644
--- a/src/energy.ts
+++ b/src/energy.ts
@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { HomeAssistant } from "custom-card-helpers";
import { Collection } from "home-assistant-js-websocket";
-import { addHours, differenceInDays } from 'date-fns';
+import { differenceInDays } from 'date-fns';
export const ENERGY_SOURCE_TYPES = ['grid', 'solar', 'battery'];
diff --git a/src/ha-sankey-chart.ts b/src/ha-sankey-chart.ts
index 9d011f2..287738d 100644
--- a/src/ha-sankey-chart.ts
+++ b/src/ha-sankey-chart.ts
@@ -9,7 +9,6 @@ import { localize } from './localize/localize';
import { normalizeConfig, renderError } from './utils';
import { SubscribeMixin } from './subscribe-mixin';
import './chart';
-import { Chart } from './chart';
import { HassEntities } from 'home-assistant-js-websocket';
import {
EnergyCollection,
@@ -55,8 +54,6 @@ export class SankeyChart extends SubscribeMixin(LitElement) {
// https://lit.dev/docs/components/properties/
@property({ attribute: false }) public hass!: HomeAssistantReal;
- @query('ha-chart-base') private _chart?: Chart;
-
@state() private config!: Config;
@state() private states: HassEntities = {};
@state() private entityIds: string[] = [];
diff --git a/src/section.ts b/src/section.ts
new file mode 100644
index 0000000..473e564
--- /dev/null
+++ b/src/section.ts
@@ -0,0 +1,164 @@
+import { LitElement, 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';
+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;
+ }
+ }
+ 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);
+
+ return html`
+
+ ${hasChildren
+ ? html`
+
+
`
+ : 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;
+
+ // 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}
+
+
+ ${extraSpacers
+ ? html``
+ : null}
+ `;
+ })}
+
+ `;
+ }
+}
+
+export default Section;
diff --git a/src/subscribe-mixin.ts b/src/subscribe-mixin.ts
index 8e05e0d..6af9373 100644
--- a/src/subscribe-mixin.ts
+++ b/src/subscribe-mixin.ts
@@ -5,6 +5,7 @@
import { HomeAssistant } from "custom-card-helpers";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { PropertyValues, ReactiveElement } from "lit";
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { property } from "lit/decorators";
export interface HassSubscribeElement {
diff --git a/src/types.ts b/src/types.ts
index 1e20879..4ee1a44 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -163,6 +163,7 @@ export interface SectionState {
total: number;
spacerH: number;
statePerPixelY: number;
+ config: Section;
}
export interface ConnectionState {
diff --git a/src/utils.ts b/src/utils.ts
index 793e312..d59a744 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -183,6 +183,23 @@ export function normalizeConfig(conf: SankeyChartConfig): Config {
};
}
+export function sortBoxes(parentBoxes: Box[], boxes: Box[], sort?: string, dir = 'desc') {
+ if (sort === 'state') {
+ const sortByParent = (a: Box, b: Box, realSort: (a: Box, b: Box) => number) => {
+ const parentIndexA = parentBoxes.findIndex(p => p.children.includes(a.entity_id));
+ const parentIndexB = parentBoxes.findIndex(p => p.children.includes(b.entity_id));
+ return parentIndexA < parentIndexB ? -1 : parentIndexA > parentIndexB ? 1 : realSort(a, b);
+ };
+
+ if (dir === 'desc') {
+ boxes.sort((a, b) => sortByParent(a, b, (a, b) => (a.state > b.state ? -1 : a.state < b.state ? 1 : 0)));
+ } else {
+ boxes.sort((a, b) => sortByParent(a, b, (a, b) => (a.state < b.state ? -1 : a.state > b.state ? 1 : 0)));
+ }
+ }
+ return boxes;
+}
+
// private _showWarning(warning: string): TemplateResult {
// return html`
// ${warning}