GraphMakie.jl cuts off text - julia

I would like to visualize some graphs with labeled vertices using GraphMakie.jl.
Sadly, makie doesn't seem to include the labels into its bounding box-calculation and the labels are therefore cut off. I would expect there to be some feature to add some padding to the Axis-object, but I can't find anything like this.
Minimal example:
using Graphs, GraphMakie
fig = Figure()
for i in 1:2
ax = Axis(fig[1,i])
g = wheel_graph(i+1)
graphplot!(ax, g, nlabels=["label" for _ in 1:i+1])
hidedecorations!(ax)
end
display(fig)
Things I tried that didn't work:
adding protrusions: ax.alignmode = Mixed(right = Makie.Protrusion(50))
refreshing limits: autolimits!(ax)
changing the layout gap: colgap!(fig.layout, 50)
manually overriding the size: ax.width = 400

The area inside the rectangle is determined by the plotted range of data. Therefore, to make the labels fit in the rectangle, you have to adjust the range in the "data domain", i.e., using xlims and ylims. To add a margin to these ranges, you have to first know these ranges and hence do the call to the graph layout-function manually. I've implemented a wrapper that does this and adds a margin-property to adjust the margins:
using Graphs, GraphMakie, NetworkLayout, CairoMakie
function graphplotWithMargin!(
ax::Axis,
g::SimpleGraph;
margin = (0.1, 0.1, 0.1, 0.1),
layout = Spring(),
args...)
local pos = layout(g)
graphplot!(ax, g; layout=_->pos, args...)
local x0, x1 = minimum(map(x->x[1],pos)), maximum(map(x->x[1],pos))
CairoMakie.xlims!(ax, x0 - margin[4] * (x1 - x0), x1 + margin[2] * (x1 - x0))
local y0, y1 = minimum(map(x->x[2],pos)), maximum(map(x->x[2],pos))
CairoMakie.ylims!(ax, y0 - margin[1] * (y1 - y0), y1 + margin[3] * (y1 - y0))
end
fig = Figure()
for i in 1:2
ax = Axis(fig[1,i])
g = wheel_graph(i+1)
graphplotWithMargin!(ax, g,
nlabels=["label" for _ in 1:i+1],
margin=(0.05, 0.15, 0.05, 0.05))
hidedecorations!(ax)
end
display(fig)

Related

hline/vline with subplots in Julia

I'm trying to add a horizontal line to a subplot, and from this discussion: https://discourse.julialang.org/t/vline-with-subplots/25479/2, I have the following
x = [1,2,3]
y1 = 2x
y2 = x.^2
plot([x, x], [y1, y2], layout = (2, 1))
hline!([4 4])
Which produces the plots.
Now what I'm trying to do is do the horizontal line on the bottom plot, but not the top one. If I just specify hline!([4]) , it defaults to the top one. Is there a way to do the bottom one only?
It's probably best practice to plot subplots separately (as mentioned on Slack by isentropic):
x = [1,2,3]
y1 = 2x
y2 = x.^2
p1 = plot(x, y1)
p2 = plot(x, y2)
hline!(p2, [4])
plot(p1, p2, layout = (2, 1))
But if you want it all in one go, you could have used
hline!([[NaN], [4]])
The trick is to keep track of the plot handles.
p = plot([x, x], [y1, y2], layout = (2, 1))
returns a plot handle (specifically, a Plots.Plot{Plots.GRBackend} object) p with two elements, p[1] (the first subplot) and p[2] (the second subplot). To add the hline to the bottom plot only, then, you can write:
x = [1,2,3]
y1 = 2x
y2 = x.^2
p = plot([x, x], [y1, y2], layout = (2, 1))
hline!(p[2], [4])

What controls the apparent buffer in strwidth?

I am trying to wrap text labels in a rectangle.
Here is a simple plot with labels:
x = mtcars$wt
y = mtcars$mpg
l = rownames(mtcars)
plot(x, y)
text(x, y, l, adj = .5) # i.e., the default
I can successfully use strwidth and strheight to accomplish the wrapping like so:
delx = strwidth(l, cex = par('cex'))
dely = strheight(l, cex = par('cex'))
rect(x - .5*delx, y - .5*dely, x + .5*delx, y + .5*dely)
Notice the "gaps" of whitespace on the left and right of each label. For now, this is fine, but it leads to issues when trying to account for adj in the plot:
adj = c(0, .5)
plot(x, y)
text(x, y, l, adj = adj)
rect(
x - adj[1L]*delx, y - adj[2L]*dely,
x + (1-adj[1L])*delx, y + (1-adj[2L])*dely
)
The box has adjusted directionally well, but it appears to be an accurate transformation, I need to account further for whatever is creating the "buffer" width.
What is that? I haven't seen anything in ?par or ?strwidth yet.
There is no internal width buffer. It's just that you need to calculate the strwidth in the context of the current device. When you resize the graphics device after your plot is drawn, the rectangles will resize but the text will not. This will cause the apparent space around your text vary with the window size.
As long as you are careful to specify your device dimensions before drawing your plot, and recalculate strwidth for text on that device, your code should produce snug boxes around the text. You can add a fixed margin without affecting centering too, and none of this requires information that's not already available.
Here's a function for illustration purposes, that allows complete control of the boxes around your text:
michael_plot <- function(width = 9, height = 6, adj = c(0.5, 0.5), margin = 0)
{
dev.new(width = width, height = height, unit = "in", noRStudioGD = TRUE)
plot(x, y)
delx <- strwidth(l, cex = par('cex'))
xmarg <- strwidth("M", cex = par('cex')) * margin
dely <- strheight(l, cex = par('cex'))
ymarg <- strheight("M", cex = par('cex')) * margin / 2
text(x, y, l, adj = adj)
rect(x - adj[1] * delx - xmarg,
y - adj[2] * dely - ymarg,
x + (1 - adj[1]) * delx + xmarg,
y + (1 - adj[2]) * dely + ymarg)
}
Starting with the default:
michael_plot()
The boxes fit perfectly; in fact, they are too close to the text, so we should add a margin to make them more legible:
michael_plot(margin = 1)
But importantly, we can move them safely with adj while keeping them centred:
michael_plot(margin = 1, adj = c(0, 0.5))

Placing arrow heads to the middle of the lines in R

