npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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.json keeps the working slug air-quality-index as a placeholder — replace it (and the npm install line below) with the real published name/scope before running npm 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) + ILo

where 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.pm25 in index.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 null rather than an extrapolated number.

Running the tests

node test/index.test.js

The 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-index below is the working slug, not a finalized npm name — see the owner TODO near the top of this README.

npm install air-quality-index
const aqi = require('air-quality-index');

aqi.aqiFromPM25(35.5);                       // 101
aqi.overallAQI({ pm25: 35.5, o3_8h: 0.04 }); // { aqi: 101, dominant: 'pm25', ... }