How to draw slope triangles in matplotlib? - python

What I would like to have are the triangles as shown in the image:
Here is my code:
import matplotlib.pyplot as plt
data= [0.2855,0.3030,0.4995]
x = [1,2,3]
plt.plot(x, data)
plt.show
Is there a simple way of inserting these slope triangles as shown in the image in an automatic fashion? I would like to have the triangle in the middle between two points and with the slope written next to it.

Depending on your idea of "automatic fashion", this might be a suitable solution:
import matplotlib.pyplot as plt
import numpy as np
# Data
x = np.array([1, 2, 3])
y = np.array([0.2855, 0.3030, 0.4995])
# Calculate triangle coordinates values
x_mid = np.convolve(x, [0.5, 0.5], mode='valid')
x_tri = np.vstack((x_mid, x_mid + 0.3))
y_tri = np.interp(x_tri, x, y)
# Calculate slopes
slopes = np.diff(y) / np.diff(x)
# Plot
plt.plot(x, y)
plt.plot(x_tri, np.tile(y_tri[0, :], [2, 1]), 'r') # red horizontal line
plt.plot(np.tile(x_tri[1, :], [2, 1]), y_tri, 'r') # red vertical line
for i, slope in enumerate(slopes): # slope values
plt.text(x_tri[1, i] + 0.05, np.mean(y_tri[:, i]), r'{0:.3f}'.format(slope))
plt.show()
Output:
Put all the triangle stuff in a separate function, and it won't affect your main code too much.
Hope that helps!

The triangle construction can also be done using mpltools:
import matplotlib.pyplot as plt
from mpltools import annotation
import numpy as np
data = [0.2855, 0.3030, 0.4995]
x = [1, 2, 3]
# get midpoint coordinates
x_mid = np.convolve(x, [0.5, 0.5], mode='valid')
y_mid = np.interp(x_mid, x, data)
# compute the gradient of each segment
gradients = np.diff(data)/np.diff(x)
# plot
plt.plot(x, data)
axes = plt.gca()
for xm, ym, g in zip(x_mid, y_mid, gradients):
annotation.slope_marker((xm, ym), g)
plt.show()
The first argument of annotation.slope_marker is a tuple containing the coordinates for the left-hand corner of the triangle and the second argument is the gradient. So here we loop over the midpoints of the line segments and their gradients and annotate with a triangular slope marker for that gradient at those coordinates.
Expected Output:

Related

Python: is there a way to define a global scale of colors and plot colored lines using this custom scale?

I've been reading about multiple lines plotting and multicolored lines, but every time I read a post about it people use continuous set of data, like some trigonometrical function:
x = np.linspace(0, 3 * np.pi, 500)
y = np.sin(x)
So, here is my problem: I'm making a plotting script for 1D finite element problems like the image attached. I'm plotting the elements as individual lines with the X and Y coordinates array, and I would like to color the lines based on a third array like the axial stress or temperature, or any other
The problem is when I try to follow the examples I've found, every line has local color distribution, instead of a global distribution. I'm thinking about defining a global color scale based on the maximum and mininum values of the third array, let's say temperature, and passing the coordinates of each element + the current average temperature may do the job, but I don't know if something alike is possible
Anyone can help?
You can map values to a particular colormap, and plot each element as a single line with a particular color, like this:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.colors import Normalize
import numpy as np
# roto-translation matrix
T = lambda t, u, v: np.array([[np.cos(t), -np.sin(t), u], [np.sin(t), np.cos(t), 0], [0, 0, 1]])
# coordinates of a triangle
triangle = np.array([[-0.5, -0.5, 1], [0.5, -0.5, 1], [-0.5, 0.5, 1], [-0.5, -0.5, 1]]).T
n_elements = 10
values = np.linspace(-5, 5, n_elements)
# create a normalizer
norm = Normalize(vmin=values.min(), vmax=values.max())
# normalize values
norm_values = norm(values)
# choose a colormap
cmap = cm.magma
# create colors
colors = cmap(norm_values)
# map values to a colorbar
mappable = cm.ScalarMappable(norm=norm, cmap=cmap)
mappable.set_array(values)
f, ax = plt.subplots(1)
ax.set_aspect("equal")
for i in range(n_elements):
tr = np.matmul(T(i * np.pi, int(i / 2), 0), triangle)
ax.plot(tr[0, :], tr[1, :], color=colors[i])
cb = f.colorbar(mappable)
cb.set_label("Value")

How to generate a array with points of this curve?

