Sphere in 3D with dates as an axis

I have a 3D plot which traces prices over a number of days. it works fine as a Scatter3d.

I’m trying to introduce a sphere, to be drawn around one of the points. I’m using the first part of Empet’s code as a reference
https://community.plotly.com/t/adding-wireframe-around-a-sphere/37661

The problem I’m facing is having dates in one of the axes. When I uncomment the createSphere statement, a sphere is drawn but at y=0, i.e. some time in the year 1970.

It’s also changing the axis labeling.

What can I do to manipulate a date/datetime in the y-axis? I’ve tried converting the date into a .timestamp() but that doesn’t seem to work.

Code is below, with my thanks in advance for any suggestions.

import plotly.graph_objects as go
import datetime


prices=[26269, 26281, 27110, 27572, 27272, 26989, 25128, 25605]

base = datetime.datetime(2022, 2, 1)
dates = [base + datetime.timedelta(days=x) for x in range(8)]
counts = [x+1 for x in range(8)]

fig = go.Figure()

fig.add_scatter3d(x=counts, y=dates, z=prices,
                  marker=dict(size=2))


def createSphere(fig, prices, dates, counts):
    from numpy import pi, sin, cos
    import numpy as np
    
    theta = np.linspace(0, 2*pi, 120)
    phi = np.linspace(0, pi, 60)
    u , v = np.meshgrid(theta, phi)
    xs = 3*cos(u)*sin(v) + counts[5]
    ys = sin(u)*sin(v) #??? how to apply a date/datetime value here?
    zs = 1000*cos(v) + prices[5]

    fig.add_surface(x=xs, y=ys, z=zs, 
                    colorscale='Greens', 
                    showscale=False, opacity=0.5)  # or opacity=1

#createSphere(fig, prices, dates, counts)

fig.show()

Hi @samri ,

I defined a new function, createSphere. Inside its body the interval [-1,1], as the range of the scalar function sin(u)*sin(v), is mapped onto an interval of dates, through two successive transformations, commented and defined below:

def createSphere(fig, prices, dates, counts):
    from numpy import pi, sin, cos
    import numpy as np
    a = datetime_to_float(dates[0]) #convert datetime to float, i.e. to seconds
    b = datetime_to_float(dates[-1])
    u = np.linspace(0, 2*pi, 120)
    v = np.linspace(0, pi, 60)
    u , v = np.meshgrid(u, v)
    #map the interval [-1, 1]  (i.e. the range of sin(u)*sin(v)), onto the interval of seconds [a, b]
    sec = (a+(b-a)*(sin(u)*sin(v)+1)/2).astype(int)
    r, c = sec.shape
    #convert seconds to datetime
    ys =  np.array([[datetime.datetime.fromtimestamp(sec[i, j]) for j in range(c)] for i in range(r)])
    xs = 3*cos(u)*sin(v) + counts[5]
    zs = 1000*cos(v) + prices[5]

    fig.add_surface(x=xs, y=ys, z=zs, 
                    colorscale='Greens', 
                    showscale=False, opacity=0.5)  

Note that your surface is not a sphere, but an ellipsoid. The parametric equations of an ellipsoid of center (x0, y0, z0) and semi-axes A, B, C are:

xe = x0+ A*cos(u)*sin(v)
ye = y0+B* sin(u)*sin(v) 
zs = z0+C*cos(v) 

For a sphere, A=B=C=sphere radius

@samri I’m back to explain how is mapped [-1,1] to [a, b]:

y = sin(u)*sin(v) in [-1,1] --->t=(y-(-1))/(1-(-1)=(y+1)/2-->a+(b-a)*t,     t in [0,1]

i.e.

sin(u)*sin(v)-->a+(b-a)*(sin(u)*sin(v)+1)/2

I used your data without being able to figure out why you chose the coeffficient B=1 in ellipsoid parameterization. With your settings, B=1 means that y goes from -1seconds to 1 seconds. If you want a greater interval of seconds for y, then you must assign a bigger value to B,
i.e. define y= B*sin(u)*sin(v)

Thanks @empet , both for this and the original code I had used.

It’s beginning to make sense now. While waiting for an answer I did try converting the dates to floats, even though there are still parts that I’m trying to understand. Here’s the code and I’ll explain afterwards my choice of value for B:

import plotly.graph_objects as go
import datetime


prices = [26269, 26281, 27110, 27572, 27272, 26989, 25128, 25605]

base   = datetime.datetime(2022, 2, 1)
dates  = [base + datetime.timedelta(days=x) for x in range(8)]
counts = [x+1 for x in range(8)]

fig = go.Figure()

fig.add_scatter3d(x=counts, y=dates, z=prices,
                  mode = 'lines+markers+text',
                  marker=dict(size=5)
                  )
    
def createSphere_Samri(fig, prices, dates, counts):
    from numpy import pi, sin, cos
    import numpy as np
    
    theta = np.linspace(0, 2*pi, 12*10)
    phi = np.linspace(0, pi, 6*10)
    u , v = np.meshgrid(theta, phi)
    
    one_day  = 24*60*60 
    tot_secs = (dates[5]-datetime.datetime(1970, 1, 1)).total_seconds()
    
    #still trying to understand why i have to multiply by 1000 (is axis in milliseconds?)
    one_day  = one_day  * 1000
    tot_secs = tot_secs * 1000
        
    xs = counts[5]       + cos(u)*sin(v) * 1/2           #multiplying by 1/2 in order to center it between count=5 and count = 6   
    zs = prices[5]       + cos(v)        * 250           #multiplying by 250, just an arbitrary value for now

    ys = (tot_secs)      + sin(u)*sin(v) * 1/2 * one_day #again, multiply by 1/2 trying to center between Feb 5 and Feb 6    
    
    # however, ellipse only appears to "center" after I add this line. Is it because a year is 365.24 days?
    ys = ys + (0.24 * one_day)
    
    fig.add_surface(x=xs, y=ys, z=zs, 
                    colorscale='Greens', 
                    showscale=False, opacity=0.5)  
    

createSphere_Samri(fig, prices, dates, counts)
fig.show()

What I’m trying to do is to take a point, let’s say February 6th (dates[5]). The price is already shown by a scatter3D line+marker. Centred around that marker, i want to create a sphere/ellipsoid that shows the price with a value added to it as its radius on the Z axis. I’m using an increment of 250 for now.

I had gotten as far as figuring out that the dates have to be offset from the epoch 1970.01.01, but I’m still trying to understand why i have to multiply by 1000. Also another things is why I have to add 0.24 days to have the ellipsoid center correctly. I’ll try to understand it using your new code and the explanation you provided. Many thanks again :pray: