Skip to content

Commit 07dc8b6

Browse files
authored
Merge pull request #163 from lukelbd/parametric-fixes
Parametric plot fixes
2 parents b002ea2 + fe85c84 commit 07dc8b6

File tree

3 files changed

+82
-66
lines changed

3 files changed

+82
-66
lines changed

docs/1dplots.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,38 +410,36 @@
410410
# %%
411411
import proplot as plot
412412
import numpy as np
413-
N = 50
414-
cmap = 'IceFire'
415-
values = np.linspace(-N / 2, N / 2, N)
416413
fig, axs = plot.subplots(
417414
share=0, ncols=2, wratios=(2, 1),
418415
axwidth='7cm', aspect=(2, 1)
419416
)
420417
axs.format(suptitle='Parametric plots demo')
418+
cmap = 'IceFire'
421419

422420
# Parametric line with smooth gradations
423421
ax = axs[0]
424422
state = np.random.RandomState(51423)
423+
N = 50
424+
x = (state.rand(N) - 0.52).cumsum()
425+
y = state.rand(N)
426+
c = np.linspace(-N / 2, N / 2, N) # color values
425427
m = ax.parametric(
426-
(state.rand(N) - 0.5).cumsum(), state.rand(N),
427-
cmap=cmap, values=values, lw=7, extend='both'
428-
)
429-
ax.format(
430-
xlabel='xlabel', ylabel='ylabel',
431-
title='Line with smooth gradations'
428+
x, y, c, cmap=cmap, lw=7, interp=5, capstyle='round', joinstyle='round'
432429
)
433-
ax.format(xlim=(-1, 5), ylim=(-0.2, 1.2))
430+
ax.format(xlabel='xlabel', ylabel='ylabel', title='Line with smooth gradations')
431+
ax.format(xmargin=0.05, ymargin=0.05)
434432
ax.colorbar(m, loc='b', label='parametric coordinate', locator=5)
435433