I want to code a program to generate an array with coordinates to follow for drawing a shape like the white here, given are the blue points. Does anyone know how to do something like that or at least can give me a tip?
You could use e.g. InterpolatedUnivariateSpline to interpolate the points. As these spline functions are usually 1D, you could calculate x and y positions separately, depending on a new variable t going from 0 to 1.
import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
# positions of the given points
px = [1, 4, 3, 2, 5]
py = [1, 3, 4, 3, 1]
# 5 t-values, at t=0 in point 1, at t=1 reaching point 5
pt = np.linspace(0, 1, len(px))
# sx and sy are functions that interpolate the points at the given t-values
sx = interpolate.InterpolatedUnivariateSpline(pt, px)
sy = interpolate.InterpolatedUnivariateSpline(pt, py)
# calculate many intermediate values
t = np.linspace(0, 1, 500)
x = sx(t)
y = sy(t)
# show the original points together with the spline
fig, ax = plt.subplots(facecolor='black')
ax.axis('off')
plt.scatter(px, py, s=80, color='skyblue')
plt.plot(x, y, color='white')
for i, (xi, yi) in enumerate(zip(px, py), start=1):
ax.text(xi, yi, f'\n {i}', ha='left', va='center', size=30, color='yellow')
plt.show()

Partially discrete colormap matplotlib

While there are a number of examples of discrete colormap (a,b,c), I would like to do something a little different. I want to have a 3D surface plot that has a sharp contrast between a small value and zero, so the colors 'jump' or the colormap is partially discrete. My reason for this is that I want to more clearly distinguish between small values and what is consider to be 'zero' within a plot.
I am generating a 3D surface plot and want to use a colormap (like 'terrain') to indicate height on the Z-axis. However, I want there to be a 'gap' in the colormap to highlight values that are sufficiently far from z=0. Specifically, let's say z<1e-6 is the bottom threshold of the colormap (e.g., dark blue for terrain), but any value above that threshold to be in the middle of the colormap (e.g. green for terrain).
Below is a simple example and the corresponding output
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
y = np.linspace(-3, 3, 100)
x = np.linspace(-3, 3, 100)
z = np.zeros(shape=(x.shape[0], y.shape[0]))
for i in range(x.shape[0]):
# creating some generic Z-axis data
z[:, i] = norm.pdf(y, loc=0, scale=0.2+(i/100))
z[:, i] = z[:, i] / np.sum(z[:, i]) # normalizing
z = np.where(z < 1e-6, 0, z) # setting 'small enough' threshold
x_mat, y_mat = np.meshgrid(x, y)
f1 = plt.axes(projection='3d')
f1.plot_surface(x_mat, y_mat, z, cmap='terrain', edgecolor='none', rstride=1)
plt.show()
Here is what the output from above:
What I want the output to look like would be all the 'light blue' regions would instead be green. Once below the defined threshold (1e-6 here), the color would jump to dark blue (so no regions would be light blue).
Alright, I figured out a solution to my own problem. I adapted the solution from HERE to address my issue. Below is the code to accomplish this.
Setup:
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
from matplotlib.cm import get_cmap
y = np.linspace(-3, 3, 100)
x = np.linspace(-3, 3, 100)
z = np.zeros(shape=(x.shape[0], y.shape[0]))
x_mat, y_mat = np.meshgrid(x, y)
# Threshold to apply
z_threshold = 1e-6
for i in range(x.shape[0]):
z[:, i] = norm.pdf(y, loc=0, scale=0.2+(i/100))
z[:, i] = z[:, i] / np.sum(z[:, i]) # normalizing
Next I define two different colormaps. The first color map applies to all values above the threshold. If values are below the threshold, it sets that square as transparent.
cmap = get_cmap('terrain')
# 1.8 and 0.2 are used to restrict the upper and lower parts of the colormap
colors = cmap((z - z_threshold) / ((np.max(z)*1.8) - (np.min(z))) + 0.2)
# if below threshold, set as transparent (alpha=0)
colors[z < z_threshold, -1] = 0
The second colormap defines the color for all places below the threshold. This step isn't fully necessary, but it does prevent the plane from being drawn below the rest of the plot.
colors2 = cmap(z)
colors2[z >= z_threshold, -1] = 0
Now the colormaps can be used in two 3D plot calls
# init 3D plot
f1 = plt.axes(projection='3d')
# Plot values above the threshold
f1.plot_surface(x_mat, y_mat, z, facecolors=colors, edgecolor='none', rstride=1)
# Plot values below the threshold
z_below = np.zeros(shape=(x.shape[0], y.shape[0]))
f1.plot_surface(x_mat, y_mat, z_below,
facecolors=colors2, edgecolor='none', rstride=1, vmin=0)
# Setting the zlimits
f1.set_zlim([0, np.max(z)])
plt.show()
The above results in the following plot

How to plot lines around images in Matplotlib

I have two lists with the coordinates of points that I want to draw using matplotlib: hx and hy, and i have a function which draws these points as images: plotImage.
Now I would like to plot a line between these points, but the line should go somehow around the pictures. It doesn't have to go excctly by the borders of the images, the important thing is that it doesn't overlap with the pictures.
Here is an example, the green line is what I have now (its the convex hull), and the red line is an example of how the line should look like.
Here's my code:
def plotImage(xData, yData, im):
for x, y in zip(xData, yData):
bb = Bbox.from_bounds(x,y,10,10)
bb2 = TransformedBbox(bb,ax.transData)
bbox_image = BboxImage(bb2,
norm = None,
origin=None,
clip_on=False)
bbox_image.set_data(im)
ax.add_artist(bbox_image)
fig=plt.figure()
ax=fig.add_subplot(111)
img = plt.imread('pic.png')
gx = list(map(lambda x: x - 5, hx)) # so the center of the picture is in the point I want to plot
gy = list(map(lambda y: y - 5, hy)) #(without this, the image is drawn so that the picture's left bottom corner is in my point)
plotImage(gx,gy,img)
plt.plot(hx, hy, 'g-', markersize=10)
So my question is: how to calculate this red hull?
A example using Incrementing area of convex hull can be applied to your case.
from scipy.spatial import ConvexHull
import matplotlib.pyplot as plt
import numpy as np
gx = [-2, 2, -2, 2, 1, -0.5]
gy = [3, 4, -2, -1, -1, 0.5]
points = np.array((gx, gy)).T
convh = ConvexHull(points)
stretchCoef = 1.2
pointsStretched = points[convh.vertices]*stretchCoef
gx_new = pointsStretched[..., 0]
gy_new = pointsStretched[..., 1]

How to add line based on slope and intercept in Matplotlib?

In R, there is a function called abline in which a line can be drawn on a plot based on the specification of the intercept (first argument) and the slope (second argument). For instance,
plot(1:10, 1:10)
abline(0, 1)
where the line with an intercept of 0 and the slope of 1 spans the entire range of the plot. Is there such a function in Matplotlib?
A lot of these solutions are focusing on adding a line to the plot that fits the data. Here's a simple solution for adding an arbitrary line to the plot based on a slope and intercept.
import matplotlib.pyplot as plt
import numpy as np
def abline(slope, intercept):
"""Plot a line from slope and intercept"""
axes = plt.gca()
x_vals = np.array(axes.get_xlim())
y_vals = intercept + slope * x_vals
plt.plot(x_vals, y_vals, '--')
I know this question is a couple years old, but since there is no accepted answer, I'll add what works for me.
You could just plot the values in your graph, and then generate another set of values for the coordinates of the best fit line and plot that over your original graph. For example, see the following code:
import matplotlib.pyplot as plt
import numpy as np
# Some dummy data
x = [1, 2, 3, 4, 5, 6, 7]
y = [1, 3, 3, 2, 5, 7, 9]
# Find the slope and intercept of the best fit line
slope, intercept = np.polyfit(x, y, 1)
# Create a list of values in the best fit line
abline_values = [slope * i + intercept for i in x]
# Plot the best fit line over the actual values
plt.plot(x, y, '--')
plt.plot(x, abline_values, 'b')
plt.title(slope)
plt.show()
As of 2021, in matplotlib 3.3.4, it supports drawing lines with slope value and a point.
fig, ax = plt.subplots()
ax.axline((0, 4), slope=3., color='C0', label='by slope')
ax.set_xlim(0, 1)
ax.set_ylim(3, 5)
ax.legend()
X = np.array([1, 2, 3, 4, 5, 6, 7])
Y = np.array([1.1,1.9,3.0,4.1,5.2,5.8,7])
scatter (X,Y)
slope, intercept = np.polyfit(X, Y, 1)
plot(X, X*slope + intercept, 'r')
It looks like this feature will be part of version 3.3.0:
matplotlib.axes.Axes.axline
You'll be, for example, able to draw a red line through points (0, 0) and (1, 1) using
axline((0, 0), (1, 1), linewidth=4, color='r')
I couldn't figure a way to do it without resorting to callbacks, but this seems to work fairly well.
import numpy as np
from matplotlib import pyplot as plt
class ABLine2D(plt.Line2D):
"""
Draw a line based on its slope and y-intercept. Additional arguments are
passed to the <matplotlib.lines.Line2D> constructor.
"""
def __init__(self, slope, intercept, *args, **kwargs):
# get current axes if user has not specified them
if not 'axes' in kwargs:
kwargs.update({'axes':plt.gca()})
ax = kwargs['axes']
# if unspecified, get the current line color from the axes
if not ('color' in kwargs or 'c' in kwargs):
kwargs.update({'color':ax._get_lines.color_cycle.next()})
# init the line, add it to the axes
super(ABLine2D, self).__init__([], [], *args, **kwargs)
self._slope = slope
self._intercept = intercept
ax.add_line(self)
# cache the renderer, draw the line for the first time
ax.figure.canvas.draw()
self._update_lim(None)
# connect to axis callbacks
self.axes.callbacks.connect('xlim_changed', self._update_lim)
self.axes.callbacks.connect('ylim_changed', self._update_lim)
def _update_lim(self, event):
""" called whenever axis x/y limits change """
x = np.array(self.axes.get_xbound())
y = (self._slope * x) + self._intercept
self.set_data(x, y)
self.axes.draw_artist(self)
I suppose for the case of (intercept, slope) of (0, 1) the following function could be used and extended to accommodate other slopes and intercepts, but won't readjust if axis limits are changed or autoscale is turned back on.
def abline():
gca = plt.gca()
gca.set_autoscale_on(False)
gca.plot(gca.get_xlim(),gca.get_ylim())
import matplotlib.pyplot as plt
plt.scatter(range(10),range(10))
abline()
plt.draw()
I'd like to expand on the answer from David Marx, where we are making sure that the sloped line does not expand over the original plotting area.
Since the x-axis limits are used to calculate the y-data for the sloped line, we need to make sure, that the calculated y-data does not extend the given ymin - ymax range. If it does crop the displayed data.
def abline(slope, intercept,**styles):
"""Plot a line from slope and intercept"""
axes = plt.gca()
xmin,xmax = np.array(axes.get_xlim())
ymin,ymax = np.array(axes.get_ylim()) # get also y limits
x_vals = np.linspace(xmin,xmax,num=1000) #increased sampling (only actually needed for large slopes)
y_vals = intercept + slope * x_vals
locpos = np.where(y_vals<ymax)[0] # if data extends above ymax
locneg = np.where(y_vals>ymin)[0] # if data extends below ymin
# select most restricitive condition
if len(locpos) >= len(locneg):
loc = locneg
else:
loc = locpos
plt.plot(x_vals[loc], y_vals[loc], '--',**styles)
return y_vals
Here's a possible workaround I came up with: suppose I have my intercept coordinates stored as x_intercept and y_intercept, and the slope (m) saved as my_slope which was found through the renowned equation m = (y2-y1)/(x2-x1), or in whichever way you managed to find it.
Using the other famous general equation for a line y = mx + q, I define the function find_second_point that first computes the q (since m, x and y are known) and then computes another random point that belongs to that line.
Once I have the two points (the initial x_intercept,y_intercept and the newly found new_x,new_y), I simply plot the segment through those two points. Here's the code:
import numpy as np
import matplotlib.pyplot as plt
x_intercept = 3 # invented x coordinate
y_intercept = 2 # invented y coordinate
my_slope = 1 # invented slope value
def find_second_point(slope,x0,y0):
# this function returns a point which belongs to the line that has the slope
# inserted by the user and that intercepts the point (x0,y0) inserted by the user
q = y0 - (slope*x0) # calculate q
new_x = x0 + 10 # generate random x adding 10 to the intersect x coordinate
new_y = (slope*new_x) + q # calculate new y corresponding to random new_x created
return new_x, new_y # return x and y of new point that belongs to the line
# invoke function to calculate the new point
new_x, new_y = find_second_point(my_slope , x_intercept, y_intercept)
plt.figure(1) # create new figure
plt.plot((x_intercept, new_x),(y_intercept, new_y), c='r', label='Segment')
plt.scatter(x_intercept, y_intercept, c='b', linewidths=3, label='Intercept')
plt.scatter(new_x, new_y, c='g', linewidths=3, label='New Point')
plt.legend() # add legend to image
plt.show()
here is the image generated by the code:
Short answer inspired by kite.com:
plt.plot(x, s*x + i)
Reproducible code:
import numpy as np
import matplotlib.pyplot as plt
i=3 # intercept
s=2 # slope
x=np.linspace(1,10,50) # from 1 to 10, by 50
plt.plot(x, s*x + i) # abline
plt.show()
One can simply create a list with the line's equation obtained from a particular intercept and slope. Put those values in a list and plot it against any set of numbers you would like. For example- (Lr being the Linear regression model)
te= []
for i in range(11):
te.append(Lr.intercept_ + Lr.coef_*i)
plt.plot(te, '--')
Gets the job done.
You can write a simple function by converting Slope-Intercept form to 2-Point Form.
def mxline(slope, intercept, start, end):
y1 = slope*start + intercept
y2 = slope*end + intercept
plt.plot([start, end], [y1, y2])
Calling the function
mxline(m,c, 0, 20)
OUTPUT

Categories

Resources