Skip to content

Commit 3c55453

Browse files
authored
Merge pull request #4007 from t20100/plot-update-mpl-tick-formatter
silx.gui.plot.PlotWidget: Updated plot axes tick labels behavior
2 parents ff756fc + 001162d commit 3c55453

File tree

3 files changed

+143
-51
lines changed

3 files changed

+143
-51
lines changed

src/silx/gui/plot/backends/BackendMatplotlib.py

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@
4444
from ... import qt
4545

4646
# First of all init matplotlib and set its backend
47-
from ...utils.matplotlib import FigureCanvasQTAgg, qFontToFontProperties
47+
from ...utils.matplotlib import (
48+
DefaultTickFormatter,
49+
FigureCanvasQTAgg,
50+
qFontToFontProperties,
51+
)
4852
import matplotlib
4953
from matplotlib.container import Container
5054
from matplotlib.figure import Figure
@@ -54,7 +58,7 @@
5458
from matplotlib.lines import Line2D
5559
from matplotlib.text import Text
5660
from matplotlib.collections import PathCollection, LineCollection
57-
from matplotlib.ticker import Formatter, ScalarFormatter, Locator
61+
from matplotlib.ticker import Formatter, Locator
5862
from matplotlib.tri import Triangulation
5963
from matplotlib.collections import TriMesh
6064
from matplotlib import path as mpath
@@ -544,21 +548,9 @@ def __init__(self, plot, parent=None):
544548
# Set axis zorder=0.5 so grid is displayed at 0.5
545549
self.ax.set_axisbelow(True)
546550

547-
# disable the use of offsets
548-
try:
549-
axes = [
550-
self.ax.get_yaxis().get_major_formatter(),
551-
self.ax.get_xaxis().get_major_formatter(),
552-
self.ax2.get_yaxis().get_major_formatter(),
553-
self.ax2.get_xaxis().get_major_formatter(),
554-
]
555-
for axis in axes:
556-
axis.set_useOffset(False)
557-
axis.set_scientific(False)
558-
except:
559-
_logger.warning(
560-
"Cannot disabled axes offsets in %s " % matplotlib.__version__
561-
)
551+
# Configure axes tick label formatter
552+
for axis in (self.ax.yaxis, self.ax.xaxis, self.ax2.yaxis, self.ax2.xaxis):
553+
axis.set_major_formatter(DefaultTickFormatter())
562554

563555
self.ax2.set_autoscaley_on(True)
564556

@@ -1263,6 +1255,23 @@ def setGraphYLimits(self, ymin, ymax, axis):
12631255

12641256
# Graph axes
12651257

1258+
def __initXAxisFormatterAndLocator(self):
1259+
if self.ax.xaxis.get_scale() != "linear":
1260+
return # Do not override formatter and locator
1261+
1262+
if not self.isXAxisTimeSeries():
1263+
self.ax.xaxis.set_major_formatter(DefaultTickFormatter())
1264+
return
1265+
1266+
# We can't use a matplotlib.dates.DateFormatter because it expects
1267+
# the data to be in datetimes. Silx works internally with
1268+
# timestamps (floats).
1269+
locator = NiceDateLocator(tz=self.getXAxisTimeZone())
1270+
self.ax.xaxis.set_major_locator(locator)
1271+
self.ax.xaxis.set_major_formatter(
1272+
NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone())
1273+
)
1274+
12661275
def setXAxisTimeZone(self, tz):
12671276
super(BackendMatplotlib, self).setXAxisTimeZone(tz)
12681277

@@ -1274,24 +1283,7 @@ def isXAxisTimeSeries(self):
12741283

