Skip to content

Commit a9ffba5

Browse files
committed
unify color processesing
1 parent 829b624 commit a9ffba5

File tree

6 files changed

+115
-51
lines changed

6 files changed

+115
-51
lines changed

control/ctrlplot.py

+75-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# Code pattern for control system plotting functions:
77
#
8-
# def name_plot(sysdata, plot=None, **kwargs):
8+
# def name_plot(sysdata, *fmt, plot=None, **kwargs):
99
# # Process keywords and set defaults
1010
# ax = kwargs.pop('ax', None)
1111
# color = kwargs.pop('color', None)
@@ -37,12 +37,13 @@
3737
# # Plot the data
3838
# lines = np.full(ax_array.shape, [])
3939
# line_labels = _process_line_labels(label, ntraces, nrows, ncols)
40+
# color_offset, color_cycle = _get_color_offset(ax)
4041
# for i, j in itertools.product(range(nrows), range(ncols)):
4142
# ax = ax_array[i, j]
42-
# color_cycle, color_offset = _process_color_keyword(ax)
4343
# for k in range(ntraces):
4444
# if color is None:
45-
# color = color_cycle[(k + color_offset) % len(color_cycle)]
45+
# color = _get_color(
46+
# color, fmt=fmt, offset=k, color_cycle=color_cycle)
4647
# label = line_labels[k, i, j]
4748
# lines[i, j] += ax.plot(data.x, data.y, color=color, label=label)
4849
#
@@ -656,11 +657,78 @@ def _add_arrows_to_line2D(
656657
return arrows
657658

658659

659-
def _get_color(colorspec, ax=None, lines=None, color_cycle=None):
660+
def _get_color_offset(ax, color_cycle=None):
661+
"""Get color offset based on current lines.
662+
663+
This function determines that the current offset is for the next color
664+
to use based on current colors in a plot.
665+
666+
Parameters
667+
----------
668+
ax : matplotlib.axes.Axes
669+
Axes containing already plotted lines.
670+
color_cycle : list of matplotlib color specs, optional
671+
Colors to use in plotting lines. Defaults to matplotlib rcParams
672+
color cycle.
673+
674+
Returns
675+
-------
676+
color_offset : matplotlib color spec
677+
Starting color for next line to be drawn.
678+
color_cycle : list of matplotlib color specs
679+
Color cycle used to determine colors.
680+
681+
"""
682+
if color_cycle is None:
683+
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
684+
685+
color_offset = 0
686+
if len(ax.lines) > 0:
687+
last_color = ax.lines[-1].get_color()
688+
if last_color in color_cycle:
689+
color_offset = color_cycle.index(last_color) + 1
690+
691+
return color_offset % len(color_cycle), color_cycle
692+
693+
694+
def _get_color(
695+
colorspec, offset=None, fmt=None, ax=None, lines=None,
696+
color_cycle=None):
697+
"""Get color to use for plotting line.
698+
699+
This function returns the color to be used for the line to be drawn (or
700+
None if the detault color cycle for the axes should be used).
701+
702+
Parameters
703+
----------
704+
colorspec : matplotlib color specification
705+
User-specified color (or None).
706+
offset : int, optional
707+
Offset into the color cycle (for multi-trace plots).
708+
fmt : str, optional
709+
Format string passed to plotting command.
710+
ax : matplotlib.axes.Axes, optional
711+
Axes containing already plotted lines.
712+
lines : list of matplotlib.lines.Line2D, optional
713+
List of plotted lines. If not given, use ax.get_lines().
714+
color_cycle : list of matplotlib color specs, optional
715+
Colors to use in plotting lines. Defaults to matplotlib rcParams
716+
color cycle.
717+
718+
Returns
719+
-------
720+
color : matplotlib color spec
721+
Color to use for this line (or None for matplotlib default).
722+
723+
"""
660724
# See if the color was explicitly specified by the user
661725
if isinstance(colorspec, dict):
662726
if 'color' in colorspec:
663727
return colorspec.pop('color')
728+
elif fmt is not None and \
729+
[isinstance(arg, str) and
730+
any([c in arg for c in "bgrcmykw#"]) for arg in fmt]:
731+
return None # *fmt will set the color
664732
elif colorspec != None:
665733
return colorspec
666734

@@ -673,7 +741,9 @@ def _get_color(colorspec, ax=None, lines=None, color_cycle=None):
673741
lines = ax.lines
674742

675743
# If we were passed a set of lines, try to increment color from previous
676-
if lines is not None:
744+
if offset is not None:
745+
return color_cycle[offset]
746+
elif lines is not None:
677747
color_offset = 0
678748
if len(ax.lines) > 0:
679749
last_color = ax.lines[-1].get_color()

control/freqplot.py

+13-18
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
from . import config
2121
from .bdalg import feedback
2222
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _find_axes_center, \
23-
_get_line_labels, _make_legend_labels, _process_ax_keyword, \
24-
_process_legend_keywords, _process_line_labels, _update_plot_title
23+
_get_color, _get_color_offset, _get_line_labels, _make_legend_labels, \
24+
_process_ax_keyword, _process_legend_keywords, _process_line_labels, \
25+
_update_plot_title
2526
from .ctrlutil import unwrap
2627
from .exception import ControlMIMONotImplemented
2728
from .frdata import FrequencyResponseData
@@ -2306,6 +2307,7 @@ def singular_values_plot(
23062307
23072308
"""
23082309
# Keyword processing
2310+
color = kwargs.pop('color', None)
23092311
dB = config._get_param(
23102312
'freqplot', 'dB', kwargs, _freqplot_defaults, pop=True)
23112313
Hz = config._get_param(
@@ -2370,14 +2372,8 @@ def singular_values_plot(
23702372
legend_loc, _, show_legend = _process_legend_keywords(
23712373
kwargs, None, 'center right')
23722374

2373-
# Handle color cycle manually as all singular values
2374-
# of the same systems are expected to be of the same color
2375-
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
2376-
color_offset = 0
2377-
if len(ax_sigma.lines) > 0:
2378-
last_color = ax_sigma.lines[-1].get_color()
2379-
if last_color in color_cycle:
2380-
color_offset = color_cycle.index(last_color) + 1
2375+
# Get color offset for first (new) line to be drawn
2376+
color_offset, color_cycle = _get_color_offset(ax_sigma)
23812377

23822378
# Create a list of lines for the output
23832379
out = np.empty(len(data), dtype=object)
@@ -2392,14 +2388,13 @@ def singular_values_plot(
23922388
else:
23932389
nyq_freq = None
23942390

2395-
# See if the color was specified, otherwise rotate
2396-
if kwargs.get('color', None) or any(
2397-
[isinstance(arg, str) and
2398-
any([c in arg for c in "bgrcmykw#"]) for arg in fmt]):
2399-
color_arg = {} # color set by *fmt, **kwargs
2400-
else:
2401-
color_arg = {'color': color_cycle[
2402-
(idx_sys + color_offset) % len(color_cycle)]}
2391+
# Determine the color to use for this response
2392+
color = _get_color(
2393+
color, fmt=fmt, offset=color_offset + idx_sys,
2394+
color_cycle=color_cycle)
2395+
2396+
# To avoid conflict with *fmt, only pass color kw if non-None
2397+
color_arg = {} if color is None else {'color': color}
24032398

24042399
# Decide on the system name
24052400
sysname = response.sysname if response.sysname is not None \

control/phaseplot.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,12 @@ def separatrices(
548548
Parameters to pass to system. For an I/O system, `params` should be
549549
a dict of parameters and values. For a callable, `params` should be
550550
dict with key 'args' and value given by a tuple (passed to callable).
551-
color : str
552-
Plot the streamlines in the given color.
551+
color : matplotlib color spec, optional
552+
Plot the separatrics in the given color. If a single color
553+
specification is given, this is used for both stable and unstable
554+
separatrices. If a tuple is given, the first element is used as
555+
the color specification for stable separatrices and the second
556+
elmeent for unstable separatrices.
553557
ax : matplotlib.axes.Axes
554558
Use the given axes for the plot, otherwise use the current axes.
555559

control/pzmap.py

+7-16
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919

2020
from . import config
2121
from .config import _process_legacy_keyword
22-
from .ctrlplot import ControlPlot, _get_line_labels, _process_ax_keyword, \
23-
_process_legend_keywords, _process_line_labels, _update_plot_title
22+
from .ctrlplot import ControlPlot, _get_color, _get_color_offset, \
23+
_get_line_labels, _process_ax_keyword, _process_legend_keywords, \
24+
_process_line_labels, _update_plot_title
2425
from .freqplot import _freqplot_defaults
2526
from .grid import nogrid, sgrid, zgrid
2627
from .iosys import isctime, isdtime
@@ -367,15 +368,8 @@ def pole_zero_plot(
367368
if grid is not None:
368369
warnings.warn("axis already exists; grid keyword ignored")
369370

370-
# Handle color cycle manually as all root locus segments
371-
# of the same system are expected to be of the same color
372-
# TODO: replace with common function?
373-
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
374-
color_offset = 0
375-
if len(ax.lines) > 0:
376-
last_color = ax.lines[-1].get_color()
377-
if last_color in color_cycle:
378-
color_offset = color_cycle.index(last_color) + 1
371+
# Get color offset for the next line to be drawn
372+
color_offset, color_cycle = _get_color_offset(ax)
379373

380374
# Create a list of lines for the output
381375
out = np.empty(
@@ -388,11 +382,8 @@ def pole_zero_plot(
388382
poles = response.poles
389383
zeros = response.zeros
390384

391-
# Get the color to use for this system
392-
if user_color is None:
393-
color = color_cycle[(color_offset + idx) % len(color_cycle)]
394-
else:
395-
color = user_color
385+
# Get the color to use for this response
386+
color = _get_color(user_color, offset=color_offset + idx)
396387

397388
# Plot the locations of the poles and zeros
398389
if len(poles) > 0:

control/tests/ctrlplot_test.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def test_plot_linestyle_processing(resp_fcn, plot_fcn):
331331
sys2 = ct.rss(4, 1, 1, strictly_proper=True, name="sys[2]")
332332

333333
# Set up arguments
334-
args, _, _, kwargs, meth_kwargs, plot_kwargs, resp_kwargs = \
334+
args1, args2, _, kwargs, meth_kwargs, plot_kwargs, resp_kwargs = \
335335
setup_plot_arguments(resp_fcn, plot_fcn)
336336
default_labels = ["sys[1]", "sys[2]"]
337337
expected_labels = ["sys1_", "sys2_"]
@@ -340,8 +340,12 @@ def test_plot_linestyle_processing(resp_fcn, plot_fcn):
340340
default_labels = ["P=sys[1]", "P=sys[2]"]
341341

342342
# Set line color
343-
cplt = plot_fcn(*args, **kwargs, **plot_kwargs, color='r')
344-
assert cplt.lines.reshape(-1)[0][0].get_color() == 'r'
343+
cplt1 = plot_fcn(*args1, **kwargs, **plot_kwargs, color='r')
344+
assert cplt1.lines.reshape(-1)[0][0].get_color() == 'r'
345+
346+
# Second plot, new line color
347+
cplt2 = plot_fcn(*args2, **kwargs, **plot_kwargs, color='g')
348+
assert cplt2.lines.reshape(-1)[0][0].get_color() == 'g'
345349

346350
# Make sure that docstring documents line properties
347351
if plot_fcn not in legacy_plot_fcns:
@@ -350,12 +354,12 @@ def test_plot_linestyle_processing(resp_fcn, plot_fcn):
350354

351355
# Set other characteristics if documentation says we can
352356
if "line properties" in plot_fcn.__doc__:
353-
cplt = plot_fcn(*args, **kwargs, **plot_kwargs, linewidth=5)
357+
cplt = plot_fcn(*args1, **kwargs, **plot_kwargs, linewidth=5)
354358
assert cplt.lines.reshape(-1)[0][0].get_linewidth() == 5
355359

356360
# If fmt string is allowed, use it to set line color and style
357361
if "*fmt" in plot_fcn.__doc__:
358-
cplt = plot_fcn(*args, 'r--', **kwargs, **plot_kwargs)
362+
cplt = plot_fcn(*args1, 'r--', **kwargs, **plot_kwargs)
359363
assert cplt.lines.reshape(-1)[0][0].get_color() == 'r'
360364
assert cplt.lines.reshape(-1)[0][0].get_linestyle() == '--'
361365

doc/plotting.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,15 @@ various ways. The following general rules apply:
449449
2x2 array of subplots should be given a 2x2 array of axes for the ``ax``
450450
keyword).
451451

452-
* The ```color``, ``linestyle``, ``linewidth``, and other matplotlib line
452+
* The ``color``, ``linestyle``, ``linewidth``, and other matplotlib line
453453
property arguments can be used to override the default line properties.
454454
If these arguments are absent, the default matplotlib line properties are
455455
used and the color cycles through the default matplotlib color cycle.
456456

457-
The :func:`~control.bode_plot`, :func:`~control.time_response_plot`, and
458-
selected other commands can also accept a matplotlib format string (e.g.,
459-
'r--'). The format string that must appear as a positional argument
460-
right after the required data argumnt.
457+
The :func:`~control.bode_plot`, :func:`~control.time_response_plot`,
458+
and selected other commands can also accept a matplotlib format
459+
string (e.g., 'r--'). The format string must appear as a positional
460+
argument right after the required data argumnt.
461461

462462
Note that line property arguments are the same for all lines generated as
463463
part of a single plotting command call, including when multiple responses

0 commit comments

Comments
 (0)