diff --git a/src/Plots.jl b/src/Plots.jl index fc3d87d0..b123958d 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -70,6 +70,8 @@ export boxplot!, violin, violin!, + quiver, + quiver!, title!, xlabel!, @@ -196,6 +198,8 @@ boxplot(args...; kw...) = plot(args...; kw..., linetype = :box) boxplot!(args...; kw...) = plot!(args...; kw..., linetype = :box) violin(args...; kw...) = plot(args...; kw..., linetype = :violin) violin!(args...; kw...) = plot!(args...; kw..., linetype = :violin) +quiver(args...; kw...) = plot(args...; kw..., linetype = :quiver) +quiver!(args...; kw...) = plot!(args...; kw..., linetype = :quiver) title!(s::AbstractString; kw...) = plot!(; title = s, kw...) diff --git a/src/args.jl b/src/args.jl index 98bba5b7..e69dcf97 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, :box, :violin + :contour, :pie, :shape, :box, :violin, :quiver ], _3dTypes) @compat const _typeAliases = KW( :n => :none, @@ -39,6 +39,8 @@ const _allTypes = vcat([ :poly => :shape, :polygon => :shape, :boxplot => :box, + :velocity => :quiver, + :gradient => :quiver, ) like_histogram(linetype::Symbol) = linetype in (:hist, :density) @@ -148,6 +150,7 @@ _seriesDefaults[:orientation] = :vertical _seriesDefaults[:xerror] = nothing _seriesDefaults[:yerror] = nothing _seriesDefaults[:ribbon] = nothing +_seriesDefaults[:quiver] = nothing const _plotDefaults = KW() @@ -333,6 +336,9 @@ end :xerrorbar => :xerror, :yerr => :yerror, :yerrorbar => :yerror, + :velocity => :quiver, + :quiver2d => :quiver, + :gradient => :quiver, ) # add all pluralized forms to the _keyAliases dict diff --git a/src/backends/gadfly.jl b/src/backends/gadfly.jl index c6724e89..01490e14 100644 --- a/src/backends/gadfly.jl +++ b/src/backends/gadfly.jl @@ -132,10 +132,16 @@ end # --------------------------------------------------------------------------- +get_shape(sym::Symbol) = _shapes[sym] +get_shape(shape::Shape) = shape + # extract the underlying ShapeGeometry object(s) -getMarkerGeom(shape::Shape) = gadflyshape(shape) -getMarkerGeom(shape::Symbol) = gadflyshape(_shapes[shape]) -getMarkerGeom(shapes::AVec) = map(getMarkerGeom, shapes) +getMarkerGeom(shapes::AVec) = gadflyshape(map(get_shape, shapes)) +getMarkerGeom(other) = gadflyshape(get_shape(other)) + +# getMarkerGeom(shape::Shape) = gadflyshape(shape) +# getMarkerGeom(shape::Symbol) = gadflyshape(_shapes[shape]) +# getMarkerGeom(shapes::AVec) = gadflyshape(map(gadflyshape, shapes)) # map(getMarkerGeom, shapes) function getMarkerGeom(d::KW) if d[:linetype] == :shape Gadfly.Geom.polygon(fill = true, preserve_order = true) diff --git a/src/backends/gadfly_shapes.jl b/src/backends/gadfly_shapes.jl index 25672598..51a3c5ba 100644 --- a/src/backends/gadfly_shapes.jl +++ b/src/backends/gadfly_shapes.jl @@ -1,8 +1,9 @@ # Geometry which displays arbitrary shapes at given (x, y) positions. +# note: vertices is a list of shapes immutable ShapeGeometry <: Gadfly.GeometryElement - vertices::AbstractVector{@compat(Tuple{Float64,Float64})} + vertices::AbstractVector #{Tuple{Float64,Float64}} tag::Symbol function ShapeGeometry(shape; tag::Symbol=Gadfly.Geom.empty_tag) @@ -66,24 +67,27 @@ function Gadfly.render(geom::ShapeGeometry, theme::Gadfly.Theme, aes::Gadfly.Aes end function gadflyshape(sv::Shape) - ShapeGeometry(sv.vertices) + ShapeGeometry(Any[sv.vertices]) +end + +function gadflyshape(sv::AVec{Shape}) + ShapeGeometry(Any[s.vertices for s in sv]) end # create a Compose context given a ShapeGeometry and the xs/ys/sizes function make_polygon(geom::ShapeGeometry, xs::AbstractArray, ys::AbstractArray, rs::AbstractArray) n = max(length(xs), length(ys), length(rs)) - T = @compat(Tuple{Compose.Measure, Compose.Measure}) + T = Tuple{Compose.Measure, Compose.Measure} polys = Array(Vector{T}, n) for i in 1:n x = Compose.x_measure(xs[mod1(i, length(xs))]) y = Compose.y_measure(ys[mod1(i, length(ys))]) r = rs[mod1(i, length(rs))] - polys[i] = T[(x + r * sx, y + r * sy) for (sx,sy) in geom.vertices] + polys[i] = T[(x + r * sx, y + r * sy) for (sx,sy) in get_mod(geom.vertices, i)] end Gadfly.polygon(polys, geom.tag) end # --------------------------------------------------------------------------------------------- - diff --git a/src/backends/supported.jl b/src/backends/supported.jl index ea8023db..8750a569 100644 --- a/src/backends/supported.jl +++ b/src/backends/supported.jl @@ -80,11 +80,12 @@ supportedArgs(::GadflyBackend) = [ :xerror, :yerror, :ribbon, + :quiver, :orientation, ] supportedAxes(::GadflyBackend) = [:auto, :left] supportedTypes(::GadflyBackend) = [:none, :line, :path, :steppre, :steppost, :sticks, - :scatter, :hist2d, :hexbin, :hist, :bar, :box, :violin, + :scatter, :hist2d, :hexbin, :hist, :bar, :box, :violin, :quiver, :hline, :vline, :contour, :shape] supportedStyles(::GadflyBackend) = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] supportedMarkers(::GadflyBackend) = vcat(_allMarkers, Shape) @@ -171,11 +172,12 @@ supportedArgs(::PyPlotBackend) = [ :xerror, :yerror, :ribbon, + :quiver, :orientation, ] supportedAxes(::PyPlotBackend) = _allAxes supportedTypes(::PyPlotBackend) = [:none, :line, :path, :steppre, :steppost, #:sticks, - :scatter, :hist2d, :hexbin, :hist, :density, :bar, :box, :violin, + :scatter, :hist2d, :hexbin, :hist, :density, :bar, :box, :violin, :quiver, :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] @@ -249,6 +251,7 @@ supportedArgs(::GRBackend) = [ :xerror, :yerror, :ribbon, + :quiver, :orientation, ] supportedAxes(::GRBackend) = _allAxes @@ -575,6 +578,7 @@ supportedArgs(::PlotlyBackend) = [ :xerror, :yerror, :ribbon, + :quiver, :orientation, ] supportedAxes(::PlotlyBackend) = [:auto, :left] @@ -649,6 +653,7 @@ supportedArgs(::PlotlyJSBackend) = [ :xerror, :yerror, :ribbon, + :quiver, :orientation, ] supportedAxes(::PlotlyJSBackend) = [:auto, :left] diff --git a/src/components.jl b/src/components.jl index da482b39..ea560eaa 100644 --- a/src/components.jl +++ b/src/components.jl @@ -9,6 +9,13 @@ export typealias P2 FixedSizeArrays.Vec{2,Float64} typealias P3 FixedSizeArrays.Vec{3,Float64} +nanpush!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); push!(a, b)) +nanappend!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); append!(a, b)) +nanpush!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); push!(a, b)) +nanappend!(a::AbstractVector{P3}, b) = (push!(a, P3(NaN,NaN,NaN)); append!(a, b)) +compute_angle(v::P2) = (angle = atan2(v[2], v[1]); angle < 0 ? 2π - angle : angle) + +# ------------------------------------------------------------- immutable Shape vertices::AVec @@ -98,6 +105,14 @@ function makecross(; offset = -0.5, radius = 1.0) end +from_polar(angle, dist) = P2(dist*cos(angle), dist*sin(angle)) + +function makearrowhead(angle; h = 2.0, w = 0.4) + tip = from_polar(angle, h) + Shape(P2[(0,0), from_polar(angle - 0.5π, w) - tip, + from_polar(angle + 0.5π, w) - tip, (0,0)]) +end + const _shapes = KW( :ellipse => makeshape(20), :rect => makeshape(4, offset=-0.25), diff --git a/src/recipes.jl b/src/recipes.jl index 9e37fc13..954db30b 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -147,12 +147,18 @@ function error_style!(d::KW) 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) - if istuple(ebar) - ebar = collect(zip(ebar...)) - end # for each point, create a line segment from the bottom to the top of the errorbar for i = 1:max(length(xorig), length(yorig)) @@ -177,19 +183,131 @@ end 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], d[:yerror]) + 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], d[:xerror]) + 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) + KW[d] +end + + + # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- diff --git a/src/series_args.jl b/src/series_args.jl index 1449648c..8a8b4098 100644 --- a/src/series_args.jl +++ b/src/series_args.jl @@ -161,11 +161,20 @@ function build_series_args(plt::AbstractPlot, kw::KW) #, idxfilter) # handle ribbons if get(d, :ribbon, nothing) != nothing - # append!(ret, apply_series_recipe(copy(d), Val{:ribbon})) rib = d[:ribbon] d[:fillrange] = (d[:y] - rib, d[:y] + rib) end + # handle quiver plots + if lt == :quiver + d[:linetype] = lt = :path + d[:linewidth] = 0 + end + if get(d, :quiver, nothing) != nothing + append!(ret, apply_series_recipe(copy(d), Val{:quiver})) + 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 diff --git a/src/utils.jl b/src/utils.jl index a37d71ce..2682c6f8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -240,6 +240,7 @@ Base.merge(a::AbstractVector, b::AbstractVector) = sort(unique(vcat(a,b))) nanpush!(a::AbstractVector, b) = (push!(a, NaN); push!(a, b)) nanappend!(a::AbstractVector, b) = (push!(a, NaN); append!(a, b)) + # --------------------------------------------------------------- wraptuple(x::@compat(Tuple)) = x