8

I am using the matplotlib scatterplot function to create the appearance of handles on vertical lines to delineate certain parts of a graph. However, in order to make them look correct, I need to be able to align the scatter plot marker to the left (for the left line / delineator) and / or right (for the right line / delineator).

Here's an example:

#create the figure
fig = plt.figure(facecolor = '#f3f3f3', figsize = (11.5, 6))
ax = plt. ax = plt.subplot2grid((1, 1), (0,0))

#make some random data
index = pandas.DatetimeIndex(start = '01/01/2000', freq  = 'b', periods = 100)
rand_levels = pandas.DataFrame( numpy.random.randn(100, 4)/252., index = index, columns = ['a', 'b', 'c', 'd'])
rand_levels = 100*numpy.exp(rand_levels.cumsum(axis = 0))
ax.stackplot(rand_levels.index, rand_levels.transpose())

#create the place holder for the vertical lines
d1, d2 = index[25], index[50]

#draw the lines
ymin, ymax = ax.get_ylim()
ax.vlines([index[25], index[50]], ymin = ymin, ymax = ymax, color = '#353535', lw = 2)

#draw the markers
ax.scatter(d1, ymax, clip_on = False, color = '#353535', marker = '>', s = 200, zorder = 3)
ax.scatter(d2, ymax, clip_on = False, color = '#353535', marker = '<', s = 200, zorder = 3)

#reset the limits
ax.set_ylim(ymin, ymax)
ax.set_xlim(rand_levels.index[0], rand_levels.index[-1])
plt.show()

The code above gives me almost the graph I'm looking for, like this:

focus_timeline

However, I'd like the leftmost marker (">") to be "aligned left" (i.e. shifted slightly to the right) so that the line is continued to the back of the marker Likewise, I'd like the rightmost marker ("<") to be "aligned right" (i.e. slightly shifted to the left). Like this:

desired_fig

Any guidance or suggestions on how to accomplish this in a flexible manner?

NOTE: In practice, my DataFrame index is pandas.Datetime not integers as I've provided for this simple example.

benjaminmgross
  • 2,052
  • 1
  • 24
  • 29

4 Answers4

8

I liked this question and was not satisfied with my first answer. In particular, it seemed unnecessarily cumbersome to create figure specific objects (mark_align_*) in order to align markers. What I eventually found was the functionality to specify a marker by verts (a list of 2-element floats, or an Nx2 array, that specifies the marker vertices relative to the target plot-point at (0, 0)). To utilize this functionality for this purpose I wrote this function,

from matplotlib import markers
from matplotlib.path import Path

def align_marker(marker, halign='center', valign='middle',):
    """
    create markers with specified alignment.

    Parameters
    ----------

    marker : a valid marker specification.
      See mpl.markers

    halign : string, float {'left', 'center', 'right'}
      Specifies the horizontal alignment of the marker. *float* values
      specify the alignment in units of the markersize/2 (0 is 'center',
      -1 is 'right', 1 is 'left').

    valign : string, float {'top', 'middle', 'bottom'}
      Specifies the vertical alignment of the marker. *float* values
      specify the alignment in units of the markersize/2 (0 is 'middle',
      -1 is 'top', 1 is 'bottom').

    Returns
    -------

    marker_array : numpy.ndarray
      A Nx2 array that specifies the marker path relative to the
      plot target point at (0, 0).

    Notes
    -----
    The mark_array can be passed directly to ax.plot and ax.scatter, e.g.::

        ax.plot(1, 1, marker=align_marker('>', 'left'))

    """

    if isinstance(halign, (str, unicode)):
        halign = {'right': -1.,
                  'middle': 0.,
                  'center': 0.,
                  'left': 1.,
                  }[halign]

    if isinstance(valign, (str, unicode)):
        valign = {'top': -1.,
                  'middle': 0.,
                  'center': 0.,
                  'bottom': 1.,
                  }[valign]

    # Define the base marker
    bm = markers.MarkerStyle(marker)

    # Get the marker path and apply the marker transform to get the
    # actual marker vertices (they should all be in a unit-square
    # centered at (0, 0))
    m_arr = bm.get_path().transformed(bm.get_transform()).vertices

    # Shift the marker vertices for the specified alignment.
    m_arr[:, 0] += halign / 2
    m_arr[:, 1] += valign / 2

    return Path(m_arr, bm.get_path().codes)

Using this function, the desired markers can be plotted as,

ax.plot(d1, 1, marker=align_marker('>', halign='left'), ms=20,
        clip_on=False, color='k', transform=ax.get_xaxis_transform())
ax.plot(d2, 1, marker=align_marker('<', halign='right'), ms=20,
        clip_on=False, color='k', transform=ax.get_xaxis_transform())

or using ax.scatter,

ax.scatter(d1, 1, 200, marker=align_marker('>', halign='left'),
           clip_on=False, color='k', transform=ax.get_xaxis_transform())
ax.scatter(d2, 1, 200, marker=align_marker('<', halign='right'),
           clip_on=False, color='k', transform=ax.get_xaxis_transform())

In both of these examples I have specified transform=ax.get_xaxis_transform() so that the vertical position of the markers is in axes coordinates (1 is the top of the axes), this has nothing to do with the marker alignment.

The obvious advantage of this solution compared to my previous one is that it does not require knowledge of the markersize, plotting function (ax.plot vs. ax.scatter), or axes (for the transform). Instead, one simply specifes a marker and its alignment!

Cheers!

farenorth
  • 10,165
  • 2
  • 39
  • 45
  • In the course of searching for an alternate solution I tried to supply a customized `matplotlib.markers.MarkerStyle` instance to the `marker` input option of `ax.plot` and `ax.scatter`, but this functionality does not yet appear to be supported. – farenorth Nov 04 '14 at 01:21
  • 1
    Wow @farenorth... just wow. First of all, it looks like by using the `markers.MarkerStyle` class, you were able to solve the issue without having to pass the `figure` to the function (which is big plus for me). I need to implement what you've done to make sure I'm using / understanding it correctly, but this is a really elegant abstraction / solution! – benjaminmgross Nov 05 '14 at 15:07
  • 1
    Just put together some tester functions and this works beautifully without needing to pass the figure (and much clearer & more expressive language). Terrific solution! – benjaminmgross Nov 05 '14 at 15:29
  • Thank you very much for putting so much effort in this anser! This should actually included in matplotlib as parameters for markers. – wedi Dec 21 '16 at 12:40
  • Why this does not work: ax.plot(d1, 1, marker=align_marker('_', halign='left'), ms=20, clip_on=False, color='k', transform=ax.get_xaxis_transform()) – Oren Jan 12 '17 at 15:10
  • 1
    @Oren, good Q. This appears to be a bug in the way markers are created. I've created an [issue](https://github.com/matplotlib/matplotlib/issues/7807), and [proposed a solution](https://github.com/matplotlib/matplotlib/pull/7809). For now, I've updated my function with a workaround. – farenorth Jan 12 '17 at 18:12
  • One other note: at least for me, using this changes the interpretation of the `s` keyword argument to `ax.scatter`. I'm not entirely sure I understand why, but I think it might have to do with the transform applied. – Mack Sep 21 '21 at 13:40
7

I found a simple solution to this problem. Matplotlib have built-in markers with different alignments: lines_bars_and_markers example code: marker_reference.py enter image description here

Simply change the '>' marker to 9 and the '<' marker to 8:

#draw the markers
ax.scatter(d1, ymax, clip_on=False, color='#353535', marker=9, s=200, zorder=3)
ax.scatter(d2, ymax, clip_on=False, color='#353535', marker=8, s=200, zorder=3)
Liran Funaro
  • 2,750
  • 2
  • 22
  • 33
2

