diff --git a/docs/src/gallery/geometries.md b/docs/src/gallery/geometries.md index cc95be366..efcf4a707 100644 --- a/docs/src/gallery/geometries.md +++ b/docs/src/gallery/geometries.md @@ -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) ``` diff --git a/src/geom/boxplot.jl b/src/geom/boxplot.jl index 7165159fb..0fc6222f2 100644 --- a/src/geom/boxplot.jl +++ b/src/geom/boxplot.jl @@ -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 @@ -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 diff --git a/src/statistics.jl b/src/statistics.jl index 1dbd0175d..8c6f18f71 100644 --- a/src/statistics.jl +++ b/src/statistics.jl @@ -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)) @@ -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) @@ -1057,9 +1060,7 @@ 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) @@ -1067,12 +1068,11 @@ function apply_statistic(stat::BoxplotStatistic, 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])]) @@ -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 diff --git a/test/testscripts/colored_boxplot.jl b/test/testscripts/colored_boxplot.jl index e938a45f2..89bbe099b 100644 --- a/test/testscripts/colored_boxplot.jl +++ b/test/testscripts/colored_boxplot.jl @@ -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) \ No newline at end of file +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) \ No newline at end of file