11

I'm trying to plot several surfaces, each of a different color, in Plotly for Python.

Specifically, a surface shows the predicted reward function for taking an action at different points in phase space. Since I have several possible actions at each point, each is a different surface. I'd like to color each surface uniquely, but independent of the x,y, or z coordinate.

I've tried to follow answer in R, but I can't figure out what I've done wrong. I always get the same blue color. Since I'm using PyPlot in other parts of my code, I'm choosing colors from the default matplotlib tableau.

Here's a basic example with toy data.

import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objs as go
import plotly.offline as off

off.init_notebook_mode()

make_int = np.vectorize(int)
cmap = plt.get_cmap("tab10")

saddle = np.array([[x**2-y**2 for x in np.arange(-10,11)] for y in np.arange(-10,11)])
paraboloid = np.array([[x**2 + y**2-100 for x in np.arange(-10,11)] for y in np.arange(-10,11)])

mycolors_a = make_int(256*np.array(cmap(1)[0:3])).reshape((1, 1,-1)).repeat(21, axis = 0).repeat(21, axis =1)
mycolors_b = make_int(256*np.array(cmap(2)[0:3])).reshape((1, 1,-1)).repeat(21, axis = 0).repeat(21, axis =1)
trace_a = go.Surface(z = saddle, surfacecolor = mycolors_a, opacity = .7, showscale = False, name = "Trace A")
trace_b = go.Surface(z = paraboloid, surfacecolor = mycolors_b, opacity = .7, showscale = False, name = "Trace B")

data = [trace_a, trace_b]
off.iplot(data)

Produces the following:

Output Sample

I should see a blue saddle and an orange paraboloid, but I don't. Note that even if I change the argument to cmap, I always get the same blue color. Thanks for your help!

Jake Stevens-Haas
  • 1,186
  • 2
  • 14
  • 26

2 Answers2

12

The documentation is a bit cryptic here.

surfacecolor

(list, numpy array, or Pandas series of numbers, strings, or datetimes.)

Sets the surface color values, used for setting a color scale independent of z.

I never managed to put a list of strings, i.e. color values like 'rgb(0.3, 0.5, 0)', or RGB tuples in it.

But you can define your own color scale with the needed colors.

colorscale = [[0, 'rgb' + str(cmap(1)[0:3])], 
              [1, 'rgb' + str(cmap(2)[0:3])]]

and then provide a numeric array with the same dimensions as your plotted values.

colors_saddle = np.zeros(shape=saddle.shape)    

All values are set to 0 and will therefore map to the first color in your colorscale. The same for the next color.

In addition you need to set cmax and cmin manually.

Complete code

import numpy as np
import matplotlib.pyplot as plt
import plotly.graph_objs as go
import plotly.offline as off


off.init_notebook_mode()

make_int = np.vectorize(int)
cmap = plt.get_cmap("tab10")

saddle = np.array([[x**2-y**2 for x in np.arange(-10,11)] for y in np.arange(-10,11)])
paraboloid = np.array([[x**2 + y**2-100 for x in np.arange(-10,11)] for y in np.arange(-10,11)])

colors_saddle = np.zeros(shape=saddle.shape)    
colors_paraboloid = np.ones(shape=paraboloid.shape)    

colorscale = [[0, 'rgb' + str(cmap(1)[0:3])], 
              [1, 'rgb' + str(cmap(2)[0:3])]]

trace_a = go.Surface(z=saddle, 
                     surfacecolor=colors_saddle, 
                     opacity=.7, 
                     name="Trace A",
                     cmin=0,
                     cmax=1,
                     colorscale=colorscale)
trace_b = go.Surface(z=paraboloid, 
                     surfacecolor=colors_paraboloid, 
                     opacity=.7, 
                     name="Trace B", 
                     cmin=0,
                     cmax=1,
                     showscale=False,
                     colorscale=colorscale)

data = [trace_a, trace_b]
off.iplot(data)

enter image description here

Maximilian Peters
  • 30,348
  • 12
  • 86
  • 99
  • Thanks! That's a great example. I'm not sure this warrants a separate question, but do you know why Plotly doesn't display overlaps correctly with transparency? It seems to suffer from the same problems as matplot3d. That is, even at 99% opacity, whichever trace is added to data last appears in front. Only at 100% opacity do the overlaps show correctly – Jake Stevens-Haas Jan 02 '19 at 16:19
  • 1
    @JakeStevens-Haas: No idea. I'd guess it would be best to open an issue on Github. – Maximilian Peters Jan 02 '19 at 17:45