I have a plot where I draw arrows from points to points. I would like to put this arrow heads not to the end of the line, but to middle. Is there a simple way to do it other than placing extra arrows with half length of the according line?
My code is this:
plot(x, y, xlim=range(x), ylim=range(y), xlab="x", ylab="y", pch=16,
main="Filled Plane")
for(i in 1:20){
arrows(x[i], y[i], x[i+1], y[i+1], length = 0.25, angle = 30, col = i)
}
Make a custom function myArrow() and add one new argument cut to control the proportion of the arrows
myArrow <- function(x0, y0, x1, y1, cut = 1, ...){
x.new <- (1 - cut) * x0 + cut * x1
y.new <- (1 - cut) * y0 + cut * y1
# segments(x0, y0, x1, y1, ...)
arrows(x0, y0, x.new, y.new, ...)
}
Note1 : The computation of x.new and y.new in this custom function uses a simple mathematical concept, i.e. the Section Formula. The value of cut must be between 0 to 1.
Note2 : The use of this function is equivalent to that of the original functionarrows() other than that it has one more new argument cut.
Note3 : If you want complete lines behind the arrows, just remove the hash(#) in the function.
Plot and try different cut value. For example, I use cut = 0.7. (If you want the arrowheads to the middle, use cut = 0.5.)
# Toy Data
x <- seq(1, 5.5, by = 0.5)
y <- rep(c(1, 5), 5)
plot(x, y, pch = 16)
for(i in 1:9){
myArrow(x[i], y[i], x[i+1], y[i+1], cut = 0.7, col = i, lwd = 2)
}
Since you do not provide your x and y, I made up some data. There is no need for the loop. arrows will handle a vector of coordinates. One way is to draw a full-length arrow with no arrowhead and another that just goes halfway but has the arrowhead.
## Some bogus data
set.seed(123)
x = runif(4)
y = runif(4)
## Compute the midpoints
midx = diff(x)/2 + x[-length(x)]
midy = diff(y)/2 + y[-length(y)]
## Draw it
plot(x,y,xlim=range(x), ylim=range(y), xlab="x", ylab="y",
main="Filled Plane",pch=16)
arrows(x[-length(x)], y[-length(y)],x[-1],y[-1],
angle = 0, col = 1:3)
arrows(x[-length(x)], y[-length(y)],midx,midy,
length = 0.25, angle = 30, col = 1:3)

How to fill area between curves with Plots.jl?

Suppose I have a curve y, and two other curves u and l in the form of vectors. How to plot:
plot(y, lab="estimate")
plot!(y-l, lab="lower bound")
plot!(y+u, lab="upper bound")
That is, an asymmetric confidence interval? I know how to plot the symmetric case with the option ribbon as explained here.
The current answers are NOT correct. Here are two ways that are correct (as of v1.10.1 of Plots.jl):
Method 1: Using fillrange
plot(x, l, fillrange = u, fillalpha = 0.35, c = 1, label = "Confidence band")
Method 2: Using ribbon
plot(x, (l .+ u) ./ 2, ribbon = (l .- u) ./ 2, fillalpha = 0.35, c = 1, label = "Confidence band")
(Here, l and u denote the the "lower" and "upper" y values, respectively, and x denotes their common x values.) The key difference between these two methods is that fillrange shades the region between l and u, while the ribbon argument is a radius, i.e. half the width of the ribbon (or in other words, the vertical deviation from the midpoints).
Example using fillrange:
x = collect(range(0, 2, length= 100))
y1 = exp.(x)
y2 = exp.(1.3 .* x)
plot(x, y1, fillrange = y2, fillalpha = 0.35, c = 1, label = "Confidence band", legend = :topleft)
Let's scatter y1 and y2 on top of the plot, just to make sure we're filling in the right region.
plot!(x,y1, line = :scatter, msw = 0, ms = 2.5, label = "Lower bound")
plot!(x,y2, line = :scatter, msw = 0, ms = 2.5, label = "Upper bound")
Result:
Example using ribbon:
mid = (y1 .+ y2) ./ 2 #the midpoints (usually representing mean values)
w = (y2 .- y1) ./ 2 #the vertical deviation around the means
plot(x, mid, ribbon = w , fillalpha = 0.35, c = 1, lw = 2, legend = :topleft, label = "Mean")
plot!(x,y1, line = :scatter, msw = 0, ms = 2.5, label = "Lower bound")
plot!(x,y2, line = :scatter, msw = 0, ms = 2.5, label = "Upper bound")
(Here, x, y1, and y2 are the same as before.)
Result:
Notice that the labels for ribbon and fillrange are different in the legends: the former labels the midpoints/means, while the latter labels the shaded region itself.
Some additional comments:
The OP's answer of plot(y, ribbon=(l,u), lab="estimate") is not correct (at least for Plots v1.10.1.). I realize this thread is over 3 years old, so perhaps it worked in the earlier version of Plots.jl that the OP was using at the time)
Similar to one of the answers given,
plot(x, [mid mid], fillrange=[mid .- w, mid .+ w], fillalpha=0.35, c = [1 4], label = ["Band 1" "Band 2"], legend = :topleft, dpi = 80)
will work, but this actually creates TWO ribbons (and hence, two icons in the legend) which may or may not be what the OP was looking for. To illustrate the point:
It turns out that the option ribbon accepts both lower and upper bounds:
plot(y, ribbon=(l,u), lab="estimate")
Notice that by passing l and u in the ribbon option, the filled area will correspond to the region between y-l and y+u. In other words, l and u should be the "deviations" from the mean curve y.
Something like this? (seen here).
plot([y y], fillrange=[y.-l y.+u], fillalpha=0.3, c=:orange)
plot!(y)
The fillrange solution in #leonidas 's answer might bring an additional boundary line (at least in Plots v1.35). To remove such a line, a workaround is to specify linealpha = 0, that is,
plot(x, l, fillrange = u, fillalpha = 0.35, c = 1, label = "Confidence band", linealpha = 0)

How to draw the curves in an energy diagram in R?

I wrote following R script:
#energy diagram
x <- c(0.1, 0.3, 0.5, 0.7, 0.9 ) #chosen randomly, reaction axis
y <- c(-5.057920, -5.057859, -5.057887,-5.057674, -5.057919 ) #energy of the educt, intermediate, transtition states and product
plot(x,y, type="p",
xlim=c(0,1),
ylim=c(-5.058,-5.0575),
xlab="reaction axis",
ylab=expression(paste(E[el] ," / ",10^6," ",kJ/mol)),
xaxt="n" #hide x-axis
)
#h- and v-lines, so i can draw curves by hand
abline(v=seq(0,1,0.1),h=seq(-5.0600,-5.0500,0.00005),col="black",lty=1,lwd=1)
abline(h=c(-5.057920, -5.057859, -5.057887,-5.057674), col="blue", lty=1,lwd=0.7)
Is it possible to draw a curve through the points that would look like a energy diagram. An example of an energy diagram is here:
A lot could be done to streamline / vectorize this code, but for a smallish diagram this works pretty well:
# get that data
x <- c(0.1, 0.3, 0.5, 0.7, 0.9 ) # reaction axis
y <- c(-5.057920, -5.057859, -5.057887,-5.057674, -5.057919 ) # energies
I'm going to make a little Bezier curve to connect each point to the next---this way we can make sure the smooth line passes through the data, not just close to it. I'll give each point a single 'control point' to define the slope. By using the same y-values for a point and it's control point, the slope at the point will be 0. I'll call the offset between the point and the control point delta. We'll start with one point-pair:
library(Hmisc)
delta = 0.15
bezx = c(0.1, 0.1 + delta, 0.3 - delta, 0.3)
bezy = rep(y[1:2], each = 2)
plot(bezx, bezy, type = 'b', col = "gray80")
lines(bezier(bezx, bezy), lwd = 2, col = "firebrick4")
Here I plotted the points and control points in gray, and the smooth line in red so we can see what's going on.
It looks promising, let's turn it into a function that we can apply to each pair of points:
bezf = function(x1, x2, y1, y2, delta = 0.15) {
bezier(x = c(x1, x1 + delta, x2 - delta, x2), y = c(y1, y1, y2, y2))
}
You can play with the delta parameter, I think 0.1 looks pretty good.
plot(x, y, xlab = "Reaction coordinate", ylab = "E", axes = F)
box(bty = "L")
axis(side = 2)
for(i in 1:(length(x) - 1)) {
lines(bezf(x1 = x[i], x2 = x[i + 1], y1 = y[i], y2 = y[i + 1], delta = 0.1))
}
You can of course tweak the plot, add labels, and ablines as in your original. (Use my for loop with the lines command to draw only the smoothed lines.) I left the points on to show that we are passing through them, not just getting close.
I prefer plotting in ggplot2, if you do too you'll need to extract the data into a data.frame:
bezlist = list()
for (i in 1:(length(x) - 1)) {
bezlist[[i]] = bezf(x1 = x[i], x2 = x[i + 1], y1 = y[i], y2 = y[i + 1], delta = 0.1)
}
xx = unlist(lapply(bezlist, FUN = '[', 'y'))
yy = unlist(lapply(bezlist, FUN = '[', 'y'))
bezdat = data.frame(react = xx, E = yy)
library(ggplot2)
ggplot(bezdat, aes(x = react, y = E)) +
geom_line() +
labs(x = "Reaction coordinate")
You could use a spline fit. Define some points along the energy diagram, and then fit to them using a spline function. The more points that you provide, the better that your fit will be. You can check out the smooth.splines function in the stats package for one implementation of the spline fit.

Resources