Matplotlib: Plotting Numerous Disconnected Line Segments with Different Colors

Matplotlib: Plotting numerous disconnected line segments with different colors

OK, I ended up rasterising the lines on a PIL image before converting it to a numpy array:

from PIL import Image
from PIL import ImageDraw
import random as rnd
import numpy as np
import matplotlib.pyplot as plt

N = 60000
s = (500, 500)

im = Image.new('RGBA', s, (255,255,255,255))
draw = ImageDraw.Draw(im)

for i in range(N):
x1 = rnd.random() * s[0]
y1 = rnd.random() * s[1]
x2 = rnd.random() * s[0]
y2 = rnd.random() * s[1]
alpha = rnd.random()
color = (int(rnd.random() * 256), int(rnd.random() * 256), int(rnd.random() * 256), int(alpha * 256))
draw.line(((x1,y1),(x2,y2)), fill=color, width=1)

plt.imshow(np.asarray(im),
origin='lower')
plt.show()

This is by far the fastest solution and it fits my real-time needs perfectly. One caveat though is the lines are drawn without anti-aliasing.

Plotting multiple segments with colors based on some variable with matplotlib

You can consider the following:

import numpy as np 
import pylab as pl

# normalize this
color_param = np.array([9.0, 2.0, 21.0])
color_param = (color_param - color_param.min())/(color_param.max() - color_param.min())

data = [(-118, -118), (34.07, 34.16),
(-117.99, -118.15), (34.07, 34.16),
(-118, -117.98), (34.16, 34.07)]

startD = data[::2]
stopD = data[1::2]

for start, stop, col in zip( startD, stopD, color_param):
pl.plot( start, stop, color = pl.cm.jet(col) )

pl.show()

Rememebr that the colormaps pl.cm.hot(0.7) will return a color value when presented a number between 0 and 1. This comes in very handy sometimes, like in your case

Edit:

For a red-to-green colormap:

import pylab as pl 
import matplotlib.colors as col
import numpy as np

cdict = {'red': [(0.0, 1.0, 1.0),
(1.0, 0.0, 0.0)],
'green': [(0.0, 0.0, 0.0),
(1.0, 1.0, 1.0)],
'blue': [(0.0, 0.0, 0.0),
(1.0, 0.0, 0.0)]}

my_cmap = col.LinearSegmentedColormap('my_colormap',cdict,256)

for theta in np.linspace(0, np.pi*2, 30):
pl.plot([0,np.cos(theta)], [0,np.sin(theta)], color=my_cmap(theta/(2*np.pi)) )

pl.show()

Create line chart with multicolored lines for different y-values with matplotlib

What are the points and segments for?
Why do i have to reshape and concatenate them?

