I am trying to use plotly surface to plot a sphere of binned data. I have the centre of each bin in longitude and latitude which I can easily convert to x, y and z. But what I am stuck on is how Surface actually works I am seeing examples where the x, y and z co-ordinates define the edge of the vertex and other examples where there aren’t enough co-ordinates for it to possibly be the edge of each vertex. So what is surface actually doing? is it the centre or the edge? If I was using matplotlib pcolormesh I would be defining the edges of each bin. Basically I need to understand what its doing with x, y and z so I know what to provide it
@08walkersj Depending on how the surface is given, i.e. by its explicit
equation z=f(x,y), (x,y)\in [a,b]\times [c,d] or by a parameterization:
\begin{array}{lll}x&=&x(u,v)\\
y&=&y(u,v), \:\: (u,v)\in[\alpha, \beta]\\
z&=&z(u,v)\\
\end{array}
the data x, y can be lists/vectors in the first case, and z a 2d array,
while in the second case x, y, z must be 2d arrays:
1. Surface z=\sin(x)*\cos(y), x, y \in[-\pi/2, \pi/2]\times[0,2\pi/3]
import plotly.graph_objects as go
import numpy as np
from numpy import pi, sin, cos
x = np.linspace(-pi/2, pi/2, 100)
y = np.linspace(0,2*pi/3, 80)
X, Y = np.meshgrid(x,y)
z = sin(X)*cos(Y)
fig1=go.Figure(go.Surface(x=x, y=y, z=z, colorscale="deep_r",
colorbar_thickness=24))
fig1.update_layout(width=500, height=500, font_size=10,
scene_camera_eye=dict(x=1.55, y=1.55, z=1))
fig1.show()
2: Sphere given through a parameterization:
theta = np.linspace(0, 2*pi, 150)
phi = np.linspace(0, pi, 150)
theta, phi = np.meshgrid(theta, phi)
x = cos(theta)*sin(phi)
y = sin(theta)*sin(phi)
z = cos(phi)
fig2 = go.Figure(go.Surface(x=x, y=y, z=z, colorscale="deep_r",
colorbar_thickness=24, colorbar_len=0.75))
fig2.update_layout(fig1.layout)
fig2.show()
Okay thank you I understand now. I haven’t really used surface plots in matplotlib either. So now I also understand the parameterisation that the surface plot is an interpolation between points that define each corner in terms of the colour scale.
I guess if I wanted something that looks like pixels I would create a big loop through each pixel in terms of edge co-ordinates and colour value and either create a surface plot for each iteration where I define all edges of the surface as having the same colour value (which is overkill) or create a square (with small z thickness) of one colour. Unless you have an alternative suggestion? I would like to of course avoid the loop but if this is the only workable solution then it is what it is.
@08walkersj Please give a matplotlib example of what you mean in your above explanation (i didn’t understand). The plotly’s surface trace does not involve pixels and edges. When you are displaying a plotly figure, in a jupyter notebook, you’ll see a svg or a webgl image, which can be saved as png.
Hi there isn’t a matplotlib example really for this but I will throw together some code tomorrow to better explain what it is I am trying to do. but in short, my understanding of the surface plot is that you provide x,y,z co-ordinates for each edge along with a data value for the colour. Then the surface plot interpolates. So say you want to create one single surface you provide (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), (x4, y4, z4) and a value at each of these points and then it will interpolate the values between the co-ordinates (fit a function) to create the colour of the surface. This is just a simplistic case.
What I am wish to do is essentially produce a 3D pcolormesh (as in the matplotlib function pcolormesh converted into 3D). In matplotlib when you want define a pcolormesh properly you give the edge points of each “pixel” or square whatever you want to to call, and it will create a solid colour in that area defined by your co-ordinate edges and it will do that for all your edges creating a plot of squares with no function fitting. So for example if you are plotting a 2D histogram it will truly represent the data points rather what is being done with a surface plot or in the 2D case a contourf (as in the matplotlib function).
My situation is I want to do this in 3D on a globe. I have the edges of the bins of a histogram produced in longitude and latitude and at a fixed height. I can easily convert these to x, y and z this means I now have co-ordinates of multiple 3D boxes that are continious across this 3D sphere. I have a single value for each box that denotes its colour. A surface plot interpolates between each point and thus is not a true representation of the data. If I was to loop through each value and bin edges of the histogram I could draw a 3D box of a single colour that is decided by the value in that bin.
In the case of the binning you each box will be sharing vertices with other boxes (just as would be the case in a 2D pcolormesh)
I hope this explains the situation a little better I will provide a simplified version of the code and example data soon. Just to be clear I understand this has moved from what surface is designed for and I am not aware of matplotlib alternative that is capable of this either. One of the challenges is that this data is not evenly spaced in x, y and because its evenly space in spherical co-ordinates.
@08walkersj I think I understood what you are saying in unknown terms for plotly. After discretizing a surface, you can define an array of the same shape like z in the first example, respectively the same shape as the parameter arrays, in the second one. This array contains in each position, a number to be mapped to a color for each “pixel”, i.e. for each point in the surface discretization. This array is the value assigned to surfacecolor
in the go.Surface
definition.
I give an example in which this magic array contains a Julia set:
import plotly.graph_objects as go
import numpy as np
from numpy import pi, sin, cos
def Julia(z,c, maxiter=80):
for n in range(maxiter):
if abs(z)>2:
return n
z = z*z + c
return maxiter
ntheta=450
nphi=400
theta=np.linspace(0, 2*pi, ntheta)
phi=np.linspace(0, pi, nphi)
theta, phi = np.meshgrid(theta, phi)
x=cos(theta)*sin(phi)
y=sin(theta)*sin(phi)
z=cos(phi)
#define the Julia set on a rectangle discretized in an array of the shape (nphi, ntheta)
rZ = np.linspace(-1.5, 1.5, ntheta)
imZ = np.linspace(-1.5, 1.5, nphi)
rZ, imZ=np.meshgrid(rZ, imZ)
Z= rZ +imZ*1j
#HERE is associated to each "pixel" a number which is mapped to a color
color_surf=np.array([Julia (Z[I,J], -0.04-0.684*1j) for I in range(nphi)
for J in range(ntheta)]).reshape(nphi, ntheta)
fig=go.Figure(go.Surface(x=x, y=y, z=z, surfacecolor=color_surf, colorscale="RdGy",
showscale=False))
fig.update_layout(width=500, height=500, font_size=10,
scene_camera_eye=dict(x=-1.55, y=1.55, z=1),
)
fig.show()
Hi what you show is what I currently do but the nature of surface is that there is function fitting that smooths. The result I am looking for would do this.
Currently the best solution I have is to use produce individual rectangles in a loop:
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
lon = np.arange(12, 360, 24)
lat= np.arange(-84, 90, 12)
clon_size= np.diff(lon)[0]
clat_size= np.diff(lat)[0]
lon, lat= np.meshgrid(lon, lat)
vals= np.random.uniform(0, 1, size=lon.shape)
def degree2radians(degree):
return degree * np.pi / 180
def mapping_map_to_sphere(lon, lat, radius=1):
lon = np.array(lon, dtype=np.float64) # Convert input longitudes to numpy array of float64 type
lat = np.array(lat, dtype=np.float64) # Convert input latitudes to numpy array of float64 type
lon = degree2radians(lon) # Convert longitudes from degrees to radians using degree2radians function
lat = degree2radians(lat) # Convert latitudes from degrees to radians using degree2radians function
xs = radius * np.cos(lon) * np.cos(lat) # Compute x-coordinates on the sphere
ys = radius * np.sin(lon) * np.cos(lat) # Compute y-coordinates on the sphere
zs = radius * np.sin(lat) # Compute z-coordinates on the sphere
return xs, ys, zs # Return the mapped coordinates as a tuple of arrays
faces=[]
htext=[]
height=1
for clon, clat, val in zip(lon.flatten(), lat.flatten(), vals.flatten()):
corners= [(clon-clon_size/2, clat-clat_size/2, height),
(clon-clon_size/2, clat+clat_size/2, height),
(clon+clon_size/2, clat+clat_size/2, height),
(clon+clon_size/2, clat-clat_size/2, height)]
corners= [mapping_map_to_sphere(*corner) for corner in corners]
faces.append(corners)
htext.append(f'lon: {clon}\n lat: {clat} \n value: {val}')
# Colorscale
colorscale = px.colors.diverging.RdBu
# Get the min and max value to normalize the colors
# values = [cuboid['value'] for cuboid in cuboids]
min_value = np.nanmin(vals)
max_value = np.nanmax(vals)
traces = []
for face, val, ht in zip(faces, vals.flatten(), htext):
face= np.array(face).T
# normalized_value = (cuboid["value"] - min_value) / (max_value - min_value)
if np.isnan(val):
color='white'
else:
color = px.colors.sample_colorscale(colorscale, [val])[0]
x, y, z= face
traces.append(go.Scatter3d(
x=x,
y=y,
z=z,
mode='lines',
surfaceaxis=0,
line=dict(color='black'),
marker=dict(size=2),
surfacecolor=color,
showlegend=False,
hoverinfo='text',
text=ht
))
traces.append(go.Surface(x=[0],
y=[0],
z=[0],
colorscale='RdBu',
surfacecolor=[0],
showscale=True,
cmin=min_value,
cmax=max_value,
colorbar=dict(thickness=20, len=0.75, ticklen=4, title='Bin Value')))
layout= go.Layout({
'font': {'family': 'Balto', 'size': 14},
'height': 800,
'paper_bgcolor': 'rgba(235,235,235, 0.9)',
'scene': {'aspectratio': {'x': 1, 'y': 1, 'z': 1},
'camera': {'eye': {'x': 1.15, 'y': 1.15, 'z': 1.15}},
'xaxis': {'range': [-2, 2],
'showbackground': False,
'showgrid': False,
'showline': False,
'showticklabels': False,
'ticks': '',
'title': {'text': ''},
'zeroline': False},
'yaxis': {'range': [-2, 2],
'showbackground': False,
'showgrid': False,
'showline': False,
'showticklabels': False,
'ticks': '',
'title': {'text': ''},
'zeroline': False},
'zaxis': {'range': [-2, 2],
'showbackground': False,
'showgrid': False,
'showline': False,
'showticklabels': False,
'ticks': '',
'title': {'text': ''},
'zeroline': False}},
'width': 800
})
# Create the figure
fig = go.Figure(data=traces, layout=layout)
fig.show()
The limitation here is that I have to loop for each face and create a dummy surface to produce a colour bar.