[Matplotlib-devel] IndexLocator vs. MultipleLocator

Previous Topic Next Topic
classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
Report Content as Inappropriate

[Matplotlib-devel] IndexLocator vs. MultipleLocator

Hello folks,

A few weeks ago, I tried to use an `IndexLocator` instance for the y-axis of an
`eventplot`, and I encoutered several weird behaviors. Maybe I was just misusing
`IndexLocator` but I was not alone to think these might actually be bugs. Here
is the relevant [Gitter

In a nutshell, the following example
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import IndexLocator

# Event sequence is "10" at 0.1 s, "21" at 0.5 s, and "10" at 0.9 s.
events = [[0.1, 0.9], [0.5]]
indices = [10, 21]
event_height = 0.9  # not 1 to better visualize rows of events

fig, axs = plt.subplots(ncols=2, num='eventplot_and_IndexLocator')
for ax in axs:
     ax.eventplot(events, lineoffsets=indices, linelengths=event_height)

# One wants the ticks to be all the indices: a possible thought may
# be to use an IndexLocator with base = 1 and offset = 0:
axs[0].yaxis.set_major_locator(IndexLocator(1, 0))
axs[0].set_title("base=1, offset=0")
# Issue 1?: the tick values are not integers...

# Case that is even worse: let's assume that for any good reason, we
# want the ticks to start at 12 (offset = 12), and go 5 by 5 (base = 5).
axs[1].yaxis.set_major_locator(IndexLocator(5, 12))
axs[1].set_title("base=5, offset=12")
# Issue 2?: one would expect the ticks to be 12, 17 and 21...

plt.tight_layout()  # for eyes' pleasure
produces the attached file named “issue_with_eventplot_and_IndexLocator.pdf”. My
analysis is that there are (at least) two problems:

1. `IndexLocator` uses the “data limits”, which does not return integer values
in the case of an eventplot with linelengths != 1 (yes I like when there is a
small space between successive rows of events). Besides, why rely on the data
limits and not the view ones?

2. The way the offset is handled does not take into account the case where
`offset >= base`, which seemed rather natural to me though if for example one
have indices that start to 10.

An easy workaround to 1. is to use a `MultipleLocator` instance instead of an
`IndexLocator` one. However, doig so, one looses the possibility to have a
offset. So I would have several question:

A. Should we consider the previous behaviors of `IndexLocator` as bugs? Or am I
just not using the right tool?

B. `IndexLocator` seems to be a really old piece of the code base, while
`MultipleLocator` is more recent and elaborated (from what I understand, it
internally handles some floating point errors and other tricks). From my
viewpoint, if `MultipleLocator` had a `offset` capability, then `IndexLocator`
would be more or less¹ a subset of `MultipleLocator`.

My genuine suggestion would be to add an “offset” keyword argument to
`MultipleLocator`, and possibly deprecate `IndexLocator` in the long term as it
would then become slightly redundant (and seems quite broken from my point of
view). But again, maybe I am missing the point with `IndexLocator`.

Here is a quick demo of what I have in mind (which produces the attached file

import numpy as np
import matplotlib.pyplot as plt

from matplotlib.ticker import IndexLocator, MultipleLocator, Base

class MultipleLocatorBis(MultipleLocator):
     def __init__(self, base=1.0, offset=0.0):
         """Yes, I have never really understand how to use `super`..."""
         self._base = Base(base)
         self._offset = offset  # <= added

     def set_params(self, base, offset=0.0):
         self._offset = offset  # <= added
         if base is not None:
          self._base = base

     def tick_values(self, vmin, vmax):
         if vmax < vmin:
          vmin, vmax = vmax, vmin
         vmin = self._base.ge(vmin)
         base = self._base.get_base()
         n = (vmax - vmin + 0.001 * base) // base
         # locs = vmin - base + np.arange(n + 3) * base  # <= vanilla case
         locs = (vmin - base + (self._offset % base) + np.arange(n + 3) * base)
         return self.raise_if_exceeds(locs)

def compare_locators(axs_row, base, offset):
     if offset is None:  # testing the default offset if it is possible
         locators = (IndexLocator(base, 0), MultipleLocator(base),
         locators = (IndexLocator(base, offset), MultipleLocator(base),
                     MultipleLocatorBis(base, offset=offset))

     for ax, loc in zip(axs_row, locators):

     axs_row[0].set_ylabel("base={b}, offset={o}".format(b=base, o=offset))

if __name__ == '__main__':


     # Event sequence is "10" at 0.1 s, "21" at 0.5 s, and "10" at 0.9 s.
     events = [[0.1, 0.9], [0.5]]
     indices = [10, 21]
     event_height = 0.9  # not 1 to better visualize rows of events

     fig, axs = plt.subplots(nrows=3, ncols=3, figsize=(9.6, 6.4), sharex=True)
     for ax in axs.flat:
         ax.eventplot(events, lineoffsets=indices, linelengths=event_height)

     for axs_row, base, offset in zip(axs, (2, 5.0, 2.5), (None, 12, -6.0)):
         compare_locators(axs_row, base, offset)

     # Cosmeticks
     axs[0, 0].set_title("IndexLocator")
     axs[0, 1].set_title("MultipleLocator (no offset)")
     axs[0, 2].set_title("MultipleLocatorBis")


Any feedback from other people about this idea would be welcome :) (, at least
to know if I am the only person who had this use case of `IndexLocator`…). I am
pretty sure too that there are some matplotlib.ticker gurus and experts around
here, who will be able to shed some light on IndexLocator!


¹: one remaining subtlety would be the “data limits” approach vs. the “view
limits” one.

Matplotlib-devel mailing list
[hidden email]

issue_with_eventplot_and_IndexLocator.pdf (13K) Download Attachment
MultipleLocatorBis_with_an_offset.pdf (21K) Download Attachment