diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index bfbf81bc67be..ba530a56282b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1083,14 +1083,13 @@ 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 + 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 @@ -1098,38 +1097,70 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, 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). + + orientation : {'horizontal', 'vertical'}, optional + Controls the direction of the event collections: + + - 'horizontal' : the lines are arranged horizontally in rows, + and are vertical. + - 'vertical' : the lines are arranged vertically in columns, + and are horizontal. + + 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*. + + 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``). + + 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. + + 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. - *lineoffsets* : - A float or array-like containing floats. + 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:: - *linelengths* : - A float or array-like containing floats. + (offset, onoffseq), - *linewidths* : - A float or array-like containing floats. + where *onoffseq* is an even length tuple of on and off ink + in points. - *colors* - must be a sequence of RGBA tuples (e.g., arbitrary color - strings, etc, not allowed) or a list of such sequences + **kwargs : optional + Other keyword arguments are line collection properties. See + :class:`~matplotlib.collections.LineCollection` for a list of + the valid properties. - *linestyles* : - [ 'solid' | 'dashed' | 'dashdot' | 'dotted' ] or an array of these - values + Returns + ------- - 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. + A list of :class:`matplotlib.collections.EventCollection` objects that + were added. - Returns a list of :class:`matplotlib.collections.EventCollection` - objects that were added. + Notes + ----- - kwargs are :class:`~matplotlib.collections.LineCollection` properties: + 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. - %(LineCollection)s + Example + ------- + + .. plot:: mpl_examples/pylab_examples/eventplot_demo.py """ self._process_unit_info(xdata=positions, ydata=[lineoffsets, linelengths], @@ -1181,6 +1212,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)) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index d9aaacf57eaa..eec3afa45dec 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1240,15 +1240,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, @@ -1259,58 +1259,60 @@ def __init__(self, **kwargs ): """ - *positions* - a sequence of numerical values or a 1D numpy array. Can be None + Parameters + ---------- + positions : 1D array-like object + Each value is an event. - *orientation* [ 'horizontal' | 'vertical' | None ] - defaults to 'horizontal' if not specified or None + 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. - *lineoffset* - a single numerical value, corresponding to the offset of the center - of the markers from the origin + lineoffset : scalar, optional, default: 0 + The offset of the center of the markers from the origin, in the + direction orthogonal to *orientation*. - *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 + linelength : scalar, optional, default: 1 + The total height of the marker (i.e. the marker stretches from + ``lineoffset - linelength/2`` to ``lineoffset + linelength/2``). - *linewidth* - a single numerical value + linewidth : scalar or None, optional, default: None + If it is None, defaults to its rcParams setting, in sequence form. - *color* - must be a sequence of RGBA tuples (e.g., arbitrary color - strings, etc, not allowed). + color : color, sequence of colors or None, optional, default: None + If it is None, defaults to its rcParams setting, in sequence form. - *linestyle* [ 'solid' | 'dashed' | 'dashdot' | 'dotted' ] + linestyle : str or tuple, optional, default: 'solid' + Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', + '-', '--', '-.', ':']. Dash tuples should be of the form:: - *antialiased* - 1 or 2 + (offset, onoffseq), - If *linewidth*, *color*, or *antialiased* is None, they - default to their rcParams setting, in sequence form. + where *onoffseq* is an even length tuple of on and off ink + in points. - *norm* - None (optional for :class:`matplotlib.cm.ScalarMappable`) - *cmap* - None (optional for :class:`matplotlib.cm.ScalarMappable`) + antialiased : {None, 1, 2}, optional + If it is None, defaults to its rcParams setting, in sequence form. - *pickradius* is the tolerance for mouse clicks picking a line. - The default is 5 pt. + **kwargs : optional + Other keyword arguments are line collection properties. See + :class:`~matplotlib.collections.LineCollection` for a list of + the valid properties. - 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. + 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: 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() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 94ba3119fa7f..b58345a51184 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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 @@ -2985,6 +2986,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():