12751284
def setXAxisTimeSeries(self, isTimeSeries):
12761285
self._isXAxisTimeSeries = isTimeSeries
1277-
if self._isXAxisTimeSeries:
1278-
# We can't use a matplotlib.dates.DateFormatter because it expects
1279-
# the data to be in datetimes. Silx works internally with
1280-
# timestamps (floats).
1281-
locator = NiceDateLocator(tz=self.getXAxisTimeZone())
1282-
self.ax.xaxis.set_major_locator(locator)
1283-
self.ax.xaxis.set_major_formatter(
1284-
NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone())
1285-
)
1286-
else:
1287-
try:
1288-
scalarFormatter = ScalarFormatter(useOffset=False)
1289-
except:
1290-
_logger.warning(
1291-
"Cannot disabled axes offsets in %s " % matplotlib.__version__
1292-
)
1293-
scalarFormatter = ScalarFormatter()
1294-
self.ax.xaxis.set_major_formatter(scalarFormatter)
1286+
self.__initXAxisFormatterAndLocator()
12951287

12961288
def setXAxisLogarithmic(self, flag):
12971289
# Workaround for matplotlib 2.1.0 when one tries to set an axis
@@ -1303,8 +1295,10 @@ def setXAxisLogarithmic(self, flag):
13031295
self.ax.set_xlim(1, 10)
13041296
self.draw()
13051297

1306-
self.ax2.set_xscale("log" if flag else "linear")
1307-
self.ax.set_xscale("log" if flag else "linear")
1298+
xscale = "log" if flag else "linear"
1299+
self.ax2.set_xscale(xscale)
1300+
self.ax.set_xscale(xscale)
1301+
self.__initXAxisFormatterAndLocator()
13081302

13091303
def setYAxisLogarithmic(self, flag):
13101304
# Workaround for matplotlib 2.0 issue with negative bounds
@@ -1322,8 +1316,15 @@ def setYAxisLogarithmic(self, flag):
13221316
if redraw:
13231317
self.draw()
13241318

1325-
self.ax2.set_yscale("log" if flag else "linear")
1326-
self.ax.set_yscale("log" if flag else "linear")
1319+
if flag:
1320+
self.ax2.set_yscale("log")
1321+
self.ax.set_yscale("log")
1322+
return
1323+
1324+
self.ax2.set_yscale("linear")
1325+
self.ax2.yaxis.set_major_formatter(DefaultTickFormatter())
1326+
self.ax.set_yscale("linear")
1327+
self.ax.yaxis.set_major_formatter(DefaultTickFormatter())
13271328

13281329
def setYAxisInverted(self, flag):
13291330
if self.ax.yaxis_inverted() != bool(flag):

src/silx/gui/plot/backends/glutils/GLPlotFrame.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
from .... import qt
4848
from ...._glutils import gl, Program
49+
from ....utils.matplotlib import DefaultTickFormatter
4950
from ..._utils import checkAxisLimits, FLOAT32_MINPOS
5051
from .GLSupport import mat4Ortho
5152
from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270
@@ -78,10 +79,14 @@ def __init__(
7879
labelVAlign=CENTER,
7980
titleAlign=CENTER,
8081
titleVAlign=CENTER,
82+
orderOffsetAlign=CENTER,
83+
orderOffsetVAlign=CENTER,
8184
titleRotate=0,
8285
titleOffset=(0.0, 0.0),
8386
):
87+
self._tickFormatter = DefaultTickFormatter()
8488
self._ticks = None
89+
self._orderAndOffsetText = ""
8590

8691
self._plotFrameRef = weakref.ref(plotFrame)
8792

@@ -96,6 +101,9 @@ def __init__(
96101
self._foregroundColor = foregroundColor
97102
self._labelAlign = labelAlign
98103
self._labelVAlign = labelVAlign
104+
self._orderOffetAnchor = (1.0, 0.0)
105+
self._orderOffsetAlign = orderOffsetAlign
106+
self._orderOffsetVAlign = orderOffsetVAlign
99107
self._titleAlign = titleAlign
100108
self._titleVAlign = titleVAlign
101109
self._titleRotate = titleRotate
@@ -193,6 +201,17 @@ def title(self, title):
193201
self._title = title
194202
self._dirtyPlotFrame()
195203

204+
@property
205+
def orderOffetAnchor(self) -> tuple[float, float]:
206+
"""Anchor position for the tick order&offset text"""
207+
return self._orderOffetAnchor
208+
209+
@orderOffetAnchor.setter
210+
def orderOffetAnchor(self, position: tuple[float, float]):
211+
if position != self._orderOffetAnchor:
212+
self._orderOffetAnchor = position
213+
self._dirtyTicks()
214+
196215
@property
197216
def titleOffset(self):
198217
"""Title offset in pixels (x: int, y: int)"""
@@ -296,6 +315,20 @@ def getVerticesAndLabels(self):
296315
)
297316
labels.append(axisTitle)
298317

