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:
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)
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")
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!
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))
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().