Skip to content

FIX: eventplot 'colors' kwarg (#8193) #8204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 65 additions & 29 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,55 +1091,82 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1,
linelengths=1, linewidths=None, colors=None,
linestyles='solid', **kwargs):
"""
Plot identical parallel lines at specific positions.
Plot identical parallel lines at the given positions.

Plot parallel lines at the given positions. positions should be a 1D
or 2D array-like object, with each row corresponding to a row or column
of lines.
*positions* should be a 1D or 2D array-like object, with each row
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we had a consensus to drop the "*".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See discussion starting here: #8172 (comment)
ref http://matplotlib.org/devdocs/devel/documenting_mpl.html#formatting
ref https://docs.python.org/devguide/documenting.html#id3

Let's keep the asterisks unless there is agreement to switch to double backquotes (which I would not favor). If we go with such a switch the documenting matplotlib guide should be updated first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently still using *. I will change it if a consensus emerges that it is not the proper markup to use (i.e. if the dev guidelines are updated :) ).

corresponding to a row or column of lines.

This type of plot is commonly used in neuroscience for representing
neural events, where it is commonly called a spike raster, dot raster,
neural events, where it is usually called a spike raster, dot raster,
or raster plot.

However, it is useful in any situation where you wish to show the
timing or position of multiple sets of discrete events, such as the
arrival times of people to a business on each day of the month or the
date of hurricanes each year of the last century.

*orientation* : [ 'horizontal' | 'vertical' ]
'horizontal' : the lines will be vertical and arranged in rows
'vertical' : lines will be horizontal and arranged in columns
Parameters
----------
positions : 1D or 2D array-like object
Each value is an event. If *positions* is a 2D array-like, each
row corresponds to a row or a column of lines (depending on the
*orientation* parameter).

*lineoffsets* :
A float or array-like containing floats.
orientation : {'horizontal', 'vertical'}, optional
Controls the direction of the event collections:

*linelengths* :
A float or array-like containing floats.
- 'horizontal' : the lines are arranged horizontally in rows,
and are vertical.
- 'vertical' : the lines are arranged vertically in columns,
and are horizontal.

*linewidths* :
A float or array-like containing floats.
lineoffsets : scalar or sequence of scalars, optional, default: 1
The offset of the center of the lines from the origin, in the
direction orthogonal to *orientation*.

*colors*
must be a sequence of RGBA tuples (e.g., arbitrary color
strings, etc, not allowed) or a list of such sequences
linelengths : scalar or sequence of scalars, optional, default: 1
The total height of the lines (i.e. the lines stretches from
``lineoffset - linelength/2`` to ``lineoffset + linelength/2``).

*linestyles* :
[ 'solid' | 'dashed' | 'dashdot' | 'dotted' ] or an array of these
values
linewidths : scalar, scalar sequence or None, optional, default: None
The line width(s) of the event lines, in points. If it is None,
defaults to its rcParams setting.

For linelengths, linewidths, colors, and linestyles, if only a single
value is given, that value is applied to all lines. If an array-like
is given, it must have the same length as positions, and each value
will be applied to the corresponding row or column in positions.
colors : color, sequence of colors or None, optional, default: None
The color(s) of the event lines. If it is None, defaults to its
rcParams setting.

Returns a list of :class:`matplotlib.collections.EventCollection`
objects that were added.
linestyles : str or tuple or a sequence of such values, optional
Default is 'solid'. Valid strings are ['solid', 'dashed',
'dashdot', 'dotted', '-', '--', '-.', ':']. Dash tuples
should be of the form::

kwargs are :class:`~matplotlib.collections.LineCollection` properties:
(offset, onoffseq),

%(LineCollection)s
where *onoffseq* is an even length tuple of on and off ink
in points.

**Example:**
**kwargs : optional
Other keyword arguments are line collection properties. See
:class:`~matplotlib.collections.LineCollection` for a list of
the valid properties.

Returns
-------

A list of :class:`matplotlib.collections.EventCollection` objects that
were added.

Notes
-----

For *linelengths*, *linewidths*, *colors*, and *linestyles*, if only
a single value is given, that value is applied to all lines. If an
array-like is given, it must have the same length as *positions*, and
each value will be applied to the corresponding row of the array.

Example
-------

.. plot:: mpl_examples/pylab_examples/eventplot_demo.py
"""
Expand Down Expand Up @@ -1193,6 +1220,15 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1,
lineoffsets = [None]
if len(colors) == 0:
colors = [None]
try:
# Early conversion of the colors into RGBA values to take care
# of cases like colors='0.5' or colors='C1'. (Issue #8193)
colors = mcolors.to_rgba_array(colors)
except ValueError:
# Will fail if any element of *colors* is None. But as long
# as len(colors) == 1 or len(positions), the rest of the
# code should process *colors* properly.
pass

