Skip to content

Commit

Permalink
fix(#180): refactor passthrough logic to support more complex configs
Browse files Browse the repository at this point in the history
  • Loading branch information
MindFreeze committed Apr 18, 2024
1 parent c5832ec commit e539913
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 79 deletions.
118 changes: 68 additions & 50 deletions src/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,34 @@ export class Chart extends LitElement {
entities.forEach(ent => {
if (ent.type === 'entity') {
this.entityIds.push(ent.entity_id);
} else if (ent.type === 'passthrough') {
return;
}
ent.children.forEach(childConf => {
const child = this.config.sections[sectionIndex + 1]?.entities.find(
e => e.entity_id === getEntityId(childConf),
);
if (!child) {
this.error = new Error(localize('common.missing_child') + ' ' + getEntityId(childConf));
throw this.error;
const passthroughs: EntityConfigInternal[] = [];
const childId = getEntityId(childConf);
let child: EntityConfigInternal | undefined = ent;
for (let i = 1; i < this.config.sections.length; i++) {
child = this.config.sections[sectionIndex + i]?.entities.find(
e => e.entity_id === childId,
);
if (!child) {
this.error = new Error(localize('common.missing_child') + ' ' + getEntityId(childConf));
throw this.error;
}
if (child.type !== 'passthrough') {
break;
}
passthroughs.push(child);
}
const connection: ConnectionState = {
parent: ent,
child,
child: child,
state: 0,
prevParentState: 0,
prevChildState: 0,
ready: false,
passthroughs,
};
this.connections.push(connection);
if (!this.connectionsByParent.has(ent)) {
Expand Down Expand Up @@ -139,21 +151,21 @@ export class Chart extends LitElement {
if (!connection.calculating) {
connection.calculating = true;
[parent, child].forEach(ent => {
if (ent.type === 'remaining_child_state' || ent.type === 'passthrough') {
if (ent.type === 'remaining_child_state') {
this.connectionsByParent.get(ent)!.forEach(c => {
if (!c.ready && !c.calculating) {
if (!c.ready) {
this.connectionsByChild.get(c.child)?.forEach(conn => {
if (conn !== connection) {
if (conn !== connection && !conn.calculating) {
this._calcConnection(conn, accountedIn, accountedOut);
}
});
}
});
} else if (ent.type === 'remaining_parent_state') {
this.connectionsByChild.get(ent)!.forEach(c => {
if (!c.ready && !c.calculating) {
if (!c.ready) {
this.connectionsByParent.get(c.parent)?.forEach(conn => {
if (conn !== connection) {
if (conn !== connection && !conn.calculating) {
this._calcConnection(conn, accountedIn, accountedOut);
}
});
Expand Down Expand Up @@ -200,9 +212,6 @@ export class Chart extends LitElement {
accountedIn.set(child, connection.prevChildState);
this._calcConnection(connection, accountedIn, accountedOut, true);
}
if (child.type === 'passthrough') {
this.entityStates.delete(child);
}
}

private _getMemoizedState(entityConfOrStr: EntityConfigInternal | string) {
Expand All @@ -214,15 +223,7 @@ export class Chart extends LitElement {
const normalized = normalizeStateValue(this.config.unit_prefix, Number(entity.state), unit_of_measurement);

if (entityConf.type === 'passthrough') {
// try to find the state from parents or children, else return Infinity
[this.connectionsByChild.get(entityConf), this.connectionsByParent.get(entityConf)].some(connections => {
if (!connections) {
// passthrough should have connections in both directions
throw new Error('Invalid entity config ' + JSON.stringify(entityConf));
}
normalized.state = connections.reduce((sum, c) => (c.ready ? sum + c.state : Infinity), 0);
return normalized.state !== Infinity;
});
normalized.state = this.connections.filter(c => c.passthroughs.includes(entityConf)).reduce((sum, c) => (c.ready ? sum + c.state : Infinity), 0);
}
if (entityConf.add_entities) {
entityConf.add_entities.forEach(subId => {
Expand Down Expand Up @@ -310,6 +311,7 @@ export class Chart extends LitElement {
connections: { parents: [] },
top: 0,
size: 0,
connectedParentState: 0,
};
});
if (!boxes.length) {
Expand Down Expand Up @@ -398,17 +400,34 @@ export class Chart extends LitElement {
return { boxes: result, statePerPixelY: this.statePerPixelY };
}

private highlightPath(entityConf: EntityConfigInternal, direction: 'parents' | 'children') {
private highlightPath(entityConf: EntityConfigInternal, direction?: 'parents' | 'children') {
this.highlightedEntities.push(entityConf);
if (direction === 'children') {
this.connectionsByParent.get(entityConf)?.forEach(c => {
c.highlighted = true;
this.highlightPath(c.child, 'children');
if (!direction || direction === 'children') {
this.connections.forEach(c => {
if (c.passthroughs.includes(entityConf) || c.parent === entityConf) {
if (!c.highlighted) {
c.passthroughs.forEach(p => this.highlightedEntities.push(p));
c.highlighted = true;
}
if (!this.highlightedEntities.includes(c.child)) {
this.highlightedEntities.push(c.child);
this.highlightPath(c.child, 'children');
}
}
});
} else {
this.connectionsByChild.get(entityConf)?.forEach(c => {
c.highlighted = true;
this.highlightPath(c.parent, 'parents');
}
if (!direction || direction === 'parents') {
this.connections.forEach(c => {
if (c.passthroughs.includes(entityConf) || c.child === entityConf) {
if (!c.highlighted) {
c.passthroughs.forEach(p => this.highlightedEntities.push(p));
c.highlighted = true;
}
if (!this.highlightedEntities.includes(c.parent)) {
this.highlightedEntities.push(c.parent);
this.highlightPath(c.parent, 'parents');
}
}
});
}
}
Expand All @@ -422,8 +441,7 @@ export class Chart extends LitElement {
}

private _handleMouseEnter(box: Box): void {
this.highlightPath(box.config, 'children');
this.highlightPath(box.config, 'parents');
this.highlightPath(box.config);
// trigger rerender
this.highlightedEntities = [...this.highlightedEntities];
}
Expand Down Expand Up @@ -480,11 +498,11 @@ export class Chart extends LitElement {
return { ...childEntity, state, attributes: { ...childEntity.attributes, unit_of_measurement } };
}
if (entityConf.type === 'passthrough') {
const connections = this.connectionsByParent.get(entityConf);
if (!connections) {
const realConnection = this.connections.find(c => c.passthroughs.includes(entityConf));
if (!realConnection) {
throw new Error('Invalid entity config ' + JSON.stringify(entityConf));
}
return this._getEntityState(connections[0].child);
return this._getEntityState(realConnection.child);
}

let entity = this.states[getEntityId(entityConf)];
Expand All @@ -504,23 +522,22 @@ export class Chart extends LitElement {
return entity;
}

private _findRelatedRealConnection(entityConf: EntityConfigInternal, direction: 'parents' | 'children') {
// find the first parent/child that is type: entity
private _findRelatedRealEntity(entityConf: EntityConfigInternal, direction: 'parents' | 'children') {
let connection: ConnectionState | undefined;
if (entityConf.type === 'passthrough') {
connection = this.connections.find(c => c.passthroughs.includes(entityConf));
} else {
const connections = direction === 'parents' ? this.connectionsByChild.get(entityConf) : this.connectionsByParent.get(entityConf);
if (!connections) {
throw new Error('Invalid entity config ' + JSON.stringify(entityConf));
}
// a deep search on the first connection must be enough
const candidate = direction === 'parents' ? connections[0].parent : connections[0].child;
if (candidate.type !== 'passthrough') {
return connections[0];
}
return this._findRelatedRealConnection(candidate, direction);
}

// find the first parent/child that is not type: passthrough
private _findRelatedRealEntity(entityConf: EntityConfigInternal, direction: 'parents' | 'children') {
const connection = this._findRelatedRealConnection(entityConf, direction);
connection = connections[0];
}
if (connection) {
return direction === 'parents' ? connection.parent : connection.child;
}
return entityConf;
}

static get styles(): CSSResultGroup {
Expand Down Expand Up @@ -569,6 +586,7 @@ export class Chart extends LitElement {
statePerPixelY: this.statePerPixelY,
connectionsByParent: this.connectionsByParent,
connectionsByChild: this.connectionsByChild,
allConnections: this.connections,
onTap: this._handleBoxTap.bind(this),
onDoubleTap: this._handleBoxDoubleTap.bind(this),
onMouseEnter: this._handleMouseEnter.bind(this),
Expand Down
26 changes: 4 additions & 22 deletions src/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function renderBranchConnectors(props: {
statePerPixelY: number;
connectionsByParent: Map<EntityConfigInternal, ConnectionState[]>;
connectionsByChild: Map<EntityConfigInternal, ConnectionState[]>;
allConnections: ConnectionState[];
}): SVGTemplateResult[] {
const { boxes } = props.section;
return boxes
Expand All @@ -22,28 +23,8 @@ export function renderBranchConnectors(props: {
const children = props.nextSection!.boxes.filter(child =>
b.children.some(c => getEntityId(c) === 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;
const connections = getChildConnections(b, children, props.allConnections, props.connectionsByParent).filter(c => {
return c.state > 0;
});
return svg`
<defs>
Expand Down Expand Up @@ -78,6 +59,7 @@ export function renderSection(props: {
statePerPixelY: number;
connectionsByParent: Map<EntityConfigInternal, ConnectionState[]>;
connectionsByChild: Map<EntityConfigInternal, ConnectionState[]>;
allConnections: ConnectionState[];
onTap: (config: Box) => void;
onDoubleTap: (config: Box) => void;
onMouseEnter: (config: Box) => void;
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export interface Box {
connections: {
parents: Connection[];
};
connectedParentState: number;
}

export interface SectionState {
Expand All @@ -193,6 +194,7 @@ export interface ConnectionState {
ready: boolean;
calculating?: boolean;
highlighted?: boolean;
passthroughs: EntityConfigInternal[];
}

export interface NormalizedState {
Expand Down
23 changes: 16 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Config,
Connection,
ConnectionState,
EntityConfigInternal,
EntityConfigOrStr,
SankeyChartConfig,
Section,
Expand Down Expand Up @@ -61,25 +62,33 @@ export function getEntityId(entity: EntityConfigOrStr | ChildConfigOrStr): strin
return typeof entity === 'string' ? entity : entity.entity_id;
}

export function getChildConnections(parent: Box, children: Box[], connections?: ConnectionState[]): Connection[] {
export function getChildConnections(parent: Box, children: Box[], allConnections: ConnectionState[], connectionsByParent: Map<EntityConfigInternal, ConnectionState[]>): Connection[] {
// @NOTE don't take prevParentState from connection because it is different
let prevParentState = 0;
let state = 0;
const childConnections = connectionsByParent.get(parent.config);
return children.map(child => {
const connection = connections?.find(c => c.child.entity_id === child.entity_id);
if (!connection) {
throw new Error(`Missing connection: ${parent.entity_id} - ${child.entity_id}`);
let connections = childConnections?.filter(c => c.child.entity_id === child.entity_id);
if (!connections?.length) {
connections = allConnections
.filter(c => c.passthroughs.includes(child) || c.passthroughs.includes(parent.config));
if (!connections.length) {
throw new Error(`Missing connection: ${parent.entity_id} - ${child.entity_id}`);
}
}
const { state, prevChildState } = connection;
state = connections.reduce((sum, c) => sum + c.state, 0);
if (state <= 0) {
// only continue if this connection will be rendered
return { state } as Connection;
}
const startY = (prevParentState / parent.state) * parent.size + parent.top;
prevParentState += state;
const startSize = Math.max((state / parent.state) * parent.size, 0);
const endY = (prevChildState / child.state) * child.size + child.top;
const endY = (child.connectedParentState / child.state) * child.size + child.top;
const endSize = Math.max((state / child.state) * child.size, 0);

child.connectedParentState += state;

return {
startY,
startSize,
Expand All @@ -88,7 +97,7 @@ export function getChildConnections(parent: Box, children: Box[], connections?:
endSize,
endColor: child.color,
state,
highlighted: connection.highlighted,
highlighted: connections.some(c => c.highlighted),
};
});
}
Expand Down

0 comments on commit e539913

Please sign in to comment.