From eb30045efa78a745f31e081ce70f3b04a9e20a36 Mon Sep 17 00:00:00 2001 From: Philip Date: Tue, 15 Jul 2025 12:43:56 +0100 Subject: [PATCH 1/6] Change gridlines to dashed --- pyqttoolkit/views/plot/matplotlib/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 3d659fe..1dd959b 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -312,8 +312,8 @@ def _set_series_visibility(self, handle, visible): def _do_update_grid_lines(self, axes): show_grid_lines = self._options_view and self._options_view.showGridLines gridline_color = axes.spines[list(axes.spines.keys())[0]].get_edgecolor() - gridline_color = gridline_color[0], gridline_color[1], gridline_color[2], 0.5 - kwargs = dict(color=gridline_color, alpha=0.5) if show_grid_lines else {} + gridline_color = gridline_color[0], gridline_color[1], gridline_color[2] + kwargs = dict(color=gridline_color, alpha=0.5, linestyle='--') if show_grid_lines else {} axes.grid(show_grid_lines, **kwargs) self.draw() From 9396ab2d1f418c0c830c202fa252f29bcaaf284c Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 11 May 2026 09:27:34 +0100 Subject: [PATCH 2/6] Concise date time axes --- pyqttoolkit/views/plot/matplotlib/base.py | 46 +++++++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 1dd959b..298726b 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -15,6 +15,7 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from io import BytesIO +import math import numpy as np from datetime import datetime @@ -23,6 +24,7 @@ from PyQt5.QtGui import QKeySequence, QImage import matplotlib +import matplotlib.dates as mdates from matplotlib.legend import DraggableLegend from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure @@ -202,6 +204,7 @@ def __init__(self, self._setting_axis_limits = False self.hasHiddenSeries = False + self._do_update_grid_lines(self._axes) enabledToolsChanged = pyqtSignal() spanChanged = pyqtSignal(SpanModel) @@ -868,16 +871,20 @@ def _update_ticks(self): if not self.data: return if hasattr(self.data, 'x_labels'): - step = self.data.x_tick_interval if hasattr(self.data, 'x_tick_interval') else None - x_ticks, x_labels, x_ticks_rotation = self._get_labels(self.data.x_labels, step, horizontal=True) - self._axes.set_xticks(x_ticks) + if all(isinstance(x, (np.datetime64, pd.Timestamp)) for x in self.data.x_labels): + x_labels = self.data.x_labels + self._update_date_xticks(x_labels) + x_ticks_rotation = 0.0 + else: + step = self.data.x_tick_interval if hasattr(self.data, 'x_tick_interval') else None + x_ticks, x_labels, x_ticks_rotation = self._get_labels(self.data.x_labels, step, horizontal=True) + self._axes.set_xticks(x_ticks, x_labels) if hasattr(self.data, 'x_ticks_rotation'): rotation = x_ticks_rotation if np.isnan(self.data.x_ticks_rotation) else self.data.x_ticks_rotation - self._axes.set_xticklabels( - x_labels, rotation=rotation, ha='right' if rotation else 'center') - self._adjust_to_xticklabels_height(x_labels, rotation) - else: - self._axes.set_xticklabels(x_labels) + self._axes.xaxis.set_tick_params(labelrotation=rotation) + matplotlib.artist.setp(self._axes.get_xticklabels(), + horizontalalignment='right' if rotation else 'center') + self._adjust_to_xticklabels_height(self._axes.get_xticklabels(), rotation) if hasattr(self.data, 'y_labels'): step = self.data.y_tick_interval if hasattr(self.data, 'y_tick_interval') else None @@ -919,6 +926,29 @@ def _adjust_to_xticklabels_height(self, labels, rotation=None): sizes[0] = Size.Fixed(height / self._figure.dpi) self._divider.set_vertical(sizes) + def _update_date_xticks(self, x_labels): + (x0, x1), _ = self._get_xy_extents() + imin, imax = max(0, math.floor(x0)), min(math.ceil(x1), len(x_labels) - 1) + date_num_min, date_num_max = mdates.date2num(x_labels[0]), mdates.date2num(x_labels[-1]) + + locator = mdates.AutoDateLocator(maxticks=min(imax - imin, 25)) + offset_formats = ['', '%Y', '%Y-%b', '%Y-%b', '%Y-%m-%d', '%Y-%m-%d'] + formatter = mdates.ConciseDateFormatter(self.axes.xaxis.get_major_locator(), + offset_formats=offset_formats) + + tick_vals = locator.tick_values(x_labels[imin], x_labels[imax]) + + def _datenum2index(values): + return (len(x_labels) - 1) * (values - date_num_min) / (date_num_max - date_num_min) - 0.5 + + ipositions = _datenum2index(tick_vals) + tick_formats = formatter.format_ticks(tick_vals) + self._axes.set_xticks(ipositions, tick_formats) + if formatter.get_offset(): + self._axes.set_xlabel(f'{self.data.xAxisTitle} ({formatter.get_offset()})') + else: + self._axes.set_xlabel(self.data.xAxisTitle) + def _get_labels(self, labels, step, horizontal=True): (x0, x1), (y0, y1) = self._get_xy_extents() start, end = (int(x0), int(x1)) if horizontal else (int(y0), int(y1)) From 0f1665a89f796166901cf894839579a43d137785 Mon Sep 17 00:00:00 2001 From: Philip Bradstock Date: Mon, 11 May 2026 16:30:05 +0100 Subject: [PATCH 3/6] Also format y ticks --- pyqttoolkit/views/plot/matplotlib/base.py | 50 ++++++++++++++--------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 298726b..9d2c739 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -887,11 +887,15 @@ def _update_ticks(self): self._adjust_to_xticklabels_height(self._axes.get_xticklabels(), rotation) if hasattr(self.data, 'y_labels'): - step = self.data.y_tick_interval if hasattr(self.data, 'y_tick_interval') else None - y_ticks, y_labels, _ = self._get_labels(self.data.y_labels, step, horizontal=False) - self._axes.set_yticks(y_ticks) - self._axes.set_yticklabels(y_labels) - self._adjust_to_yticklabels_width(y_labels) + if all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels): + y_labels = self.data.y_labels + self._update_date_yticks(y_labels) + else: + step = self.data.y_tick_interval if hasattr(self.data, 'y_tick_interval') else None + y_ticks, y_labels, _ = self._get_labels(self.data.y_labels, step, horizontal=False) + self._axes.set_yticks(y_ticks) + self._axes.set_yticklabels(y_labels) + self._adjust_to_yticklabels_width(y_labels) def _adjust_to_yticklabels_width(self, labels): sizes = self._divider.get_horizontal() @@ -926,28 +930,34 @@ def _adjust_to_xticklabels_height(self, labels, rotation=None): sizes[0] = Size.Fixed(height / self._figure.dpi) self._divider.set_vertical(sizes) + def _update_date_yticks(self, y_labels): + x_extent, y_extent = self._get_xy_extents() + ipositions, tick_formats, axis_label = self._determine_date_ticks(y_labels, self.axes.yaxis, self.data.yAxisTitle, y_extent) + self._axes.set_yticks(ipositions, tick_formats) + self._axes.set_ylabel(axis_label) + self._adjust_to_yticklabels_width(tick_formats) + def _update_date_xticks(self, x_labels): - (x0, x1), _ = self._get_xy_extents() - imin, imax = max(0, math.floor(x0)), min(math.ceil(x1), len(x_labels) - 1) - date_num_min, date_num_max = mdates.date2num(x_labels[0]), mdates.date2num(x_labels[-1]) + x_extent, y_extent = self._get_xy_extents() + ipositions, tick_formats, axis_label = self._determine_date_ticks(x_labels, self.axes.xaxis, self.data.xAxisTitle, x_extent) + self._axes.set_xticks(ipositions, tick_formats) + self._axes.set_xlabel(axis_label) + + def _determine_date_ticks(self, labels, axis_obj, axis_title, extent): + e0, e1 = extent + imin, imax = max(0, math.floor(e0)), min(math.ceil(e1), len(labels) - 1) + date_num_min, date_num_max = mdates.date2num(labels[0]), mdates.date2num(labels[-1]) locator = mdates.AutoDateLocator(maxticks=min(imax - imin, 25)) offset_formats = ['', '%Y', '%Y-%b', '%Y-%b', '%Y-%m-%d', '%Y-%m-%d'] - formatter = mdates.ConciseDateFormatter(self.axes.xaxis.get_major_locator(), + formatter = mdates.ConciseDateFormatter(axis_obj.get_major_locator(), offset_formats=offset_formats) - tick_vals = locator.tick_values(x_labels[imin], x_labels[imax]) - - def _datenum2index(values): - return (len(x_labels) - 1) * (values - date_num_min) / (date_num_max - date_num_min) - 0.5 - - ipositions = _datenum2index(tick_vals) + tick_vals = locator.tick_values(labels[imin], labels[imax]) + ipositions = (len(labels) - 1) * (tick_vals - date_num_min) / (date_num_max - date_num_min) - 0.5 tick_formats = formatter.format_ticks(tick_vals) - self._axes.set_xticks(ipositions, tick_formats) - if formatter.get_offset(): - self._axes.set_xlabel(f'{self.data.xAxisTitle} ({formatter.get_offset()})') - else: - self._axes.set_xlabel(self.data.xAxisTitle) + axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title + return ipositions, tick_formats, axis_label def _get_labels(self, labels, step, horizontal=True): (x0, x1), (y0, y1) = self._get_xy_extents() From 498b2ae3614985dd788b8541cc2a3a7d7a3e79df Mon Sep 17 00:00:00 2001 From: Philip Bradstock Date: Fri, 22 May 2026 21:04:44 +0100 Subject: [PATCH 4/6] revert gridlines commit to keep it separate --- pyqttoolkit/views/plot/matplotlib/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 9d2c739..6c94f49 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -315,8 +315,8 @@ def _set_series_visibility(self, handle, visible): def _do_update_grid_lines(self, axes): show_grid_lines = self._options_view and self._options_view.showGridLines gridline_color = axes.spines[list(axes.spines.keys())[0]].get_edgecolor() - gridline_color = gridline_color[0], gridline_color[1], gridline_color[2] - kwargs = dict(color=gridline_color, alpha=0.5, linestyle='--') if show_grid_lines else {} + gridline_color = gridline_color[0], gridline_color[1], gridline_color[2], 0.5 + kwargs = dict(color=gridline_color, alpha=0.5) if show_grid_lines else {} axes.grid(show_grid_lines, **kwargs) self.draw() From 083a6cbee47b1f1a9b894c4d45004e0dfde52494 Mon Sep 17 00:00:00 2001 From: phil-brad <35700541+phil-brad@users.noreply.github.com> Date: Tue, 26 May 2026 10:56:26 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyqttoolkit/views/plot/matplotlib/base.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 6c94f49..4429320 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -204,7 +204,7 @@ def __init__(self, self._setting_axis_limits = False self.hasHiddenSeries = False - self._do_update_grid_lines(self._axes) + self._axes.grid(False) enabledToolsChanged = pyqtSignal() spanChanged = pyqtSignal(SpanModel) @@ -884,10 +884,10 @@ def _update_ticks(self): self._axes.xaxis.set_tick_params(labelrotation=rotation) matplotlib.artist.setp(self._axes.get_xticklabels(), horizontalalignment='right' if rotation else 'center') - self._adjust_to_xticklabels_height(self._axes.get_xticklabels(), rotation) + self._adjust_to_xticklabels_height(x_labels, rotation) if hasattr(self.data, 'y_labels'): - if all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels): + if self.data.y_labels and all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels): y_labels = self.data.y_labels self._update_date_yticks(y_labels) else: @@ -948,11 +948,18 @@ def _determine_date_ticks(self, labels, axis_obj, axis_title, extent): imin, imax = max(0, math.floor(e0)), min(math.ceil(e1), len(labels) - 1) date_num_min, date_num_max = mdates.date2num(labels[0]), mdates.date2num(labels[-1]) - locator = mdates.AutoDateLocator(maxticks=min(imax - imin, 25)) + locator = mdates.AutoDateLocator(maxticks=max(1, min(imax - imin, 25))) offset_formats = ['', '%Y', '%Y-%b', '%Y-%b', '%Y-%m-%d', '%Y-%m-%d'] - formatter = mdates.ConciseDateFormatter(axis_obj.get_major_locator(), + formatter = mdates.ConciseDateFormatter(locator, offset_formats=offset_formats) + if date_num_max == date_num_min: + tick_vals = np.array([date_num_min]) + ipositions = np.array([(imin + imax) / 2.0]) + tick_formats = formatter.format_ticks(tick_vals) + axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title + return ipositions, tick_formats, axis_label + tick_vals = locator.tick_values(labels[imin], labels[imax]) ipositions = (len(labels) - 1) * (tick_vals - date_num_min) / (date_num_max - date_num_min) - 0.5 tick_formats = formatter.format_ticks(tick_vals) From 77a7c47c6c4c8168010c762ac8e6bda38891aa10 Mon Sep 17 00:00:00 2001 From: Philip Bradstock Date: Tue, 26 May 2026 12:07:12 +0100 Subject: [PATCH 6/6] Fix inverted y axis --- pyqttoolkit/views/plot/matplotlib/base.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pyqttoolkit/views/plot/matplotlib/base.py b/pyqttoolkit/views/plot/matplotlib/base.py index 4429320..6e876fc 100644 --- a/pyqttoolkit/views/plot/matplotlib/base.py +++ b/pyqttoolkit/views/plot/matplotlib/base.py @@ -871,7 +871,7 @@ def _update_ticks(self): if not self.data: return if hasattr(self.data, 'x_labels'): - if all(isinstance(x, (np.datetime64, pd.Timestamp)) for x in self.data.x_labels): + if len(self.data.x_labels) > 0 and all(isinstance(x, (np.datetime64, pd.Timestamp)) for x in self.data.x_labels): x_labels = self.data.x_labels self._update_date_xticks(x_labels) x_ticks_rotation = 0.0 @@ -884,10 +884,10 @@ def _update_ticks(self): self._axes.xaxis.set_tick_params(labelrotation=rotation) matplotlib.artist.setp(self._axes.get_xticklabels(), horizontalalignment='right' if rotation else 'center') - self._adjust_to_xticklabels_height(x_labels, rotation) + self._adjust_to_xticklabels_height(self._axes.get_xticklabels(), rotation) if hasattr(self.data, 'y_labels'): - if self.data.y_labels and all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels): + if len(self.data.y_labels) > 0 and all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels): y_labels = self.data.y_labels self._update_date_yticks(y_labels) else: @@ -946,7 +946,10 @@ def _update_date_xticks(self, x_labels): def _determine_date_ticks(self, labels, axis_obj, axis_title, extent): e0, e1 = extent imin, imax = max(0, math.floor(e0)), min(math.ceil(e1), len(labels) - 1) - date_num_min, date_num_max = mdates.date2num(labels[0]), mdates.date2num(labels[-1]) + # Convert all labels to date numbers for robust min/max handling + label_dates = [mdates.date2num(lbl) for lbl in labels] + date_num_min = min(label_dates) + date_num_max = max(label_dates) locator = mdates.AutoDateLocator(maxticks=max(1, min(imax - imin, 25))) offset_formats = ['', '%Y', '%Y-%b', '%Y-%b', '%Y-%m-%d', '%Y-%m-%d'] @@ -960,12 +963,19 @@ def _determine_date_ticks(self, labels, axis_obj, axis_title, extent): axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title return ipositions, tick_formats, axis_label - tick_vals = locator.tick_values(labels[imin], labels[imax]) - ipositions = (len(labels) - 1) * (tick_vals - date_num_min) / (date_num_max - date_num_min) - 0.5 + # Ensure correct order for tick_values + if label_dates[imin] <= label_dates[imax]: + tick_vals = locator.tick_values(labels[imin], labels[imax]) + ipositions = (len(labels) - 1) * (tick_vals - date_num_min) / (date_num_max - date_num_min) - 0.5 + else: + tick_vals = locator.tick_values(labels[imax], labels[imin]) + ipositions = (len(labels) - 1) * (tick_vals[::-1] - date_num_min) / (date_num_max - date_num_min) - 0.5 + tick_formats = formatter.format_ticks(tick_vals) axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title return ipositions, tick_formats, axis_label + def _get_labels(self, labels, step, horizontal=True): (x0, x1), (y0, y1) = self._get_xy_extents() start, end = (int(x0), int(x1)) if horizontal else (int(y0), int(y1))