# Colour carries one distinction only: red is the truck, whether it is
# loaded or not. Loaded and empty are told apart by the line style and by
# the L and U labels, the way the slide tells them apart.
truck = RGBf(0.80, 0.11, 0.11)
dist = RGBf(0.08, 0.10, 0.16)
fillc = RGBf(0.78, 0.92, 0.78)
edgec = RGBf(0.25, 0.55, 0.30)
ink = RGBf(0.10, 0.12, 0.14)
# EQUAL SCALE ON BOTH AXES IS WHAT MAKES THE GEOMETRY WORK, and leaving it
# out is what put white gaps between the arrows and the nodes: a node is a
# marker sized in PIXELS, so trimming a lane by a radius in DATA units is
# only right when one data unit is the same number of pixels each way.
# DataAspect fixes the ratio; the limits below are chosen to match the
# figure's own so nothing is padded to achieve it.
XLO, XHI, YLO, YHI = -0.10, 5.55, 0.22, 1.36
WIDTH = 687
PXU = WIDTH / (XHI - XLO) # pixels per data unit, both axes
# (x, y, marker size in px). The free ends of the inbound and outbound
# lanes are not nodes at all, so they have no marker and no radius.
GSO = (0.60, 0.52, 18)
RDU = (1.45, 1.05, 26)
GNV = (3.95, 1.05, 26)
JAX = (4.80, 0.52, 18)
inb = (0.00, 0.97, 0)
out = (5.40, 0.97, 0)
rad(nd) = nd[3] / 2 / PXU # node radius in data units
HEAD = 12 # arrowhead marker size, px
# A lane ENDS ON the node outline. `dy` displaces the whole lane
# sideways, which shortens the chord it cuts from a circular node,
# so the trim is the half-chord sqrt(r^2 - h^2) and not r: that is
# the gap on the two
# Raleigh-to-Gainesville edges. The arrowhead is then pulled back by half
# its own height so its TIP lands on the outline rather than inside it.
function lane!(ax, a, b, style; dy = 0.0, lab = "", side = -1)
L = hypot(b[1] - a[1], b[2] - a[2])
ux, uy = (b[1] - a[1]) / L, (b[2] - a[2]) / L
trim(nd) = (r = rad(nd); h = abs(dy * ux);
r <= h ? 0.0 : sqrt(r^2 - h^2))
ta, tb = trim(a), trim(b)
x1, y1 = a[1] + ta*ux, a[2] + ta*uy + dy
x2, y2 = b[1] - tb*ux, b[2] - tb*uy + dy
lines!(ax, [x1, x2], [y1, y2]; color = truck, linewidth = 2.0,
linestyle = style)
back = HEAD / 2 / PXU
scatter!(ax, [x2 - back*ux], [y2 - back*uy]; color = truck,
marker = :utriangle, markersize = HEAD,
rotation = atan(uy, ux) - pi/2)
if lab != ""
# Perpendicular to the lane, so a label can never sit on its own
# arrow however the lane is angled.
px, py = -uy, ux
off = 15 / PXU
text!(ax, (x1 + x2)/2 + side*px*off, (y1 + y2)/2 + side*py*off;
text = lab, color = truck, fontsize = 17,
align = (:center, :center))
end
end
fig = Figure(size = (WIDTH, round(Int, (YHI - YLO) * PXU)))
ax = Axis(fig[1, 1]; aspect = DataAspect())
hidedecorations!(ax); hidespines!(ax)
limits!(ax, XLO, XHI, YLO, YHI)
lane!(ax, inb, GSO, :solid; lab = "L", side = -1) # arrives loaded
lane!(ax, GSO, RDU, :dash; lab = "U", side = -1) # repositions empty
lane!(ax, RDU, GNV, :solid; dy = -0.035, lab = "L", side = -1)
lane!(ax, GNV, JAX, :dash; lab = "U", side = 1) # repositions empty
lane!(ax, JAX, out, :solid; lab = "L", side = -1) # away loaded again
# The distance is a fact about the lane rather than a movement of the
# truck, so it is black and unarrowed, above the leg it measures. Same
# half-chord trim, so it meets both nodes as the red lane does.
let dy = 0.035, h = sqrt(rad(RDU)^2 - dy^2)
lines!(ax, [RDU[1] + h, GNV[1] - h], fill(RDU[2] + dy, 2);
color = dist, linewidth = 1.6)
end
text!(ax, 2.70, RDU[2] + 0.06; text = "532 mi", color = dist,
align = (:center, :bottom), fontsize = 14)
for (nd, nm, above) in [(GSO, "Greensboro", false),
(RDU, "Raleigh", true),
(GNV, "Gainesville", true),
(JAX, "Jacksonville", false)]
scatter!(ax, [nd[1]], [nd[2]]; color = fillc, markersize = nd[3],
strokecolor = edgec, strokewidth = 1.8)
text!(ax, nd[1], nd[2] + (above ? 1 : -1) * (rad(nd) + 0.045);
text = nm, color = ink,
align = (:center, above ? :bottom : :top), fontsize = 15)
end
fig