powerflow
v0.1.4
Published
Animated, framework-agnostic SVG energy-flow diagram — solar, grid, home, battery and wallbox with power-proportional flow animation. Ships as a <power-flow> Web Component and a vanilla API; works with React, Angular, Vue or plain HTML.
Maintainers
Readme
⚡ powerflow
Animated, framework-agnostic SVG energy-flow diagram.
Live power flow between solar, grid, home, battery and two wallboxes — with dots whose speed is proportional to the actual power.
It ships as a <power-flow> Web Component, so it works natively in React,
Angular, Vue, Svelte or plain HTML — plus a tiny vanilla API. No canvas, just
crisp scalable vectors; no runtime framework dependency.
- Optional nodes — solar, battery and both wallboxes appear automatically when you pass their values; empty rows are trimmed so there's no dead space.
- Power-proportional animation — dot speed scales with watts and stays smooth (no jumping) as values update live.
- Consistent flow math — each source is split across its sinks with no double-counting, modelled after Home Assistant's power-flow-card-plus.
- Coverage rings — home ring shows load sources (solar / battery / grid); grid ring shows export sources; battery ring shows state of charge.
- Themeable — every node colour (including separate charge/discharge colours for battery and grid), every label, and every node icon is overridable.
- Adjustable animation — dot speed multiplier lets you slow down or speed up the flow independently of the power values.
- Tiny & isolated — ~7 kB min+gzip, zero runtime deps, shadow DOM so its styles never leak into your app.
Install
npm install powerflow…or straight from a CDN, no build step:
<script type="module" src="https://unpkg.com/powerflow"></script>Quick start (any framework / plain HTML)
<script type="module">
import 'powerflow'; // registers the <power-flow> element
</script>
<power-flow id="pf"></power-flow>
<script type="module">
const pf = document.getElementById('pf');
pf.data = {
solar: 3000, // PV production (W); omit/null hides the node
grid: -600, // grid power: positive = import, negative = export
load: 2400, // total house consumption (W)
battery: -500, // negative = charging, positive = discharging; omit/null hides
batterySoc: 72, // state of charge in % (optional, shows SoC ring)
wallbox: 3600, // EV charger below the house (optional)
wallbox2: 3600, // second EV charger above the house (optional)
};
</script>data, colors, labels, icons and speedScale are set as JS
properties. In plain HTML you can also pass data, colors and labels as
JSON attributes:
<power-flow data='{"solar":2400,"grid":-600,"load":1800}'></power-flow>.
Framework usage
import 'powerflow';
import { useRef, useEffect } from 'react';
export function Energy({ data }) {
const ref = useRef(null);
useEffect(() => {
ref.current.data = data;
}, [data]);
return <power-flow ref={ref} />;
}React ≥ 19 also lets you pass data={data} directly.
Add CUSTOM_ELEMENTS_SCHEMA to your module/component, import "powerflow";
once, then:
<power-flow [data]="data" [colors]="colors"></power-flow>import "powerflow"; once, tell Vue the tag is a custom element
(compilerOptions.isCustomElement), then:
<power-flow :data="data" :labels="labels" />import { createPowerFlow } from 'powerflow';
const pf = createPowerFlow(document.getElementById('box'), { data });
pf.update({ data: nextData }); // cheap, call as often as you like
pf.destroy();The diagram renders into a shadow root on the host element, so its styles never leak into your app.
API
| Property / option | Type | Description |
| ----------------- | --------------------- | --------------------------------------------------------- |
| data | FlowData | Live power readings (watts). |
| colors | Partial<FlowColors> | Override any accent colour. |
| labels | Partial<FlowLabels> | Override node labels (i18n). |
| icons | Partial<FlowIcons> | Override node icons (any SVG <path d=""> string). |
| speedScale | number | Dot speed multiplier. 1 = default, 2 = twice as fast. |
FlowData
| Field | Type | Description |
| ------------ | ---------------- | -------------------------------------------------------- |
| solar | number \| null | Solar / PV production (≥ 0). Optional. |
| grid | number | Grid power. Positive = import, negative = export. |
| load | number | Total house consumption (≥ 0). |
| battery | number \| null | Positive = discharging, negative = charging. Optional. |
| batterySoc | number \| null | Battery state of charge in percent. Optional. |
| wallbox | number \| null | EV charger consumption, drawn below the house. Optional. |
| wallbox2 | number \| null | Second EV charger, drawn above the house. Optional. |
Only
gridandloadare required. Omitting (or passingnullfor)solar/battery/wallbox/wallbox2hides that node, and the diagram trims the now-empty row so there's no dead space. Both wallboxes are sub-consumers ofload, not extra load on top of it.
colors
{
solar: "#fcd34d", // amber-yellow
home: "#818cf8", // periwinkle
gridIn: "#60a5fa", // sky blue — importing from grid
gridOut: "#f472b6", // pink — exporting to grid
batteryIn: "#4ade80", // lime green — charging
batteryOut: "#fb923c", // orange — discharging
wallbox: "#22d3ee", // cyan
wallbox2: "#2dd4bf", // teal
}labels (i18n)
Defaults are English. Override per language, e.g.
{ grid: "Netz", home: "Haus", battery: "Akku" }.
icons
Each value is a valid SVG <path d="…"> string. The defaults use
Material Design Icons, but any
SVG path drawn in a 24×24 viewBox works:
import { mdiSolarPanel, mdiFlash } from '@mdi/js';
pf.icons = {
solar: mdiSolarPanel, // swap the default solar-power-variant icon
grid: mdiFlash, // swap the transmission tower
// home / battery / wallbox / wallbox2 — all optional
};speedScale
Multiplies the base dot speed for all animated legs. The base speed is already
proportional to power, so speedScale lets you tune the visual intensity
without changing the underlying data:
pf.speedScale = 0.5; // half speed — calmer animation
pf.speedScale = 2; // twice as fast — more energetic feelHow the flows are computed
Meters only tell you the net at each node, so powerflow decomposes them into
the individual legs by priority — every source is split across the sinks it
feeds, with nothing double-counted:
- a charging battery is fed from solar first (the rest from the grid),
- remaining solar serves the house, then exports,
- a discharging battery covers the house's remaining demand, then exports,
- the grid covers whatever the house still needs.
This matches the convention of
power-flow-card-plus, so e.g.
solar 1000 W, load 1000 W, battery charging 100 W, grid +100 W correctly shows
solar→battery 100, solar→home 900 and grid→home 100 — not a single solar→home
line.
Development
npm install
npm run dev # playground at localhost:5173 — sliders, test cases, simulate day
npm run build # build:lib + build:site
npm run build:lib # → dist/ publishable library (JS bundles + .d.ts)
npm run build:site # → dist-site/ static playground (GitHub Pages)
npm run capture:gif # re-generate docs/preview.gif (requires ffmpeg + chromium)Credits
Inspired by
power-flow-card-plus by
@flixlix — the excellent Home Assistant card.
powerflow reuses its flow-allocation conventions but is a standalone,
framework-agnostic Web Component with no Home Assistant dependency.
License
MIT © Thomas Mihailovits
