Skip to content

feat(styling): add ColorScaling.log for a plain-logarithmic colour scale - #330

Merged
MAfarrag merged 9 commits into
mainfrom
feat/colorscaling-lognorm
Sep 5, 2026
Merged

feat(styling): add ColorScaling.log for a plain-logarithmic colour scale#330
MAfarrag merged 9 commits into
mainfrom
feat/colorscaling-lognorm

Conversation

@MAfarrag

@MAfarrag MAfarrag commented Sep 4, 2026

Copy link
Copy Markdown
Member

Description

ColorScaling could express linear / power / sym-lognorm / boundary-norm / midpoint, but not a plain
matplotlib.colors.LogNorm
— even though the string-keyed data-style path in styling/colors.py already builds
one (with a positive-data check). So a caller using the typed grouped colour object — the API pyramids re-exports
and Dataset.plot(color=...) accepts — could not ask for a plain log scale, and sym_log is not a substitute (it
is linear within +/-linthresh, so it renders strictly-positive data's low end differently and cannot reproduce a
LogNorm).

This adds the missing variant and, per the issue, makes the two log paths share one validation so they agree on what
a log scale requires.

  • Add a LOGNORM member to ColorScale ("lognorm") and a ColorScaling.log() variant constructor plus its
    build_norm branch, producing a LogNorm over the positive tick range (same LogFormatter colorbar as
    sym_log).
  • Extract a shared build_log_norm(vmin, vmax, *, context=...) helper in styling.colors and route both the
    data-style norm="log" path and ColorScaling.log() through it, so the two agree on the requirement: a
    strictly-positive, ascending range. A non-positive range raises a clear error steering the caller at a
    symmetric-log scale. The data-style path's positive-data fallback (clamping vmin when the data hits 0) is
    preserved.

Verified end to end: ArrayGlyph.plot(color=ColorScaling.log()) renders a LogNorm; the issue's reproduction now
succeeds.

Issues

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Dev changes (CI/pyproject.toml/docs/examples/testing)

How Has This Been Tested?

  • New tests in tests/test_scaling.pyColorScaling.log().build_norm(...) builds a LogNorm over the tick
    range, a non-positive range raises (steering at sym_log), the options round-trip (color_scale="lognorm" ->
    LOGNORM), and log() emits the full six-key option dict.
  • tests/test_colors.py — the 3 data-style norm='log' error tests updated to the unified message; the
    positive-vmin-clamp fallback test still passes. The shared build_log_norm helper carries a doctest.
  • Regression sweep — test_scaling.py / test_colors.py / test_glyph.py / test_array_glyph.py all green
    (933 passed); doctests on scaling.py / styles.py / colors.py57 passed.
  • End-to-end: ArrayGlyph.plot(color=ColorScaling.log()) yields im.norm of type LogNorm (vmin=1, vmax=1000).

Checklist:

  • updated version number in pyproject.toml
  • added changes to History.rst
  • updated the latest version in README file
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

ColorScaling could express linear/power/sym-lognorm/boundary-norm/midpoint
but not a plain matplotlib LogNorm, even though the string-keyed data-style
path (styling.colors) already builds one. So a caller using the typed
grouped colour object -- the API pyramids re-exports and Dataset.plot(color=)
accepts -- could not ask for a log scale, and sym_log is not a substitute
(it is linear within +/-linthresh, so it renders strictly-positive data's
low end differently).

- Add a LOGNORM member to ColorScale and a ColorScaling.log() variant
  constructor plus its build_norm branch, producing a LogNorm over the
  positive tick range.
- Extract a shared build_log_norm(vmin, vmax, context=...) helper in
  styling.colors and route both the data-style norm='log' path and
  ColorScaling.log() through it, so the two agree on what a log scale
  requires: a strictly-positive, ascending range. Non-positive ranges raise
  a clear error steering the caller at a symmetric-log scale.
- Tests for the constructor, build_norm, options round-trip, and the shared
  helper; docs and the norm-dispatch diagram updated.

Closes #329
…nt data)

A constant positive field yields a single tick, so vmin == vmax, and the
LOGNORM branch called build_log_norm(v, v) which raised -- crashing
ArrayGlyph.plot(color=ColorScaling.log()) on data every other scale kind
renders, with a message that wrongly told the user their (strictly positive)
data spans zero/negatives. Widen the zero-width range to [v, v+1], matching
the data-style norm='log' path, so the two agree; the error now fires only
for genuinely non-positive data. Passing floats also cleans the message.
…log_norm

The shared log-range error rendered numpy scalars via !r (e.g.
'vmin=np.float64(5.0)') on the ColorScaling.log() path, and the unified
message dropped the concrete keyword the old data-style message gave. Coerce
vmin/vmax to plain floats in the message, and add a per-caller 'remedy'
argument so the data-style path names norm='symlog' and the ColorScaling
path names ColorScaling.sym_log().
The public docs were updated for the new lognorm scale but three in-source
docstrings (glyph._create_norm_and_cbar_kw, array_glyph._plot_im_get_cbar_kw,
mesh_glyph) still enumerated only the five original scales. Add lognorm.
…gNorm

Assert that ArrayGlyph.plot(color=ColorScaling.log()) puts a LogNorm on the
image with the expected vmin/vmax -- the render path the PR verified only by
hand.
…Scaling.log()

The degenerate-range widening ran before build_log_norm built its error, so a
constant negative/zero field's message reported the widened vmax (e.g. -4.0
for all -5.0 data) rather than the real bound. Gate the widen on lo > 0 so a
non-positive constant raises with its true value; positive constants still
widen to [v, v+1].
Round 1 added lognorm to the in-source docstrings but two mesh docs still
said 'all 5 color scale types': bump both to 6, add a lognorm row to the
scale table, and add ColorScaling.log() to the factory list.
The new LOGNORM member broke test_member_names_and_values, which pins the
exact enum value set (it expected the original five). Add lognorm to the set,
bump the docstring 'five' -> 'six', and add a case-insensitive construction
case for it.
… S5778)

The two ColorScaling.log() error tests invoked ColorScaling.log(),
build_norm and np.array inside the pytest.raises block. Build the scale and
ticks first so only the throwing build_norm call sits in the block.
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@MAfarrag
MAfarrag merged commit 9ba42c5 into main Sep 5, 2026
10 checks passed
@MAfarrag
MAfarrag deleted the feat/colorscaling-lognorm branch September 5, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ColorScaling has no plain-log variant, though styling.colors already builds LogNorm

1 participant