Skip to content

feat: 21 new charts and options (marimekko, chord, battery, spiral, strip, cycle, badge, energy source, transforms, units, export and more) - #119

Merged
tempus2016 merged 63 commits into
mainfrom
integrate/wave3
Nov 11, 2026
Merged

tempus2016 merged 63 commits into
mainfrom
integrate/wave3

Conversation

@tempus2016

Copy link
Copy Markdown
Owner

Twenty-one new charts and options, built and tested together. Every option is in the visual editor and every new string is translated into all six languages.

New charts

  • Marimekko (style: marimekko): columns as wide as each group's total, split by category.
  • Chord (style: chord): flows between nodes round a circle; reads the Sankey's from/to.
  • Battery grid (style: battery): every battery as a filled glyph, coloured by band.
  • Spiral (style: spiral): one turn per day or week, so daily rhythm lines up.
  • Strip plot (style: strip_plot): every reading as a dot, with a median or mean line.
  • Cycle plot (style: cycle_plot): a small panel per weekday, hour or day of the month.
  • Badge (type: custom:chartdeck-badge): value, sparkline, ring or thin bar in the badge row.

Added to existing charts

  • Gauge history (ghost: 24h | 7d): the recent range and mean behind the needle, arc and ring gauges.
  • Error bars (error_bars: std | min_max | ci95) on grouped column charts.
  • Forecast (per entity forecast:): a weather or Solcast forecast drawn dashed after now.
  • Anomalies (anomalies: true): readings far from normal are marked.
  • Earlier spans (compare: [1d, 7d]): yesterday or last week faintly behind today, aligned by wall clock.
  • Stepped threshold (annotation history: true, zone, severity): a threshold line that follows an entity's history.
  • Now line (now_marker: line | time | countdown) on line and column charts.
  • Legend toggles (legend_toggle: true): tap a name to hide a series, hold to show only it.

Data options

  • Running totals (per entity transform: cumulative, reset day/week/month/billing day).
  • Rate of change (per entity transform: derivative, per s/min/h/d, with smoothing).
  • Unit conversion: an entity's unit now converts its value, history, limits and bands (energy, power, temperature, speed, pressure, length). Limits and bands read from another entity now convert from that entity's unit.
  • Conditional series (when: {entity, state | above | below}, per entity or card): keep readings only while a condition holds, with an optional faint copy and condition strip.
  • Energy dashboard (source: energy, energy_flows, energy_split): line, column, area, pie and Sankey read the Energy dashboard's settings, no entities needed.
  • Export menu (export: true): save as PNG or SVG, copy CSV or YAML.

Fixed along the way

  • Statistics charts no longer blank and redraw when the dashboard moves the card.
  • Strip plot and beeswarm show a sensor that held one value all period.
  • Timing-dependent tests pin the clock.

Tested: 1508 tests, coverage above every threshold, dist/ matches a fresh build, and every chart checked on a test Home Assistant (2026.11) with real history, statistics and Energy settings.

The tooltip, sync, wind rose and extremes tests build their history
relative to Date.now() when the file loads, but the chart's span ends at
Date.now() when it renders. On a busy machine seconds pass between the two,
so a reading 12 h old lands at 49.99% instead of 50%, the synced guides
drift off 83.33%, the wind rose shares and the time-weighted average shift,
and the synced period label can roll over a minute. Fake only Date and pin
it to 15:00 local time in those files; timers stay real.

Pinning the clock showed the sync test only passed because time moved:
half way across was a dead heat between the readings 4 h and 20 h ago,
broken towards the newer one by the drift. Point at 0.6 instead, which is
clearly nearer the 4 h readings.

The draw-in test relies on the move happening within the 1 s draw-in; under
load the two animation frames can take longer than that, so hold
performance.now() still for the test.

Under load some tests that mount several charts take longer than vitest's
default 5 s per-test timeout without anything being wrong, so raise it to
30 s.
…gory

New style "marimekko" (Parts of a total). Each column is one group and is as
wide as that group's share of the whole; inside it, each entity is a segment
as tall as its share of the column. Two questions in one chart: which room
uses most, and what for.

A column comes from the entity's own "group", or from the card's "group_by"
(floor, area, device, label), read the same way as on the treemap. A segment's
colour comes from its "category" (the entity's name when it has none); the
first own "color" in a category paints it. Categories stack in the order they
are listed, the same way in every column, even when the card sorts entities,
so a colour sits in the same place across columns. The card's "sort" orders
the columns by total.

"segment_labels" (percent, value, both, none; default percent) puts text in a
segment when it fits. Column totals sit above (only when every entity shares a
unit), group names below, and a category legend under that. Entities with no
reading are listed underneath and still open their more-info.

Editor: Chart section has "Text on each part" and "Group by"; each entity's
options have Column (group), Category and Category colour. Switching between
the treemap, sunburst and marimekko keeps a place "group_by". Strings are in
all six languages.
New style "chord" (Parts of a total). Nodes sit round a circle, each arc as
long as everything flowing in and out of it, and each flow is a ribbon joining
its two nodes, as wide as the flow at both ends and coloured by where it
starts (or by the flow's own "color").

It reads the same per-entity "from" and "to" as the Sankey, so a card can
switch between the two and keep its flows. Unlike the Sankey, flows may go
both ways between two nodes and round a loop; a flow from a node to itself is
still an error. Flows with nothing flowing (zero, negative, unavailable) are
left out.

Ribbon ends on each node are stacked by where the other node sits round the
circle, so ribbons don't cross where they need not. The card's "sort" orders
the nodes by total. "pad" sets the gap between nodes in degrees (0 to 20,
default 3) and "ribbon_opacity" how solid the ribbons are (default 0.35 in a
light theme, 0.45 in a dark one). Each node is labelled outside its arc with
its name and total (Display switches), cut short on a narrow card.

Hovering or focusing a ribbon lights it and shows "from -> to value";
hovering a node's arc lights all its ribbons. Tapping a ribbon opens its
entity.

Editor: Chart section has "Gap between nodes" and "Ribbon opacity"; each
entity's options have Flows from, Flows to and Flow colour, as on the Sankey.
Strings are in all six languages.
New style "battery" (Battery grid, in Bars). Each entity is a battery glyph
filled to its level, with the value and name underneath, in a grid. It
replaces a long bar list for the 20 to 40 battery sensors a house has, and
pairs well with "include: {device_class: battery}".

Colour follows the card's or entity's severity bands; with none set it is red
below 20, amber below 40 and green above. The level is the entity's place
between min and max (0 to 100 by default). An unavailable or missing entity
shows an empty dashed battery and why it has no reading, even with values
hidden. Tapping a battery opens its more-info.

"columns" (Display) is a maximum, as for the tile styles: tiles keep a minimum
width and wrap to fewer on a narrow card; unset, as many fit as there is room
for. "sort" (Display) orders them; picking the Battery grid in the editor sets
"sort: ascending" when no sort is set, so the emptiest come first. YAML
without "sort" keeps the order listed, like every other style, so "As listed"
stays selectable in the editor.

The fill grows in on first draw and stays still under reduced motion.
Strings are in all six languages.
A new history chart, `style: spiral`. Each entity's history winds out
from the middle, one turn per day (or per week with `turn: week`), the
oldest turn innermost and the present outermost. Every hour is a stretch
of the ring shaded by its value, so anything on a daily (weekly) rhythm
lines up along one radius: the evening peak sits at the same angle every
turn, and drift or a one-off day stands out.

How it works: the span is `turns` days or weeks back from now (7 days or
4 weeks by default; up to 31 days or 13 weeks), read an hour at a time
through the usual history frame, so the source (history or statistics),
how each hour is combined (`aggregate`), `period.offset` and the
previous / next buttons all work as on the other history charts. The
spiral sets the span, so `period.last`, `unit` and calendar spans are
ignored. Position along the spiral follows the wall clock in Home
Assistant's time zone: midnight is always at the top (the first weekday
for week turns) and a day across a clock change is still exactly one
turn. Colours reuse the heatmap's scale (faint to full entity colour,
`color_min` / `color_max` to fix the ends) or the colour bands with
`color_by: bands`. `ring_width` (20-100 %) sets how much of the gap
between turns each ring fills.

Hours with no readings show the faint track, not zero; unavailable and
missing entities draw the empty track. A dot marks now while the span is
live. Hovering or touching a stretch shows its day, hour and value. One
spiral per entity, with its name, value, a note of the span and the
colour legend beside it on a wide card or under it on a narrow one.

In the editor: Style > History charts > Spiral, then in the Chart
section One turn per, Turns, Periods back, the buttons, Source, Combine
with, Colour, Ring width, Lowest / Highest colour at, Legend and the
hover switch. Each entity has a Colour. Strings in all six languages.
A new distribution chart, `style: strip_plot`. One row per entity, one
dot per reading in the period along a shared value axis, scattered
across the row so they don't sit on top of each other, with a line at
the median. Spread and clusters show without a violin's smoothing, and
without the packing cost of the beeswarm on a long history: dots are
placed, not packed, so a row draws up to 2000 of them (more are sampled
evenly, and the row still counts them all).

The scatter across the row comes from each reading's own time (the
scatter chart's jitter hash), not its place in the list, so a dot stays
put as new readings arrive and old ones drop off: redraws don't shimmer.
`jitter` (0-100 %, default 70) is how much of the row the dots spread
over, less the dot itself so it stays inside; 0 puts them all on one
line. `dot_size` is 2-16 px (default 5). `marker` puts the line at the
median (default) or the mean of the readings, or leaves it out; the
line has a halo in the card colour so it reads on top of dense dots.
The row's second line gives that value ("Median 21.4 °C"), or the
count with no line.

Readings are those taken in the period: the value carried in from
before it and the copy at the right edge are not dots. Each counts
once, so the marker sits among the dots it sums up. Hovering a row
shows its count (and dots shown, when sampled), median, average, middle
half and lowest to highest. It shares the beeswarm's rows: across or
upright, value labels and lines, fixed ends, compact, every history
option (period, calendar spans, buttons, statistics, grouping), and
tapping a row opens its entity. Unavailable and missing entities get an
empty row that says so.

In the editor: Style > Distributions > Strip plot, then in the Chart
section the history fields, Line across each row at, Spread across the
row, Dot size (px), Direction, Value labels, Lines at each value label,
Lowest / Highest value shown, the hover switch and Compact. Each entity
has a Colour. Strings in all six languages.
A new history chart, `style: cycle_plot`. A row of small panels on one
value scale, one per part of a cycle, each with its line and a dashed
line at the panel's average, so a pattern a single line hides ("Monday
mornings spike", "Saturdays are flat") reads at a glance.

`cycle` picks the panels:
- day_of_week (default): a panel per weekday, from the user's first
  weekday, its line the average of each hour of that day over the span
  (every Monday 8:00 averaged into one point).
- hour_of_day: a panel per hour, its line that hour on each day of the
  span in turn (Cleveland's cycle plot): how 7:00 drifts through the
  weeks.
- day_of_month: a panel per day of the month, its line that day in each
  month of the span.

`weeks` is the span back from now (8, 2 and 26 by default; up to 26, 13
and 104). Readings are grouped by the hour (by the day for day of the
month) through the usual history frame, so `aggregate` (change for
meters), the source, `period.offset` and the previous / next buttons
work as on the other history charts; day of the month reads long-term
statistics by default, as history rarely keeps months. Everything goes
by the wall clock in Home Assistant's time zone. Hours or days with no
readings are gaps in the line, never zero.

Panels sit in one row when they fit (at least 30 px each), else wrap
onto evened rows with each panel's name in a strip at its top. Value
labels down the left, the unit above them, and under the first panel
what its line runs across (12 AM - 11 PM, or the dates of the span).
With several entities each draws its own line and dashed average in its
colour. Hovering or touching a panel shows that point's time, each
entity's value and the panel's average, with a guide line. The series
header and the legend open their entity.

In the editor: Style > History charts > Cycle plot, then in the Chart
section One panel per, Weeks, Periods back, the buttons, Source (its
default follows the cycle), Combine with and the hover switch. Each
entity has a Colour. Strings in all six languages.
…ring gauges

A gauge shows where a value is now, but not whether now is usual. The new
gauge option `ghost` (24h, 7d or off, off by default) draws a faint band
behind the needle, arc and ring gauges from the lowest to the highest
reading of the last 24 hours or 7 days, with a stronger tick at the mean.
Hovering the band says "Last 24 hours: 22 % to 68 %, mean 46 %".

The last 24 hours come from the recorder's history, the mean weighted by
how long each reading held (as the grouped charts do). Seven days come
from the hourly long-term statistics (lowest low, highest high, mean of
the means), which is far less to load; a sensor without statistics, or an
attribute, falls back to its history. What is loaded is shared by every
card on the page per entity and span, and loaded again on the next draw
once it is five minutes old (an hour for 7 days), so there are no timers
to stop. A steady reading still shows as a thin sliver; a missing entity
or a failed load simply draws no ghost.

Value styles could not redraw once data arrived, so their render context
now carries an optional requestUpdate, passed by the card.

In the editor it is "Recent range behind the gauge" in the gauge options
under the style picker, for ring, arc and needle. Strings are translated
into all six languages.
A column of daily means says nothing about whether the day was steady or
all over the place. The column chart's new option `error_bars` (none, std,
min_max or ci95; none by default) draws a whisker over each grouped
column from the spread of the readings inside its group:

- std: the mean plus and minus one standard deviation, each reading
  weighted by how long it held, so the whisker is centred on the column.
- ci95: the mean plus and minus 1.96 standard deviations over the square
  root of the number of readings. Readings follow on from each other, so
  treat it as a way to compare groups rather than a strict interval.
- min_max: the lowest and highest value that held in the group.

A std or ci95 whisker never reaches past the group's lowest or highest
reading, so it can't push a chart of positive values below zero. The value
axis widens to fit the whiskers, and the tooltip adds the whisker's ends.

Whiskers draw on grouped columns, one series or several side by side.
Stacked and range columns get none, and neither do columns that add up
(sum and change, such as a meter's energy per day). std and ci95 need the
mean; min_max also fits min, max, first and last. With long-term
statistics the chart asks for hourly rows and groups them itself, so each
day keeps the rows its spread comes from; min_max then uses the rows' own
lows and highs. To do that, the shared history frame now keeps each
series' readings as loaded and can be asked for hourly statistics.

In the editor it is "Error bars (spread in each column)" in the column
chart's Chart section, shown while columns are grouped. Strings are
translated into all six languages.
Adds `custom:chartdeck-badge`, a badge for the Sections view's badge row
(Home Assistant 2024.8 and later). It shows one entity as a 36 px pill in
the style of Home Assistant's own badges, in one of four looks (`style`):

- value (the default): the value and name beside the entity's icon (or
  its own `icon`) in a circle tinted with the badge's colour, or a dot
  with show_icon: false.
- sparkline: a small line of the last `hours` (24 by default, a week at
  most) of history, then the value.
- ring: a small ring filled to the value's place between min and max.
- thin: a short bar filled the same way.

The badge reads the card's options (min, max, severity bands, unit,
decimals, name, icon, attribute, tap, hold and double-tap actions)
through the card's own config code, so a band or a limit means the same
on both: min and max can be a number or an entity id, and band starts
can read entities too. Its colour is the band's when it has bands, else
its own `color`, else the theme's primary. An unavailable or missing
entity shows its state in grey. It redraws only when its entity (or an
entity a limit or band reads) changes; the sparkline uses the card's
history store and stops when the badge leaves the page. A tap opens
more-info by default.

It ships in the same file as the card and registers itself with
customElements and window.customBadges. It has its own visual editor
covering every option: entity, look, attribute, name, hours (sparkline
only), the name and icon switches and an icon picker (value look only),
unit, decimals and colour; min and max with the card's limit field
(default, a number, or an entity); the card's colour band editor; and
the three actions. Its stub config picks a battery sensor as a ring.
Strings are translated into all six languages.
Setting `unit` on an entity (or on the card) used to change only the
text after the number: `unit: kW` on a sensor reporting W wrote
"1500 kW". Now, when the sensor's own unit and the one asked for are a
pair the card knows, it converts:

- energy: Wh, kWh, MWh, GWh
- power: W, kW, MW
- temperature: °C, °F, K
- speed: m/s, km/h, mph, kn, ft/s
- pressure: Pa, hPa, mbar, kPa, bar, inHg, mmHg, psi
- length: mm, cm, m, in, ft

The value, its history (on every chart that reads it through the
shared history frame, and on the box plot, candlestick and 24-hour
clock), statistics' lowest and highest, and any min, max, target or
colour band start read from another entity are converted. A limit or
band read from an entity converts from that entity's own unit, so a
°F input_number can set the top of a °C bar. Numbers typed into the
card (max: 5, a band from 20) are taken as being in the unit the card
shows and are never converted.

Charts that read units their own way convert too:

- Wind rose: the speed entity's `unit` converts its readings, and the
  default speed bands and calm limit are the ones for the shown unit
  (an m/s sensor shown in km/h gets the km/h bands). Band starts read
  from entities convert from their own unit.
- Climate: a thermostat reports in Home Assistant's unit system, so a
  `unit` of °C, °F or K converts from that: the header's temperature
  and set point, and the history of the room temperature and set
  points.
- Annotation lines and bands whose value is an entity convert from
  that entity's unit to the chart's (its first series' unit), so a
  °F helper draws at the right height on a °C chart.

A unit the card doesn't know, a pair from different families, a
sensor without a unit and an attribute (which has no unit) keep
today's behaviour: the unit is only shown.

Without set decimals, a sensor's display precision moves with the
unit (W at 0 decimals is kW at 3, m/s at 1 is km/h at 0); a sensor
without one uses the shown unit's default.

In the editor, an entity's Unit becomes a dropdown of the units its
sensor converts to; any other text can still be typed.
Any entity on a history chart can now be drawn as its running total
instead of its readings, with `transform: cumulative` on the entity.

A meter (state_class total_increasing) adds up how much it went up,
with resets of the meter handled the same way grouping's `change`
handles them. A `total` meter adds up every change, falls included,
so a net meter can go down. Anything else, a power sensor say, is
integrated over time: each reading counts for as long as it held
(the left Riemann sum, the same weighting as the time-weighted
grouping mean), and the last reading keeps counting up to now. W, kW
and MW give Wh, kWh and MWh; with `unit: kWh` (or Wh, MWh) set on a
sensor reporting a power unit, the total is scaled to that unit.
Other units get "·h".

The total starts at zero at the chart's left edge. With
`transform_reset: day | week | month` it drops back to zero at each
calendar boundary in the card's time zone. `transform_reset: billing`
with `transform_reset_day: 1-28` (1 by default) drops it at midnight
on that day of every month, for a supplier's billing period; days
stop at 28 so every month has one. Gaps in the readings stay gaps in
the line; a meter's total carries on across them. The header,
tooltip and axis show the latest total in the new unit.

It is applied before grouping through one shared step, so every chart
that draws history as numbers gets it: line, column, area, combined,
range area, sparkline, trend, streamgraph, histogram, violin,
beeswarm, heatmap, slope, dumbbell, waterfall, scatter and bubble
through the shared history frame, and the box plot, candlestick and
the 24-hour clock's number rings, which load their own history. On
the 24-hour clock a transformed entity is averaged by default rather
than summed as a meter's change. Charts of states (timeline, the
clock's state rings) skip it. The climate chart and wind rose don't
offer it: a running total of a room temperature drawn against its set
point, or of wind speed sorted into speed bands, means nothing; a
`transform` left in their YAML is ignored.

In the editor it is in each entity's options, in a form of its own
below the chart's per-entity fields: "Transform" (None / Running
total), "Running total starts again" while any entity uses it, and
"Billing day of the month" while one starts again on a billing day.
The strings are in all six languages.
Any entity on a history chart can now be drawn as how fast it changes
instead of its readings, with `transform: derivative` on the entity:
power from an energy meter, degrees per hour from a thermometer,
litres per minute from a water meter, without making a derivative
helper for each one.

Each reading after the first gives the change since the reading
before divided by the time between them, drawn at the later reading
as Home Assistant's derivative helper does. `transform_unit_time`
(s, min, h or d; h by default) sets the time unit. Energy per hour
reads as power (kWh/h is kW, Wh/h W, MWh/h MW); anything else is
shown as "<unit>/<time>", like °C/h.

A meter that only counts up (state_class total_increasing) falling is
a reset: that step is dropped and the next is measured from the new
reading. A gap in the readings stays a gap, and the reading after it
starts a new run. The reading carried in from before the span is
never measured from, since its real time is earlier than the left
edge. The header, tooltip and axis show the latest rate.

Two ways to smooth it. `transform_smoothing: n` averages each rate
with the n - 1 before it. `transform_smoothing_time` (a number and s,
min, h or d, like 15min or 1h, up to 7 days) averages over that much
time instead, each rate weighted by how much of the window it covers,
so a sensor that reports at uneven intervals is smoothed over the
same stretch of time every time; for a meter it reads as how much it
rose in the window. When both are set, the time window is used.

It is applied before grouping through one shared step, so every chart
that draws history as numbers gets it: line, column, area, combined,
range area, sparkline, trend, streamgraph, histogram, violin,
beeswarm, heatmap, slope, dumbbell, waterfall, scatter and bubble
through the shared history frame, and the box plot, candlestick and
the 24-hour clock's number rings, which load their own history. A
meter drawn as its rate no longer counts as a meter for the column,
combined and 24-hour clock charts, so they average it rather than
taking its change. Charts of states skip it. The climate chart and
wind rose don't offer it: a rate of a room temperature drawn against
its set point, or of wind speed sorted into speed bands, means
nothing; a `transform` left in their YAML is ignored.

In the editor it is in each entity's options, in a form of its own:
"Transform" (None / Rate of change), and "Rate per", "Smooth over
readings" and "Smooth over time" while any entity uses it. The
strings are in all six languages.
A line annotation whose value is an entity used to sit at that entity's
current state. With `history: true` it now follows every state the entity
had over the chart's span, drawn as a stepped line (each value held until
the next), so a tariff, a set point or a schedule shows how it changed
against the readings. `zone: above` or `zone: below` shades that side of
the line in the annotation's colour.

Give the annotation its own `severity` bands (from / color, as on
entities) and the zone is shaded step by step in the colour of the band
each value falls in: a tariff's cheap, normal and peak rates in green,
amber and red. The stepped line keeps the annotation's colour. The card's
own severity bands are not used for this: they are in the series' units
(watts, say), while the threshold is usually another quantity (pence per
kWh).

The threshold loads the way the card loads its series: from long-term
statistics on a `source: statistics` card (rows as long as the card's own
when it groups them) and for any span over seven days, so a year on a
chatty entity is a few hundred daily rows rather than every state change;
from history otherwise. An entity without statistics (a set point or
tariff with no state class) falls back to its history, as do all of them
if the statistics fail to load. History is kept up to date from live
states without refetching; statistics reload once a period. Gaps
(unavailable, unknown) break the step, holding the value before the gap
up to its start; the last value is carried on to now. The value axis
widens to every step inside the span. While the readings load, fail or
are empty the line sits at the current state as before, so the chart
never blanks.

In the editor: Annotations, on a line, "Follow the entity's history
(stepped)" and "Shade" (nothing / above / below the line); once a shade
is chosen, "Shade colours by the line's value" below it takes the bands,
with the same presets and rows as the entity bands.
Each entity on a history chart can have a condition, `when: { entity,
state | above | below }`, and is then shown only while it holds: power
while someone is home, flow temperature while the heat pump runs. A state
can be one, a list, or text with commas between; above and below can be
combined, and everything given must hold. Readings outside are gaps. A
`when` on the card applies to every series without one of its own
(including entities added by `include`); a series' own `when` wins.

The condition is applied to the loaded readings before grouping, so
lines, grouped columns and means, highs, lows and averages, and the
tooltip all see the kept readings only. Where the condition starts to
hold, the value then in force is repeated (marked as carried, so it is
not counted as a new reading); where it stops, the line runs on to that
moment and breaks. Unavailable or unknown condition states count as not
met. The condition entity's history loads through a second history store
per card and keeps up with live states; its changes redraw the card. The
chart shows Loading until the condition history is in, and a series whose
condition cannot be loaded is hidden rather than shown unfiltered. With
statistics, an hourly row is kept when the condition held at its start.

It works on every chart that loads history through the shared frame and
also on the box plot, candlestick, day clock (time on, switch-ons and
value slots count only the time while it held) and wind rose (direction
and speed each masked; a gap in either drops that stretch from the rose).
The climate chart does not take it: it is one thermostat's own record,
and its heating band already says when it ran.

Two drawing options on line and area charts, both off by default:
`when_ghost: true` draws every reading as a faint line behind each
conditional series (area: unstacked only; small multiples too), with the
value axis fitted to it so it stays on the plot; `when_strip: true` adds a
strip under the plot, one row per distinct condition, filled where it held
across the visible span in the series' colour and labelled ("John: Home",
"Heat pump power > 50"). With the zoom overview both stack under the plot,
and the plot keeps its height.

In the editor: each entity's options on those charts have "Show only
while this entity", "is in state", "is above" and "is below", in a form
of their own. The Chart section has the card's condition ("Show every
series only while this entity" and the same three fields) and, on line
and area charts, "Faint line of the hidden readings" and "Strip showing
when it held".
A series on the line chart can now name a forecast. The chart draws it
dashed in the series' colour from now on, and a span that ends now (a
rolling span, or a calendar span drawn to now) stretches its time axis
past now to the last forecast reading, at most forecast_hours ahead
(24 by default). A span that already runs to the end of today keeps its
length; a past span shows no forecast.