436434
# Parametric line with stepped gradations
437435
N = 12
438436
ax = axs[1]
439-
values = np.linspace(-N / 2, N / 2, N + 1)
440437
radii = np.linspace(1, 0.2, N + 1)
441438
angles = np.linspace(0, 4 * np.pi, N + 1)
442439
x = radii * np.cos(1.4 * angles)
443440
y = radii * np.sin(1.4 * angles)
444-
m = ax.parametric(x, y, cmap=cmap, values=values, linewidth=15, interp=False)
441+
c = np.linspace(-N / 2, N / 2, N + 1)
442+
m = ax.parametric(x, y, c, cmap=cmap, lw=15)
445443
ax.format(
446444
xlim=(-1, 1), ylim=(-1, 1), title='Step gradations',
447445
xlabel='cosine angle', ylabel='sine angle'

proplot/axes/base.py

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
_add_errorbars, _bar_wrapper, _barh_wrapper, _boxplot_wrapper,
1515
_cmap_changer, _cycle_changer,
1616
_fill_between_wrapper, _fill_betweenx_wrapper,
17-
_hist_wrapper, _plot_wrapper, _scatter_wrapper,
17+
_hist_wrapper, _parametric_wrapper, _plot_wrapper, _scatter_wrapper,
1818
_standardize_1d, _standardize_2d,
1919
_text_wrapper, _violinplot_wrapper,
2020
colorbar_wrapper, legend_wrapper,
@@ -1506,6 +1506,7 @@ def panel_axes(self, side, **kwargs):
15061506
side = self._loc_translate(side, 'panel')
15071507
return self.figure._add_axes_panel(self, side, **kwargs)
15081508

1509+
@_parametric_wrapper
15091510
@_standardize_1d
15101511
@_cmap_changer
15111512
def parametric(
@@ -1522,15 +1523,19 @@ def parametric(
15221523
15231524
Parameters
15241525
----------
1525-
*args : (y,) or (x,y)
1526+
*args : (y,), (x, y), or (x, y, values)
15261527
The coordinates. If `x` is not provided, it is inferred from `y`.
1527-
cmap : colormap spec, optional
1528-
The colormap specifier, passed to `~proplot.constructor.Colormap`.
15291528
values : list of float
15301529
The parametric values used to map points on the line to colors
1531-
in the colormap.
1530+
in the colormap. This can also be passed as a third positional argument.
1531+
cmap : colormap spec, optional
1532+
The colormap specifier, passed to `~proplot.constructor.Colormap`.
1533+
cmap_kw : dict, optional
1534+
Keyword arguments passed to `~proplot.constructor.Colormap`.
15321535
norm : normalizer spec, optional
15331536
The normalizer, passed to `~proplot.constructor.Norm`.
1537+
norm_kw : dict, optional
1538+
Keyword arguments passed to `~proplot.constructor.Norm`.
15341539
interp : int, optional
15351540
If greater than ``0``, we interpolate to additional points
15361541
between the `values` coordinates. The number corresponds to the
@@ -1552,74 +1557,35 @@ def parametric(
15521557
The parametric line. See `this matplotlib example \
15531558
<https://matplotlib.org/gallery/lines_bars_and_markers/multicolored_line>`__.
15541559
"""
1555-
# First error check
1556-
# WARNING: So far this only works for 1D *x* and *y* coordinates.
1557-
# Cannot draw multiple colormap lines at once
1558-
if values is None:
1559-
raise ValueError('Requires a "values" keyword arg.')
1560-
if len(args) not in (1, 2):
1561-
raise ValueError(f'Requires 1-2 arguments, got {len(args)}.')
1562-
y = np.array(args[-1]).squeeze()
1563-
x = np.arange(
1564-
y.shape[-1]) if len(args) == 1 else np.array(args[0]).squeeze()
1565-
values = np.array(values).squeeze()
1566-
if x.ndim != 1 or y.ndim != 1 or values.ndim != 1:
1567-
raise ValueError(
1568-
f'x ({x.ndim}d), y ({y.ndim}d), and values ({values.ndim}d)'
1569-
' must be 1-dimensional.'
1570-
)
1571-
if len(x) != len(y) or len(x) != len(values) or len(y) != len(values):
1572-
raise ValueError(
1573-
f'{len(x)} xs, {len(y)} ys, but {len(values)} '
1574-
' colormap values.'
1575-
)
1576-
1577-
# Interpolate values to allow for smooth gradations between values
1578-
# (interp=False) or color switchover halfway between points
1579-
# (interp=True). Then optionally interpolate the colormap values.
1580-
if interp > 0:
1581-
x, y, values = [], [], []
1582-
xorig, yorig, vorig = x, y, values
1583-
for j in range(xorig.shape[0] - 1):
1584-
idx = slice(None)
1585-
if j + 1 < xorig.shape[0] - 1:
1586-
idx = slice(None, -1)
1587-
x.extend(
1588-
np.linspace(xorig[j], xorig[j + 1], interp + 2)[idx].flat
1589-
)
1590-
y.extend(
1591-
np.linspace(yorig[j], yorig[j + 1], interp + 2)[idx].flat
1592-
)
1593-
values.extend(
1594-
np.linspace(vorig[j], vorig[j + 1], interp + 2)[idx].flat
1595-
)
1596-
x, y, values = np.array(x), np.array(y), np.array(values)
1597-
15981560
# Get x/y coordinates and values for points to the 'left' and 'right'
15991561
# of each joint
1562+
x, y = args # standardized by parametric wrapper
1563+
interp # avoid U100 unused argument error (arg is handled by wrapper)
16001564
coords = []
16011565
levels = edges(values)
16021566
for j in range(y.shape[0]):
16031567
if j == 0:
16041568
xleft, yleft = [], []
16051569
else:
1606-
xleft = [(x[j - 1] + x[j]) / 2, x[j]]
1607-
yleft = [(y[j - 1] + y[j]) / 2, y[j]]
1570+
xleft = [0.5 * (x[j - 1] + x[j]), x[j]]
1571+
yleft = [0.5 * (y[j - 1] + y[j]), y[j]]
16081572
if j + 1 == y.shape[0]:
16091573
xright, yright = [], []
16101574
else:
16111575
xleft = xleft[:-1] # prevent repetition when joined with right
16121576
yleft = yleft[:-1]
1613-
xright = [x[j], (x[j + 1] + x[j]) / 2]
1614-
yright = [y[j], (y[j + 1] + y[j]) / 2]
1577+
xright = [x[j], 0.5 * (x[j + 1] + x[j])]
1578+
yright = [y[j], 0.5 * (y[j + 1] + y[j])]
16151579
pleft = np.stack((xleft, yleft), axis=1)
16161580
pright = np.stack((xright, yright), axis=1)
16171581
coords.append(np.concatenate((pleft, pright), axis=0))
1582+
coords = np.array(coords)
16181583

16191584
# Create LineCollection and update with values
1585+
# NOTE: Default capstyle is butt but this may look weird with vector graphics
16201586
hs = mcollections.LineCollection(
1621-
np.array(coords), cmap=cmap, norm=norm,
1622-
linestyles='-', capstyle='butt', joinstyle='miter'
1587+
coords, cmap=cmap, norm=norm,
1588+
linestyles='-', capstyle='butt', joinstyle='miter',
16231589
)
16241590
hs.set_array(np.array(values))
16251591
hs.update({

proplot/axes/plot.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,56 @@ def add_errorbars(
10621062
return obj
10631063

10641064

1065+
def _parametric_wrapper_temporary(self, func, *args, interp=0, **kwargs):
1066+
"""
1067+
Calls `~proplot.axes.Axes.parametric` and optionally interpolates values before
1068+
they get passed to `cmap_changer` and the colormap boundaries are drawn.
1069+
"""
1070+
# Parse input arguments
1071+
# WARNING: So far this only works for 1D *x* and *y* coordinates.
1072+
# Cannot draw multiple colormap lines at once
1073+
if len(args) == 3:
1074+
x, y, values = args
1075+
elif 'values' in kwargs:
1076+
values = kwargs.pop('values')
1077+
if len(args) == 1:
1078+
y = np.asarray(args[0])
1079+
x = np.arange(y.shape[-1])
1080+
elif len(args) == 2:
1081+
x, y = args
1082+
else:
1083+
raise ValueError(f'1 to 3 positional arguments required, got {len(args)}.')
1084+
else:
1085+
raise ValueError('Missing required keyword argument "values".')
1086+
x, y, values = np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(values)
1087+
if (
1088+
any(_.ndim != 1 for _ in (x, y, values))
1089+
or len({x.size, y.size, values.size}) > 1
1090+
):
1091+
raise ValueError(
1092+
f'x {x.shape}, y {y.shape}, and values {values.shape} '
1093+
'must be 1-dimensional and have the same size.'
1094+
)
1095+
1096+
# Interpolate values to allow for smooth gradations between values
1097+
# (interp=False) or color switchover halfway between points
1098+
# (interp=True). Then optionally interpolate the colormap values.
1099+
if interp > 0:
1100+
xorig, yorig, vorig = x, y, values
1101+
x, y, values = [], [], []
1102+
for j in range(xorig.shape[0] - 1):
1103+
idx = slice(None)
1104+
if j + 1 < xorig.shape[0] - 1:
1105+
idx = slice(None, -1)
1106+
x.extend(np.linspace(xorig[j], xorig[j + 1], interp + 2)[idx].flat)
1107+
y.extend(np.linspace(yorig[j], yorig[j + 1], interp + 2)[idx].flat)
1108+
values.extend(np.linspace(vorig[j], vorig[j + 1], interp + 2)[idx].flat)
1109+
x, y, values = np.array(x), np.array(y), np.array(values)
1110+
1111+
# Call main function
1112+
return func(self, x, y, values=values, **kwargs)
1113+
1114+
10651115
def _plot_wrapper_deprecated(
10661116
self, func, *args, cmap=None, values=None, **kwargs
10671117
):
@@ -1350,6 +1400,7 @@ def barh_wrapper(
13501400
# NOTE: You *must* do juggling of barh keyword order --> bar keyword order
13511401
# --> barh keyword order, because horizontal hist passes arguments to bar
13521402
# directly and will not use a 'barh' method with overridden argument order!
1403+
func # avoid U100 error
13531404
kwargs.setdefault('orientation', 'horizontal')
13541405
if y is None and width is None:
13551406
raise ValueError(
@@ -3439,6 +3490,7 @@ def _wrapper(self, *args, **kwargs):
34393490
_fill_between_wrapper = _wrapper_decorator(fill_between_wrapper)
34403491
_fill_betweenx_wrapper = _wrapper_decorator(fill_betweenx_wrapper)
34413492
_hist_wrapper = _wrapper_decorator(hist_wrapper)
3493+
_parametric_wrapper = _wrapper_decorator(_parametric_wrapper_temporary)
34423494
_plot_wrapper = _wrapper_decorator(_plot_wrapper_deprecated)
34433495
_scatter_wrapper = _wrapper_decorator(scatter_wrapper)
34443496
_standardize_1d = _wrapper_decorator(standardize_1d)

0 commit comments

Comments
 (0)