[Matplotlib-devel] IndexLocator vs. MultipleLocator

vincent.adrien at gmail.com vincent.adrien at gmail.com
Sat Mar 18 07:29:21 EDT 2017


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 
discussion](https://gitter.im/matplotlib/matplotlib?at=58b7f75000c00c3d4fabe3b6).

In a nutshell, the following example
```python
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
plt.show()
```
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 
“MultipleLocatorBis_with_an_offset.pdf”):

```python
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),
                     MultipleLocatorBis(base))
     else:
         locators = (IndexLocator(base, offset), MultipleLocator(base),
                     MultipleLocatorBis(base, offset=offset))

     for ax, loc in zip(axs_row, locators):
         ax.yaxis.set_major_locator(loc)

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


if __name__ == '__main__':

     plt.close('all')

     # 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")
     plt.tight_layout()

     plt.show()
```

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!

Best,
Adrien

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


-------------- next part --------------
A non-text attachment was scrubbed...
Name: issue_with_eventplot_and_IndexLocator.pdf
Type: application/pdf
Size: 10311 bytes
Desc: not available
URL: <http://mail.python.org/pipermail/matplotlib-devel/attachments/20170318/b8bba8a3/attachment-0002.pdf>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: MultipleLocatorBis_with_an_offset.pdf
Type: application/pdf
Size: 16327 bytes
Desc: not available
URL: <http://mail.python.org/pipermail/matplotlib-devel/attachments/20170318/b8bba8a3/attachment-0003.pdf>


More information about the Matplotlib-devel mailing list