Skip to content

Commit

Permalink
implement contour labels (#2496)
Browse files Browse the repository at this point in the history
* first working implementation

* hoist out `text!` and `boundingbox`

* handle color - add basic test

* add `label_attributes`

* handle label rotation

* rework angle

* attempt to fix `3D` labels

* fix 2d rot

* update masking algorithm

* simplify

* split computation

* rework `text!`

* new bbox

* fix type

* add post-rotation note

* attempt to reduce triggers

* ignore depth component of bounding box

* fix labels on projection change

* rework label attributes

* remove underscores

* update

* reorder

* ignore label attributes for volume contours

* add doc example

* update example

* add `labelformatter`

* Update contours.jl

---------

Co-authored-by: Anshul Singhvi <asinghvi17@simons-rock.edu>
Co-authored-by: Simon <sdanisch@protonmail.com>
  • Loading branch information
3 people committed Apr 1, 2023
1 parent 6f507c1 commit fb1f7f7
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 21 deletions.
31 changes: 31 additions & 0 deletions ReferenceTests/src/tests/examples2d.jl
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,37 @@ end
f
end

@reference_test "contour labels 2D" begin
paraboloid = (x, y) -> 10(x^2 + y^2)

x = range(-4, 4; length = 40)
y = range(-4, 4; length = 60)
z = paraboloid.(x, y')

fig, ax, hm = heatmap(x, y, z)
Colorbar(fig[1, 2], hm)

contour!(
ax, x, y, z;
color = :red, levels = 0:20:100, labels = true,
labelsize = 15, labelfont = :bold, labelcolor = :orange,
)
fig
end

@reference_test "contour labels 3D" begin
fig = Figure()
Axis3(fig[1, 1])

xs = ys = range(-.5, .5; length = 50)
zs = @. (xs^2 + ys'^2)

levels = .025:.05:.475
contour3d!(-zs; levels = -levels, labels = true, color = :blue)
contour3d!(+zs; levels = +levels, labels = true, color = :red, labelcolor = :black)
fig
end

@reference_test "marker offset in data space" begin
f = Figure()
ax = Axis(f[1, 1]; xticks=0:1, yticks=0:10)
Expand Down
19 changes: 19 additions & 0 deletions docs/examples/plotting_functions/contour.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,22 @@ contour!(zs,levels=-1:0.1:1)
f
```
\end{examplefigure}

One can also add labels and control label attributes such as `labelsize`, `labelcolor` or `labelfont`.

\begin{examplefigure}{}
```julia
using CairoMakie
CairoMakie.activate!() # hide


himmelblau(x, y) = (x^2 + y - 11)^2 + (x + y^2 - 7)^2
x = y = range(-6, 6; length=100)
z = himmelblau.(x, y')

levels = 10.0.^range(0.3, 3.5; length=10)
colormap = Makie.sampler(:hsv, 100; scaling=Makie.Scaling(x -> x^(1 / 10), nothing))
f, ax, ct = contour(x, y, z; labels=true, levels, colormap)
f
```
\end{examplefigure}
154 changes: 133 additions & 21 deletions src/basic_recipes/contours.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
function contour_label_formatter(level::Real)::String
lev_short = round(level; digits = 2)
string(isinteger(lev_short) ? round(Int, lev_short) : lev_short)
end

"""
contour(x, y, z)
Expand All @@ -12,6 +16,8 @@ The attribute levels can be either
an AbstractVector{<:Real} that lists n consecutive edges from low to high, which result in n-1 levels or bands
To add contour labels, use `labels = true`, and pass additional label attributes such as `labelcolor`, `labelsize`, `labelfont` or `labelformatter`.
## Attributes
$(ATTRIBUTES)
"""
Expand All @@ -28,7 +34,12 @@ $(ATTRIBUTES)
linestyle = nothing,
alpha = 1.0,
enable_depth = true,
transparency = false
transparency = false,
labels = false,
labelfont = theme(scene, :font),
labelcolor = nothing, # matches color by default
labelformatter = contour_label_formatter,
labelsize = 10, # arbitrary
)
end

Expand All @@ -45,32 +56,49 @@ $(ATTRIBUTES)
default_theme(scene, Contour)
end

function contourlines(::Type{<: Contour}, contours, cols)
result = Point2f[]
angle(p1::Union{Vec2f,Point2f}, p2::Union{Vec2f,Point2f})::Float32 =
atan(p2[2] - p1[2], p2[1] - p1[1]) # result in [-π, π]

function label_info(lev, vertices, col)
mid = ceil(Int, 0.5f0 * length(vertices))
pts = (vertices[max(firstindex(vertices), mid - 1)], vertices[mid], vertices[min(mid + 1, lastindex(vertices))])
(
lev,
map(p -> to_ndim(Point3f, p, lev), Tuple(pts)),
col,
)
end

function contourlines(::Type{<: Contour}, contours, cols, labels)
points = Point2f[]
colors = RGBA{Float32}[]
lev_pos_col = Tuple{Float32,NTuple{3,Point2f},RGBA{Float32}}[]
for (color, c) in zip(cols, Contours.levels(contours))
for elem in Contours.lines(c)
append!(result, elem.vertices)
push!(result, Point2f(NaN32))
append!(points, elem.vertices)
push!(points, Point2f(NaN32))
append!(colors, fill(color, length(elem.vertices) + 1))
labels && push!(lev_pos_col, label_info(c.level, elem.vertices, color))
end
end
result, colors
points, colors, lev_pos_col
end

function contourlines(::Type{<: Contour3d}, contours, cols)
result = Point3f[]
function contourlines(::Type{<: Contour3d}, contours, cols, labels)
points = Point3f[]
colors = RGBA{Float32}[]
lev_pos_col = Tuple{Float32,NTuple{3,Point3f},RGBA{Float32}}[]
for (color, c) in zip(cols, Contours.levels(contours))
for elem in Contours.lines(c)
for p in elem.vertices
push!(result, Point3f(p[1], p[2], c.level))
push!(points, to_ndim(Point3f, p, c.level))
end
push!(result, Point3f(NaN32))
push!(points, Point3f(NaN32))
append!(colors, fill(color, length(elem.vertices) + 1))
labels && push!(lev_pos_col, label_info(c.level, elem.vertices, color))
end
end
result, colors
points, colors, lev_pos_col
end

to_levels(x::AbstractVector{<: Number}, cnorm) = x
Expand Down Expand Up @@ -104,15 +132,15 @@ function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol}
cmap = to_colormap(_cmap)
v_interval = cliprange[1] .. cliprange[2]
# resample colormap and make the empty area between iso surfaces transparent
return map(1:N) do i
map(1:N) do i
i01 = (i-1) / (N - 1)
c = Makie.interpolated_getindex(cmap, i01)
isoval = vrange[1] + (i01 * (vrange[2] - vrange[1]))
line = reduce(levels, init = false) do v0, level
(isoval in v_interval) || return false
v0 || (abs(level - isoval) <= iso_eps)
isoval in v_interval || return false
v0 || abs(level - isoval) <= iso_eps
end
return RGBAf(Colors.color(c), line ? alpha : 0.0)
RGBAf(Colors.color(c), line ? alpha : 0.0)
end
end

Expand All @@ -121,6 +149,12 @@ function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol}
attr[:colormap] = cmap
attr[:algorithm] = 7
pop!(attr, :levels)
# unused attributes
pop!(attr, :labels)
pop!(attr, :labelfont)
pop!(attr, :labelsize)
pop!(attr, :labelcolor)
pop!(attr, :labelformatter)
volume!(plot, attr, x, y, z, volume)
end

Expand Down Expand Up @@ -170,18 +204,96 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d}

replace_automatic!(()-> zrange, plot, :colorrange)

@extract plot (labels, labelsize, labelfont, labelcolor, labelformatter)
args = @extract plot (color, colormap, colorrange, alpha)
level_colors = lift(color_per_level, plot, args..., levels)
result = lift(plot, x, y, z, levels, level_colors) do x, y, z, levels, level_colors
cont_lines = lift(plot, x, y, z, levels, level_colors, labels) do x, y, z, levels, level_colors, labels
t = eltype(z)
# Compute contours
xv, yv = to_vector(x, size(z,1), t), to_vector(y, size(z,2), t)
contours = Contours.contours(xv, yv, z, convert(Vector{eltype(z)}, levels))
contourlines(T, contours, level_colors)
xv, yv = to_vector(x, size(z, 1), t), to_vector(y, size(z, 2), t)
contours = Contours.contours(xv, yv, z, convert(Vector{t}, levels))
contourlines(T, contours, level_colors, labels)
end

P = T <: Contour ? Point2f : Point3f
scene = parent_scene(plot)
space = plot.space[]

texts = text!(
plot,
Observable(P[]);
color = Observable(RGBA{Float32}[]),
rotation = Observable(Float32[]),
text = Observable(String[]),
align = (:center, :center),
fontsize = labelsize,
font = labelfont,
)

lift(scene.camera.projectionview, scene.px_area, labels, labelcolor, labelformatter, cont_lines) do _, _,
labels, labelcolor, labelformatter, (_, _, lev_pos_col)
labels || return
pos = texts.positions.val; empty!(pos)
rot = texts.rotation.val; empty!(rot)
col = texts.color.val; empty!(col)
lbl = texts.text.val; empty!(lbl)
for (lev, (p1, p2, p3), color) in lev_pos_col
rot_from_horz::Float32 = angle(project(scene, p1), project(scene, p3))
# transition from an angle from horizontal axis in [-π; π]
# to a readable text with a rotation from vertical axis in [-π / 2; π / 2]
rot_from_vert::Float32 = if abs(rot_from_horz) > 0.5f0 * π
rot_from_horz - copysign(Float32(π), rot_from_horz)
else
rot_from_horz
end
push!(col, labelcolor === nothing ? color : to_color(labelcolor))
push!(rot, rot_from_vert)
push!(lbl, labelformatter(lev))
push!(pos, p1)
end
notify(texts.text)
nothing
end

bboxes = lift(labels, texts.text) do labels, _
labels || return
broadcast(texts.plots[1][1].val, texts.positions.val, texts.rotation.val) do gc, pt, rot
# drop the depth component of the bounding box for 3D
Rect2f(boundingbox(gc, project(scene.camera, space, :pixel, pt), to_rotation(rot)))
end
end

masked_lines = lift(labels, bboxes) do labels, bboxes
segments = cont_lines.val[1]
labels || return segments
n = 1
bb = bboxes[n]
nlab = length(bboxes)
masked = copy(segments)
nan = P(NaN32)
for (i, p) in enumerate(segments)
if isnan(p) && n < nlab
bb = bboxes[n += 1] # next segment is materialized by a NaN, thus consider next label
# wireframe!(plot, bb, space = :pixel) # toggle to debug labels
elseif project(scene.camera, space, :pixel, p) in bb
masked[i] = nan
for dir in (-1, +1)
j = i
while true
j += dir
checkbounds(Bool, segments, j) || break
project(scene.camera, space, :pixel, segments[j]) in bb || break
masked[j] = nan
end
end
end
end
masked
end

lines!(
plot, lift(first, plot, result);
color=lift(last, plot, result),
plot, masked_lines;
color = lift(x -> x[2], plot, cont_lines),
linewidth = plot.linewidth,
inspectable = plot.inspectable,
transparency = plot.transparency,
Expand Down

0 comments on commit fb1f7f7

Please sign in to comment.