Creating a 3D scatterplot with equal scale along all axes

I have a 3D scatterplot which is initialized with the following code:

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.graph_objs as go
import numpy as np

init_notebook_mode(connected=True)

x = subset.iloc[:, 0]
y = subset.iloc[:, 1]
z = subset.iloc[:, 2]

splot = go.Scatter3d(
    x=np.array(x),
    y=np.array(y),
    z=np.array(z),
    mode='markers',
    marker=dict(
        color='rgb(255, 0, 0)',
        size=1,
        symbol='circle',
        line=dict(
            color='rgb(255, 0, 0)',
            width=1
        )
    ),
    opacity=0.1
)

pdata = [splot]
layout = go.Layout(
    width=1024,
    height=1024,
    scene = dict(
        xaxis = dict(title=x.name, range = [0,6]),
        yaxis = dict(title=y.name, range = [0,6]),
        zaxis = dict(title=z.name, range = [0,6]))
)
fig = go.Figure(data=pdata, layout=layout)

iplot(fig, image='svg', filename='scatterplot.svg', image_width=1280, image_height=1280)

Although I specify the same range for all three axes in the layout’s scene, the rendered figure does not show all axes drawn on equal scale.

The axes’ ranges are correct. But the figure is squashed along the XY and XZ planes.

Are there any parameters available that can help make the figure render with equally-scaled axes?

@AlexReynolds, Your 3d plot is drawn with respect to the default layout. You can set the layout attributes to control its appearance setting the layout.scene:

scene=dict(camera=dict(eye=dict(x=1.15, y=1.15, z=0.8)), #the default values are 1.25, 1.25, 1.25
           xaxis=dict(),
           yaxis=dict(),
           zaxis=dict(),
           aspectmode=string, #this string can be 'data', 'cube', 'auto', 'manual'
           #a custom aspectratio is defined as follows:
           aspectratio=dict(x=1, y=1, z=0.95)
           )

eye gives the position of the camera eye;
aspectmode='cube', the scene’s axes are drawn as a cube, regardless of the axes’ ranges
aspectmode='data' preserves the proportion of axes ranges
'manual' when you set the aspectratio,
'auto' the scene’s axes are drawn with 'data', except when one axis is more than four times the size of the two others; in that case the 'cube' is used.

3 Likes

Adding aspectmode='cube' to the scene appears to raise a ValueError exception:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-10-611837ba12e9> in <module>()
     34         yaxis = dict(title=y.name, range = [0,6]),
     35         zaxis = dict(title=z.name, range = [0,6]),
---> 36         aspectratio='cube')
     37 )
     38 fig = go.Figure(data=pdata, layout=layout)

~/anaconda3/lib/python3.6/site-packages/plotly/graph_objs/_layout.py in __init__(self, arg, angularaxis, annotations, autosize, bargap, bargroupgap, barmode, barnorm, boxgap, boxgroupgap, boxmode, calendar, clickmode, colorway, datarevision, direction, dragmode, extendpiecolors, font, geo, grid, height, hiddenlabels, hiddenlabelssrc, hidesources, hoverdistance, hoverlabel, hovermode, images, legend, mapbox, margin, orientation, paper_bgcolor, piecolorway, plot_bgcolor, polar, radialaxis, scene, selectdirection, separators, shapes, showlegend, sliders, spikedistance, template, ternary, title, titlefont, updatemenus, violingap, violingroupgap, violinmode, width, xaxis, yaxis, **kwargs)
   3995         self['radialaxis'] = radialaxis if radialaxis is not None else _v
   3996         _v = arg.pop('scene', None)
-> 3997         self['scene'] = scene if scene is not None else _v
   3998         _v = arg.pop('selectdirection', None)
   3999         self['selectdirection'

~/anaconda3/lib/python3.6/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   3692         if match is None:
   3693             # Set as ordinary property
-> 3694             super(BaseLayoutHierarchyType, self).__setitem__(prop, value)
   3695         else:
   3696             # Set as subplotid property

~/anaconda3/lib/python3.6/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   2764             # ### Handle compound property ###
   2765             if isinstance(validator, CompoundValidator):
-> 2766                 self._set_compound_prop(prop, value)
   2767 
   2768             # ### Handle compound array property ###

~/anaconda3/lib/python3.6/site-packages/plotly/basedatatypes.py in _set_compound_prop(self, prop, val)
   3068         validator = self._validators.get(prop)
   3069         # type: BasePlotlyType
-> 3070         val = validator.validate_coerce(val, skip_invalid=self._skip_invalid)
   3071 
   3072         # Save deep copies of current and new states

~/anaconda3/lib/python3.6/site-packages/_plotly_utils/basevalidators.py in validate_coerce(self, v, skip_invalid)
   1909 
   1910         elif isinstance(v, dict):
-> 1911             v = self.data_class(skip_invalid=skip_invalid, **v)
   1912 
   1913         elif isinstance(v, self.data_class):

~/anaconda3/lib/python3.6/site-packages/plotly/graph_objs/layout/_scene.py in __init__(self, arg, annotations, aspectmode, aspectratio, bgcolor, camera, domain, dragmode, hovermode, xaxis, yaxis, zaxis, **kwargs)
   1493         self['aspectmode'] = aspectmode if aspectmode is not None else _v
   1494         _v = arg.pop('aspectratio', None)
-> 1495         self['aspectratio'] = aspectratio if aspectratio is not None else _v
   1496         _v = arg.pop('bgcolor', None)
   1497         self['bgcolor'] = bgcolor if bgcolor is not None else _v

~/anaconda3/lib/python3.6/site-packages/plotly/basedatatypes.py in __setitem__(self, prop, value)
   2764             # ### Handle compound property ###
   2765             if isinstance(validator, CompoundValidator):
-> 2766                 self._set_compound_prop(prop, value)
   2767 
   2768             # ### Handle compound array property ###

~/anaconda3/lib/python3.6/site-packages/plotly/basedatatypes.py in _set_compound_prop(self, prop, val)
   3068         validator = self._validators.get(prop)
   3069         # type: BasePlotlyType
-> 3070         val = validator.validate_coerce(val, skip_invalid=self._skip_invalid)
   3071 
   3072         # Save deep copies of current and new states

~/anaconda3/lib/python3.6/site-packages/_plotly_utils/basevalidators.py in validate_coerce(self, v, skip_invalid)
   1918                 v = self.data_class()
   1919             else:
-> 1920                 self.raise_invalid_val(v)
   1921 
   1922         v._plotly_name = self.plotly_name

~/anaconda3/lib/python3.6/site-packages/_plotly_utils/basevalidators.py in raise_invalid_val(self, v)
    242             typ=type_str(v),
    243             v=repr(v),
--> 244             valid_clr_desc=self.description()))
    245 
    246     def raise_invalid_elements(self, invalid_els):

ValueError: 
    Invalid value of type 'builtins.str' received for the 'aspectratio' property of layout.scene
        Received value: 'cube'

    The 'aspectratio' property is an instance of Aspectratio
    that may be specified as:
      - An instance of plotly.graph_objs.layout.scene.Aspectratio
      - A dict of string/value properties that will be passed
        to the Aspectratio constructor

        Supported dict properties:
            
            x

            y

            z

aspectmode should be set as 'cube', not aspectratio.

1 Like

Thanks, that works now!

Old thread but was unable to find a good solution anywhere online so sharing my workaround here.

I had a situation where it was important to have an even scale, but the range of the axes were not even, and would not be known until runtime. My solution was to add an invisible scatterplot of the (0,0,0) point and the (x_max, y_max, z_max) point. Then setting aspectmode="data". This results in the plot always being scaled correctly, and not changing shape when toggling data points from the legend.

invisible_scale = go.Scatter3d(
    name="",
    visible=True,
    showlegend=False,
    opacity=0,
    hoverinfo='none',
    x=[0,x_max],
    y=[0,y_max],
    z=[0,z_max]
)

Any word on this? Having the same issue listed above however, the aspectmode=data doesn’t appear to give me the results I am looking for. I have explained it in this stackoverflow question but I’ll restate it here.

My Code:

fig = go.Figure(
    data = go.Scatter3d(
        x=df['frame'], 
        y=df['x'], 
        z=df['y'],
        mode='markers',
        marker=dict(
            color=clusters.labels_,
            colorscale="Cividis",
            opacity=0.8
        )
    )
)
fig['layout']['scene']['aspectmode'] = "data"
fig['layout']['scene']['zaxis']['range'] = [1080, 0]
fig['layout']['scene']['yaxis']['range'] = [0, 1920]
fig.show()

My Image:

As you can see, the y axis at the bottom is extremely elongated relative to the z axis and the aspect ratio doesn’t appear to be consistent.

I have been having the same issue and it’s extremely frustrating! Here’s a minimal example with some of the data I’m working with.

import plotly.graph_objects as go

if __name__ == "__main__":

    fig = go.Figure()

    # Trajectory
    fig.add_trace(go.Scatter3d(
            x=[0.154427,0.269253,0.277201,0.295929,0.311209,0.315244,0.406273,0.463333,0.478319,0.481436],
            y=[-9.68098e-06,-4.89248e-05,-4.99059e-05,-5.01413e-05,-4.80049e-05,-4.70878e-05,-4.12876e-06,1.36401e-05,1.01835e-05,8.83736e-06],
            z=[6348.47,6499.52,6411.02,6138.39,5848.34,5761.82,3174.42,1305.1,810.247,707.661],
            mode='lines',
        ),
    )

    fig.update_layout(
        scene = dict(
            # aspectratio=dict(x=1, y=1, z=1), # <---- tried this too
            aspectmode='cube'
        ),
        template='plotly_dark',
    )
    fig.show()

My solution was by two invisible points “rgba(255, 255, 255, 0)”, one with the coordinates with the highest value obtained, and the other with the coordinates with the lowest value obtained.

Hi @antoniovandre
Thanks for sharing and welcome to the community.

Can you also please share your code?

1 Like
var trace1 = {
    x: xarr, y: yarr, z: zarr,
    mode: 'markers',
    marker: {
        size: 2,
        color: "rgba(0, 0, 0, 0.5)"},
    type: 'scatter3d'
};

var maxa = [maxvalue];

var trace2 = {
    x: maxa, y: maxa, z: maxa,
    mode: 'markers',
    marker: {
        color: "rgba(255, 255, 255, 0)"},
    type: 'scatter3d'
};

var mina = [minvalue];

var trace3 = {
    x: mina, y: mina, z: mina,
    mode: 'markers',
    marker: {
        color: "rgba(255, 255, 255, 0)"},
    type: 'scatter3d'
};

var data = [trace1, trace2, trace3];

var layout = {margin: {
        l: 0,
        r: 0,
        b: 0,
        t: 0
    },
    height: 800,
    width: 800
};

1 Like

My code is in JavaScript.