Skip to content

[Bug]: Cursor Hover-Over with Polar Plots with Log Scale Incorrect Scaling #26485

@jrbrodie77

Description

@jrbrodie77

Bug summary

In a polar Axes with a log scale for the radial axis, the values reported on mouse hover are incorrect. This is due to a bug in the transData.transform stack.

The bug is located in/near:

matplotlib.projections.polar.InvertedPolarTransform.transform_non_affine

Code for reproduction

"""Example to show bug in Polar plots with Log radial scale."""

import sys
import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd
import numpy as np

import matplotlib

print(f"Matplotlib version: {matplotlib._version.version}")
print(f"Python version: {sys.version}")


_freq = np.arange(100, 10000, 200)
_angle = np.arange(-180, 180, 5) / 57.29

freqs, angles = np.meshgrid(_freq, _angle)
vals = np.random.random(freqs.shape)


fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(111, projection="polar")

ax.set_rscale("log")
ax.set_rlim(200, 20000)

colormesh = ax.pcolormesh(angles, freqs, vals)


# Define a trivial format_coord
# without this function, we get an overflow when cursor hovers
# over axis.
def format_coord(x, y):
    return f"{x}, {y}"


ax.format_coord = format_coord


# Demonstrate the bug. Transform from data coordinates to graphics
# coordinates and then back. The radial value ends up with an
# extra factor of 10^(minimum value of r axis).
# This bug is located in:
# matplotlib.projections.polar.InvertedPolarTransform.transform_non_affine
# see workaround at bottom of this file for more info.

#                     ang, rval
x, r = test_coords = (0.1, 1000)
round_trip_x, round_trip_r = ax.transData.inverted().transform(
    ax.transData.transform(test_coords)
)

if np.isclose(round_trip_x, x):
    print("X value made it through scaling round trip")
else:
    print(f"X value did not make it through scaling was/is: {x}/{round_trip_x}")

if np.isclose(round_trip_r, r):
    print("Y value made it through scaling round trip")
else:
    print(f"Y value did not make it through scaling was/is: {r}/{round_trip_r}")


plt.show()

Actual outcome

Note the power of 10^200 in the lower right hand corner of the plot. This 200 comes from the minimum value of the r-axis.

image image

Expected outcome

The reported value from the cursor output and from the transform round trip should match the plot data. (hovering over (1, 500) should report (1, 500) instead of (1, 1.2e200) etc.

Additional information

The problem lies with the stack of transforms that ends up being contained in ax.transData.inverted()

When there is a polar axes and a log axis, the inverted scaling doesn't happen in the correct order. See the workaround below. I'm using this fix, but I think it's possibly not the best spot to fix the bug, it could just be the order that the transforms are being applied. I didn't have time to completely figure out how/when transData gets built up. (I looked through the nested ._a and ._b of each scaling class to find the Polar transform bug fixed in the workaround.

# WORK AROUND
# This works, but isn't likely the most robust fix. self._axis.get_yscale() == 'log'
# doesn't look robust to me!

def my_transform_non_affine(self, values):
    # docstring inherited
    x, y = values.T
    r = np.hypot(x, y)
    theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi)
    # PolarAxes does not use the theta transforms here, but apply them for
    # backwards-compatibility if not being used by it.
    if self._apply_theta_transforms and self._axis is not None:
        theta -= self._axis.get_theta_offset()
        theta *= self._axis.get_theta_direction()
        theta %= 2 * np.pi
    if self._use_rmin and self._axis is not None:

      ------------ FIX FIX FIX FIX is here.
        if self._axis.get_yscale() == "log":
            base = self._axis.yaxis.get_transform().base
            log_origin = np.log10(self._axis.get_rorigin()) / np.log10(base)
            r += log_origin
        else:
            r += self._axis.get_rorigin()
      -----------


        r *= self._axis.get_rsign()
    return np.column_stack([theta, r])


matplotlib.projections.polar.InvertedPolarTransform.transform_non_affine = (
    my_transform_non_affine
)

Operating system

Mac OS

Matplotlib Version

3.7.2

Matplotlib Backend

MacOSX

Python version

3.8.17

Jupyter version

No response

Installation

conda

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions