Compare commits

...
Author SHA1 Message Date
Maarten Lakerveld e7d3a5c2b7 Reuse cluster bubble avatars across rebuilds
Cluster bubbles are rebuilt whenever the camera settles or an entity
changes state, and each rebuild created new ha-entity-marker elements.
Pictures were re-decoded, the entity state subscription was torn down
and set up again, and styles applied to a marker by styling tools were
lost on every pan or zoom.

Avatars are now cached per entity id and moved into the new bubble.
An avatar still on screen is left alone: Leaflet's markercluster keeps
the outgoing bubble visible during its zoom animation, and taking its
avatar would empty it mid-fade. Avatars of entities no longer shown are
dropped when entities are redrawn.
2026-09-21 11:42:24 +01:00
Maarten Lakerveld cdb68d0b56 Expose CSS parts on ha-entity-marker
Styling tools can target ::part(marker) and ::part(picture) from the
map's scope, so a style applies to markers as they are created instead
of being re-injected into each new element. Documents the marker's
custom properties and parts as its styling surface.
2026-09-21 11:42:24 +01:00
2 changed files with 37 additions and 4 deletions
+6
View File
@@ -6,6 +6,10 @@ import { consumeEntityState } from "../../common/decorators/consume-context-entr
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
/**
* @csspart marker - The framed avatar.
* @csspart picture - The entity picture inside the frame.
*/
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement {
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
@@ -29,6 +33,7 @@ class HaEntityMarker extends LitElement {
protected render() {
return html`
<div
part="marker"
class="marker ${this.entityPicture ? "picture" : ""}"
style=${styleMap({ "--ha-marker-selected-color": this.entityColor })}
@click=${this._badgeTap}
@@ -36,6 +41,7 @@ class HaEntityMarker extends LitElement {
${
this.entityPicture
? html`<div
part="picture"
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.entityPicture})`,
+31 -4
View File
@@ -350,6 +350,11 @@ export class HaMap extends ReactiveElement {
private _entityHandles: MapMarkerHandle[] = [];
private _clusterAvatars = new Map<
string,
HTMLElementTagNameMap["ha-entity-marker"]
>();
private _zoneHandles: MapItemHandle[] = [];
private _pathHandles: MapItemHandle[] = [];
@@ -412,6 +417,7 @@ export class HaMap extends ReactiveElement {
this._startingEngine = undefined;
this._loading = false;
this._entityHandles = [];
this._clusterAvatars.clear();
this._zoneHandles = [];
this._pathHandles = [];
this._removeEditableLocations();
@@ -1124,6 +1130,7 @@ export class HaMap extends ReactiveElement {
this._focusZonePoints = [];
if (!this.entities) {
this._clusterAvatars.clear();
engine.setClustering(null);
return;
}
@@ -1326,6 +1333,13 @@ export class HaMap extends ReactiveElement {
}
}
const shownIds = new Set(this.entities.map(getEntityId));
for (const entityId of this._clusterAvatars.keys()) {
if (!shownIds.has(entityId)) {
this._clusterAvatars.delete(entityId);
}
}
engine.setClustering(
this.clusterMarkers
? {
@@ -1353,7 +1367,19 @@ export class HaMap extends ReactiveElement {
const bubble = document.createElement("div");
bubble.className = "cluster-bubble";
for (const member of shown) {
const avatar = document.createElement("ha-entity-marker");
let avatar = member?.entityId
? this._clusterAvatars.get(member.entityId)
: undefined;
// Leaflet keeps the outgoing bubble on screen during its zoom animation
if (avatar && (avatar.isConnected || avatar.parentElement === bubble)) {
avatar = undefined;
}
if (!avatar) {
avatar = document.createElement("ha-entity-marker");
if (member?.entityId) {
this._clusterAvatars.set(member.entityId, avatar);
}
}
avatar.entityId = member?.entityId;
avatar.entityName = member?.label ?? "";
avatar.entityUnit = member?.unit ?? "";
@@ -1366,10 +1392,11 @@ export class HaMap extends ReactiveElement {
member?.color ?? "var(--primary-color)"
);
avatar.style.setProperty("--ha-marker-border-width", "2px");
} else {
avatar.style.removeProperty("--ha-marker-color");
avatar.style.removeProperty("--ha-marker-border-width");
}
if (member?.selected) {
avatar.selected = true;
}
avatar.selected = member?.selected ?? false;
bubble.appendChild(avatar);
}