✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
🏦 Standard & Poor's chooses Dash Enterprise for ESG analysis. Learn why, sign up for the June 23 Webinar here!

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.

2 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]
)