Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

incorrect representation of mixed rgb and rgba colors in mesh3d #2306

Open
z--m-n opened this issue Mar 23, 2020 · 20 comments
Open

incorrect representation of mixed rgb and rgba colors in mesh3d #2306

z--m-n opened this issue Mar 23, 2020 · 20 comments
Labels
bug something broken P3 backlog

Comments

@z--m-n
Copy link

z--m-n commented Mar 23, 2020

Hi folks!

I encountered some unexpected behavior in Mesh3d. I hope you could have a look. It is hard for me to judge if these are issues, here or upstream, or I am missing something.

image

  1. Incorrect colors in plots with intensity and a discontinuous colorscale (see panel f2).
    a) Colors with opacity (rgba) are shown without opacity (rgb). This issue was reported before.
    b) Discontinuous transitions in the colorscale appear to be ignored. As a result, colors are introduced that are not found in the colorscale. In the example, panel f2: purple = black + magenta, raspberry = magenta + orange.
    c) I would expect the same colorscale behavior as in 'scatter' or 'heatmap' (panel f1).

  2. Render artifacts occur for facecolor or vertexcolor surfaces, when rgb and rgba colors are put in the same trace (see panel f3).
    a) Surfaces do look opaque when only rgb colors are present (panel f5).
    b) A partial workaround was to plot rgb and rgba vertices in separate traces (panel f6). Not sure what to do when vertices have a gradient between rgb and rgba...

  3. An additional trace is needed to add a colorbar when facecolor or vertexcolor is used instead of intensity.
    a) I do expect the colorbar to be dropped when colors are hardcoded. But the workaround to add a custom colorbar or coloraxis with a 'hidden' trace may not be obvious to everyone. Add an example to the documentation?

  4. The rgba colors in the colorbar appear mixed with layout paper_bgcolor, effectively the transparent magenta colors are too dark in the Working Example.
    a) Can a colorbar background be set to match the (default) plot background color?

  5. The image above is a screenshot and differs from the exported image, using f7.write_image(...) after the Working Example below.
    a) In my exported copy, some of the layout scene axes appeared 'ghost'-duplicated into the next subplot. Only visible if the scene axes (ranges) change between subplots. Issue on my system or in Orca?

Screenshot image vs Orca[1.3.0] image

Finally, for a large number of shapes (or animations) I noticed significant performance loss in looking up and hardcoding the colors of vertices. Ideally, I would get the intensity method of Mesh3d to give me the expected result (more like f6, less like f2). Any ideas? Your advise is appreciated!

import numpy as np
import itertools
import plotly as py
import plotly.graph_objects as go
from plotly.subplots import make_subplots

def flatten_dict(d):
    return({k: [y for x in [v[k] for v in d] for y in x] for k in d[0]})

def reindex_voxels(dl):
    m=0
    res = []
    for i,od in enumerate(dl):
        ix = len(dl[i]['x'])
        d = od.copy()
        for n in ['i','j','k']:
            d[n] = [ m + j for j in d[n] ]
        m += ix
        res.append(d)
    return(flatten_dict(res))

# some data
x = y = np.linspace(0,1,3)
x,y = np.meshgrid(x,y)
z = 1-(x+y)/2 

# custom colorscale, mixed rgb and rgba, discontinuous breaks
cs = [[0.0, 'rgb(64, 64, 64)'],
    [0.25, 'rgb(64, 64, 64)'],
    [0.25, 'rgba(255, 0, 255, 0.1)'],
    [0.5, 'rgba(255, 0, 255, 0.5)'],
    [0.5, 'rgb(255, 160, 33)'],
    [0.8, 'rgb(255, 255, 129)'],
    [1.0, 'rgb(255, 255, 225)']]