0

You can combine all surfaces in one and set in colorscale range for each surface It can also resolve overlapping problem, so you would see the line of surfaces intersection clearly like here

    import numpy as np
    import plotly.graph_objs as go

    # normalize values to range [start,end] for getting color from cmap
    def norm_v_in_range(v,start,end):
        v_min = v.min()
        v_max = v.max()
        range_length = (end - start)

        if v_min-v_max == 0 :
            v.fill(range_length/5 + start)
            return v

        return (v-v_min)/(v_max-v_min)*range_length + start

    def combine_all_surfaces_in_one(X,Y,*Z) :

        # prepare colors and ranges for diffrent surfaces
        colors = [  'rgb(180, 110,  20)', 'rgb( 20, 180, 110)', 'rgb(110, 20, 180)',
                    'rgb(180, 180,  20)', 'rgb( 20, 180, 180)', 'rgb(180, 20, 180)',
                    'rgb(180,  20,  20)', 'rgb( 20, 180,  20)', 'rgb( 20, 20, 180)',
                    'rgb(180, 110,  20)', 'rgb( 20, 180, 110)', 'rgb(110, 20, 180)',
                    'rgb(255, 127, 127)', 'rgb(127, 255, 127)']

        N = len(Z)
        points = np.linspace(0, 1, N + 1)
        custom_colorscale = []
        ranges = []

        for i in range(1,N+1) :
            ranges.append([points[i-1],points[i]-0.05])
            custom_colorscale.append([points[i-1], colors[i]])
            custom_colorscale.append([points[i]-0.05,'rgb(255, 250, 220)'])
        custom_colorscale.append([1, 'rgb(220, 250, 220)'])


        # transparent connection between grahps: np.nan in z prevent ploting points
        transparen_link = np.empty_like(X[0], dtype=object)
        transparen_link.fill(np.nan)

        # include first graph
        combined_X = X
        combined_Y = Y
        combined_Z = Z[0]

        # prepare collor matrix for first graph (Z[0])
        start = ranges[0][0]
        end = ranges[0][1]

        custom_surfacecolor = norm_v_in_range(Z[0],start,end)

        # second aray combined with first in backward direction, so connection would on one side of graphs, not intersect them 
        direction = -1

        range_index = 1

        for next_Z in Z[1:] :

            combined_X = np.vstack([combined_X, combined_X[-1], X[::direction][0], X[::direction][0], X[::direction]])
            combined_Y = np.vstack([combined_Y, combined_Y[-1], Y[::direction][0], Y[::direction][0], Y[::direction]])
            combined_Z = np.vstack([combined_Z, combined_Z[-1], transparen_link, next_Z[::direction][0], next_Z[::direction]])

            # prepare collors for next Z_ 
            start = ranges[range_index][0]
            end = ranges[range_index][1]
            next_surfacecolor = norm_v_in_range(next_Z,start,end)
            custom_surfacecolor = np.vstack([custom_surfacecolor,custom_surfacecolor[-1], transparen_link, next_surfacecolor[::direction][0], next_surfacecolor[::direction]])

            # change direction
            direction *= -1

            range_index += 1

        return combined_X, combined_Y, combined_Z, custom_surfacecolor, custom_colorscale



    X = np.arange(-1.2, 1.06, 0.1)
    Y = np.arange(0.2, 1.06, 0.1)
    X, Y = np.meshgrid(X, Y)


    Z1 = 2*np.sin(np.sqrt(20*X**2+20*Y**2))
    Z2 = 2*np.cos(np.sqrt(20*X**2+20*Y**2))
    Z3 = X*2+0.5
    Z4 = Y*0+1.0
    Z5 = Y*0-1.0
    Z6 = Y*0+0.0
    x,y,z,custom_surfacecolor,custom_colorscale = combine_all_surfaces_in_one(X,Y,Z1,Z2,Z3,Z4,Z5)

    # opacity =0.9 - many overlaped areas, better witot it
    fig = go.Figure(data=[go.Surface(x=x, y=y, z=z, 
                                     surfacecolor=custom_surfacecolor, cmin=0, cmax = 1,
                                     colorscale=custom_colorscale,showscale=False,
                                     )] )

    fig.show()