From 85d504551e4e7855a2ac20029e031d187b687ac4 Mon Sep 17 00:00:00 2001 From: Jerry Ling Date: Thu, 6 Mar 2025 00:18:04 -0500 Subject: [PATCH 1/6] add smart fillto to enable logscale y-axis stacked barplot --- src/basic_recipes/barplot.jl | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index ecb4a1a4b45..5a8b0f7180f 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -17,23 +17,28 @@ Returns a Tuple of new y positions and offset arrays. - `ys`: The y-values passed to `barplot`. - `offset`: The `offset` parameter passed to `barplot`. """ -function bar_default_fillto(tf, ys, offset, in_y_direction) - return ys, offset -end - -# `fillto` is related to `y-axis` transformation only, thus we expect `tf::Tuple` -function bar_default_fillto(tf::Tuple, ys, offset, in_y_direction) - _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} - if in_y_direction && tf[2] isa _logT || (!in_y_direction && tf[1] isa _logT) +function bar_default_fillto(tf, ys, offset, in_y_direction; log_clamp=false) + if log_clamp # x-scale log and !(in_y_direction) is equiavlent to y-scale log in_y_direction # use the minimal non-zero y divided by 2 as lower bound for log scale - smart_fillto = minimum(y -> y<=0 ? oftype(y, Inf) : y, ys) / 2 - return clamp.(ys, smart_fillto, Inf), smart_fillto + _smart_log_clamp(ys) else return ys, offset end end +""" + _smart_log_clamp(ys, ref=ys)::(ys_clamped, min_ref/2) + +Clamps the values of `ys` to a minimum value that is half the minimum non-zero value in `ref`. +""" +function _smart_log_clamp(ys, ref=ys) + # x-scale log and !(in_y_direction) is equiavlent to y-scale log in_y_direction + # use the minimal non-zero y divided by 2 as lower bound for log scale + smart_fillto = minimum(y -> y<=0 ? oftype(y, Inf) : y, ref) / 2 + return clamp.(ys, smart_fillto, Inf), smart_fillto +end + """ barplot(positions, heights; kwargs...) @@ -141,7 +146,7 @@ function stack_from_to(i_stack, y) (from = view(from, inv_perm), to = view(to, inv_perm)) end -function stack_grouped_from_to(i_stack, y, grp) +function stack_grouped_from_to(i_stack, y, grp; log_clamp=false) from = Array{Float64}(undef, length(y)) to = Array{Float64}(undef, length(y)) @@ -162,6 +167,9 @@ function stack_grouped_from_to(i_stack, y, grp) to[inds] .= fromto.to end + if log_clamp + from, _ = _smart_log_clamp(from, to) + end (from = from, to = to) end @@ -291,9 +299,11 @@ function Makie.plot!(p::BarPlot) # ----------- Stacking ----------- # -------------------------------- + _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} + log_clamp = transformation isa Tuple && in_y_direction && transformation[2] isa _logT || (!in_y_direction && transformation[1] isa _logT) if stack === automatic if fillto === automatic - y, fillto = bar_default_fillto(transformation, y, offset, in_y_direction) + y, fillto = bar_default_fillto(transformation, y, offset, in_y_direction; log_clamp = log_clamp) end elseif eltype(stack) <: Integer fillto === automatic || @warn "Ignore keyword fillto when keyword stack is provided" @@ -303,7 +313,7 @@ function Makie.plot!(p::BarPlot) end i_stack = stack - from, to = stack_grouped_from_to(i_stack, y, (x = x̂,)) + from, to = stack_grouped_from_to(i_stack, y, (x = x̂,); log_clamp = log_clamp) y, fillto = to, from else ArgumentError("The keyword argument `stack` currently supports only `AbstractVector{<: Integer}`") |> throw From 8f81e66e3239802bbcf9007edec56fc6388facb8 Mon Sep 17 00:00:00 2001 From: Jerry Ling Date: Thu, 6 Mar 2025 00:21:57 -0500 Subject: [PATCH 2/6] add reference image --- ReferenceTests/src/tests/examples2d.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index c661ce1f60d..b4cf06b5887 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1108,6 +1108,12 @@ end f end +@reference_test "Log scale stacked barplot" begin + f = Figure() + barplot(f[1,1], [1,2,3,1,2,3], [1,2,3,1,2,3], stack=[1,1,1,2,2,2], color=[1,1,1,2,2,2], gap=0; axis=(; yscale=log)) + f +end + @reference_test "Log scale histogram (barplot)" begin f = Figure() hist( @@ -1925,4 +1931,4 @@ end translate!(a.scene, 0.1, 0.05) # test that pattern are anchored to the plot Makie.step!(st) st -end \ No newline at end of file +end From d19c7700a52fb4a2479d345ff839f1263fdf997b Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 6 Mar 2025 11:34:42 -0500 Subject: [PATCH 3/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1693624d37a..3a46ede1953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## [Unreleased] - +- Use the `bar_default_fillto` method from [#3004](https://github.com/MakieOrg/Makie.jl/pull/3004) in stacked barplots as well, to stop the first member of the stack from disappearing in log scale [#4670](https://github.com/MakieOrg/Makie.jl/pull/4849). ## [0.22.2] - 2025-02-26 From c406bfb80d5a8813bb509c505d90af2c6596201b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 5 Apr 2025 05:49:44 +0200 Subject: [PATCH 4/6] try explicit transform with tweaked boundingbox --- src/basic_recipes/barplot.jl | 98 ++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 5a8b0f7180f..6f19f8adfa4 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -2,43 +2,6 @@ bar_label_formatter(value::Number) = string(round(value; digits=3)) bar_label_formatter(label::String) = label bar_label_formatter(label::LaTeXString) = label -""" - bar_default_fillto(tf, ys, offset)::(ys, offset) - -Returns the default y-positions and offset positions for the given transform `tf`. - -In order to customize this for your own transformation type, you can dispatch on -`tf`. - -Returns a Tuple of new y positions and offset arrays. - -## Arguments -- `tf`: `plot.transformation.transform_func[]`. -- `ys`: The y-values passed to `barplot`. -- `offset`: The `offset` parameter passed to `barplot`. -""" -function bar_default_fillto(tf, ys, offset, in_y_direction; log_clamp=false) - if log_clamp - # x-scale log and !(in_y_direction) is equiavlent to y-scale log in_y_direction - # use the minimal non-zero y divided by 2 as lower bound for log scale - _smart_log_clamp(ys) - else - return ys, offset - end -end - -""" - _smart_log_clamp(ys, ref=ys)::(ys_clamped, min_ref/2) - -Clamps the values of `ys` to a minimum value that is half the minimum non-zero value in `ref`. -""" -function _smart_log_clamp(ys, ref=ys) - # x-scale log and !(in_y_direction) is equiavlent to y-scale log in_y_direction - # use the minimal non-zero y divided by 2 as lower bound for log scale - smart_fillto = minimum(y -> y<=0 ? oftype(y, Inf) : y, ref) / 2 - return clamp.(ys, smart_fillto, Inf), smart_fillto -end - """ barplot(positions, heights; kwargs...) @@ -93,13 +56,24 @@ end conversion_trait(::Type{<: BarPlot}) = PointBased() -function bar_rectangle(x, y, width, fillto, in_y_direction) +function bar_rectangle(x, y, width, fillto, in_y_direction, transform_func) # y could be smaller than fillto... ymin = min(fillto, y) ymax = max(fillto, y) w = abs(width) rect = Rectd(x - (w / 2f0), ymin, w, ymax - ymin) - return in_y_direction ? rect : flip(rect) + rect = in_y_direction ? rect : flip(rect) + + # To deal with log transforms we explicitly transform here and "fix" ±Inf + # results by clamping them to numeric values. Since Rect2f is represented + # with widths we need to make sure that origin + widths is still resolvable + # with enough precision to correctly align rects. This is done by + # max_resolvable requiring at ~100 eps precision at each vertex. + ps = coordinates(rect) + ps = apply_transform(transform_func, ps) + max_resolvable = 0.01 ./ max.(mapreduce(p -> eps.(p), finite_max, ps), eps(Float64)) + ps = map(p -> clamp.(p, -max_resolvable, max_resolvable), ps) + return Rect2d(ps) end flip(r::Rect2) = Rect2(reverse(origin(r)), reverse(widths(r))) @@ -146,7 +120,7 @@ function stack_from_to(i_stack, y) (from = view(from, inv_perm), to = view(to, inv_perm)) end -function stack_grouped_from_to(i_stack, y, grp; log_clamp=false) +function stack_grouped_from_to(i_stack, y, grp) from = Array{Float64}(undef, length(y)) to = Array{Float64}(undef, length(y)) @@ -167,10 +141,7 @@ function stack_grouped_from_to(i_stack, y, grp; log_clamp=false) to[inds] .= fromto.to end - if log_clamp - from, _ = _smart_log_clamp(from, to) - end - (from = from, to = to) + return (from = from, to = to) end function calculate_bar_label_align(label_align, label_rotation::Real, in_y_direction::Bool, flip::Bool) @@ -299,11 +270,9 @@ function Makie.plot!(p::BarPlot) # ----------- Stacking ----------- # -------------------------------- - _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} - log_clamp = transformation isa Tuple && in_y_direction && transformation[2] isa _logT || (!in_y_direction && transformation[1] isa _logT) if stack === automatic if fillto === automatic - y, fillto = bar_default_fillto(transformation, y, offset, in_y_direction; log_clamp = log_clamp) + fillto = offset end elseif eltype(stack) <: Integer fillto === automatic || @warn "Ignore keyword fillto when keyword stack is provided" @@ -313,7 +282,7 @@ function Makie.plot!(p::BarPlot) end i_stack = stack - from, to = stack_grouped_from_to(i_stack, y, (x = x̂,); log_clamp = log_clamp) + from, to = stack_grouped_from_to(i_stack, y, (x = x̂,)) y, fillto = to, from else ArgumentError("The keyword argument `stack` currently supports only `AbstractVector{<: Integer}`") |> throw @@ -332,7 +301,7 @@ function Makie.plot!(p::BarPlot) labels[], label_aligns[], label_offsets[], label_colors[] = label_args end - return bar_rectangle.(x̂, y .+ offset, barwidth, fillto, in_y_direction) + return bar_rectangle.(x̂, y .+ offset, barwidth, fillto, in_y_direction, Ref(transformation)) end bars = lift(calculate_bars, p, p[1], p.fillto, p.offset, p.transformation.transform_func, p.width, p.dodge, p.n_dodge, p.gap, @@ -343,9 +312,40 @@ function Makie.plot!(p::BarPlot) strokewidth = p.strokewidth, strokecolor = p.strokecolor, visible = p.visible, inspectable = p.inspectable, transparency = p.transparency, space = p.space, highclip = p.highclip, lowclip = p.lowclip, nan_color = p.nan_color, alpha = p.alpha, + transformation = :inherit_model ) if !isnothing(p.bar_labels[]) text!(p, labels; align=label_aligns, offset=label_offsets, color=label_colors, font=p.label_font, fontsize=p.label_size, rotation=p.label_rotation) end end + +data_limits(p::BarPlot) = update_boundingbox(Rect3d(p[1][]), Vec3d(NaN, 0, NaN)) +function boundingbox(p::BarPlot, space::Symbol = :data) + # plot construction will error check this + in_y_direction = p.direction[] == :y + transformation = transform_func(p) + _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} + is_log = transformation isa Tuple && in_y_direction && transformation[2] isa _logT || (!in_y_direction && transformation[1] isa _logT) + + bb_transformed = boundingbox(p.plots[1]) + if !is_log + return bb_transformed + else + # use the minimal non-zero y divided by 2 as lower bound for log scale + ps = p[1][] + dim = ifelse(in_y_direction, 2, 1) + smart_fillto = minimum(p -> p[dim] <= 0 ? oftype(p[dim], Inf) : p[dim], ps) / 2 + + # get transformed fillto + mini = to_ndim(Point3d, minimum(ps), 0) + mini = ntuple(i -> ifelse(i == dim, smart_fillto, mini[i]), 3) + fillto_transformed = apply_transform_and_model(p, mini)[2] + + # replace matching origin in the transformed rect (do not adjust and transform + # data_limits as those don't include bar widths) + mini, maxi = extrema(bb_transformed) + mini = ntuple(i -> ifelse(i == dim, fillto_transformed, mini[i]), 3) + return Rect3d(mini, maxi .- mini) + end +end \ No newline at end of file From 4bdc6a62963cc0dabac287d4b95773b57e7f5b3d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 5 Apr 2025 15:53:17 +0200 Subject: [PATCH 5/6] fix issues --- src/basic_recipes/barplot.jl | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 6f19f8adfa4..fd9aa32b174 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -63,17 +63,11 @@ function bar_rectangle(x, y, width, fillto, in_y_direction, transform_func) w = abs(width) rect = Rectd(x - (w / 2f0), ymin, w, ymax - ymin) rect = in_y_direction ? rect : flip(rect) - - # To deal with log transforms we explicitly transform here and "fix" ±Inf - # results by clamping them to numeric values. Since Rect2f is represented - # with widths we need to make sure that origin + widths is still resolvable - # with enough precision to correctly align rects. This is done by - # max_resolvable requiring at ~100 eps precision at each vertex. - ps = coordinates(rect) - ps = apply_transform(transform_func, ps) - max_resolvable = 0.01 ./ max.(mapreduce(p -> eps.(p), finite_max, ps), eps(Float64)) - ps = map(p -> clamp.(p, -max_resolvable, max_resolvable), ps) - return Rect2d(ps) + # Transform coordinates of bar rectangle and clamp result to a workable value range. + # Do not repack as Rect because the representation with widths can cause float + # precision issues for vertices. + ps = apply_transform(transform_func, coordinates(rect)) + return map(p -> clamp.(p, -1e35, 1e35), ps) end flip(r::Rect2) = Rect2(reverse(origin(r)), reverse(widths(r))) @@ -328,24 +322,33 @@ function boundingbox(p::BarPlot, space::Symbol = :data) _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} is_log = transformation isa Tuple && in_y_direction && transformation[2] isa _logT || (!in_y_direction && transformation[1] isa _logT) - bb_transformed = boundingbox(p.plots[1]) if !is_log + bb_transformed = boundingbox(p.plots[1]) return bb_transformed else # use the minimal non-zero y divided by 2 as lower bound for log scale ps = p[1][] dim = ifelse(in_y_direction, 2, 1) - smart_fillto = minimum(p -> p[dim] <= 0 ? oftype(p[dim], Inf) : p[dim], ps) / 2 + smart_min = minimum(p -> p[dim] <= 0 ? oftype(p[dim], Inf) : p[dim], ps) / 2 # get transformed fillto mini = to_ndim(Point3d, minimum(ps), 0) - mini = ntuple(i -> ifelse(i == dim, smart_fillto, mini[i]), 3) - fillto_transformed = apply_transform_and_model(p, mini)[2] - - # replace matching origin in the transformed rect (do not adjust and transform - # data_limits as those don't include bar widths) - mini, maxi = extrema(bb_transformed) - mini = ntuple(i -> ifelse(i == dim, fillto_transformed, mini[i]), 3) - return Rect3d(mini, maxi .- mini) + mini = ntuple(i -> ifelse(i == dim, smart_min, mini[i]), 3) + smart_min_transformed = apply_transform_and_model(p, mini)[dim] + + # Since Rect represents maximum as rect.origin + rect.widths it will + # have float precision issues if maximum ≲ eps(maximum) * widths. To + # avoid this we need to explicitly calculate the bounds: + rect_verts = p.plots[1][1][] + mini_transformed = Point3d(Inf) + maxi_transformed = Point3d(-Inf) + for verts in rect_verts + low, high = extrema(verts) + mini_transformed = min.(mini_transformed, to_ndim(Point3d, low, 0)) + maxi_transformed = max.(maxi_transformed, to_ndim(Point3d, high, 0)) + end + # With smart_min_transformed it should be resolvable + mini_transformed = ntuple(i -> ifelse(i == dim, smart_min_transformed, mini_transformed[i]), 3) + return apply_model(p.model[], Rect3d(mini_transformed, maxi_transformed .- mini_transformed)) end end \ No newline at end of file From 6cd5e7e3507fafb399ae4357c84b69b6afced481 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 5 Apr 2025 17:18:24 +0200 Subject: [PATCH 6/6] try lower value limits --- src/basic_recipes/barplot.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index fd9aa32b174..81dd3304971 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -67,7 +67,7 @@ function bar_rectangle(x, y, width, fillto, in_y_direction, transform_func) # Do not repack as Rect because the representation with widths can cause float # precision issues for vertices. ps = apply_transform(transform_func, coordinates(rect)) - return map(p -> clamp.(p, -1e35, 1e35), ps) + return map(p -> clamp.(p, -1e32, 1e32), ps) end flip(r::Rect2) = Rect2(reverse(origin(r)), reverse(widths(r)))