# 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 # ---------------------------------------------------------------------------------- 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 # # if it's not a recipe, just do nothing and return the args # function RecipesBase.apply_recipe(d::KW, args...; issubplot=false) # if issubplot && !isempty(args) && !haskey(d, :n) && !haskey(d, :layout) # # put in a sensible default # d[:n] = maximum(map(num_series, args)) # end # args # end if is_installed("DataFrames") @eval begin import DataFrames DFS = Union{Symbol, AbstractArray{Symbol}} function handle_dfs(df::DataFrames.AbstractDataFrame, d::KW, letter, dfs::DFS) if isa(dfs, Symbol) get!(d, Symbol(letter * "guide"), string(dfs)) collect(df[dfs]) else get!(d, :label, reshape(dfs, 1, length(dfs))) Any[collect(df[s]) for s in dfs] end end function extractGroupArgs(group::Symbol, df::DataFrames.AbstractDataFrame, args...) extractGroupArgs(collect(df[group])) end function handle_group(df::DataFrames.AbstractDataFrame, d::KW) if haskey(d, :group) g = d[:group] if isa(g, Symbol) d[:group] = collect(df[g]) end end end @recipe function f(df::DataFrames.AbstractDataFrame, sy::DFS) handle_group(df, d) handle_dfs(df, d, "y", sy) end @recipe function f(df::DataFrames.AbstractDataFrame, sx::DFS, sy::DFS) handle_group(df, d) x = handle_dfs(df, d, "x", sx) y = handle_dfs(df, d, "y", sy) x, y end @recipe function f(df::DataFrames.AbstractDataFrame, sx::DFS, sy::DFS, sz::DFS) handle_group(df, d) x = handle_dfs(df, d, "x", sx) y = handle_dfs(df, d, "y", sy) z = handle_dfs(df, d, "z", sz) x, y, z 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) 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 npoints = pop!(d, :npoints, 30) # 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 # @show lzrng, sizeof(lzrng) map(t -> 1+floor(Int, t * (length(rng)-1)), ts) # choose the line_z value of the control point just before this t push!(newlz, 0.0) append!(newlz, map(t -> lzrng[1+floor(Int, t * (length(rng)-1))], ts)) # lzrng = vcat() # nanappend!(newlz, #map(t -> bezier_value(cycle(lz,rng), t), 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] : default_gradient()) 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) edges = if nx == ny # x is centers, calc the edges # TODO: use bar_width, etc midpoints = x halfwidths = diff(midpoints) * 0.5 Float64[if i == 1 midpoints[1] - halfwidths[1] elseif i == ny+1 midpoints[i-1] + halfwidths[i-2] else midpoints[i-1] + halfwidths[i-1] end for i=1:ny+1] elseif nx == ny + 1 # x is edges x 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 # make fillto a vector... default fills to 0 fillto = d[:fillrange] if fillto == nothing fillto = zeros(1) elseif isa(fillto, Number) fillto = Float64[fillto] end nf = length(fillto) npts = 3ny + 1 heights = y x = zeros(npts) y = zeros(npts) fillrng = zeros(npts) # create the path in triplets. after the first bottom-left coord of the first bar: # add the top-left, top-right, and bottom-right coords for each height x[1] = edges[1] y[1] = fillto[1] fillrng[1] = fillto[1] for i=1:ny idx = 3i rng = idx-1:idx+1 fi = fillto[mod1(i,nf)] x[rng] = [edges[i], edges[i+1], edges[i+1]] y[rng] = [heights[i], heights[i], fi] fillrng[rng] = [fi, fi, fi] end x := x y := y fillrange := fillrng seriestype := :path () end @deps bar path # --------------------------------------------------------------------------- # 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) = v # 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) 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) # function apply_series_recipe(d::KW, ::Type{Val{:box}}) @recipe function f(::Type{Val{:boxplot}}, x, y, z; notch=false, range=1.5) # Plots.dumpdict(d, "box before", true) # create a list of shapes, where each shape is a single boxplot shapes = Shape[] groupby = extractGroupArgs(x) outliers_y = Float64[] outliers_x = Float64[] warning = false for (i, glabel) in enumerate(groupby.groupLabels) # filter y values values = d[:y][groupby.groupIds[i]] # then compute quantiles q1,q2,q3,q4,q5 = quantile(values, linspace(0,1,5)) # notch n = notch_width(q2, q4, length(values)) 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] l, m, r = center - _box_halfwidth, center, center + _box_halfwidth # internal nodes for notches L, R = center - 0.5 * _box_halfwidth, center + 0.5 * _box_halfwidth # 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 xcoords = notch::Bool ? [ m, l, r, m, m, NaN, # lower T l, l, L, R, r, r, l, NaN, # lower box l, l, L, R, r, r, l, NaN, # upper box m, l, r, m, m, NaN, # upper T ] : [ 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, NaN, # upper T ] ycoords = notch::Bool ? [ q1, q1, q1, q1, q2, NaN, # lower T q2, q3-n, q3, q3, q3-n, q2, q2, NaN, # lower box q4, q3+n, q3, q3, q3+n, q4, q4, NaN, # upper box q5, q5, q5, q5, q4, NaN, # upper T ] : [ 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[:plotarg_overrides] = KW(:xticks => (1:length(shapes), groupby.groupLabels)) seriestype := :shape # n = length(groupby.groupLabels) # xticks --> (linspace(0.5,n-0.5,n), groupby.groupLabels) # clean d pop!(d, :notch) pop!(d, :range) # we want to set the fields directly inside series recipes... args are ignored d[:x], d[:y] = Plots.shape_coords(shapes) # Outliers @series begin seriestype := :scatter markershape := :circle x := outliers_x y := outliers_y label := "" primary := false () end () # expects a tuple returned 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 # function apply_series_recipe(d::KW, ::Type{Val{:violin}}) @recipe function f(::Type{Val{:violin}}, x, y, z; trim=true) delete!(d, :trim) # create a list of shapes, where each shape is a single boxplot shapes = Shape[] groupby = extractGroupArgs(x) for (i, glabel) in enumerate(groupby.groupLabels) # get the edges and widths y = d[:y][groupby.groupIds[i]] widths, centers = violin_coords(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!(shapes, Shape(xcoords, ycoords)) end seriestype := :shape d[:x], d[:y] = shape_coords(shapes) () 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 # clean up d pop!(d, :trim) () end @deps density path # --------------------------------------------------------------------------- # 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...) # ================================================= # 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...)