The forecast comes from an attribute of another entity: a list of
objects (Solcast's detailedForecast), a list of [time, value] pairs, or
an object of time: value (Forecast.Solar's watts). Time and value keys
are guessed from common names (datetime, period_start, start...; value,
pv_estimate, watts, price, temperature...) unless value_key and
time_key say otherwise, and multiply scales the values (1000 turns kW
into W). A weather entity with no attribute is read through Home
Assistant's weather/subscribe_forecast websocket subscription (hourly
unless type says daily or twice_daily), or its old forecast attribute
where it still has one. The subscription is kept per card, closed when
the forecast is removed or the card leaves the page.

The drawn forecast starts at now with its value there, read between the
points either side, so slots before now are left to the history. The
value axis fits it, a small "Forecast" label marks where it begins, and
the tooltip lists "<name> forecast" (a dash before it starts). Zoom and
the log scale apply; small multiples do not draw forecasts.

In the editor each series' options end with a folding Forecast section
(entity, attribute, weather type, value key, time key, multiply), and
the Chart section shows "Show forecasts this far ahead" once any series
has one. A forecast without an entity yet is ignored, not an error.
With anomalies: true, the line chart judges each reading against the
other readings within a time window centred on it, and marks those
more than anomalies_sigma deviations out (3 by default) with a dot. Up
to three get a label such as "+4.2σ", the furthest out first; past ten
deviations it reads "over +10σ" or "under −10σ" (in the user's
language). Marks close together (a spike a few readings wide, or two
series spiking at once near each other) share one label, on the
furthest out, and the rest keep only their dot. A label is drawn only
where it fits inside the plot clear of every dot, the other labels and
a header laid over the plot; otherwise it is left out and the dot still
names its value on hover and for screen readers.

The band of normal (typical value plus or minus sigma deviations) is
shaded behind each series in its colour unless anomalies_band is false;
anomalies_color sets the markers' colour (red by default).

The window is anomalies_window (30min, 2h, 1d, 1w, or a number of
minutes); by default it is a twelfth of the span drawn. The typical
value and spread are the median and 1.4826 times the median absolute
deviation, the robust forms of the mean and standard deviation, so a
spike a few readings wide cannot widen its neighbours' band and hide
itself. The spread never drops below the series' reading-to-reading
noise, so a short window with a few neighbours that agree by chance
does not flag ordinary noise, and where most neighbours share one value
the mean absolute deviation stands in. A reading needs five neighbours
and two on each side to be judged: against one side only, every reading
at the start or end of a slope looks out of line. Gaps break the band.

Each marker is labelled for screen readers ("Humidity: 95 %, +4.2σ
from normal"). Zoom and the log scale apply; small multiples do not
draw anomalies.

In the editor, the Chart section has "Mark readings far from normal"
after the average setting; once on it shows the window, how far out to
mark, the marker colour and the band switch.
With compare: [1d, 7d] the line chart also draws its entities over
earlier spans (yesterday, a week ago, a year ago), faint and thin
behind the current lines, moved forward so each earlier moment sits
over the same wall-clock time now. It is an overlay: the chart still
shows its own span, unlike period.offset or the previous / next
buttons, which show an earlier span instead. Stepping back with the
buttons steps the ghosts back too.

Offsets are <n>h (by the clock) or <n>d, <n>w, <n>mo, <n>y (by the
calendar, keeping the time of day), up to four. Each is loaded once
into its own history store per card and reloaded only when the chart's
span has moved on; it is grouped exactly as the chart is (hours, days,
statistics) and carried to its end like the chart's own series. One
that is loading or fails to load is simply left out.

Each point of a ghost moves forward to the same wall-clock time in the
card's time zone, so a day, week or year back still lines up across a
clock change: a reading at noon yesterday sits at noon today even when
the clocks went forward or back in between. <n>h offsets go by the
clock and move by one fixed amount. A time the later day skips (clocks
forward) lands on the moment the clocks jumped; on a day with an hour
that happens twice (clocks back) the second run of that hour is left
out, so a ghost never runs backwards. Days without a clock change on
either side move as one block, so this costs a few time-zone lookups
per day, not per reading.

compare_style: grey (the default) draws them in the secondary text
colour; series draws a faded copy of each series' colour. The first
offset is solid, the next dashed, then dotted and dash-dot. A key under
the header names them (Yesterday, Last week, Last year, or "3 days
earlier"), hidden with the names; the tooltip lists each one's value
at the pointer. The value axis fits them, and zoom and the log scale
apply; small multiples do not draw them.

In the editor the Chart section has "Compare with earlier spans" after
the history settings, a list offering Yesterday, Last week, 28 days
earlier and Last year that also takes any other offset typed in; the
colour choice appears once one is set.
Line and column charts can draw a vertical line at the current time when
their span runs past the present (today, this week or month with "until
the end", or a grouped column chart whose last group is still running).
The line moves once a minute. Its label is the time, or a countdown to
the next set time any time annotation marks ("Peak rate in 1 h 12 m",
"Next in 25 m" when the mark has no label), falling back to the time when
nothing is due in the next two days. `now_marker: off | line | time |
countdown`; off by default.

A rolling span ends at the present, so the line would sit on its right
edge: it is left out there. Each card keeps one timer, set to the start
of the next minute only while the marker is on show and re-armed by the
redraw it triggers; a hidden tab skips the redraw, and the timer is
cleared when the card leaves the page. The marker slides to its new place
unless the card is static (reduced motion or animate: false). Times and
countdowns follow Home Assistant's time zone.

In the editor: the Chart section of the line and column charts, "Now
line".
With `legend_toggle: true`, the names above a history chart become toggles.
Tapping one hides that series and tapping it again brings it back. Holding
one for half a second (or shift-clicking it) shows only that series, and
holding it again shows everything. The chart redraws from the series still
shown, so the value axis, the tooltip, labels and highs and lows all refit to
them, and every series keeps its colour.

The hidden set is kept on the card and in the browser's localStorage under a
key made from the card's config, so a dashboard comes back the way it was
left. Changing the card's config starts again with everything shown. Every
storage call is wrapped, so a private window or full storage only means the
choice lasts until the page reloads.

It works on every chart drawn with the shared header: line, column, area,
combined, range area, histogram, streamgraph, box plot and candlestick. The
candlestick's own entity can't be hidden; its name still opens the entity.
Toggling is off while the header is hidden, split into small multiples, or
shows neither names nor values, so a series can never be hidden with no way
back. With everything hidden the chart says so and keeps the names.

While toggling is on, tapping a name no longer opens more-info; the marks in
the chart still do. The names are buttons with aria-pressed and a hint.

Streamgraph's streams now carry their entity's index (not their position)
and its labels read their layer by position, so both stay right when a
series is left out.

In the editor: Chart section, "Tap a name to hide or show it", under
"Names and values".
A line, column, area, pie or Sankey chart with `source: energy` draws what
Home Assistant's Energy dashboard is set up with: grid import and export,
solar, battery out and in by default, and gas, water and individual devices
when `energy_flows` asks for them. No entity list is needed; any entities
the card does list are drawn after the energy ones.

The card reads the dashboard's settings once per page (`energy/get_prefs`,
one request shared by every card, read again after 30 minutes and retried a
minute after a failure), then the metadata of every statistic they name
(`recorder/get_statistics_metadata`). It takes each flow's statistic id
from the settings (older grid settings with flow_from/flow_to and flat ones
both work, and each id is used once) and draws those as long-term
statistics through the usual `recorder/statistics_during_period` load.

A flow with one statistic is named after the flow ("Solar"); a flow with
several, and each device, keeps its own name: the device name from the
Energy settings, else the entity's, else the statistic's. Colours are Home
Assistant's own energy colours (--energy-solar-color and friends, with its
defaults), so a theme that changes them changes the chart.

A statistic that is not an entity (an integration's external statistic such
as tibber:energy_..., or a meter whose entity is gone) is never treated as
a missing entity: it takes its name and unit from its metadata (kWh for
electricity when that says nothing), shows no "Entity not found" anywhere,
is kept by hide_unavailable, and does nothing when tapped, since there is no
entity to open.

Over time (line, column, area), unless the card says otherwise, it reads
each period's change and groups by hour (the column chart keeps picking its
own grouping for the span). The header shows each flow's total over the
span instead of a meter's lifetime reading. On column and area charts the
energy that leaves the house, grid export and battery charging, is drawn
below the axis, so a stacked column is what came in above zero and what
went out below it, and its tooltip total is the net. Tooltips, labels and
the header still read those flows as positive amounts. `energy_split:
false` stacks every flow upwards as before.

A pie or Sankey draws each flow's total over the card's period (the
statistics' change, read by the hour, by the day past a month). The pie
shares out where the energy came from (grid import, solar, battery out) by
default, or the devices, or any flows ticked; `sort` sorts by total. The
Sankey runs grid import, solar and battery out into the house and out to
grid export, battery charging and each device; whatever the devices don't
account for is drawn as "Untracked use" ("Used at home" with no devices),
so the house node balances. Gas and water can't go in a Sankey. While the
statistics load or if they can't, the card says so.

While the settings load, if they can't be read, or when none of the flows is
set up, the card says so. Other styles refuse the source with a readable
error, as do `energy_flows` and `energy_split` without it.

In the editor: Chart section. On line, column and area, "Readings from"
gains "Energy dashboard"; choosing it also sets hourly change, and a list of
energy flows to tick appears, plus "Export and battery charging below the
axis" on column and area. On pie and Sankey, "Readings from" chooses current
values or the Energy dashboard; the latter shows the span (today when first
chosen) and the flows. Switching back drops the energy options.
…card menu

A chart card with `export: true` gets a ⋮ button at its top right. Its menu
offers Save as PNG, Save as SVG, Copy CSV and Copy YAML. Everything happens
in the browser; nothing is sent anywhere.

The button never covers the chart. With a title it sits in the title row,
centred on the title's text line (measured once ha-card has drawn its
header, so any theme's header height works, and kept inside the row).
Without a title it gets a short row of its own above the chart, so period
buttons, header values and the plot stay clear. Compact charts get a
smaller button.

The picture is drawn from what is on screen: each SVG drawing in the card is
copied with its computed colours, strokes and transforms written onto it (so
theme variables become real colours), and the HTML around it - names,
values, axis labels, columns, swatches, borders - is redrawn as rectangles,
lines and text at the same places, under the card's background and title.
That works for every chart style without code of its own. The PNG is that
SVG drawn onto a canvas at twice the size.

A canvas can't load the page's web fonts, so the card's font is written into
the picture: faces of the card's font family that the page has loaded are
looked up in the page's own @font-face rules and, when their files are on
the same origin as the dashboard (Home Assistant serves Roboto itself) or
already data URLs, embedded as base64. Another host's font is never
fetched. When a font can't be embedded, the text falls back to the
system's font of the closest generic family (sans-serif, serif or
monospace).

The CSV lists what a history chart drew (timestamp, entity, value, unit),
leaving out gaps and values carried in from before the span; a chart of
current values lists each entity's current value. The YAML is the card's
config in block style, as the code editor shows it.

Each browser API is checked before use. Where the clipboard can't be used (a
dashboard served over plain http, or a refused permission) the text is saved
as a file instead, and where a file can't be saved or a canvas drawn the
card says so. Files are named from the title and the date. The menu works
from the keyboard (arrows, Escape) and closes on a press outside it.

In the editor: Display section, "Export menu", shown for chart styles.
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
#	src/chart/frame.ts
With `legend_toggle: true`, tapping a name leaves that series out of the
chart's series list. The earlier spans (`compare`) read each series' history
by its position in that list, so once the first series was hidden the second
one's faint line was drawn from the first entity's earlier readings.

Each earlier span is now read by the series' own index among the card's
entities, which stays the same whichever series are hidden.
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
#	src/chart/frame.ts
#	src/locales/de.json
#	src/locales/en.json
#	src/locales/es.json
#	src/locales/fr.json
#	src/locales/nl.json
#	src/locales/pt.json
#	src/styles/column.ts
# Conflicts:
#	dist/chartdeck-card.js
#	dist/chartdeck-card.js.map
#	src/chart/frame.ts
#	src/locales/de.json
#	src/locales/en.json
#	src/locales/es.json
#	src/locales/fr.json
#	src/locales/nl.json
#	src/locales/pt.json
Copy CSV (`export: true`) lists what a history chart drew. Two cases with
`source: energy` came out wrong:

- Grid export and battery charging are drawn below the axis as negative
  numbers (`energy_split`). The CSV listed them negative, while the tooltip,
  labels and header read them positive. The CSV now reads them positive too.
- A pie or Sankey of the Energy dashboard reads hourly statistics only to add
  them up, and that left the hourly rows as "what was drawn". Its CSV now lists
  each flow's total over the period, the values the chart draws.

The card also forgets what an earlier config drew when it gets a new one, so
switching a card from a history chart to a chart of current values no longer
exports the old readings.
The running total counts the last reading on to the moment the card draws, so under a loaded test run the total came out a few Wh over the exact figure. The check now allows ten seconds of the last reading.
With `error_bars` and `legend_toggle: true` together on a column chart, hiding
a series left the others' whiskers in drawn order, but the tooltip looked them
up by each series' index among the card's entities. Hiding the first series
then broke the tooltip (it looked past the end of the list), and in other
cases a series could show another one's spread.