One solution would be to use mpl.transforms, and the transform input parameter to ax.scatter or ax.plot. Specifically, I would start by adding,

from matplotlib import transforms as tf

In this approach I use tf.offset_copy to create markers that are offset by half of their size. But what are the size of markers? It turns out that ax.scatter and ax.plot specify marker sizes differently. See this question for more info.

  1. The s= input parameter to ax.scatter specifies marker sizes in points^2 (i.e. this is the area of the square that the markers fit into).

  2. The markersize input parameter to ax.plot specifies the width and height of the markers in points (i.e. the width and height of the square that the markers fit into).

Using ax.scatter

So, if you want to plot your markers with ax.scatter you could do,

ms_scatter = 200  # define markersize
mark_align_left_scatter = tf.offset_copy(ax.get_xaxis_transform(), fig,
                                         ms_scatter ** 0.5 / 2,
                                         units='points')
mark_align_right_scatter = tf.offset_copy(ax.get_xaxis_transform(), fig,
                                          -ms_scatter ** 0.5 / 2,
                                          units='points')

Here I have used the ax.get_xaxis_transform, which is a transform that places points in data-coordinates along the x-axis, but in axes (0 to 1) coordinates on the y-axis. This way, rather than using ymax, I can place the point at the top of the plot with 1. Furthermore, if I pan or zoom the figure, the markers will still be at the top! Once I've defined the new transforms, I assign them to the transform property when I call ax.scatter,

ax.scatter(d1, 1, s=ms_scatter, marker='>', transform=mark_align_left_scatter,
           clip_on=False, color='k')
ax.scatter(d2, 1, s=ms_scatter, marker='<', transform=mark_align_right_scatter,
           clip_on=False, color='k')

Using ax.plot

Because it is somewhat simpler, I would probably use ax.plot. In that case I would do,

ms = 20

mark_align_left = tf.offset_copy(ax.get_xaxis_transform(), fig,
                                 ms / 2, units='points')
mark_align_right = tf.offset_copy(ax.get_xaxis_transform(), fig,
                                  -ms / 2, units='points')

ax.plot(d1, 1, marker='>', ms=ms, transform=mark_align_left,
        clip_on=False, color='k')
ax.plot(d2, 1, marker='<', ms=ms, transform=mark_align_right,
        clip_on=False, color='k')

Final Comments

You may want to create a wrapper to make creation of the mark_align_* transforms easier, but I'll leave that for you to implement if you want to.

Whether you use ax.scatter or ax.plot your output plot will look something like,

Final figure

Victor Le Pochat
  • 241
  • 4
  • 11
farenorth
  • 10,165
  • 2
  • 39
  • 45
0

Not the most elegant solution, but if I'm understanding your question correctly, subtracting and adding one from/to d1 and d2 respectively should do it:

ax.scatter(d1-1, ymax, clip_on = False, color = '#353535', marker = '>', s = 200, zorder = 3)
ax.scatter(d2+1, ymax, clip_on = False, color = '#353535', marker = '<', s = 200, zorder = 3)
Ryan
  • 3,555
  • 1
  • 22
  • 36
  • take a peek at my NOTE, I state that in my actual problem the `DataFrame` index is dates, and this needs to be a flexible solution, not one that just works for the example. Thanks so much for your thoughts, though. – benjaminmgross Nov 01 '14 at 12:49
  • Oh, I missed that part. Could you provide sample code in which the `DataFrame` is `pandas.Datetime` then? – Ryan Nov 01 '14 at 14:18
  • Just added the code to made the `Index` of type `Datetime` instead of `int` – benjaminmgross Nov 01 '14 at 14:34
  • Looks like if you change `freq = 'b'` to `freq = 'h'` or `freq = 'd'` the posted solution works – Ryan Nov 01 '14 at 14:38
  • this is just sample data that I've generated. It will have real dates (that are most-like 'business days'), so unfortunately, I can't just change `'b'` to `'h'`. – benjaminmgross Nov 01 '14 at 14:48