318+
if self._orderAndOffsetText:
319+
xOrderOffset, yOrderOffet = self.orderOffetAnchor
320+
labels.append(
321+
Text2D(
322+
text=self._orderAndOffsetText,
323+
font=font,
324+
color=self._foregroundColor,
325+
x=xOrderOffset,
326+
y=yOrderOffet,
327+
align=self._orderOffsetAlign,
328+
valign=self._orderOffsetVAlign,
329+
devicePixelRatio=self.devicePixelRatio,
330+
)
331+
)
299332
return vertices, labels
300333

301334
def _dirtyPlotFrame(self):
@@ -320,6 +353,8 @@ def _ticksGenerator(self):
320353
"""Generator of ticks as tuples:
321354
((x, y) in display, dataPos, textLabel).
322355
"""
356+
self._orderAndOffsetText = ""
357+
323358
dataMin, dataMax = self.dataRange
324359
if self.isLog and dataMin <= 0.0:
325360
_logger.warning("Getting ticks while isLog=True and dataRange[0]<=0.")
@@ -373,20 +408,25 @@ def _ticksGenerator(self):
373408
tickDensity = 1.3 * self.devicePixelRatio / self.dotsPerInch
374409

375410
if not self.isTimeSeries:
376-
tickMin, tickMax, step, nbFrac = niceNumbersAdaptative(
411+
tickMin, tickMax, step, _ = niceNumbersAdaptative(
377412
dataMin, dataMax, nbPixels, tickDensity
378413
)
379414

380-
for dataPos in self._frange(tickMin, tickMax, step):
381-
if dataMin <= dataPos <= dataMax:
382-
xPixel = x0 + (dataPos - dataMin) * xScale
383-
yPixel = y0 + (dataPos - dataMin) * yScale
415+
visibleTickPositions = [
416+
pos
417+
for pos in self._frange(tickMin, tickMax, step)
418+
if dataMin <= pos <= dataMax
419+
]
420+
self._tickFormatter.axis.set_view_interval(dataMin, dataMax)
421+
self._tickFormatter.axis.set_data_interval(dataMin, dataMax)
422+
texts = self._tickFormatter.format_ticks(visibleTickPositions)
423+
self._orderAndOffsetText = self._tickFormatter.get_offset()
424+
425+
for dataPos, text in zip(visibleTickPositions, texts):
426+
xPixel = x0 + (dataPos - dataMin) * xScale
427+
yPixel = y0 + (dataPos - dataMin) * yScale
428+
yield ((xPixel, yPixel), dataPos, text)
384429

385-
if nbFrac == 0:
386-
text = "%g" % dataPos
387-
else:
388-
text = ("%." + str(nbFrac) + "f") % dataPos
389-
yield ((xPixel, yPixel), dataPos, text)
390430
else:
391431
# Time series
392432
try:
@@ -795,6 +835,8 @@ def __init__(self, marginRatios, foregroundColor, gridColor):
795835
foregroundColor=self._foregroundColor,
796836
labelAlign=CENTER,
797837
labelVAlign=TOP,
838+
orderOffsetAlign=RIGHT,
839+
orderOffsetVAlign=TOP,
798840
titleAlign=CENTER,
799841
titleVAlign=TOP,
800842
titleRotate=0,
@@ -810,6 +852,8 @@ def __init__(self, marginRatios, foregroundColor, gridColor):
810852
foregroundColor=self._foregroundColor,
811853
labelAlign=RIGHT,
812854
labelVAlign=CENTER,
855+
orderOffsetAlign=LEFT,
856+
orderOffsetVAlign=BOTTOM,
813857
titleAlign=CENTER,
814858
titleVAlign=BOTTOM,
815859
titleRotate=ROTATE_270,
@@ -822,6 +866,8 @@ def __init__(self, marginRatios, foregroundColor, gridColor):
822866
foregroundColor=self._foregroundColor,
823867
labelAlign=LEFT,
824868
labelVAlign=CENTER,
869+
orderOffsetAlign=RIGHT,
870+
orderOffsetVAlign=BOTTOM,
825871
titleAlign=CENTER,
826872
titleVAlign=TOP,
827873
titleRotate=ROTATE_270,
@@ -1281,6 +1327,25 @@ def _buildVerticesAndLabels(self):
12811327

12821328
self._x2AxisCoords = ((xCoords[0], yCoords[1]), (xCoords[1], yCoords[1]))
12831329

1330+
# Set order&offset anchor **before** handling Y axis inversion
1331+
font = qt.QApplication.instance().font()
1332+
fontPixelSize = font.pixelSize()
1333+
if fontPixelSize == -1:
1334+
fontPixelSize = font.pointSizeF() / 72.0 * self.dotsPerInch
1335+
1336+
self.axes[0].orderOffetAnchor = (
1337+
xCoords[1],
1338+
yCoords[0] + fontPixelSize * 1.2,
1339+
)
1340+
self.axes[1].orderOffetAnchor = (
1341+
xCoords[0],
1342+
yCoords[1] - 4 * self.devicePixelRatio,
1343+
)
1344+
self._y2Axis.orderOffetAnchor = (
1345+
xCoords[1],
1346+
yCoords[1] - 4 * self.devicePixelRatio,
1347+
)
1348+
12841349
if self.isYAxisInverted:
12851350
# Y axes are inverted, axes coordinates are inverted
12861351
yCoords = yCoords[1], yCoords[0]

src/silx/gui/utils/matplotlib.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,26 @@
5454

5555
from matplotlib.font_manager import FontProperties
5656
from matplotlib.mathtext import MathTextParser
57-
from matplotlib import figure
57+
from matplotlib.ticker import ScalarFormatter as _ScalarFormatter
58+
from matplotlib import figure, font_manager
59+
from packaging.version import Version
60+
61+
_MATPLOTLIB_VERSION = Version(matplotlib.__version__)
62+
63+
64+
class DefaultTickFormatter(_ScalarFormatter):
65+
"""Tick label formatter"""
66+
67+
def __init__(self):
68+
super().__init__(useOffset=True, useMathText=True)
69+
self.set_scientific(True)
70+
self.create_dummy_axis()
71+
72+
if _MATPLOTLIB_VERSION < Version("3.1.0"):
73+
74+
def format_ticks(self, values):
75+
self.set_locs(values)
76+
return [self(value, i) for i, value in enumerate(values)]
5877

5978

6079
_FONT_STYLES = {
@@ -67,8 +86,15 @@
6786
def qFontToFontProperties(font: qt.QFont):
6887
"""Convert a QFont to a matplotlib FontProperties"""
6988
weightFactor = 10 if qt.BINDING == "PyQt5" else 1
89+
families = [font.family(), font.defaultFamily()]
90+
if _MATPLOTLIB_VERSION >= Version("3.6.0"):
91+
# Prevent 'Font family not found warnings'
92+
availableNames = font_manager.get_font_names()
93+
families = [f for f in families if f in availableNames]
94+
families.append(font_manager.fontManager.defaultFamily["ttf"])
95+
7096
return FontProperties(
71-
family=[font.family(), font.defaultFamily()],
97+
family=families,
7298
style=_FONT_STYLES[font.style()],
7399
weight=weightFactor * font.weight(),
74100
size=font.pointSizeF(),

0 commit comments

Comments
 (0)