Skip to content

Commit

Permalink
feat(#104): Drilldown / Zoom action
Browse files Browse the repository at this point in the history
  • Loading branch information
MindFreeze committed Oct 1, 2023
1 parent e84c9e1 commit 71354bd
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 32 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ This card is intended to display connections between entities with numeric state
| color_below | string | **Optional** | var(--primary-color)| Color for state value below color_limit
| add_entities | list | **Optional** | | Experimental. List of entity ids. Their states will be added to this entity, showing a sum.
| subtract_entities | list | **Optional** | | Experimental. List of entity ids. Their states will be subtracted from this entity's state
| tap_action | action | **Optional** | more-info | Home assistant action to perform on tap. Supported action types are `more-info`, `navigate`, `url`, `toggle`, `call-service`, `fire-dom-event`
| tap_action | action | **Optional** | more-info | Home assistant action to perform on tap. Supported action types are `more-info`, `zoom`, `navigate`, `url`, `toggle`, `call-service`, `fire-dom-event`

### Entity types

Expand Down Expand Up @@ -204,6 +204,10 @@ Currently this chart just shows historical data based on a energy-date-selection

**A:** The easiest way is to do it with a template sensor in HA. However it can be done in the chart without a new HA entity. If you have an entity with `type: remaining_parent_state` and it is the only child of its parents, it will just be a sum of all the parents. Similarly if you have an entity with `type: remaining_child_state` and it is the only parent of all its children, it will be a sum of all the children.

**Q: How do I zoom back out after using the zoom action?**

**A:** Tap the same (currently top level) entity again to reset the zoom level.

## Development

1. `npm i`
Expand Down
69 changes: 69 additions & 0 deletions __tests__/zoom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Config } from '../src/types';
import { filterConfigByZoomEntity } from '../src/zoom';

const config = {
type: '',
sections: [
{
entities: [
{
entity_id: 'ent1',
children: ['ent2', 'ent3'],
},
],
},
{
entities: [
{
entity_id: 'ent2',
children: ['ent4'],
},
{
entity_id: 'ent3',
children: ['ent5'],
},
],
},
{
entities: [
{
entity_id: 'ent4',
children: [],
},
{
entity_id: 'ent5',
children: [],
},
],
},
],
} as Config;

describe('zoom action', () => {
it('filters a config based on zoom entity', async () => {
expect(filterConfigByZoomEntity(config, config.sections[1].entities[0])).toEqual({
type: '',
sections: [
{
entities: [
{
entity_id: 'ent2',
children: ['ent4'],
},
],
},
{
entities: [
{
entity_id: 'ent4',
children: [],
},
],
},
],
});
});
it('returns the same config when there is no zoom entity', async () => {
expect(filterConfigByZoomEntity(config, undefined)).toEqual(config);
});
});
37 changes: 15 additions & 22 deletions src/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import styles from './styles';
import { formatState, getChildConnections, getEntityId, normalizeStateValue, renderError } from './utils';
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
import { handleAction } from './handle-actions';
import { filterConfigByZoomEntity } from './zoom';

@customElement('sankey-chart-base')
export class Chart extends LitElement {
Expand All @@ -31,13 +32,14 @@ export class Chart extends LitElement {
@state() private entityStates: Map<EntityConfigInternal, NormalizedState> = new Map();
@state() private highlightedEntities: EntityConfigInternal[] = [];
@state() private lastUpdate = 0;
@state() public zoomEntity?: EntityConfigInternal;

// https://lit.dev/docs/components/lifecycle/#reactive-update-cycle-performing
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.config) {
return false;
}
if (changedProps.has('forceUpdateTs')) {
if (changedProps.has('config') || changedProps.has('forceUpdateTs') || changedProps.has('highlightedEntities') || changedProps.has('zoomEntity')) {
return true;
}
const now = Date.now();
Expand All @@ -52,12 +54,7 @@ export class Chart extends LitElement {
}, now - this.lastUpdate);
return false;
}
if (changedProps.has('highlightedEntities')) {
return true;
}
if (changedProps.has('config')) {
return true;
}

const oldStates = changedProps.get('states') as HomeAssistant | undefined;
if (!oldStates) {
return false;
Expand Down Expand Up @@ -224,7 +221,8 @@ export class Chart extends LitElement {

private _calcBoxes() {
this.statePerPixelY = 0;
this.sections = this.config.sections
const filteredConfig = filterConfigByZoomEntity(this.config, this.zoomEntity);
this.sections = filteredConfig.sections
.map(section => {
let total = 0;
const boxes: Box[] = section.entities
Expand Down Expand Up @@ -379,10 +377,14 @@ export class Chart extends LitElement {
}
}

private _handleBoxClick(box: Box): void {
private _handleBoxTap(box: Box): void {
handleAction(this, this.hass, box.config, 'tap');
}

private _handleBoxDoubleTap(box: Box): void {
handleAction(this, this.hass, box.config, 'double_tap');
}

private _handleMouseEnter(box: Box): void {
this.highlightPath(box.config, 'children');
this.highlightPath(box.config, 'parents');
Expand All @@ -397,13 +399,6 @@ export class Chart extends LitElement {
});
}

// private _handleAction(ev: ActionHandlerEvent): void {
// console.log('@TODO');
// if (this.hass && this.config && ev.detail.action) {
// // handleAction(this, this.hass, this.config, ev.detail.action);
// }
// }

private _getEntityState(entityConf: EntityConfigInternal) {
if (entityConf.type === 'remaining_parent_state') {
const connections = this.connectionsByChild.get(entityConf);
Expand Down Expand Up @@ -510,7 +505,8 @@ export class Chart extends LitElement {
<div class=${'box type-' + box.config.type!} style=${styleMap({ height: box.size + 'px' })}>
<div
style=${styleMap({ backgroundColor: box.color })}
@click=${() => this._handleBoxClick(box)}
@click=${() => this._handleBoxTap(box)}
@dblclick=${() => this._handleBoxDoubleTap(box)}
@mouseenter=${() => this._handleMouseEnter(box)}
@mouseleave=${this._handleMouseLeave}
title=${name}
Expand Down Expand Up @@ -616,11 +612,6 @@ export class Chart extends LitElement {

this.lastUpdate = Date.now();

// @action=${this._handleAction}
// .actionHandler=${actionHandler({
// hasHold: hasAction(this.config.hold_action),
// hasDoubleClick: hasAction(this.config.double_tap_action),
// })}
return html`
<ha-card label="Sankey Chart" .header=${this.config.title}>
<div class=${containerClasses} style=${styleMap({ height: this.config.height + 'px' })}>
Expand All @@ -634,3 +625,5 @@ export class Chart extends LitElement {
}
}
}

export default Chart;
21 changes: 13 additions & 8 deletions src/handle-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HomeAssistant, fireEvent, forwardHaptic, navigate, toggleEntity } from "custom-card-helpers";
import { ActionConfigExtended } from "./types";
import type { EntityConfigInternal } from "./types";
import type Chart from "./chart";

interface ToastActionParams {
action: () => void;
Expand All @@ -15,14 +16,9 @@ const showToast = (el: HTMLElement, params: ShowToastParams) =>
fireEvent(el, "hass-notification", params);

export const handleAction = async (
node: HTMLElement,
node: Chart,
hass: HomeAssistant,
config: {
entity_id: string;
hold_action?: ActionConfigExtended;
tap_action?: ActionConfigExtended;
double_tap_action?: ActionConfigExtended;
},
config: EntityConfigInternal,
action: string
): Promise<void> => {
let actionConfig = config.tap_action;
Expand Down Expand Up @@ -119,6 +115,15 @@ export const handleAction = async (
}
case "fire-dom-event": {
fireEvent(node, "ll-custom", actionConfig);
break;
}
case "zoom": {
if (node.zoomEntity === config) {
node.zoomEntity = undefined;
break;
}
node.zoomEntity = config;
break;
}
}
};
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface EntityConfig {
color_below?: string;
color_limit?: number;
tap_action?: ActionConfigExtended;
double_tap_action?: ActionConfigExtended;
hold_action?: ActionConfigExtended;
// @deprecated
remaining?:
| string
Expand All @@ -53,7 +55,7 @@ export type EntityConfigInternal = EntityConfig & {

export type EntityConfigOrStr = string | EntityConfig;

export type ActionConfigExtended = ActionConfig | CallServiceActionConfig | MoreInfoActionConfig;
export type ActionConfigExtended = ActionConfig | CallServiceActionConfig | MoreInfoActionConfig | ZoomActionConfig;

export interface MoreInfoActionConfig extends BaseActionConfig {
action: 'more-info';
Expand All @@ -63,6 +65,10 @@ export interface MoreInfoActionConfig extends BaseActionConfig {
};
}

export interface ZoomActionConfig extends BaseActionConfig {
action: 'zoom';
}

export interface CallServiceActionConfig extends BaseActionConfig {
action: 'call-service';
service: string;
Expand Down
21 changes: 21 additions & 0 deletions src/zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Config, EntityConfigInternal } from "./types";

export function filterConfigByZoomEntity(config: Config, zoomEntity?: EntityConfigInternal) {
if (!zoomEntity) {
return config;
}
let children: string[] = [];
const newSections = config.sections.map(section => {
const newEntities = section.entities.filter(entity => entity === zoomEntity || children.includes(entity.entity_id));
children = newEntities.flatMap(entity => entity.children);
return {
...section,
entities: newEntities,
};
}).filter(section => section.entities.length > 0);

return {
...config,
sections: newSections,
};
}

0 comments on commit 71354bd

Please sign in to comment.