Plots.jl/src/recipes.jl
2016-05-06 11:39:17 -04:00

683 lines
21 KiB
Julia

# TODO: there should be a distinction between an object that will manage a full plot, vs a component of a plot.
# the PlotRecipe as currently implemented is more of a "custom component"
# a recipe should fully describe the plotting command(s) and call them, likewise for updating.
# actually... maybe those should explicitly derive from AbstractPlot???
abstract PlotRecipe
getRecipeXY(recipe::PlotRecipe) = Float64[], Float64[]
getRecipeArgs(recipe::PlotRecipe) = ()
plot(recipe::PlotRecipe, args...; kw...) = plot(getRecipeXY(recipe)..., args...; getRecipeArgs(recipe)..., kw...)
plot!(recipe::PlotRecipe, args...; kw...) = plot!(getRecipeXY(recipe)..., args...; getRecipeArgs(recipe)..., kw...)
plot!(plt::Plot, recipe::PlotRecipe, args...; kw...) = plot!(getRecipeXY(recipe)..., args...; getRecipeArgs(recipe)..., kw...)
num_series(x::AMat) = size(x,2)
num_series(x) = 1
_apply_recipe(d::KW, kw::KW) = ()
# if it's not a recipe, just do nothing and return the args
function _apply_recipe(d::KW, kw::KW, args...; issubplot=false)
if issubplot && !haskey(d, :n) && !haskey(d, :layout)
# put in a sensible default
d[:n] = maximum(map(num_series, args))
end
args
end
macro kw(k, v)
esc(:(get!(d, $k, $v)))
end
function _is_arrow_tuple(expr::Expr)
expr.head == :tuple &&
isa(expr.args[1], Expr) &&
expr.args[1].head == :(-->)
end
function _equals_symbol(arg::Symbol, sym::Symbol)
arg == sym
end
function _equals_symbol(arg::Expr, sym::Symbol)
arg.head == :quote && arg.args[1] == sym
end
# TODO: when this is moved out of Plots, also move the replacement of key aliases to just after the _apply_recipe calls
function replace_recipe_arrows!(expr::Expr)
for (i,e) in enumerate(expr.args)
if isa(e,Expr)
# process trailing flags, like:
# a --> b, :quiet, :force
quiet, require, force = false, false, false
if _is_arrow_tuple(e)
for flag in e.args
if _equals_symbol(flag, :quiet)
quiet = true
elseif _equals_symbol(flag, :require)
require = true
elseif _equals_symbol(flag, :force)
force = true
end
end
e = e.args[1]
end
# we are going to recursively swap out `a --> b, flags...` commands
if e.head == :(-->)
k, v = e.args
keyexpr = :(get(Plots._keyAliases, $k, $k))
set_expr = if force
# forced override user settings
:(d[$keyexpr] = $v)
else
# if the user has set this keyword, use theirs
:(get!(d, $keyexpr, $v))
end
expr.args[i] = if quiet
# quietly ignore keywords which are not supported
:($keyexpr in supportedArgs() ? $set_expr : nothing)
elseif require
# error when not supported by the backend
:($keyexpr in supportedArgs() ? $set_expr : error("In recipe: required keyword ", $k, " is not supported by backend $(backend_name())"))
else
set_expr
end
# @show quiet, force, expr.args[i]
elseif e.head != :call
# we want to recursively replace the arrows, but not inside function calls
# as this might include things like Dict(1=>2)
replace_recipe_arrows!(e)
end
end
end
end
macro recipe(funcexpr::Expr)
lhs, body = funcexpr.args
if !(funcexpr.head in (:(=), :function))
error("Must wrap a valid function call!")
end
if !(isa(lhs, Expr) && lhs.head == :call)
error("Expected `lhs = ...` with lhs as a call Expr... got: $lhs")
end
# for parametric definitions, take the "curly" expression and add the func
front = lhs.args[1]
func = :(Plots._apply_recipe)
if isa(front, Expr) && front.head == :curly
front.args[1] = func
func = front
end
# get the arg list, stripping out any keyword parameters into a
# bunch of get!(kw, key, value) lines
args = lhs.args[2:end]
kw_body = Expr(:block)
if isa(args[1], Expr) && args[1].head == :parameters
for kwpair in args[1].args
k, v = kwpair.args
push!(kw_body.args, :(get!(kw, $(QuoteNode(k)), $v)))
end
args = args[2:end]
end
# replace all the key => value lines with argument setting logic
replace_recipe_arrows!(body)
# now build a function definition for _apply_recipe, wrapping the return value in a tuple if needed
esc(quote
function $func(d::KW, kw::KW, $(args...); issubplot=false)
$kw_body
ret = $body
if typeof(ret) <: Tuple
ret
else
(ret,)
end
end
end)
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
const _box_halfwidth = 0.4
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 - _box_halfwidth, i, i + _box_halfwidth
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
# ---------------------------------------------------------------------------
# Violin Plot
# if the user has KernelDensity installed, use this for violin plots.
# otherwise, just use a histogram
try
Pkg.installed("KernelDensity")
import KernelDensity
# warn("using KD for violin")
@eval function violin_coords(y)
kd = KernelDensity.kde(y, npoints = 30)
kd.density, kd.x
end
catch
# warn("using hist for violin")
@eval function violin_coords(y)
edges, widths = hist(y, 20)
centers = 0.5 * (edges[1:end-1] + edges[2:end])
ymin, ymax = extrema(y)
vcat(0.0, widths, 0.0), vcat(ymin, centers, ymax)
end
end
function apply_series_recipe(d::KW, ::Type{Val{:violin}})
# 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)
# get the edges and widths
y = d[:y][groupby.groupIds[i]]
widths, centers = violin_coords(y)
# normalize
widths = _box_halfwidth * widths / maximum(widths)
# make the violin
xcoords = vcat(widths, -reverse(widths)) + i
ycoords = vcat(centers, reverse(centers))
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
# ---------------------------------------------------------------------------
# Error Bars
function error_style!(d::KW)
d[:linetype] = :path
d[:linecolor] = d[:markerstrokecolor]
d[:linewidth] = d[:markerstrokewidth]
d[:label] = ""
end
# if we're passed a tuple of vectors, convert to a vector of tuples
function error_zipit(ebar)
if istuple(ebar)
collect(zip(ebar...))
else
ebar
end
end
function error_coords(xorig, yorig, ebar)
# init empty x/y, and zip errors if passed Tuple{Vector,Vector}
x, y = zeros(0), zeros(0)
# for each point, create a line segment from the bottom to the top of the errorbar
for i = 1:max(length(xorig), length(yorig))
xi = get_mod(xorig, i)
yi = get_mod(yorig, i)
ebi = get_mod(ebar, i)
nanappend!(x, [xi, xi])
e1, e2 = if istuple(ebi)
first(ebi), last(ebi)
elseif isscalar(ebi)
ebi, ebi
else
error("unexpected ebi type $(typeof(ebi)) for errorbar: $ebi")
end
nanappend!(y, [yi - e1, yi + e2])
end
x, y
end
# we will create a series of path segments, where each point represents one
# side of an errorbar
function apply_series_recipe(d::KW, ::Type{Val{:yerror}})
error_style!(d)
d[:markershape] = :hline
d[:x], d[:y] = error_coords(d[:x], d[:y], error_zipit(d[:yerror]))
KW[d]
end
function apply_series_recipe(d::KW, ::Type{Val{:xerror}})
error_style!(d)
d[:markershape] = :vline
d[:y], d[:x] = error_coords(d[:y], d[:x], error_zipit(d[:xerror]))
KW[d]
end
# ---------------------------------------------------------------------------
# quiver
# function apply_series_recipe(d::KW, ::Type{Val{:quiver}})
# d[:label] = ""
# d[:linetype] = :scatter
#
# # create a second series to draw the arrow shaft
# dpath = copy(d)
# error_style!(dpath)
# dpath[:markershape] = :none
#
# velocity = error_zipit(d[:quiver])
# xorig, yorig = d[:x], d[:y]
#
# # for each point, we create an arrow of velocity vi, translated to the x/y coordinates
# # x, y = zeros(0), zeros(0)
# paths = P2[]
# arrows = P2[]
# arrowshapes = Shape[]
# for i = 1:max(length(xorig), length(yorig))
#
# # get the starting position
# xi = get_mod(xorig, i)
# yi = get_mod(yorig, i)
# p = P2(xi, yi)
#
# # get the velocity
# vi = get_mod(velocity, i)
# vx, vy = if istuple(vi)
# first(vi), last(vi)
# elseif isscalar(vi)
# vi, vi
# else
# error("unexpected vi type $(typeof(vi)) for quiver: $vi")
# end
# v = P2(vx, vy)
#
# nanappend!(paths, [p, p+v])
# push!(arrows, p+v)
# push!(arrowshapes, makearrowhead(compute_angle(v)))
#
# # # dist = sqrt(vx^2 + vy^2)
# # dist = norm(v)
# # arrow_h = 0.1dist # height of arrowhead
# # arrow_w = 0.5arrow_h # halfwidth of arrowhead
# # U1 = v ./ dist # vector of arrowhead height
# # U2 = P2(-U1[2], U1[1]) # vector of arrowhead halfwidth
# # U1 *= arrow_h
# # U2 *= arrow_w
# #
# # append!(pts, P2(xi, yi) .+ P2[(0,0), v-U1, v-U1+U2, v, v-U1-U2, v-U1, (NaN,NaN)])
# # # a1 = v - arrow_h * U1 + arrow_w * U2
# # # a2 = v - arrow_h * U1 - arrow_w * U2
# # # nanappend!(x, xi + [0.0, vx, a1[1], a2[1], vx])
# # # nanappend!(y, yi + [0.0, vy, a1[2], a2[2], vy])
# end
#
# # d[:x], d[:y] = Plots.unzip(pts)
# dpath[:x], dpath[:y] = Plots.unzip(paths)
# d[:x], d[:y] = Plots.unzip(arrows)
# d[:markershape] = arrowshapes
#
# KW[dpath, d]
# end
function apply_series_recipe(d::KW, ::Type{Val{:quiver}})
d[:label] = ""
d[:linetype] = :shape
velocity = error_zipit(d[:quiver])
xorig, yorig = d[:x], d[:y]
# for each point, we create an arrow of velocity vi, translated to the x/y coordinates
pts = P2[]
for i = 1:max(length(xorig), length(yorig))
# get the starting position
xi = get_mod(xorig, i)
yi = get_mod(yorig, i)
p = P2(xi, yi)
# get the velocity
vi = get_mod(velocity, i)
vx, vy = if istuple(vi)
first(vi), last(vi)
elseif isscalar(vi)
vi, vi
elseif isa(vi,Function)
vi(xi, yi)
else
error("unexpected vi type $(typeof(vi)) for quiver: $vi")
end
v = P2(vx, vy)
dist = norm(v)
arrow_h = 0.1dist # height of arrowhead
arrow_w = 0.5arrow_h # halfwidth of arrowhead
U1 = v ./ dist # vector of arrowhead height
U2 = P2(-U1[2], U1[1]) # vector of arrowhead halfwidth
U1 *= arrow_h
U2 *= arrow_w
ppv = p+v
nanappend!(pts, P2[p, ppv-U1, ppv-U1+U2, ppv, ppv-U1-U2, ppv-U1])
end
d[:x], d[:y] = Plots.unzip(pts[2:end])
KW[d]
end
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
function rotate(x::Real, y::Real, θ::Real; center = (0,0))
cx = x - center[1]
cy = y - center[2]
xrot = cx * cos(θ) - cy * sin(θ)
yrot = cy * cos(θ) + cx * sin(θ)
xrot + center[1], yrot + center[2]
end
# ---------------------------------------------------------------------------
type EllipseRecipe <: PlotRecipe
w::Float64
h::Float64
x::Float64
y::Float64
θ::Float64
end
EllipseRecipe(w,h,x,y) = EllipseRecipe(w,h,x,y,0)
# return x,y coords of a rotated ellipse, centered at the origin
function rotatedEllipse(w, h, x, y, θ, rotθ)
# # coord before rotation
xpre = w * cos(θ)
ypre = h * sin(θ)
# rotate and translate
r = rotate(xpre, ypre, rotθ)
x + r[1], y + r[2]
end
function getRecipeXY(ep::EllipseRecipe)
x, y = unzip([rotatedEllipse(ep.w, ep.h, ep.x, ep.y, u, ep.θ) for u in linspace(0,2π,100)])
top = rotate(0, ep.h, ep.θ)
right = rotate(ep.w, 0, ep.θ)
linex = Float64[top[1], 0, right[1]] + ep.x
liney = Float64[top[2], 0, right[2]] + ep.y
Any[x, linex], Any[y, liney]
end
function getRecipeArgs(ep::EllipseRecipe)
[(:line, (3, [:dot :solid], [:red :blue], :path))]
end
# # -------------------------------------------------
"Sparsity plot... heatmap of non-zero values of a matrix"
function spy{T<:Real}(z::AMat{T}; kw...)
mat = map(zi->float(zi!=0), z)'
xn, yn = size(mat)
heatmap(mat; leg=false, yflip=true, aspect_ratio=:equal,
xlim=(0.5, xn+0.5), ylim=(0.5, yn+0.5),
kw...)
end
"Adds a+bx... straight line over the current plot"
function abline!(plt::Plot, a, b; kw...)
plot!(plt, [extrema(plt)...], x -> b + a*x; kw...)
end
abline!(args...; kw...) = abline!(current(), args...; kw...)
# =================================================
# Arc and chord diagrams
"Takes an adjacency matrix and returns source, destiny and weight lists"
function mat2list{T}(mat::AbstractArray{T,2})
nrow, ncol = size(mat) # rows are sources and columns are destinies
nosymmetric = !issym(mat) # plots only triu for symmetric matrices
nosparse = !issparse(mat) # doesn't plot zeros from a sparse matrix
L = length(mat)
source = Array(Int, L)
destiny = Array(Int, L)
weight = Array(T, L)
idx = 1
for i in 1:nrow, j in 1:ncol
value = mat[i, j]
if !isnan(value) && ( nosparse || value != zero(T) ) # TODO: deal with Nullable
if i < j
source[idx] = i
destiny[idx] = j
weight[idx] = value
idx += 1
elseif nosymmetric && (i > j)
source[idx] = i
destiny[idx] = j
weight[idx] = value
idx += 1
end
end
end
resize!(source, idx-1), resize!(destiny, idx-1), resize!(weight, idx-1)
end
# ---------------------------------------------------------------------------
# Arc Diagram
curvecolor(value, min, max, grad) = getColorZ(grad, (value-min)/(max-min))
"Plots a clockwise arc, from source to destiny, colored by weight"
function arc!(source, destiny, weight, min, max, grad)
radius = (destiny - source) / 2
arc = Plots.partialcircle(0, π, 30, radius)
x, y = Plots.unzip(arc)
plot!(x .+ radius .+ source, y, line = (curvecolor(weight, min, max, grad), 0.5, 2), legend=false)
end
"""
`arcdiagram(source, destiny, weight[, grad])`
Plots an arc diagram, form `source` to `destiny` (clockwise), using `weight` to determine the colors.
"""
function arcdiagram(source, destiny, weight; kargs...)
args = KW(kargs)
grad = pop!(args, :grad, ColorGradient([colorant"darkred", colorant"darkblue"]))
if length(source) == length(destiny) == length(weight)
vertices = unique(vcat(source, destiny))
sort!(vertices)
xmin, xmax = extrema(vertices)
plot(xlim=(xmin - 0.5, xmax + 0.5), legend=false)
wmin,wmax = extrema(weight)
for (i, j, value) in zip(source,destiny,weight)
arc!(i, j, value, wmin, wmax, grad)
end
scatter!(vertices, zeros(length(vertices)); legend=false, args...)
else
throw(ArgumentError("source, destiny and weight should have the same length"))
end
end
"""
`arcdiagram(mat[, grad])`
Plots an arc diagram from an adjacency matrix, form rows to columns (clockwise),
using the values on the matrix as weights to determine the colors.
Doesn't show edges with value zero if the input is sparse.
For simmetric matrices, only the upper triangular values are used.
"""
arcdiagram{T}(mat::AbstractArray{T,2}; kargs...) = arcdiagram(mat2list(mat)...; kargs...)
# ---------------------------------------------------------------------------
# Chord diagram
arcshape(θ1, θ2) = Shape(vcat(Plots.partialcircle(θ1, θ2, 15, 1.1),
reverse(Plots.partialcircle(θ1, θ2, 15, 0.9))))
colorlist(grad, ::Void) = :darkgray
function colorlist(grad, z)
zmin, zmax = extrema(z)
RGBA{Float64}[getColorZ(grad, (zi-zmin)/(zmax-zmin)) for zi in z]'
end
"""
`chorddiagram(source, destiny, weight[, grad, zcolor, group])`
Plots a chord diagram, form `source` to `destiny`,
using `weight` to determine the edge colors using `grad`.
`zcolor` or `group` can be used to determine the node colors.
"""
function chorddiagram(source, destiny, weight; kargs...)
args = KW(kargs)
grad = pop!(args, :grad, ColorGradient([colorant"darkred", colorant"darkblue"]))
zcolor= pop!(args, :zcolor, nothing)
group = pop!(args, :group, nothing)
if zcolor !== nothing && group !== nothing
throw(ErrorException("group and zcolor can not be used together."))
end
if length(source) == length(destiny) == length(weight)
plt = plot(xlim=(-2,2), ylim=(-2,2), legend=false, grid=false,
xticks=nothing, yticks=nothing,
xlim=(-1.2,1.2), ylim=(-1.2,1.2))
nodemin, nodemax = extrema(vcat(source, destiny))
weightmin, weightmax = extrema(weight)
A = 1.5π # Filled space
B = 0.5π # White space (empirical)
Δα = A / nodemax
Δβ = B / nodemax
δ = Δα + Δβ
for i in 1:length(source)
curve = BezierCurve(P2[ (cos((source[i ]-1)*δ + 0.5Δα), sin((source[i ]-1)*δ + 0.5Δα)), (0,0),
(cos((destiny[i]-1)*δ + 0.5Δα), sin((destiny[i]-1)*δ + 0.5Δα)) ])
plot!(curve_points(curve), line = (Plots.curvecolor(weight[i], weightmin, weightmax, grad), 1, 1))
end
if group === nothing
c = colorlist(grad, zcolor)
elseif length(group) == nodemax
idx = collect(0:(nodemax-1))
for g in group
plot!([arcshape(n*δ, n*δ + Δα) for n in idx[group .== g]]; args...)
end
return plt
else
throw(ErrorException("group should the ", nodemax, " elements."))
end
plot!([arcshape(n*δ, n*δ + Δα) for n in 0:(nodemax-1)]; mc=c, args...)
return plt
else
throw(ArgumentError("source, destiny and weight should have the same length"))
end
end
"""
`chorddiagram(mat[, grad, zcolor, group])`
Plots a chord diagram from an adjacency matrix,
using the values on the matrix as weights to determine edge colors.
Doesn't show edges with value zero if the input is sparse.
For simmetric matrices, only the upper triangular values are used.
`zcolor` or `group` can be used to determine the node colors.
"""
chorddiagram(mat::AbstractMatrix; kargs...) = chorddiagram(mat2list(mat)...; kargs...)