I was looking for a way to build cubes of the same size, then draw a line through this space and output the result in the form of coordinates of cubes that this line intersects and paint these cubes with a different color. The line can be either straight or curved.
I used matplotlib to plot cubes and lines. From these sources:
https://www.geeksforgeeks.org/how-to-draw-3d-cube-using-matplotlib-in-python/
Representing voxels with matplotlib
Example code:
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
axes = [5, 5, 5]
# Create Data
data = np.ones(axes, dtype=np.bool)
# Controll Tranperency
alpha = 0.3
# Control colour
colors = np.empty(axes + [4], dtype=np.float32)
colors[0] = [1, 0, 0, alpha] # red
colors[1] = [0, 1, 0, alpha] # green
colors[2] = [0, 0, 1, alpha] # blue
colors[3] = [1, 1, 0, alpha] # yellow
colors[4] = [1, 1, 1, alpha] # grey
# Plot figure
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
x1 = [1, 4]
y1 = [0, 5]
z1 = [0, 5]
ax.plot3D(x1, y1, z1, 'black', linewidth = 5)
# Voxels is used to customizations of
# the sizes, positions and colors.
ax.voxels(data, facecolors=colors, edgecolors='grey')
result
In short: I need to plot a grid of cubes and draw a line through it. After determine which cubes this line intersects.
Is it possible to do this in Matplotlib or do I need to use another library to solve my question?
God, why do I put myself though this.
Anyways, here is an iterative solution because I do not feel like doing linear algebra. I tried and I failed.
# Here be dragons
def linelamb(x,y,z):
return lambda s: [int(i) for i in [x[0]+s*(x[1]-x[0]), y[0]+s*(y[1]-y[0]), z[0]+s*(z[1]-z[0])]]
line = linelamb(x1,y1,z1)
hitboxes = np.zeros(axes)
x,y,z = 0,0,0
for r in [i for i in np.arange(0,1,0.001)]:
xnew,ynew,znew = line(r)
if not (x == xnew and y == ynew and z == znew):
hitboxes[xnew,ynew,znew] = 1
x,y,z = xnew,ynew,znew
ax.voxels(hitboxes, facecolors=[0,0,0,0.5], edgecolors='black');
I spent some extra time to make this more adaptable but my brain stopped working. You might want to adaptively change the step size of the range but good luck.
Related
I have been trying to make a Rubix Cube in python using Matplotlib. I've struggled to get each face of the cube to have its own colour? How do I do this in Matplotlib?
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
axes = [3, 3, 3]
data = np.ones(axes)
alpha = 0.9
colours = np.empty(axes + [4])
colours[0] = [1, 0, 0, alpha] # red
colours[1] = [0, 0, 1, alpha] # blue
colours[2] = [1, 1, 0, alpha] # yellow
figure = plt.figure()
ax = figure.add_subplot(111, projection = '3d')
ax.voxels(data, facecolors = colours, edgecolors = 'grey')
plt.show()
I would like to draw a parallelepiped in Python using matplotlib centred in (0,0,0), with the top face of a different color (or each face of a different parametrized color), and with these dimensions:
L = 1
l = 0.7
s = 0.4
This is the code I developed to draw a cube with the same face color.
import matplotlib.pyplot as plt
import numpy as np
# Create axis
axes = [5, 5, 5]
# Create Data
data = np.ones(axes, dtype = np.bool)
# Control Tranperency
alpha = 0.9
# Control colour
colors = np.empty(axes + [4], dtype = np.float32)
colors[:] = [1, 0, 0, alpha] # red
# Plot figure
fig2 = plt.figure()
ax = fig2.add_subplot(111, projection='3d')
ax.voxels(data, facecolors=colors)
Any suggestion to modify it? Considering that I would like to rotate it with a rotation matrix/quaternion operator in a second time, it would be useful to define the coordinates of the vertices or of some ley points of the parallelepiped.
thank you all!
SOLVED (see below)
On 2D matpotlib scatter plot I can turn on and off points by accessing _offsets property of scatter plot object and setting it's .mask attribute True/False for indexes of those points we want to show/hide like this:
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import random
def TogglePoints(event, plot):
plot._offsets.mask = [ [1, 1], [1, 1], [1, 1], [0, 0], [0, 0], [0, 0] ]
plot.figure.canvas.draw()
x = [random.randint(-10, 10) for n in range(6)]
y = [random.randint(-10, 10) for n in range(6)]
ax = plt.axes()
sc = ax.scatter(x, y, marker='o', s=20, alpha=1)
ax_button = plt.axes([0.81, 0.01, 0.1, 0.05])
button= Button(ax_button, "Toggle")
button.on_clicked(lambda event: TogglePoints(event, sc))
plt.show()
When you click the "Toggle" button on the figure, points with indexes 0, 1, 2 will disappear. You can make them re-appear by setting _offsets.mask back to False and re-drawing plot.
This is what I want to achieve with matplotlib 3D scatter plot.
Using _offsets.mask = [ [1, 1], [1, 1], [1, 1], [0, 0], [0, 0], [0, 0] ] on 3D scatter plot doesn't seem to work.
Actually it alters type of underlying array from MaskedArray to numpy.ndarray for some reason (see: Numpy MaskedArray in matplotlib 3D scatter plot, turns into ndarray when called by PyQt5 button click).
I know that 3D scatter plots have _offsets3d property. However I don't know how I can use it to show/hide points on the plot. Or maybe there's some other way ?
Does anyone know how I can do that ?
Thanks to this post:
Get working alpha value of scatter points in mpl_toolkits.basemap.Basemap
I've found a workaround that serves my purpose.
It concerns setting alpha values of points with set_facecolors().
So the working code now looks like this:
...
import pandas as pd #added
def TogglePointsOFF(event, plot):
for n in range(3): # n = index of point
fc_colors[n, 3] = 0 # 4th value is alpha
plot.set_facecolors(fc_colors)
plot.figure.canvas.draw()
def TogglePointsON(event, plot):
for n in range(3): # n = index of point
fc_colors[n, 3] = 1 # 4th value is alpha
plot.set_facecolors(fc_colors)
plot.figure.canvas.draw()
#I've put it into DataFrame() so you can better see
df = pd.DataFrame()
df['label'] = ["data_"+str(n) for n in range(6)]
df['id'] = [1, 1, 1, 2, 2, 2]
['x'] = [random.randint(-10, 10) for n in range(6)]
['y'] = [random.randint(-10, 10) for n in range(6)]
['z'] = [random.randint(-10, 10) for n in range(6)]
colors = {1:'red', 2:'blue'} # to map colors with df 'id'
#plot points colored according to value of df['id']
ax = plt.axes()
sc = ax.scatter(df['x'], df['y'], df['z'], c=df['id'].map(colors), marker='o', s=20, depthshade=False)
global fc_colors #yeah yeah globals...
face_colors = sc._face_colors
ax_button = plt.axes([0.81, 0.01, 0.1, 0.05])
ax_button_1 = plt.axes([0.68, 0.01, 0.12, 0.05])
button= Button(ax_button, "OFF")
button_1= Button(ax_button_1, "ON")
button.on_clicked(lambda event: TogglePointsOFF(event, sc))
button_1.on_clicked(lambda event: TogglePointsON(event, sc))
plt.show()
Clicking buttons "ON" and "OFF" will hide/show group of points based on index.
I've tried using set_alpha() and passing iterable of alpha values like: [0, 0, 0, 1, 1, 1] however it seemed to work on random points and set alpha of incorrect points.
Also getting face_colors from get_facecolors() seemed to get colors with random index alignment. This may be connected why passing iterable with alpha values to set_alpha() didn't work. That's why I take colors of points from: sc._face_colors .
Thank you for your time.
WARNING! Be advised.
This doesn't work when you use any 'official' colormap like this:
sc = ax.scatter(df['x'], df['y'], df['z'], cmap='tab10, vmin=10, vmax=10, marker='o', s=20, depthshade=False)
For setting alpha of points as described above you have to "kind-off" make you own colormap mapping like it was done here:
c=df['id'].map(colors)
or, use Normalizer object to map any colormap to some custom values like this:
from matplotlib.colors import Normalize #added
#let's assume we have some score values coresponding with data points:
score = [random.uniform(0.101, 100.123) for n in range(6)]
#but we can use any iterable with numbers
norm = Normalize(min(score), max(score)
cmap = matplotlib.cm.get_cmap('Spectral') #get some built in colormap
colors = cmap(norm(score))
#now you can use colors as 'c' parameter:
sc = ax.scatter(df['x'], df['y'], df['z'], c=colors, marker='o', s=20, depthshade=False)
Remember! don't put any alpha parameter and use depthshade=False to prevent fading of points in the back of the plot.
I hope you found this usefull.
Keep scrolling.
I am exploring random stackings of cubes.
I started with 2D and can generate random packings within a rectangle like this:
Now I have the code to generalize the stacking to 3D, but I am struggling to generalize the visualization. An example data set is, filling a 3x3x3 cube with 1x1x1 and 2x2x2 cubes,
#the coordinates of a corner vertex of the 19 1x1x1 cubes
x1 = [1, 0, 2, 0, 0, 0, 2, 1, 0, 1, 2, 2, 0, 0, 0, 2, 0, 1, 1]
y1 = [1, 1, 0, 2, 0, 0, 2, 2, 2, 0, 1, 0, 1, 2, 1, 0, 0, 0, 0]
z1 = [2, 1, 1, 0, 1, 2, 2, 2, 2, 1, 2, 0, 0, 1, 2, 2, 0, 0, 2]
#the coordinates of a corner vertex of the 1 2x2x2 cube
x2 = [1]
y2 = [1]
z2 = [0]
# I believe the random filling is working because
# the total volumes equal: 19 + 2**3 = 3**3
#I would like to start with the lists
X = [x1,x2]
Y = [y1,y2]
Z = [z1,z2]
sizes = [1,2]
#because I want to generalize the visualization to n sizes
So far, all I have the knowledge to do is plot a 3D scatter of the data
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for ii in range(len(sizes)):
ax.scatter(X[ii],Y[ii],Z[ii])
plt.show()
I would like to make a plot more like this, except with variable sizes.
Any help would be greatly appreciated! I have a lot to learn about matplotlib/pyplot and so on.
I have made a little bit of progress:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, PathPatch
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.art3d as art3d
def cube(a,b,c,l):
for zz in [c,c+l]:
for i in ["x","y","z"]:
side = Rectangle((a, b), l,l)
ax.add_patch(side)
art3d.pathpatch_2d_to_3d(side, z=zz, zdir=i)
fig = plt.figure()
ax=fig.gca(projection='3d')
cube(0,0,0,1)
ax.set_xlim3d(-2, 2)
ax.set_ylim3d(-2, 2)
ax.set_zlim3d(-2, 2)
plt.show()
This plots a single cube.
EDIT:
More progress, I am now very close
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, PathPatch
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.art3d as art3d
cmap = plt.get_cmap('spring') #define the colors of the plot
colors = [cmap(i) for i in np.linspace(0.1, 0.9, n+1)]
def cube(a,b,c,l): #plots a cube of side l at (a,b,c)
for ll in [0,l]:
for i in range(3):
dire= ["x","y","z"]
xdire = [b,a,a]
ydire = [c,c,b]
zdire = [a,b,c]
side = Rectangle((xdire[i], ydire[i]),facecolors[np.where(sizes == l)[0]],edgecolor='black')
ax.add_patch(side)
art3d.pathpatch_2d_to_3d(side, z=zdire[i]+ll, zdir=dire[i])
def plotter3D(X,Y,Z,sizes): #run cube(a,b,c,l) over the whole data set
for iX in range(len(X)):
x = X[iX]
y = Y[iX]
z = Z[iX]
for ix in range(len(x)):
cube(x[ix],y[ix],z[ix],sizes[iX])
fig = plt.figure() #open a figure
ax=fig.gca(projection='3d') #make it 3d
plotter3D(X,Y,Z,sizes) #generate the cubes from the data set
ax.set_xlim3d(0, length) #set the plot ranges
ax.set_ylim3d(0, width)
ax.set_zlim3d(0, height)
plt.show()
This generates the desired output, although it seems to be see-through in some places when viewed from certain angles. You can see this in the small cube-- dead center at coordinates (1.5,2,3) Any idea how to fix this?
Another edit:
The solution outined above has two problems: (1) I can't get equal aspect ratios for the three axes, and (2) The cubes are see-through from certain angles. Here's what the output looks like for a larger system
I have sparse scatter plot to visualize the comparison of predicted vs actual values. The range of the values are 1-4 and there are no decimal points.
I have tried plotly so far with hte following code (but I can also use a matplotlib solution):
my_scatter = go.Scatter(
x = y_actual, y = y_pred, mode = 'markers',
marker = dict(color = 'rgb(240, 189, 89)', opacity=0.5)
)
This prints the graph nicely (see below). I use opacity to see the density at each point. I.e. if two points lie on top of each other, the point will be shown in darker color. However, this is not explanatory enough. Is it possible to add the counts at each point as a label? There are some overlaps at certain intersections. I want to display how many points intersects. Can this be done automatically using matplotlib or plotly?
This answer uses matplotlib.
To answer the initial question first: You need to find out how often the data produces a point at a given coordinate to be able to annotate the points. If all values are integers this can easily be done using a 2d histogram. Out of the hstogram one would then select only those bins where the count value is nonzero and annotate the respective values in a loop:
x = [3, 0, 1, 2, 2, 0, 1, 3, 3, 3, 4, 1, 4, 3, 0]
y = [1, 0, 4, 3, 2, 1, 4, 0, 3, 0, 4, 2, 3, 3, 1]
import matplotlib.pyplot as plt
import numpy as np
x = np.array(x)
y = np.array(y)
hist, xbins,ybins = np.histogram2d(y,x, bins=range(6))
X,Y = np.meshgrid(xbins[:-1], ybins[:-1])
X = X[hist != 0]; Y = Y[hist != 0]
Z = hist[hist != 0]
fig, ax = plt.subplots()
ax.scatter(x,y, s=49, alpha=0.4)
for i in range(len(Z)):
ax.annotate(str(int(Z[i])), xy=(X[i],Y[i]), xytext=(4,0),
textcoords="offset points" )
plt.show()
You may then decide not to plot all points but the result from the histogramming which offers the chance to change the color and size of the scatter points,
ax.scatter(X,Y, s=(Z*20)**1.4, c = Z/Z.max(), cmap="winter_r", alpha=0.4)
Since all values are integers, you may also opt for an image plot,
fig, ax = plt.subplots()
ax.imshow(hist, cmap="PuRd")
for i in range(len(Z)):
ax.annotate(str(int(Z[i])), xy=(X[i],Y[i]), xytext=(0,0), color="w",
ha="center", va="center", textcoords="offset points" )
Without the necesity to calculate the number of occurances, another option is to use a hexbin plot. This gives slightly inaccurate positions of the dots, du to the hexagonal binning, but I still wanted to mention this option.
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x = np.array(x)
y = np.array(y)
fig, ax = plt.subplots()
cmap = plt.cm.PuRd
cmaplist = [cmap(i) for i in range(cmap.N)]
cmaplist[0] = (1.0,1.0,1.0,1.0)
cmap = matplotlib.colors.LinearSegmentedColormap.from_list('mcm',cmaplist, cmap.N)
ax.hexbin(x,y, gridsize=20, cmap=cmap, linewidth=0 )
plt.show()