diff --git a/docs/src/plotting_functions/boxplot.md b/docs/src/plotting_functions/boxplot.md new file mode 100644 index 000000000..67dda78e5 --- /dev/null +++ b/docs/src/plotting_functions/boxplot.md @@ -0,0 +1,30 @@ +# boxplot + +```@docs +boxplot +``` + +### Examples + +```@example +using CairoMakie +CairoMakie.activate!() # hide +AbstractPlotting.inline!(true) # hide + +xs = rand(["a", "b", "c"], 1000) +ys = randn(1000) + +boxplot(xs, ys) +``` + +```@example +using CairoMakie +CairoMakie.activate!() # hide +AbstractPlotting.inline!(true) # hide + +xs = rand(["a", "b", "c"], 1000) +ys = randn(1000) +dodge = rand(1:2, 1000) + +boxplot(xs, ys, dodge = dodge, show_notch = true) +``` diff --git a/docs/src/plotting_functions/crossbar.md b/docs/src/plotting_functions/crossbar.md new file mode 100644 index 000000000..801e638c4 --- /dev/null +++ b/docs/src/plotting_functions/crossbar.md @@ -0,0 +1,21 @@ +# crossbar + +```@docs +crossbar +``` + +### Examples + +```@example +using CairoMakie +CairoMakie.activate!() # hide +AbstractPlotting.inline!(true) # hide + +xs = [1, 1, 2, 2, 3, 3] +ys = rand(6) +ymins = ys .- 1 +ymaxs = ys .+ 1 +dodge = [1, 2, 1, 2, 1, 2] + +crossbar(xs, ys, ymins, ymaxs, dodge = dodge, show_notch = true) +``` diff --git a/docs/src/plotting_functions/violin.md b/docs/src/plotting_functions/violin.md new file mode 100644 index 000000000..824575d35 --- /dev/null +++ b/docs/src/plotting_functions/violin.md @@ -0,0 +1,38 @@ +# violin + +```@docs +violin +``` + +### Examples + +```@example +using CairoMakie +CairoMakie.activate!() # hide +AbstractPlotting.inline!(true) # hide + +xs = rand(["a", "b", "c"], 1000) +ys = randn(1000) + +violin(xs, ys) +``` + +```@example +using CairoMakie +CairoMakie.activate!() # hide +AbstractPlotting.inline!(true) # hide + +xs1 = rand(["a", "b", "c"], 1000) +ys1 = randn(1000) +dodge1 = rand(1:2, 1000) + +xs2 = rand(["a", "b", "c"], 1000) +ys2 = randn(1000) +dodge2 = rand(1:2, 1000) + +fig = Figure() +ax = Axis(fig[1, 1]) +violin!(ax, xs1, ys1, dodge = dodge1, side = :left, color = "orange") +violin!(ax, xs2, ys2, dodge = dodge2, side = :right, color = "teal") +fig +``` diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 95c0458cb..38d46c69c 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -38,6 +38,21 @@ end flip(r::Rect2D) = Rect2D(reverse(origin(r)), reverse(widths(r))) +function xw_from_dodge(x, width, minimum_distance, x_gap, dodge, n_dodge, dodge_gap) + width === automatic && (width = (1 - x_gap) * minimum_distance) + if dodge === automatic + i_dodge = 1 + elseif eltype(dodge) <: Integer + i_dodge = dodge + else + ArgumentError("The keyword argument `dodge` currently supports only `AbstractVector{<: Integer}`") |> throw + end + n_dodge === automatic && (n_dodge = maximum(i_dodge)) + dodge_width = scale_width(dodge_gap, n_dodge) + shifts = shift_dodge.(i_dodge, dodge_width, dodge_gap) + return x .+ width .* shifts, width * dodge_width +end + function AbstractPlotting.plot!(p::BarPlot) in_y_direction = lift(p.direction) do dir @@ -55,31 +70,16 @@ function AbstractPlotting.plot!(p::BarPlot) x = first.(xy) y = last.(xy) - # compute width of bars + minimum_distance = nothing + # only really compute `minimum_distance` if `width` is `automatic` if width === automatic x_unique = unique(filter(isfinite, x)) x_diffs = diff(sort(x_unique)) minimum_distance = isempty(x_diffs) ? 1.0 : minimum(x_diffs) - width = (1 - x_gap) * minimum_distance end - # -------------------------------- - # ------------ Dodging ----------- - # -------------------------------- - - if dodge === automatic - i_dodge = 1 - elseif eltype(dodge) <: Integer - i_dodge = dodge - else - ArgumentError("The keyword argument `dodge` currently supports only `AbstractVector{<: Integer}`") |> throw - end - - n_dodge === automatic && (n_dodge = maximum(i_dodge)) - - dodge_width = scale_width(dodge_gap, n_dodge) - - shifts = shift_dodge.(i_dodge, dodge_width, dodge_gap) + # compute width of bars and x̂ (horizontal position after dodging) + x̂, barwidth = xw_from_dodge(x, width, minimum_distance, x_gap, dodge, n_dodge, dodge_gap) # -------------------------------- # ----------- Stacking ----------- @@ -93,14 +93,13 @@ function AbstractPlotting.plot!(p::BarPlot) fillto === automatic || @warn "Ignore keyword fillto when keyword stack is provided" i_stack = stack - grp = dodge === automatic ? (x = x, ) : (i_dodge = i_dodge, x = x) - from, to = stack_grouped_from_to(i_stack, y, grp) + 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 end - rects = @. bar_rectangle(x + width * shifts, y, width * dodge_width, fillto) + rects = @. bar_rectangle(x̂, y, barwidth, fillto) return in_y_direction ? rects : flip.(rects) end diff --git a/src/stats/boxplot.jl b/src/stats/boxplot.jl index 527152d25..1e423dad1 100644 --- a/src/stats/boxplot.jl +++ b/src/stats/boxplot.jl @@ -28,13 +28,17 @@ The boxplot has 3 components: - `show_outliers`: show outliers as points """ @recipe(BoxPlot, x, y) do scene - t = Theme( + Theme( color = theme(scene, :color), colormap = theme(scene, :colormap), colorrange = automatic, orientation = :vertical, - # box - width = 0.8, + # box and dodging + width = automatic, + dodge = automatic, + n_dodge = automatic, + x_gap = 0.2, + dodge_gap = 0.03, strokecolor = :white, strokewidth = 0.0, # notch @@ -51,13 +55,12 @@ The boxplot has 3 components: whiskerlinewidth = 1.0, # outliers points show_outliers = true, - marker = :circle, - markersize = automatic, + marker = Circle, + markersize = 10, + outliercolor = automatic, outlierstrokecolor = :black, outlierstrokewidth = 1.0, ) - get!(t, :outliercolor, t[:color]) - t end conversion_trait(x::Type{<:BoxPlot}) = SampleBased() @@ -69,17 +72,18 @@ _flip_xy(p::Point2f0) = reverse(p) _flip_xy(r::Rect{2,T}) where {T} = Rect{2,T}(reverse(r.origin), reverse(r.widths)) function AbstractPlotting.plot!(plot::BoxPlot) - args = @extract plot (width, range, show_outliers, whiskerwidth, show_notch, orientation) + args = @extract plot (width, range, show_outliers, whiskerwidth, show_notch, orientation, x_gap, dodge, n_dodge, dodge_gap) signals = lift( plot[1], plot[2], args..., - ) do x, y, bw, range, show_outliers, whiskerwidth, show_notch, orientation + ) do x, y, width, range, show_outliers, whiskerwidth, show_notch, orientation, x_gap, dodge, n_dodge, dodge_gap + x̂, boxwidth = xw_from_dodge(x, width, 1.0, x_gap, dodge, n_dodge, dodge_gap) if !(whiskerwidth == :match || whiskerwidth >= 0) error("whiskerwidth must be :match or a positive number. Found: $whiskerwidth") end - ww = whiskerwidth == :match ? bw : whiskerwidth * bw + ww = whiskerwidth == :match ? boxwidth : whiskerwidth * boxwidth outlier_points = Point2f0[] centers = Float32[] medians = Float32[] @@ -88,7 +92,7 @@ function AbstractPlotting.plot!(plot::BoxPlot) notchmin = Float32[] notchmax = Float32[] t_segments = Point2f0[] - for (i, (center, idxs)) in enumerate(StructArrays.finduniquesorted(x)) + for (i, (center, idxs)) in enumerate(StructArrays.finduniquesorted(x̂)) values = view(y, idxs) # compute quantiles @@ -150,6 +154,7 @@ function AbstractPlotting.plot!(plot::BoxPlot) notchmax = notchmax, outliers = outlier_points, t_segments = t_segments, + boxwidth = boxwidth, ) end centers = @lift($signals.centers) @@ -160,16 +165,17 @@ function AbstractPlotting.plot!(plot::BoxPlot) notchmax = @lift($show_notch ? $signals.notchmax : automatic) outliers = @lift($signals.outliers) t_segments = @lift($signals.t_segments) + boxwidth = @lift($signals.boxwidth) + + outliercolor = lift(plot[:outliercolor], plot[:color]) do outliercolor, color + outliercolor === automatic ? color : outliercolor + end scatter!( plot, - color = plot[:outliercolor], + color = outliercolor, marker = plot[:marker], - markersize = lift( - (w, ms) -> ms === automatic ? w * 0.1 : ms, - width, - plot.markersize, - ), + markersize = plot[:markersize], strokecolor = plot[:outlierstrokecolor], strokewidth = plot[:outlierstrokewidth], outliers, @@ -191,7 +197,7 @@ function AbstractPlotting.plot!(plot::BoxPlot) midlinewidth = plot[:medianlinewidth], show_midline = plot[:show_median], orientation = orientation, - width = width, + width = boxwidth, show_notch = show_notch, notchmin = notchmin, notchmax = notchmax, diff --git a/src/stats/crossbar.jl b/src/stats/crossbar.jl index 32ea50c15..c94604f3c 100644 --- a/src/stats/crossbar.jl +++ b/src/stats/crossbar.jl @@ -26,8 +26,12 @@ It is most commonly used as part of the `boxplot`. colormap=theme(scene, :colormap), colorrange=automatic, orientation=:vertical, - # box - width=0.8, + # box and dodging + width = automatic, + dodge = automatic, + n_dodge = automatic, + x_gap = 0.2, + dodge_gap = 0.03, strokecolor=:white, strokewidth=0.0, # notch @@ -44,7 +48,7 @@ It is most commonly used as part of the `boxplot`. end function AbstractPlotting.plot!(plot::CrossBar) - args = @extract plot (width, show_notch, notchmin, notchmax, notchwidth, orientation) + args = @extract plot (width, dodge, n_dodge, x_gap, dodge_gap, show_notch, notchmin, notchmax, notchwidth, orientation) signals = lift( plot[1], @@ -52,7 +56,8 @@ function AbstractPlotting.plot!(plot::CrossBar) plot[3], plot[4], args..., - ) do x, y, ymin, ymax, bw, show_notch, nmin, nmax, nw, orientation + ) do x, y, ymin, ymax, width, dodge, n_dodge, x_gap, dodge_gap, show_notch, nmin, nmax, nw, orientation + x̂, boxwidth = xw_from_dodge(x, width, 1.0, x_gap, dodge, n_dodge, dodge_gap) show_notch = show_notch && (nmin !== automatic && nmax !== automatic) # for horizontal crossbars just flip all components @@ -62,8 +67,8 @@ function AbstractPlotting.plot!(plot::CrossBar) end # make the shape - hw = bw ./ 2 # half box width - l, m, r = x .- hw, x, x .+ hw + hw = boxwidth ./ 2 # half box width + l, m, r = x̂ .- hw, x̂, x̂ .+ hw if show_notch && nmin !== automatic && nmax !== automatic if any(nmin < ymin || nmax > ymax) @@ -88,7 +93,7 @@ function AbstractPlotting.plot!(plot::CrossBar) end midlines = Pair.(fpoint.(m .- nw .* hw, y), fpoint.(m .+ nw .* hw, y)) else - boxes = frect.(l, ymin, bw, ymax .- ymin) + boxes = frect.(l, ymin, boxwidth, ymax .- ymin) midlines = Pair.(fpoint.(l, y), fpoint.(r, y)) end return [boxes;], [midlines;] diff --git a/src/stats/density.jl b/src/stats/density.jl index c89ad7ea9..4c0d856bd 100644 --- a/src/stats/density.jl +++ b/src/stats/density.jl @@ -16,24 +16,6 @@ function convert_arguments(P::PlotFunc, d::KernelDensity.BivariateKDE) to_plotspec(ptype, convert_arguments(ptype, d.x, d.y, d.density)) end -function searchrange(x, xlims) - min, max = xlims - i1 = searchsortedfirst(x, min) - i2 = searchsortedlast(x, max) - return i1:i2 -end - -function trim_density(k::KernelDensity.UnivariateKDE, xlims) - range = searchrange(k.x, xlims) - KernelDensity.UnivariateKDE(k.x[range], k.density[range]) -end - -function _density(x; trim = false) - k = KernelDensity.kde(x) - return trim ? trim_density(k, extrema_nan(x)) : k -end - - """ density(values; npoints = 200, offset = 0.0, direction = :x) diff --git a/src/stats/violin.jl b/src/stats/violin.jl index 0a8ab27e3..144c28ed1 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -1,8 +1,26 @@ +""" + violin(x, y; kwargs...) +Draw a violin plot. +# Arguments +- `x`: positions of the categories +- `y`: variables whose density is computed +# Keywords +- `orientation=:vertical`: orientation of the violins (`:vertical` or `:horizontal`) +- `width=0.8`: width of the violin +- `show_median=true`: show median as midline +""" @recipe(Violin, x, y) do scene Theme(; default_theme(scene, Poly)..., + npoints = 200, + boundary = automatic, + bandwidth = automatic, side = :both, - width = 0.8, + width = automatic, + dodge = automatic, + n_dodge = automatic, + x_gap = 0.2, + dodge_gap = 0.03, trim = false, strokecolor = :white, show_median = false, @@ -14,17 +32,24 @@ end conversion_trait(x::Type{<:Violin}) = SampleBased() function plot!(plot::Violin) - width, side, trim, show_median = plot[:width], plot[:side], plot[:trim], plot[:show_median] + x, y, width, side, show_median = plot[1], plot[2], plot[:width], plot[:side], plot[:show_median] + npoints, boundary, bandwidth = plot[:npoints], plot[:boundary], plot[:bandwidth] + dodge, n_dodge, x_gap, dodge_gap = plot[:dodge], plot[:n_dodge], plot[:x_gap], plot[:dodge_gap] - signals = lift(plot[1], plot[2], width, side, trim, show_median) do x, y, bw, vside, trim, show_median + signals = lift(x, y, width, dodge, n_dodge, x_gap, dodge_gap, side, show_median, npoints, boundary, bandwidth) do x, y, width, dodge, n_dodge, x_gap, dodge_gap, vside, show_median, n, bound, bw + x̂, violinwidth = xw_from_dodge(x, width, 1, x_gap, dodge, n_dodge, dodge_gap) vertices = Vector{Point2f0}[] lines = Pair{Point2f0, Point2f0}[] - for (key, idxs) in StructArrays.finduniquesorted(x) + for (key, idxs) in StructArrays.finduniquesorted(x̂) v = view(y, idxs) - - spec = (x = key, kde = _density(v; trim = trim), median = median(v)) + k = KernelDensity.kde(v; + npoints = n, + (bound === automatic ? NamedTuple() : (boundary = bound,))..., + (bw === automatic ? NamedTuple() : (bandwidth = bw,))..., + ) + spec = (x = key, kde = k, median = median(v)) min, max = extrema_nan(spec.kde.density) - scale = 0.5*bw/max + scale = 0.5*violinwidth/max xl = reverse(spec.x .- spec.kde.density .* scale) xr = spec.x .+ spec.kde.density .* scale yl = reverse(spec.kde.x)