-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
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
[ENH]: Color along line by a value #28242
Comments
The work around is to over interpolate your data, which is essentially what would have to happen to make a continuous line anyway. Whether marplotlib could try and do a better job with this is a reasonable question. We have long relied on the color line example, but it's not an unreasonable ask to make this its own api. |
No low-level backend natively supports multicolored lines. Thus, every Implenetation has to be made up of separate single-colored segments; i.e. you cannot get fundamentally better than what LineCollection offers. But you may be able to use it more intelligently. There are two issues with a naive approach:
Taking these two together, it may be an option to center the colors around the points; i.e. calculate middle points in between your actual data points and make 3-point segments |
@timhoffm this all seems correct to me and would avoid needing to overinterpolate. |
Thanks for the suggestion @timhoffm! I tried all the approaches, and yours turned out great (and has the bonus of requiring no interpolation). I suppose the question now is whether this is functionality you'd be open to add to the matplotlib interface (I'd be happy to help with this), rather than every user having to do it manually. It seems like quite a few people are looking for similar functionality. Source code to produce these figuresimport numpy as np
from scipy.interpolate import splprep, splev
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
# ==============================================================================
# Utilities to make setup and plotting easier
# ==============================================================================
# -------------- Line x, y, and color value sampler --------------
x = [0.0, 0.8, 0.75, 0.7, 1.5]
y = [0.0, 0.3, 1.0, 0.3, 0.0]
c = [0.0, 0.1, 0.5, 0.9, 1.0]
u = np.linspace(0, 1, len(x))
# Interpolate x, y, and c
spl, _ = splprep([x, y, c], u=u, s=0)
# Function to get x, y, and c values out at a parametric coordinate
line = lambda u_sample: splev(u_sample, spl)
# Suppose this is our "raw" data we'd like to plot
u_raw = np.linspace(0, 1, 100)
x_raw, y_raw, c_raw = line(u_raw)
# Use this colormap
cmap = "plasma"
# -------------- Function to set up axes and plot --------------
def plot_results(plotting_callback, outfile):
fig, ax = plt.subplots()
# Inset axes around peak
width = 0.1
height = 0.1
y_shift = -0.025
x1, x2 = x[2] - width / 2, x[2] + width / 2
y1, y2 = y[2] - height / 2 + y_shift, y[2] + height / 2 + y_shift
ax_zoom = ax.inset_axes([0.65, 0.55, 0.4, 0.4 * height / width])
ax_zoom.set_xlim(x1, x2)
ax_zoom.set_ylim(y1, y2)
ax_zoom.set_xticks([])
ax_zoom.set_yticks([])
_, connectors = ax.indicate_inset_zoom(ax_zoom, edgecolor="k", alpha=1.0)
for connector in connectors:
connector.set_visible(False)
for ax_cur, zoomed in zip([ax, ax_zoom], [False, True]):
# Approximate the zoomed in scale by changing line width/scatter point size
plotting_callback(ax_cur, zoomed)
ax.set_aspect("equal")
ax_zoom.set_aspect("equal")
ax.axis("off")
ax.set_xlim(-0.1, 1.6)
ax.set_ylim(-0.2, 1.1)
fig.savefig(outfile, bbox_inches="tight", dpi=400)
plt.close(fig)
# ==============================================================================
# Line collection-based approaches
# ==============================================================================
# -------------- Original LineCollection approach with straight segment for each point --------------
def line_collection_orig(ax, zoomed):
"""
Adapted from the top answer on https://stackoverflow.com/questions/8500700/how-to-plot-a-gradient-color-line
"""
scale = 25 if zoomed else 4
points = np.array([x_raw, y_raw]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1, :], points[1:, :]], axis=1)
lc = LineCollection(segments, array=c_raw, cmap=cmap, linewidth=2.0 * scale)
ax.add_collection(lc)
plot_results(line_collection_orig, "colorline_line_collection_orig.jpeg")
# -------------- New LineCollection approach with 3-point segments to midpoints --------------
def line_collection_new(ax, zoomed):
scale = 25 if zoomed else 4
# Compute the midpoints of the line segments. Include the first and last points
# twice so we don't need any special syntax to handle them.
x_midpoints = np.hstack((x_raw[0], 0.5 * (x_raw[1:] + x_raw[:-1]), x_raw[-1]))
y_midpoints = np.hstack((y_raw[0], 0.5 * (y_raw[1:] + y_raw[:-1]), y_raw[-1]))
# Interlace the midpoints with the raw data
new_size = x_raw.size * 2 + 1
x_new = np.zeros(new_size)
y_new = np.zeros(new_size)
x_new[::2] = x_midpoints
x_new[1::2] = x_raw
y_new[::2] = y_midpoints
y_new[1::2] = y_raw
# Similar approach here to line_collection_orig, but use three points for each line segment
points = np.array([x_new, y_new]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1:2, :], points[1::2, :], points[2::2, :]], axis=1)
lc = LineCollection(segments, array=c_raw, cmap=cmap, linewidth=2.0 * scale, joinstyle="bevel", capstyle="butt")
ax.add_collection(lc)
plot_results(line_collection_new, "colorline_line_collection_new.jpeg")
# ==============================================================================
# Scatter-based approaches
# ==============================================================================
# -------------- Scatter the raw points --------------
def scatter(ax, zoomed):
scale = 30 if zoomed else 1
ax.scatter(x_raw, y_raw, 100 * scale, c=c_raw, cmap=cmap)
plot_results(scatter, "colorline_scatter.jpeg")
# -------------- Interpolate x, y, and color values and scatter them --------------
def scatter_interp(ax, zoomed):
# Interpolate very finely
u = np.linspace(0, 1, 2000)
x, y, c = line(u)
scale = 30 if zoomed else 1
ax.scatter(x, y, 100 * scale, c=c, cmap=cmap)
plot_results(scatter_interp, "colorline_scatter_interp.jpeg") Original
|
@eytanadler Do you want to write up an example? I think this should replace https://matplotlib.org/devdocs/gallery/color/color_by_yvalue.html because it's a far superior solution. |
Sure, I can do that! Do you think it's too much to include this functionality directly in the matplotlib API? It seems like a common request and a bit silly for people to manually implement this solution every time. I do get that it's a bit specialized, so I could see an argument for either way. |
Let's start with an example. Making this a core function requires careful API design. If we get something wrong, it's very hard to change later due to our back-compatibility policy. Also, the LineCollection method has the "white lines" issue, OTOH, scatter requires strong upsampling. So there's not yet the one optimal solution providing the high quality we aspire for core functionality. |
Should this also replace this example? |
Coincidentally, I just came across #19286, which appears at first blush to implement things similarly? However, the contributor does seem to have disappeared. I didn't review any of the code, so I don't know what the state of the PR is right now. |
#19286 is only very loosely coupled. It creates a new |
I'm the contributor of #19286 My implementation was only intended to work for straight lines, without curvatures, and was based on implementations in the StackOverflow page that is shared in the issue. Since there are better suggestions like using scatter plot for smoother curve and color change, the PR is safe to ignore. |
Closed by implementing as examples in |
That PR doesn't exist yet; did you mean #28307, which isn't merged yet? |
Yes, I've closed preemtively, because #28307 is ready to go modulo minor comments. It does not reference this as "closes #..." and AFAIK, that pattern does not work in followup comments, so I saw the danger of this being forgotten and left open. While not strictly following the process, IMHO there's little harm in closing already. |
You can link it from the Development box on the right-hand side, though several times GitHub will arbitrarily decide not to complete issues even if you type the whole number. It did work this time though, so I've linked the PR back to this issue. |
Problem
I'd like to plot a path and assign a color or gradient along that path by some third value via a colormap. Current solutions seem to be hacking line collections (1) or using lots of scatter points to replicate a line (2, 3). The line collection approach appears to be the most common but can get ugly in regions with lots of curvature (see image). The limitation seems to be that lines and points in matplotlib can have only a single color. As far as I can tell, "continuous" gradients are supported only with
imshow
, which discretizes them. How challenging would it be to implement this feature properly? If the answer is "very challenging," is there a better workaround?Proposed solution
No response
The text was updated successfully, but these errors were encountered: