pythonarraysplotplotlysurface

Colouring a surface using go.Surface in plotly


I aim to colour a surface in a 3d plot, created using plotly.graph_objects.

I've been working on an approach incorporating Mesh3d, the code runs, but the wanted surface is not coloured. How can this be solved in plotly?

The final outcome should look similar to this example plot: enter image description here

And this is where I am at now: enter image description here

So, the aim is to colour the surface where the vertical lines are connecting the two curves (i.e., a 3-dimensional curve and its projection onto the x-y plane).

Example data can be obtained here.

Here is my code:

import pandas as pd
import plotly.graph_objects as go
import numpy as np


data = pd.read_excel(r"")

x = data['x'].values
y = data['y'].values
z = data['alt'].values


z_min_adjusted = np.maximum(z, 350)


def plot_3d_course_profile(x, y, z, color_attribute, colorbar_label):
    fig = go.Figure()

    # Projection at z = 350
    fig.add_trace(go.Scatter3d(
        x=x,
        y=y,
        z=[350] * len(x),
        mode='lines',
        line=dict(
            color='black',
            width=5,
        )
    ))

    # Profile curve
    fig.add_trace(go.Scatter3d(
        x=x,
        y=y,
        z=z,
        mode='lines',
        line=dict(
            color=color_attribute,
            width=13,
            colorscale='Jet',
            colorbar=dict(
                title=dict(
                    text=colorbar_label,
                    font=dict(size=14, color='black')
                ),
                thickness=20,
                len=0.6,
                tickfont=dict(size=12, color='black'),
                tickmode='linear',
                tickformat='.2f',
                outlinewidth=1,
                outlinecolor='black'
            )
        )
    ))

    # Vertical lines
    for i in range(0, len(x), 20):
        xi, yi, zi = x[i], y[i], z[i]
        fig.add_trace(go.Scatter3d(
            x=[xi, xi],
            y=[yi, yi],
            z=[350, zi],
            mode='lines',
            line=dict(color='dimgray', width=4),
            opacity=0.6,
            showlegend=False
        ))

    #Layout
    fig.update_layout(
        template='plotly_white',
        scene=dict(
            xaxis=dict(title='Meters north from start position', showgrid=True, gridcolor='lightblue', zeroline=False),
            yaxis=dict(title='Meters east from start position', showgrid=True, gridcolor='lightblue', zeroline=False),
            zaxis=dict(title='Altitude [m]', showgrid=True, gridcolor='lightblue', zeroline=False),
            aspectmode='manual',
            aspectratio=dict(x=1.25, y=1, z=0.7)
        ),
        title=f'{colorbar_label}',
        margin=dict(l=0, r=0, b=0, t=50)
    )

    fig.show()



plot_3d_course_profile(x, y, z_min_adjusted, z_min_adjusted, 'Altitude [m]')

Solution

  • I could solve the problem by creating simple arrays using numpy for surface plotting and switching to go.Surface from Mesh3D. Any solutions concerning the latter are still welcomed.

    The complete code:

    import pandas as pd
    import plotly.graph_objects as go
    import numpy as np
    
    
    data = pd.read_excel(r'')
    
    x = data['x'].values
    y = data['y'].values
    z = data['alt'].values
    i = data['incl'].values
    
    
    X = np.vstack([x, x])
    Y = np.vstack([y, y])
    Z = np.vstack([[350] * len(x), z -0.1]) 
    I = np.vstack([i, i])
    
    fig = go.Figure()
    
    fig.add_trace(go.Scatter3d(
            x=x,
            y=y,
            z=z,
            mode='lines',
            line=dict(
                color=i,
                width=13,
                colorscale='Jet',
                colorbar=dict(
                    title=dict(
                        text='Incline [deg]',
                        font=dict(size=14, color='black')
                    ),
                    thickness=20,
                    len=0.6,
                    tickfont=dict(size=12, color='black'),
                    tickmode='linear',
                    tickformat='.2f',
                    outlinewidth=1,
                    outlinecolor='black'
                )
            )
        ))
    
    for j in range(0, len(x), 20):
        color_value= i[j]
    
    fig.add_trace(go.Surface(
        z=Z,
        x=X,
        y=Y,
        surfacecolor=I,
        colorscale='jet',  
        cmin=min(i), cmax=max(i), 
        showscale=False,
        opacity=0.7
    ))
    
    
    fig.add_trace(go.Scatter3d(
        x=x,
        y=y,
        z=[350] * len(x),
        mode='lines',
        line=dict(color='black', width=3),
        name="Profile Curve"
    ))
    
    
    for j in range(0, len(x), 20):
        fig.add_trace(go.Scatter3d(
            x=[x[j], x[j]],
            y=[y[j], y[j]],
            z=[350, z[j]],
            mode='lines',
            line=dict(color='dimgray', width=2),
            opacity=0.6,
            showlegend=False
        ))
    
    fig.update_layout(
        title="3D Course Profile Colored by Incline",
        template='plotly_white',
        scene=dict(
            xaxis=dict(title='Meters north from start position', showgrid=True, zeroline=False),
            yaxis=dict(title='Meters east from start position', showgrid=True, zeroline=False),
            zaxis=dict(title='Altitude [m]', showgrid=True, zeroline=False),
            aspectmode='manual',
            aspectratio=dict(x=1.25, y=1, z=0.7)
        ),
        margin=dict(l=0, r=0, b=0, t=50)
    )
    
    fig.show()
    

    Resulting in: enter image description here

    Version nr.2 using matplotlib.pyplot:

    I post a simpler solution using matplotlib.pyplot but without edge colouring:

    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    from matplotlib import cm
    import matplotlib.ticker as ticker
    
    
    data = pd.read_excel(r'')
    
    X_1d = data['x'].values
    Y_1d = data['y'].values
    Z_1d = data['alt'].values
    I_1d = data['incl'].values
    
    fig = plt.figure(figsize=(8, 5))
    ax = fig.add_subplot(111, projection='3d')
    
    
    X = np.vstack([X_1d, X_1d])
    Y = np.vstack([Y_1d, Y_1d])
    Z = np.vstack([Z_1d, Z_1d + (max(Z_1d)-min(Z_1d))])     #[350]*len(X_1d), Z_1d + 50      (and below twice)
    I = np.vstack([I_1d, I_1d])  
    
    
    norm = plt.Normalize(I.min(), I.max())
    colors = cm.jet(norm(I))
    
    fig = plt.figure(figsize=(8, 5))
    ax = fig.add_subplot(111, projection='3d')
    
    ax.plot(X_1d, Y_1d, Z_1d + (max(Z_1d)-min(Z_1d)), color='black', linewidth=2)
    
    surf = ax.plot_surface(X, Y, Z, facecolors=cm.jet(norm(I)), rstride=1, cstride=1, linewidth=0, antialiased=True, alpha=0.7)
    
    ax.plot_wireframe(X, Y, Z, color='k', linewidth=1, alpha=1.0)
    
    mappable = cm.ScalarMappable(norm=norm, cmap=cm.jet)
    mappable.set_array(I)
    
    cbar = fig.colorbar(mappable, ax=ax, shrink=0.5, alpha=0.7, aspect=5, extend='both', extendrect=True, spacing='proportional', format=ticker.FormatStrFormatter('%.1f'))
    cbar.set_label('Incline [deg]', rotation=270, labelpad=15)
    
    cbar.ax.axhline(max(I_1d), color='black', linewidth=1.2, linestyle='--')
    cbar.ax.axhline(min(I_1d), color='black', linewidth=1.2, linestyle='--')
    
    
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_zticks([])
    
    ax.xaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
    ax.yaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
    ax.zaxis.line.set_color((0.0, 0.0, 0.0, 0.0))
    
    ax.xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
    ax.yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
    ax.zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
    
    plt.tight_layout()
    plt.show()
    

    Resulting in: enter image description here