Plot 3D Cube and Draw Line on 3D in Python - python
I know, for those who know Python well piece of cake a question.
I have an excel file and it looks like this:
1 7 5 8 2 4 6 3
1 7 4 6 8 2 5 3
6 1 5 2 8 3 7 4
My purpose is to draw a cube in Python and draw a line according to the order of these numbers.
Note: There is no number greater than 8 in arrays.
I can explain better with a pictures.
First Step:
Second Step
Last Step:
I need to print the final version of the 3D cube for each row in Excel.
My way to solution
import numpy as np
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection, Line3DCollection
import matplotlib.pyplot as plt
df = pd.read_csv("uniquesolutions.csv",header=None,sep='\t')
myArray = df.values
points = solutionsarray
def connectpoints(x,y,p1,p2):
x1, x2 = x[p1], x[p2]
y1, y2 = y[p1], y[p2]
plt.plot([x1,x2],[y1,y2],'k-')
# cube[0][0][0] = 1
# cube[0][0][1] = 2
# cube[0][1][0] = 3
# cube[0][1][1] = 4
# cube[1][0][0] = 5
# cube[1][0][1] = 6
# cube[1][1][0] = 7
# cube[1][1][1] = 8
for i in range():
connectpoints(cube[i][i][i],cube[],points[i],points[i+1]) # Confused!
ax = fig.add_subplot(111, projection='3d')
# plot sides
ax.add_collection3d(Poly3DCollection(verts,
facecolors='cyan', linewidths=1, edgecolors='r', alpha=.25))
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
In the question here, they managed to draw something with the points given inside the cube.
I tried to use this 2D connection function.
Last Question: Can I print the result of red lines in 3D? How can I do this in Python?
First, it looks like you are using pandas with pd.read_csv without importing it. Since, you are not reading the headers and just want a list of values, it is probably sufficient to just use the numpy read function instead.
Since I don't have access to your csv, I will define the vertex lists as variables below.
vertices = np.zeros([3,8],dtype=int)
vertices[0,:] = [1, 7, 5, 8, 2, 4, 6, 3]
vertices[1,:] = [1, 7, 4, 6, 8, 2, 5, 3]
vertices[2,:] = [6, 1, 5, 2, 8, 3, 7, 4]
vertices = vertices - 1 #(adjust the vertex numbers by one since python starts with zero indexing)
Here I used a 2d numpy array to define the vertices. The first dimension, with length 3, is for the number of vertex list, and the second dimension, with length 8, is each vertex list.
I subtract 1 from the vertices list because we will use this list to index another array and python indexing starts at 0, not 1.
Then, define the cube coordaintes.
# Initialize an array with dimensions 8 by 3
# 8 for each vertex
# -> indices will be vertex1=0, v2=1, v3=2 ...
# 3 for each coordinate
# -> indices will be x=0,y=1,z=1
cube = np.zeros([8,3])
# Define x values
cube[:,0] = [0, 0, 0, 0, 1, 1, 1, 1]
# Define y values
cube[:,1] = [0, 1, 0, 1, 0, 1, 0, 1]
# Define z values
cube[:,2] = [0, 0, 1, 1, 0, 0, 1, 1]
Then initialize the plot.
# First initialize the fig variable to a figure
fig = plt.figure()
# Add a 3d axis to the figure
ax = fig.add_subplot(111, projection='3d')
Then add the red lines for vertex list 1. You can repeat this for the other vertex list by increasing the first index of vertices.
# Plot first vertex list
ax.plot(cube[vertices[0,:],0],cube[vertices[0,:],1],cube[vertices[0,:],2],color='r-')
# Plot second vertex list
ax.plot(cube[vertices[1,:],0],cube[vertices[1,:],1],cube[vertices[1,:],2],color='r-')
The faces can be added by defining the edges of each faces. There is a numpy array for each face. In the array there are 5 vertices, where the edge are defined by the lines between successive vertices. So the 5 vertices create 4 edges.
# Initialize a list of vertex coordinates for each face
# faces = [np.zeros([5,3])]*3
faces = []
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
# Bottom face
faces[0][:,0] = [0,0,1,1,0]
faces[0][:,1] = [0,1,1,0,0]
faces[0][:,2] = [0,0,0,0,0]
# Top face
faces[1][:,0] = [0,0,1,1,0]
faces[1][:,1] = [0,1,1,0,0]
faces[1][:,2] = [1,1,1,1,1]
# Left Face
faces[2][:,0] = [0,0,0,0,0]
faces[2][:,1] = [0,1,1,0,0]
faces[2][:,2] = [0,0,1,1,0]
# Left Face
faces[3][:,0] = [1,1,1,1,1]
faces[3][:,1] = [0,1,1,0,0]
faces[3][:,2] = [0,0,1,1,0]
# front face
faces[4][:,0] = [0,1,1,0,0]
faces[4][:,1] = [0,0,0,0,0]
faces[4][:,2] = [0,0,1,1,0]
# front face
faces[5][:,0] = [0,1,1,0,0]
faces[5][:,1] = [1,1,1,1,1]
faces[5][:,2] = [0,0,1,1,0]
ax.add_collection3d(Poly3DCollection(faces, facecolors='cyan', linewidths=1, edgecolors='k', alpha=.25))
All together it looks like this.
import numpy as np
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import matplotlib.pyplot as plt
vertices = np.zeros([3,8],dtype=int)
vertices[0,:] = [1, 7, 5, 8, 2, 4, 6, 3]
vertices[1,:] = [1, 7, 4, 6, 8, 2, 5, 3]
vertices[2,:] = [6, 1, 5, 2, 8, 3, 7, 4]
vertices = vertices - 1 #(adjust the indices by one since python starts with zero indexing)
# Define an array with dimensions 8 by 3
# 8 for each vertex
# -> indices will be vertex1=0, v2=1, v3=2 ...
# 3 for each coordinate
# -> indices will be x=0,y=1,z=1
cube = np.zeros([8,3])
# Define x values
cube[:,0] = [0, 0, 0, 0, 1, 1, 1, 1]
# Define y values
cube[:,1] = [0, 1, 0, 1, 0, 1, 0, 1]
# Define z values
cube[:,2] = [0, 0, 1, 1, 0, 0, 1, 1]
# First initialize the fig variable to a figure
fig = plt.figure()
# Add a 3d axis to the figure
ax = fig.add_subplot(111, projection='3d')
# plotting cube
# Initialize a list of vertex coordinates for each face
# faces = [np.zeros([5,3])]*3
faces = []
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
faces.append(np.zeros([5,3]))
# Bottom face
faces[0][:,0] = [0,0,1,1,0]
faces[0][:,1] = [0,1,1,0,0]
faces[0][:,2] = [0,0,0,0,0]
# Top face
faces[1][:,0] = [0,0,1,1,0]
faces[1][:,1] = [0,1,1,0,0]
faces[1][:,2] = [1,1,1,1,1]
# Left Face
faces[2][:,0] = [0,0,0,0,0]
faces[2][:,1] = [0,1,1,0,0]
faces[2][:,2] = [0,0,1,1,0]
# Left Face
faces[3][:,0] = [1,1,1,1,1]
faces[3][:,1] = [0,1,1,0,0]
faces[3][:,2] = [0,0,1,1,0]
# front face
faces[4][:,0] = [0,1,1,0,0]
faces[4][:,1] = [0,0,0,0,0]
faces[4][:,2] = [0,0,1,1,0]
# front face
faces[5][:,0] = [0,1,1,0,0]
faces[5][:,1] = [1,1,1,1,1]
faces[5][:,2] = [0,0,1,1,0]
ax.add_collection3d(Poly3DCollection(faces, facecolors='cyan', linewidths=1, edgecolors='k', alpha=.25))
# plotting lines
ax.plot(cube[vertices[0,:],0],cube[vertices[0,:],1],cube[vertices[0,:],2],color='r')
ax.plot(cube[vertices[1,:],0],cube[vertices[1,:],1],cube[vertices[1,:],2],color='r')
ax.plot(cube[vertices[2,:],0],cube[vertices[2,:],1],cube[vertices[2,:],2],color='r')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
Alternatively, If you want each set of lines to have their own color, replace
ax.plot(cube[vertices[0,:],0],cube[vertices[0,:],1],cube[vertices[0,:],2],color='r')
ax.plot(cube[vertices[1,:],0],cube[vertices[1,:],1],cube[vertices[1,:],2],color='r')
ax.plot(cube[vertices[2,:],0],cube[vertices[2,:],1],cube[vertices[2,:],2],color='r')
with
colors = ['r','g','b']
for i in range(3):
ax.plot(cube[vertices[i,:],0],cube[vertices[i,:],1],cube[vertices[i,:],2],color=colors[i])
Related
How to get equally spaced grid points in an irregularly shaped figure?
I have an irregularly shaped image and I want to get equally spaced grid points inside that. The image that I have for example is Image I have I am thinking of using OpenCV to get the corner coordinates and that is easy. But I do not know how to pass all the corner coordinates or divide my shape in identifiable geometric shapes and do this. Right now, I have hard coded the coordinates and created a function to pass the coordinates. import numpy as np import matplotlib.pyplot as plt import functools def gridFunc(arr): center = np.mean(arr, axis=0) x = np.arange(min(arr[:, 0]), max(arr[:, 0]) + 0.04, 0.4) y = np.arange(min(arr[:, 1]), max(arr[:, 1]) + 0.04, 0.4) a, b = np.meshgrid(x, y) points = np.stack([a.reshape(-1), b.reshape(-1)]).T def normal(a, b): v = b - a n = np.array([v[1], -v[0]]) # normal needs to point out if (center - a) # n > 0: n *= -1 return n mask = functools.reduce(np.logical_and, [((points - a) # normal(a, b)) < 0 for a, b in zip(arr[:-1], arr[1:])]) #plt.plot(arr[:, 0], arr[:, 1]) #plt.gca().set_aspect('equal') #plt.scatter(points[mask][:, 0], points[mask][:, 1]) #plt.show() return points[mask] arr1 = np.array([[0, 7],[3, 10],[3, 4],[0, 7]]) arr2 = np.array([[3, 0], [3, 14], [12, 14], [12, 0], [3,0]]) arr3 = np.array([[12, 4], [12, 10], [20, 10], [20, 4], [12, 4]]) arr_1 = gridFunc(arr1) arr_2 = gridFunc(arr2) arr_3 = gridFunc(arr3) res = np.append(arr_1, arr_2) res = np.reshape(res, (-1, 2)) res = np.append(res, arr_3) res = np.reshape(res, (-1, 2)) plt.scatter(res[:,0], res[:,1]) plt.show() The image that I get is this, But I am doing this manually And I want to extend this to other shapes as well. Image I get
Plotly: How to set node positions in a Sankey Diagram?
The sample data is as follows: unique_list = ['home0', 'page_a0', 'page_b0', 'page_a1', 'page_b1', 'page_c1', 'page_b2', 'page_a2', 'page_c2', 'page_c3'] sources = [0, 0, 1, 2, 2, 3, 3, 4, 4, 7, 6] targets = [3, 4, 4, 3, 5, 6, 8, 7, 8, 9, 9] values = [2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2] Using the sample code from the documentation fig = go.Figure(data=[go.Sankey( node = dict( pad = 15, thickness = 20, line = dict(color = "black", width = 0.5), label = unique_list, color = "blue" ), link = dict( source = sources, target = targets, value = values ))]) fig.show() This outputs the following sankey diagram However, I would like to get all the values which end in the same number in the same vertical column, just like how the leftmost column has all of it's nodes ending with a 0. I see in the docs that it is possible to move the node positions, however I was wondering if there was a cleaner way to do it other than manually inputting x and y values. Any help appreciated.
In go.Sankey() set arrangement='snap' and adjust x and y positions in x=<list> and y=<list>. The following setup will place your nodes as requested. Plot: Please note that the y-values are not explicitly set in this example. As soon as there are more than one node for a common x-value, the y-values will be adjusted automatically for all nodes to be displayed in the same vertical position. If you do want to set all positions explicitly, just set arrangement='fixed' Edit: I've added a custom function nodify() that assigns identical x-positions to label names that have a common ending such as '0' in ['home0', 'page_a0', 'page_b0']. Now, if you as an example change page_c1 to page_c2 you'll get this: Complete code: import plotly.graph_objects as go unique_list = ['home0', 'page_a0', 'page_b0', 'page_a1', 'page_b1', 'page_c1', 'page_b2', 'page_a2', 'page_c2', 'page_c3'] sources = [0, 0, 1, 2, 2, 3, 3, 4, 4, 7, 6] targets = [3, 4, 4, 3, 5, 6, 8, 7, 8, 9, 9] values = [2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2] def nodify(node_names): node_names = unique_list # uniqe name endings ends = sorted(list(set([e[-1] for e in node_names]))) # intervals steps = 1/len(ends) # x-values for each unique name ending # for input as node position nodes_x = {} xVal = 0 for e in ends: nodes_x[str(e)] = xVal xVal += steps # x and y values in list form x_values = [nodes_x[n[-1]] for n in node_names] y_values = [0.1]*len(x_values) return x_values, y_values nodified = nodify(node_names=unique_list) # plotly setup fig = go.Figure(data=[go.Sankey( arrangement='snap', node = dict( pad = 15, thickness = 20, line = dict(color = "black", width = 0.5), label = unique_list, color = "blue", x=nodified[0], y=nodified[1] ), link = dict( source = sources, target = targets, value = values ))]) fig.show()
Converting points back to 3D
I have points in a 3D plane that I have converted to a 2D projection using the following method: import numpy as np # Calculate axes for 2D projection # Create random vector to cross rv = np.add(self.plane.normal, [-1.0, 0.0, 1.0]) rv = np.divide(rv, np.linalg.norm(rv)) horizontal = np.cross(self.plane.normal, rv) vertical = np.cross(self.plane.normal, horizontal) diff2 = np.zeros((len(point23D), 3), dtype=np.float32) diff2[:, 0] = np.subtract(point23D[:, 0], self.plane.origin[0]) diff2[:, 1] = np.subtract(point23D[:, 1], self.plane.origin[1]) diff2[:, 2] = np.subtract(point23D[:, 2], self.plane.origin[2]) x2 = np.add(np.add(np.multiply(diff2[:, 0], horizontal[0]), np.multiply(diff2[:, 1], horizontal[1])), np.multiply(diff2[:, 2], horizontal[2])) y2 = np.add(np.add(np.multiply(diff2[:, 0], vertical[0]), np.multiply(diff2[:, 1], vertical[1])), np.multiply(diff2[:, 2], vertical[2])) twodpoints2 = np.zeros((len(point23D), 3), dtype=np.float32) twodpoints2[:, 0] = x2 twodpoints2[:, 1] = y2 I then do some calculations on these points in 2D space. After that I need to get the points back in 3D space on the same relative position. I have written the following code for that: # Transform back to 3D rotation_matrix = np.array([[horizontal[0], vertical[0], -self.plane.normal[0]], [horizontal[1], vertical[1], -self.plane.normal[1]], [horizontal[2], vertical[2], -self.plane.normal[2]]]) transformed_vertices = np.matmul(twodpoints, rotation_matrix) transformed_vertices = np.add(transformed_vertices, self.plane.origin) But this doesn't seem to do the projection correctly, the points projected back in 3D do not lie on the original 3D plane at all. Does anyone know why this is wrong or does anyone have a suggestion that would work better? In this example I just projected the same points back into 3D to see if it works correctly, which it doesn't. In reality I'll have different points that need to be projected back, but they still need to be in the same plane in 3D space.
# You have a plane perpendicular to a vector # N = np.array([x_N, y_N, z_N]) # and passing through a point # Q = np.array([x_Q, y_Q, z_Q]) U = np.zeros((3,3)) U[2,:] = N / np.linalg.norm(N) e = np.array([0,0,0]) e[np.argmin(np.abs(U[0,:]))] = 1 U[0, :] = np.cross(e, U[2,:]) U[0, :] = U[0, :] / np.linalg.norm(U[0, :]) U[1, :] = np.cross(U[2, :], U[0, :]) point2D = (point23D - Q).dot(U) result_point2D = some_calcs(point2D) result_point23D = res_point2D.dot(U.transpose())
Matplotlib render all internal voxels (with alpha)
I want to render a volume in matplotlib. The volume is a simple 7x7x7 cube, and I want to be able to see all internal voxels (even though I know it will look like a mess). I've been able to render voxels with transparency, but any voxel not on the surface seems to never be drawn. Each 7x7 slice of the volume should look like this: I've thrown together a MWE The following code creates a 5x5x5 volume with a red,green,blue,yellow, and cyan 5x5 layers. The alpha of each layer is set to .5, so the whole thing should be see-through. Then I chang the colors of all non-surface voxels to black with alpha 1, so if they were showing we should be able to see a black box in the center. Rendering it by itself produces the figure on the left, but if we remove the fill from the cyan layer, we can see that the black box does indeed exist, it is just not being shown because it is 100% occluded even though those occluding voxels have alpha less than 1. import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # NOQA spatial_axes = [5, 5, 5] filled = np.ones(spatial_axes, dtype=np.bool) colors = np.empty(spatial_axes + [4], dtype=np.float32) alpha = .5 colors[0] = [1, 0, 0, alpha] colors[1] = [0, 1, 0, alpha] colors[2] = [0, 0, 1, alpha] colors[3] = [1, 1, 0, alpha] colors[4] = [0, 1, 1, alpha] # set all internal colors to black with alpha=1 colors[1:-1, 1:-1, 1:-1, 0:3] = 0 colors[1:-1, 1:-1, 1:-1, 3] = 1 fig = plt.figure() ax = fig.add_subplot('111', projection='3d') ax.voxels(filled, facecolors=colors, edgecolors='k') fig = plt.figure() ax = fig.add_subplot('111', projection='3d') filled[-1] = False ax.voxels(filled, facecolors=colors, edgecolors='k') Is there any way to render all occluded voxels?
To turn my comments above into an answer: You may always just plot all voxels as in Representing voxels with matplotlib 3D discrete heatmap in matplotlib The official example solves this problem by offsettingt the faces of the voxels by a bit, such they are all drawn. This matplotlib issue discusses the missing faces on internal cubes. There is a pull request which has some issues still and it hence not merged yet. Despite the small issues, you may monkey patch the current status of the pull request into your code: import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D, art3d # NOQA from matplotlib.cbook import _backports from collections import defaultdict import types def voxels(self, *args, **kwargs): if len(args) >= 3: # underscores indicate position only def voxels(__x, __y, __z, filled, **kwargs): return (__x, __y, __z), filled, kwargs else: def voxels(filled, **kwargs): return None, filled, kwargs xyz, filled, kwargs = voxels(*args, **kwargs) # check dimensions if filled.ndim != 3: raise ValueError("Argument filled must be 3-dimensional") size = np.array(filled.shape, dtype=np.intp) # check xyz coordinates, which are one larger than the filled shape coord_shape = tuple(size + 1) if xyz is None: x, y, z = np.indices(coord_shape) else: x, y, z = (_backports.broadcast_to(c, coord_shape) for c in xyz) def _broadcast_color_arg(color, name): if np.ndim(color) in (0, 1): # single color, like "red" or [1, 0, 0] return _backports.broadcast_to( color, filled.shape + np.shape(color)) elif np.ndim(color) in (3, 4): # 3D array of strings, or 4D array with last axis rgb if np.shape(color)[:3] != filled.shape: raise ValueError( "When multidimensional, {} must match the shape of " "filled".format(name)) return color else: raise ValueError("Invalid {} argument".format(name)) # intercept the facecolors, handling defaults and broacasting facecolors = kwargs.pop('facecolors', None) if facecolors is None: facecolors = self._get_patches_for_fill.get_next_color() facecolors = _broadcast_color_arg(facecolors, 'facecolors') # broadcast but no default on edgecolors edgecolors = kwargs.pop('edgecolors', None) edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') # include possibly occluded internal faces or not internal_faces = kwargs.pop('internal_faces', False) # always scale to the full array, even if the data is only in the center self.auto_scale_xyz(x, y, z) # points lying on corners of a square square = np.array([ [0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0] ], dtype=np.intp) voxel_faces = defaultdict(list) def permutation_matrices(n): """ Generator of cyclic permutation matices """ mat = np.eye(n, dtype=np.intp) for i in range(n): yield mat mat = np.roll(mat, 1, axis=0) for permute in permutation_matrices(3): pc, qc, rc = permute.T.dot(size) pinds = np.arange(pc) qinds = np.arange(qc) rinds = np.arange(rc) square_rot = square.dot(permute.T) for p in pinds: for q in qinds: p0 = permute.dot([p, q, 0]) i0 = tuple(p0) if filled[i0]: voxel_faces[i0].append(p0 + square_rot) # draw middle faces for r1, r2 in zip(rinds[:-1], rinds[1:]): p1 = permute.dot([p, q, r1]) p2 = permute.dot([p, q, r2]) i1 = tuple(p1) i2 = tuple(p2) if filled[i1] and (internal_faces or not filled[i2]): voxel_faces[i1].append(p2 + square_rot) elif (internal_faces or not filled[i1]) and filled[i2]: voxel_faces[i2].append(p2 + square_rot) # draw upper faces pk = permute.dot([p, q, rc-1]) pk2 = permute.dot([p, q, rc]) ik = tuple(pk) if filled[ik]: voxel_faces[ik].append(pk2 + square_rot) # iterate over the faces, and generate a Poly3DCollection for each voxel polygons = {} for coord, faces_inds in voxel_faces.items(): # convert indices into 3D positions if xyz is None: faces = faces_inds else: faces = [] for face_inds in faces_inds: ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2] face = np.empty(face_inds.shape) face[:, 0] = x[ind] face[:, 1] = y[ind] face[:, 2] = z[ind] faces.append(face) poly = art3d.Poly3DCollection(faces, facecolors=facecolors[coord], edgecolors=edgecolors[coord], **kwargs ) self.add_collection3d(poly) polygons[coord] = poly return polygons spatial_axes = [5, 5, 5] filled = np.ones(spatial_axes, dtype=np.bool) colors = np.empty(spatial_axes + [4], dtype=np.float32) alpha = .5 colors[0] = [1, 0, 0, alpha] colors[1] = [0, 1, 0, alpha] colors[2] = [0, 0, 1, alpha] colors[3] = [1, 1, 0, alpha] colors[4] = [0, 1, 1, alpha] # set all internal colors to black with alpha=1 colors[1:-1, 1:-1, 1:-1, 0:3] = 0 colors[1:-1, 1:-1, 1:-1, 3] = 1 fig = plt.figure() ax = fig.add_subplot('111', projection='3d') ax.voxels = types.MethodType(voxels, ax) ax.voxels(filled, facecolors=colors, edgecolors='k',internal_faces=True) fig = plt.figure() ax = fig.add_subplot('111', projection='3d') ax.voxels = types.MethodType(voxels, ax) filled[-1] = False ax.voxels(filled, facecolors=colors, edgecolors='k',internal_faces=True) plt.show()
Matplotlib: Hysteresis loop using Mirrored or Split x axis
In an experiment, a load cell advances in equal increments of distance with time, compresses a sample; stops when a specified distance from the start point is reached; then retracts in equal increments of distance with time back to the starting position. A plot of pressure (load cell reading) on the y axis against pressure on the x axis produces a familiar hysteresis loop. A plot of pressure (load cell reading) on the y axis against time on the x axis produces an assymetric peak with the maximum pressure in the centre, corresponding to the maximum advancement point of the sensor. Instead of the above, I'd like to plot pressure on the y axis against distance on the x axis, with the additional constraint that the x axis is labelled starting at 0, with maximum pressure at the middle of the x axis, and 0 again at the right hand end of the x-axis. In other words, the curve will be identical in shape to the plot of pressure v time, but will be of pressure v distance, where the left half of the plot indicates the distance of the probe from its starting position during advancement; and the right half of the plot indicates distance of the probe from its starting position during retraction. My actual datasets contain thousands of rows of data but by way of illustration, a minimal dummy dataset would look something like the following, where the 3 columns correspond to Time, Distance of probe from origin, and Pressure measured by probe respectively: [ [0,0,0], [1,2,10], [2,4,30], [3,6,60], [4,4,35], [5,2,15], [6,0,0] ] I can't work out how to get MatPlotlib to construct the x-axis so that the range goes from 0 to a maximum, then back to 0 again. I'd be grateful for advice on how to achieve this plot in the most simple and elegant way. Many thanks.
As you have time, you can use it for the x axis values and just change the x tick labels: import numpy as np import matplotlib.pyplot as plt # Time, Distance, Pressure data = [[0, 0, 0], [1, 2, 10], [2, 4, 30], [3, 6, 60], [4, 4, 35], [5, 2, 15], [6, 0, 0]] # convert to array to allow indexing like [i, j] data = np.array(data) fig = plt.figure() ax = fig.add_subplot(111) max_ticks = 10 skip = (data.shape[0] / max_ticks) + 1 ax.plot(data[:, 0], data[:, 2]) # Pressure(time) ax.set_xticks(data[::skip, 0]) ax.set_xticklabels(data[::skip, 1]) # Pressure(Distance(time)) ? ax.set_ylabel('Pressure [Pa?]') ax.set_xlabel('Distance [m?]') fig.show() The skip is just so you don't end up with too many ticks on the plot, change as you like. As said in comment, the above only holds for uniforme changes in distance as a function of time. For non uniform changes, you'll have to use something like: data = [[0, 0, 0], [1, 2, 10], [2, 4, 30], [3, 6, 60], [3.5, 5.4, 40], [4, 4, 35], [5, 2, 15], [6, 0, 0]] # convert to array to allow indexing like [i, j] data = np.array(data) def find_max_pos(data, column=0): return np.argmax(data[:, column]) def reverse_unload(data, unload_start): # prepare new_data with new column: new_shape = np.array(data.shape) new_shape[1] += 1 new_data = np.empty(new_shape) # copy all correct data new_data[:, 0] = data[:, 0] new_data[:, 1] = data[:, 1] new_data[:, 2] = data[:, 2] new_data[:unload_start+1, 3] = data[:unload_start+1, 1] # use gradient to fill the rest gradient = -np.gradient(data[:, 1]) for i in range(unload_start + 1, data.shape[0]): new_data[i, 3] = new_data[i-1, 3] + gradient[i] return new_data data = reverse_unload(data, find_max_pos(data, 1)) fig = plt.figure() ax = fig.add_subplot(111) max_ticks = 10 skip = (data.shape[0] / max_ticks) + 1 ax.plot(data[:, 3], data[:, 2]) # Pressure("Distance") ax.set_xticks(data[::skip, 3]) ax.set_xticklabels(data[::skip, 1]) ax.grid() # added for clarity ax.set_ylabel('Pressure [Pa?]') ax.set_xlabel('Distance [m?]') fig.show() Regarding the fact that using the measured values as the ticks results in these not being round nice numbers, I found it was just easier to map the automatic ticks from matplotlib to the correct values: import numpy as np import matplotlib.pyplot as plt data = [[0, 0, 0], [1, 2, 10], [2, 4, 30], [3, 6, 60], [3.5, 5.4, 40], [4, 4, 35], [5, 2, 15], [6, 0, 0]] # convert to array to allow indexing like [i, j] data = np.array(data) def find_max_pos(data, column=0): return np.argmax(data[:, column]) def reverse_unload(data): unload_start = find_max_pos(data, 1) # prepare new_data with new column: new_shape = np.array(data.shape) new_shape[1] += 1 new_data = np.empty(new_shape) # copy all correct data new_data[:, 0] = data[:, 0] new_data[:, 1] = data[:, 1] new_data[:, 2] = data[:, 2] new_data[:unload_start+1, 3] = data[:unload_start+1, 1] # use gradient to fill the rest gradient = data[unload_start:-1, 1]-data[unload_start+1:, 1] for i, j in enumerate(range(unload_start + 1, data.shape[0])): new_data[j, 3] = new_data[j-1, 3] + gradient[i] return new_data def create_map_function(data): """ Return function that maps values of distance folded over the maximum pressure applied. """ max_index = find_max_pos(data, 1) x0, y0 = data[max_index, 1], data[max_index, 1] x1, y1 = 2*data[max_index, 1], 0 m = (y1 - y0) / (x1 - x0) b = y0 - m*x0 def map_function(x): if x < x0: return x else: return m*x+b return map_function def process_data(data): data = reverse_unload(data) map_function = create_map_function(data) fig, ax = plt.subplots() ax.plot(data[:, 3], data[:, 2]) ax.set_xticklabels([map_function(x) for x in ax.get_xticks()]) ax.grid() ax.set_ylabel('Pressure [Pa?]') ax.set_xlabel('Distance [m?]') fig.show() if __name__ == '__main__': process_data(data)
Update: Have found a workaround to the problem of rounding ticks to the nearest integer by using the np.around function which rounds decimals to the nearest even value, to a specified number of decimal places (default = 0): e.g. 1.5 and 2.5 round to 2.0, -0.5 and 0.5 round to 0.0, etc. More info here: https://docs.scipy.org/doc/numpy1.10.4/reference/generated/numpy.around.html So berna1111's code becomes: import numpy as np import matplotlib.pyplot as plt # Time, Distance, Pressure data = [[0, 0, 0], [1, 1.9, 10], # Dummy data including decimals to demonstrate rounding [2, 4.1, 30], [3, 6.1, 60], [4, 3.9, 35], [5, 1.9, 15], [6, -0.2, 0]] # convert to array to allow indexing like [i, j] data = np.array(data) fig = plt.figure() ax = fig.add_subplot(111) max_ticks = 10 skip = (data.shape[0] / max_ticks) + 1 ax.plot(data[:, 0], data[:, 2]) # Pressure(time) ax.set_xticks(data[::skip, 0]) ax.set_xticklabels(np.absolute(np.around((data[::skip, 1])))) # Pressure(Distance(time)); rounded to nearest integer ax.set_ylabel('Pressure [Pa?]') ax.set_xlabel('Distance [m?]') fig.show() According to the numpy documentation, np.around should round the final value of -0.2 for Distance to '0.0'; however it seems to round to '-0.0' instead. Not sure why this occurs, but since all my xticklabels in this particular case need to be positive integers or zero, I can correct this behaviour by using the np.absolute function as shown above. Everything now seems to work OK for my requirements, but if I'm missing something, or there's a better solution, please let me know.