Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement contour labels #2496

Merged
merged 32 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4dc6e0d
first working implementation
t-bltg Dec 13, 2022
a6f4dac
hoist out `text!` and `boundingbox`
t-bltg Dec 13, 2022
1c1372b
handle color - add basic test
t-bltg Dec 13, 2022
c1fad44
add `label_attributes`
t-bltg Dec 13, 2022
7875e35
handle label rotation
t-bltg Dec 13, 2022
d7b7eab
rework angle
t-bltg Dec 13, 2022
3ae326e
attempt to fix `3D` labels
t-bltg Dec 13, 2022
1d0c001
fix 2d rot
t-bltg Dec 14, 2022
535f749
update masking algorithm
t-bltg Dec 14, 2022
21109ca
simplify
t-bltg Dec 14, 2022
373fbfe
split computation
t-bltg Dec 14, 2022
d7f61f9
rework `text!`
t-bltg Dec 15, 2022
5c2b08d
new bbox
t-bltg Dec 15, 2022
ed9adef
fix type
t-bltg Dec 15, 2022
2852dcc
add post-rotation note
t-bltg Dec 15, 2022
3f71b9c
attempt to reduce triggers
t-bltg Dec 15, 2022
9e26a4c
ignore depth component of bounding box
t-bltg Dec 15, 2022
140d4d2
fix labels on projection change
t-bltg Dec 17, 2022
37ca69e
rework label attributes
t-bltg Dec 18, 2022
97685a0
remove underscores
t-bltg Dec 18, 2022
c337cb8
update
t-bltg Dec 18, 2022
6144566
reorder
t-bltg Dec 18, 2022
2b03a17
ignore label attributes for volume contours
t-bltg Dec 18, 2022
f3e8582
add doc example
t-bltg Dec 21, 2022
50f1b80
update example
t-bltg Dec 21, 2022
eea86fa
add `labelformatter`
t-bltg Jan 15, 2023
133a4f6
Merge branch 'master' into cont_lab
asinghvi17 Jan 26, 2023
e3939f3
Merge branch 'master' into cont_lab
asinghvi17 Feb 6, 2023
17ebff1
Merge branch 'master' into cont_lab
SimonDanisch Feb 7, 2023
29a9d2c
Merge branch 'master' into cont_lab
SimonDanisch Feb 12, 2023
a458994
Merge branch 'master' into cont_lab
t-bltg Apr 1, 2023
704e54b
Update contours.jl
SimonDanisch Apr 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
149 changes: 128 additions & 21 deletions src/basic_recipes/contours.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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`.

## Attributes
$(ATTRIBUTES)
"""
Expand All @@ -28,7 +30,11 @@ $(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
labelsize = 10, # arbitrary
)
end

Expand All @@ -45,32 +51,50 @@ $(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_short = round(lev; digits = 2)
(
string(isinteger(lev_short) ? round(Int, lev_short) : lev_short),
t-bltg marked this conversation as resolved.
Show resolved Hide resolved
map(p -> to_ndim(Point3f, p, lev), Tuple(pts)),
col,
)
end

function contourlines(::Type{<: Contour}, contours, cols, labels)
points = Point2f[]
colors = RGBA{Float32}[]
str_pos_col = Tuple{String,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!(str_pos_col, label_info(c.level, elem.vertices, color))
end
end
result, colors
points, colors, str_pos_col
end

function contourlines(::Type{<: Contour3d}, contours, cols)
result = Point3f[]
function contourlines(::Type{<: Contour3d}, contours, cols, labels)
points = Point3f[]
colors = RGBA{Float32}[]
str_pos_col = Tuple{String,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!(str_pos_col, label_info(c.level, elem.vertices, color))
end
end
result, colors
points, colors, str_pos_col
end

to_levels(x::AbstractVector{<: Number}, cnorm) = x
Expand Down Expand Up @@ -106,15 +130,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 @@ -123,6 +147,11 @@ 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)
volume!(plot, attr, x, y, z, volume)
end

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

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

@extract plot (labels, labelsize, labelfont, labelcolor)
args = @extract plot (color, colormap, colorrange, alpha)
level_colors = lift(color_per_level, args..., levels)
result = lift(x, y, z, levels, level_colors) do x, y, z, levels, level_colors
cont_lines = lift(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, cont_lines) do _, _,
labels, labelcolor, (_, _, str_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 (str, (p1, p2, p3), color) in str_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, str)
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, result);
color = lift(last, result),
plot, masked_lines;
color = lift(x -> x[2], cont_lines),
linewidth = plot.linewidth,
inspectable = plot.inspectable,
transparency = plot.transparency,
Expand Down