Skip to content

feat: add Worldview Rainbow 4 and NAQFC colormaps - #200

Merged
maxrjones merged 8 commits into
mainfrom
feat/naqfc-tempo-colormaps
Oct 20, 2026
Merged

maxrjones merged 8 commits into
mainfrom
feat/naqfc-tempo-colormaps

Conversation

@maxrjones

@maxrjones maxrjones commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR adds 3 custom colormaps requested for TEMPO and NAQFC rendering for the VEDA portal.

Testing

Tested using by appending this to the dev deployment URL: WebMercatorQuad/map.html?url=s3%3A%2F%2Fairquality-data-store-develop%2Ftempo%2Fno2%2Fv04%2F&variable=vertical_column_troposphere&sel=time%3Dnearest%3A%3A2026-10-06T17%3A37%3A16Z&algorithm=mean&where=eff_cloud_fraction<%3D0.5&where=solar_zenith_angle<%3D80&where=main_data_quality_flag<%3D1&tilesize=512&datetime=2020-01-01T00%3A00%3A00Z%2F2026-10-06T17%3A37%3A16Z&colormap_name=worldview_rainbow_4&rescale=0%2C20000000000000000&resampling=nearest

PR checks

  • Standard CI runs automatically on each push.
  • To run the CDK synth check, add the run-cdk-checks label to this PR.
  • If you push more commits after that run completes, remove and re-add the label to run it again.
  • To trigger a dev deployment, add the deploy-dev label. It smoke-tests tiles from the native MUR, virtual MUR, and virtual NLDAS Icechunk stores after deployment.

Register custom colormaps from `src/titiler/multidim/cmap_data/*.npy` and
expose them through both the tile endpoints (`colormap_dependency`) and
`/colorMaps` (`supported_colormaps`), the same wiring veda-backend uses.

Three colormaps ship, generated by `scripts/make_cmaps.py` (numpy only):

- `worldview_rainbow_4`: NASA Worldview's "Rainbow 4" palette, verbatim
  from nasa-gibs/worldview (255 colors, top color repeated), for TEMPO NO2
  with e.g. `rescale=0,2e16`.
- `naqfc_o3`: NOAA Air Quality Forecast Guidance Viewer ozone ramp over
  0-200 ppb (`rescale=0,200`, or `0,0.2` for ppm).
- `naqfc_pm25`: the ozone ramp stretched so each EPA 24-hour PM2.5 AQI
  category spans the same colors as its 8-hour ozone category, over
  0-225.4 ug/m3 (`rescale=0,225.4`).

`tests/test_cmaps.py` regenerates every file and fails if a committed
`.npy` differs from the script, checks the Worldview palette is verbatim,
and requests a tile and the `/colorMaps` entries through the app.

Near-term counterpart to NASA-IMPACT/veda-backend#665 for data served by
titiler-multidim. Longer term these move to a shared colormap package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chuckwondo

Copy link
Copy Markdown
Contributor

Thanks for putting this together, Max. The colormaps themselves look great, and I especially like that the Worldview palette is a verbatim copy of the original and that the known limits are included.

My one real concern is the generated .npy files. The drift test does a nice job of keeping them honest, but building the tables when the package is imported would completely eliminate their need, and thus eliminate any drift to worry about.

I'm sorry to send you another gist. I started with just that change, and as I worked through it I kept finding small things to tidy, so it grew. The three colormaps come out exactly the same, and the notes at the top of the gist explain each change. Please treat everything beyond the .npy change as suggestions you can take or leave.

Since @hrodmn has already approved, feel free to merge this as is if that's easier, and I can open a follow-up PR with these changes, if you like.

https://gist.github.com/chuckwondo/d97813dd8889d648bb2cb8ce31774d39

@chuckwondo chuckwondo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @maxrjones. My review is in this comment.

@maxrjones

Copy link
Copy Markdown
Member Author

I should've mentioned in the beginning that I think this is a temporary solution, and adds a bit of tech debt. See https://github.com/NASA-IMPACT/veda-architecture/issues/899 for a proposal for a more sustainable approach.

@chuckwondo thanks for your comments. I similarly dislike committed .npy files. I'll look into your suggestions, and maybe we can port them to veda-backend as well in the future, which also uses the .npy files.

@chuckwondo

Copy link
Copy Markdown
Contributor

I should've mentioned in the beginning that I think this is a temporary solution, and adds a bit of tech debt. See NASA-IMPACT/veda-architecture#899 for a proposal for a more sustainable approach.

@chuckwondo thanks for your comments. I similarly dislike committed .npy files. I'll look into your suggestions, and maybe we can port them to veda-backend as well in the future, which also uses the .npy files.

Thanks Max. A shared library sounds like the right home for these, and I added a few thoughts on NASA-IMPACT/veda-architecture#899.

maxrjones and others added 5 commits October 19, 2026 15:15
Per review on #200: the three colormaps are now built when the package is
imported (`titiler/multidim/colormaps.py`), so there are no generated files
to drift from their source. This removes `scripts/make_cmaps.py`, the
`cmap_data/` .npy files, and the drift test. The generated tables are
byte-identical to the previous .npy files.

Also from the review:
- the dict passed to `cmap.register` is the only list of colormaps
  (previously a script dict plus a directory glob in main.py)
- ramp stops and AQI breakpoints are dicts, so pairs can't misalign
- rio-tiler's `parse_color` replaces the hand-written hex parsers
- `rainbow_4.json` lives in the package and is read via importlib.resources
- the Worldview source link is pinned to commit 7d16ee5
- the colormap table and known limits move into the main README
- tests check every rainbow color, catch out-of-order interpolation
  points, and are parametrized per colormap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every colormap is now a `cmap_data/<name>.json` file with a `source` and
either `stops` (data value -> "#rrggbb") or `ramp` + `breakpoints` (data
value -> value on another file's stops). `colormaps.py` registers each file
under its name and interpolates it into a 256-row table, so the ozone ramp
and PM2.5 breakpoints are data rather than Python constants.

Worldview's Rainbow 4 is converted to one stop per row (alpha was ff for
all 255 colors; the top color is repeated for row 255), so the upstream
file is no longer kept verbatim; its `source` pins the upstream commit.

All three generated tables are identical to the previous ones. Tests now
check every file against the schema and every stop/breakpoint list for
increasing order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an optional `notes` field to the cmap_data schema and move the NAQFC
screenshot-sampling limit into naqfc_o3.json (naqfc_pm25.json points to it),
next to the stops it describes. The README keeps what API users need, since
/colorMaps serves only the table: each colormap's `rescale`, a one-line
description linking to its file, and the limits that apply to every custom
colormap. Provenance details live only in each file's `source`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each colormap file now has a `reference` (an https URL) alongside its
`source` text, which no longer embeds links: the pinned Worldview file,
veda-backend#665 for the NAQFC ozone stops, and the AirNow AQI Technical
Assistance Document for the PM2.5 breakpoints. The schema test checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PM2.5 (88101, 24 HOUR) and 8-hour ozone (44201) breakpoints in
naqfc_pm25.json and the naqfc_o3 notes match this table exactly
(9.0/35.4/55.4/125.4/225.4 ug/m3; 54/70/85/105/200 ppb).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maxrjones

Copy link
Copy Markdown
Member Author

Thanks again for your comments, @chuckwondo. I've re-requested a review because I applied your suggestions, but then made some minor changes because I'd rather have the colormaps in a schematized format rather than a .py module. So, the current implementation isn't exactly what you recommended.

@chuckwondo chuckwondo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @maxrjones. Looks great! I like the move of the tables from python code into separate resource-loadable json files. Nice adjustment.

I'm approving, but there are 2 more suggestions I've made to avoid a couple of traps.

Comment thread uv.lock
Comment on lines +15 to +18
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P1W"

@chuckwondo chuckwondo Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you have somehow set exclude-newer, perhaps in your own global uv config file, or the UV_EXCLUDE_NEWER env var, which causes the addition of these lines.

Unfortunately, this is not portable because someone who does not also have the same setting will cause this entry to be removed (via uv sync), resulting in unnecessary churn in the lock file.

However, this can be robustly remedied by adding the following block in pyproject.toml, which will ensure the lines remain in place even when someone else does not have the same config setting you have:

[tool.uv]
exclude-newer = "1 week"

@chuckwondo chuckwondo Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: I edited the comment above. Previously I suggested removing the extra uv.lock lines, but that was a mistake. That's what would happen without having the additional block I suggest adding to pyproject.toml and also without having the same global config you have.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the tables to JSON files is a nice improvement!

The only trap we need to avoid is the potential for accidental duplicate keys, which can go silently unnoticed. For example, if we were to accidentally write the "110" key as "100", it would be a duplicate of the preceding, correct entry for "100", but json.loads doesn't complain about duplicate keys. It simply allows the last key to "win".

However, if we add the following to .pre-commit-config.yaml, it will fail pre-commit if any JSON file contains duplicate keys anywhere (I suggest listing it as the first repo in the repos list. Although order doesn't matter, it's simply conventional for pre-commit's own hooks.):

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
    - id: check-json

Comment thread tests/test_cmaps.py Resolved
Comment thread tests/test_cmaps.py Resolved
Comment thread tests/test_cmaps.py Resolved
Co-authored-by: Chuck Daniels <cjdaniels4@gmail.com>
Set exclude-newer to "1 week" under [tool.uv]. The [options] block in
uv.lock then comes from the project instead of a contributor's global uv
config. A plain `uv sync` no longer strips it.

Add the check-json hook from pre-commit-hooks. json.loads keeps the last
of two duplicate keys without warning. A mistyped breakpoint key in
cmap_data would otherwise replace an earlier entry silently.

Co-authored-by: Chuck Daniels <cjdaniels4@gmail.com>
@maxrjones
maxrjones enabled auto-merge (squash) October 20, 2026 16:36
@maxrjones
maxrjones merged commit 304ec66 into main Oct 20, 2026
13 checks passed
@maxrjones
maxrjones deleted the feat/naqfc-tempo-colormaps branch October 20, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants