diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f298005b1..b1690be935e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- Added `scale` attribute to `violin` [#3352](https://github.com/MakieOrg/Makie.jl/pull/3352). ## [0.20.8] - 2024-02-22 diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 72a8cb6a595..f1d0a4608b4 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1392,3 +1392,16 @@ end ylims!(ax, 0, 1) fig end + +@reference_test "Violin plots differently scaled" begin + fig = Figure() + xs = vcat([fill(i, i * 1000) for i in 1:4]...) + ys = vcat(randn(6000), randn(4000) * 2) + for (i, scale) in enumerate([:area, :count, :width]) + ax = Axis(fig[i, 1]) + violin!(ax, xs, ys; scale, show_median=true) + Makie.xlims!(0.2, 4.8) + ax.title = "scale=:$(scale)" + end + fig +end diff --git a/docs/reference/plots/violin.md b/docs/reference/plots/violin.md index 82ac842180b..9d4ffd1754b 100644 --- a/docs/reference/plots/violin.md +++ b/docs/reference/plots/violin.md @@ -17,6 +17,25 @@ violin(categories, values) ``` \end{examplefigure} +\begin{examplefigure}{} +```julia +using Makie, CairoMakie +CairoMakie.activate!() # hide + + +fig = Figure() +xs = vcat([fill(i, i * 1000) for i in 1:4]...) +ys = vcat(randn(6000), randn(4000) * 2) +for (i, scale) in enumerate([:area, :count, :width]) + ax = Axis(fig[i, 1]) + violin!(ax, xs, ys; scale, show_median=true) + Makie.xlims!(0.2, 4.8) + ax.title = "scale=:$(scale)" +end +fig +``` +\end{examplefigure} + \begin{examplefigure}{} ```julia using CairoMakie diff --git a/src/stats/violin.jl b/src/stats/violin.jl index d3d7d5c9707..73bbe24a788 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -11,6 +11,7 @@ Draw a violin plot. - `gap=0.2`: shrinking factor, `width -> width * (1 - gap)` - `show_median=false`: show median as midline - `side=:both`: specify `:left` or `:right` to only plot the violin on one side +- `scale=:width`: scale density by area (`:area`), count (`:count`), or width (`:width`). - `datalimits`: specify values to trim the `violin`. Can be a `Tuple` or a `Function` (e.g. `datalimits=extrema`) """ @recipe(Violin, x, y) do scene @@ -21,6 +22,7 @@ Draw a violin plot. bandwidth = automatic, weights = automatic, side = :both, + scale = :area, orientation = :vertical, width = automatic, dodge = automatic, @@ -49,10 +51,10 @@ end function plot!(plot::Violin) x, y = plot[1], plot[2] - args = @extract plot (width, side, color, show_median, npoints, boundary, bandwidth, weights, + args = @extract plot (width, side, scale, color, show_median, npoints, boundary, bandwidth, weights, datalimits, max_density, dodge, n_dodge, gap, dodge_gap, orientation) signals = lift(plot, x, y, - args...) do x, y, width, vside, color, show_median, n, bound, bw, w, limits, max_density, + args...) do x, y, width, vside, scale_type, color, show_median, n, bound, bw, w, limits, max_density, dodge, n_dodge, gap, dodge_gap, orientation x̂, violinwidth = compute_x_and_width(x, width, gap, dodge, n_dodge, dodge_gap) @@ -81,13 +83,20 @@ function plot!(plot::Violin) i1, i2 = searchsortedfirst(k.x, l1), searchsortedlast(k.x, l2) kde = (x = view(k.x, i1:i2), density = view(k.density, i1:i2)) c = getuniquevalue(color, idxs) - return (x = key.x, side = key.side, color = to_color(c), kde = kde, median = median(v)) + return (x = key.x, side = key.side, color = to_color(c), kde = kde, median = median(v), amount = length(idxs)) end + (scale_type ∈ [:area, :count, :width]) || error("Invalid scale type: $(scale_type)") + max = if max_density === automatic maximum(specs) do spec - _, max = extrema_nan(spec.kde.density) - return max + if scale_type === :area + return extrema_nan(spec.kde.density) |> last + elseif scale_type === :count + return extrema_nan(spec.kde.density .* spec.amount) |> last + elseif scale_type === :width + return NaN + end end else max_density @@ -98,7 +107,14 @@ function plot!(plot::Violin) colors = RGBA{Float32}[] for spec in specs - scale = 0.5*violinwidth/max + scale = 0.5 * violinwidth + if scale_type === :area + scale = scale / max + elseif scale_type === :count + scale = scale / max * spec.amount + elseif scale_type === :width + scale = scale / (extrema_nan(spec.kde.density) |> last) + end xl = reverse(spec.x .- spec.kde.density .* scale) xr = spec.x .+ spec.kde.density .* scale yl = reverse(spec.kde.x)