Plotlyjs and composite figures

Hi all,

I’m trying to create composite figures which involve a heatmap as well as an overlying contour. I’d like to control the aspect ratio, the contour thickness and the colorbars. I chose plotlyjs backend of Plots because it’s light and it allows to hover the mouse and pick the data values directly, which I find extremely useful.

However, I have some trouble controlling the appearance of the figure, in particular:

  • the aspect ratio does not appear correct. Even with aspect_ratio=1.0, there is still a vertical exagerration (the circular contour appears as an ellipse).
  • modifying the contour linewidth has no effect
  • once two data sets are used (one for heatmap, one for contour), the colorbar somehow merges the values of the two datasets. I have tried setting colorbar=false for the contour plot but this had no effect.
  • sometimes the contour does not appear on top of the heatmap

I have pasted a MWE below. Does anyone see obvious misuses? Is this an expected behaviour? Is there another (light) way to make such type of figures with Julia?

Cheers!

using Plots
plotlyjs()

function main()
    # Mesh
    x = LinRange(-2.0, 2.0, 100)
    y = LinRange(-1.0, 1.0, 100)
    # One continuous field 
    f = 1e3.*exp.(-x.^2 .- (y').^2)
    # One discontinuous field 
    g = zero(f)
    g[(x.^2 .+ (y').^2) .< 0.5^2] .= 1.0
    # Compose figure
    p = plot()
    p = heatmap!(x, y, f')
    p = contour!(x, y, g', c=:black, linewidth=0.1) # contour does not appear on top of heatmap, colorbar is messed up, linewidth fails
    p = plot!(aspect_ratio=1) # aspect ratio fails: does not seem 1.0 on my screen
    display(p)
end

main()

I post here the code in pure PlotlyJS, to adapt it to Plots with plotlyjs():

using PlotlyJS
x = range(-2.0, 2.0, length=200)
y = range(-1.0, 1.0, length=100)
# One continuous field 
f = 1e3.*exp.(-x'.^2 .- y.^2)
t= range(0, 2pi, length=100)
p = Plot([heatmap(x=x, y=y, z=f, colorscale=colors.plasma,colorbar_thickness=24), 
        scatter(x=0.5*cos.(t), y=0.5*sin.(t), mode="lines",
                line_width=2, line_color="black")],
        Layout(width=600, height=320,
               xaxis_range=[-2,2], yaxis_range=[-1,1]))
display(p)

heatmap-scatter

1 Like

Oh, thanks a lot, this looks great!
The non fully functional / non working features as Plots’ backend are fully functional.
I had no clue that there would be such a difference!

Your original code seemed more difficult, as it involved adding contours over a heatmap.

To draw a circle on top of a heatmap, you can use Plots.jl, plotlyjs():

heatmap(x, y, f', ratio=1, xlims=(-2,2), ylims=(-1,1))
plot!(Plots.partialcircle(0, 2π, 100, 0.5), c=:black)

There is interactivity and the coordinates and data values can be read but, as in the example above, the circle looks elliptical to me:

NB:
The pythonplot() backend displays a perfect circle but does not have the same data reading features.

Thanks a lot! In practice, I’d like to draw contours of arbitrary shapes. Here it just happened to be a circle for the purpose of the MWE.
I will try to port the full PlotlyJS example into my visualisation code, then I will let you know if it is a good solution to my problems.

In PlotlyJS, the relevant attributes (see Single-page reference in Julia) for fixing the aspect ratio are scaleanchor, scaleratio and (not allowed simultaneously with these) matches:

p = Plot([heatmap(x=x, y=y, z=f, colorscale=colors.plasma,colorbar_thickness=24), 
               scatter(x=0.5*cos.(t), y=0.5*sin.(t), mode="lines",
                       line_width=2, line_color="black")],
               Layout(width=600, height=320, yaxis_scaleanchor="x", yaxis_scaleratio=1,
                      xaxis_range=[-2,2], yaxis_range=[-1,1]))

2 Likes

Thanks for the help! I’ve tried adapting the MWE. I would like to use contour instead of scatter (for the purpose of contouring another field than the one represented with a heatmap).
It seems that adding a contour (the 0/1 boundary) erases the underlying heatmap and overprints the colorbar.
Would there be a way to draw the contour line on top of the heatmap and not to link the contour to the colorbar?
Also, while forcing the aspect ratio to avoid vertical exagerration, is there a way to crop out the additional grey space out of the xaxis_range/yaxis-range?

using PlotlyJS
# Mesh
x = LinRange(-2.0, 2.0, 200)
y = LinRange(-1.0, 1.0, 100)
Lx, Ly = x[end]-x[1], y[end]-y[1]   # Added after edit
# One continuous field 
f = 1e3.*exp.(-x.^2  .- (y').^2)
# One discontinuous field 
g = zero(f)
g[(x.^2 .+ (y.^2)') .< 0.5^2] .= 1.0
# Compose figure
p = Plot([heatmap(x=x, y=y, z=f', colorscale=colors.plasma, colorbar_thickness=24), 
contour(x=x, y=y, z=g', mode="lines",
                line_width=2, colorscale=[[1, "rgb(255,255,255)"]])],
Layout(width=600, height=600*Ly/Lx,
                xaxis_range=[-2,2], yaxis_range=[-1,1], yaxis_scaleanchor="x", yaxis_scaleratio=1))
display(p)

EDIT: the contour overlying a heatmap issue is also discussed there. I have just tested the proposed Plots solution. It involves using clim to rescale the colobar while drawing the contour. This work with gr() but does not seem to work with plotlyJS().

EDIT: add transpose of in the heatmap and defined Lx and Ly for aspect ratio.

I can’t reproduce your exact code as Lx and Ly are not defined in the pasted code you gave so an error is thrown.
You should be able to to remove the fill of the contour zones and have just the coloring of the contour lines controlled by the line_color attribute by adding to your countour call the following kwarg: contours_coloring="none".
So your contour call above would be:

contour(x=x, y=y, z=g', mode="lines", line_width=2, contours_coloring="none")

Note: I removed the colorscale to contour as if you set contours_coloring to "none" no colorbar is generated and the color of the line of the contour is not controlled by the colorscale but by line_color.

Edit: I just noted you also asked how to crop out the extra space in the axis. You can do this by changing the constrain parameter of the xaxis inside Layout to "domain".

Here is an example in Pluto:

And here is the code modified to use PlotlyJS as you do instead of PlutoPlotly.

using PlotlyJS
# Mesh
x = LinRange(-2.0, 2.0, 200)
y = LinRange(-1.0, 1.0, 100)
# One continuous field 
f = 1e3.*exp.(-x.^2  .- (y').^2)
# One discontinuous field 
g = zero(f)
g[(x.^2 .+ (y.^2)') .< 0.5^2] .= 1.0
# Compose figure
p = Plot([
	heatmap(x=x, y=y, z=f, colorscale=colors.plasma, colorbar_thickness=24), 
	contour(x=x, y=y, z=g', mode="lines", line_width=1, contours_coloring="none")],
	Layout(width=600, height=320,
		xaxis = attr(;
			range = [-2,2],
			constrain = "domain",
		),
		yaxis = attr(;
			range = [-1,1],
			scaleanchor = "x",
			scaleratio = 1,
		)
	))
display(p)
1 Like

wow, thanks a lot. This is now great!
I’ve added your suggestions. One more thing, I added the heatmap back on top of the contour, such that mouse hovering will pick up heatmap values instead of that of the contoured field (old matlab inherited trick…).

Here’s the complete code:

using PlotlyJS

function main_PlotlyJS()
    # Mesh
    x = LinRange(-2.0, 2.0, 200)
    y = LinRange(-1.0, 1.0, 100)
    Lx, Ly = x[end]-x[1], y[end]-y[1]
    # One continuous field 
    f = 1e3.*exp.(-x.^2  .- (y').^2)
    # One discontinuous field 
    g = zero(f)
    g[(x.^2 .+ (y.^2)') .< 0.5^2] .= 1.0
    # Compose figure
    p = Plot([heatmap(x=x, y=y, z=f', colorscale=colors.plasma, colorbar_thickness=24), 
    contour(x=x, y=y, z=g', mode="lines",
                    line_width=2, colorscale=[[1, "rgb(255,255,255)"]], contours_coloring="none"),
                    heatmap(x=x, y=y, z=f', colorscale=colors.plasma, colorbar_thickness=24)],
    Layout(width=600, height=600*Ly/Lx,
    xaxis = attr(;
    range = [-2,2],
    constrain = "domain",
    ),
    yaxis = attr(;
    range = [-1,1],
    scaleanchor = "x",
    scaleratio = 1,
    )
    ))
    display(p)
end

main_PlotlyJS()

I will try to repeat it by using Plots with PlotlyJS backend. Should be doable if contours_coloring is also exposed there… Many thanks again!