Heatmap for irregular shapes

Hey all,
I am trying to use Heatmaps to color a 2D finite element mesh according to some data defined on it. The way I am plotting the mesh itself (i.e. just the grids) is from this post. In there, I have created the connectivity array so I would know the coordinates of the 4 vertices that form one of the elements.
When the grid is regular, i.e. all rectangle shaped, like in the post, it is easy to then impose a heatmap onto it. However, what if the grids are irregular? For example, if I take “empet” 's code in the post, and just change the coordinates of the last node to [0.5, 0.8], I get the mesh here:
newplot (12)
Now the elements are trapezoids. I am wondering how can I color those with Heatmap. Thank you so much!

@axelwang
You can find in this thread how to associate a regular grid and interpolate the z-values at its points from the irregular ones: Heatmap complain NaN z values when there are actually valid z values

Hey @empet. Thanks for your help!
I am not sure though if what you suggest is what I am looking for. I am not looking for a smoothed heatmap color plot, but rather I would like the colors to strictly follow the boundaries. In other words, I simply want to color the 4 trapezoids shown above, and my knowns are the coordinates of the 4 corners forming one trapezoid, and a value corresponding to what this trapezoid should be colored to.
I am still unable to find a solution to this. I looked at the documentation of Heatmap and it seems like the issue is with how to define the 'cells" for each color?

Maybe what I ought to use is “Scatter” rather than “Heatmap”. Then I can define the shape of each element and use the “fill” option from “Scatter”, like in here. But the problem with this is I can’t seem to find a way to change “fillcolor” from one element to another, but rather they will all be colored to one color.

import numpy as np
import plotly.graph_objs as go


nodes=[[0, 0],
       [1, 0],
       [1, 1],
       [0, 1],
       [0.5, 0],
       [1, 0.5],
       [0.5, 1],
       [0, 0.5],
       [0.5, 0.8]]

elements= [[0, 4, 8, 7],
           [7, 8, 6, 3],
           [4, 1, 5, 8],
           [8, 5, 2, 6]]

nodes=np.asarray(nodes)
xn, yn=nodes.T

field = [1,2,3,4]

y_plotly=[]
z_plotly=[]
for elem in elements:
    elem.append(elem[0])
    y_plotly.extend(xn[elem].tolist()+[None])
    z_plotly.extend(yn[elem].tolist()+[None])
    
   
trace=dict(type='scatter',
          x=y_plotly,
          y=z_plotly,
          mode='lines',
          line=dict(color='blue', width=2),
          fill='toself',
          fillcolor="red")

layout=dict(width=400, height=300,
           xaxis=dict(showgrid=False, zeroline=False),
           yaxis=dict(showgrid=False, zeroline=False) 
           )
fig=go.FigureWidget(data=[trace], layout=layout)

newplot (13)

This fills all trapezoids to red. But what I really want is to fill them according with a custom colorscale that corresponds to field.

@axelwang
If you are calling go.Scatter to fill elements, then you should define a trace for each element. I updated your code to Plotly version 5.+, because the examples you followed on this forum are old:

import numpy as np
import plotly.graph_objects as go
nodes=[[0, 0],
       [1, 0],
       [1, 1],
       [0, 1],
       [0.5, 0],
       [1, 0.5],
       [0.5, 1],
       [0, 0.5],
       [0.5, 0.8]]

elements= [[0, 4, 8, 7],
           [7, 8, 6, 3],
           [4, 1, 5, 8],
           [8, 5, 2, 6]]

nodes=np.asarray(nodes)
xn, yn=nodes.T

field = [1,2,3,4]
colors = ['#183924', '#097d4a', '#72ba6c', '#d6f9cf']
f2color=dict(zip(field, colors))

data= []
for k, elem in enumerate(elements):
    elem.append(elem[0])
    data.append(go.Scatter(
          x=xn[elem],
          y=yn[elem],
          mode='lines',
          line=dict(color='rgb(150, 150, 150)', width=2),
          fill='toself',
          fillcolor=f2color[field[k]]))
   
fig = go.Figure(data)
d = dict(showline=False, zeroline=False)
fig.update_layout(width=400, height=300, showlegend=False,
                  xaxis=d, yaxis=d,
                  template="none")

elements

Since you didn’t specify how many elements are you plotting in a figure, and whether they are colored with arbitrary discrete colors or colors from a colorscale, I gave here only the code for the four elements and the associated field, you posted above.

Hey @empet. Thank you so much for this. It has been very helpful!
Could you please help me one more time by showing how to actually do this with a colorscale, i.e. map the 4 values in field to an existing colorscale, say "Viridis". I have been playing around with this for a bit but couldn’t make it to work. My main confusion is how to create your f2color dictionary when colors is a built in colorscale.
Thank you very much!

@axelwang

Any graphic library works with normalized data, and to each normalized value, nval in [0,1], one associates a color in the chosen colorscale. From a Plotly colorscale, as a list of lists:

