Correct use of coordinates to plot surface data with Plotly?

Viewed 268

Suppose I have an array X of x values and Y of y values, and a dictionary results, such that results[x][y] accesses a z value. How would I construct a z variable do correctly match the coordinates that are generating it?

For instance:

X = [0,2,3]
Y = [-1,-2,-3]
results = {x:{y: x/y for y in Y} for x in X}

Z = np.array([[results[x][y] for y in Y] for x in X ])
fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y)])
fig.update_layout(title='Plot', autosize=False,
                  width=500, height=500,
                  margin=dict(l=65, r=50, b=65, t=90))
fig.update_layout(scene = dict(xaxis_title='X',
                    yaxis_title='Y'
                              )
                 )
fig.show()

gives a wrong graph:

enter image description here

How does this coordinate system work? Doing Z = np.array([[results[x][y] for x in X] for y in Y ]) gives the correct result, but this doesn't make sense to me. Why is x the second coordinate?

1 Answers

The problem

The reason you're getting weird results is due to how computers represent matrices, which is by rows and columns where the "first" element, at the index 0, 0 is at the top-left corner, whereas it's more intuitive to be thinking of the XY-plane where bottom-left corner is the origin, which is the standard way to view graphs in mathematics.

Solution

First, by taking full advantage of numpy, you can compute z much quicker via np.divide.outer(). This gives the correct result for z without the unnecessary dict and nested comprehension (and will be WAY more efficient for larger arrays).

x = np.array([0, 2, 3])
y = np.array([-1, -2, -3])
z = np.divide.outer(x, y)

Now since computers see matrices differently than what is intuitively viewed as z = f(x, y), you need to plot the transpose of z:

fig = go.Figure(data=[go.Surface(z=z.T, x=x, y=y)])

Result:

Slightly more detailed explanation

If we orient the plot such that we're looking at the x-axis oriented how we normally view it on 2D graphs (increasing from left to right):

You can see that on the left edge of the surface, z, all the values are zero. But notice how z is represented as an array:

>>> np.round(z, 2)
array([[-0.  , -0.  , -0.  ],
       [-2.  , -1.  , -0.67],
       [-3.  , -1.5 , -1.  ]])

If you transpose z, you'll see that the "left" edge of the matrix is all zeros:

>>> np.round(z.T, 2)
array([[-0.  , -2.  , -3.  ],
       [-0.  , -1.  , -1.5 ],
       [-0.  , -0.67, -1.  ]])

This hopefully helps shine some light on the difference between xy-coordinates and the row-column representation of arrays. Something else to note is that the y-axis is "reversed" in the reoriented plot above. This is because the top row of a matrix is considered the "first" element, but in a normal xy-coordinate system, the y-axis increases going vertically (i.e., starting at the bottom).

Using a meshgrid for multivariate functions

Something you can do to avoid having to transpose z is using np.meshgrid():

>>> x_coords, y_coords = np.meshgrid(x, y, indexing="xy")
>>> z = x_coords / y_coords
>>> z
array([[-0.  , -2.  , -3.  ],
       [-0.  , -1.  , -1.5 ],
       [-0.  , -0.67, -1.  ]])

Notice now that z is already in the proper format for plotting as a surface. Note that this also gets rid of the necessity of np.outer(). Using a meshgrid is also much better for evaluating arbitrary functions on a coordinate grid.

Related