Skip to content

Commit

Permalink
Merge pull request #1246 from Mattriks/boxplot_dodge
Browse files Browse the repository at this point in the history
Boxplot dodging
  • Loading branch information
bjarthur authored Feb 3, 2019
2 parents 270858c + 8f6f52d commit 13cf145
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 47 deletions.
16 changes: 13 additions & 3 deletions docs/src/gallery/geometries.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,19 @@ plot(dataset("lattice", "singer"), x="VoicePart", y="Height", Geom.beeswarm)
## [`Geom.boxplot`](@ref)

```@example
using Gadfly, RDatasets
set_default_plot_size(14cm, 8cm)
plot(dataset("lattice", "singer"), x="VoicePart", y="Height", Geom.boxplot)
using Compose, Gadfly, RDatasets
set_default_plot_size(21cm, 8cm)
singers, salaries = dataset("lattice", "singer"), dataset("car","Salaries")
salaries.Salary /= 1000.0
salaries.Discipline = ["Discipline $(x)" for x in salaries.Discipline]
p1 = plot(singers, x=:VoicePart, y=:Height, Geom.boxplot,
Theme(default_color="MidnightBlue"))
p2 = plot(salaries, x=:Discipline, y=:Salary, color=:Rank,
Scale.x_discrete(levels=["Discipline A", "Discipline B"]),
Geom.boxplot, Theme(boxplot_spacing=0.1cx),
Guide.colorkey(title="", pos=[0.78w,-0.4h])
)
hstack(p1, p2)
```


Expand Down
8 changes: 4 additions & 4 deletions src/geom/boxplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ outliers.
Alternatively, if the `y` aesthetic is specified instead, the middle, hinges,
fences, and outliers aesthetics will be computed using [`Stat.boxplot`](@ref).
Boxplots will be automatically dodged by specifying a `color` aesthetic different to the `x` aesthetic.
"""
const boxplot = BoxplotGeometry

Expand Down Expand Up @@ -125,11 +127,9 @@ function render(geom::BoxplotGeometry, theme::Gadfly.Theme, aes::Gadfly.Aestheti
xys = collect(Iterators.flatten(zip(cycle([x]), ys, cycle([c]))
for (x, ys, c) in zip(xs, aes.outliers, cs)))
compose!(ctx, (context(),
(context(), Shape.circle([x for (x, y, c) in xys],
[y for (x, y, c) in xys],
[theme.point_size], to), svgclass("marker")),
Shape.circle([x for (x, y, c) in xys], [y for (x, y, c) in xys], [theme.point_size], to), svgclass("marker"),
stroke([theme.discrete_highlight_color(c) for (x, y, c) in xys]),
fill([c for (x, y, c) in xys])))
fill([c for (x, y, c) in xys]) ))
end

# Middle
Expand Down
77 changes: 40 additions & 37 deletions src/statistics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -996,14 +996,17 @@ function apply_statistic(stat::BoxplotStatistic,
coord::Gadfly.CoordinateElement,
aes::Gadfly.Aesthetics)

if aes.x === nothing
aes_x = [1]
aes.x_label = x -> fill("", length(x))
else
xflag = aes.x != nothing
aes_x = (xflag ? eltype(aes.x) : Int)[]
if xflag
aes_x = aes.x
else
aes_x = ones(Int, length(aes.y))
aes.x_label = x -> fill("", length(x))
end
aes_color = aes.color === nothing ? [nothing] : aes.color

colorflag = aes.color != nothing
aes_color = colorflag ? aes.color : fill(nothing, length(aes_x))

if aes.y == nothing
groups = Any[]
for (x, c) in zip(aes.x, cycle(aes_color))
Expand All @@ -1024,30 +1027,30 @@ function apply_statistic(stat::BoxplotStatistic,
aes.yviewmax = yviewmax
end
else
T = isempty(aes.y) ? eltype(aes.y) : typeof(aes.y[1] / 1)
groups = DefaultOrderedDict(() -> T[])

for (x, y, c) in zip(cycle(aes_x), aes.y, cycle(aes_color))
push!(groups[(x, c)], y)
end

m = length(groups)
aes.x = Array{eltype(aes.x)}(undef, m)
aes.middle = Array{T}(undef, m)
aes.lower_hinge = Array{T}(undef, m)
aes.upper_hinge = Array{T}(undef, m)
aes.lower_fence = Array{T}(undef, m)
aes.upper_fence = Array{T}(undef, m)
aes.outliers = Vector{T}[]

for (i, ((x, c), ys)) in enumerate(groups)
YT = isempty(aes.y) ? eltype(aes.y) : typeof(aes.y[1] / 1)
CT, XT = eltype(aes_color), eltype(aes_x)
groups = collect(Tuple{CT, XT}, zip(aes_color, aes_x))
ug = unique(groups)

K = Tuple{CT, XT}
grouped_y = Dict{K, Vector{YT}}(g=>aes.y[groups.==[g]] for g in ug)

m = length(ug)
aes.x = Vector{XT}(undef, m)
aes.middle = Vector{YT}(undef, m)
aes.lower_hinge = Vector{YT}(undef, m)
aes.upper_hinge = Vector{YT}(undef, m)
aes.lower_fence = Vector{YT}(undef, m)
aes.upper_fence = Vector{YT}(undef, m)
aes.outliers = Vector{YT}[]

for (i, ((c, x), ys)) in enumerate(grouped_y)
sort!(ys)

aes.x[i] = x

if stat.method == :tukey
aes.lower_hinge[i], aes.middle[i], aes.upper_hinge[i] =
quantile(ys, [0.25, 0.5, 0.75])
aes.lower_hinge[i], aes.middle[i], aes.upper_hinge[i] = quantile(ys, [0.25, 0.5, 0.75])
iqr = aes.upper_hinge[i] - aes.lower_hinge[i]

idx = searchsortedfirst(ys, aes.lower_hinge[i] - 1.5iqr)
Expand All @@ -1057,22 +1060,19 @@ function apply_statistic(stat::BoxplotStatistic,
aes.upper_fence[i] = ys[idx]
elseif isa(stat.method, Vector)
qs = stat.method
if length(qs) != 5
error("Stat.boxplot requires exactly five quantiles.")
end
(length(qs) != 5) && error("Stat.boxplot requires exactly five quantiles.")

aes.lower_fence[i], aes.lower_hinge[i], aes.middle[i],
aes.upper_hinge[i], aes.upper_fence[i] = quantile!(ys, qs)
else
error("Invalid method specified for State.boxplot")
end

push!(aes.outliers,
filter(y -> y < aes.lower_fence[i] || y > aes.upper_fence[i], ys))
push!(aes.outliers, filter(y -> y < aes.lower_fence[i] || y > aes.upper_fence[i], ys))
end
end

if length(aes.x) > 1 && (haskey(scales, :x) && isa(scales[:x], Scale.ContinuousScale))
if length(aes.x) > 1 && haskey(scales, :x) && isa(scales[:x], Scale.ContinuousScale)
xmin, xmax = minimum(aes.x), maximum(aes.x)
minspan = minimum([xj - xi for (xi, xj) in zip(aes.x[1:end-1], aes.x[2:end])])

Expand All @@ -1088,14 +1088,17 @@ function apply_statistic(stat::BoxplotStatistic,
end
end

if isa(aes_x, IndirectArray)
aes.x = discretize_make_ia(aes.x, aes_x.values)
if haskey(scales, :x) && isa(scales[:x], Scale.DiscreteScale)
aes.xviewmin = minimum(aes.x)-0.5
aes.xviewmax = maximum(aes.x)+0.5
aes.pad_categorical_x = false
end

if aes.color !== nothing
aes.color = discretize_make_ia(RGB{Float32}[c for (x, c) in keys(groups)],
aes.color.values)
end
isa(aes_x, IndirectArray) && (aes.x = discretize_make_ia(aes.x, aes_x.values))

colorflag && (aes.color = discretize_make_ia(first.(keys(grouped_y)), aes.color.values))

Stat.apply_statistic(Stat.dodge(), scales, coord, aes)

nothing
end
Expand Down
12 changes: 9 additions & 3 deletions test/testscripts/colored_boxplot.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
using Gadfly, RDatasets
using Compose, Gadfly, RDatasets

set_default_plot_size(6inch, 3inch)
set_default_plot_size(21cm, 16cm)

mpg = dataset("ggplot2","mpg")
plot(mpg, x=:Class, y=:Hwy, Geom.boxplot, color=:Class)
p1 = plot(mpg, x=:Class, y=:Hwy, color=:Class, Geom.boxplot, Theme(boxplot_spacing=0.3cx) )

p2 = plot(mpg, x=:Class, y=:Hwy, color=:Drv, Geom.boxplot,
Guide.colorkey(labels=["front", "4-wheel\t\t","rear"]),
Theme(boxplot_spacing=0.1cx) )

vstack(p1, p2)

0 comments on commit 13cf145

Please sign in to comment.