pl_plasma= 
[[0.0, '#eff821'],
 [0.08, '#fad524'],
 [0.17, '#fdb32e'],
 [0.25, '#f79341'],
 [0.33, '#ec7853'],
 [0.42, '#dd5f65'],
 [0.5, '#ca4678'],
 [0.58, '#b52e8c'],
 [0.67, '#9b179e'],
 [0.75, '#7c02a7'],
 [0.83, '#5c00a5'],
 [0.92, '#3a049a'],
 [1.0, '#0c0786']]

you cannot find which color corresponds to any normalized value in ([0,1]), because it’s plotly.js which interpolates the colors. That’s why you should import matplotlib.cm to find out the color in a coloscale corresponding to normalized field values. Hence to the previous code posted above we add a function that maps the filed values to a coloscale and returns the dictionary used above:

import numpy as np
import plotly.graph_objects as go
from matplotlib import cm

def get_field2color(field, mpl_cmap):
    field = np.asarray(field)
    vmin, vmax = field.min(), field.max()
    if vmin == vmax:
        raise ValueError("field contains the same values in each position")
    norm_field = (field-vmin)/(vmax-vmin)
    rgbcolors = (mpl_cmap(norm_field)*255).astype(np.uint8)
    fcolors = [f"rgb{tuple(rgbc)[:3]}" for rgbc in rgbcolors]
    return dict(zip(field, fcolors))


field =[1,2,3,4]
nodes=[[0, 0],
       [1, 0],
       [1, 1],
       [0, 1],
       [0.5, 0],
       [1, 0.5],
       [0.5, 1],
       [0, 0.5],
       [0.5, 0.8]]

elements= [[0, 4, 8, 7],
           [7, 8, 6, 3],
           [4, 1, 5, 8],
           [8, 5, 2, 6]]

nodes=np.asarray(nodes)
xn, yn=nodes.T
cmap = cm.viridis     #cm.Greens_r          #cm.Reds_r
f2color=get_field2color(field, cmap)

data= []

for k, elem in enumerate(elements):
    elem.append(elem[0])
    data.append(go.Scatter(
          x=xn[elem],
          y=yn[elem],
          mode='lines',
          line=dict(color='rgb(150, 150, 150)', width=2),
          fill='toself',
          fillcolor=f2color[field[k]]))
   
fig = go.Figure(data)
d = dict(showline=False, zeroline=False)
fig.update_layout(width=400, height=300, showlegend=False,
                  xaxis=d, yaxis=d,
                  template="none")

Hey @empet.
This has been extremely helpful. Thank you so much.
I believe I have one last thing to ask for your help. How can I actually plot a color bar for the filled area plot? Again, I have tried myself for a while but couldn’t figure this out. I think it is quite tricky because the elements are filled in one by one so the go.Scatter() doesn’t see the full range of color at once. Also, seems the built-in color bar option for go.Scatter() can only work for line or marker as part of their dictionary, but not with respect to fillcolor.

I am thinking maybe the key really is to just take f2color and make a color bar from that, and then somehow append this to the go.Scatter() plot below. This is the part I get stuck now. Would be really grateful if you can help on this or offer another strategy to do it.

Thank you so much!

@axelwang
I modified the function that returnd the dict f2color, such that to define a discrete colorscale according to field values:

def get_field2color(field, cmap):
    if 0 in field:
        raise ValueError("the field code must be >0")

    field = np.asarray(field)
    vmin, vmax = field.min(), field.max()
    if vmin == vmax:
        raise ValueError("field contains the same values in each position")
    norm_field = (field-vmin)/(vmax-vmin)
    rgbcolors = (cmap(norm_field)*255).astype(np.uint8)
    fcolors = [f"rgb{tuple(rgbc)[:3]}" for rgbc in rgbcolors]
    d= dict(zip(field, fcolors))
    vals = np.concatenate(([0], field))
    vmin, vmax = vals.min(), vals.max()
    nvals=(vals-vmin)/(vmax-vmin)
    ids = np.argsort(nvals)
    dcolorscale = [] #discrete colorscale
    for k in range(len(fcolors)):
        dcolorscale.extend([[nvals[ids[k]], fcolors[ids[k]]], [nvals[ids[k+1]], fcolors[ids[k]]]])
    return d, dcolorscale 

Hence in the main code replace

f2color=get_field2color(field, cmap)

by

f2color, dcolorscale= get_field2color(field, cmap)

and to get displayed the colorbar, add a dummy trace to the initial one as follows:

ids = [el[0] for el in elements]
fig.add_scatter(x=xn[ids], y=yn[ids], mode="markers", 
                marker=dict(size=0.01, 
                            color= field,  
                            colorscale=dcolorscale, 
                            showscale=True,  
                            colorbar_thickness=25))

irreg-heatmap

2 Likes

I appreciate this thread and the proposed solution as I’m dealing with the same problem of wanting to display a 2D FE grid using PlotlyJS.jl. That said, I really wish there was a simple mesh2d() to compliment the existing mesh3d().