Skip to content

Commit 9a407ac

Browse files
committed
unify color processesing
1 parent 829b624 commit 9a407ac

File tree

6 files changed

+114
-50
lines changed

6 files changed

+114
-50
lines changed

control/ctrlplot.py

Lines changed: 75 additions & 5 deletions
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

Lines changed: 13 additions & 18 deletions
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

Lines changed: 6 additions & 2 deletions
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

Lines changed: 7 additions & 16 deletions
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

Lines changed: 9 additions & 5 deletions
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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,10 +454,10 @@ various ways. The following general rules apply:
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)