The tooltip now finds each series' whiskers by its index, so it reads the
spread drawn on that series' columns whichever series are hidden.
One fresh build of every merged feature and integration fix: the main bundle, its map and one chunk per language. The hashed chunks the merged branches carried, which this build no longer writes, are removed.
On a real Home Assistant the Energy dashboard column chart showed its
header total and its axes but no columns. The data was right: the card
drew every day's columns. But a long-term statistics chart read its rows
again whenever the card came back to the page, and while it did it
replaced the chart with "Loading history…". A masonry view takes its
cards off the page and puts them back each time it lays out its columns
(a resize, scrolling a card into view for a screenshot), so the chart
was rebuilt from scratch each time, and the new columns started their
grow-in animation from nothing. Caught at that moment, the chart had
its header and axes and no columns.

Statistics read again for the same span (the card came back, or the
next period was written) now keep drawing the rows they have until the
new ones land, so the columns stay where they are. A read that fails
in the background keeps the old rows and tries again a period later,
instead of turning the chart into an error. A new span, new entities or
new columns still show "Loading" as before.

Tested with the rows Home Assistant sent for an imported meter (daily
change with negative sums, then flat): a column per day at the right
height, and the same columns kept when the card is moved.
…he period

A Sankey of the Energy dashboard with nothing in its period said
"Nothing is flowing right now." That wording is for a Sankey of current
values. With source: energy the chart draws each flow's total over the
card's period (today, by default), so it now says "Nothing flowed in this
period." A pie of the Energy dashboard with every total at zero drew an
empty grey ring, which reads like missing data; it now says the same.

A Sankey or pie of ordinary entities keeps its old behaviour. The new
text is in all six languages.
…period

A strip plot over 24 hours of sensors that hadn't changed all day said
"No readings" on every row, with an axis from -0.5 to 1.5, while a line
chart of the same sensors drew a flat line. Home Assistant's history for
a sensor that held one value has no reading in the span, only the state
it started the span with. The strip plot and beeswarm draw one dot per
reading taken in the span and left that starting state out, so a row of
a perfectly steady sensor looked empty.

What it shows now: the starting state is the sensor's value for the
whole span, so the row draws it as one dot (with no marker line over it,
which would hide it), the row's line reads "No change: 21.4 °C", the tooltip
says "Held all period 21.4 °C", and the axis is fitted around the
values. One dot rather than a row of copies, because it is one reading
that held; spreading it out would fake readings that were never taken,
and the median, mean and spread of one steady value are that value. The
"No change" text makes the difference from a row with one real reading
plain. A row with readings in the span is unchanged, and a sensor with
no history at all still says "No readings".

The beeswarm shares this code and had the same problem: placed by value
it now draws the one dot the same way. Placed by time of day it draws
no dot, since the value was read at no particular time, but the row
still says "No change" with the value. Long-term statistics always have
a row per period, so they are not affected. New text in all six
languages.
transform: derivative on an energy meter that hadn't changed in the
chart's span said "No readings in this period." Home Assistant's history
for such a meter holds only the reading carried in to the span's start,
and a rate needs two readings with a known time between them, so the
series came out empty.

A meter (or any sensor) that held one value all span changed at a rate
of 0, so the chart now draws 0 from that reading to now, and the header
shows 0 in the rate's unit (0.0 kW for a kWh meter). The same holds for
every chart that takes a rate of change: box plot, candlestick and the
24-hour clock go through the same code. A series with no reading at all
still says "No readings in this period", and a series whose value did
change keeps its measured rates as before.
With now_marker: countdown and a time annotation that has its own label,
the countdown pill ("Night rate in 9 h 3 m") ran right up to the
annotation's "Night rate" label at the target time, and the two touched.

The now marker's label still sits right of the line by default, but now
it checks where the time annotations' labels sit along the top of the
plot. If going right would run off the plot or come within 4 px of one
of them, it goes left of the line instead. If neither side is clear, it
stays on the side it fits, as before.
Rebuilt for the real-install fixes: statistics charts keep their drawing while they reload, period wording for empty Energy dashboard totals, steady sensors in strip plots and beeswarms, a flat meter's rate of change, and the now marker's label beside time annotation labels.
…cycle plot in the editor

The visual editor decides which charts show the per-entity condition (when) and transform (running total, rate of change) fields from three lists. Range area was spelt range-area in the condition list, so its editor never showed the condition fields, and the spiral, strip plot and cycle plot were built alongside these options and never added, though all four load through the shared history frame and honour both options in YAML. A test now checks every name in those lists is a real style and that every numeric chart on the shared frame is in each list.
@github-actions github-actions Bot added documentation README, wiki, or in-code documentation gauges Rings, arcs and needle gauges editor The visual editor and the card picker translations Languages other than English tests The unit suite and coverage ci Workflows, build, release plumbing labels Nov 11, 2026
@tempus2016
tempus2016 merged commit 0fe2541 into main Nov 11, 2026
9 checks passed
@tempus2016
tempus2016 deleted the integrate/wave3 branch November 11, 2026 16:11
@github-project-automation github-project-automation Bot moved this from Todo to Done in Chartdeck Planner Nov 11, 2026
github-actions Bot pushed a commit that referenced this pull request Nov 11, 2026
## [2.2.0](v2.1.0...v2.2.0) (2026-11-11)

### Features

* 21 new charts and options (marimekko, chord, battery, spiral, strip, cycle, badge, energy source, transforms, units, export and more) ([#119](#119)) ([0fe2541](0fe2541))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Workflows, build, release plumbing documentation README, wiki, or in-code documentation editor The visual editor and the card picker gauges Rings, arcs and needle gauges tests The unit suite and coverage translations Languages other than English

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant