# Oregon County Distress × Demographic Fragility — Interactive Chart

A self-contained static single-page HTML visualization that overlays Business Oregon's
economic distress index against a derived demographic-fragility risk index for the 36
Oregon counties in the merged analytic dataset.

## Files

- `index.html` — entry point. Open directly in a browser or serve statically.
- `styles.css` — design tokens, layout, chart styling.
- `app.js` — D3 v7 chart, tooltip, details panel, search, filter chips, reset, and
  the embedded dataset (`RAW = [...]`).
- `data.csv` — source CSV copied from
  `oregon_policy_population/outputs/oregon_county_population_policy_risk_with_business_oregon_distress.csv`.
- `data.json` — same dataset normalized to JSON (kept alongside as a reference; the
  page itself does not fetch it because the data is embedded directly in `app.js`).
- `qa_desktop_final.png`, `qa_mobile_final.png` — QA screenshots.

## Stack

- D3 v7 from the official CDN — only runtime dependency.
- Fonts: **Fraunces** (display) + **Inter** (body) + **JetBrains Mono** (numerics),
  loaded from Google Fonts with system-font fallbacks.
- No backend, no `localStorage`/`sessionStorage`/cookies, no build step.

## Updating the data

The page is self-contained: the dataset is embedded inside `app.js` as the `RAW`
constant near the top. To refresh from a new CSV:

```bash
cp /path/to/new.csv data.csv
python3 -c "
import csv, json
rows = []
with open('data.csv') as f:
    for row in csv.DictReader(f):
        for k, v in list(row.items()):
            if v == '': row[k] = None
            else:
                try: row[k] = float(v)
                except: pass
        for k in ('biz_is_distressed_2026','biz_misses_demographic_watchlist','biz_distressed_but_low_demographic_risk'):
            if isinstance(row.get(k), str): row[k] = (row[k].lower() == 'true')
        rows.append(row)
print(json.dumps(rows, separators=(',',':')))
" > _new_data.json
# Then paste the contents of _new_data.json after `const RAW = ` in app.js
```

The field-to-display mapping lives in `app.js` in the `data = RAW.map(...)` block.
Add a new field there and a new row in the relevant `renderDetails` `kv-*` section
to surface it in the details panel.

## Field/label changes

- Axis titles, threshold labels, and quadrant labels: search for `axis-title`,
  `threshold-label`, and the `quads` array in `app.js`.
- Details panel section headings: in `index.html` under `aside.details`.
- Filter chip labels: in `index.html` inside `.chips`.
- Source/footer copy: in `index.html` inside `footer.foot`.
- Color tokens (warm neutrals, teal, rust, watchlist brown): the `:root` block at
  the top of `styles.css`.

## Local preview

```bash
cd business_oregon_interactive_chart
python3 -m http.server 8000
# open http://localhost:8000/
```

## Notes

- Multnomah is in the dataset but is not plotted because its PSU-forecast-based
  demographic risk index is unavailable at this county geography. This is called
  out on the page itself.
- The labeling heuristic shows direct labels only for "notable" counties
  (demographic risk ≥ 55, distress index ≤ 0.85, or the currently selected county),
  with a greedy de-overlap routine.