if len(lineoffsets) == 1 and len(positions) != 1:
lineoffsets = np.tile(lineoffsets, len(positions))
Expand Down
80 changes: 39 additions & 41 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,15 +1243,15 @@ class EventCollection(LineCollection):
'''
A collection of discrete events.

An event is a 1-dimensional value, usually the position of something along
an axis, such as time or length. Events do not have an amplitude. They
are displayed as v
The events are given by a 1-dimensional array, usually the position of
something along an axis, such as time or length. They do not have an
amplitude and are displayed as vertical or horizontal parallel bars.
'''

_edge_default = True

def __init__(self,
positions, # Can be None.
positions, # Cannot be None.
orientation=None,
lineoffset=0,
linelength=1,
Expand All @@ -1262,62 +1262,60 @@ def __init__(self,
**kwargs
):
"""
*positions*
a sequence of numerical values or a 1D numpy array. Can be None

*orientation* [ 'horizontal' | 'vertical' | None ]
defaults to 'horizontal' if not specified or None
Parameters
----------
positions : 1D array-like object
Each value is an event.

*lineoffset*
a single numerical value, corresponding to the offset of the center
of the markers from the origin
orientation : {None, 'horizontal', 'vertical'}, optional
The orientation of the **collection** (the event bars are along
the orthogonal direction). Defaults to 'horizontal' if not
specified or None.

*linelength*
a single numerical value, corresponding to the total height of the
marker (i.e. the marker stretches from lineoffset+linelength/2 to
lineoffset-linelength/2). Defaults to 1
lineoffset : scalar, optional, default: 0
The offset of the center of the markers from the origin, in the
direction orthogonal to *orientation*.

*linewidth*
a single numerical value
linelength : scalar, optional, default: 1
The total height of the marker (i.e. the marker stretches from
``lineoffset - linelength/2`` to ``lineoffset + linelength/2``).

*color*
must be a sequence of RGBA tuples (e.g., arbitrary color
strings, etc, not allowed).
linewidth : scalar or None, optional, default: None
If it is None, defaults to its rcParams setting, in sequence form.

*linestyle* [ 'solid' | 'dashed' | 'dashdot' | 'dotted' ]
color : color, sequence of colors or None, optional, default: None
If it is None, defaults to its rcParams setting, in sequence form.

*antialiased*
1 or 2
linestyle : str or tuple, optional, default: 'solid'
Valid strings are ['solid', 'dashed', 'dashdot', 'dotted',
'-', '--', '-.', ':']. Dash tuples should be of the form::

If *linewidth*, *color*, or *antialiased* is None, they
default to their rcParams setting, in sequence form.
(offset, onoffseq),

*norm*
None (optional for :class:`matplotlib.cm.ScalarMappable`)
*cmap*
None (optional for :class:`matplotlib.cm.ScalarMappable`)
where *onoffseq* is an even length tuple of on and off ink
in points.

*pickradius* is the tolerance for mouse clicks picking a line.
The default is 5 pt.
antialiased : {None, 1, 2}, optional
If it is None, defaults to its rcParams setting, in sequence form.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can antialiased really be a sequence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I have no idea of what this parameter is doing except that it has to do with AA. So I tried to avoid changing the original sentence as much as possible.

From a very quick look at other docstrings in colllections, a sequence form is mentioned very often for the parameters antialiased(s). However, it is not totally consistent with this one here, as most of them state that is has to be boolean or sequence of boolean (or 0 and 1 values)… I'll try to have a deeper look today.


The use of :class:`~matplotlib.cm.ScalarMappable` is optional.
If the :class:`~matplotlib.cm.ScalarMappable` array
:attr:`~matplotlib.cm.ScalarMappable._A` is not None (i.e., a call to
:meth:`~matplotlib.cm.ScalarMappable.set_array` has been made), at
draw time a call to scalar mappable will be made to set the colors.
**kwargs : optional
Other keyword arguments are line collection properties. See
:class:`~matplotlib.collections.LineCollection` for a list of
the valid properties.

**Example:**
Example
-------

.. plot:: mpl_examples/pylab_examples/eventcollection_demo.py
"""

segment = (lineoffset + linelength / 2.,
lineoffset - linelength / 2.)
if len(positions) == 0:
if positions is None or len(positions) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would just write positions = np.asarray(positions) (or np.atleast_1d(positions) if we want to support scalars)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and then you know for sure it has an ndim attribute)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring does not say that one should expect scalars to be supported, and the other collections classes does not seem to support it either. So I would say => np.asarray.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now using positions = np.asarray(positions) since fc212ee.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted on a @tacaswell's comment.

segments = []
elif hasattr(positions, 'ndim') and positions.ndim > 1:
raise ValueError('if positions is an ndarry it cannot have '
'dimensionality great than 1 ')
raise ValueError('positions cannot be an array with more than '
'one dimension.')
elif (orientation is None or orientation.lower() == 'none' or
orientation.lower() == 'horizontal'):
positions.sort()
Expand Down
29 changes: 29 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import matplotlib.colors as mcolors
from numpy.testing import assert_allclose, assert_array_equal
from matplotlib.cbook import IgnoredKeywordWarning
from matplotlib.cbook._backports import broadcast_to

# Note: Some test cases are run twice: once normally and once with labeled data
# These two must be defined in the same test function or need to have
Expand Down Expand Up @@ -2909,6 +2910,34 @@ def test_eventplot_defaults():
colls = axobj.eventplot(data)


@pytest.mark.parametrize(('colors'), [
('0.5',), # string color with multiple characters: not OK before #8193 fix
('tab:orange', 'tab:pink', 'tab:cyan', 'bLacK'), # case-insensitive
('red', (0, 1, 0), None, (1, 0, 1, 0.5)), # a tricky case mixing types
('rgbk',) # len('rgbk') == len(data) and each character is a valid color
])
def test_eventplot_colors(colors):
'''Test the *colors* parameter of eventplot. Inspired by the issue #8193.
'''
data = [[i] for i in range(4)] # 4 successive events of different nature

# Build the list of the expected colors
expected = [c if c is not None else 'C0' for c in colors]
# Convert the list into an array of RGBA values
# NB: ['rgbk'] is not a valid argument for to_rgba_array, while 'rgbk' is.
if len(expected) == 1:
expected = expected[0]
expected = broadcast_to(mcolors.to_rgba_array(expected), (len(data), 4))

fig, ax = plt.subplots()
if len(colors) == 1: # tuple with a single string (like '0.5' or 'rgbk')
colors = colors[0]
collections = ax.eventplot(data, colors=colors)

for coll, color in zip(collections, expected):
assert_allclose(coll.get_color(), color)


@image_comparison(baseline_images=['test_eventplot_problem_kwargs'],
extensions=['png'], remove_text=True)
def test_eventplot_problem_kwargs():
Expand Down