Skip to content
This repository has been archived by the owner on Jul 13, 2021. It is now read-only.

Commit

Permalink
Refactor dodge and improve boxplot, violin and crossbar (#712)
Browse files Browse the repository at this point in the history
* dodge violin

* simplify barplot code

* add dodge support to boxplot and crossbar

* docs

* remove dodge_gap for violin

* revert removing dodge_gap

* boxplot cleanup

* clean up boxplot

* fix docs

* outlier color matches color by default

* code uniformity

* uniform density and violin
  • Loading branch information
Pietro Vertechi authored Apr 28, 2021
1 parent 8d56022 commit ef3af81
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 72 deletions.
30 changes: 30 additions & 0 deletions docs/src/plotting_functions/boxplot.md
Original file line number Diff line number Diff line change
@@ -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)
```
21 changes: 21 additions & 0 deletions docs/src/plotting_functions/crossbar.md
Original file line number Diff line number Diff line change
@@ -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)
```
38 changes: 38 additions & 0 deletions docs/src/plotting_functions/violin.md
Original file line number Diff line number Diff line change
@@ -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
```
43 changes: 21 additions & 22 deletions src/basic_recipes/barplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 -----------
Expand All @@ -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(, y, barwidth, fillto)
return in_y_direction ? rects : flip.(rects)
end

Expand Down
42 changes: 24 additions & 18 deletions src/stats/boxplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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[]
Expand All @@ -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())
values = view(y, idxs)

# compute quantiles
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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,
Expand Down
19 changes: 12 additions & 7 deletions src/stats/crossbar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,15 +48,16 @@ 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],
plot[2],
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
Expand All @@ -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 = .- hw, x̂, x̂ .+ hw

if show_notch && nmin !== automatic && nmax !== automatic
if any(nmin < ymin || nmax > ymax)
Expand All @@ -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;]
Expand Down
18 changes: 0 additions & 18 deletions src/stats/density.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit ef3af81

Please sign in to comment.