From 686a9555703aabb7a1bc04e8667be173128359a6 Mon Sep 17 00:00:00 2001 From: s-weigand Date: Sun, 2 Jul 2023 18:28:35 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=91=8C=20Add=20minor=20ticks=20to=20l?= =?UTF-8?q?inlog=20plots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plotting/plot_concentrations.py | 2 + pyglotaran_extras/plotting/plot_data.py | 3 + pyglotaran_extras/plotting/plot_residual.py | 2 + pyglotaran_extras/plotting/plot_svd.py | 3 + pyglotaran_extras/plotting/plot_traces.py | 2 + pyglotaran_extras/plotting/utils.py | 96 +++++++++++++++++++ 6 files changed, 108 insertions(+) diff --git a/pyglotaran_extras/plotting/plot_concentrations.py b/pyglotaran_extras/plotting/plot_concentrations.py index 692e7a3a..e0aa4c3e 100644 --- a/pyglotaran_extras/plotting/plot_concentrations.py +++ b/pyglotaran_extras/plotting/plot_concentrations.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from pyglotaran_extras.plotting.style import PlotStyle +from pyglotaran_extras.plotting.utils import MinorSymLogLocator from pyglotaran_extras.plotting.utils import add_cycler_if_not_none from pyglotaran_extras.plotting.utils import get_shifted_traces @@ -69,3 +70,4 @@ def plot_concentrations( if linlog: ax.set_xscale("symlog", linthresh=linthresh, linscale=linscale) + ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) diff --git a/pyglotaran_extras/plotting/plot_data.py b/pyglotaran_extras/plotting/plot_data.py index e94f3c75..1ae198eb 100644 --- a/pyglotaran_extras/plotting/plot_data.py +++ b/pyglotaran_extras/plotting/plot_data.py @@ -12,6 +12,7 @@ from pyglotaran_extras.plotting.plot_svd import plot_lsv_data from pyglotaran_extras.plotting.plot_svd import plot_rsv_data from pyglotaran_extras.plotting.plot_svd import plot_sv_data +from pyglotaran_extras.plotting.utils import MinorSymLogLocator from pyglotaran_extras.plotting.utils import not_single_element_dims from pyglotaran_extras.plotting.utils import shift_time_axis_by_irf_location @@ -109,6 +110,7 @@ def plot_data_overview( if linlog: data_ax.set_xscale("symlog", linthresh=linthresh) + data_ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) return fig, (data_ax, lsv_ax, sv_ax, rsv_ax) @@ -150,4 +152,5 @@ def _plot_single_trace( if linlog: ax.set_xscale("symlog", linthresh=linthresh) + ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) return fig, ax diff --git a/pyglotaran_extras/plotting/plot_residual.py b/pyglotaran_extras/plotting/plot_residual.py index 5c3d995e..e5811da9 100644 --- a/pyglotaran_extras/plotting/plot_residual.py +++ b/pyglotaran_extras/plotting/plot_residual.py @@ -7,6 +7,7 @@ from pyglotaran_extras.plotting.plot_irf_dispersion_center import _plot_irf_dispersion_center from pyglotaran_extras.plotting.style import PlotStyle +from pyglotaran_extras.plotting.utils import MinorSymLogLocator from pyglotaran_extras.plotting.utils import add_cycler_if_not_none from pyglotaran_extras.plotting.utils import shift_time_axis_by_irf_location @@ -83,4 +84,5 @@ def plot_residual( ax.legend() if linlog: ax.set_xscale("symlog", linthresh=linthresh) + ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) ax.set_title(title) diff --git a/pyglotaran_extras/plotting/plot_svd.py b/pyglotaran_extras/plotting/plot_svd.py index 6fc44479..796898c8 100644 --- a/pyglotaran_extras/plotting/plot_svd.py +++ b/pyglotaran_extras/plotting/plot_svd.py @@ -6,6 +6,7 @@ from glotaran.io.prepare_dataset import add_svd_to_dataset from pyglotaran_extras.plotting.style import PlotStyle +from pyglotaran_extras.plotting.utils import MinorSymLogLocator from pyglotaran_extras.plotting.utils import add_cycler_if_not_none from pyglotaran_extras.plotting.utils import shift_time_axis_by_irf_location @@ -140,6 +141,7 @@ def plot_lsv_data( ax.set_title("data. LSV") if linlog: ax.set_xscale("symlog", linthresh=linthresh) + ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) def plot_rsv_data( @@ -240,6 +242,7 @@ def plot_lsv_residual( ax.set_title("res. LSV") if linlog: ax.set_xscale("symlog", linthresh=linthresh) + ax.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) def plot_rsv_residual( diff --git a/pyglotaran_extras/plotting/plot_traces.py b/pyglotaran_extras/plotting/plot_traces.py index d982d11f..1adb8b6c 100644 --- a/pyglotaran_extras/plotting/plot_traces.py +++ b/pyglotaran_extras/plotting/plot_traces.py @@ -8,6 +8,7 @@ from pyglotaran_extras.io.utils import result_dataset_mapping from pyglotaran_extras.plotting.style import PlotStyle +from pyglotaran_extras.plotting.utils import MinorSymLogLocator from pyglotaran_extras.plotting.utils import PlotDuplicationWarning from pyglotaran_extras.plotting.utils import add_cycler_if_not_none from pyglotaran_extras.plotting.utils import add_unique_figure_legend @@ -97,6 +98,7 @@ def plot_data_and_fits( [next(axis._get_lines.prop_cycler) for _ in range(2)] if linlog: axis.set_xscale("symlog", linthresh=linthresh) + axis.xaxis.set_minor_locator(MinorSymLogLocator(linthresh)) if show_zero_line is True: axis.axhline(0, color="k", linewidth=1) axis.set_ylabel(y_label) diff --git a/pyglotaran_extras/plotting/utils.py b/pyglotaran_extras/plotting/utils.py index 7449f531..8081ebee 100644 --- a/pyglotaran_extras/plotting/utils.py +++ b/pyglotaran_extras/plotting/utils.py @@ -7,6 +7,7 @@ import numpy as np import xarray as xr +from matplotlib.ticker import Locator from pyglotaran_extras.inspect.utils import pretty_format_numerical_iterable from pyglotaran_extras.io.utils import result_dataset_mapping @@ -482,3 +483,98 @@ def not_single_element_dims(data_array: xr.DataArray) -> list[Hashable]: Names of dimensions in ``data`` which don't have a size equal to one. """ return [dim for dim, values in data_array.coords.items() if values.size != 1] + + +class MinorSymLogLocator(Locator): + """Dynamically find minor tick positions based on major ticks for a symlog scaling. + + Ref.: https://stackoverflow.com/a/45696768 + """ + + def __init__(self, linthresh: float, nints: int = 10) -> None: + """Ticks will be placed between the major ticks. + + The placement is linear for x between -linthresh and linthresh, + otherwise its logarithmically. nints gives the number of + intervals that will be bounded by the minor ticks. + + Parameters + ---------- + linthresh : float + A single float which defines the range (-x, x), within which the plot is linear. + nints : int + Number of minor tick between major ticks. Defaults to 10 + """ + self.linthresh = linthresh + self.nintervals = nints + + def __call__(self) -> list[float]: + """Return the locations of the ticks. + + Returns + ------- + list[float] + Minor ticks position. + """ + # Return the locations of the ticks + majorlocs = self.axis.get_majorticklocs() + + if len(majorlocs) == 1: + return self.raise_if_exceeds(np.array([])) + + # add temporary major tick locs at either end of the current range + # to fill in minor tick gaps + dmlower = majorlocs[1] - majorlocs[0] # major tick difference at lower end + dmupper = majorlocs[-1] - majorlocs[-2] # major tick difference at upper end + + # add temporary major tick location at the lower end + if majorlocs[0] != 0.0 and ( + (majorlocs[0] != self.linthresh and dmlower > self.linthresh) + or (dmlower == self.linthresh and majorlocs[0] < 0) + ): + majorlocs = np.insert(majorlocs, 0, majorlocs[0] * 10.0) + else: + majorlocs = np.insert(majorlocs, 0, majorlocs[0] - self.linthresh) + + # add temporary major tick location at the upper end + if majorlocs[-1] != 0.0 and ( + (np.abs(majorlocs[-1]) != self.linthresh and dmupper > self.linthresh) + or (dmupper == self.linthresh and majorlocs[-1] > 0) + ): + majorlocs = np.append(majorlocs, majorlocs[-1] * 10.0) + else: + majorlocs = np.append(majorlocs, majorlocs[-1] + self.linthresh) + + # iterate through minor locs + minorlocs: list[float] = [] + + # handle the lowest part + for i in range(1, len(majorlocs)): + majorstep = majorlocs[i] - majorlocs[i - 1] + if abs(majorlocs[i - 1] + majorstep / 2) < self.linthresh: + ndivs = self.nintervals + else: + ndivs = self.nintervals - 1 + + minorstep = majorstep / ndivs + locs = np.arange(majorlocs[i - 1], majorlocs[i], minorstep)[1:] + minorlocs.extend(locs) + + return self.raise_if_exceeds(np.array(minorlocs)) + + def tick_values(self, vmin: float, vmax: float) -> None: + """Return the values of the located ticks given **vmin** and **vmax** (not implemented). + + Parameters + ---------- + vmin : float + Minimum value. + vmax : float + Maximum value. + + Raises + ------ + NotImplementedError + Not used + """ + raise NotImplementedError(f"Cannot get tick locations for a {type(self)} type.") From 2500e0a382a01a8f2a2cdcb55c0cdbbdb3d7ceae Mon Sep 17 00:00:00 2001 From: s-weigand Date: Sun, 2 Jul 2023 18:48:18 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=A7=F0=9F=93=9A=20Added=20change?= =?UTF-8?q?=20to=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index bb896d62..0b744820 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ## 0.8.0 (Unreleased) - 🩹 Fix crashes of plot_doas and plot_coherent_artifact for non dispersive IRF (#173) +- 👌 Add minor ticks to linlog plots (#183) - 🚧📦 Remove upper python version limit (#174) (changes-0_7_0)=