# list of shapes
vxl = \
[{'x': [-0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25],
  'y': [-0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25],
  'z': [0.875, 0.875, 0.875, 0.875, 1.125, 1.125, 1.125, 1.125],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]},
 {'x': [0.25, 0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75],
  'y': [-0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25],
  'z': [0.625, 0.625, 0.625, 0.625, 0.875, 0.875, 0.875, 0.875],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]},
 {'x': [0.75, 0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25],
  'y': [-0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25],
  'z': [0.375, 0.375, 0.375, 0.375, 0.625, 0.625, 0.625, 0.625],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]},
 {'x': [-0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25],
  'y': [0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75, 0.25],
  'z': [0.625, 0.625, 0.625, 0.625, 0.875, 0.875, 0.875, 0.875],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]},
 {'x': [0.25, 0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75],
  'y': [0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75, 0.25],
  'z': [0.375, 0.375, 0.375, 0.375, 0.625, 0.625, 0.625, 0.625],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]},
 {'x': [0.75, 0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25],
  'y': [0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75, 0.25],
  'z': [0.125, 0.125, 0.125, 0.125, 0.375, 0.375, 0.375, 0.375],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]},
 {'x': [-0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25],
  'y': [0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25, 0.75],
  'z': [0.375, 0.375, 0.375, 0.375, 0.625, 0.625, 0.625, 0.625],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]},
 {'x': [0.25, 0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75],
  'y': [0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25, 0.75],
  'z': [0.125, 0.125, 0.125, 0.125, 0.375, 0.375, 0.375, 0.375],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]},
 {'x': [0.75, 0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25],
  'y': [0.75, 1.25, 1.25, 0.75, 0.75, 1.25, 1.25, 0.75],
  'z': [-0.125, -0.125, -0.125, -0.125, 0.125, 0.125, 0.125, 0.125],
  'i': [7, 0, 0, 0, 4, 4, 2, 6, 4, 0, 3, 7],
  'j': [3, 4, 1, 2, 5, 6, 5, 5, 0, 1, 2, 2],
  'k': [0, 7, 2, 3, 6, 7, 1, 2, 5, 5, 7, 6],
  'intensity': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}]

# shape colors
cid = ['rgb(255, 255, 220)','rgb(255, 239, 113)', 
       'rgb(255, 160, 33)','rgb(255, 239, 113)',
       'rgb(255, 160, 33)','rgba(255, 0, 255, 0.2)',
       'rgb(255, 160, 33)','rgba(255, 0, 255, 0.2)', 
       'rgb(64, 64, 64)']

# concatenate shapes
vxa = reindex_voxels(vxl)

# concatenate and replace 'intensity' with 'vertexcolor'
vxb = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                       **{'vertexcolor': [k]*len(v['x'])}} for k,v in zip(cid,vxl)])

# split vertices with opacity (rgba) and without opacity (rgb) , and replace 'intensity' by 'vertexcolor'
vxc0 = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                        **{'vertexcolor': [k]*len(v['x'])}} for k,v in zip(cid,vxl) if k.startswith('rgba(')])
vxc1 = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                        **{'vertexcolor': [k]*len(v['x'])}} for k,v in zip(cid,vxl) if k.startswith('rgb(')])

# figures
f1 = go.Figure(data=[go.Heatmap(x=x.ravel(),y=y.ravel(),z=z.ravel(),colorscale=cs)])
f2 = go.Figure(data=[go.Mesh3d(**vxa,colorscale=cs)])
f3 = go.Figure(data=[go.Mesh3d(**vxb,colorscale=cs)])
f4 = go.Figure(data=[go.Mesh3d(**vxc0)])
f5 = go.Figure(data=[go.Mesh3d(**vxc1)])

# multiple traces; 'vertexcolor' for rgb and rgba _and_ a transparent trace with 'intensity' to add a 'colorscale'
f6 = go.Figure(data=[go.Mesh3d(**vxc0),
                     go.Mesh3d(**vxc1),
                     go.Mesh3d(**vxa,opacity=0,colorscale=cs)])

#f6.show()  

# merge figures
ncol=2
nrow=3
fig = make_subplots(cols=ncol,rows=nrow,vertical_spacing=0.05,horizontal_spacing=0.02,
                    specs= ( np.array([{'type':'heatmap'},{'type':'scatter3d'}]+[{'type':'scatter3d'}]*4)
                            .reshape(nrow,ncol).tolist() ),
                    subplot_titles=['f1: <b>Heatmap</b>; correct',
                                    'f2: <b>Mesh3d</b> intensity; incorrect rgb & rgba',
                                    'f3: <b>Mesh3d</b> vertexcolor; render artifacts in rgb',
                                    'f4: <b>Mesh3d</b> vertexcolor; correct for only rgba',
                                    'f5: <b>Mesh3d</b> vertexcolor; correct for only rgb',
                                    'f6: <b>Mesh3d</b> vertexcolor; separate traces for rgb & rgba'])
fig.update_layout(height=1200,width=1000,title_text='3D representation of mixed rgb and rgba colors [plotly.py={}]'.format(py.__version__))

f7 = go.Figure(fig)
for loc,f in zip(list(itertools.product(*[range(1,nrow+1),range(1,ncol+1)])),[f1,f2,f3,f4,f5,f6]):
    if f is not None:
        for d in f['data']:
            f7.add_trace(d,row=loc[0],col=loc[1])              

f7.show()

Sorry, not a very short MWE...

@empet
Copy link

empet commented Mar 23, 2020

Mesh3d has the property, intensitymode, that can be set as 'vertex' (the default case) or 'cell'.
In the former case the intensity values are assigned to vertices, and len(intensity)=len(x), while in latter, to faces, and len(intensity)=len(i) (see https://plot.ly/~empet/15416).
In both cases the intensity values are colormapped to a colorscale..
In your examples the intensitymode='vertex', and in this case the colors in colorscale, assigned to vertices of a face, are interpolated (see the middle hand at the above link).
That's why your discrete colorscale does not color your voxels as expected.
I'm not sure whether a colorscale of mixed color codes (rgb and rgba) works/is allowed in a Mesh3d. @archmoj could you please clarify this aspect?
I suppose that you wanted to assign different colors and opacities to your voxels. In this case it's better to set facecolor, instead of intensity and a colorscale.
facecolor is a list of rgb and rgba color codes, of length equal to the number of mesh faces (triangles).

@archmoj
Copy link
Contributor

archmoj commented Mar 23, 2020

@empet at the moment Mesh3d does not support opacityscale similar to volume and isosurface. But you could open an issue for it, if that's something useful. Also to note the next version of plotly.js would enable opacityscale feature for surface plots. See plotly/plotly.js#4480.

@z--m-n
To avoid undesirable auto-smoothing on mesh3d object and speed up drawing, I suggest using a setup like this:

"flatshading": true,
"lighting": {
    "vertexnormalsepsilon": 0,
    "facenormalsepsilon": 0
},

which is applied in this demo: https://rreusser.github.io/plotly-mock-viewer/#gl3d_bunny_cell-area

@archmoj
Copy link
Contributor

archmoj commented Mar 23, 2020

One more note related to exporting trasparent 3-D plots.
The downloadImage would be fixed to match the live graph, when plotly.js v1.53.0 is out.
See plotly/plotly.js#4566.

@z--m-n
Copy link
Author

z--m-n commented Mar 23, 2020

I suppose that you wanted to assign different colors and opacities to your voxels. In this case it's better to set facecolor, instead of intensity and a colorscale.
facecolor is a list of rgb and rgba color codes, of length equal to the number of mesh faces (triangles).

Okay, I will play with that. Please note that the output of the MWE does not change when using facecolor instead of vertexcolor.

Replacing the definitions:

...
vxc0 = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                        **{'facecolor': [k]*len(v['i'])}} for k,v in zip(cid,vxl) if k.startswith('rgba(')])
vxc1 = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                        **{'facecolor': [k]*len(v['i'])}} for k,v in zip(cid,vxl) if k.startswith('rgb(')])
...

.

My use case for low opacity values is to mask out-of-range (e.g., NaN) values without having to update the coordinates (x, y, z, i, j k). At the moment only the whole trace opacity can be set in combination with intensity colors, as far as I understand. Using facecolor/vertexcolor is one way around that. At least, for the moment.

@z--m-n
Copy link
Author

z--m-n commented Mar 23, 2020

That's why your discrete colorscale does not color your voxels as expected.

I am not sure...

Changing the intensitymode does not seem to yield a different result, both show the same mis-interpolated colors.

Adding to the MWE:

...
# concatenate shapes and replace 'intensity' (vertex-mode) with 'intensity' (cell-mode)
vxd = reindex_voxels([{**{i:j for i,j in v.items() if i in ['x','y','z','i','j','k']},
                                      **{'intensity': [v['intensity'][0]]*len(v['i'])}} for k,v in zip(cid,vxl)])
go.Figure(data=[go.Mesh3d(**vxd,colorscale=cs,intensitymode='cell')])

.

@empet
Copy link

empet commented Mar 24, 2020

@z--m-n As @archmoj says opacityscale (colorscale with colors of different opacities) for Mesh3d is not yet implemented in plotly.js.

@z--m-n
Copy link
Author

z--m-n commented Mar 24, 2020

@empet @archmoj Thanks for the info, links and tweaks. Very helpful! I'll follow the developments over in plotly.js.

Regarding the discontinuous breaks. Would a right (or left) side closed lookup at discontinuous colorscale breaks be a useful feature or default (as in Heatmap)?

In the example below: the middle square would become either black or light blue, not a mix of black and blue.

image

import numpy as np
import plotly.graph_objects as go

# custom colorscale, discontinuous break at 0.5
cs = [[0.0, 'rgb(64, 64, 64)'],
    [0.50, 'rgb(64, 64, 64)'],
    [0.50, 'rgb(127, 255, 255)'],
    [1.00, 'rgb(127, 255, 255)']]

# some shapes with color intensity 0.0, 0.5 and 1.0
vxl = [{'x': [-0.25, -0.25, 0.25, 0.25],
  'y': [-0.25, 0.25, 0.25, -0.25],
  'z': [-0.25, -0.25, -0.25, -0.25],
  'i': [0, 0],
  'j': [1, 2],
  'k': [2, 3],
  'intensity': [0.0, 0.0, 0.0, 0.0]},
 {'x': [0.25, 0.25, 0.75, 0.75],
  'y': [-0.25, 0.25, 0.25, -0.25],
  'z': [-0.25, -0.25, -0.25, -0.25],
  'i': [0, 0],
  'j': [1, 2],
  'k': [2, 3],
  'intensity': [0.5, 0.5, 0.5, 0.5]},
 {'x': [0.75, 0.75, 1.25, 1.25],
  'y': [-0.25, 0.25, 0.25, -0.25],
  'z': [-0.25, -0.25, -0.25, -0.25],
  'i': [0, 0],
  'j': [1, 2],
  'k': [2, 3],
  'intensity': [1.0, 1.0, 1.0, 1.0]}]

go.Figure(data=[go.Mesh3d(**v,cmin=0,cmax=1,colorscale=cs) for v in vxl])

@empet
Copy link

empet commented Mar 24, 2020

The middle square is gray because plotly.js interpolates colors for Mesh3d. It works only with continuous colorscales, since it provides the property facecolor to assign a particular color to each face.

@archmoj
Copy link
Contributor

archmoj commented Mar 24, 2020

Please have a look at this demo.

@connorhazen
Copy link

connorhazen commented Apr 25, 2023

Regarding` the discontinuous breaks. Would a right (or left) side closed lookup at discontinuous colorscale breaks be a useful feature or default (as in Heatmap)?

In the example below: the middle square would become either black or light blue, not a mix of black and blue.

This is still an issue when your intensity value near the boundary of two colors.

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:
newplot (1)

@empet
Copy link

empet commented Apr 26, 2023

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, 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 [a, b]=[0.2, 0.7], and the normalising function is f(t)=(t-0.2)/0.5.
f evaluaed 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()

mesh-discrette-colorscale

@connorhazen
Copy link

thank you for the reply, however, this is not really addressing the bug I was calling out.

A few notes:

  1. I am setting Cmin and Cmax to 0 and 1 respectively. This means my normalization function is really (z-cmin)/(cmax-cmin). Since I set the bounds at these values, the data is already normalized.

For example:
z= .4
norm_z = (.4-0)/(1-0) == .4
norm_z=z

  1. I am not just trying to set discrete points. My use case is dealing with 40000 faces where I need to set some hard cutoff points between discrete gradients. So If I have 40000 values from 0->1, and I pick a cutoff point of .501 (like in the above example) then everything below this value should get the 0->.501 gradient (green in this example) and everything above .501 should get the other gradient (red in this example). This works with heatmaps, not with meshes.

Example of heatmap with same setup:
Screenshot 2023-04-26 at 9 51 16 AM

example of mesh with setup:

newplot (2)

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]
x_heat = [1, 1, 1]
y_heat = [1, 2, 3]

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


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

t1 = go.Heatmap(x=x_heat, y=y_heat, z=intesity, colorscale=scale, zmin=cmin, zmax=cmax)

fig.add_trace(t1)
fig.show()

fig2 = go.Figure()
fig2.add_trace(t2)
fig2.show()

you can see that I am using the same color scale, intensity values (z for heatmaps), cmin/cmax (zmin/zmax for heatmaps). Yet, the two plots handle boundaries quite differently.

I do have a solution, I make the color faces from my color scale with my own interpolation function modeled off of your heatmap code. However, this should not be necessary, why do the different plots treat boundaries differently?? that is the bug I am calling out

@empet
Copy link

empet commented Apr 26, 2023

Just add cmin=0, cmax=1, to my code, and you'll notice that the middle triangle is green, i.e. it corresponds to the expectations, and isn't of interpolated color. Your colorscale is not defined following the algorithm of construction of a discrete colorscale, adapted to data i.e. to a sequence of values a=x₁< x₂< ... <xₙ=b, as I did here, and in a few examples on plotly community forum (I answered your question, there too). If you define your discrete colorscale according to this rule, you'll get the right color.

@connorhazen
Copy link

connorhazen commented Apr 26, 2023

Yes adding it to your code would make it green as .602 is well above .5. However, this will break down as you move that value closer. When you have thousands of points, continuously spread out, some will have to be near the line. That is what the simplified example is trying to show. I did not post the more comprehensive version as I thought the simpler example showed off the bug with as little extra fluff as possible.

The key thing here is that the middle triangle color in my example is a given a color that should never exist in the range. .5 is not normalizing to exactly my break point, it is below that breakpoint, but somehow the red still bleeds in. That is what I am showing. In fact there seems to be a range at which the color will bleed in.

to try and make this as clear as possible, here are some screen shots where I adjust the break point. In each one, the data stays the same, the cmin/cmax do not change etc. The only thing I change is where that break point is.

.502:
newplot (2)

.503:

newplot (1)

.504:

newplot (3)

  1. the first two show the same green/red color for the middle triangle, even though the colorscale breakpoint has shifted up. Then when I get to .504, it finally "snaps" to the green it should be.
  2. That is .4% higher than where it should have occurred. How was it ever the hue? why is there this arbitrary threshold at which it finally figures out where my number should lie (it actually exists at 0.50392 -> 0.50393 in this case to get even more specific, 0.50392 is red/green color, 0.50393 is green)

It should be binary. I am above the breakpoint, or below (I would understand if there was an edge case where points on the line get a hue, but that is not whats happening here). Instead of being binary, how close you are to the line seems to have an effect. Again, this does not happen with other plot types, just meshes

@alexcjohnson
Copy link
Collaborator

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.

@connorhazen
Copy link

connorhazen commented Apr 26, 2023

Thanks for the reply, and the much better visual to explain it :). I wanted to point this out because it took quite a bit of testing to figure out this actually existed and was my problem, in case others also experience this.

I do have a solution on my end. I just do the color mapping my self and set as face color:

Below is the function (sorry its in react) I created to call to map values. It could possibly be improved with binary search to speed it up, but my scale is somewhat simple most of the time, so did not implement yet

Esentially, just move your point through the scale until you find what "insertion" point it has. then do you interpolation to find what color it should be assigned.

let facecolors = plotlyDiv.data[dataIndex].customdata.map((x: number) =>
      getFaceColors(x, colorBarPars)
)
update['facecolor'] = [facecolors]

function getFaceColors(x,colorBarPars) {
   let normVal = (value - colorBarPars.zmin) / (colorBarPars.zmax - colorBarPars.zmin)

  if (normVal >= 1) {
    return colorBarPars.colorbar[colorBarPars.colorbar.length - 1][1]
  }
  if (normVal <= 0) {
    return colorBarPars.colorbar[0][1]
  }
  let i = 0
  while (i < colorBarPars.colorbar.length) {
    if (normVal == colorBarPars.colorbar[i][0]) {
      return colorBarPars.colorbar[i][1]
    }
    if (normVal < colorBarPars.colorbar[i + 1][0]) {
      let indexInterp: number =
        (normVal - colorBarPars.colorbar[i][0]) /
        (colorBarPars.colorbar[i + 1][0] - colorBarPars.colorbar[i][0])
      return interpolateRgb(
        colorBarPars.colorbar[i][1],
        colorBarPars.colorbar[i + 1][1]
      )(indexInterp)
    }
    i++
  }
 }

** interpolateRGB comes from d3-interpolate (copied that from the plotly repo)
** colorbar is the same format as the colorscale you supply normally to plots

Maybe this is helpful for others.

@empet
Copy link

empet commented Apr 26, 2023

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. @connorhazen 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). That's why it interpolated the colors.
mesh-discr-colorscale

@connorhazen
Copy link

connorhazen commented Apr 26, 2023

hmm ok.....

Few notes:

your colorscale is still a continuous colorscale at its root.

You are not addressing the actual problem raised. I would recommend re-reading the messages above.

Your 'solution' does not scale, due to the boundary condition

@empet
Copy link

empet commented Apr 26, 2023

I answered based on your initial example. Meanwhile I worked on the above code and did not read to see how the discussion evolved. I'll think about your new case.

@connorhazen
Copy link

connorhazen commented Apr 26, 2023

Ran your code with these intensities: [0, 0.499, 0.501, 1], as you can see, it does not work.

I would also like to make it very clear that my goal is not to create a discrete scale. I am using this example to showcase a bug.

newplot (5)


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],
        [0, 1, 0],
        [1, 0, 0],
        [0, 0, 1],
        [0, 1, 1],
        [1, 0, 1],
        [0, 0, 2],
        [0, 1, 2],
        [1, 0, 2],
        [0, 0, 3],
        [0, 1, 3],
        [1, 0, 3],
    ],
    dtype=float,
)

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

intensity = [0, 0.499, 0.501, 1]
uintens = np.unique(intensity)
print(len(uintens))
colors = [
    "rgb(0,0,255)",
    "rgb(255,0,0)",
    "rgb(0,255,0)",
]
print(len(colors))
dcolorscale = discrete_colorscale(uintens, colors)

print(dcolorscale)
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()

I hope that makes the problem more clear, although I would suggest alexcjohnson post above for the clearest visual

@gvwilson gvwilson self-assigned this Jul 8, 2024
@gvwilson gvwilson removed their assignment Aug 2, 2024
@gvwilson gvwilson added the P3 backlog label Aug 12, 2024
@gvwilson gvwilson changed the title Representation of mixed rgb and rgba colors in mesh3d incorrect representation of mixed rgb and rgba colors in mesh3d Aug 12, 2024
@gvwilson gvwilson added the bug something broken label Aug 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken P3 backlog
Projects
None yet
Development

No branches or pull requests

6 participants