A standard plot uses an array of x-values ([x0, x1, x2, ...] and y-values ([y0, y1, y2, ...]. These will be connected as points (x0, y0) to (x1, y1) to (x2, y2) to ... . But this approach only allows for one single color for everything.

The solution you copied, uses single line segments, the first segment is "(x0, y0) to (x1, y1)". The second segment reuses (x1, y1), drawing (x1, y1) to (x2, y2). Such segments can be given individual colors. For this, it is needed that each segment is represented as [[x0, y0], [x1, y1]] (a 2D array). These segments can be created from the original x and y arrays. All segments together form a 3D array [[[x0, y0], [x1, y1]], [[x1, y1], [x2, y2]], ... ].

The relevant code looks like:

points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)

np.array([x, y]) creates a 2xN array of x and corresponding y positions. Calling .T transposes this to a Nx2 array. To make it similar to the structure needed for the 3D segments array, the points array is reshaped to a Nx1x2 array (note that -1 here gets replaced by the value needed to have the same number of elements in the reshaped array as in the original one). Now the points have the structure [[[x0, y0]], [[x1, y1]], ...]

points[:-1] is just a list of all points except the last one. These can be used for the starting points of the segments.

Similarly, points[1:] is just a list of all points except the first. These can be used for the end points of the segments.

np.concatenate([..., ..., axis=1) joins the two lists of points together over their second dimension (axis=1) in the structure for the segments array. So, this creates the desired list of segments: [[[x0, y0], [x1, y1]], [[x1, y1], [x2, y2]], ... ].

what does lc.set_array(y) do in this code?

Here set_array assigns one "color value" to each individual each line segment. Instead of y any other numerical value could be used. Replacing y by x would have the colors following the x-axis. These "color values" get converted to real colors using the assigned colormap. The lowest of the values will be mapped to the lowest color, the highest value will be mapped to the highest color, with the rest following smoothly in-between. (This also works with continuous colormaps, for example a range from blue over white to red.)

Is there any way i can make the code shorter and more tidy especially the part where i assign label, line width and autoview? Can I combine all of them in one line? Instead of writing each line for each attribute.

There now are 3 simple calls, where you can add extra parameters (fontsize, color, ...) in a clear way. Just leave out the parameters and calls you don't need, matplotlib will provide adequate defaults. Changing everything to one complex call, where it wouldn't be clear which setting applies to what, would be less readable and harder to maintain.

Normally, when plotting a curve, the limits for the x and y axes are calculated automatically. But for line segments the axes limits aren't modified (this allows to add arrows and auxiliary lines to a plot while preserving the initial limits). Calling ax.autoscale_view() will recalculate the limits in case it is desired.

Two point segment plot in matplotlib

Does this achieve what you were hoping?

import numpy as np
import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5, 6]
y = [1.2, 1.2, 2.1, 2.1, -4.1, -4.1]

plt.plot(x, y, 'm--')

pair_x_array = np.reshape(x, (-1, 2))
pair_y_array = np.reshape(y, (-1, 2))
for i, pair_x in enumerate(pair_x_array):
pair_y = pair_y_array[i]
plt.plot(pair_x, pair_y, 'm', linewidth=3)

plt.show()

Matplotlib: How to colorize a large number of line segments as independent gradients, efficiently

One (minor) speedup would be adding a single line collection instead of 10000 separate line collections.

As long as all of the lines share the same colormap, you can group them into a single line collection, and each can still have an independent gradient.

Matplotlib is still slow for this sort of thing. It's optimized for quality output, rather than fast draw time. However, you can speed things up a bit (~3x).

So, as an example of how I think you're probably (?) doing it now:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
# Make random number generation consistent between runs
np.random.seed(5)

def main():
numlines, numpoints = 2, 3
lines = np.random.random((numlines, numpoints, 2))

fig, ax = plt.subplots()
for line in lines:
# Add "num" additional segments to the line
segments, color_scalar = interp(line, num=20)
coll = LineCollection(segments)
coll.set_array(color_scalar)
ax.add_collection(coll)
plt.show()

def interp(data, num=20):
"""Add "num" additional points to "data" at evenly spaced intervals and
separate into individual segments."""
x, y = data.T
dist = np.hypot(np.diff(x - x.min()), np.diff(y - y.min())).cumsum()
t = np.r_[0, dist] / dist.max()

ti = np.linspace(0, 1, num, endpoint=True)
xi = np.interp(ti, t, x)
yi = np.interp(ti, t, y)

# Insert the original vertices
indices = np.searchsorted(ti, t)
xi = np.insert(xi, indices, x)
yi = np.insert(yi, indices, y)

return reshuffle(xi, yi), ti

def reshuffle(x, y):
"""Reshape the line represented by "x" and "y" into an array of individual
segments."""
points = np.vstack([x, y]).T.reshape(-1,1,2)
points = np.concatenate([points[:-1], points[1:]], axis=1)
return points

if __name__ == '__main__':
main()

Instead, I would reccomend doing something along these lines (the only differences are in the main function):

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
# Make random number generation consistent between runs
np.random.seed(5)

def main():
numlines, numpoints = 2, 3
points = np.random.random((numlines, numpoints, 2))

# Add "num" additional segments to each line
segments, color_scalar = zip(*[interp(item, num=20) for item in points])

segments = np.vstack(segments)
color_scalar = np.hstack(color_scalar)

fig, ax = plt.subplots()
coll = LineCollection(segments)
coll.set_array(color_scalar)
ax.add_collection(coll)

plt.show()

def interp(data, num=20):
"""Add "num" additional points to "data" at evenly spaced intervals and
separate into individual segments."""
x, y = data.T
dist = np.hypot(np.diff(x - x.min()), np.diff(y - y.min())).cumsum()
t = np.r_[0, dist] / dist.max()

ti = np.linspace(0, 1, num, endpoint=True)
xi = np.interp(ti, t, x)
yi = np.interp(ti, t, y)

# Insert the original vertices
indices = np.searchsorted(ti, t)
xi = np.insert(xi, indices, x)
yi = np.insert(yi, indices, y)

return reshuffle(xi, yi), ti

def reshuffle(x, y):
"""Reshape the line represented by "x" and "y" into an array of individual
segments."""
points = np.vstack([x, y]).T.reshape(-1,1,2)
points = np.concatenate([points[:-1], points[1:]], axis=1)
return points

if __name__ == '__main__':
main()

Both versions generate an identical plot:

Sample Image


If we crank the number of lines up to 10000, though, we'll start to see significant differences in performance.

Using 10000 lines, with 3 points each and an additional 20 points interpolated throughout for the color gradient (23 segments in each line) and looking at the time it takes to save a figure to a png:

Took 10.866694212 sec with a single collection
Took 28.594727993 sec with multiple collections

So, using a single line collection will give a bit less than a 3x speedup in this particular case. It's not stellar, but it's better than nothing.

Here's the timing code and the output figure, for whatever it's worth (The output figures aren't quite identical due to different orderings of the drawing. If you need control over z-level, you'll have to stick to separate line collections):

Sample Image

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import time
# Make random number generation consistent between runs
np.random.seed(5)

def main():
numlines, numpoints = 10000, 3
lines = np.random.random((numlines, numpoints, 2))

# Overly simplistic timing, but timeit is overkill for this exmaple
tic = time.time()
single_collection(lines).savefig('/tmp/test_single.png')
toc = time.time()
print 'Took {} sec with a single collection'.format(toc-tic)

tic = time.time()
multiple_collections(lines).savefig('/tmp/test_multiple.png')
toc = time.time()
print 'Took {} sec with multiple collections'.format(toc-tic)

def single_collection(lines):
# Add "num" additional segments to each line
segments, color_scalar = zip(*[interp(item, num=20) for item in lines])
segments = np.vstack(segments)
color_scalar = np.hstack(color_scalar)

fig, ax = plt.subplots()
coll = LineCollection(segments)
coll.set_array(color_scalar)
ax.add_collection(coll)
return fig

def multiple_collections(lines):
fig, ax = plt.subplots()
for line in lines:
# Add "num" additional segments to the line
segments, color_scalar = interp(line, num=20)
coll = LineCollection(segments)
coll.set_array(color_scalar)
ax.add_collection(coll)
return fig

def interp(data, num=20):
"""Add "num" additional points to "data" at evenly spaced intervals and
separate into individual segments."""
x, y = data.T
dist = np.hypot(np.diff(x - x.min()), np.diff(y - y.min())).cumsum()
t = np.r_[0, dist] / dist.max()

ti = np.linspace(0, 1, num, endpoint=True)
xi = np.interp(ti, t, x)
yi = np.interp(ti, t, y)

# Insert the original vertices
indices = np.searchsorted(ti, t)
xi = np.insert(xi, indices, x)
yi = np.insert(yi, indices, y)

return reshuffle(xi, yi), ti

def reshuffle(x, y):
"""Reshape the line represented by "x" and "y" into an array of individual
segments."""
points = np.vstack([x, y]).T.reshape(-1,1,2)
points = np.concatenate([points[:-1], points[1:]], axis=1)
return points

if __name__ == '__main__':
main()

Plotting lines connecting points

I think you're going to need separate lines for each segment:

import numpy as np
import matplotlib.pyplot as plt

x, y = np.random.random(size=(2,10))

for i in range(0, len(x), 2):
plt.plot(x[i:i+2], y[i:i+2], 'ro-')

plt.show()

(The numpy import is just to set up some random 2x10 sample data)

Sample Image

Is there a way to color segments of a line in base R?

This is possible, with the segments function.

for(i in 1:(length(templog$time)-1)){

segments(templog$time[i],
templog$temp[i],
templog$time[i+1],
templog$temp[i+1],

col=templog$heaterstatus[i])

}

Basically, you're iterating through each pair of points & draws a straight line in the specified color. Also, you can simplify your plot() call-

plot(temp~time, data=templog, type='n')

would suffice.

Hope this helps :-)



Related Topics



Leave a reply



Submit