Designing glanceable dashboards for home environmental telemetry
An indoor air-quality sensor, a government regional PM2.5 feed, a dozen crowd low-cost stations and a phone position all land in the same two tools here: a Home Assistant dashboard and a Grafana board. This page is the design half - what each data item should look like, how the canon from dashboard literature and SRE practice maps onto lovelace cards and Grafana panels, and which mechanics are load-bearing.
The plumbing is elsewhere. OpenAQ community PM2.5 in Home Assistant covers the OpenAQ v3 routes, station-roster polling, entity-id slugification and the nearest-station projections; the indoor sensor is AirGradient ONE on ESPHome, fully local; the government feed and its live map are Singapore weather alerts and live maps with Home Assistant and Grafana; and the Prometheus and Grafana instances these boards read are described in Self-hosted monitoring topology.
Verified on a personal homelab, 2026-10-08: Home Assistant with the Prometheus exporter enabled, a Grafana 13 instance, an AirGradient indoor sensor on a 30 s ESPHome update_interval, and 14 OpenAQ community stations within 25 km. The grid, templating and staleness findings below were reproduced live on that dashboard; the vendor claims are documentary and marked as such in What was checked here.
TL;DR:
- A dashboard is one screen. A display that spans several screens, or needs scrolling to answer its own question, is an analytical view rather than a dashboard, and Few’s objection to the word’s misuse still holds twenty years on.1
- Overview first, then detail on demand. Shneiderman’s ordering is the layout rule: summary indicators at the top, per-station detail below or behind a drill-in.2
- Only some SRE method transfers. The four golden signals and USE are checklists for picking what to instrument; a home air-quality board has no saturation dimension and no SLO, so the useful half is “answer the operator’s questions” and “delete the signal nothing shows or alerts on.”34
- Choose the visualisation from the question, not the data. A current value is a number, a shape over time is a line, a pattern across two time dimensions is a heatmap, a state over time is a state timeline, and a position is a map. Everything else is decoration.
- The severity palette comes from a published AQI standard and is used identically in both tools. The AirNow AQI bands and the NEA PSI bands are the two published severity scales for this domain; inventing a local gradient makes two panels disagree about the same air.56
- Colour alone is not a coding. Roughly one in twelve men of European descent, one in 20 of Asian descent and one in 25 of African descent cannot separate red from green, so severity needs a second channel: a shape, a line type, a pattern, or a number.7
- Staleness is the failure mode that hides. A station that stops reporting keeps its last value in Home Assistant forever, so a board without a reading age is a board that reports yesterday’s air as now.
grid_options.columns, notlayout_options.grid_columns. The sections view lays cards on 12 columns per section column, the legacy key is multiplied by three on load, so a legacy 12 in a two-column-wide section becomes 36 and wraps onto its own row.89
Data flow, and where each decision sits
Section titled “Data flow, and where each decision sits”Read as a sequence: the four sources land in Home Assistant, which is the only store for the phone position and the only place a stale value silently persists. From there the data splits. The lovelace dashboard draws from the recorder and from long-term statistics; Prometheus scrapes the exporter and Grafana draws from Prometheus. Every design decision below applies at one of the two right-hand boxes, and the two boxes have different failure modes: the HA board shows stale values as if they were current, and the Grafana board shows missing values as gaps.
- Source to Home Assistant: cadence is set here, and it differs by three orders of magnitude between the indoor sensor and the government feed.
- Home Assistant to the lovelace dashboard: layout, per-item card choice, colour.
- Home Assistant to Prometheus: which entities are exported at all, and at what scrape interval.
- Prometheus to Grafana: time range, null handling, thresholds and freshness queries.
- Both surfaces: one severity palette, one staleness rule, one naming convention.
Which tool for which question
Section titled “Which tool for which question”| Question | Home Assistant | Grafana |
|---|---|---|
| What is the indoor value right now, and can I act on it | Gauge or tile card, in the same section as the sensor | Possible, but the board is a page away |
| Answer a question about the last hour without leaving the room | History graph card | Time series, if the panel is in the default range |
| Compare 14 stations on one shared axis | History graph card caps at eight entities, so two cards | Time series with a per-series legend, or a table |
| Hour-of-day by day-of-week pattern | Not expressible as a card | Heatmap over a PromQL range |
| Alerts on a value | Native automations, no extra hop | Grafana alerting against Prometheus |
| History beyond the recorder retention window | Long-term statistics, hourly and never purged | Prometheus TSDB, wherever retention is set |
The split is not “HA for looking, Grafana for analysis”. It is that Home Assistant owns the entity and the staleness, and Grafana owns the long window and the two-dimensional roll-up.
The canon in brief
Section titled “The canon in brief”Dashboard literature is mostly about business intelligence and web analytics; operations monitoring has its own twenty-year literature; statistical publications have a third. Three of those four strands transfer.
What transfers
Section titled “What transfers”| Source | The part that transfers | The part that does not |
|---|---|---|
| Few, on dashboards | A dashboard is a single-screen, at-a-glance display that answers a small set of questions; the word’s misuse for multi-screen analytical displays is the original complaint | His business-intelligence context, and the ratio metrics (percentages of quota, variance to target) that home telemetry has no analogue for1 |
| Shneiderman, on visual information seeking | Overview first, zoom and filter, then details on demand | The large-collection framing; 15 stations is not a collection that needs dynamic query widgets2 |
| SRE, on what to monitor | Answer the operator’s questions with a small number of signals; collect nothing that no display or alert uses; track error latency rather than filtering errors out | Service-oriented signals. There is no traffic, no saturation and no SLO for an air-quality reading3 |
| Brendan Gregg’s USE method | The checklist discipline: for every resource, check utilisation, saturation and errors, so that a blank panel is a decision rather than an oversight | A sensor is not a resource with utilisation. The transferable part is the checklist, not the three letters4 |
| Grafana’s dashboard guidance | Left-to-right reading order, a logical progression, one question per row, delete what is not refreshing, do not stack overlapping series | Its examples are service metrics; the ordering advice is tool-independent10 |
| ONS data-visualisation guidance | Axis rules (bar and area charts start at zero), and that a dashboard is a maintenance commitment justified by users who revisit it | ONS’s governance layer: who reviews a dashboard request, and publication-level accessibility obligations1112 |
| NN/g on the F-pattern | Eyes fixate along the top and down the left edge first | The conclusion is about text pages, not panels; a dashboard grid gives you the top-left slot for free and the guidance is to put the answer there13 |
Three of those rows give the same instruction in different vocabularies: put the thing the operator came for at the top left, in the first row, at the largest weight. Shneiderman calls it overview-first, Few calls it at-a-glance, NN/g calls it the F-pattern’s first fixation.
What does not transfer
Section titled “What does not transfer”- SLOs and error budgets. An SLO needs a service with a user and a promise. A PM2.5 ceiling is a threshold, not an objective; the panel shows a band, not a burn rate.
- Golden signals for every entity. Latency, traffic, errors and saturation describe a request path. The nearest analogue here is reporting health (freshness, availability, disagreement between stations), which is a different four-item list.
- Percent-of-target framing. Business dashboards love “83% of goal”, which invites a gauge whose max is a target. For a concentration, the scale is the published AQI band, and the useful question is which band the value is in, not how far along a target.
- Drill-down widgets. With 15 series, a filter panel costs more attention than it saves. Sort, cap and label instead.
Layout: overview first, top-left, one screen
Section titled “Layout: overview first, top-left, one screen”The ordering rule is Shneiderman’s, and it settles most layout arguments: an overview at the top, filters and grouping in the middle, per-item detail at the bottom or behind a tap.2 In a lovelace sections view the top-left slot is the first section of the first row, which is also the first fixation of the F-pattern.13
Concretely, four rows in descending priority:
- The answer. Indoor value with its band, the outdoor regional value, and a staleness indicator. Numbers, not graphs.
- The shape. Indoor PM2.5 and the nearest station over the last hour or day, one shared axis.
- The comparison. The station roster as a table or a sorted list, so disagreement is visible without reading 14 lines.
- The provenance. A markdown card naming each source, its cadence, its licence and its known limitation, plus a timestamp for the last roster refresh.
One screen versus scroll is a real decision, not a preference. Few’s definition makes the first screen the dashboard and everything after it an analytical view; a board that scrolls to reach its own summary has the summary in the wrong place.1 In practice, the answer row and the shape row fit on one screen on a phone in landscape if the cards are sized deliberately; the roster and provenance rows are the ones allowed to sit below the fold, because they are read deliberately rather than glanced at.
Grafana’s own ordering advice agrees and adds a mechanical rule: keep a consistent left-to-right progression and group panels into rows that each answer one question, because a reader who has to re-learn the reading order per row stops scanning.10
On the Grafana side, one screen also means the default time range. A board whose useful answer lives in a two-hour window should open on two hours, not on the last six hours of mostly-empty axis; a Grafana dashboard also has to justify a refresh interval, since a panel that refreshes faster than its data changes spends resources to show the same pixels.10
Choosing the visualisation per data item
Section titled “Choosing the visualisation per data item”The rule that decides this table: match the panel to the shape of the question, then check that the tool can express it at all. Home Assistant cards are entity-shaped and capped by their own limits; Grafana panels are query-shaped.
| Data item | Home Assistant card | Grafana panel | Why |
|---|---|---|---|
| Indoor PM2.5 now | Gauge with a severity map, or a tile | Stat with value colour mode | One number against a published band; a gauge’s arc adds the band, and a stat’s colour mode does the same without the dial1415 |
| Indoor PM2.5 over the last hour | History graph | Time series | Shape and events; a line is the right mark for a continuous quantity over time16 |
| Indoor PM2.5 at hourly resolution | Statistics graph, period: hour | Time series over a recording rule | Hourly min/mean/max is what the statistics store keeps, so a 30-day card reads long-term statistics rather than the recorder17 |
| Hour-of-day by day-of-week pattern | Not expressible | Heatmap | The question has two time dimensions; a line collapses one of them |
| Nearest regional government value | Statistic card, or a sensor card | Stat | A slower, coarser reference reading; it exists to sanity-check the indoor value18 |
| Several community stations compared | One history graph card per up-to-eight entities | Time series, no stacking | Same unit, same window, one axis; 14 entities is beyond what a single card or a reader can hold, so cap and sort |
| The whole roster, triaged | Entities card or an entity filter | Table | 14 rows of numbers; a table sorts and a line chart spaghetti-fies19 |
| Reading age of each source | Markdown card with a template | Stat or table over last_over_time | Staleness is a value, not a decoration, and it needs a number20 |
| Station online or offline over the day | Conditional card, or a markdown list | State timeline | Discrete states over time; a line implies interpolation between them21 |
| Phone position and the stations near it | Map card | Geomap | Geography is the axis; the map card plots entities with numeric latitude and longitude attributes, which a device tracker has and a plain station sensor does not2223 |
| Which station is nearest, and how far | Markdown table | Table with a distance column | Positional selection is a comparison of numbers, not a picture |
| Sources, cadence, licence, caveats | Markdown card | Text panel | The documentation belongs on the board; sourced from the provenance row of the layout |
Two rows in that table are the ones that get skipped: reading age and provenance. Both are text, both look less impressive than a graph, and both are what makes the graph trustworthy.
An entity filter with show_empty: false is the cheapest way to make a roster card honest, since a station that has stopped reporting can be filtered out of the current-readings card entirely and shown instead in the staleness card.19 That split is the staleness rule from the Home Assistant mechanics section, expressed as a card choice.
Time series rules
Section titled “Time series rules”- Zero baselines for bars and areas; cropped axes for lines. ONS is explicit: charts where the mark is connected to the axis (bar, area) must start at zero, because the filled length is the encoding; charts whose points float (line, scatter) may crop, and a range of 92 to 100 is better shown from 90 than from 0.11
- No stacked areas for overlapping series. Grafana’s own guidance says to be careful with stacking because it can create misleading graphs, and the visualization reference repeats the warning.24 The indoor sensor and the regional feed are not parts of a total; stacking them invents a sum nobody wants.
- One axis, or split the panel. Two series with the same unit share an axis. Two series with different units or ranges belong in different panels, not on a second Y-axis, because a reader cannot compare the two slopes without knowing the ratio between them.10
- Thresholds are lines, and the line carries its source. Grafana thresholds can render as lines, dashed lines, filled regions, or both; for a published band, the filled region is the band and the line is its edge, and the panel title or description names the standard the edge came from.25
- Nulls are gaps, not zeroes. “Connect null values” set to Never leaves a hole where a station stopped reporting, which is the honest rendering; a zero would read as clean air.24 The HA Prometheus exporter unexports an entity when it becomes unavailable or unknown, so the gap is the natural state rather than something to be suppressed.26
- State over time is not a line. A station that alternates between online and offline, or a phone fix that alternates between fresh and stale, is a discrete series; the state timeline exists for exactly that and a line chart would imply intermediate values.21
- Freshness is a second series. Reading age belongs on the same board as the value it qualifies, at the same or higher prominence.
- Refresh at the data’s cadence, not faster. HA’s own example scrape config uses a 60 s interval, and a panel is not improved by refreshing every 5 s over a series that changes every 30 s.2610
Colour
Section titled “Colour”One palette, chosen once, from a published standard, and used identically in both tools.
| Band | Source | Use |
|---|---|---|
| Good, Moderate, Unhealthy for Sensitive Groups, Unhealthy, Very Unhealthy, Hazardous | The US EPA AQI categories as published by AirNow | The severity palette for any index reading, in HA and Grafana alike5 |
| The 1-hour PM2.5 reading and its band | Singapore NEA, published with a health advisory and the caveat that the 1-hour figure is volatile | The regional reference line; do not silently convert it into an AQI band without saying so627 |
Four rules on top of the palette:
- Severity colour is reserved for severity. If the indoor line is green because it is good and the outdoor line is blue because it is a different series, the reader has to hold two colour languages. Series identity goes in the legend and the line label.
- Redundant coding. The severity must survive colour-blindness. Roughly 8% of men of European descent, 5% of Asian descent and 4% of African descent have a red-green deficiency, and the fix is a second channel: shape, position, line type, or a pattern, not a different shade.7
- Sequential, diverging and qualitative are different jobs. A concentration is sequential; a delta against a baseline is diverging; a station roster is qualitative. The rainbow and jet colormaps are in the miscellaneous family and are not perceptually uniform, so a reader cannot order the colours by value.28 For the roster, pick a small qualitative set where the categories are genuinely unordered.
- Both tools express the palette with the same mechanism. In Grafana, thresholds colour the value and the panel, and value mappings handle the special cases, including mapping a null or NaN to a visible “no data” rather than leaving a blank that looks like a zero.2925 In Home Assistant the gauge card takes a severity map of colours against start values, and from the 2026.6 release the history graph and statistics graph cards take a per-entity colour from the visual editor or YAML, so the palette no longer has to be argued with the frontend default colour order.1430 The instance behind this page runs 2026.5.4, so its graph cards still use the default colour order.
Low-cost sensors and uncertainty
Section titled “Low-cost sensors and uncertainty”A crowd station and a reference station do not deserve the same visual weight, and the dashboard should say which is which.
- Official performance testing exists, and it is not pass/fail. The US EPA’s air sensor toolbox publishes performance targets and testing protocols for sensors used in non-regulatory supplemental and informational monitoring, in two scenarios (base, which is a field evaluation, and enhanced, which adds a laboratory evaluation), with a list of target values per metric rather than a single verdict.31 A low-cost station on a board is an NSIM-grade instrument: useful for pattern and for local contrast, not for compliance.
- The crowd networks publish their own confidence marker. PurpleAir assigns every two-channel sensor a confidence score from 0 to 100 percent, shown on its map.32 Where a station carries a quality score, it belongs on the roster row rather than in a tooltip.
- Aggregators do not verify placement. OpenAQ does not check where a station sits, so an indoor sensor appears in the outdoor feed; the mitigation is a sanity check against the regional reference, which is covered in the OpenAQ reference.
- Show ranges, not standard errors. ONS’s uncertainty guidance is the usable rule: show a range of two values, describe it in plain language (“we are 95% sure the value falls in this range”), keep single-number measures such as standard error or coefficient of variation out of charts because they are hard to interpret, and omit the range entirely where it is small enough not to change the reading.33
- Not every chart type can show a range. ONS names stacked bar charts as one that cannot show uncertainty effectively, which is a second reason not to stack the sources.33
- Cadence is part of the claim. A 30 s indoor series and an hourly regional feed are not the same measurement, and a panel that draws them at the same visual weight implies a comparison the data does not support.34
Home Assistant mechanics
Section titled “Home Assistant mechanics”Five findings from the live dashboard, each of which changed a layout decision.
The sections grid is 12 columns per section column
Section titled “The sections grid is 12 columns per section column”A grid section declares --base-column-count: 12 and computes its track count as 12 multiplied by the section’s column span, so a section two columns wide lays out on 24 card columns.9 A card’s own footprint comes from grid_options.columns, defaulting to 12.8
The trap is the legacy key. layout_options.grid_columns is migrated to grid_options.columns by multiplying by GRID_COLUMN_MULTIPLIER, which is 3, so a value written as 12 of 24 is loaded as 36 and the card wraps onto a row of its own.8 Use grid_options.columns with a value that matches the section width, and make each row’s columns sum to that width rather than to 12.
Card defaults, read from their sources, matter when a row is being planned:
| Card | Default columns | Default rows |
|---|---|---|
| History graph | 12 | auto35 |
| Statistics graph | 12 | auto36 |
| Entities | 12 | auto37 |
| Map | full width | 438 |
A map is the exception on both axes, so a row containing one is not a 12-column row. The directory of card sources, which is where these defaults are declared, is the reference for any card not listed here.39
A markdown table dies at the first blank line
Section titled “A markdown table dies at the first blank line”The markdown card renders through Marked.js, which supports GitHub Flavored Markdown including tables.40 GFM terminates a table at the first blank line, so any blank line inside a table body ends the table and the remaining rows render as literal pipe text. Jinja blocks emit those blank lines: a {% for %} with a newline after the tag puts a newline, and therefore a blank line, between the rows. Use whitespace control on both tags ({%- ... -%}) and keep the generated rows contiguous, or the card shows a column of pipes instead of a table.
last_reported is a state property, not an attribute
Section titled “last_reported is a state property, not an attribute”state.last_updated advances only when the state or attributes change; state.last_reported advances whenever the entity reports at all, even when the value is unchanged.41 For “how fresh is the phone fix”, last_reported is the right field, because a stationary phone reports the same coordinates indefinitely and last_updated would show a fix from hours ago.
The second half of the finding is where it lives. Both are properties of the state object, not attributes, so state_attr(sensor.phone, 'last_reported') returns none and the template has to read states.<entity_id>.last_reported instead.
Staleness must be visible
Section titled “Staleness must be visible”Home Assistant keeps an entity’s last value indefinitely when it stops updating, so a station that died yesterday still renders as a reading today. Nothing in the default card set shows that. Two mechanisms make it visible: a header markdown card that renders the reading age per source from states.*.last_reported, and a split between the fresh and stale sets so the current-readings row cannot contain a dead station. The same split is what the entity filter’s show_empty: false does for a roster card.19
Stat panels need one style per row
Section titled “Stat panels need one style per row”A row of stat-style panels that mixes a gauge with a tile with a plain sensor card reads as broken even when every value is right, and the failure compounds with truncated titles: a title cut off by the card width is read as a missing value. Pick one style for the row, size the cards so the titles fit, and put the unit in the card’s own unit field rather than in the title string.
Grafana mechanics
Section titled “Grafana mechanics”- The exporter decides what exists. Home Assistant exports metrics only for a fixed list of domains, and an entity in an unavailable or unknown state is unexported until it recovers, which is where the gaps in a Grafana panel come from.26 The documented
entity_availablefilter is no longer needed for the common case, and the docs say so.26 - Scrape interval is a design choice. The example scrape config uses 60 s; a scrape slower than the series you are charting turns a live signal into a sampled one, and a scrape faster than the sensor’s own cadence adds resolution without information.26
last_over_timeanswers “how fresh”. It returns the most recent sample in the interval, which is exactly the freshness query, andts_of_last_over_timegives the timestamp of that sample.20- Subqueries answer min-over-a-window. A subquery runs an instant query over a range at a resolution and yields a range vector, which is how a short peak inside a wide range stays visible.42
- Recording rules keep the heavy panels cheap. A recording rule precomputes a frequently needed or expensive expression, and querying the precomputed result is often much faster than executing the original every refresh; the freshness and roll-up panels are the candidates.43
- Staleness is tracked by Prometheus too, which is the mechanism behind the gap rather than a value of zero.42
- Panel choice follows the table above, and Grafana’s visualization index is the check that the panel you want exists: time series is the default and main graph visualization, the state timeline is for state changes over time, and the histogram, bar chart, table and gauges are the other primaries.44
- Grafana 13’s gauge is a rewrite. The revamped gauge visualization went generally available in Grafana 13 after a public preview in January, so gauge screenshots from older documentation do not match the current panel.45 The Home Assistant gauge card was also redesigned in the 2026.4 release, so a gauge in either tool is worth re-checking against current documentation before copying an old configuration.46
Checklist
Section titled “Checklist”Before calling a board finished:
- The answer is in the first row, top-left, as numbers.
- Reading age is visible for every source, and stale sources are out of the current-readings row.
- Every threshold line names the standard it came from.
- One severity palette, taken from a published AQI scale, identical in both tools.
- Severity survives colour-blindness through a second channel.
- Bars and areas start at zero; lines may crop.
- No stacked areas over overlapping sources.
- Nulls render as gaps, not zeroes.
- Each row sums to the section width in
grid_options.columns, and no card useslayout_options.grid_columns. - The markdown tables have no blank line inside them, and their Jinja tags carry whitespace control.
- Every card in a stat row is the same style and none has a truncated title.
- Sources, cadences and licences are on the board.
- Every panel answers a question someone asked; the rest are deleted.
What was checked here
Section titled “What was checked here”| Claim | How it was checked | Status |
|---|---|---|
| A grid section computes 12 card columns per section column | Read --base-column-count and the track calculation in the sections source9 | Documented, source-read |
layout_options.grid_columns is multiplied by 3 on load | Read GRID_COLUMN_MULTIPLIER in the frontend source, then reproduced the wrap on the live dashboard8 | Measured |
| Card defaults: 12 columns for history graph, statistics graph and entities; full width and 4 rows for the map | Read each card’s getGridOptions in the frontend source35363738 | Documented, source-read |
| A markdown table ends at the first blank line, and Jinja whitespace control prevents the blank lines | Reproduced on the live dashboard | Measured |
last_reported advances without a state change and is not reachable through state_attr | Read the state-object documentation, then probed a template on the live host41 | Measured |
| A station that stops reporting keeps its last value | Observed on the live dashboard | Measured |
| Mixed stat-panel styles and truncated titles read as broken | Observed on the live dashboard | Measured |
| Grafana 13’s gauge is generally available | Release notes45 | Documented, not tested |
| Home Assistant’s gauge card was redesigned in 2026.4 | Release notes46 | Documented, not tested |
| Per-entity colours on graph cards arrive in HA 2026.6 | Release notes30 | Documented, not tested |
| EPA publishes sensor performance targets for non-regulatory monitoring | Air sensor toolbox page31 | Documented, not tested |
| PurpleAir’s confidence score runs 0 to 100 percent | Community documentation32 | Documented, not tested |
Related docs
Section titled “Related docs”- OpenAQ community PM2.5 in Home Assistant - the data plumbing this page assumes, including entity-id slugification and the roster refresh
- AirGradient ONE on ESPHome, fully local - the indoor sensor, its 30 s cadence and the AQI template
- Singapore weather alerts and live maps with Home Assistant and Grafana - the NEA regional feed integrated the same way
- Self-hosted monitoring topology - the Prometheus and Grafana instances these panels read
- Home IoT: an ESPHome fleet on a Flint-bridged VLAN - the network the sensors sit on
References
Section titled “References”-
Stephen Few, “Dashboard Confusion,” Perceptual Edge. https://www.perceptualedge.com/articles/ie/dashboard_confusion.pdf ↩ ↩2 ↩3
-
Ben Shneiderman, “The Eyes Have It: A Task by Data Type Taxonomy for Information Visualizations,” University of Maryland. https://www.cs.umd.edu/~ben/papers/Shneiderman1996eyes.pdf ↩ ↩2 ↩3
-
Google, “Monitoring Distributed Systems,” Site Reliability Engineering. https://sre.google/sre-book/monitoring-distributed-systems/ ↩ ↩2
-
Brendan Gregg, “The USE Method,” brendangregg.com. https://www.brendangregg.com/usemethod.html ↩ ↩2
-
US EPA, “AQI Basics,” AirNow. https://www.airnow.gov/aqi/aqi-basics/ ↩ ↩2
-
Singapore National Environment Agency, “Air pollution frequently asked questions,” NEA. https://www.nea.gov.sg/our-services/pollution-control/air-pollution/faqs ↩ ↩2
-
Masataka Okabe and Kei Ito, “Color Universal Design,” J*Fly. https://jfly.uni-koeln.de/color/ ↩ ↩2
-
Home Assistant, “compute-card-grid-size.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/common/compute-card-grid-size.ts ↩ ↩2 ↩3 ↩4
-
Home Assistant, “hui-grid-section.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/sections/hui-grid-section.ts ↩ ↩2 ↩3
-
Grafana Labs, “Best practices for dashboards,” Grafana documentation. https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/best-practices/ ↩ ↩2 ↩3 ↩4 ↩5
-
Office for National Statistics, “Axes and gridlines,” Data visualisation service manual. https://service-manual.ons.gov.uk/data-visualisation/guidance/axes-and-gridlines ↩ ↩2
-
Office for National Statistics, “Dashboards,” Data visualisation service manual. https://service-manual.ons.gov.uk/data-visualisation/guidance/dashboards ↩
-
NN/g, “F-Shaped Pattern of Reading on the Web,” NN/g. https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/ ↩ ↩2
-
Home Assistant, “Gauge card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/gauge/ ↩ ↩2
-
Grafana Labs, “Stat,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/stat/ ↩
-
Home Assistant, “History graph card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/history-graph/ ↩
-
Home Assistant, “Statistics graph card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/statistics-graph/ ↩
-
Home Assistant, “Statistic card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/statistic/ ↩
-
Home Assistant, “Entity filter card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/entity-filter/ ↩ ↩2 ↩3
-
Prometheus, “Query functions,” Prometheus documentation. https://prometheus.io/docs/prometheus/latest/querying/functions/ ↩ ↩2
-
Grafana Labs, “State timeline,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/state-timeline/ ↩ ↩2
-
Home Assistant, “Map card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/map/ ↩
-
Grafana Labs, “Geomap,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/ ↩
-
Grafana Labs, “Time series,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/ ↩ ↩2
-
Grafana Labs, “Configure thresholds,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/configure-thresholds/ ↩ ↩2
-
Home Assistant, “Prometheus integration,” Home Assistant Docs. https://www.home-assistant.io/integrations/prometheus/ ↩ ↩2 ↩3 ↩4 ↩5
-
Singapore National Environment Agency, “Haze situation,” haze.gov.sg. https://www.haze.gov.sg/ ↩
-
Matplotlib, “Choosing colormaps in Matplotlib,” Matplotlib documentation. https://matplotlib.org/stable/users/explain/colors/colormaps.html ↩
-
Grafana Labs, “Configure value mappings,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/configure-value-mappings/ ↩
-
Home Assistant, “2026.6 release notes,” Home Assistant Blog. https://www.home-assistant.io/blog/2026/06/03/release-20266/ ↩ ↩2
-
US EPA, “Air Sensor Performance Targets and Testing Protocols,” Air Sensor Toolbox. https://www.epa.gov/air-sensor-toolbox/air-sensor-performance-targets-and-testing-protocols ↩ ↩2
-
PurpleAir, “The Confidence Score,” PurpleAir Community. https://community.purpleair.com/t/the-confidence-score/5193 ↩ ↩2
-
Office for National Statistics, “Showing uncertainty in charts,” Data visualisation service manual. https://service-manual.ons.gov.uk/data-visualisation/guidance/showing-uncertainty-in-charts ↩ ↩2
-
Erfi Anugrah, “AirGradient ONE on ESPHome, fully local,” erfi.dev. https://erfi.dev/guides/airgradient-one-esphome-local/ ↩
-
Home Assistant, “hui-history-graph-card.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/cards/hui-history-graph-card.ts ↩ ↩2
-
Home Assistant, “hui-statistics-graph-card.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/cards/hui-statistics-graph-card.ts ↩ ↩2
-
Home Assistant, “hui-entities-card.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/cards/hui-entities-card.ts ↩ ↩2
-
Home Assistant, “hui-map-card.ts,” frontend repository. https://raw.githubusercontent.com/home-assistant/frontend/dev/src/panels/lovelace/cards/hui-map-card.ts ↩ ↩2
-
Home Assistant, “Lovelace card sources,” frontend repository. https://github.com/home-assistant/frontend/tree/dev/src/panels/lovelace/cards ↩
-
Home Assistant, “Markdown card,” Home Assistant Docs. https://www.home-assistant.io/dashboards/markdown/ ↩
-
Home Assistant, “State objects,” Home Assistant Docs. https://www.home-assistant.io/docs/configuration/state_object/ ↩ ↩2
-
Prometheus, “Querying basics,” Prometheus documentation. https://prometheus.io/docs/prometheus/latest/querying/basics/ ↩ ↩2
-
Prometheus, “Recording rules,” Prometheus documentation. https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ ↩
-
Grafana Labs, “Visualizations,” Grafana documentation. https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/ ↩
-
Grafana Labs, “What’s new in Grafana v13.0,” Grafana documentation. https://grafana.com/docs/grafana/latest/whatsnew/whats-new-in-v13-0/ ↩ ↩2
-
Home Assistant, “2026.4 release notes,” Home Assistant Blog. https://www.home-assistant.io/blog/2026/04/01/release-20264/ ↩ ↩2