diff --git a/autoarray/plot/utils.py b/autoarray/plot/utils.py index cc095d99f..9dde85441 100644 --- a/autoarray/plot/utils.py +++ b/autoarray/plot/utils.py @@ -1143,20 +1143,32 @@ def _inward_ticks(lo: float, hi: float, factor: float, n: int) -> np.ndarray: ) -def _round_ticks(values: np.ndarray, sig: int = 2) -> np.ndarray: - """Round *values* to *sig* significant figures. +def _round_ticks(values: np.ndarray, sig: int = 2, max_sig: int = 10) -> np.ndarray: + """Round *values* to *sig* significant figures, adding figures until distinct ticks stay distinct. + + Two significant figures suit an extent that spans its own magnitude (e.g. -2" to 2"), but on a + narrow extent far from zero (a few milli-arcseconds at 1.2") every tick rounds to the same value + and the labels collapse to one repeated number. The significant figures are therefore raised, up + to *max_sig*, until the rounded ticks are as distinct as the input ticks. After rounding, values smaller than 1e-10 of the overall tick scale are clamped to zero so that floating-point noise (e.g. 1e-16 centre ticks on symmetric extents) does not appear as scientific notation in labels. """ - with np.errstate(divide="ignore", invalid="ignore"): - nonzero = np.where(values != 0, np.abs(values), 1.0) - mags = np.where(values != 0, 10 ** (sig - 1 - np.floor(np.log10(nonzero))), 1.0) - rounded = np.round(values * mags) / mags - scale = float(np.max(np.abs(rounded))) if len(rounded) > 0 else 1.0 - if scale > 0: - rounded[np.abs(rounded) < scale * 1e-10] = 0.0 + values = np.asarray(values, dtype=float) + n_distinct = len(np.unique(values)) + for n_sig in range(sig, max(sig, max_sig) + 1): + with np.errstate(divide="ignore", invalid="ignore"): + nonzero = np.where(values != 0, np.abs(values), 1.0) + mags = np.where( + values != 0, 10 ** (n_sig - 1 - np.floor(np.log10(nonzero))), 1.0 + ) + rounded = np.round(values * mags) / mags + scale = float(np.max(np.abs(rounded))) if len(rounded) > 0 else 1.0 + if scale > 0: + rounded[np.abs(rounded) < scale * 1e-10] = 0.0 + if len(np.unique(rounded)) == n_distinct: + break return rounded diff --git a/test_autoarray/plot/test_utils.py b/test_autoarray/plot/test_utils.py index c8a08912f..5c4c9b155 100644 --- a/test_autoarray/plot/test_utils.py +++ b/test_autoarray/plot/test_utils.py @@ -619,3 +619,35 @@ def test__plot_regions__single_polygon_region_is_labelled_once(): ) finally: plt.close(figure) + + +def test__round_ticks__two_significant_figures_by_default(): + from autoarray.plot.utils import _round_ticks + + assert list(_round_ticks(np.array([-1.875, 0.0, 1.875]))) == [-1.9, 0.0, 1.9] + + +def test__round_ticks__adds_significant_figures_until_ticks_are_distinct(): + from autoarray.plot.utils import _round_ticks + + # A few milli-arcseconds at 1.2": two (and three) significant figures collapse every tick to 1.2. + ticks = _round_ticks(np.array([1.19655, 1.2, 1.20345])) + + assert len(np.unique(ticks)) == 3 + assert list(ticks) == [1.197, 1.2, 1.203] + + +def test__apply_extent__narrow_extent_far_from_zero_has_distinct_tick_labels(): + import matplotlib.pyplot as plt + + from autoarray.plot.utils import apply_extent + + fig, ax = plt.subplots() + apply_extent(ax, [1.1954, 1.2046, -0.8046, -0.7954]) + + xlabels = [label.get_text() for label in ax.get_xticklabels()] + ylabels = [label.get_text() for label in ax.get_yticklabels()] + plt.close(fig) + + assert len(set(xlabels)) == len(xlabels) == 3 + assert len(set(ylabels)) == len(ylabels) == 3