Hover background color on scatter 3d

On the demo page:

The second graph (3D Scatter Plot with Colorscaling) uses the viridis colorscale. Yet the hover background color is always the same. Is this on purpose?

Similarly, setting individual colors on a scatter 3d will only use one color. But when using a 2d scatter, the hover background matches the data marker color. Again, wondering if this is by design.

If it is by design, is there a way to have the 3d hover background color match the marker color?

Thanks

Any thoughts? Anybody has a suggestion to fix this?

@franky If you know the color code for each point in a Scatter3d instance, define the list of those color codes and the trace3d, as follows:

trace3d=dict(type='scatter3d',
             x=mydata[:,0],
             y=mydata[:,1],
             z=mydata[:,2],
             mode='markers',
             marker=dict(size=6, color=list_of_point_colors, line=dict(width=0.5, color='rgb(100,100,100)')),
             text=tooltips,#tooltips is a list of strings to be displayed on hover
             hoverinfo='text',
             hoverlabel=dict(bgcolor=list_of_point_colors)   
            )
1 Like

Iโ€™m encountering this issue as well with a very large dataset.

For all other plot types, the hovertext background color source is automatically updated to the marker color type. For the scatter3d plot type, it seems to be fixed to the first marker drawn. Given that Iโ€™m using dynamically scaled marker colors, is there a way to reference back to the marker.color property?

Thanks!

toy example showing issueโ€ฆ

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

df = pd.DataFrame(
    {
        'x': [3, 5, 6, 7],
        'y': [2, 1, 5, 3],
        'z': [3, 2, 6, 3],
        'size': [40, 30, 20, 10],
        'color': [-3, 3, 5, 6]},
        index=[0, 1, 2, 3])   
 
fig = go.Figure(
    data=[
        go.Scatter3d(
            x=df["x"],
            y=df["y"],
            z=df["z"],
            mode="markers",
            marker=dict(
                symbol="circle",
                size = df['size'],
                sizemode="diameter",
                color=df['color'],
                opacity=0.6,
                colorscale="Jet",
                line_width=0,
                showscale=True,
            ),
        )
    ]
)
fig.show()

@empet Iโ€™m encountering the same issue in my project: https://github.com/giotto-ai/giotto-tda/pull/406#issuecomment-632168264

So far, our workaround (which might be of interest to @sstraka) has been to use matplotlib's get_cmap and rgb2hex to recreate the interpolation according to the marker colorscale and colors. But this introduces an extra dependency for our project in matplotlib, which is not otherwise needed.

I have not been able to find a plotly.py function responsible for the interpolation according to a colorscale. Might it be that this is done by the plotly.js components e.g. in a jupyter notebook context? If so, do you think there can be a workaround requiring no external dependencies?

Thanks in advance!

@umberto.lupo

A workaround is to define a function that maps a value in an interval [vmin,vmax] to the corresponding color in a plotly colorscale.

Below is defined a function for Plotly colorscales with rgb color codes. A few lines must be added for hex colors:

from plotly.colors import * #https://github.com/plotly/plotly.py/tree/master/packages/python/plotly/_plotly_utils/colors

from ast import literal_eval
import numpy as np

def get_color_for_val(val, vmin, vmax, pl_colors):
    
    if pl_colors[0][:3] != 'rgb':
        raise ValueError('This function works only with Plotly  rgb-colorscales')
    # to do: add lines for hex to rgb   
    if vmin >= vmax:
        raise ValueError('vmin should be < vmax')
    
    scale = [k/(len(pl_colors)-1) for k in range(len(pl_colors))] 
    colors_01 = np.array([literal_eval(color[3:]) for color in pl_colors])/255.  #color codes in [0,1]
    v= (val - vmin) / (vmax - vmin) # val is mapped to v in [0,1]
    #find two consecutive values in plotly_scale such that   v is in  the corresponding interval
    idx = 1
    while(v > scale[idx]): #sequential searching of the interval
        idx += 1
    vv = (v - scale[idx-1]) / (scale[idx] -scale[idx-1] )
    
    #get   [0,1]-valued color code representing the rgb color corresponding to val
    val_color01 = colors_01[idx-1] + vv * (colors_01[idx ] - colors_01[idx-1])
    val_color_0255 = (255*val_color01+0.5).astype(int)
    return f'rgb{tuple(val_color_0255)}'

#####################
import plotly.graph_objects as go

pl_colors =  cmocean.deep[::-1]  #define the deep colorscale
np.random.seed(123)
color_vals= 1+4*np.random.rand(20)
vmin= color_vals.min()
vmax=color_vals.max()
bgcolor = [get_color_for_val(v, vmin,vmax,pl_colors) for v in color_vals]

fig= go.Figure(go.Scatter3d(x=np.random.randint(2, 10, 20), 
                            y=np.random.randint(2, 10, 20),
                            z=np.random.randint(2, 10, 20),
                            mode='markers', marker_size=8,
                            marker_color=color_vals,
                            marker_colorscale='deep_r',
                            text =list(range(20)),
                            hoverinfo='text',
                            hoverlabel_bgcolor=bgcolor))
1 Like

@empet many thanks for the quick and very helpful reply!

The solution looks good in the example you provide. Iโ€™ll try to figure out how to deal with hex colorscales (e.g. Viridis which is quite popular), and post a code snippet here when I manage.

By the way, out of academic curiosity at this point: could you confirm/correct my working theory that the ultimate mapping to individual colors for each marker (presumably by an interpolation similar to the one you provided) is performed by javascript components and not python ones? Sorry if this question is ill-formed.

@umberto.lupo

Yes, you are right!! :slight_smile:

1 Like

@umberto.lupo

I edited the last line from function definition:

  return f'rgb{tuple(val_color_0255)}'

@empet @sstraka

I came up with a vectorised version of empetโ€™s answer and allowed also for hex colors input/output. I consistently observe ~50-100x speedup in examples (small or large), relative to the initial solution.

def hex_to_rgb(value):
    """Convert a hex-formatted color to rgb, ignoring alpha values."""
    value = value.lstrip("#")
    return [int(value[i:i + 2], 16) for i in range(0, 6, 2)]


def rbg_to_hex(c):
    """Convert an rgb-formatted color to hex, ignoring alpha values."""
    return f"#{c[0]:02x}{c[1]:02x}{c[2]:02x}"


def get_colors_for_vals(vals, vmin, vmax, colorscale, return_hex=True):
    """Given a float array vals, interpolate based on a colorscale to obtain
    rgb or hex colors. Inspired by
    `user empet's answer in \
    <community.plotly.com/t/hover-background-color-on-scatter-3d/9185/6>`_."""
    from numbers import Number
    from ast import literal_eval

    if vmin >= vmax:
        raise ValueError("`vmin` should be < `vmax`.")

    if (len(colorscale[0]) == 2) and isinstance(colorscale[0][0], Number):
        scale, colors = zip(*colorscale)
    else:
        scale = np.linspace(0, 1, num=len(colorscale))
        colors = colorscale
    scale = np.asarray(scale)

    if colors[0][:3] == "rgb":
        colors = np.asarray([literal_eval(color[3:]) for color in colors],
                            dtype=np.float_)
    elif colors[0][0] == "#":
        colors = np.asarray(list(map(hex_to_rgb, colors)), dtype=np.float_)
    else:
        raise ValueError("This colorscale is not supported.")

    colorscale = np.hstack([scale.reshape(-1, 1), colors])
    colorscale = np.vstack([colorscale, colorscale[0, :]])
    colorscale_diffs = np.diff(colorscale, axis=0)
    colorscale_diff_ratios = colorscale_diffs[:, 1:] / colorscale_diffs[:, [0]]
    colorscale_diff_ratios[-1, :] = np.zeros(3)

    vals_scaled = (vals - vmin) / (vmax - vmin)

    left_bin_indices = np.digitize(vals_scaled, scale) - 1
    left_endpts = colorscale[left_bin_indices]
    vals_scaled -= left_endpts[:, 0]
    diff_ratios = colorscale_diff_ratios[left_bin_indices]

    vals_rgb = (
            left_endpts[:, 1:] + diff_ratios * vals_scaled[:, np.newaxis] + 0.5
    ).astype(np.uint8)

    if return_hex:
        return list(map(rbg_to_hex, vals_rgb))
    return [f"rgb{tuple(v)}" for v in vals_rgb]

#####################
import plotly.graph_objects as go

pl_colors = cmocean.deep[::-1]
np.random.seed(123)
color_vals = 1 + 4 * np.random.rand(20)
vmin = color_vals.min()
vmax = color_vals.max()
bgcolor = get_colors_for_vals(color_vals, vmin, vmax, pl_colors)

fig= go.Figure(go.Scatter3d(x=np.random.randint(2, 10, 20), 
                            y=np.random.randint(2, 10, 20),
                            z=np.random.randint(2, 10, 20),
                            mode='markers', marker_size=8,
                            marker_color=color_vals,
                            marker_colorscale='deep_r',
                            text=list(range(20)),
                            hoverinfo='text',
                            hoverlabel_bgcolor=bgcolor))
3 Likes