@verifyhash/air-quality-index
v1.0.0
Published
Zero-dependency US EPA Air Quality Index (AQI) calculator for PM2.5, PM10, O3, CO, SO2 and NO2.
Downloads
146
Maintainers
Readme
air-quality-index
TODO (owner): pick the final npm name/scope before publishing. The npm package name is not finalized by this project. Convention used here:
package.jsonkeeps the working slugair-quality-indexas a placeholder — replace it (and thenpm installline below) with the real published name/scope before runningnpm publish.
A zero-dependency, zero-network Node.js library that computes the U.S. EPA Air Quality Index (AQI) from raw pollutant concentrations. Pure functions, no runtime deps, no I/O — give it a number, get an AQI back.
What it is
The AQI is a 0–500 scale the EPA uses to report air quality. Each pollutant has a published breakpoint table; a measured concentration is mapped to a sub-index with a piecewise-linear interpolation, and the reported AQI for a location is the maximum of the available sub-indices. This library implements that math for six criteria pollutants and the six EPA category bands.
Who it is for
Developers building air-quality dashboards, widgets, weather add-ons, IoT displays, or data pipelines who have concentration values (from a sensor, an API like AirNow/OpenAQ, or a model) and need the corresponding AQI number, category label, and color — without pulling in a heavy dependency or making a network call.
Breakpoint source
Breakpoints and the interpolation formula are taken from the EPA Technical Assistance Document, "Technical Assistance Document for the Reporting of Daily Air Quality – the Air Quality Index (AQI)", EPA-454/B-18-007 (September 2018), Table 5. The interpolation is EPA Equation 1:
Ip = ((IHi - ILo) / (BPHi - BPLo)) * (Cp - BPLo) + ILowhere Cp is the concentration truncated to the table's decimal precision,
and BPLo/BPHi are the breakpoints straddling Cp.
Note on PM2.5: this library uses the pre-2024 PM2.5 breakpoints (Good tops out at 12.0 µg/m³, USG starts at 35.5). The EPA revised these in May 2024. This is a deliberate choice so the well-known reference fixtures hold (12.0 → 50, 35.5 → 101, 55.5 → 151); if you need the 2024 table, edit
BREAKPOINTS.pm25inindex.js.
Pollutants and units
| Function | Pollutant | Averaging | Unit | Truncation |
|-------------------|-----------|-----------|-------|------------|
| aqiFromPM25(c) | PM2.5 | 24-hour | µg/m³ | 1 decimal |
| aqiFromPM10(c) | PM10 | 24-hour | µg/m³ | integer |
| aqiFromO3_8h(c) | Ozone | 8-hour | ppm | 3 decimals |
| aqiFromCO(c) | CO | 8-hour | ppm | 1 decimal |
| aqiFromSO2(c) | SO₂ | 1-hour | ppb | integer |
| aqiFromNO2(c) | NO₂ | 1-hour | ppb | integer |
Each returns a rounded integer AQI, or null if the input is not a usable
finite number in range.
API
const aqi = require('air-quality-index');
aqi.aqiFromPM25(35.5); // 101
aqi.aqiFromPM25(12.0); // 50
aqi.aqiFromO3_8h(0.071); // 101
aqi.aqiFromPM25(-5); // null (bad input)
aqi.category(101);
// { index: 3, label: 'Unhealthy for Sensitive Groups', color: '#ff7e00' }
aqi.overallAQI({ pm25: 35.5, o3_8h: 0.04, no2: 20 });
// {
// aqi: 101,
// dominant: 'pm25',
// dominantLabel: 'PM2.5',
// category: { index: 3, label: 'Unhealthy for Sensitive Groups', color: '#ff7e00' },
// subIndices: { pm25: 101, o3_8h: 37, no2: 19 }
// }category(aqi)
Returns { index, label, color } for the six EPA bands, or null for
non-numbers or values outside 0–500:
| AQI | index | label | color |
|-----------|-------|--------------------------------|-----------|
| 0–50 | 1 | Good | #00e400 |
| 51–100 | 2 | Moderate | #ffff00 |
| 101–150 | 3 | Unhealthy for Sensitive Groups | #ff7e00 |
| 151–200 | 4 | Unhealthy | #ff0000 |
| 201–300 | 5 | Very Unhealthy | #8f3f97 |
| 301–500 | 6 | Hazardous | #7e0023 |
overallAQI(readings)
readings is an object with any subset of the keys pm25, pm10, o3_8h,
co, so2, no2. Each provided value is converted to a sub-index; the reported
AQI is the maximum, and the pollutant producing it is dominant. Pollutants that
are absent or produce a null sub-index (out of range) are ignored. Returns
null if nothing usable was provided.
Error handling
Every function is total — it never throws on bad input. Undefined, null, NaN,
non-numbers, negative values, and concentrations above the top breakpoint all
return null (or are skipped, in overallAQI).
Honest limits
- This is reported-AQI math, not sensor calibration. It assumes you already have a properly-averaged concentration (e.g. a 24-hour PM2.5 mean). It does not do NowCast, temporal averaging, unit conversion (µg/m³ ↔ ppb), or quality control of raw sensor data.
- Ozone above 0.200 ppm returns
null. The 8-hour ozone table only defines AQI up to 300; higher values require the 1-hour ozone table, which is not implemented. - SO₂ above 304 ppb returns
null. The 1-hour SO₂ table only reaches AQI 200; AQI 201+ requires the 24-hour SO₂ average, which is not implemented. - PM2.5 uses the pre-2024 breakpoints (see the note above).
- Values above each pollutant's highest breakpoint (e.g. PM2.5 > 500.4 µg/m³)
return
nullrather than an extrapolated number.
Running the tests
node test/index.test.jsThe suite has no dependencies. It exits non-zero on any failure and prints a
pass/fail summary. It covers each pollutant with hand-verifiable EPA fixtures
(PM2.5 12.0 → 50, 35.5 → 101, 55.5 → 151), every category boundary, out-of-range
handling, and overallAQI dominant-pollutant selection.
License
MIT.
Install
Placeholder name: the
air-quality-indexbelow is the working slug, not a finalized npm name — see the owner TODO near the top of this README.
npm install air-quality-indexconst aqi = require('air-quality-index');
aqi.aqiFromPM25(35.5); // 101
aqi.overallAQI({ pm25: 35.5, o3_8h: 0.04 }); // { aqi: 101, dominant: 'pm25', ... }