From beee6ecd59ddb9fb56192e95f809f828d4262306 Mon Sep 17 00:00:00 2001 From: hksamm Date: Sun, 26 Jul 2026 03:45:11 +0900 Subject: [PATCH] Fix ZeroDivisionError in LinearColormap.to_step() with a single step Step colors are sampled at points spread evenly across the colormap using the weight i / (n - 1.0), which is 0 / 0 when there is only one step. n is recomputed as len(index) - 1, so this affects every path that yields a two-element index -- to_step(n=1), an explicit two-element index=, and the data= variants for the linear, log and quantiles methods -- not just the to_step(n=1) case in the report. A single bin spans the whole range, so sample the middle of the colormap for it rather than either endpoint. The n >= 2 branch is left byte-for-byte identical: index[i + 1] * i / (n - 1.0) parses as (index[i + 1] * i) / (n - 1.0), so hoisting the weight into a variable changes the floating-point result for many n. Co-Authored-By: Claude Opus 5 --- branca/colormap.py | 20 ++++++++++++++------ tests/test_colormap.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/branca/colormap.py b/branca/colormap.py index 2e76ba5..957e4c1 100644 --- a/branca/colormap.py +++ b/branca/colormap.py @@ -445,12 +445,20 @@ def to_step( if round_method == "log10": index = [_base(x) for x in index] - colors = [ - scaled_cm.rgba_floats_tuple( - index[i] * (1.0 - i / (n - 1.0)) + index[i + 1] * i / (n - 1.0), - ) - for i in range(n) - ] + if n == 1: + # Steps are normally sampled at points spread evenly across the + # colormap, the first at `index[0]` and the last at `index[-1]`. + # A single step is the degenerate case of that spread (0 / 0), so + # sample the middle instead: the one bin covers the whole range + # and gets a color to match. + colors = [scaled_cm.rgba_floats_tuple((index[0] + index[1]) / 2.0)] + else: + colors = [ + scaled_cm.rgba_floats_tuple( + index[i] * (1.0 - i / (n - 1.0)) + index[i + 1] * i / (n - 1.0), + ) + for i in range(n) + ] caption = self.caption text_color = self.text_color diff --git a/tests/test_colormap.py b/tests/test_colormap.py index cd30405..eebb263 100644 --- a/tests/test_colormap.py +++ b/tests/test_colormap.py @@ -89,6 +89,25 @@ def test_linear_to_step(): lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1], round_method="log10") +@pytest.mark.parametrize( + "kwargs", + [ + {"n": 1}, + {"index": [0, 1]}, + {"data": [0, 0.5, 1], "n": 1}, + {"data": [0.5, 0.75, 1], "n": 1, "method": "log"}, + {"data": [0, 0.5, 1], "quantiles": [0, 1]}, + ], +) +def test_linear_to_step_single_step(kwargs): + """A one-bin colormap is valid; every path here raised ZeroDivisionError.""" + linear = cm.LinearColormap(["red", "blue"], vmin=0, vmax=1) + step = linear.to_step(**kwargs) + assert len(step.colors) == 1 + # The single bin spans the whole range, so it takes the middle color. + assert step.colors[0] == linear.rgba_floats_tuple(0.5) + + def test_step_to_linear(): step = cm.StepColormap( ["green", "yellow", "red"],