# PM2.5 to AQI Conversion

My home weather station measures PM2.5 concentration in micrograms per cubic meter (μg/m³). But if I’m concerned about air quality, I want to know the Air Quality Index (AQI). AQI is a policy tool and as such has a complicated definition. So, here’s a tool to convert between PM2.5 and AQI (assuming that PM2.5 is the only pollutant you care about).

Drag either slider below to choose the selected PM2.5 or AQI.

## PM2.5 (μg/m³) / AQI

| PM2.5 (μg/m³) | AQI |
|----------------|-----|
| 0  | Good |
| 50 | Moderate |
| 100 | Unhealthy for sensitive groups |
| 150 | Unhealthy |
| 200 | Very unhealthy |
| 300 | Hazardous |

The cell below defines the six official [Air Quality Index (AQI) categories](https://www.airnow.gov/aqi/aqi-basics/): “Each category corresponds to a different level of health concern. Each category also has a specific color. The color makes it easy for people to quickly determine whether air quality is reaching unhealthy levels in their communities.”

```javascript
categories = [
    {max:50,color:"green",name:"Good"},
    {max:100,color:"yellow",name:"Moderate"},
    {max:150,color:"orange",name:"Unhealthy for sensitive groups"},
    {max:200,color:"red",name:"Unhealthy"},
    {max:300,color:"purple",name:"Very unhealthy"},
    {max:500,color:"maroon",name:"Hazardous"}
];
```

The `pm25_aqi` function converts from a PM2.5 concentration in micrograms per cubic meter to the corresponding AQI value (assuming that PM2.5 is the only contributor to AQI). It is the inverse of `aqi_pm25`.

```javascript
function pm25_aqi(pm25) {
    const c = Math.floor(10 * pm25) / 10;
    const a = c < 0 ? 0 :
        c < 12.1 ? lerp(0, 50, 0.0, 12.0, c) :
        c < 35.5 ? lerp(51, 100, 12.1, 35.4, c) :
        c < 55.5 ? lerp(101, 150, 35.5, 55.4, c) :
        c < 150.5 ? lerp(151, 200, 55.5, 150.4, c) :
        c < 250.5 ? lerp(201, 300, 150.5, 250.4, c) :
        c < 350.5 ? lerp(301, 400, 250.5, 350.4, c) :
        c < 500.5 ? lerp(401, 500, 350.5, 500.4, c) :
        500;
    return Math.round(a);
}
```

The `aqi_pm25` function converts from an AQI value to the corresponding PM2.5 concentration in micrograms per cubic meter (assuming that PM2.5 is the only contributor to AQI). It is the inverse of `pm25_aqi`.

```javascript
function aqi_pm25(aqi) {
    const a = Math.round(aqi);
    const c = a < 0 ? 0 :
        a <= 50 ? lerp(0.0, 12.0, 0, 50, a) :
        a <= 100 ? lerp(12.1, 35.4, 51, 100, a) :
        a <= 150 ? lerp(35.5, 55.4, 101, 150, a) :
        a <= 200 ? lerp(55.5, 150.4, 151, 200, a) :
        a <= 300 ? lerp(150.5, 250.4, 201, 300, a) :
        a <= 400 ? lerp(250.5, 350.4, 301, 400, a) :
        a <= 500 ? lerp(350.5, 500.4, 401, 500, a) :
        500;
    return Math.floor(10 * c) / 10;
}
```

The `lerp` function is like d3.scaleLinear, redux.

```javascript
function lerp(ylo, yhi, xlo, xhi, x) {
    return ((x - xlo) / (xhi - xlo)) * (yhi - ylo) + ylo;
}
```

The `bind` function is like [Inputs.bind](https://github.com/observablehq/inputs/blob/main/README.md#bind), except it applies a transform to convert between units, and only propagates on trusted events. This way when the user interacts with the target, it’ll propagate to the source, but the source won’t propagate back to the target. This transform needs to be invertible so that you can drag either range input to affect the other.

```javascript
function bind(target, source, {
    invalidation = Inputs.disposal(target),
    transform = d => d,
    invert = d => d
} = {}) {
    const onsource = (event) => {
        if (!event.isTrusted) return;
        target.value = transform(source.value);
    };

const ontarget = (event) => {
        if (!event.isTrusted) return;
        source.value = invert(target.value);
        source.dispatchEvent(new Event("input", { bubbles: true }));
    };

target.value = transform(source.value);
    target.addEventListener("input", ontarget);
    source.addEventListener("input", onsource);
    invalidation.then(() => source.removeEventListener("input", onsource));
    return target;
}
```
