When the normalized intensity in mesh3d is close to the border of two colors in the scale, interpolation is thrown off

I am trying to make cutoff points in my color scales. I do this same strategy with heat maps to create customized color scales that combine gradients. For example, if I set two colors in the color scale to the same normalized value, then they should form a hard break in the scale.

Indeed, this works for heatmaps.

However, for meshes, it takes values near the cutoff point and somehow forms a color that should not exist in the range.

In the example below, I have a color scale that goes from green to green for normalized points between 0 → .501 and red from .501 → 1

A triangle with intensity .5 should display as green since it falls in the 0->.501 range. However, it instead is given an interpolated color of red->green.

Code: running python 3.8.12 and plotly 5.9.0

import plotly.graph_objects as go

fig = go.Figure()

x = [1, 2, 3, 1, 2, 3, 1, 2, 3]
y = [1, 1, 1, 2, 2, 2, 3, 3, 3]
z = [1, 3, 2, 1, 3, 2, 1, 3, 2]
i = [0, 3, 6]
j = [1, 4, 7]
k = [2, 5, 8]
intesity = [0.2, 0.5, 0.7]

scale = [
    [0, "rgba(0, 128, 0,1)"],
    [0.501, "rgba(0, 128, 0,1)"],
    [0.501, "rgba(255, 0, 0,1)"],
    [1, "rgba(255, 0, 0,1)"],
]
cmin = 0
cmax = 1


t1 = go.Mesh3d(
    x=x,
    y=y,
    z=z,
    i=i,
    j=j,
    k=k,
    intensity=intesity,
    intensitymode="cell",
    colorscale=scale,
    cmin=cmin,
    cmax=cmax,
)

fig.add_trace(t1)
fig.show()


Output:

This seems like an internal plotly bug. It does not affect other color scales in heat maps where I employ the same technique.

example of heat map with above approach:

Screenshot 2023-04-25 at 3.28.17 PM

Seems similar to this issue:

when you want to map an interval of real values, [a,b], onto a discrete colorscale,
then you have to normalize the interval [a, b] through the map f(t)=(t-a)/(b-a) ∈ [0,1].
If c ∈(a,b) is the break point for colors, then the discrete colorscale
is defined as follows:

dclrsc =[[0, "rgb(0, 128, 0)"], [(c-a)/(b-a), "rgb(0, 128, 0)"], [(c-a)/(b-a)], "rgb(255, 0, 0)"],
    [1, "rgb(255, 0, 0)"]]

Your interval is [a, b]=[0.2, 0.7], and the normalising function is f(t)=(t-0.2)/0.5.
f evaluated at the break point is f(0.501)=0.602

Hence your discrete colorscale should be:

dclrsc=  [[0, "rgb(0, 128, 0)"], 
          [0.602, "rgb(0, 128, 0)"], 
          [0.602, "rgb(255, 0, 0)"],
          [1, "rgb(255, 0, 0)"]]  
        
import plotly.graph_objects as go
x = [1, 2, 3, 1, 2, 3, 1, 2, 3]
y = [1, 1, 1, 2, 2, 2, 3, 3, 3]
z = [1, 3, 2, 1, 3, 2, 1, 3, 2]
i = [0, 3, 6]
j = [1, 4, 7]
k = [2, 5, 8]
intesity = [0.2, 0.5, 0.7] 
        
fig=go.Figure(go.Mesh3d(
    x=x,
    y=y,
    z=z,
    i=i,
    j=j,
    k=k,
    intensity=intesity,
    intensitymode="cell",
    colorscale=dclrsc,
))
fig.show()

I explained this algorithm, from time to time, for each user conributing to this long thread of 20 comments: Colors for discrete ranges in heatmaps

Thanks for the reply. Unfortunately your solution does not really capture the problem.

See alexcjohnson’s comment near the bottom:

Thanks @connorhazen - you’re right, there is some point at which we start to do interpolation for Mesh3d when we shouldn’t. Here’s a comparison with Scatter3d:

import plotly.graph_objects as go

x = [1, 2, 3] * 101
y = [1 + (i//3) * 0.02 for i in range(303)]
z = [1, 3, 2] * 101
i = [i * 3 for i in  range(101)]
j = [i * 3 + 1 for i in  range(101)]
k = [i * 3 + 2 for i in  range(101)]

intensity = [0.490 + (i * 0.0002) for i in range(101)]

scale = [
    [0, "rgba(0, 128, 0,1)"],
    [0.501, "rgba(0, 128, 0,1)"],
    [0.501, "rgba(255, 0, 0,1)"],
    [1, "rgba(255, 0, 0,1)"],
]
cmin = 0
cmax = 1


t = go.Mesh3d(
    x=x,
    y=y,
    z=z,
    i=i,
    j=j,
    k=k,
    intensity=intensity,
    intensitymode="cell",
    colorscale=scale,
    cmin=cmin,
    cmax=cmax,
    lighting={"vertexnormalsepsilon": 0, "facenormalsepsilon": 0},
    facecolor=["red", "green", "blue"],
)

t2 = go.Scatter3d(
    x=[1]*101,
    y=[1 + (i * 0.02) for i in range(101)],
    z=[3] * 101,
    marker=dict(color=intensity, cmin=cmin, cmax=cmax, colorscale=scale)
)

fig = go.Figure()
fig.add_trace(t)
fig.add_trace(t2)
fig.show()

Where we can see Scatter3d abruptly changes from green to red, but for Mesh3d there’s a transition region:
Screenshot 2023-04-26 at 11 22 44
My assumption is we’re discretizing the colorscale for Mesh3d so as soon as you get down within one increment you see this gradation. If that’s the case it may take a substantial rewrite to do better, which may bring with it a performance penalty.

This is an example to illustrate that with a discrete colorscale, defined according to data,
Mesh3d, with intensitymode="cell", doesn’t interpolate the neighgboring colors.

import plotly.graph_objects as go
import numpy as np

def discrete_colorscale(bvals, colors):
    """
    bvals - list of values bounding intervals/ranges of interest
    colors - list of rgb or hex colorcodes for values in [bvals[k], bvals[k+1]],0<=k < len(bvals)-1
    returns the plotly  discrete colorscale
    """
    if len(bvals) != len(colors)+1:
        raise ValueError('len(boundary values) should be equal to  len(colors)+1')
    bvals = sorted(bvals)     
    nvals = [(v-bvals[0])/(bvals[-1]-bvals[0]) for v in bvals]  #normalized values
    
    dcolorscale = [] #discrete colorscale
    for k in range(len(colors)):
        dcolorscale.extend([[nvals[k], colors[k]], [nvals[k+1], colors[k]]])
    return dcolorscale 

verts = np.array([[0, 0, 0],
        [1, 0, 0],
        [1,1, 0],
        [0, 1, 0],
        [0,0,1],
        [1, 0, 1],
        [1,1,1], 
        [0, 1, 1],
        ], dtype=float)

triangles = np.array([[0, 1, 2], 
                      [0,2,3], 
                      [6, 2, 3], 
                      [6, 3, 7],
                      [5, 1, 0], 
                      [5, 0, 4], 
                      [5, 1, 2], 
                      [5, 2, 6], 
                      [4, 0, 3], 
                      [4, 3, 7], 
                      [4, 5, 6], 
                      [4, 6, 7]],
                      dtype=int)

intensity = [0.5, 1.0, 1.5, 2, 3, 4.5, 5.75, 0.5, 1.0, 1.5, 2, 3, 4.5, 5.75]
uintens=np.unique(intensity)

colors= [
"#680003",
"#BC0000",
"#F5704A",
"#EFB9AD",
"#828D00",
"#BB35AE"
]
dcolorscale= discrete_colorscale(uintens, colors)

x, y, z= verts.T
i,j, k=triangles.T
fig=go.Figure(go.Mesh3d(x=x, y=y, z=z, 
                        i=i, j=j, k=k, 
                        intensity = intensity, 
                        intensitymode="cell", 
                        colorscale=dcolorscale, 
                       ))
fig.update_layout(width=450, height=450)
fig.show()

Any colorscale not defined by this algorithm works as a continuous colorscale, and as alexcjohnson pointed out, an interpolation occurs. The original poster considered
his colorscale as being discrete, but it was constructed arbitrarily,
with no connecion to intensity values 0.2, 0.5, 0.7,
(or the boundary values, as in the function defined above).
I leave his example here for users interested in discrete colorscales to be used with intensitymode="cell".

1 Like

Once again thank you for the reply.

HOWEVER, I would suggest re-reading the problem as I have laid it out. I appreciate the ‘solution’ but it does not solve this bug nor does it acknowledge the actual issue with Plotly. The example I gave was to highlight the bug conditions, I was not looking for a way to color a triangle green, that is trivial and not the point.

In case this is still confusing, test with intensity values [0, .499, .501, 1]

Also, when the CTO of plotly (alexcjohnson) acknowledges the problem, I would generally take a second to make sure you grasp it before replying with a ‘solution’

@connorhazen I saw @alexcjohnson’s comment after I had pasted the code. As I told you I did not watch the discussion, because I worked on setting up the cube triangulation. Do you think he is “firing” me for posting that example? :grinning:
I was convinced that putting any values as boundary value in the discrete colorscale definition should work because of its normalized value repetition in the scale.
I worked very much with 3d meshes, as well as wih discrete colorscales, but did not notice such an anomaly.

1 Like

@empet Thank you for all the help. I really do appreciate it, I did not intend to sound disparaging but I recognize I may have come across that way. I apologize.

Anyway, very strange anomaly. Did you have a chance to see the code I posted about doing the color mapping ‘manually’ and using as surface color? If you have any advice for a better solution, I would love to hear it. Or if you see a more performant method.

Any chance this would be a patch at somepoint down the line?

1 Like

In general I assume @empet knows far more than I do about our 3D traces so it’s nice to know we can still find something she hasn’t tried :sweat_smile:
@connorhazen thanks for pointing this bug out, and for being persistent until we understood what you were getting at! As I said in the issue I suspect a “real” fix will be difficult. I could imagine a mode that tells plotly.js not to interpolate, and then to adjust the color bin boundaries to try and match any discrete breakpoints in the colorscale… but that sounds a little fragile to me. If anyone else reading this has ideas (and ideally some appetite to dig into the WebGL code behind this :pray: ) I’d love to hear it!

1 Like