Plots.jl/src/recipes.jl

1016 lines
28 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???
"""
You can easily define your own plotting recipes with convenience methods:
```
@userplot type GroupHist
args
end
@recipe function f(gh::GroupHist)
# set some attributes, add some series, using gh.args as input
end
# now you can plot like:
grouphist(rand(1000,4))
```
"""
macro userplot(expr)
_userplot(expr)
end
function _userplot(expr::Expr)
if expr.head != :type
errror("Must call userplot on a type/immutable expression. Got: $expr")
end
typename = expr.args[2]
funcname = Symbol(lowercase(string(typename)))
funcname2 = Symbol(funcname, "!")
# return a code block with the type definition and convenience plotting methods
esc(quote
$expr
export $funcname, $funcname2
$funcname(args...; kw...) = plot($typename(args); kw...)
$funcname2(args...; kw...) = plot!($typename(args); kw...)
end)
end
function _userplot(sym::Symbol)
_userplot(:(type $sym
args
end))
end
# ----------------------------------------------------------------------------------
const _series_recipe_deps = Dict()
function series_recipe_dependencies(st::Symbol, deps::Symbol...)
_series_recipe_deps[st] = deps
end
function seriestype_supported(st::Symbol)
seriestype_supported(backend(), st)
end
# returns :no, :native, or :recipe depending on how it's supported
function seriestype_supported(pkg::AbstractBackend, st::Symbol)
# is it natively supported
if st in supported_types(pkg)
return :native
end
haskey(_series_recipe_deps, st) || return :no
supported = true
for dep in _series_recipe_deps[st]
if seriestype_supported(pkg, dep) == :no
supported = false
end
end
supported ? :recipe : :no
end
macro deps(st, args...)
:(series_recipe_dependencies($(quot(st)), $(map(quot, args)...)))
end
# get a list of all seriestypes
function all_seriestypes()
sts = Set{Symbol}(keys(_series_recipe_deps))
for bsym in backends()
btype = _backendType[bsym]
sts = union(sts, Set{Symbol}(supported_types(btype())))
end
sort(collect(sts))
end
# ----------------------------------------------------------------------------------
num_series(x::AMat) = size(x,2)
num_series(x) = 1
RecipesBase.apply_recipe{T}(d::KW, ::Type{T}, plt::Plot) = throw(MethodError("Unmatched plot recipe: $T"))
if is_installed("DataFrames")
@eval begin
import DataFrames
# if it's one symbol, set the guide and return the column
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, sym::Symbol)
get!(d, Symbol(letter * "guide"), string(sym))
collect(df[sym])
end
# if it's an array of symbols, set the labels and return a Vector{Any} of columns
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, syms::AbstractArray{Symbol})
get!(d, :label, reshape(syms, 1, length(syms)))
Any[collect(df[s]) for s in syms]
end
# for anything else, no-op
function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, anything)
anything
end
# handle grouping by DataFrame column
function extractGroupArgs(group::Symbol, df::DataFrames.AbstractDataFrame, args...)
extractGroupArgs(collect(df[group]))
end
# if a DataFrame is the first arg, lets swap symbols out for columns
@recipe function f(df::DataFrames.AbstractDataFrame, args...)
# if any of these attributes are symbols, swap out for the df column
for k in (:fillrange, :line_z, :marker_z, :markersize, :ribbon, :weights, :xerror, :yerror)
if haskey(d, k) && isa(d[k], Symbol)
d[k] = collect(df[d[k]])
end
end
# return a list of new arguments
tuple(Any[handle_dfs(df, d, (i==1 ? "x" : i==2 ? "y" : "z"), arg) for (i,arg) in enumerate(args)]...)
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, st) = KW[d]
# for seriestype `line`, need to sort by x values
@recipe function f(::Type{Val{:line}}, x, y, z)
indices = sortperm(x)
x := x[indices]
y := y[indices]
if typeof(z) <: AVec
z := z[indices]
end
seriestype := :path
()
end
@deps line path
# @recipe function f(::Type{Val{:sticks}}, x, y, z)
# nx = length(x)
# n = 3nx
# newx, newy = zeros(n), zeros(n)
# for i=1:nx
# rng = 3i-2:3i
# newx[rng] = x[i]
# newy[rng] = [0., y[i], 0.]
# end
# x := newx
# y := newy
# seriestype := :path
# ()
# end
# @deps sticks path
function hvline_limits(axis::Axis)
vmin, vmax = axis_limits(axis)
if vmin >= vmax
if isfinite(vmin)
vmax = vmin + 1
else
vmin, vmax = 0.0, 1.1
end
end
vmin, vmax
end
@recipe function f(::Type{Val{:hline}}, x, y, z)
xmin, xmax = hvline_limits(d[:subplot][:xaxis])
n = length(y)
newx = repmat(Float64[xmin, xmax, NaN], n)
newy = vec(Float64[yi for i=1:3,yi=y])
x := newx
y := newy
seriestype := :path
()
end
@deps hline path
@recipe function f(::Type{Val{:vline}}, x, y, z)
ymin, ymax = hvline_limits(d[:subplot][:yaxis])
n = length(y)
newx = vec(Float64[yi for i=1:3,yi=y])
newy = repmat(Float64[ymin, ymax, NaN], n)
x := newx
y := newy
seriestype := :path
()
end
@deps vline path
# ---------------------------------------------------------------------------
# steps
function make_steps(x, y, st)
n = length(x)
newx, newy = zeros(2n-1), zeros(2n-1)
for i=1:n
idx = 2i-1
newx[idx] = x[i]
newy[idx] = y[i]
if i > 1
newx[idx-1] = x[st == :steppre ? i-1 : i]
newy[idx-1] = y[st == :steppre ? i : i-1]
end
end
newx, newy
end
# create a path from steps
@recipe function f(::Type{Val{:steppre}}, x, y, z)
d[:x], d[:y] = make_steps(x, y, :steppre)
seriestype := :path
# create a secondary series for the markers
if d[:markershape] != :none
@series begin
seriestype := :scatter
x := x
y := y
label := ""
primary := false
()
end
markershape := :none
end
()
end
@deps steppre path scatter
# create a path from steps
@recipe function f(::Type{Val{:steppost}}, x, y, z)
d[:x], d[:y] = make_steps(x, y, :steppost)
seriestype := :path
# create a secondary series for the markers
if d[:markershape] != :none
@series begin
seriestype := :scatter
x := x
y := y
label := ""
primary := false
()
end
markershape := :none
end
()
end
@deps steppost path scatter
# ---------------------------------------------------------------------------
# sticks
sticks_fillfrom(fr::Void, i::Integer) = 0.0
sticks_fillfrom(fr::Number, i::Integer) = fr
sticks_fillfrom(fr::AVec, i::Integer) = fr[mod1(i, length(fr))]
# create vertical line segments from fill
@recipe function f(::Type{Val{:sticks}}, x, y, z)
n = length(x)
fr = d[:fillrange]
newx, newy = zeros(3n), zeros(3n)
for i=1:n
rng = 3i-2:3i
newx[rng] = [x[i], x[i], NaN]
newy[rng] = [sticks_fillfrom(fr,i), y[i], NaN]
end
x := newx
y := newy
fillrange := nothing
seriestype := :path
# create a secondary series for the markers
if d[:markershape] != :none
@series begin
seriestype := :scatter
x := x
y := y
label := ""
primary := false
()
end
markershape := :none
end
()
end
@deps sticks path scatter
# ---------------------------------------------------------------------------
# bezier curves
# get the value of the curve point at position t
function bezier_value(pts::AVec, t::Real)
val = 0.0
n = length(pts)-1
for (i,p) in enumerate(pts)
val += p * binomial(n, i-1) * (1-t)^(n-i+1) * t^(i-1)
end
val
end
# create segmented bezier curves in place of line segments
@recipe function f(::Type{Val{:curves}}, x, y, z; npoints = 30)
args = z != nothing ? (x,y,z) : (x,y)
newx, newy = zeros(0), zeros(0)
fr = d[:fillrange]
newfr = fr != nothing ? zeros(0) : nothing
newz = z != nothing ? zeros(0) : nothing
lz = d[:line_z]
newlz = lz != nothing ? zeros(0) : nothing
# for each line segment (point series with no NaNs), convert it into a bezier curve
# where the points are the control points of the curve
for rng in iter_segments(args...)
length(rng) < 2 && continue
ts = linspace(0, 1, npoints)
nanappend!(newx, map(t -> bezier_value(cycle(x,rng), t), ts))
nanappend!(newy, map(t -> bezier_value(cycle(y,rng), t), ts))
if z != nothing
nanappend!(newz, map(t -> bezier_value(cycle(z,rng), t), ts))
end
if fr != nothing
nanappend!(newfr, map(t -> bezier_value(cycle(fr,rng), t), ts))
end
if lz != nothing
lzrng = cycle(lz, rng) # the line_z's for this segment
push!(newlz, 0.0)
append!(newlz, map(t -> lzrng[1+floor(Int, t * (length(rng)-1))], ts))
end
end
x := newx
y := newy
if z == nothing
seriestype := :path
else
seriestype := :path3d
z := newz
end
if fr != nothing
fillrange := newfr
end
if lz != nothing
line_z := newlz
linecolor := (isa(d[:linecolor], ColorGradient) ? d[:linecolor] : cgrad())
end
# Plots.DD(d)
()
end
@deps curves path
# ---------------------------------------------------------------------------
# create a bar plot as a filled step function
@recipe function f(::Type{Val{:bar}}, x, y, z)
nx, ny = length(x), length(y)
x = if nx == ny
x
elseif nx == ny + 1
diff(x) + x[1:end-1]
else
error("bar recipe: x must be same length as y (centers), or one more than y (edges).\n\t\tlength(x)=$(length(x)), length(y)=$(length(y))")
end
axis = d[:subplot][isvertical(d) ? :xaxis : :yaxis]
bw = d[:bar_width]
hw = if bw == nothing
0.5mean(diff([discrete_value!(axis, xi)[1] for xi=x]))
else
Float64[0.5cycle(bw,i) for i=1:length(x)]
end
# make fillto a vector... default fills to 0
fillto = d[:fillrange]
if fillto == nothing
fillto = 0
end
# create the bar shapes by adding x/y segments
xseg, yseg = Segments(), Segments()
for i=1:ny
center = discrete_value!(axis, x[i])[1]
hwi = cycle(hw,i)
yi = y[i]
fi = cycle(fillto,i)
push!(xseg, center-hwi, center-hwi, center+hwi, center+hwi, center-hwi)
push!(yseg, yi, fi, fi, yi, yi)
end
# switch back
if !isvertical(d)
xseg, yseg = yseg, xseg
end
x := xseg.pts
y := yseg.pts
seriestype := :shape
()
end
@deps bar shape
# ---------------------------------------------------------------------------
# Histograms
# edges from number of bins
function calc_edges(v, bins::Integer)
vmin, vmax = extrema(v)
linspace(vmin, vmax, bins+1)
end
# just pass through arrays
calc_edges(v, bins::AVec) = bins
# find the bucket index of this value
function bucket_index(vi, edges)
for (i,e) in enumerate(edges)
if vi <= e
return max(1,i-1)
end
end
return length(edges)-1
end
function my_hist(v, bins; normed = false, weights = nothing)
edges = calc_edges(v, bins)
counts = zeros(length(edges)-1)
# add a weighted count
for (i,vi) in enumerate(v)
idx = bucket_index(vi, edges)
counts[idx] += (weights == nothing ? 1.0 : weights[i])
end
# normalize by bar area?
norm_denom = normed ? sum(diff(edges) .* counts) : 1.0
if norm_denom == 0
norm_denom = 1.0
end
edges, counts ./ norm_denom
end
@recipe function f(::Type{Val{:histogram}}, x, y, z)
edges, counts = my_hist(y, d[:bins],
normed = d[:normalize],
weights = d[:weights])
x := edges
y := counts
seriestype := :bar
()
end
@deps histogram bar
# ---------------------------------------------------------------------------
# Histogram 2D
# if tuple, map out bins, otherwise use the same for both
calc_edges_2d(x, y, bins) = calc_edges(x, bins), calc_edges(y, bins)
calc_edges_2d{X,Y}(x, y, bins::Tuple{X,Y}) = calc_edges(x, bins[1]), calc_edges(y, bins[2])
# the 2D version
function my_hist_2d(x, y, bins; normed = false, weights = nothing)
xedges, yedges = calc_edges_2d(x, y, bins)
counts = zeros(length(yedges)-1, length(xedges)-1)
# add a weighted count
for i=1:length(x)
r = bucket_index(y[i], yedges)
c = bucket_index(x[i], xedges)
counts[r,c] += (weights == nothing ? 1.0 : weights[i])
end
# normalize to cubic area of the imaginary surface towers
norm_denom = normed ? sum((diff(yedges) * diff(xedges)') .* counts) : 1.0
if norm_denom == 0
norm_denom = 1.0
end
xedges, yedges, counts ./ norm_denom
end
centers(v::AVec) = v[1] + cumsum(diff(v))
@recipe function f(::Type{Val{:histogram2d}}, x, y, z)
xedges, yedges, counts = my_hist_2d(x, y, d[:bins],
normed = d[:normalize],
weights = d[:weights])
x := centers(xedges)
y := centers(yedges)
z := Surface(counts)
linewidth := 0
seriestype := :heatmap
()
end
@deps histogram2d heatmap
# ---------------------------------------------------------------------------
# scatter 3d
@recipe function f(::Type{Val{:scatter3d}}, x, y, z)
seriestype := :path3d
if d[:markershape] == :none
markershape := :circle
end
linewidth := 0
linealpha := 0
()
end
# note: don't add dependencies because this really isn't a drop-in replacement
# ---------------------------------------------------------------------------
# Box Plot
const _box_halfwidth = 0.4
notch_width(q2, q4, N) = 1.58 * (q4-q2)/sqrt(N)
@recipe function f(::Type{Val{:boxplot}}, x, y, z; notch=false, range=1.5)
xsegs, ysegs = Segments(), Segments()
glabels = sort(collect(unique(x)))
warning = false
outliers_x, outliers_y = zeros(0), zeros(0)
for (i,glabel) in enumerate(glabels)
# filter y
values = y[filter(i -> cycle(x,i) == glabel, 1:length(y))]
# compute quantiles
q1,q2,q3,q4,q5 = quantile(values, linspace(0,1,5))
# notch
n = notch_width(q2, q4, length(values))
# warn on inverted notches?
if notch && !warning && ( (q2>(q3-n)) || (q4<(q3+n)) )
warn("Boxplot's notch went outside hinges. Set notch to false.")
warning = true # Show the warning only one time
end
# make the shape
center = discrete_value!(d[:subplot][:xaxis], glabel)[1]
hw = d[:bar_width] == nothing ? _box_halfwidth : 0.5cycle(d[:bar_width], i)
l, m, r = center - hw, center, center + hw
# internal nodes for notches
L, R = center - 0.5 * hw, center + 0.5 * hw
# outliers
if Float64(range) != 0.0 # if the range is 0.0, the whiskers will extend to the data
limit = range*(q4-q2)
inside = Float64[]
for value in values
if (value < (q2 - limit)) || (value > (q4 + limit))
push!(outliers_y, value)
push!(outliers_x, center)
else
push!(inside, value)
end
end
# change q1 and q5 to show outliers
# using maximum and minimum values inside the limits
q1, q5 = extrema(inside)
end
# Box
if notch
push!(xsegs, m, l, r, m, m) # lower T
push!(xsegs, l, l, L, R, r, r, l) # lower box
push!(xsegs, l, l, L, R, r, r, l) # upper box
push!(xsegs, m, l, r, m, m) # upper T
push!(ysegs, q1, q1, q1, q1, q2) # lower T
push!(ysegs, q2, q3-n, q3, q3, q3-n, q2, q2) # lower box
push!(ysegs, q4, q3+n, q3, q3, q3+n, q4, q4) # upper box
push!(ysegs, q5, q5, q5, q5, q4) # upper T
else
push!(xsegs, m, l, r, m, m) # lower T
push!(xsegs, l, l, r, r, l) # lower box
push!(xsegs, l, l, r, r, l) # upper box
push!(xsegs, m, l, r, m, m) # upper T
push!(ysegs, q1, q1, q1, q1, q2) # lower T
push!(ysegs, q2, q3, q3, q2, q2) # lower box
push!(ysegs, q4, q3, q3, q4, q4) # upper box
push!(ysegs, q5, q5, q5, q5, q4) # upper T
end
end
# Outliers
@series begin
seriestype := :scatter
markershape := :circle
markercolor := d[:fillcolor]
markeralpha := d[:fillalpha]
markerstrokecolor := d[:linecolor]
markerstrokealpha := d[:linealpha]
x := outliers_x
y := outliers_y
primary := false
()
end
seriestype := :shape
x := xsegs.pts
y := ysegs.pts
()
end
@deps boxplot shape scatter
# ---------------------------------------------------------------------------
# Violin Plot
# if the user has KernelDensity installed, use this for violin plots.
# otherwise, just use a histogram
if is_installed("KernelDensity")
@eval import KernelDensity
@eval function violin_coords(y; trim::Bool=false)
kd = KernelDensity.kde(y, npoints = 200)
if trim
xmin, xmax = extrema(y)
inside = Bool[ xmin <= x <= xmax for x in kd.x]
return(kd.density[inside], kd.x[inside])
end
kd.density, kd.x
end
else
@eval function violin_coords(y; trim::Bool=false)
edges, widths = hist(y, 30)
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
@recipe function f(::Type{Val{:violin}}, x, y, z; trim=true)
xsegs, ysegs = Segments(), Segments()
glabels = sort(collect(unique(x)))
for glabel in glabels
widths, centers = violin_coords(y[filter(i -> cycle(x,i) == glabel, 1:length(y))], trim=trim)
isempty(widths) && continue
# normalize
widths = _box_halfwidth * widths / maximum(widths)
# make the violin
xcenter = discrete_value!(d[:subplot][:xaxis], glabel)[1]
xcoords = vcat(widths, -reverse(widths)) + xcenter
ycoords = vcat(centers, reverse(centers))
push!(xsegs, xcoords)
push!(ysegs, ycoords)
end
seriestype := :shape
x := xsegs.pts
y := ysegs.pts
()
end
@deps violin shape
# ---------------------------------------------------------------------------
# density
@recipe function f(::Type{Val{:density}}, x, y, z; trim=false)
newx, newy = violin_coords(y, trim=trim)
if isvertical(d)
newx, newy = newy, newx
end
x := newx
y := newy
seriestype := :path
()
end
@deps density path
# ---------------------------------------------------------------------------
# contourf - filled contours
@recipe function f(::Type{Val{:contourf}}, x, y, z)
fillrange := true
seriestype := :contour
()
end
# ---------------------------------------------------------------------------
# Error Bars
function error_style!(d::KW)
d[:seriestype] = :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 = cycle(xorig, i)
yi = cycle(yorig, i)
ebi = cycle(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
@recipe function f(::Type{Val{:yerror}}, x, y, z)
error_style!(d)
markershape := :hline
d[:x], d[:y] = error_coords(d[:x], d[:y], error_zipit(d[:yerror]))
()
end
@deps yerror path
@recipe function f(::Type{Val{:xerror}}, x, y, z)
error_style!(d)
markershape := :vline
d[:y], d[:x] = error_coords(d[:y], d[:x], error_zipit(d[:xerror]))
()
end
@deps xerror path
# ---------------------------------------------------------------------------
# quiver
# function apply_series_recipe(d::KW, ::Type{Val{:quiver}})
function quiver_using_arrows(d::KW)
d[:label] = ""
d[:seriestype] = :path
if !isa(d[:arrow], Arrow)
d[:arrow] = arrow()
end
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)
for i = 1:max(length(xorig), length(yorig))
# get the starting position
xi = cycle(xorig, i)
yi = cycle(yorig, i)
# get the velocity
vi = cycle(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
# add the points
nanappend!(x, [xi, xi+vx, NaN])
nanappend!(y, [yi, yi+vy, NaN])
end
d[:x], d[:y] = x, y
# KW[d]
end
# function apply_series_recipe(d::KW, ::Type{Val{:quiver}})
function quiver_using_hack(d::KW)
d[:label] = ""
d[:seriestype] = :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 = cycle(xorig, i)
yi = cycle(yorig, i)
p = P2(xi, yi)
# get the velocity
vi = cycle(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 apply_series_recipe(d::KW, ::Type{Val{:quiver}})
@recipe function f(::Type{Val{:quiver}}, x, y, z)
if :arrow in supported_args()
quiver_using_arrows(d)
else
quiver_using_hack(d)
end
()
end
@deps quiver shape path
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# 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
# -------------------------------------------------
# TODO: this should really be in another package...
type OHLC{T<:Real}
open::T
high::T
low::T
close::T
end
Base.convert(::Type{OHLC}, tup::Tuple) = OHLC(tup...)
# Base.tuple(ohlc::OHLC) = (ohlc.open, ohlc.high, ohlc.low, ohlc.close)
# get one OHLC path
function get_xy(o::OHLC, x, xdiff)
xl, xm, xr = x-xdiff, x, x+xdiff
ox = [xl, xm, NaN,
xm, xm, NaN,
xm, xr]
oy = [o.open, o.open, NaN,
o.low, o.high, NaN,
o.close, o.close]
ox, oy
end
# get the joined vector
function get_xy(v::AVec{OHLC}, x = 1:length(v))
xdiff = 0.3mean(abs(diff(x)))
x_out, y_out = zeros(0), zeros(0)
for (i,ohlc) in enumerate(v)
ox,oy = get_xy(ohlc, x[i], xdiff)
nanappend!(x_out, ox)
nanappend!(y_out, oy)
end
x_out, y_out
end
# these are for passing in a vector of OHLC objects
# TODO: when I allow `@recipe f(::Type{T}, v::T) = ...` definitions to replace convertToAnyVector,
# then I should replace these with one definition to convert to a vector of 4-tuples
# to squash ambiguity warnings...
@recipe f(x::AVec{Function}, v::AVec{OHLC}) = error()
@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(x::AVec{Function}, v::AVec{Tuple{R1,R2,R3,R4}}) = error()
# this must be OHLC?
@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(x::AVec, ohlc::AVec{Tuple{R1,R2,R3,R4}}) = x, OHLC[OHLC(t...) for t in ohlc]
@recipe function f(x::AVec, v::AVec{OHLC})
seriestype := :path
get_xy(v, x)
end
@recipe function f(v::AVec{OHLC})
seriestype := :path
get_xy(v)
end
# the series recipe, when passed vectors of 4-tuples
# -------------------------------------------------
# TODO: everything below here should be either changed to a
# series recipe or moved to PlotRecipes
"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...)