Skip to content

Axis ticker update#2827

Merged
j9ac9k merged 12 commits intopyqtgraph:masterfrom
NilsNemitz:axis_ticker_update
Oct 31, 2023
Merged

Axis ticker update#2827
j9ac9k merged 12 commits intopyqtgraph:masterfrom
NilsNemitz:axis_ticker_update

Conversation

@NilsNemitz
Copy link
Copy Markdown
Contributor

The automatic tick generator AxisItem.tickSpacing() has a few quirks:

  • For small plots, it often generates only a single labeled tick. That makes the tick spacing indecipherable.
  • For particularly unlucky configurations, it generates no labeled ticks. This usually collapses the space reserved for the axis, while it is presently in use for zoom or pan.
  • The code now only supports multiples of 1 and 2 per decade, creating a five-fold jump in tick density when crossing from 10 to 2. Including multiples of 5 reduces this jump, but occasionally results in unevenly spaced tick labels like [2, 4, 5, 6, 8] where the not fully overlapping sequences of major and minor ticks both get labeled.
  • For long axes, very few major ticks are generated. A different part of the code often considers these too sparse and activates emergency text labels for each tick. Down to the third-rank extra ticks, that would not usually be considered for labelling.

These issues intensify when working with larger font sizes, particularly because labels current cannot extend beyond the height/width of the viewbox, and (large) labels near the edges are culled.

Here is a demo to play with some of the issues:

import numpy as np
from PyQt5.QtGui import( QFont )
import pyqtgraph.pyqtgraph as pg
from pyqtgraph.pyqtgraph.Qt import QtCore

app = pg.mkQApp("Axes issues demo")

x = np.arange(30) + 123000
y = 0.5 * np.random.normal(size=30)

win = pg.GraphicsLayoutWidget(show=True, title="axes demo")
win.resize(1000,800)

p1 = win.addPlot(title="Only one tick label", x=x, y=y, row=0, col=0)
p1.setYRange(-1, 1, padding = 0.)

p2 = win.addPlot(title="sometimes ticks are dense...", x=x, y=y, row=0, col=1, rowspan=2 )
p2.setYRange(-1.75, 1.75, padding = 0.)

p3 = win.addPlot(title="... sometimes very sparse", y=y, row=0, col=2, rowspan=3 )
p3.setYRange(-2.10, 2.10, padding = 0.)
axis = p3.getAxis('bottom')

p4 = win.addPlot(title="It's worse with large fonts", x=x, y=y, row=1, col=0)
font = QFont()
font.setPointSize(25)
axis = p4.getAxis('left')
axis.setTickFont(font)
p4.setYRange(-0.5, 9.5, padding = 0.)

p5 = win.addPlot(title="wonky with long x-axis labels (pan to see)", x=x, y=y, row=2, col=0, colspan=2)
p5.getAxis('bottom').setTickFont(font)
p5.setXRange(123010, 123019.5, padding = 0.)

p6 = win.addPlot(title="Emergency labels on third rank ticks", y=y, row=3, col=0, colspan=3)
axis = p6.getAxis('bottom')
if hasattr(axis,'setTickDensity'): axis.setTickDensity(5.0)
axis = p6.getAxis('left')
if hasattr(axis,'setTickDensity'): axis.setTickDensity(4.0)
p6.setYRange(-1, 1, padding = 0.)

if __name__ == '__main__':
    pg.exec()

Changes

This PR replaces the generation code:

  • It starts by creating a major spacing that ensures(*) that two ticks are displayed.
  • If the best major spacing is 1, then the minor spacing will be 0.5.
  • If the best major spacing is 2 or 5, then the minor spacing will be 1. This usually avoids having non-aligned spacings of 2 and 5 in the same plot.
  • Sub-minor (extra) ticks are generated at 10% of the major spacing.
  • Undersized spacings (of less than 2 pixels on screen) are enlarged, extra ticks are only shown if visible.
  • An added method setTickDensity() makes it possible to adjust the auto-generated ticks somewhat without full manual specification.

(*): This still makes implicit assumptions on text lable sizes, but referencing the actual sizes would be rather complexity.

Results

Current master branch:
current

Revised axis ticks:
revised

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Oct 4, 2023

Thanks for this PR @NilsNemitz

Axis spacing code has been a bit of a "here be dragons" area of the code-base, love that you've taken this on!

The change looks good, some nit-picky comments regarding avoiding equality comparisons for floats. CodeQL is complaining about an unused import, and I'm curious if we can move away from that 10 ** floor(log10(float)) and do something like 2 ** int(float).bit_length() instead which should be much faster.

EDIT: also go ahead and remove the unused import 👍🏻

@NilsNemitz
Copy link
Copy Markdown
Contributor Author

Hi @j9ac9k ,

Sorry for the delay, I've finally gotten around to figuring out why the pyside tests were failing: It seems that sometimes the power of ten calculation came back with integer values. That should no longer happen.

The code now guesses the power of ten magnitude based on the IEEE exponent value. No more log10 calculation, at the cost of running one extra check for the tick multiplier.

Can you see why the docs build is failing?

@NilsNemitz NilsNemitz marked this pull request as draft October 30, 2023 12:41
@NilsNemitz NilsNemitz marked this pull request as ready for review October 30, 2023 12:42
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Oct 31, 2023

Thanks for tackling this @NilsNemitz ...this was one of those areas that each time I thought about, I couldn't think of a good way to characterize ideal behavior better than what the library did already; but it appears you figured it out!

Really appreciate the comments too ❤️

LGTM, merging!

@j9ac9k j9ac9k merged commit 296a6ac into pyqtgraph:master Oct 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants