Can I do Scatter3d in spherical coordinates?

Hi, I have succesfully made a light cone plot (kind of a wedge plot) in polar coordinates with this code:

fig = px.scatter_polar(df_rdwz, r='z', theta='ra',
                       range_theta=[-6,6], range_r=[0.8,3.2], start_angle=0, direction="counterclockwise")
fig.show()

The result looks like this:

But I have as data not just the theta angle, but also a phi angle. I would like to use the phi angle to make this plot 3d, means to give it depth. Is there something like px.scatter_spherical or px.scatter_3d_spherical that would allow me this? Alternatively, I know I can use go.Scatter3d with cartesian coordinates (x,y,z) - can I then make go.Scatter3d just have the axis drawn spherical (theta, phi, r) instead of cartesian (x,y,z), while the dots themselves are plotted in cartesian coordinates?
Any other suggestions of how to make this plot in 3d with spherical axis labels are welcome! tnx

There is no special px function that generates a 3d scatter plot from points given in spherical coordinates, but it is simple to define a function that converts the spheric to cartesian coordinates, and to plot the corresponding points:

import plotly.graph_objects as go
import numpy as np
from numpy import pi, sin, cos

def spheric2cartesian(r, theta, phi):
    x = r*cos(theta) *sin(phi)
    y = r*sin(theta)*sin(phi)
    z= r*cos(phi)
    return x, y, z
n=250 #number of points to be generated
R = 1.5 #sphere radius
theta_m = pi/12  #min theta  val
theta_M = theta_m + pi/6 #max theta val
phi_m= pi/5    #min phi
phi_M=  phi_m + pi/6  #max phi
#generate n points within a spherical sector 
r = R*np.random.rand(n)
theta = theta_m + (theta_M-theta_m) *np.random.rand(n)
phi= phi_m +(phi_M-phi_m)* np.random.rand(n)
x, y, z = spheric2cartesian (r, theta, phi)
fig = go.Figure(go.Scatter3d(x=x, y=y, z=z, mode="markers", marker_size=3))
fig.update_layout(width=600, height=600, scene_camera_eye=dict(x=1.5, y=-1.5, z=1),           scene_aspectmode="data")

A spherical system of coordinates is a curvilinear one, hence don’t expect to plot “special axes” associated to r, theta and phi. For a point P(x,y,z), phi is the angle between Oz and OP, theta is the angle measured within the plane xOy, between OP’ and Ox, where P’ is the orthogonal projection of P onto the plane xOy (i.e. a plane of equation z=0). Finally r is the distance from P to origin (the sphere center).
Referencing the points given in spherical coordinates, to the cartesian system, xOyz, allows to estimate visually its r, phi and theta limits.

Thanks for the reply, empet! I actually do have all my data in both spherical as well as cartesian coordinates, so I don’t need to do this transformation. My problem is another one - I want to have the AXIS of a 3D PLOT to be spherical, instead of cartesian. So far I know I can do:
1.) 3d plot with cartesian coordinates (but I want spherical)
2.) 2d plot in polar coordinates, like in my plot above (but I want it to actually have 1 axis more)
Can you tell me if I can expand/alter options 1.) or 2.) to achieve my desired result - a 3d plot with axis being spherical coordinates? tnx

@NeStack
As I already pointed out there is no trace type that works in spherical coordinates or a layout specific for spheric plots.
I’m curious what do you mean by spherical axes. Could you post, please, an image of a manually drawn spheric system of axes?

Thanks for replying!
“Could you post, please, an image of a manually drawn spheric system of axes?” - Hm, strangely enough I can’t find on the internet images of what I am actually looking for and what I thought would be common. The closest I find is this image, but in 2D:


It doesn’t really have spherical “axis”, but it has a spherical coordinate system GRID, what is what I guess I am looking for. It is pretty much what I already achieved with px.scatter_polar. I drew with red the extension of the GRID in a 3rd dimension, another angle.
So I guess a paraphrasing of my question would be if I can give a 3d plot a spherical coordinates grid? Tnx

@NeStack

I tried to define a layout for spherical coordinates. As you can see it depends on data plotted as spheric scatter3d. The “floor” of the “sectors” of coordinates is not horizontal, like the plane z=min(zdata) in a cartesian system. It is included in the plane where phi coords are constant and equal to the phi- max value. I suppose that you intend to use such a layout for a single plot, because otherwise it is a nightmare to generate it, since Plotly has no definition for it.
Here is the code for some data. It’s not a smart code (it isn’t parameterized), but just one that works to illustrate what could be mathematically reasonable for such a layout. You must adapt it to your data, and add ticklabels as 3D annotations https://plotly.com/python/text-and-annotations/#3d-annotations.

import plotly.graph_objects as go
import numpy as np
from numpy import pi, sin, cos
from scipy.spatial.transform import Rotation as Rot

np.random.seed(2022)
deg2radians= lambda deg: deg*pi/180
def spheric2cartesian(r, theta, phi):
    x = r*cos(theta) *sin(phi)
    y = r*sin(theta)*sin(phi)
    z= r*cos(phi)
    return x, y, z

R = 1.5
theta_m = 15   
theta_M = theta_m + 40
phi_m= 70    
phi_M=  phi_m +50
 

thetar_m, thetar_M = deg2radians(theta_m), deg2radians(theta_M)
phir_m, phir_M = deg2radians(phi_m), deg2radians(phi_M)

colorscale=[[0, "lightblue"],[1, "lightblue"]]

r_plus =0.1  #a small amount to be added to the radius
t_plus=0.05  # to be substracted/added to thetar and phir to avoid points  lying on the sector of coordinates

#define the circular sector under the scatter points, i.e. equivalent to the plane z=cst for cartesian axes
r = np.linspace(0, R+r_plus, 100)
theta = np.linspace(thetar_m-t_plus, thetar_M+t_plus, 50)
rm, theta = np.meshgrid(r, theta)
phi = (phir_M+t_plus)*np.ones(rm.shape)
xb, yb, zb = spheric2cartesian(rm, theta, phi)

lighting =dict(ambient=0.8,
               diffuse=0.8,
               specular=0.2,
               roughness=0.5,
               fresnel=0.4)
lightposition =dict(x=1000, y=1000, z=-1000)
fig=go.Figure(go.Surface(x=xb, y=yb, z=zb, coloraxis="coloraxis", 
                         lighting=lighting,
                        lightposition=lightposition)) #The floor of the plot

#plot the sector, equivalent to the plane behind the points  in cartesian axes
Phi = np.linspace(phir_m-t_plus, phir_M+t_plus, 75)
rp, Phi =np.meshgrid(r, Phi)
Theta = (thetar_M+t_plus)*np.ones(rp.shape)
xb, yb, zb = spheric2cartesian(rp, Theta, Phi)
fig.add_surface(x=xb, y=yb, z=zb, 
                coloraxis="coloraxis",
                lighting=lighting,
               lightposition=lightposition)

#####
#Generate points to be ploted with respect to spheric layout
n=250
rp = R*np.random.rand(n)
thetap = thetar_m + (thetar_M-thetar_m) *np.random.rand(n)
phip= phir_m +(phir_M-phir_m)* np.random.rand(n)
X, Y, Z = spheric2cartesian (rp, thetap, phip)

fig.add_scatter3d(x=X, y=Y, z=Z, mode="markers", 
                  marker_size=3, marker_color="RoyalBlue")
# grid lines on the floor sector:
thetagr = deg2radians(np.asarray([15, 25, 35, 45]))
XL = []
YL = []
ZL = []
for t in thetagr:
    xl, yl, zl =spheric2cartesian(np.linspace(0, R+r_plus, 10), t, phir_M+t_plus)               
    XL.extend(list(xl))
    XL.append(None)
    YL.extend(list(yl)) 
    YL.append(None)
    ZL.extend(list(zl))  
    ZL.append(None)
rgr  =[0.5, 1, 1.5]    
for rg in rgr:
    xl, yl, zl =spheric2cartesian( rg,  np.linspace(thetar_m-t_plus, thetar_M+t_plus, 30), (phir_M+t_plus)*np.ones(30))
    XL.extend(list(xl))
    XL.append(None)
    YL.extend(list(yl)) 
    YL.append(None)
    ZL.extend(list(zl))  
    ZL.append(None)
fig.add_scatter3d(x=XL, y=YL, z=ZL, mode="lines", line_color="rgb(200, 200, 200)", line_width=3) 
    
# grid Lines on the back sector:
phigr = deg2radians(np.asarray([75, 85, 95, 105, 115]))
XL = []
YL = []
ZL = []
for p in phigr:
    xl, yl, zl =spheric2cartesian(np.linspace(0, R+r_plus, 10), thetar_M+t_plus, p)               
    XL.extend(list(xl))
    XL.append(None)
    YL.extend(list(yl)) 
    YL.append(None)
    ZL.extend(list(zl))  
    ZL.append(None)
for rg in rgr:
    xl, yl, zl =spheric2cartesian( rg, (thetar_M+t_plus)*np.ones(30), np.linspace(phir_m-t_plus, phir_M+t_plus, 30))
    XL.extend(list(xl))
    XL.append(None)
    YL.extend(list(yl)) 
    YL.append(None)
    ZL.extend(list(zl))  
    ZL.append(None)
fig.add_scatter3d(x=XL, y=YL, z=ZL, mode="lines", line_color="rgb(200, 200, 200)", line_width=3) 


fig.update_layout(width=800, height=800, 
                  showlegend=False,
                  coloraxis=dict(colorscale=colorscale, showscale=False),
                  scene =dict(aspectmode="data",
                              xaxis_visible=False, 
                              yaxis_visible=False, zaxis_visible=False,
                              camera_eye=dict(x=1.85, y=-1.85, z=0.8))) ;
fig.show()

lighting and lightposition must also be tuned according to your data.

Wow, thank you a lot! It looks like it works for me, I just need to tinker a few things.
Can you tell me if I can make, when I hover over points in the 3d figure, the coordinates to be presented in theta, phi, r? Now they are presented in x,y,z, see here:
Screenshot from 2022-02-02 20-56-21

@NeStack

Yes, we can display the spheric coords on hover as follows:

  • define customdata for r, theta, phi
  • add customdata to the corresponding Scatter3d definition, as well as hovertemplate (see the code below);
    theta, phi are represented by their unicode to be displayed as greek letters , while :.2f sets the number of decimals to be displayed on hover (you can change 2 with your desired number);
  • since the other traces are for layout, insert in their definition
hoverinfo="skip"

otherwise on hover we can see the coords x,y,z in grid lines, or the coords x, y, z on the two sectors, and these data are not of interest.

  • BUT due to skipping hovering in the nearby graphical objects, now it is difficult to place the mouse such that the r, theta, and phi to be displayed. Only in some postion of the rotated plot we can get these spherical coords.
X, Y, Z = spheric2cartesian (rp, thetap, phip)
customdata= np.stack((rp, thetap, phip), axis=-1)
hovertemplate="r: %{customdata[0]:.2f}<br>&#952;: %{customdata[1]:.2f}"+\
              "<br>&#966;: %{customdata[2]:.2f}<extra></extra>"
fig.add_scatter3d(x=X, y=Y, z=Z, mode="markers", 
                  customdata =customdata,
                  hovertemplate=hovertemplate,
                  marker_size=3, marker_color="RoyalBlue")
1 Like

Thank you a lot! These all work fine, I hope I will not come up with another question later :slight_smile: