From f3f29fb54f2d1bf7199e466b108d4454ac412518 Mon Sep 17 00:00:00 2001 From: Thomas Breloff Date: Thu, 7 Apr 2016 15:56:09 -0400 Subject: [PATCH] apply_series_recipe framework and boxplot; fix Gadfly scales for tick labels --- src/Plots.jl | 4 +++ src/args.jl | 3 ++- src/backends/gadfly.jl | 16 +++++++++--- src/backends/supported.jl | 4 +-- src/plot.jl | 7 ++++++ src/recipes.jl | 53 +++++++++++++++++++++++++++++++++++++++ src/series_args.jl | 53 +++++++++++---------------------------- src/utils.jl | 1 + 8 files changed, 95 insertions(+), 46 deletions(-) diff --git a/src/Plots.jl b/src/Plots.jl index 548bbc14..c894e9e9 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -66,6 +66,8 @@ export scatter3d, scatter3d!, abline!, + boxplot, + boxplot!, title!, xlabel!, @@ -188,6 +190,8 @@ plot3d(args...; kw...) = plot(args...; kw..., linetype = :path3d) plot3d!(args...; kw...) = plot!(args...; kw..., linetype = :path3d) scatter3d(args...; kw...) = plot(args...; kw..., linetype = :scatter3d) scatter3d!(args...; kw...) = plot!(args...; kw..., linetype = :scatter3d) +boxplot(args...; kw...) = plot(args...; kw..., linetype = :box) +boxplot!(args...; kw...) = plot!(args...; kw..., linetype = :box) title!(s::AbstractString; kw...) = plot!(; title = s, kw...) diff --git a/src/args.jl b/src/args.jl index 81065ed5..a110cf0f 100644 --- a/src/args.jl +++ b/src/args.jl @@ -11,7 +11,7 @@ const _3dTypes = [:path3d, :scatter3d, :surface, :wireframe] const _allTypes = vcat([ :none, :line, :path, :steppre, :steppost, :sticks, :scatter, :heatmap, :hexbin, :hist, :hist2d, :hist3d, :density, :bar, :hline, :vline, :ohlc, - :contour, :pie, :shape + :contour, :pie, :shape, :box ], _3dTypes) @compat const _typeAliases = KW( :n => :none, @@ -38,6 +38,7 @@ const _allTypes = vcat([ :shapes => :shape, :poly => :shape, :polygon => :shape, + :boxplot => :box, ) like_histogram(linetype::Symbol) = linetype in (:hist, :density) diff --git a/src/backends/gadfly.jl b/src/backends/gadfly.jl index d074620e..c6724e89 100644 --- a/src/backends/gadfly.jl +++ b/src/backends/gadfly.jl @@ -315,17 +315,25 @@ function addGadflyTicksGuide(gplt, ticks, isx::Bool) replaceType(gplt.guides, gtype(ticks = collect(ticks))) # set the ticks and the labels + # Note: this is pretty convoluted, but I think it works. We set the ticks using Gadfly.Guide, + # and then set the label function (wraps a dict lookup) through a continuous Gadfly.Scale. elseif ttype == :ticks_and_labels gtype = isx ? Gadfly.Guide.xticks : Gadfly.Guide.yticks replaceType(gplt.guides, gtype(ticks = collect(ticks[1]))) - # TODO add xtick_label function (given tick, return label??) - # Scale.x_discrete(; labels=nothing, levels=nothing, order=nothing) + # # TODO add xtick_label function (given tick, return label??) + # # Scale.x_discrete(; labels=nothing, levels=nothing, order=nothing) + # filterGadflyScale(gplt, isx) + # gfunc = isx ? Gadfly.Scale.x_discrete : Gadfly.Scale.y_discrete + # labelmap = Dict(zip(ticks...)) + # labelfunc = val -> labelmap[val] + # push!(gplt.scales, gfunc(levels = collect(ticks[1]), labels = labelfunc)) + filterGadflyScale(gplt, isx) - gfunc = isx ? Gadfly.Scale.x_discrete : Gadfly.Scale.y_discrete + gfunc = isx ? Gadfly.Scale.x_continuous : Gadfly.Scale.y_continuous labelmap = Dict(zip(ticks...)) labelfunc = val -> labelmap[val] - push!(gplt.scales, gfunc(levels = collect(ticks[1]), labels = labelfunc)) + push!(gplt.scales, gfunc(labels = labelfunc)) else error("Invalid input for $(isx ? "xticks" : "yticks"): ", ticks) diff --git a/src/backends/supported.jl b/src/backends/supported.jl index 1d7433f2..d27c9404 100644 --- a/src/backends/supported.jl +++ b/src/backends/supported.jl @@ -80,7 +80,7 @@ supportedArgs(::GadflyBackend) = [ ] supportedAxes(::GadflyBackend) = [:auto, :left] supportedTypes(::GadflyBackend) = [:none, :line, :path, :steppre, :steppost, :sticks, - :scatter, :hist2d, :hexbin, :hist, :bar, + :scatter, :hist2d, :hexbin, :hist, :bar, :box, :hline, :vline, :contour, :shape] supportedStyles(::GadflyBackend) = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] supportedMarkers(::GadflyBackend) = vcat(_allMarkers, Shape) @@ -167,7 +167,7 @@ supportedArgs(::PyPlotBackend) = [ ] supportedAxes(::PyPlotBackend) = _allAxes supportedTypes(::PyPlotBackend) = [:none, :line, :path, :steppre, :steppost, #:sticks, - :scatter, :hist2d, :hexbin, :hist, :density, :bar, + :scatter, :hist2d, :hexbin, :hist, :density, :bar, :box, :hline, :vline, :contour, :path3d, :scatter3d, :surface, :wireframe, :heatmap] supportedStyles(::PyPlotBackend) = [:auto, :solid, :dash, :dot, :dashdot] # supportedMarkers(::PyPlotBackend) = [:none, :auto, :rect, :ellipse, :diamond, :utriangle, :dtriangle, :cross, :xcross, :star5, :hexagon] diff --git a/src/plot.jl b/src/plot.jl index cc3c3441..9c27161f 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -219,6 +219,13 @@ function _add_series(plt::Plot, d::KW, ::Void, args...; delete!(di, k) end + # merge in plotarg_overrides + plotarg_overrides = pop!(di, :plotarg_overrides, nothing) + if plotarg_overrides != nothing + merge!(plt.plotargs, plotarg_overrides) + end + # dumpdict(plt.plotargs, "pargs", true) + dumpdict(di, "Series $i") _add_series(plt.backend, plt; di...) diff --git a/src/recipes.jl b/src/recipes.jl index 3e88113e..5bed50bd 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -28,6 +28,59 @@ function _apply_recipe(d::KW, args...; issubplot=false, kw...) args end + +# ------------------------------------------------- + +""" +`apply_series_recipe` should take a processed series KW dict and break it up +into component parts. For example, a box plot is made up of `shape` for the +boxes, `path` for the lines, and `scatter` for the outliers. + +Returns a Vector{KW}. +""" +apply_series_recipe(d::KW, lt) = KW[d] + +# ------------------------------------------------- +# Box Plot + +function apply_series_recipe(d::KW, ::Type{Val{:box}}) + # dumpdict(d, "box before", true) + # TODO: add scatter series with outliers + + # create a list of shapes, where each shape is a single boxplot + shapes = Shape[] + d[:linetype] = :shape + groupby = extractGroupArgs(d[:x]) + + for (i, glabel) in enumerate(groupby.groupLabels) + + # filter y values, then compute quantiles + q1,q2,q3,q4,q5 = quantile(d[:y][groupby.groupIds[i]], linspace(0,1,5)) + + # make the shape + l, m, r = i - 0.3, i, i + 0.3 + xcoords = [ + m, l, r, m, m, NaN, # lower T + l, l, r, r, l, NaN, # lower box + l, l, r, r, l, NaN, # upper box + m, l, r, m, m # upper T + ] + ycoords = [ + q1, q1, q1, q1, q2, NaN, # lower T + q2, q3, q3, q2, q2, NaN, # lower box + q4, q3, q3, q4, q4, NaN, # upper box + q5, q5, q5, q5, q4, NaN, # upper T + ] + push!(shapes, Shape(xcoords, ycoords)) + end + + d[:x], d[:y] = shape_coords(shapes) + d[:plotarg_overrides] = KW(:xticks => (1:length(shapes), groupby.groupLabels)) + + KW[d] +end + + # ------------------------------------------------- function rotate(x::Real, y::Real, θ::Real; center = (0,0)) diff --git a/src/series_args.jl b/src/series_args.jl index 296c88d3..49ce585c 100644 --- a/src/series_args.jl +++ b/src/series_args.jl @@ -92,7 +92,7 @@ compute_xyz(x::Void, y::Void, z::Void) = error("x/y/z are all nothing!") # create n=max(mx,my) series arguments. the shorter list is cycled through # note: everything should flow through this function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) - x, y, z = map(a -> pop!(kw, a, nothing), (:x, :y, :z)) + x, y, z = map(sym -> pop!(kw, sym, nothing), (:x, :y, :z)) if nothing == x == y == z return [], nothing, nothing end @@ -101,18 +101,6 @@ function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) ys, ymeta = convertToAnyVector(y, kw) zs, zmeta = convertToAnyVector(z, kw) - # if idxfilter != nothing - # xs = filter_data(xs, idxfilter) - # ys = filter_data(ys, idxfilter) - # zs = filter_data(zs, idxfilter) - # # # filter the data - # # for sym in (:x, :y, :z) - # # @show "before" sym, d[sym], idxfilter - # # d[sym] = filter_data(get(d, sym, nothing), idxfilter) - # # @show "after" sym, d[sym], idxfilter - # # end - # end - mx = length(xs) my = length(ys) mz = length(zs) @@ -135,30 +123,11 @@ function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) n = plt.n + i dumpdict(d, "before getSeriesArgs") - # @show numUncounted i n commandIndex convertSeriesIndex(plt, n) d = getSeriesArgs(plt.backend, getplotargs(plt, n), d, commandIndex, convertSeriesIndex(plt, n), n) dumpdict(d, "after getSeriesArgs") - # @show map(typeof, (xs[mod1(i,mx)], ys[mod1(i,my)], zs[mod1(i,mz)])) d[:x], d[:y], d[:z] = compute_xyz(xs[mod1(i,mx)], ys[mod1(i,my)], zs[mod1(i,mz)]) - # @show map(typeof, (d[:x], d[:y], d[:z])) - - - # # NOTE: this should be handled by the time it gets here lt = d[:linetype] - # if isa(d[:y], Surface) - # if lt in (:contour, :heatmap, :surface, :wireframe) - # z = d[:y] - # d[:y] = 1:size(z,2) - # d[:z] = z - # end - # end - - # if haskey(d, :idxfilter) - # idxfilter = pop!(d, :idxfilter) - # d[:x] = d[:x][idxfilter] - # d[:y] = d[:y][idxfilter] - # end # for linetype `line`, need to sort by x values if lt == :line @@ -169,6 +138,11 @@ function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) d[:linetype] = :path end + # special handling for missing x in box plot... all the same category + if lt == :box && xs[mod1(i,mx)] == nothing + d[:x] = ones(Int, length(d[:y])) + end + # map functions to vectors if isa(d[:zcolor], Function) d[:zcolor] = map(d[:zcolor], d[:x]) @@ -177,14 +151,15 @@ function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) d[:fillrange] = map(d[:fillrange], d[:x]) end - # cleanup those fields that were used only for generating kw args - # delete!(d, :dataframe) - # for k in (:idxfilter, :numUncounted, :dataframe) - # delete!(d, k) - # end + # now that we've processed a given series... optionally split into + # multiple dicts through a recipe (for example, a box plot is split into component + # parts... polygons, lines, and scatters) + # note: we pass in a Val type (i.e. Val{:box}) so that we can dispatch on the linetype + kwlist = apply_series_recipe(d, Val{lt}) + append!(ret, kwlist) - # add it to our series list - push!(ret, d) + # # add it to our series list + # push!(ret, d) end ret, xmeta, ymeta diff --git a/src/utils.jl b/src/utils.jl index 787d1e8f..9df8d7c5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -118,6 +118,7 @@ end nop() = nothing +notimpl() = error("This has not been implemented yet") get_mod(v::AVec, idx::Int) = v[mod1(idx, length(v))] get_mod(v::AMat, idx::Int) = size(v,1) == 1 ? v[1, mod1(idx, size(v,2))] : v[:, mod1(idx, size(v,2))]