Repository navigation
feat: 21 new charts and options (marimekko, chord, battery, spiral, strip, cycle, badge, energy source, transforms, units, export and more) - #119
Merged
Conversation
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 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))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
style: marimekko): columns as wide as each group's total, split by category.style: chord): flows between nodes round a circle; reads the Sankey'sfrom/to.style: battery): every battery as a filled glyph, coloured by band.style: spiral): one turn per day or week, so daily rhythm lines up.style: strip_plot): every reading as a dot, with a median or mean line.style: cycle_plot): a small panel per weekday, hour or day of the month.type: custom:chartdeck-badge): value, sparkline, ring or thin bar in the badge row.Added to existing charts
ghost: 24h | 7d): the recent range and mean behind the needle, arc and ring gauges.error_bars: std | min_max | ci95) on grouped column charts.forecast:): a weather or Solcast forecast drawn dashed after now.anomalies: true): readings far from normal are marked.compare: [1d, 7d]): yesterday or last week faintly behind today, aligned by wall clock.history: true,zone,severity): a threshold line that follows an entity's history.now_marker: line | time | countdown) on line and column charts.legend_toggle: true): tap a name to hide a series, hold to show only it.Data options
transform: cumulative, reset day/week/month/billing day).transform: derivative, per s/min/h/d, with smoothing).unitnow 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.when: {entity, state | above | below}, per entity or card): keep readings only while a condition holds, with an optional faint copy and condition strip.source: energy,energy_flows,energy_split): line, column, area, pie and Sankey read the Energy dashboard's settings, no entities needed.export: true): save as PNG or SVG, copy CSV or YAML.Fixed along the way
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.