diff --git a/.travis.yml b/.travis.yml index 45ef5b92..1253ee9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ language: julia os: - linux - - osx + # - osx julia: - 0.5 - - nightly matrix: allow_failures: - julia: nightly diff --git a/NEWS.md b/NEWS.md index a832f440..c1c98498 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,43 @@ ## 0.9 (current master/dev) +#### 0.9.5 + +- added dependency on PlotThemes +- set_theme --> theme +- remove Compat from REQUIRE +- warning for DataFrames without StatPlots +- closeall exported and implemented for gr/pyplot +- fix DateTime recipe +- reset theme with theme(:none) +- fix link_axes! for nested subplots +- fix plotly lims for log scale + +#### 0.9.4 + +- optimizations surrounding Subplot.series_list +- better Atom support, support plotlyjs +- gr: + - gks_wstype defaults and gr_set_output + - heatmap uses GR.drawimage +- histogram2d puts NaN for zeros +- improved support of NaN in heatmaps +- rebuilt spy recipes to output scatters with marker_z set +- deprecate png support in plotly... point to plotlyjs +- fixes: + - axis widen with lims set + - reset_extrema, setxyz + - bar plot widen + - better tick padding + - consistent tick rotation + - consistent aspect_ratio + - pyplot dpi + - plotly horizontal bars + - handle series attributes when combining subplots + - gr images transposed + - converted Date/DateTime to new type recipe approach for arrays +- issues closed include: #505 #513 #479 #523 #526 #529 + #### 0.9.3 - support pdf and eps in plotlyjs backend diff --git a/REQUIRE b/REQUIRE index f0aaec83..67e5f781 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,9 +1,9 @@ -julia 0.5- +julia 0.5 RecipesBase PlotUtils +PlotThemes Reexport -Compat FixedSizeArrays Measures Showoff diff --git a/src/Plots.jl b/src/Plots.jl index ff0c7bc9..9b4bcb10 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -1,40 +1,27 @@ -__precompile__() - module Plots -# using Compat using Reexport -# @reexport using Colors -# using Requires using FixedSizeArrays @reexport using RecipesBase using Base.Meta @reexport using PlotUtils +@reexport using PlotThemes import Showoff export - AbstractPlot, - Plot, - Subplot, - AbstractLayout, - GridLayout, grid, - EmptyLayout, bbox, plotarea, @layout, - AVec, - AMat, KW, wrap, - set_theme, - add_theme, + theme, plot, plot!, - update!, + attr!, current, default, @@ -70,6 +57,8 @@ export savefig, png, gui, + inline, + closeall, backend, backends, @@ -77,12 +66,10 @@ export backend_object, add_backend, aliases, - # dataframes, Shape, text, font, - Axis, stroke, brush, Surface, @@ -94,11 +81,12 @@ export Animation, frame, gif, + mov, + mp4, + animate, @animate, @gif, - spy, - test_examples, iter_segments, coords, @@ -110,9 +98,7 @@ export center, P2, P3, - BezierCurve, - curve_points, - directed_curve + BezierCurve # --------------------------------------------------------- @@ -206,28 +192,29 @@ yflip!(flip::Bool = true; kw...) = plot!(; yflip = flip xaxis!(args...; kw...) = plot!(; xaxis = args, kw...) yaxis!(args...; kw...) = plot!(; yaxis = args, kw...) -title!(plt::Plot, s::AbstractString; kw...) = plot!(plt; title = s, kw...) -xlabel!(plt::Plot, s::AbstractString; kw...) = plot!(plt; xlabel = s, kw...) -ylabel!(plt::Plot, s::AbstractString; kw...) = plot!(plt; ylabel = s, kw...) -xlims!{T<:Real,S<:Real}(plt::Plot, lims::Tuple{T,S}; kw...) = plot!(plt; xlims = lims, kw...) -ylims!{T<:Real,S<:Real}(plt::Plot, lims::Tuple{T,S}; kw...) = plot!(plt; ylims = lims, kw...) -zlims!{T<:Real,S<:Real}(plt::Plot, lims::Tuple{T,S}; kw...) = plot!(plt; zlims = lims, kw...) -xlims!(plt::Plot, xmin::Real, xmax::Real; kw...) = plot!(plt; xlims = (xmin,xmax), kw...) -ylims!(plt::Plot, ymin::Real, ymax::Real; kw...) = plot!(plt; ylims = (ymin,ymax), kw...) -zlims!(plt::Plot, zmin::Real, zmax::Real; kw...) = plot!(plt; zlims = (zmin,zmax), kw...) -xticks!{T<:Real}(plt::Plot, ticks::AVec{T}; kw...) = plot!(plt; xticks = ticks, kw...) -yticks!{T<:Real}(plt::Plot, ticks::AVec{T}; kw...) = plot!(plt; yticks = ticks, kw...) -xticks!{T<:Real,S<:AbstractString}(plt::Plot, - ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(plt; xticks = (ticks,labels), kw...) -yticks!{T<:Real,S<:AbstractString}(plt::Plot, - ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(plt; yticks = (ticks,labels), kw...) -annotate!(plt::Plot, anns...; kw...) = plot!(plt; annotation = anns, kw...) -annotate!{T<:Tuple}(plt::Plot, anns::AVec{T}; kw...) = plot!(plt; annotation = anns, kw...) -xflip!(plt::Plot, flip::Bool = true; kw...) = plot!(plt; xflip = flip, kw...) -yflip!(plt::Plot, flip::Bool = true; kw...) = plot!(plt; yflip = flip, kw...) -xaxis!(plt::Plot, args...; kw...) = plot!(plt; xaxis = args, kw...) -yaxis!(plt::Plot, args...; kw...) = plot!(plt; yaxis = args, kw...) - +let PlotOrSubplot = Union{Plot, Subplot} + title!(plt::PlotOrSubplot, s::AbstractString; kw...) = plot!(plt; title = s, kw...) + xlabel!(plt::PlotOrSubplot, s::AbstractString; kw...) = plot!(plt; xlabel = s, kw...) + ylabel!(plt::PlotOrSubplot, s::AbstractString; kw...) = plot!(plt; ylabel = s, kw...) + xlims!{T<:Real,S<:Real}(plt::PlotOrSubplot, lims::Tuple{T,S}; kw...) = plot!(plt; xlims = lims, kw...) + ylims!{T<:Real,S<:Real}(plt::PlotOrSubplot, lims::Tuple{T,S}; kw...) = plot!(plt; ylims = lims, kw...) + zlims!{T<:Real,S<:Real}(plt::PlotOrSubplot, lims::Tuple{T,S}; kw...) = plot!(plt; zlims = lims, kw...) + xlims!(plt::PlotOrSubplot, xmin::Real, xmax::Real; kw...) = plot!(plt; xlims = (xmin,xmax), kw...) + ylims!(plt::PlotOrSubplot, ymin::Real, ymax::Real; kw...) = plot!(plt; ylims = (ymin,ymax), kw...) + zlims!(plt::PlotOrSubplot, zmin::Real, zmax::Real; kw...) = plot!(plt; zlims = (zmin,zmax), kw...) + xticks!{T<:Real}(plt::PlotOrSubplot, ticks::AVec{T}; kw...) = plot!(plt; xticks = ticks, kw...) + yticks!{T<:Real}(plt::PlotOrSubplot, ticks::AVec{T}; kw...) = plot!(plt; yticks = ticks, kw...) + xticks!{T<:Real,S<:AbstractString}(plt::PlotOrSubplot, + ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(plt; xticks = (ticks,labels), kw...) + yticks!{T<:Real,S<:AbstractString}(plt::PlotOrSubplot, + ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(plt; yticks = (ticks,labels), kw...) + annotate!(plt::PlotOrSubplot, anns...; kw...) = plot!(plt; annotation = anns, kw...) + annotate!{T<:Tuple}(plt::PlotOrSubplot, anns::AVec{T}; kw...) = plot!(plt; annotation = anns, kw...) + xflip!(plt::PlotOrSubplot, flip::Bool = true; kw...) = plot!(plt; xflip = flip, kw...) + yflip!(plt::PlotOrSubplot, flip::Bool = true; kw...) = plot!(plt; yflip = flip, kw...) + xaxis!(plt::PlotOrSubplot, args...; kw...) = plot!(plt; xaxis = args, kw...) + yaxis!(plt::PlotOrSubplot, args...; kw...) = plot!(plt; yaxis = args, kw...) +end # --------------------------------------------------------- @@ -247,11 +234,4 @@ end # --------------------------------------------------------- -# if VERSION >= v"0.4.0-dev+5512" -# include("precompile.jl") -# _precompile_() -# end - -# --------------------------------------------------------- - end # module diff --git a/src/animation.jl b/src/animation.jl index c991ada9..ee7428df 100644 --- a/src/animation.jl +++ b/src/animation.jl @@ -1,62 +1,109 @@ immutable Animation - dir::String - frames::Vector{String} + dir::String + frames::Vector{String} end function Animation() - tmpdir = convert(String, mktempdir()) - Animation(tmpdir, String[]) + tmpdir = convert(String, mktempdir()) + Animation(tmpdir, String[]) end function frame{P<:AbstractPlot}(anim::Animation, plt::P=current()) - i = length(anim.frames) + 1 - filename = @sprintf("%06d.png", i) - png(plt, joinpath(anim.dir, filename)) - push!(anim.frames, filename) + i = length(anim.frames) + 1 + filename = @sprintf("%06d.png", i) + png(plt, joinpath(anim.dir, filename)) + push!(anim.frames, filename) end +giffn() = (isijulia() ? "tmp.gif" : tempname()*".gif") +movfn() = (isijulia() ? "tmp.mov" : tempname()*".mov") +mp4fn() = (isijulia() ? "tmp.mp4" : tempname()*".mp4") + +type FrameIterator + itr + every::Int + kw +end +FrameIterator(itr; every=1, kw...) = FrameIterator(itr, every, kw) + +""" +Animate from an iterator which returns the plot args each iteration. +""" +function animate(fitr::FrameIterator, fn = giffn(); kw...) + anim = Animation() + for (i, plotargs) in enumerate(fitr.itr) + if mod1(i, fitr.every) == 1 + plot(wraptuple(plotargs)...; fitr.kw...) + frame(anim) + end + end + gif(anim, fn; kw...) +end + +# most things will implement this +function animate(obj, fn = giffn(); every=1, fps=20, loop=0, kw...) + animate(FrameIterator(obj, every, kw), fn; fps=fps, loop=loop) +end # ----------------------------------------------- "Wraps the location of an animated gif so that it can be displayed" immutable AnimatedGif - filename::String + filename::String end -function gif(anim::Animation, fn = (isijulia() ? "tmp.gif" : tempname()*".gif"); fps::Integer = 20, loop::Integer = 0) - fn = abspath(fn) +file_extension(fn) = Base.Filesystem.splitext(fn)[2][2:end] - try +gif(anim::Animation, fn = giffn(); kw...) = buildanimation(anim.dir, fn; kw...) +mov(anim::Animation, fn = movfn(); kw...) = buildanimation(anim.dir, fn; kw...) +mp4(anim::Animation, fn = mp4fn(); kw...) = buildanimation(anim.dir, fn; kw...) - # high quality - speed = round(Int, 100 / fps) - file = joinpath(Pkg.dir("ImageMagick"), "deps","deps.jl") - if isfile(file) && !haskey(ENV, "MAGICK_CONFIGURE_PATH") - include(file) +const _imagemagick_initialized = Ref(false) + +function buildanimation(animdir::AbstractString, fn::AbstractString; + fps::Integer = 20, loop::Integer = 0) + fn = abspath(fn) + try + if !_imagemagick_initialized[] + file = joinpath(Pkg.dir("ImageMagick"), "deps","deps.jl") + if isfile(file) && !haskey(ENV, "MAGICK_CONFIGURE_PATH") + include(file) + end + _imagemagick_initialized[] = true + end + + # prefix = get(ENV, "MAGICK_CONFIGURE_PATH", "") + # high quality + speed = round(Int, 100 / fps) + run(`convert -delay $speed -loop $loop $(joinpath(animdir, "*.png")) -alpha off $fn`) + + catch err + warn("""Tried to create gif using convert (ImageMagick), but got error: $err + ImageMagick can be installed by executing `Pkg.add("ImageMagick")` + Will try ffmpeg, but it's lower quality...)""") + + # low quality + run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -y $fn`) + # run(`ffmpeg -v warning -i "fps=$fps,scale=320:-1:flags=lanczos"`) end - # prefix = get(ENV, "MAGICK_CONFIGURE_PATH", "") - run(`convert -delay $speed -loop $loop $(joinpath(anim.dir, "*.png")) -alpha off $fn`) - catch err - warn("""Tried to create gif using convert (ImageMagick), but got error: $err - ImageMagick can be installed by executing `Pkg.add("ImageMagick")` - Will try ffmpeg, but it's lower quality...)""") - - # low quality - run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(anim.dir)/%06d.png -y $fn`) - # run(`ffmpeg -v warning -i "fps=$fps,scale=320:-1:flags=lanczos"`) - end - - info("Saved animation to ", fn) - AnimatedGif(fn) + info("Saved animation to ", fn) + AnimatedGif(fn) end # write out html to view the gif... note the rand call which is a hack so the image doesn't get cached function Base.show(io::IO, ::MIME"text/html", agif::AnimatedGif) - write(io, "\" />") + ext = file_extension(agif.filename) + write(io, if ext == "gif" + "\" />" + elseif ext in ("mov", "mp4") + "" + else + error("Cannot show animation with extension $ext: $agif") + end) end @@ -122,7 +169,7 @@ Example: ``` """ macro gif(forloop::Expr, args...) - _animate(forloop, args...; callgif = true) + _animate(forloop, args...; callgif = true) end """ @@ -131,13 +178,13 @@ Collect one frame per for-block iteration and return an `Animation` object. Example: ``` - p = plot(1) - anim = @animate for x=0:0.1:5 +p = plot(1) +anim = @animate for x=0:0.1:5 push!(p, 1, sin(x)) - end - gif(anim) +end +gif(anim) ``` """ macro animate(forloop::Expr, args...) - _animate(forloop, args...) + _animate(forloop, args...) end diff --git a/src/arg_desc.jl b/src/arg_desc.jl index 06f525bb..b4503c1e 100644 --- a/src/arg_desc.jl +++ b/src/arg_desc.jl @@ -19,7 +19,7 @@ const _arg_desc = KW( :markersize => "Number or AbstractVector. Size (radius pixels) of the markers.", :markerstrokestyle => "Symbol. Style of the marker stroke (border). Choose from $(_allStyles)", :markerstrokewidth => "Number. Width of the marker stroke (border. in pixels)", -:markerstrokecolor => "Color Type. Color of the marker stroke (border). `:match` will take the value from `:seriescolor`.", +:markerstrokecolor => "Color Type. Color of the marker stroke (border). `:match` will take the value from `:foreground_color_subplot`.", :markerstrokealpha => "Number in [0,1]. The alpha/opacity override for the marker stroke (border). `nothing` (the default) means it will take the alpha value of markerstrokecolor.", :bins => "Integer, NTuple{2,Integer}, AbstractVector. For histogram-types, defines the number of bins, or the edges, of the histogram.", :smooth => "Bool. Add a regression line?", @@ -85,7 +85,7 @@ const _arg_desc = KW( :grid => "Bool. Show the grid lines?", :annotations => "(x,y,text) tuple(s). Can be a single tuple or a list of them. Text can be String or PlotText (created with `text(args...)`) Add one-off text annotations at the x,y coordinates.", :projection => "Symbol or String. '3d' or 'polar'", -:aspect_ratio => "Symbol (:equal) or Number (width to height ratio of plot area).", +:aspect_ratio => "Symbol (:equal) or Number. Plot area is resized so that 1 y-unit is the same size as `apect_ratio` x-units.", :margin => "Measure (multiply by `mm`, `px`, etc). Base for individual margins... not directly used. Specifies the extra padding around subplots.", :left_margin => "Measure (multiply by `mm`, `px`, etc) or `:match` (matches `:margin`). Specifies the extra padding to the left of the subplot.", :top_margin => "Measure (multiply by `mm`, `px`, etc) or `:match` (matches `:margin`). Specifies the extra padding on the top of the subplot.", diff --git a/src/args.jl b/src/args.jl index 133974b3..119dd125 100644 --- a/src/args.jl +++ b/src/args.jl @@ -123,6 +123,16 @@ const _markerAliases = Dict{Symbol,Symbol}( :dtri => :dtriangle, :downtri => :dtriangle, :downtriangle => :dtriangle, + :> => :rtriangle, + :rt => :rtriangle, + :rtri => :rtriangle, + :righttri => :rtriangle, + :righttriangle => :rtriangle, + :< => :ltriangle, + :lt => :ltriangle, + :ltri => :ltriangle, + :lighttri => :ltriangle, + :lighttriangle => :ltriangle, # :+ => :cross, :plus => :cross, # :x => :xcross, @@ -165,7 +175,7 @@ const _series_defaults = KW( :markershape => :none, :markercolor => :match, :markeralpha => nothing, - :markersize => 6, + :markersize => 4, :markerstrokestyle => :solid, :markerstrokewidth => 1, :markerstrokecolor => :match, @@ -194,7 +204,7 @@ const _series_defaults = KW( :match_dimensions => false, # do rows match x (true) or y (false) for heatmap/image/spy? see issue 196 # this ONLY effects whether or not the z-matrix is transposed for a heatmap display! :subplot => :auto, # which subplot(s) does this series belong to? - :series_annotations => [], # a list of annotations which apply to the coordinates of this series + :series_annotations => nothing, # a list of annotations which apply to the coordinates of this series :primary => true, # when true, this "counts" as a series for color selection, etc. the main use is to allow # one logical series to be broken up (path and markers, for example) :hover => nothing, # text to display when hovering over the data points @@ -353,6 +363,7 @@ add_aliases(:seriescolor, :c, :color, :colour) add_aliases(:linecolor, :lc, :lcolor, :lcolour, :linecolour) add_aliases(:markercolor, :mc, :mcolor, :mcolour, :markercolour) add_aliases(:markerstrokecolor, :msc, :mscolor, :mscolour, :markerstrokecolour) +add_aliases(:markerstrokewidth, :msw, :mswidth) add_aliases(:fillcolor, :fc, :fcolor, :fcolour, :fillcolour) add_aliases(:background_color, :bg, :bgcolor, :bg_color, :background, @@ -387,7 +398,7 @@ add_aliases(:foreground_color_guide, :fg_guide, :fgguide, :fgcolor_guide, :fg_co # alphas add_aliases(:seriesalpha, :alpha, :α, :opacity) add_aliases(:linealpha, :la, :lalpha, :lα, :lineopacity, :lopacity) -add_aliases(:makeralpha, :ma, :malpha, :mα, :makeropacity, :mopacity) +add_aliases(:markeralpha, :ma, :malpha, :mα, :markeropacity, :mopacity) add_aliases(:markerstrokealpha, :msa, :msalpha, :msα, :markerstrokeopacity, :msopacity) add_aliases(:fillalpha, :fa, :falpha, :fα, :fillopacity, :fopacity) @@ -439,7 +450,7 @@ add_aliases(:match_dimensions, :transpose, :transpose_z) add_aliases(:subplot, :sp, :subplt, :splt) add_aliases(:projection, :proj) add_aliases(:title_location, :title_loc, :titleloc, :title_position, :title_pos, :titlepos, :titleposition, :title_align, :title_alignment) -add_aliases(:series_annotations, :series_ann, :seriesann, :series_anns, :seriesanns, :series_annotation) +add_aliases(:series_annotations, :series_ann, :seriesann, :series_anns, :seriesanns, :series_annotation, :text, :txt, :texts, :txts) add_aliases(:html_output_format, :format, :fmt, :html_format) add_aliases(:orientation, :direction, :dir) add_aliases(:inset_subplots, :inset, :floating) @@ -691,6 +702,11 @@ function preprocessArgs!(d::KW) end delete!(d, :fill) + # handle series annotations + if haskey(d, :series_annotations) + d[:series_annotations] = series_annotations(wraptuple(d[:series_annotations])...) + end + # convert into strokes and brushes if haskey(d, :arrow) @@ -841,6 +857,7 @@ function convertLegendValue(val::Symbol) end convertLegendValue(val::Bool) = val ? :best : :none convertLegendValue(val::Void) = :none +convertLegendValue{S<:Real, T<:Real}(v::Tuple{S,T}) = v convertLegendValue(v::AbstractArray) = map(convertLegendValue, v) # ----------------------------------------------------------------------------- @@ -1028,8 +1045,14 @@ end # ----------------------------------------------------------------------------- function _update_subplot_periphery(sp::Subplot, anns::AVec) - # extend annotations - sp.attr[:annotations] = vcat(anns, sp[:annotations]) + # extend annotations, and ensure we always have a (x,y,PlotText) tuple + newanns = vcat(anns, sp[:annotations]) + for (i,ann) in enumerate(newanns) + x,y,tmp = ann + ptxt = isa(tmp, PlotText) ? tmp : text(tmp) + newanns[i] = (x,y,ptxt) + end + sp.attr[:annotations] = newanns # handle legend/colorbar sp.attr[:legend] = convertLegendValue(sp.attr[:legend]) @@ -1093,7 +1116,7 @@ function _update_axis(axis::Axis, d_in::KW, letter::Symbol, subplot_index::Int) end # update the axis - update!(axis, args...; kw...) + attr!(axis, args...; kw...) return end diff --git a/src/axes.jl b/src/axes.jl index 0cd8ac4c..b9840e7a 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -29,7 +29,7 @@ function Axis(sp::Subplot, letter::Symbol, args...; kw...) d[:discrete_values] = [] # update the defaults - update!(Axis([sp], d), args...; kw...) + attr!(Axis([sp], d), args...; kw...) end function get_axis(sp::Subplot, letter::Symbol) @@ -83,7 +83,7 @@ function process_axis_arg!(d::KW, arg, letter = "") end # update an Axis object with magic args and keywords -function update!(axis::Axis, args...; kw...) +function attr!(axis::Axis, args...; kw...) # first process args d = axis.d for arg in args @@ -381,6 +381,9 @@ function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis) if amax <= amin && isfinite(amin) amax = amin + 1.0 end + if !isfinite(amin) && !isfinite(amax) + amin, amax = 0.0, 1.0 + end if should_widen widen(amin, amax) else diff --git a/src/backends.jl b/src/backends.jl index 7cd4e898..16fcad2c 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -51,6 +51,21 @@ _before_layout_calcs(plt::Plot) = nothing title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefont].pointsize * pt guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefont].pointsize * pt +"Returns the (width,height) of a text label." +function text_size(lablen::Int, sz::Number, rot::Number = 0) + # we need to compute the size of the ticks generically + # this means computing the bounding box and then getting the width/height + # note: + ptsz = sz * pt + width = 0.8lablen * ptsz + + # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles + height = abs(sind(rot)) * width + abs(cosd(rot)) * ptsz + width = abs(sind(rot+90)) * width + abs(cosd(rot+90)) * ptsz + width, height +end +text_size(lab::AbstractString, sz::Number, rot::Number = 0) = text_size(length(lab), sz, rot) + # account for the size/length/rotation of tick labels function tick_padding(axis::Axis) ticks = get_ticks(axis) @@ -58,19 +73,24 @@ function tick_padding(axis::Axis) 0mm else vals, labs = ticks - ptsz = axis[:tickfont].pointsize * pt - - # we need to compute the size of the ticks generically - # this means computing the bounding box and then getting the width/height + isempty(labs) && return 0mm + # ptsz = axis[:tickfont].pointsize * pt longest_label = maximum(length(lab) for lab in labs) - labelwidth = 0.8longest_label * ptsz # generalize by "rotating" y labels rot = axis[:rotation] + (axis[:letter] == :y ? 90 : 0) - # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles - hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm - hgt + # # we need to compute the size of the ticks generically + # # this means computing the bounding box and then getting the width/height + # labelwidth = 0.8longest_label * ptsz + # + # + # # now compute the generalized "height" after rotation as the "opposite+adjacent" of 2 triangles + # hgt = abs(sind(rot)) * labelwidth + abs(cosd(rot)) * ptsz + 1mm + # hgt + + # get the height of the rotated label + text_size(longest_label, axis[:tickfont].pointsize, rot)[2] end end diff --git a/src/backends/glvisualize.jl b/src/backends/glvisualize.jl index 75a28103..f1e40181 100644 --- a/src/backends/glvisualize.jl +++ b/src/backends/glvisualize.jl @@ -1,4 +1,4 @@ -#= +``#= TODO * move all gl_ methods to GLPlot * integrate GLPlot UI @@ -66,6 +66,7 @@ function _initialize_backend(::GLVisualizeBackend; kw...) import GLAbstraction: Style import GLVisualize: visualize import Plots.GL + import UnicodeFun Plots.slice_arg(img::Images.AbstractImage, idx::Int) = img is_marker_supported(::GLVisualizeBackend, shape::GLVisualize.AllPrimitives) = true is_marker_supported{Img<:Images.AbstractImage}(::GLVisualizeBackend, shape::Union{Vector{Img}, Img}) = true @@ -78,11 +79,11 @@ end function add_backend_string(b::GLVisualizeBackend) """ For those incredibly brave souls who assume full responsibility for what happens next... - There's an easy way to get what you need for the GLVisualize backend to work: + There's an easy way to get what you need for the GLVisualize backend to work (until Pkg3 is usable): Pkg.clone("https://github.com/tbreloff/MetaPkg.jl") - import MetaPkg - MetaPkg.checkout("MetaGL") + using MetaPkg + meta_checkout("MetaGL") See the MetaPkg readme for details... """ @@ -97,13 +98,34 @@ end # GLPlot.init() # end const _glplot_deletes = [] + +function close_child_signals!(screen) + for child in screen.children + for (k, s) in child.inputs + empty!(s.actions) + end + for (k, cam) in child.cameras + for f in fieldnames(cam) + s = getfield(cam, f) + if isa(s, Signal) + close(s, false) + end + end + end + empty!(child.cameras) + close_child_signals!(child) + end + return +end function empty_screen!(screen) if isempty(_glplot_deletes) - screen.renderlist = () - for c in screen.children - empty!(c) + close_child_signals!(screen) + empty!(screen) + empty!(screen.cameras) + for (k, s) in screen.inputs + empty!(s.actions) end - empty!(screen.children) + empty!(screen) else for del_signal in _glplot_deletes push!(del_signal, true) # trigger delete @@ -112,36 +134,63 @@ function empty_screen!(screen) end nothing end -function _create_backend_figure(plt::Plot{GLVisualizeBackend}) - # init a screen - if isempty(GLVisualize.get_screens()) - s = GLVisualize.glscreen() - Reactive.stop() - @async begin - while isopen(s) - tic() - GLWindow.pollevents() - if Base.n_avail(Reactive._messages) > 0 - Reactive.run_till_now() - GLWindow.render_frame(s) - GLWindow.swapbuffers(s) - end - yield() - diff = (1/60) - toq() - while diff >= 0.001 - tic() - sleep(0.001) # sleep for the minimal amount of time - diff -= toq() - end - end - GLWindow.destroy!(s) - GLVisualize.cleanup_old_screens() - end - else - s = GLVisualize.current_screen() - empty_screen!(s) +function poll_reactive() + # run_till_now blocks when message queue is empty! + Base.n_avail(Reactive._messages) > 0 && Reactive.run_till_now() +end + + +function get_plot_screen(list::Vector, name, result = []) + for elem in list + get_plot_screen(elem, name, result) end - s + return result +end +function get_plot_screen(screen, name, result = []) + if screen.name == name + push!(result, screen) + return result + end + get_plot_screen(screen.children, name, result) +end + +function create_window(plt::Plot{GLVisualizeBackend}, visible) + name = Symbol("Plots.jl") + # make sure we have any screen open + if isempty(GLVisualize.get_screens()) + # create a fresh, new screen + parent_screen = GLVisualize.glscreen( + "Plot", + resolution = plt[:size], + visible = visible + ) + @async GLWindow.waiting_renderloop(parent_screen) + end + # now lets get ourselves a permanent Plotting screen + plot_screens = get_plot_screen(GLVisualize.get_screens(), name) + screen = if isempty(plot_screens) # no screen with `name` + parent = GLVisualize.current_screen() + screen = GLWindow.Screen( + parent, area = map(GLWindow.zeroposition, parent.area), + name = name + ) + for (k, s) in screen.inputs # copy signals, so we can clean them up better + screen.inputs[k] = map(identity, s) + end + screen + elseif length(plot_screens) == 1 + plot_screens[1] + else + # okay this is silly! Lets see if we can. There is an ID we could use + # will not be fine for more than 255 screens though -.-. + error("multiple Plot screens. Please don't use any screen with the name Plots.jl") + end + # Since we own this window, we can do deep cleansing + empty_screen!(screen) + plt.o = screen + GLWindow.set_visibility!(screen, visible) + resize!(screen, plt[:size]...) + screen end # --------------------------------------------------------------------------- @@ -154,6 +203,8 @@ const _gl_marker_map = KW( :xcross => '❌', :utriangle => '▲', :dtriangle => '▼', + :ltriangle => '◀', + :rtriangle => '▶', :pentagon => '⬟', :octagon => '⯄', :star4 => '✦', @@ -163,26 +214,37 @@ const _gl_marker_map = KW( :hline => '━', :+ => '+', :x => 'x', + :circle => '●' ) -function gl_marker(shape, size) +function gl_marker(shape) shape end -function gl_marker(shape::Shape, size::FixedSizeArrays.Vec{2,Float32}) - points = Point2f0[Vec{2,Float32}(p)*10f0 for p in zip(shape.x, shape.y)] +function gl_marker(shape::Shape) + points = Point2f0[Vec{2,Float32}(p) for p in zip(shape.x, shape.y)] + bb = GeometryTypes.AABB(points) + mini, maxi = minimum(bb), maximum(bb) + w3 = maxi-mini + origin, width = Point2f0(mini[1], mini[2]), Point2f0(w3[1], w3[2]) + map!(p -> ((p - origin) ./ width) - 0.5f0, points) # normalize and center GeometryTypes.GLNormalMesh(points) end # create a marker/shape type -function gl_marker(shape::Symbol, msize) - isa(msize, Array) && (msize = first(msize)) # size doesn't really matter now +function gl_marker(shape::Vector{Symbol}) + String(map(shape) do sym + get(_gl_marker_map, sym, '●') + end) +end + +function gl_marker(shape::Symbol) if shape == :rect - GeometryTypes.HyperRectangle(Vec{2, Float32}(0), msize) + GeometryTypes.HyperRectangle(Vec2f0(0), Vec2f0(1)) elseif shape == :circle || shape == :none - GeometryTypes.HyperSphere(Point{2, Float32}(0), maximum(msize)) + GeometryTypes.HyperSphere(Point2f0(0), 1f0) elseif haskey(_gl_marker_map, shape) _gl_marker_map[shape] elseif haskey(_shapes, shape) - gl_marker(_shapes[shape], msize) + gl_marker(_shapes[shape]) else error("Shape $shape not supported by GLVisualize") end @@ -198,24 +260,32 @@ function extract_limits(sp, d, kw_args) nothing end +to_vec{T <: FixedVector}(::Type{T}, vec::T) = vec +to_vec{T <: FixedVector}(::Type{T}, s::Number) = T(s) + +to_vec{T <: FixedVector{2}}(::Type{T}, vec::FixedVector{3}) = T(vec[1], vec[2]) +to_vec{T <: FixedVector{3}}(::Type{T}, vec::FixedVector{2}) = T(vec[1], vec[2], 0) + +to_vec{T <: FixedVector}(::Type{T}, vecs::AbstractVector) = map(x-> to_vec(T, x), vecs) + function extract_marker(d, kw_args) dim = Plots.is3d(d) ? 3 : 2 scaling = dim == 3 ? 0.003 : 2 - if haskey(d, :markersize) - msize = d[:markersize] - if isa(msize, AbstractArray) - kw_args[:scale] = map(x->GeometryTypes.Vec{dim, Float32}(x*scaling), msize) - else - kw_args[:scale] = GeometryTypes.Vec{dim, Float32}(msize*scaling) - end - end if haskey(d, :markershape) shape = d[:markershape] - shape = gl_marker(shape, kw_args[:scale]) + shape = gl_marker(shape) if shape != :none kw_args[:primitive] = shape end end + dim = isa(kw_args[:primitive], GLVisualize.Sprites) ? 2 : 3 + if haskey(d, :markersize) + msize = d[:markersize] + kw_args[:scale] = to_vec(GeometryTypes.Vec{dim, Float32}, msize .* scaling) + end + if haskey(d, :offset) + kw_args[:offset] = d[:offset] + end # get the color key = :markercolor haskey(d, key) || return @@ -246,6 +316,7 @@ end function _extract_surface(d::AbstractArray) d end + # TODO when to transpose?? function extract_surface(d) map(_extract_surface, (d[:x], d[:y], d[:z])) @@ -258,7 +329,7 @@ function extract_points(d) array = (d[:x], d[:y], d[:z])[1:dim] topoints(Point{dim, Float32}, array) end -function make_gradient{C<:Colorant}(grad::Vector{C}) +function make_gradient{C <: Colorant}(grad::Vector{C}) grad end function make_gradient(grad::ColorGradient) @@ -304,9 +375,10 @@ end function extract_stroke(d, kw_args) extract_c(d, kw_args, :line) if haskey(d, :linewidth) - kw_args[:thickness] = d[:linewidth]*3 + kw_args[:thickness] = d[:linewidth] * 3 end end + function extract_color(d, sym) d[Symbol("$(sym)color")] end @@ -344,6 +416,7 @@ end dist(a, b) = abs(a-b) mindist(x, a, b) = min(dist(a, x), dist(b, x)) + function gappy(x, ps) n = length(ps) x <= first(ps) && return first(ps) - x @@ -357,7 +430,7 @@ function gappy(x, ps) return last(ps) - x end function ticks(points, resolution) - Float16[gappy(x, points) for x=linspace(first(points),last(points), resolution)] + Float16[gappy(x, points) for x = linspace(first(points),last(points), resolution)] end @@ -392,9 +465,11 @@ function extract_linestyle(d, kw_args) extract_c(d, kw_args, :line) nothing end + function hover(to_hover::Vector, to_display, window) hover(to_hover[], to_display, window) end + function get_cam(x) if isa(x, GLAbstraction.Context) return get_cam(x.children) @@ -405,6 +480,7 @@ function get_cam(x) end end + function hover(to_hover, to_display, window) if isa(to_hover, GLAbstraction.Context) return hover(to_hover.children, to_display, window) @@ -412,23 +488,20 @@ function hover(to_hover, to_display, window) area = map(window.inputs[:mouseposition]) do mp SimpleRectangle{Int}(round(Int, mp+10)..., 100, 70) end - background = visualize((GLVisualize.ROUNDED_RECTANGLE, Point2f0[0]), - color=RGBA{Float32}(0,0,0,0), scale=Vec2f0(100, 70), offset=Vec2f0(0), - stroke_color=RGBA{Float32}(0,0,0,0.4), - stroke_width=-1.0f0 - ) mh = GLWindow.mouse2id(window) - popup = GLWindow.Screen(window, area=area, hidden=true) + popup = GLWindow.Screen( + window, + hidden = map(mh-> !(mh.id == to_hover.id), mh), + area = area, + stroke = (2f0, RGBA(0f0, 0f0, 0f0, 0.8f0)) + ) cam = get!(popup.cameras, :perspective) do GLAbstraction.PerspectiveCamera( popup.inputs, Vec3f0(3), Vec3f0(0), - keep=Signal(false), - theta= Signal(Vec3f0(0)), trans= Signal(Vec3f0(0)) + keep = Signal(false), + theta = Signal(Vec3f0(0)), trans = Signal(Vec3f0(0)) ) end - Reactive.preserve(map(mh) do mh - popup.hidden = !(mh.id == to_hover.id) - end) map(enumerate(to_display)) do id i,d = id @@ -443,13 +516,12 @@ function hover(to_hover, to_display, window) else cam.projectiontype.value = GLVisualize.ORTHOGRAPHIC end - GLVisualize._view(robj, popup, camera=cam) - GLVisualize._view(background, popup, camera=:fixed_pixel) + GLVisualize._view(robj, popup, camera = cam) bb = GLAbstraction.boundingbox(robj).value mini = minimum(bb) w = GeometryTypes.widths(bb) - wborder = w*0.08f0 #8 percent border - bb = GeometryTypes.AABB{Float32}(mini-wborder, w+2f0*wborder) + wborder = w * 0.08f0 #8 percent border + bb = GeometryTypes.AABB{Float32}(mini - wborder, w + 2 * wborder) GLAbstraction.center!(cam, bb) end end) @@ -458,14 +530,14 @@ function hover(to_hover, to_display, window) end function extract_extrema(d, kw_args) - xmin,xmax = extrema(d[:x]); ymin,ymax = extrema(d[:y]) + xmin, xmax = extrema(d[:x]); ymin, ymax = extrema(d[:y]) kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(xmin, ymin, xmax-xmin, ymax-ymin) nothing end function extract_font(font, kw_args) kw_args[:family] = font.family - kw_args[:relative_scale] = font.pointsize*1.5 ./ GLVisualize.glyph_scale!('X') + kw_args[:relative_scale] = pointsize(font) kw_args[:color] = gl_color(font.color) end @@ -489,6 +561,7 @@ function extract_colornorm(d, kw_args) kw_args[:intensity] = map(Float32, collect(z)) end end + function extract_gradient(d, kw_args, sym) key = Symbol("$(sym)color") haskey(d, key) || return @@ -498,6 +571,7 @@ function extract_gradient(d, kw_args, sym) kw_args[:color_map] = c return end + function extract_c(d, kw_args, sym) key = Symbol("$(sym)color") haskey(d, key) || return @@ -505,7 +579,11 @@ function extract_c(d, kw_args, sym) kw_args[:color] = nothing kw_args[:color_map] = nothing kw_args[:color_norm] = nothing - if isa(c, AbstractVector) + if ( + isa(c, AbstractVector) && + ((haskey(d, :marker_z) && d[:marker_z] != nothing) || + (haskey(d, :line_z) && d[:line_z] != nothing)) + ) extract_colornorm(d, kw_args) kw_args[:color_map] = c else @@ -545,7 +623,7 @@ function draw_grid_lines(sp, grid_segs, thickness, style, model, color) end function align_offset(startpos, lastpos, atlas, rscale, font, align) - xscale, yscale = GLVisualize.glyph_scale!('X').*rscale + xscale, yscale = GLVisualize.glyph_scale!('X', rscale) xmove = (lastpos-startpos)[1]+xscale if align == :top return -Vec2f0(xmove/2f0, yscale) @@ -555,60 +633,81 @@ function align_offset(startpos, lastpos, atlas, rscale, font, align) error("Align $align not known") end end + function align_offset(startpos, lastpos, atlas, rscale, font, align::Vec) - xscale, yscale = GLVisualize.glyph_scale!('X').*rscale - xmove = (lastpos-startpos)[1]+xscale + xscale, yscale = GLVisualize.glyph_scale!('X', rscale) + xmove = (lastpos-startpos)[1] + xscale return -Vec2f0(xmove, yscale) .* align end + function alignment2num(x::Symbol) (x in (:hcenter, :vcenter)) && return 0.5 (x in (:left, :bottom)) && return 0.0 (x in (:right, :top)) && return 1.0 0.0 # 0 default, or better to error? end + function alignment2num(font::Plots.Font) Vec2f0(map(alignment2num, (font.halign, font.valign))) end -function draw_ticks(axis, ticks, align, move, isx, lims, model, text = "", positions = Point2f0[], offsets=Vec2f0[]) - sz = axis[:tickfont].pointsize - rscale2 = Vec2f0(3/sz) - m = Reactive.value(model) - xs, ys = m[1,1], m[2,2] - rscale = rscale2 ./ Vec2f0(xs, ys) +pointsize(font) = font.pointsize * 2 + +function draw_ticks( + axis, ticks, isx, lims, m, text = "", + positions = Point2f0[], offsets=Vec2f0[] + ) + sz = pointsize(axis[:tickfont]) atlas = GLVisualize.get_texture_atlas() - font = GLVisualize.DEFAULT_FONT_FACE - if !(ticks in (nothing, false)) - # x labels - flip = axis[:flip] - for (cv, dv) in zip(ticks...) - x,y = cv, (flip ? lims[2] : lims[1]) - startpos = Point2f0(isx ? (x,y) : (y,x))-move - # @show cv dv ymin xi yi - str = string(dv) - position = GLVisualize.calc_position(str, startpos, rscale, font, atlas) - offset = GLVisualize.calc_offset(str, rscale2, font, atlas) - alignoff = align_offset(startpos, last(position), atlas, rscale, font, align) - map!(position) do pos - pos .+ alignoff - end - append!(positions, position) - append!(offsets, offset) - text *= str + font = GLVisualize.defaultfont() + + flip = axis[:flip]; mirror = axis[:mirror] + + align = if isx + mirror ? :bottom : :top + else + mirror ? :left : :right + end + axis_gap = Point2f0(isx ? 0 : sz / 2, isx ? sz / 2 : 0) + for (cv, dv) in zip(ticks...) + + x, y = cv, lims[1] + xy = isx ? (x, y) : (y, x) + _pos = m * GeometryTypes.Vec4f0(xy[1], xy[2], 0, 1) + startpos = Point2f0(_pos[1], _pos[2]) - axis_gap + str = string(dv) + # need to tag a new UnicodeFun version for this... also the numbers become + # so small that it looks terrible -.- + # _str = split(string(dv), "^") + # if length(_str) == 2 + # _str[2] = UnicodeFun.to_superscript(_str[2]) + # end + # str = join(_str, "") + position = GLVisualize.calc_position(str, startpos, sz, font, atlas) + offset = GLVisualize.calc_offset(str, sz, font, atlas) + alignoff = align_offset(startpos, last(position), atlas, sz, font, align) + map!(position) do pos + pos .+ alignoff end + append!(positions, position) + append!(offsets, offset) + text *= str + end text, positions, offsets end + function text(position, text, kw_args) text_align = alignment2num(text.font) startpos = Vec2f0(position) atlas = GLVisualize.get_texture_atlas() - font = GLVisualize.DEFAULT_FONT_FACE + font = GLVisualize.defaultfont() rscale = kw_args[:relative_scale] - m = Reactive.value(kw_args[:model]) + position = GLVisualize.calc_position(text.str, startpos, rscale, font, atlas) offset = GLVisualize.calc_offset(text.str, rscale, font, atlas) alignoff = align_offset(startpos, last(position), atlas, rscale, font, text_align) + map!(position) do pos pos .+ alignoff end @@ -646,38 +745,42 @@ function gl_draw_axes_2d(sp::Plots.Subplot{Plots.GLVisualizeBackend}, model, are xlim = Plots.axis_limits(xaxis) ylim = Plots.axis_limits(yaxis) - m = Reactive.value(model) - xs, ys = m[1,1], m[2,2] - # TODO: we should make sure we actually need to draw these... - t, positions, offsets = draw_ticks(xaxis, xticks, :top, Point2f0(0, 7/ys), true, ylim, model) - t, positions, offsets = draw_ticks(yaxis, yticks, :right, Point2f0(7/xs, 0), false, xlim, model, t, positions, offsets) - sz = xaxis[:tickfont].pointsize - kw_args = Dict{Symbol, Any}( - :position => positions, - :offset => offsets, - :color => fcolor, - :relative_scale => Vec2f0(3/sz), - :model => model, - :scale_primitive => false - ) - if !(xaxis[:ticks] in (nothing,false,:none)) - push!(axis_vis, visualize(t, Style(:default), kw_args)) + + if !(xaxis[:ticks] in (nothing, false, :none)) + ticklabels = map(model) do m + mirror = xaxis[:mirror] + t, positions, offsets = draw_ticks(xaxis, xticks, true, ylim, m) + mirror = xaxis[:mirror] + t, positions, offsets = draw_ticks( + yaxis, yticks, false, xlim, m, + t, positions, offsets + ) + end + kw_args = Dict{Symbol, Any}( + :position => map(x-> x[2], ticklabels), + :offset => map(last, ticklabels), + :color => fcolor, + :relative_scale => pointsize(xaxis[:tickfont]), + :scale_primitive => false + ) + push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args)) end + area_w = GeometryTypes.widths(area) if sp[:title] != "" tf = sp[:titlefont]; color = gl_color(sp[:foreground_color_title]) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, tf.rotation, color) - xy = Point2f0(area.w/2, area_w[2]) - kw = Dict(:model => text_model(font, xy), :scale_primitive=>true) + xy = Point2f0(area.w/2, area_w[2] + pointsize(tf)/2) + kw = Dict(:model => text_model(font, xy), :scale_primitive => true) extract_font(font, kw) t = PlotText(sp[:title], font) push!(axis_vis, text(xy, t, kw)) end if xaxis[:guide] != "" tf = xaxis[:guidefont]; color = gl_color(xaxis[:foreground_color_guide]) - xy = Point2f0(area.w/2, 0) + xy = Point2f0(area.w/2, - pointsize(tf)/2) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :bottom, tf.rotation, color) - kw = Dict(:model => text_model(font, xy), :scale_primitive=>true) + kw = Dict(:model => text_model(font, xy), :scale_primitive => true) t = PlotText(xaxis[:guide], font) extract_font(font, kw) push!(axis_vis, text(xy, t, kw)) @@ -686,7 +789,7 @@ function gl_draw_axes_2d(sp::Plots.Subplot{Plots.GLVisualizeBackend}, model, are if yaxis[:guide] != "" tf = yaxis[:guidefont]; color = gl_color(yaxis[:foreground_color_guide]) font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, 90f0, color) - xy = Point2f0(0, area.h/2) + xy = Point2f0(-pointsize(tf)/2, area.h/2) kw = Dict(:model => text_model(font, xy), :scale_primitive=>true) t = PlotText(yaxis[:guide], font) extract_font(font, kw) @@ -854,8 +957,8 @@ function gl_viewport(bb, rect) l, b, bw, bh = bb rw, rh = rect.w, rect.h GLVisualize.SimpleRectangle( - round(Int, rect.x + rw * l), - round(Int, rect.y + rh * b), + round(Int, rw * l), + round(Int, rh * b), round(Int, rw * bw), round(Int, rh * bh) ) @@ -881,71 +984,99 @@ end # ---------------------------------------------------------------- -function _display(plt::Plot{GLVisualizeBackend}) - screen = plt.o - empty_screen!(screen) + +function scale_for_annotations!(series::Series, scaletype::Symbol = :pixels) + anns = series[:series_annotations] + if anns != nothing && !isnull(anns.baseshape) + # we use baseshape to overwrite the markershape attribute + # with a list of custom shapes for each + msw, msh = anns.scalefactor + offsets = Array(Vec2f0, length(anns.strs)) + series[:markersize] = map(1:length(anns.strs)) do i + str = cycle(anns.strs, i) + # get the width and height of the string (in mm) + sw, sh = text_size(str, anns.font.pointsize) + + # how much to scale the base shape? + # note: it's a rough assumption that the shape fills the unit box [-1,-1,1,1], + # so we scale the length-2 shape by 1/2 the total length + xscale = 0.5to_pixels(sw) * 1.8 + yscale = 0.5to_pixels(sh) * 1.8 + + # we save the size of the larger direction to the markersize list, + # and then re-scale a copy of baseshape to match the w/h ratio + s = Vec2f0(xscale, yscale) + offsets[i] = -s + s + end + series[:offset] = offsets + end + return +end + + + + +function _display(plt::Plot{GLVisualizeBackend}, visible = true) + screen = create_window(plt, visible) sw, sh = plt[:size] sw, sh = sw*px, sh*px - # TODO: use plt.subplots... plt.spmap can't be trusted - for (name, sp) in plt.spmap + + for sp in plt.subplots _3d = Plots.is3d(sp) # camera = :perspective # initialize the sub-screen for this subplot - # note: we create a lift function to update the size on resize - rel_bbox = Plots.bbox_to_pcts(bbox(sp), sw, sh) sub_area = map(screen.area) do rect Plots.gl_viewport(rel_bbox, rect) end c = plt[:background_color_outside] sp_screen = GLVisualize.Screen( - screen, name = name, color = c, + screen, color = c, area = sub_area ) + sp.o = sp_screen cam = get!(sp_screen.cameras, :perspective) do inside = sp_screen.inputs[:mouseinside] theta = _3d ? nothing : Signal(Vec3f0(0)) # surpress rotation for 2D (nothing will get usual rotation controle) GLAbstraction.PerspectiveCamera( sp_screen.inputs, Vec3f0(3), Vec3f0(0), - keep=inside, theta=theta + keep = inside, theta = theta ) end - sp.o = sp_screen rel_plotarea = Plots.bbox_to_pcts(plotarea(sp), sw, sh) - model_m = map(Plots.to_modelmatrix, screen.area, sub_area, Signal(rel_plotarea), Signal(sp)) - for ann in sp[:annotations] - x, y, plot_text = ann - txt_args = Dict{Symbol, Any}(:model => eye(GeometryTypes.Mat4f0)) - x, y, _1, _1 = Reactive.value(model_m) * Vec{4,Float32}(x, y, 0, 1) - extract_font(plot_text.font, txt_args) - t = text(Point2f0(x, y), plot_text, txt_args) - GLVisualize._view(t, sp_screen, camera=cam) - end + model_m = map(Plots.to_modelmatrix, + screen.area, sub_area, + Signal(rel_plotarea), Signal(sp) + ) + # loop over the series and add them to the subplot if !_3d axis = gl_draw_axes_2d(sp, model_m, Reactive.value(sub_area)) - GLVisualize._view(axis, sp_screen, camera=cam) + GLVisualize._view(axis, sp_screen, camera=:perspective) cam.projectiontype.value = GLVisualize.ORTHOGRAPHIC Reactive.run_till_now() # make sure Reactive.push! arrives GLAbstraction.center!(cam, GeometryTypes.AABB( - Vec3f0(-10), Vec3f0((GeometryTypes.widths(sp_screen)+20f0)..., 1) + Vec3f0(-20), Vec3f0((GeometryTypes.widths(sp_screen)+40f0)..., 1) ) ) else axis = gl_draw_axes_3d(sp, model_m) - GLVisualize._view(axis, sp_screen, camera=cam) + GLVisualize._view(axis, sp_screen, camera=:perspective) push!(cam.projectiontype, GLVisualize.PERSPECTIVE) end - for series in Plots.series_list(sp) + for series in Plots.series_list(sp) + d = series.d st = d[:seriestype]; kw_args = KW() # exctract kw + kw_args[:model] = model_m # add transformation if !_3d # 3D is treated differently, since we need boundingboxes for camera kw_args[:boundingbox] = nothing # don't calculate bb, we dont need it end - + scale_for_annotations!(series) if st in (:surface, :wireframe) x, y, z = extract_surface(d) extract_gradient(d, kw_args, :fill) @@ -974,7 +1105,7 @@ function _display(plt::Plot{GLVisualizeBackend}) if d[:fillrange] != nothing kw = copy(kw_args) fr = d[:fillrange] - ps = if all(x->x>=0, diff(d[:x])) # if is monotonic + ps = if all(x-> x >= 0, diff(d[:x])) # if is monotonic vcat(points, Point2f0[(points[i][1], cycle(fr, i)) for i=length(points):-1:1]) else points @@ -1026,37 +1157,47 @@ function _display(plt::Plot{GLVisualizeBackend}) else error("failed to display plot type $st") end - if isa(vis, Array) && isempty(vis) - continue # nothing to see here - end - GLVisualize._view(vis, sp_screen, camera=cam) + + isa(vis, Array) && isempty(vis) && continue # nothing to see here + + GLVisualize._view(vis, sp_screen, camera=:perspective) if haskey(d, :hover) && !(d[:hover] in (false, :none, nothing)) hover(vis, d[:hover], sp_screen) end if isdefined(:GLPlot) && isdefined(Main.GLPlot, :(register_plot!)) - del_signal = Main.GLPlot.register_plot!(vis, sp_screen) + del_signal = Main.GLPlot.register_plot!(vis, sp_screen, create_gizmo=false) append!(_glplot_deletes, del_signal) end + anns = series[:series_annotations] + for (x, y, str, font) in EachAnn(anns, d[:x], d[:y]) + txt_args = Dict{Symbol, Any}(:model => eye(GLAbstraction.Mat4f0)) + x, y = Reactive.value(model_m) * Vec{4, Float32}(x, y, 0, 1) + extract_font(font, txt_args) + t = text(Point2f0(x, y), PlotText(str, font), txt_args) + GLVisualize._view(t, sp_screen, camera = :perspective) + end + end + generate_legend(sp, sp_screen, model_m) if _3d GLAbstraction.center!(sp_screen) end + Reactive.post_empty() + yield() end - Reactive.post_empty() - yield() end function _show(io::IO, ::MIME"image/png", plt::Plot{GLVisualizeBackend}) - _display(plt) - GLWindow.pollevents() - yield() + _display(plt, false) + GLWindow.poll_glfw() if Base.n_avail(Reactive._messages) > 0 Reactive.run_till_now() end - GLWindow.render_frame(plt.o) + yield() + GLWindow.render_frame(GLWindow.rootscreen(plt.o)) GLWindow.swapbuffers(plt.o) buff = GLWindow.screenbuffer(plt.o) - png = Images.Image(buff, + png = Images.Image(map(RGB{U8}, buff), colorspace = "sRGB", spatialorder = ["y", "x"] ) @@ -1106,6 +1247,7 @@ function gl_lines(points, kw_args) end return result end + function gl_shape(d, kw_args) points = Plots.extract_points(d) result = [] @@ -1116,29 +1258,42 @@ function gl_shape(d, kw_args) end result end -tovec2(x::FixedSizeArrays.Vec{2, Float32}) = x -tovec2(x::AbstractVector) = map(tovec2, x) -tovec2(x::FixedSizeArrays.Vec) = Vec2f0(x[1], x[2]) + function gl_scatter(points, kw_args) prim = get(kw_args, :primitive, GeometryTypes.Circle) if isa(prim, GLNormalMesh) - kw_args[:scale] = map(kw_args[:model]) do m - s = m[1,1], m[2,2], m[3,3] - 1f0./Vec3f0(s) + if haskey(kw_args, :model) + p = get(kw_args, :perspective, eye(GeometryTypes.Mat4f0)) + kw_args[:scale] = GLAbstraction.const_lift(kw_args[:model], kw_args[:scale], p) do m, sc, p + s = Vec3f0(m[1,1], m[2,2], m[3,3]) + ps = Vec3f0(p[1,1], p[2,2], p[3,3]) + r = sc ./ (s .* ps) + r + end end else # 2D prim - kw_args[:scale] = tovec2(kw_args[:scale]) + kw_args[:scale] = to_vec(Vec2f0, kw_args[:scale]) end + if haskey(kw_args, :stroke_width) s = Reactive.value(kw_args[:scale]) sw = kw_args[:stroke_width] if sw*5 > cycle(Reactive.value(s), 1)[1] # restrict marker stroke to 1/10th of scale (and handle arrays of scales) - kw_args[:stroke_width] = s[1]/5f0 + kw_args[:stroke_width] = s[1] / 5f0 end end kw_args[:scale_primitive] = false + if isa(prim, String) + kw_args[:position] = points + if !isa(kw_args[:scale], Vector) # if not vector, we can assume it's relative scale + kw_args[:relative_scale] = kw_args[:scale] + delete!(kw_args, :scale) + end + return visualize(prim, Style(:default), kw_args) + end + visualize((prim, points), Style(:default), kw_args) end @@ -1158,6 +1313,9 @@ function gl_poly(points, kw_args) result end + + + function gl_surface(x,y,z, kw_args) if isa(x, Range) && isa(y, Range) main = z @@ -1166,8 +1324,8 @@ function gl_surface(x,y,z, kw_args) if isa(x, AbstractMatrix) && isa(y, AbstractMatrix) main = map(s->map(Float32, s), (x, y, z)) elseif isa(x, AbstractVector) || isa(y, AbstractVector) - x = Float32[x[i] for i=1:size(z,1), j=1:size(z,2)] - y = Float32[y[j] for i=1:size(z,1), j=1:size(z,2)] + x = Float32[x[i] for i = 1:size(z,1), j = 1:size(z,2)] + y = Float32[y[j] for i = 1:size(z,1), j = 1:size(z,2)] main = (x, y, map(Float32, z)) else error("surface: combination of types not supported: $(typeof(x)) $(typeof(y)) $(typeof(z))") @@ -1177,8 +1335,10 @@ function gl_surface(x,y,z, kw_args) faces = Cuint[] idx = (i,j) -> sub2ind(size(z), i, j) - 1 for i=1:size(z,1), j=1:size(z,2) + i < size(z,1) && push!(faces, idx(i, j), idx(i+1, j)) j < size(z,2) && push!(faces, idx(i, j), idx(i, j+1)) + end color = get(kw_args, :stroke_color, RGBA{Float32}(0,0,0,1)) kw_args[:color] = color @@ -1194,15 +1354,18 @@ function gl_surface(x,y,z, kw_args) end -function gl_contour(x,y,z, kw_args) +function gl_contour(x, y, z, kw_args) if kw_args[:fillrange] != nothing + delete!(kw_args, :intensity) I = GLVisualize.Intensity{1, Float32} main = I[z[j,i] for i=1:size(z, 2), j=1:size(z, 1)] return visualize(main, Style(:default), kw_args) + else h = kw_args[:levels] - levels = Contour.contours(x, y, z, h) + T = eltype(z) + levels = Contour.contours(map(T, x), map(T, y), z, h) result = Point2f0[] zmin, zmax = get(kw_args, :limits, Vec2f0(extrema(z))) cmap = get(kw_args, :color_map, get(kw_args, :color, RGBA{Float32}(0,0,0,1))) @@ -1212,12 +1375,13 @@ function gl_contour(x,y,z, kw_args) append!(result, elem.vertices) push!(result, Point2f0(NaN32)) col = GLVisualize.color_lookup(cmap, c.level, zmin, zmax) - append!(colors, fill(col, length(elem.vertices)+1)) + append!(colors, fill(col, length(elem.vertices) + 1)) end end kw_args[:color] = colors kw_args[:color_map] = nothing kw_args[:color_norm] = nothing + kw_args[:intensity] = nothing return visualize(result, Style(:lines),kw_args) end end @@ -1238,20 +1402,120 @@ end -function text_plot(text, alignment, kw_args) - transmat = kw_args[:model] - obj = visualize(text, Style(:default), kw_args) - bb = value(GLAbstraction.boundingbox(obj)) - w,h,_ = widths(bb) - x,y,_ = minimum(bb) - pivot = origin(alignment) - pos = pivot - (Point2f0(x, y) .* widths(alignment)) - if kw_args[:rotation] != 0.0 - rot = GLAbstraction.rotationmatrix_z(Float32(font.rotation)) - transmat *= translationmatrix(pivot)*rot*translationmatrix(-pivot) +""" +Ugh, so much special casing (╯°□°)╯︵ ┻━┻ +""" +function label_scatter(d, w, ho) + kw = KW() + extract_stroke(d, kw) + extract_marker(d, kw) + kw[:scale] = Vec2f0(w/2) + kw[:offset] = Vec2f0(-w/4) + if haskey(kw, :intensity) + cmap = kw[:color_map] + norm = kw[:color_norm] + kw[:color] = GLVisualize.color_lookup(cmap, kw[:intensity][1], norm) + delete!(kw, :intensity) + delete!(kw, :color_map) + delete!(kw, :color_norm) + else + color = get(kw, :color, nothing) + kw[:color] = isa(color, Array) ? first(color) : color end - - transmat *= GLAbstraction.translationmatrix(Vec3f0(pos..., 0)) - GLAbstraction.transformation(obj, transmat) - view(obj, img.screen, camera=:orthographic_pixel) + p = get(kw, :primitive, GeometryTypes.Circle) + if isa(p, GLNormalMesh) + bb = GeometryTypes.AABB{Float32}(GeometryTypes.vertices(p)) + bbw = GeometryTypes.widths(bb) + if isapprox(bbw[3], 0) + bbw = Vec3f0(bbw[1], bbw[2], 1) + end + mini = minimum(bb) + m = GLAbstraction.translationmatrix(-mini) + m *= GLAbstraction.scalematrix(1 ./ bbw) + kw[:primitive] = m * p + kw[:scale] = Vec3f0(w/2) + delete!(kw, :offset) + end + GL.gl_scatter(Point2f0[(w/2, ho)], kw) +end + + +function make_label(sp, series, i) + GL = Plots + w, gap, ho = 20f0, 5f0, 5 + result = [] + d = series.d + st = d[:seriestype] + kw_args = KW() + if (st in (:path, :path3d)) && d[:linewidth] > 0 + points = Point2f0[(0, ho), (w, ho)] + kw = KW() + extract_linestyle(d, kw) + append!(result, GL.gl_lines(points, kw)) + if d[:markershape] != :none + push!(result, label_scatter(d, w, ho)) + end + elseif st in (:scatter, :scatter3d) #|| d[:markershape] != :none + push!(result, label_scatter(d, w, ho)) + else + extract_c(d, kw_args, :fill) + if isa(kw_args[:color], AbstractVector) + kw_args[:color] = first(kw_args[:color]) + end + push!(result, visualize( + GeometryTypes.SimpleRectangle(-w/2, ho-w/4, w/2, w/2), + Style(:default), kw_args + )) + end + labeltext = if isa(series[:label], Array) + i += 1 + series[:label][i] + else + series[:label] + end + color = sp[:foreground_color_legend] + ft = sp[:legendfont] + font = Plots.Font(ft.family, ft.pointsize, :left, :bottom, 0.0, color) + xy = Point2f0(w+gap, 0.0) + kw = Dict(:model => text_model(font, xy), :scale_primitive=>false) + extract_font(font, kw) + t = PlotText(labeltext, font) + push!(result, text(xy, t, kw)) + GLAbstraction.Context(result...), i +end + + +function generate_legend(sp, screen, model_m) + legend = GLAbstraction.Context[] + if sp[:legend] != :none + i = 0 + for series in series_list(sp) + should_add_to_legend(series) || continue + result, i = make_label(sp, series, i) + push!(legend, result) + end + if isempty(legend) + return + end + list = visualize(legend, gap=Vec3f0(0,5,0)) + bb = GLAbstraction._boundingbox(list) + wx,wy,_ = GeometryTypes.widths(bb) + xmin, _ = Plots.axis_limits(sp[:xaxis]) + _, ymax = Plots.axis_limits(sp[:yaxis]) + area = map(model_m) do m + p = m * GeometryTypes.Vec4f0(xmin, ymax, 0, 1) + h = round(Int, wy)+20 + w = round(Int, wx)+20 + x,y = round(Int, p[1])+30, round(Int, p[2]-h)-30 + GeometryTypes.SimpleRectangle(x, y, w, h) + end + sscren = GLWindow.Screen( + screen, area = area, + color = sp[:background_color_legend], + stroke = (2f0, RGBA(0.3, 0.3, 0.3, 0.9)) + ) + GLAbstraction.translate!(list, Vec3f0(10,10,0)) + GLVisualize._view(list, sscren, camera=:fixed_pixel) + end + return end diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 090aa499..5de4957a 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -30,6 +30,7 @@ const _gr_attr = merge_with_base_supported([ :normalize, :weights, :inset_subplots, :bar_width, + :arrow, ]) const _gr_seriestype = [ :path, :scatter, @@ -131,7 +132,7 @@ gr_set_textcolor(c) = GR.settextcolorind(gr_getcolorind(cycle(c,1))) # draw line segments, splitting x/y into contiguous/finite segments # note: this can be used for shapes by passing func `GR.fillarea` -function gr_polyline(x, y, func = GR.polyline) +function gr_polyline(x, y, func = GR.polyline; arrowside=:none) iend = 0 n = length(x) while iend < n-1 @@ -159,6 +160,12 @@ function gr_polyline(x, y, func = GR.polyline) # if we found a start and end, draw the line segment, otherwise we're done if istart > 0 && iend > 0 func(x[istart:iend], y[istart:iend]) + if arrowside in (:head,:both) + GR.drawarrow(x[iend-1], y[iend-1], x[iend], y[iend]) + end + if arrowside in (:tail,:both) + GR.drawarrow(x[istart+1], y[istart+1], x[istart], y[istart]) + end else break end @@ -263,11 +270,13 @@ end # draw ONE Shape function gr_draw_marker(xi, yi, msize, shape::Shape) - sx, sy = shape_coords(shape) + sx, sy = coords(shape) + # convert to ndc coords (percentages of window) GR.selntran(0) xi, yi = GR.wctondc(xi, yi) - GR.fillarea(xi + sx * 0.0015msize, - yi + sy * 0.0015msize) + ms_ndc_x, ms_ndc_y = gr_pixels_to_ndc(msize, msize) + GR.fillarea(xi .+ sx .* ms_ndc_x, + yi .+ sy .* ms_ndc_y) GR.selntran(1) end @@ -281,10 +290,11 @@ end # draw the markers, one at a time function gr_draw_markers(series::Series, x, y, msize, mz) - shape = series[:markershape] - if shape != :none + shapes = series[:markershape] + if shapes != :none for i=1:length(x) msi = cycle(msize, i) + shape = cycle(shapes, i) cfunc = isa(shape, Shape) ? gr_set_fillcolor : gr_set_markercolor cfuncind = isa(shape, Shape) ? GR.setfillcolorind : GR.setmarkercolorind @@ -349,6 +359,14 @@ function gr_set_font(f::Font; halign = f.halign, valign = f.valign, GR.settextalign(gr_halign[halign], gr_valign[valign]) end +function gr_nans_to_infs!(z) + for (i,zi) in enumerate(z) + if zi == NaN + z[i] = Inf + end + end +end + # -------------------------------------------------------------------------------------- # viewport plot area @@ -356,6 +374,9 @@ end # values are [xmin, xmax, ymin, ymax]. they range [0,1]. const viewport_plotarea = zeros(4) +# the size of the current plot in pixels +const gr_plot_size = zeros(2) + function gr_viewport_from_bbox(bb::BoundingBox, w, h, viewport_canvas) viewport = zeros(4) viewport[1] = viewport_canvas[2] * (left(bb) / w) @@ -410,6 +431,13 @@ gr_view_ycenter() = 0.5 * (viewport_plotarea[3] + viewport_plotarea[4]) gr_view_xdiff() = viewport_plotarea[2] - viewport_plotarea[1] gr_view_ydiff() = viewport_plotarea[4] - viewport_plotarea[3] +function gr_pixels_to_ndc(x_pixels, y_pixels) + w,h = gr_plot_size + totx = w * gr_view_xdiff() + toty = h * gr_view_ydiff() + x_pixels / totx, y_pixels / toty +end + # -------------------------------------------------------------------------------------- @@ -437,6 +465,7 @@ function gr_display(plt::Plot) # compute the viewport_canvas, normalized to the larger dimension viewport_canvas = Float64[0,1,0,1] w, h = plt[:size] + gr_plot_size[:] = [w, h] if w > h ratio = float(h) / w msize = display_width_ratio * w @@ -534,16 +563,16 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # set the scale flags and window xmin, xmax, ymin, ymax = data_lims - scale = 0 + scaleop = 0 xtick, ytick = 1, 1 if xmax > xmin && ymax > ymin # NOTE: for log axes, the major_x and major_y - if non-zero (omit labels) - control the minor grid lines (1 = draw 9 minor grid lines, 2 = no minor grid lines) # NOTE: for log axes, the x_tick and y_tick - if non-zero (omit axes) - only affect the output appearance (1 = nomal, 2 = scientiic notation) - xaxis[:scale] == :log10 && (scale |= GR.OPTION_X_LOG) - yaxis[:scale] == :log10 && (scale |= GR.OPTION_Y_LOG) - xaxis[:flip] && (scale |= GR.OPTION_FLIP_X) - yaxis[:flip] && (scale |= GR.OPTION_FLIP_Y) - if scale & GR.OPTION_X_LOG == 0 + xaxis[:scale] == :log10 && (scaleop |= GR.OPTION_X_LOG) + yaxis[:scale] == :log10 && (scaleop |= GR.OPTION_Y_LOG) + xaxis[:flip] && (scaleop |= GR.OPTION_FLIP_X) + yaxis[:flip] && (scaleop |= GR.OPTION_FLIP_Y) + if scaleop & GR.OPTION_X_LOG == 0 majorx = 1 #5 xtick = GR.tick(xmin, xmax) / majorx else @@ -551,7 +580,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) xtick = 2 # scientific notation majorx = 2 # no minor grid lines end - if scale & GR.OPTION_Y_LOG == 0 + if scaleop & GR.OPTION_Y_LOG == 0 majory = 1 #5 ytick = GR.tick(ymin, ymax) / majory else @@ -562,7 +591,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # NOTE: setwindow sets the "data coordinate" limits of the current "viewport" GR.setwindow(xmin, xmax, ymin, ymax) - GR.setscale(scale) + GR.setscale(scaleop) end # draw the axes @@ -585,8 +614,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # GR.setlinetype(GR.LINETYPE_DOTTED) if sp[:grid] - GR.grid3d(xtick, 0, ztick, xmin, ymin, zmin, 2, 0, 2) - GR.grid3d(0, ytick, 0, xmax, ymin, zmin, 0, 2, 0) + GR.grid3d(xtick, 0, ztick, xmin, ymax, zmin, 2, 0, 2) + GR.grid3d(0, ytick, 0, xmin, ymax, zmin, 0, 2, 0) end GR.axes3d(xtick, 0, ztick, xmin, ymin, zmin, 2, 0, 2, -ticksize) GR.axes3d(0, ytick, 0, xmax, ymin, zmin, 0, 2, 0, ticksize) @@ -722,12 +751,16 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) x, y, z = series[:x], series[:y], series[:z] frng = series[:fillrange] + # add custom frame shapes to markershape? + series_annotations_shapes!(series) + # ------------------------------------------------------- + # recompute data if typeof(z) <: Surface - if st == :heatmap - expand_extrema!(sp[:xaxis], (x[1]-0.5*(x[2]-x[1]), x[end]+0.5*(x[end]-x[end-1]))) - expand_extrema!(sp[:yaxis], (y[1]-0.5*(y[2]-y[1]), y[end]+0.5*(y[end]-y[end-1]))) - end + # if st == :heatmap + # expand_extrema!(sp[:xaxis], (x[1]-0.5*(x[2]-x[1]), x[end]+0.5*(x[end]-x[end-1]))) + # expand_extrema!(sp[:yaxis], (y[1]-0.5*(y[2]-y[1]), y[end]+0.5*(y[end]-y[end-1]))) + # end z = vec(transpose_z(series, z.surf, false)) elseif ispolar(sp) if frng != nothing @@ -756,7 +789,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # draw the line(s) if st == :path gr_set_line(series[:linewidth], series[:linestyle], series[:linecolor]) #, series[:linealpha]) - gr_polyline(x, y) + arrowside = isa(series[:arrow], Arrow) ? series[:arrow].side : :none + gr_polyline(x, y; arrowside = arrowside) end end @@ -808,16 +842,19 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) cmap && gr_colorbar(sp) elseif st == :heatmap - # z = vec(transpose_z(series, z.surf, false)) zmin, zmax = gr_lims(zaxis, true) clims = sp[:clims] if is_2tuple(clims) isfinite(clims[1]) && (zmin = clims[1]) isfinite(clims[2]) && (zmax = clims[2]) end - GR.setspace(zmin, zmax, 0, 90) - # GR.surface(x, y, z, GR.OPTION_COLORED_MESH) - GR.surface(x, y, z, GR.OPTION_HEATMAP) + grad = isa(series[:fillcolor], ColorGradient) ? series[:fillcolor] : cgrad() + colors = [grad[clamp((zi-zmin) / (zmax-zmin), 0, 1)] for zi=z] + rgba = map(c -> UInt32( round(Int, alpha(c) * 255) << 24 + + round(Int, blue(c) * 255) << 16 + + round(Int, green(c) * 255) << 8 + + round(Int, red(c) * 255) ), colors) + GR.drawimage(xmin, xmax, ymax, ymin, length(x), length(y), rgba) cmap && gr_colorbar(sp) elseif st in (:path3d, :scatter3d) @@ -904,8 +941,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) elseif st == :image - img = series[:z].surf - h, w = size(img) + z = transpose_z(series, series[:z].surf, true) + h, w = size(z) if eltype(z) <: Colors.AbstractGray grey = round(UInt8, float(z) * 255) rgba = map(c -> UInt32( 0xff000000 + Int(c)<<16 + Int(c)<<8 + Int(c) ), grey) @@ -918,6 +955,13 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) GR.drawimage(xmin, xmax, ymax, ymin, w, h, rgba) end + # this is all we need to add the series_annotations text + anns = series[:series_annotations] + for (xi,yi,str,fnt) in EachAnn(anns, x, y) + gr_set_font(fnt) + gr_text(GR.wctondc(xi, yi)..., str) + end + GR.restorestate() end @@ -1025,22 +1069,25 @@ const _gr_mimeformats = Dict( ) const _gr_wstype_default = @static if is_linux() - "cairox11" + "x11" + # "cairox11" elseif is_apple() "quartz" else - "windows" + "use_default" end +const _gr_wstype = Ref(get(ENV, "GKS_WSTYPE", _gr_wstype_default)) +gr_set_output(wstype::String) = (_gr_wstype[] = wstype) + for (mime, fmt) in _gr_mimeformats @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) GR.emergencyclosegks() filepath = tempname() * "." * $fmt - withenv("GKS_WSTYPE" => $fmt, # $fmt == "png" ? "cairopng" : $fmt, - "GKS_FILEPATH" => filepath) do - gr_display(plt) - GR.emergencyclosegks() - end + ENV["GKS_WSTYPE"] = $fmt + ENV["GKS_FILEPATH"] = filepath + gr_display(plt) + GR.emergencyclosegks() write(io, readstring(filepath)) rm(filepath) end @@ -1050,18 +1097,20 @@ function _display(plt::Plot{GRBackend}) if plt[:display_type] == :inline GR.emergencyclosegks() filepath = tempname() * ".pdf" - withenv("GKS_WSTYPE" => "pdf", - "GKS_FILEPATH" => filepath) do - gr_display(plt) - GR.emergencyclosegks() - end + ENV["GKS_WSTYPE"] = "pdf" + ENV["GKS_FILEPATH"] = filepath + gr_display(plt) + GR.emergencyclosegks() content = string("\033]1337;File=inline=1;preserveAspectRatio=0:", base64encode(open(readbytes, filepath)), "\a") println(content) rm(filepath) else - withenv("GKS_WSTYPE" => get(ENV, "GKS_WSTYPE", _gr_wstype_default), - "GKS_DOUBLE_BUF" => get(ENV ,"GKS_DOUBLE_BUF", "true")) do - gr_display(plt) + ENV["GKS_DOUBLE_BUF"] = true + if _gr_wstype[] != "use_default" + ENV["GKS_WSTYPE"] = _gr_wstype[] end + gr_display(plt) end end + +closeall(::GRBackend) = GR.emergencyclosegks() diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index 956cddb9..a70ddf60 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -30,6 +30,7 @@ const _plotly_attr = merge_with_base_supported([ :aspect_ratio, :hover, :inset_subplots, + :bar_width, ]) const _plotly_seriestype = [ @@ -56,6 +57,7 @@ end const _plotly_js_path = joinpath(dirname(@__FILE__), "..", "..", "deps", "plotly-latest.min.js") +const _plotly_js_path_remote = "https://cdn.plot.ly/plotly-latest.min.js" function _initialize_backend(::PlotlyBackend; kw...) @eval begin @@ -92,6 +94,20 @@ end # ---------------------------------------------------------------- +const _plotly_legend_pos = KW( + :right => [1., 0.5], + :left => [0., 0.5], + :top => [0.5, 1.], + :bottom => [0.5, 0.], + :bottomleft => [0., 0.], + :bottomright => [1., 0.], + :topright => [1., 1.], + :topleft => [0., 1.] + ) + +plotly_legend_pos(pos::Symbol) = get(_plotly_legend_pos, pos, [1.,1.]) +plotly_legend_pos{S<:Real, T<:Real}(v::Tuple{S,T}) = v + function plotly_font(font::Font, color = font.color) KW( :family => font.family, @@ -100,6 +116,7 @@ function plotly_font(font::Font, color = font.color) ) end + function plotly_annotation_dict(x, y, val; xref="paper", yref="paper") KW( :text => val, @@ -162,14 +179,17 @@ function plotly_apply_aspect_ratio(sp::Subplot, plotarea, pcts) if aspect_ratio == :equal aspect_ratio = 1.0 end + xmin,xmax = axis_limits(sp[:xaxis]) + ymin,ymax = axis_limits(sp[:yaxis]) + want_ratio = ((xmax-xmin) / (ymax-ymin)) / aspect_ratio parea_ratio = width(plotarea) / height(plotarea) - if aspect_ratio > parea_ratio + if want_ratio > parea_ratio # need to shrink y - ratio = parea_ratio / aspect_ratio + ratio = parea_ratio / want_ratio pcts[2], pcts[4] = shrink_by(pcts[2], pcts[4], ratio) - elseif aspect_ratio < parea_ratio + elseif want_ratio < parea_ratio # need to shrink x - ratio = aspect_ratio / parea_ratio + ratio = want_ratio / parea_ratio pcts[1], pcts[3] = shrink_by(pcts[1], pcts[3], ratio) end pcts @@ -214,7 +234,7 @@ function plotly_axis(axis::Axis, sp::Subplot) # lims lims = axis[:lims] if lims != :auto && limsType(lims) == :limits - ax[:range] = lims + ax[:range] = map(scalefunc(axis[:scale]), lims) end # flip @@ -289,17 +309,32 @@ function plotly_layout(plt::Plot) # legend d_out[:showlegend] = sp[:legend] != :none + xpos,ypos = plotly_legend_pos(sp[:legend]) if sp[:legend] != :none d_out[:legend] = KW( :bgcolor => rgba_string(sp[:background_color_legend]), :bordercolor => rgba_string(sp[:foreground_color_legend]), :font => plotly_font(sp[:legendfont], sp[:foreground_color_legend]), + :x => xpos, + :y => ypos ) end # annotations append!(d_out[:annotations], KW[plotly_annotation_dict(ann...; xref = "x$spidx", yref = "y$spidx") for ann in sp[:annotations]]) + # series_annotations + for series in series_list(sp) + anns = series[:series_annotations] + for (xi,yi,str,fnt) in EachAnn(anns, series[:x], series[:y]) + push!(d_out[:annotations], plotly_annotation_dict( + xi, + yi, + PlotText(str,fnt); xref = "x$spidx", yref = "y$spidx") + ) + end + end + # # arrows # for sargs in seriesargs # a = sargs[:arrow] @@ -359,7 +394,7 @@ function plotly_close_shapes(x, y) xs, ys = nansplit(x), nansplit(y) for i=1:length(xs) shape = Shape(xs[i], ys[i]) - xs[i], ys[i] = shape_coords(shape) + xs[i], ys[i] = coords(shape) end nanvcat(xs), nanvcat(ys) end @@ -420,8 +455,11 @@ function plotly_series(plt::Plot, series::Series) elseif st == :bar d_out[:type] = "bar" - d_out[:x], d_out[:y] = x, y - d_out[:orientation] = isvertical(series) ? "v" : "h" + d_out[:x], d_out[:y], d_out[:orientation] = if isvertical(series) + x, y, "v" + else + y, x, "h" + end d_out[:marker] = KW(:color => rgba_string(series[:fillcolor])) elseif st == :heatmap @@ -592,8 +630,12 @@ end # ---------------------------------------------------------------- +const _use_remote = Ref(false) + function html_head(plt::Plot{PlotlyBackend}) - "" + jsfilename = _use_remote[] ? _plotly_js_path_remote : _plotly_js_path + # "" + "" end function html_body(plt::Plot{PlotlyBackend}, style = nothing) @@ -624,7 +666,8 @@ end function _show(io::IO, ::MIME"image/png", plt::Plot{PlotlyBackend}) - show_png_from_html(io, plt) + # show_png_from_html(io, plt) + error("png output from the plotly backend is not supported. Please use plotlyjs instead.") end function _show(io::IO, ::MIME"image/svg+xml", plt::Plot{PlotlyBackend}) diff --git a/src/backends/plotlyjs.jl b/src/backends/plotlyjs.jl index e35f7bd5..3f02d7b1 100644 --- a/src/backends/plotlyjs.jl +++ b/src/backends/plotlyjs.jl @@ -40,7 +40,11 @@ end function _create_backend_figure(plt::Plot{PlotlyJSBackend}) - PlotlyJS.plot() + if !isplotnull() && plt[:overwrite_figure] && isa(current().o, PlotlyJS.SyncPlot) + PlotlyJS.SyncPlot(PlotlyJS.Plot(), current().o.view) + else + PlotlyJS.plot() + end end @@ -59,7 +63,7 @@ function _series_updated(plt::Plot{PlotlyJSBackend}, series::Series) kw = KW(xsym => (series.d[:x],), ysym => (series.d[:y],)) z = series[:z] if z != nothing - kw[:z] = (transpose_z(series, series[:z].surf, false),) + kw[:z] = (isa(z,Surface) ? transpose_z(series, series[:z].surf, false) : z,) end PlotlyJS.restyle!( plt.o, @@ -82,7 +86,11 @@ end # ---------------------------------------------------------------- function _show(io::IO, ::MIME"image/svg+xml", plt::Plot{PlotlyJSBackend}) - show(io, MIME("text/html"), plt.o) + if Plots.isijulia() + print(io, PlotlyJS.html_body(plt.o)) + else + show(io, MIME("text/html"), plt.o) + end end function plotlyjs_save_hack(io::IO, plt::Plot{PlotlyJSBackend}, ext::String) @@ -97,3 +105,10 @@ _show(io::IO, ::MIME"image/eps", plt::Plot{PlotlyJSBackend}) = plotlyjs_save_hac function _display(plt::Plot{PlotlyJSBackend}) display(plt.o) end + + +function closeall(::PlotlyJSBackend) + if !isplotnull() && isa(current().o, PlotlyJS.SyncPlot) + close(current().o) + end +end diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 0ea2390d..b1f7e2c0 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -64,7 +64,7 @@ function _initialize_backend(::PyPlotBackend) # problem: https://github.com/tbreloff/Plots.jl/issues/308 # solution: hack from @stevengj: https://github.com/stevengj/PyPlot.jl/pull/223#issuecomment-229747768 otherdisplays = splice!(Base.Multimedia.displays, 2:length(Base.Multimedia.displays)) - import PyPlot + import PyPlot, PyCall import LaTeXStrings: latexstring append!(Base.Multimedia.displays, otherdisplays) @@ -117,7 +117,9 @@ py_color(grad::ColorGradient) = py_color(grad.colors) function py_colormap(grad::ColorGradient) pyvals = [(z, py_color(grad[z])) for z in grad.values] - pycolors.LinearSegmentedColormap[:from_list]("tmp", pyvals) + cm = pycolors.LinearSegmentedColormap[:from_list]("tmp", pyvals) + cm[:set_bad](color=(0,0,0,0.0), alpha=0.0) + cm end py_colormap(c) = py_colormap(cgrad()) @@ -140,7 +142,7 @@ function py_linestyle(seriestype::Symbol, linestyle::Symbol) end function py_marker(marker::Shape) - x, y = shape_coords(marker) + x, y = coords(marker) n = length(x) mat = zeros(n+1,2) for i=1:n @@ -246,6 +248,12 @@ function labelfunc(scale::Symbol, backend::PyPlotBackend) end end +function py_mask_nans(z) + # PyPlot.pywrap(pynp.ma[:masked_invalid](PyPlot.pywrap(z))) + PyCall.pycall(pynp.ma[:masked_invalid], Any, z) + # pynp.ma[:masked_where](pynp.isnan(z),z) +end + # --------------------------------------------------------------------------- function fix_xy_lengths!(plt::Plot{PyPlotBackend}, series::Series) @@ -437,6 +445,9 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) error("Only numbers and vectors are supported with levels keyword") end + # add custom frame shapes to markershape? + series_annotations_shapes!(series, :xy) + # for each plotting command, optionally build and add a series handle to the list # line plot @@ -552,16 +563,46 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) else xyargs end - handle = ax[:scatter](xyargs...; - label = series[:label], - zorder = series[:series_plotindex] + 0.5, - marker = py_marker(series[:markershape]), - s = py_dpi_scale(plt, series[:markersize] .^ 2), - edgecolors = py_markerstrokecolor(series), - linewidths = py_dpi_scale(plt, series[:markerstrokewidth]), - extrakw... - ) - push!(handles, handle) + + if isa(series[:markershape], AbstractVector{Shape}) + # this section will create one scatter per data point to accomodate the + # vector of shapes + handle = [] + x,y = xyargs + shapes = series[:markershape] + msc = py_markerstrokecolor(series) + lw = py_dpi_scale(plt, series[:markerstrokewidth]) + for i=1:length(y) + extrakw[:c] = if series[:marker_z] == nothing + py_color_fix(py_color(cycle(series[:markercolor],i)), x) + else + extrakw[:c] + end + + push!(handle, ax[:scatter](cycle(x,i), cycle(y,i); + label = series[:label], + zorder = series[:series_plotindex] + 0.5, + marker = py_marker(cycle(shapes,i)), + s = py_dpi_scale(plt, cycle(series[:markersize],i) .^ 2), + edgecolors = msc, + linewidths = lw, + extrakw... + )) + end + push!(handles, handle) + else + # do a normal scatter plot + handle = ax[:scatter](xyargs...; + label = series[:label], + zorder = series[:series_plotindex] + 0.5, + marker = py_marker(series[:markershape]), + s = py_dpi_scale(plt, series[:markersize] .^ 2), + edgecolors = py_markerstrokecolor(series), + linewidths = py_dpi_scale(plt, series[:markerstrokewidth]), + extrakw... + ) + push!(handles, handle) + end end if st == :hexbin @@ -730,12 +771,11 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) end clims = sp[:clims] - if is_2tuple(clims) - isfinite(clims[1]) && (extrakw[:vmin] = clims[1]) - isfinite(clims[2]) && (extrakw[:vmax] = clims[2]) - end + zmin, zmax = extrema(z) + extrakw[:vmin] = (is_2tuple(clims) && isfinite(clims[1])) ? clims[1] : zmin + extrakw[:vmax] = (is_2tuple(clims) && isfinite(clims[2])) ? clims[2] : zmax - handle = ax[:pcolormesh](x, y, z; + handle = ax[:pcolormesh](x, y, py_mask_nans(z); label = series[:label], zorder = series[:series_plotindex], cmap = py_fillcolormap(series), @@ -763,16 +803,6 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) push!(handle, ax[:add_patch](patches)) end end - # path = py_path(x, y) - # patches = pypatches.pymember("PathPatch")(path; - # label = series[:label], - # zorder = series[:series_plotindex], - # edgecolor = py_linecolor(series), - # facecolor = py_fillcolor(series), - # linewidth = py_dpi_scale(plt, series[:linewidth]), - # fill = true - # ) - # handle = ax[:add_patch](patches) push!(handles, handle) end @@ -842,6 +872,12 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) ) push!(handles, handle) end + + # this is all we need to add the series_annotations text + anns = series[:series_annotations] + for (xi,yi,str,fnt) in EachAnn(anns, x, y) + py_add_annotations(sp, xi, yi, PlotText(str, fnt)) + end end # -------------------------------------------------------------------------- @@ -1217,3 +1253,5 @@ for (mime, fmt) in _pyplot_mimeformats ) end end + +closeall(::PyPlotBackend) = PyPlot.plt[:close]("all") diff --git a/src/backends/unicodeplots.jl b/src/backends/unicodeplots.jl index 710e3a4d..f5e34834 100644 --- a/src/backends/unicodeplots.jl +++ b/src/backends/unicodeplots.jl @@ -16,7 +16,8 @@ const _unicodeplots_seriestype = [ :path, :scatter, # :bar, :shape, - :histogram2d + :histogram2d, + :spy ] const _unicodeplots_style = [:auto, :solid] const _unicodeplots_marker = [:none, :auto, :circle] @@ -44,6 +45,18 @@ end # ------------------------------- +const _canvas_type = Ref(:auto) + +function _canvas_map() + KW( + :braille => UnicodePlots.BrailleCanvas, + :ascii => UnicodePlots.AsciiCanvas, + :block => UnicodePlots.BlockCanvas, + :dot => UnicodePlots.DotCanvas, + :density => UnicodePlots.DensityCanvas, + ) +end + # do all the magic here... build it all at once, since we need to know about all the series at the very beginning function rebuildUnicodePlot!(plt::Plot, width, height) @@ -65,7 +78,27 @@ function rebuildUnicodePlot!(plt::Plot, width, height) y = Float64[ylim[1]] # create a plot window with xlim/ylim set, but the X/Y vectors are outside the bounds - canvas_type = isijulia() ? UnicodePlots.AsciiCanvas : UnicodePlots.BrailleCanvas + ct = _canvas_type[] + canvas_type = if ct == :auto + isijulia() ? UnicodePlots.AsciiCanvas : UnicodePlots.BrailleCanvas + else + _canvas_map()[ct] + end + + # special handling for spy + if length(sp.series_list) == 1 + series = sp.series_list[1] + if series[:seriestype] == :spy + push!(plt.o, UnicodePlots.spy( + series[:z].surf, + width = width, + height = height, + title = sp[:title], + canvas = canvas_type + )) + continue + end + end # # make it a bar canvas if plotting bar # if any(series -> series[:seriestype] == :bar, series_list(sp)) diff --git a/src/components.jl b/src/components.jl index 895f82bc..73c62f50 100644 --- a/src/components.jl +++ b/src/components.jl @@ -23,21 +23,24 @@ immutable Shape # end end Shape(verts::AVec) = Shape(unzip(verts)...) +Shape(s::Shape) = deepcopy(s) get_xs(shape::Shape) = shape.x get_ys(shape::Shape) = shape.y vertices(shape::Shape) = collect(zip(shape.x, shape.y)) +#deprecated +@deprecate shape_coords coords -function shape_coords(shape::Shape) +function coords(shape::Shape) shape.x, shape.y end -function shape_coords(shapes::AVec{Shape}) +function coords(shapes::AVec{Shape}) length(shapes) == 0 && return zeros(0), zeros(0) xs = map(get_xs, shapes) ys = map(get_ys, shapes) - x, y = map(copy, shape_coords(shapes[1])) + x, y = map(copy, coords(shapes[1])) for shape in shapes[2:end] nanappend!(x, shape.x) nanappend!(y, shape.y) @@ -72,13 +75,13 @@ function makestar(n; offset = -0.5, radius = 1.0) z2 = z1 + π / (n) outercircle = partialcircle(z1, z1 + 2π, n+1, radius) innercircle = partialcircle(z2, z2 + 2π, n+1, 0.4radius) - Shape(weave(outercircle, innercircle)[1:end-2]) + Shape(weave(outercircle, innercircle)) end "create a shape by picking points around the unit circle. `n` is the number of point/sides, `offset` is the starting angle" function makeshape(n; offset = -0.5, radius = 1.0) z = offset * π - Shape(partialcircle(z, z + 2π, n+1, radius)[1:end-1]) + Shape(partialcircle(z, z + 2π, n+1, radius)) end @@ -88,7 +91,7 @@ function makecross(; offset = -0.5, radius = 1.0) outercircle = partialcircle(z1, z1 + 2π, 9, radius) innercircle = partialcircle(z2, z2 + 2π, 5, 0.5radius) Shape(weave(outercircle, innercircle, - ordering=Vector[outercircle,innercircle,outercircle])[1:end-2]) + ordering=Vector[outercircle,innercircle,outercircle])) end @@ -110,6 +113,8 @@ const _shape_keys = Symbol[ :xcross, :utriangle, :dtriangle, + :rtriangle, + :ltriangle, :pentagon, :heptagon, :octagon, @@ -127,8 +132,10 @@ const _shapes = KW( :circle => makeshape(20), :rect => makeshape(4, offset=-0.25), :diamond => makeshape(4), - :utriangle => makeshape(3), - :dtriangle => makeshape(3, offset=0.5), + :utriangle => makeshape(3, offset=0.5), + :dtriangle => makeshape(3, offset=-0.5), + :rtriangle => makeshape(3, offset=0.0), + :ltriangle => makeshape(3, offset=1.0), :pentagon => makeshape(5), :hexagon => makeshape(6), :heptagon => makeshape(7), @@ -143,12 +150,14 @@ for n in [4,5,6,7,8] _shapes[Symbol("star$n")] = makestar(n) end +Shape(k::Symbol) = deepcopy(_shapes[k]) + # ----------------------------------------------------------------------- # uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon function center(shape::Shape) - x, y = shape_coords(shape) + x, y = coords(shape) n = length(x) A, Cx, Cy = 0.0, 0.0, 0.0 for i=1:n @@ -166,7 +175,7 @@ function center(shape::Shape) end function Base.scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) - sx, sy = shape_coords(shape) + sx, sy = coords(shape) cx, cy = c for i=1:length(sx) sx[i] = (sx[i] - cx) * x + cx @@ -177,11 +186,11 @@ end function Base.scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) shapecopy = deepcopy(shape) - scale!(shape, x, y, c) + scale!(shapecopy, x, y, c) end function translate!(shape::Shape, x::Real, y::Real = x) - sx, sy = shape_coords(shape) + sx, sy = coords(shape) for i=1:length(sx) sx[i] += x sy[i] += y @@ -191,7 +200,7 @@ end function translate(shape::Shape, x::Real, y::Real = x) shapecopy = deepcopy(shape) - translate!(shape, x, y) + translate!(shapecopy, x, y) end function rotate_x(x::Real, y::Real, Θ::Real, centerx::Real, centery::Real) @@ -208,7 +217,7 @@ function rotate(x::Real, y::Real, θ::Real, c = center(shape)) end function rotate!(shape::Shape, Θ::Real, c = center(shape)) - x, y = shape_coords(shape) + x, y = coords(shape) cx, cy = c for i=1:length(x) xi = rotate_x(x[i], y[i], Θ, cx, cy) @@ -226,7 +235,7 @@ end # ----------------------------------------------------------------------- -immutable Font +type Font family::AbstractString pointsize::Int halign::Symbol @@ -283,6 +292,17 @@ function font(args...) Font(family, pointsize, halign, valign, rotation, color) end +function scalefontsize(k::Symbol, factor::Number) + f = default(k) + f.pointsize = round(Int, factor * f.pointsize) + default(k, f) +end +function scalefontsizes(factor::Number) + for k in (:titlefont, :guidefont, :tickfont, :legendfont) + scalefontsize(k, factor) + end +end + "Wrap a string with font info" immutable PlotText str::AbstractString @@ -296,11 +316,7 @@ function text(str, args...) PlotText(string(str), font(args...)) end - -annotations(::Void) = [] -annotations(anns::AVec) = anns -annotations(anns) = Any[anns] - +Base.length(t::PlotText) = length(t.str) # ----------------------------------------------------------------------- @@ -314,9 +330,9 @@ immutable Stroke end function stroke(args...; alpha = nothing) - width = nothing - color = nothing - style = nothing + width = 1 + color = :black + style = :solid for arg in args T = typeof(arg) @@ -350,8 +366,8 @@ immutable Brush end function brush(args...; alpha = nothing) - size = nothing - color = nothing + size = 1 + color = :black for arg in args T = typeof(arg) @@ -376,6 +392,109 @@ end # ----------------------------------------------------------------------- +type SeriesAnnotations + strs::AbstractVector # the labels/names + font::Font + baseshape::Nullable + scalefactor::Tuple +end +function series_annotations(strs::AbstractVector, args...) + fnt = font() + shp = Nullable{Any}() + scalefactor = (1,1) + for arg in args + if isa(arg, Shape) || (isa(arg, AbstractVector) && eltype(arg) == Shape) + shp = Nullable(arg) + elseif isa(arg, Font) + fnt = arg + elseif isa(arg, Symbol) && haskey(_shapes, arg) + shp = _shapes[arg] + elseif isa(arg, Number) + scalefactor = (arg,arg) + elseif is_2tuple(arg) + scalefactor = arg + else + warn("Unused SeriesAnnotations arg: $arg ($(typeof(arg)))") + end + end + # if scalefactor != 1 + # for s in get(shp) + # scale!(s, scalefactor, scalefactor, (0,0)) + # end + # end + SeriesAnnotations(strs, fnt, shp, scalefactor) +end +series_annotations(anns::SeriesAnnotations) = anns +series_annotations(::Void) = nothing + +function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) + anns = series[:series_annotations] + # msw,msh = anns.scalefactor + # ms = series[:markersize] + # msw,msh = if isa(ms, AbstractVector) + # 1,1 + # elseif is_2tuple(ms) + # ms + # else + # ms,ms + # end + + # @show msw msh + if anns != nothing && !isnull(anns.baseshape) + # we use baseshape to overwrite the markershape attribute + # with a list of custom shapes for each + msw,msh = anns.scalefactor + msize = Float64[] + shapes = Shape[begin + str = cycle(anns.strs,i) + + # get the width and height of the string (in mm) + sw, sh = text_size(str, anns.font.pointsize) + + # how much to scale the base shape? + # note: it's a rough assumption that the shape fills the unit box [-1,-1,1,1], + # so we scale the length-2 shape by 1/2 the total length + scalar = (backend() == PyPlotBackend() ? 1.7 : 1.0) + xscale = 0.5to_pixels(sw) * scalar + yscale = 0.5to_pixels(sh) * scalar + + # we save the size of the larger direction to the markersize list, + # and then re-scale a copy of baseshape to match the w/h ratio + maxscale = max(xscale, yscale) + push!(msize, maxscale) + baseshape = cycle(get(anns.baseshape),i) + shape = scale(baseshape, msw*xscale/maxscale, msh*yscale/maxscale, (0,0)) + end for i=1:length(anns.strs)] + series[:markershape] = shapes + series[:markersize] = msize + end + return +end + +type EachAnn + anns + x + y +end +Base.start(ea::EachAnn) = 1 +Base.done(ea::EachAnn, i) = ea.anns == nothing || isempty(ea.anns.strs) || i > length(ea.y) +function Base.next(ea::EachAnn, i) + tmp = cycle(ea.anns.strs,i) + str,fnt = if isa(tmp, PlotText) + tmp.str, tmp.font + else + tmp, ea.anns.font + end + ((cycle(ea.x,i), cycle(ea.y,i), str, fnt), i+1) +end + +annotations(::Void) = [] +annotations(anns::AVec) = anns +annotations(anns) = Any[anns] +annotations(sa::SeriesAnnotations) = sa + +# ----------------------------------------------------------------------- + "type which represents z-values for colors and sizes (and anything else that might come up)" immutable ZValues values::Vector{Float64} @@ -452,19 +571,25 @@ Base.eltype{T}(vol::Volume{T}) = T # style is :open or :closed (for now) immutable Arrow style::Symbol + side::Symbol # :head (default), :tail, or :both headlength::Float64 headwidth::Float64 end function arrow(args...) style = :simple + side = :head headlength = 0.3 headwidth = 0.3 setlength = false for arg in args T = typeof(arg) if T == Symbol - style = arg + if arg in (:head, :tail, :both) + side = arg + else + style = arg + end elseif T <: Number # first we apply to both, but if there's more, then only change width after the first number headwidth = Float64(arg) @@ -478,7 +603,7 @@ function arrow(args...) warn("Skipped arrow arg $arg") end end - Arrow(style, headlength, headwidth) + Arrow(style, side, headlength, headwidth) end @@ -508,54 +633,34 @@ end # ----------------------------------------------------------------------- type BezierCurve{T <: FixedSizeArrays.Vec} - control_points::Vector{T} + control_points::Vector{T} end function (bc::BezierCurve)(t::Real) - p = zero(P2) - n = length(bc.control_points)-1 - for i in 0:n - p += bc.control_points[i+1] * binomial(n, i) * (1-t)^(n-i) * t^i - end - p + p = zero(P2) + n = length(bc.control_points)-1 + for i in 0:n + p += bc.control_points[i+1] * binomial(n, i) * (1-t)^(n-i) * t^i + end + p end Base.mean(x::Real, y::Real) = 0.5*(x+y) Base.mean{N,T<:Real}(ps::FixedSizeArrays.Vec{N,T}...) = sum(ps) / length(ps) -curve_points(curve::BezierCurve, n::Integer = 30; range = [0,1]) = map(curve, linspace(range..., n)) +@deprecate curve_points coords + +coords(curve::BezierCurve, n::Integer = 30; range = [0,1]) = map(curve, linspace(range..., n)) # build a BezierCurve which leaves point p vertically upwards and arrives point q vertically upwards. # may create a loop if necessary. Assumes the view is [0,1] -function directed_curve(p::P2, q::P2; xview = 0:1, yview = 0:1) -mn = mean(p, q) -diff = q - p - -minx, maxx = minimum(xview), maximum(xview) -miny, maxy = minimum(yview), maximum(yview) -diffpct = P2(diff[1] / (maxx - minx), - diff[2] / (maxy - miny)) - -# these points give the initial/final "rise" -# vertical_offset = P2(0, (maxy - miny) * max(0.03, min(abs(0.5diffpct[2]), 1.0))) -vertical_offset = P2(0, max(0.15, 0.5norm(diff))) -upper_control = p + vertical_offset -lower_control = q - vertical_offset - -# try to figure out when to loop around vs just connecting straight -# TODO: choose loop direction based on sign of p[1]?? -# x_close_together = abs(diffpct[1]) <= 0.05 -p_is_higher = diff[2] <= 0 -inside_control_points = if p_is_higher - # add curve points which will create a loop - sgn = mn[1] < 0.5 * (maxx + minx) ? -1 : 1 - inside_offset = P2(0.3 * (maxx - minx), 0) - additional_offset = P2(sgn * diff[1], 0) # make it even loopier - [upper_control + sgn * (inside_offset + max(0, additional_offset)), - lower_control + sgn * (inside_offset + max(0, -additional_offset))] -else - [] +function directed_curve(args...; kw...) + error("directed_curve has been moved to PlotRecipes") end -BezierCurve([p, upper_control, inside_control_points..., lower_control, q]) +function extrema_plus_buffer(v, buffmult = 0.2) + vmin,vmax = extrema(v) + vdiff = vmax-vmin + buffer = vdiff * buffmult + vmin - buffer, vmax + buffer end diff --git a/src/examples.jl b/src/examples.jl index cb1fef93..96872851 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -40,7 +40,7 @@ PlotExample("Colors", "Access predefined palettes (or build your own with the `colorscheme` method). Line/marker colors are auto-generated from the plot's palette, unless overridden. Set the `z` argument to turn on series gradients.", [:(begin y = rand(100) - plot(0:10:100,rand(11,4),lab="lines",w=3,palette=:grays,fill=(0,:auto), α=0.6) + plot(0:10:100,rand(11,4),lab="lines",w=3,palette=:grays,fill=0, α=0.6) scatter!(y, zcolor=abs(y-.5), m=(:heat,0.8,stroke(1,:green)), ms=10*abs(y-0.5)+4, lab="grad") end)] ), diff --git a/src/layouts.jl b/src/layouts.jl index dba708ee..a5814ba1 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -96,6 +96,32 @@ function Base.show(io::IO, bbox::BoundingBox) print(io, "BBox{l,t,r,b,w,h = $(left(bbox)),$(top(bbox)), $(right(bbox)),$(bottom(bbox)), $(width(bbox)),$(height(bbox))}") end +# ----------------------------------------------------------- + +# points combined by x/y, pct, and length +type MixedMeasures + xy::Float64 + pct::Float64 + len::AbsoluteLength +end + +function resolve_mixed(mix::MixedMeasures, sp::Subplot, letter::Symbol) + xy = mix.xy + pct = mix.pct + if mix.len != 0mm + f = (letter == :x ? width : height) + totlen = f(plotarea(sp)) + @show totlen + pct += mix.len / totlen + end + if pct != 0 + amin, amax = axis_limits(sp[Symbol(letter,:axis)]) + xy += pct * (amax-amin) + end + xy +end + + # ----------------------------------------------------------- # AbstractLayout @@ -692,9 +718,22 @@ function link_axes!(axes::Axis...) end end +# figure out which subplots to link +function link_subplots(a::AbstractArray{AbstractLayout}, axissym::Symbol) + subplots = [] + for l in a + if isa(l, Subplot) + push!(subplots, l) + elseif isa(l, GridLayout) && size(l) == (1,1) + push!(subplots, l[1,1]) + end + end + subplots +end + # for some vector or matrix of layouts, filter only the Subplots and link those axes function link_axes!(a::AbstractArray{AbstractLayout}, axissym::Symbol) - subplots = filter(l -> isa(l, Subplot), a) + subplots = link_subplots(a, axissym) axes = [sp.attr[axissym] for sp in subplots] if length(axes) > 0 link_axes!(axes...) diff --git a/src/output.jl b/src/output.jl index b71dc7ed..ff4a1def 100644 --- a/src/output.jl +++ b/src/output.jl @@ -52,6 +52,16 @@ function tex(plt::Plot, fn::AbstractString) end tex(fn::AbstractString) = tex(current(), fn) +function html(plt::Plot, fn::AbstractString) + fn = addExtension(fn, "html") + io = open(fn, "w") + _use_remote[] = true + show(io, MIME("text/html"), plt) + _use_remote[] = false + close(io) +end +html(fn::AbstractString) = html(current(), fn) + # ---------------------------------------------------------------- @@ -63,6 +73,7 @@ const _savemap = Dict( "ps" => ps, "eps" => eps, "tex" => tex, + "html" => html, ) function getExtension(fn::AbstractString) @@ -111,6 +122,13 @@ savefig(fn::AbstractString) = savefig(current(), fn) gui(plt::Plot = current()) = display(PlotsDisplay(), plt) +# IJulia only... inline display +function inline(plt::Plot = current()) + isijulia() || error("inline() is IJulia-only") + Main.IJulia.clear_output(true) + display(Main.IJulia.InlineDisplay(), plt) +end + function Base.display(::PlotsDisplay, plt::Plot) prepare_output(plt) _display(plt) @@ -119,6 +137,13 @@ end # override the REPL display to open a gui window Base.display(::Base.REPL.REPLDisplay, ::MIME"text/plain", plt::Plot) = gui(plt) + +_do_plot_show(plt, showval::Bool) = showval && gui(plt) +function _do_plot_show(plt, showval::Symbol) + showval == :gui && gui(plt) + showval in (:inline,:ijulia) && inline(plt) +end + # --------------------------------------------------------- const _mimeformats = Dict( @@ -172,6 +197,8 @@ for mime in keys(_mimeformats) end end +closeall() = closeall(backend()) + # --------------------------------------------------------- # A backup, if no PNG generation is defined, is to try to make a PDF and use FileIO to convert diff --git a/src/pipeline.jl b/src/pipeline.jl index 958594a0..6c2e05c3 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -283,7 +283,11 @@ function _subplot_setup(plt::Plot, d::KW, kw_list::Vector{KW}) # override subplot/axis args. `sp_attrs` take precendence for (idx,sp) in enumerate(plt.subplots) - attr = merge(d, get(sp_attrs, sp, KW())) + attr = if !haskey(d, :subplot) || d[:subplot] == idx + merge(d, get(sp_attrs, sp, KW())) + else + get(sp_attrs, sp, KW()) + end _update_subplot_args(plt, sp, attr, idx, false) end @@ -333,14 +337,17 @@ function _prepare_annotations(sp::Subplot, d::KW) # strip out series annotations (those which are based on series x/y coords) # and add them to the subplot attr sp_anns = annotations(sp[:annotations]) - anns = annotations(pop!(d, :series_annotations, [])) - if length(anns) > 0 - x, y = d[:x], d[:y] - nx, ny, na = map(length, (x,y,anns)) - n = max(nx, ny, na) - anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(anns[mod1(i,na)])) for i=1:n] - end - sp.attr[:annotations] = vcat(sp_anns, anns) + # series_anns = annotations(pop!(d, :series_annotations, [])) + # if isa(series_anns, SeriesAnnotations) + # series_anns.x = d[:x] + # series_anns.y = d[:y] + # elseif length(series_anns) > 0 + # x, y = d[:x], d[:y] + # nx, ny, na = map(length, (x,y,series_anns)) + # n = max(nx, ny, na) + # series_anns = [(x[mod1(i,nx)], y[mod1(i,ny)], text(series_anns[mod1(i,na)])) for i=1:n] + # end + # sp.attr[:annotations] = vcat(sp_anns, series_anns) end function _expand_subplot_extrema(sp::Subplot, d::KW, st::Symbol) diff --git a/src/plot.jl b/src/plot.jl index 87f06ea1..2e8063af 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -89,8 +89,16 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) plt.o = _create_backend_figure(plt) plt.init = true + series_attr = KW() + for (k,v) in d + if haskey(_series_defaults, k) + series_attr[k] = pop!(d,k) + end + end + # create the layout and initialize the subplots plt.layout, plt.subplots, plt.spmap = build_layout(layout, num_sp, copy(plts)) + cmdidx = 1 for (idx, sp) in enumerate(plt.subplots) _initialize_subplot(plt, sp) serieslist = series_list(sp) @@ -100,8 +108,11 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) sp.plt = plt sp.attr[:subplot_index] = idx for series in serieslist + merge!(series.d, series_attr) + _add_defaults!(series.d, plt, sp, cmdidx) push!(plt.series_list, series) _series_added(plt, series) + cmdidx += 1 end end @@ -115,9 +126,7 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) # finish up current(plt) - if get(d, :show, default(:show)) - gui() - end + _do_plot_show(plt, get(d, :show, default(:show))) plt end @@ -150,6 +159,11 @@ end function _plot!(plt::Plot, d::KW, args::Tuple) d[:plot_object] = plt + if !isempty(args) && !isdefined(Main, :StatPlots) && + first(split(string(typeof(args[1])), ".")) == "DataFrames" + warn("You're trying to plot a DataFrame, but this functionality is provided by StatPlots") + end + # -------------------------------- # "USER RECIPES" # -------------------------------- @@ -218,9 +232,10 @@ function _plot!(plt::Plot, d::KW, args::Tuple) current(plt) # do we want to force display? - if plt[:show] - gui(plt) - end + # if plt[:show] + # gui(plt) + # end + _do_plot_show(plt, plt[:show]) plt end diff --git a/src/recipes.jl b/src/recipes.jl index 4c985025..156bda3e 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -93,57 +93,11 @@ 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")) - -# # TODO: remove when StatPlots is ready -# 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 - - # --------------------------------------------------------------------------- @@ -511,6 +465,11 @@ centers(v::AVec) = 0.5 * (v[1:end-1] + v[2:end]) xedges, yedges, counts = my_hist_2d(x, y, d[:bins], normed = d[:normalize], weights = d[:weights]) + for (i,c) in enumerate(counts) + if c == 0 + counts[i] = NaN + end + end x := centers(xedges) y := centers(yedges) z := Surface(counts) @@ -536,179 +495,6 @@ end # note: don't add dependencies because this really isn't a drop-in replacement -# # TODO: move boxplots and violin plots to StatPlots when it's ready - -# # --------------------------------------------------------------------------- -# # 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 - -# const _violin_warned = [false] - -# # 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) -# if !_violin_warned[1] -# warn("Install the KernelDensity package for best results.") -# _violin_warned[1] = true -# end -# edges, widths = my_hist(y, 10) -# 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 @@ -740,8 +526,7 @@ 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) - + x, y = Array(float_extended_type(xorig), 0), Array(Float64, 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) @@ -947,15 +732,52 @@ end # 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...) +# "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 + +# Only allow matrices through, and make it seriestype :spy so the backend can +# optionally handle it natively. + +@userplot Spy + +@recipe function f(g::Spy) + @assert length(g.args) == 1 && typeof(g.args[1]) <: AbstractMatrix + seriestype := :spy + mat = g.args[1] + n,m = size(mat) + Plots.SliceIt, 1:m, 1:n, Surface(mat) end +@recipe function f(::Type{Val{:spy}}, x,y,z) + yflip := true + aspect_ratio := 1 + rs, cs, zs = findnz(z.surf) + xlim := extrema(cs) + ylim := extrema(rs) + if d[:markershape] == :none + markershape := :circle + end + if d[:markersize] == default(:markersize) + markersize := 1 + end + markerstrokewidth := 0 + marker_z := zs + label := "" + x := cs + y := rs + z := nothing + seriestype := :scatter + () +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...) @@ -967,14 +789,16 @@ abline!(args...; kw...) = abline!(current(), args...; kw...) # ------------------------------------------------- # Dates -@recipe function f{T<:AbstractArray{Date}}(::Type{T}, dts::T) - date_formatter = dt -> string(convert(Date, dt)) - xformatter := date_formatter - map(dt->convert(Int,dt), dts) -end +@recipe f(::Type{Date}, dt::Date) = (dt -> convert(Int,dt), dt -> string(convert(Date,dt))) +@recipe f(::Type{DateTime}, dt::DateTime) = (dt -> convert(Int,dt), dt -> string(convert(DateTime,dt))) -@recipe function f{T<:AbstractArray{DateTime}}(::Type{T}, dts::T) - date_formatter = dt -> string(convert(DateTime, dt)) - xformatter := date_formatter - map(dt->convert(Int,dt), dts) +# ------------------------------------------------- +# Complex Numbers + +@userplot ComplexPlot +@recipe function f(cp::ComplexPlot) + xguide --> "Real Part" + yguide --> "Imaginary Part" + seriestype --> :scatter + real(cp.args[1]), imag(cp.args[1]) end diff --git a/src/series.jl b/src/series.jl index 86be6954..12fdf318 100644 --- a/src/series.jl +++ b/src/series.jl @@ -42,8 +42,8 @@ convertToAnyVector(v::Volume, d::KW) = Any[v], nothing # # vector of OHLC # convertToAnyVector(v::AVec{OHLC}, d::KW) = Any[v], nothing -# dates -convertToAnyVector{D<:Union{Date,DateTime}}(dts::AVec{D}, d::KW) = Any[dts], nothing +# # dates +# convertToAnyVector{D<:Union{Date,DateTime}}(dts::AVec{D}, d::KW) = Any[dts], nothing # list of things (maybe other vectors, functions, or something else) function convertToAnyVector(v::AVec, d::KW) @@ -322,18 +322,18 @@ end @recipe function f(shape::Shape) seriestype --> :shape - shape_coords(shape) + coords(shape) end @recipe function f(shapes::AVec{Shape}) seriestype --> :shape - shape_coords(shapes) + coords(shapes) end @recipe function f(shapes::AMat{Shape}) seriestype --> :shape for j in 1:size(shapes,2) - @series shape_coords(vec(shapes[:,j])) + @series coords(vec(shapes[:,j])) end end diff --git a/src/themes.jl b/src/themes.jl index 0de2bbdf..3429585a 100644 --- a/src/themes.jl +++ b/src/themes.jl @@ -1,65 +1,40 @@ -const _invisible = RGBA(0,0,0,0) +function theme(s::Symbol; kw...) + # reset? + if s == :none || s == :default + PlotUtils._default_gradient[] = :inferno + default(; + bg = :white, + bglegend = :match, + bginside = :match, + bgoutside = :match, + fg = :auto, + fglegend = :match, + fggrid = :match, + fgaxis = :match, + fgtext = :match, + fgborder = :match, + fgguide = :match, + palette = :auto + ) + return + end -const _themes = KW( - :default => KW( - :bg => :white, - :bglegend => :match, - :bginside => :match, - :bgoutside => :match, - :fg => :auto, - :fglegend => :match, - :fggrid => :match, - :fgaxis => :match, - :fgtext => :match, - :fgborder => :match, - :fgguide => :match, + # update the default gradient and other defaults + thm = PlotThemes._themes[s] + if thm.gradient != nothing + PlotUtils._default_gradient[] = PlotThemes.gradient_name(s) + end + default(; + bg = thm.bg_secondary, + bginside = thm.bg_primary, + fg = thm.lines, + fgtext = thm.text, + fgguide = thm.text, + fglegend = thm.text, + palette = thm.palette, + kw... ) -) - -function add_theme(sym::Symbol, theme::KW) - _themes[sym] = theme end -# add a new theme, using an existing theme as the base -function add_theme(sym::Symbol; - base = :default, # start with this theme - bg = _themes[base][:bg], - bglegend = _themes[base][:bglegend], - bginside = _themes[base][:bginside], - bgoutside = _themes[base][:bgoutside], - fg = _themes[base][:fg], - fglegend = _themes[base][:fglegend], - fggrid = _themes[base][:fggrid], - fgaxis = _themes[base][:fgaxis], - fgtext = _themes[base][:fgtext], - fgborder = _themes[base][:fgborder], - fgguide = _themes[base][:fgguide], - kw...) - _themes[sym] = merge(KW( - :bg => bg, - :bglegend => bglegend, - :bginside => bginside, - :bgoutside => bgoutside, - :fg => fg, - :fglegend => fglegend, - :fggrid => fggrid, - :fgaxis => fgaxis, - :fgtext => fgtext, - :fgborder => fgborder, - :fgguide => fgguide, - ), KW(kw)) -end - -add_theme(:ggplot2, - bglegend = :lightgray, - bginside = :lightgray, - fg = :black, - fggrid = :white, - fgborder = _invisible, - fgaxis = _invisible -) - -function set_theme(sym::Symbol) - default(; _themes[sym]...) -end +@deprecate set_theme(s) theme(s) diff --git a/src/types.jl b/src/types.jl index b9965eec..0f1e4909 100644 --- a/src/types.jl +++ b/src/types.jl @@ -90,10 +90,18 @@ end # ----------------------------------------------------------------------- Base.getindex(plt::Plot, i::Integer) = plt.subplots[i] +Base.length(plt::Plot) = length(plt.subplots) +Base.endof(plt::Plot) = length(plt) + Base.getindex(plt::Plot, r::Integer, c::Integer) = plt.layout[r,c] +Base.size(plt::Plot) = size(plt.layout) +Base.size(plt::Plot, i::Integer) = size(plt.layout)[i] +Base.ndims(plt::Plot) = 2 + # attr(plt::Plot, k::Symbol) = plt.attr[k] # attr!(plt::Plot, v, k::Symbol) = (plt.attr[k] = v) Base.getindex(sp::Subplot, i::Integer) = series_list(sp)[i] +Base.endof(sp::Subplot) = length(series_list(sp)) # ----------------------------------------------------------------------- diff --git a/src/utils.jl b/src/utils.jl index 379a89cc..7c2af0fd 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -228,25 +228,34 @@ function Base.next(itr::SegmentsIterator, nextidx::Int) istart:iend, i end +# Find minimal type that can contain NaN and x +# To allow use of NaN separated segments with categorical x axis + +float_extended_type{T}(x::AbstractArray{T}) = Union{T,Float64} +float_extended_type{T<:Real}(x::AbstractArray{T}) = Float64 + # ------------------------------------------------------------------------------------ nop() = nothing notimpl() = error("This has not been implemented yet") -Base.cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj -Base.cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj +isnothing(x::Void) = true +isnothing(x) = false -Base.cycle(v::AVec, idx::Int) = v[mod1(idx, length(v))] -Base.cycle(v::AMat, idx::Int) = size(v,1) == 1 ? v[1, mod1(idx, size(v,2))] : v[:, mod1(idx, size(v,2))] -Base.cycle(v, idx::Int) = v +cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj +cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj -Base.cycle(v::AVec, indices::AVec{Int}) = map(i -> cycle(v,i), indices) -Base.cycle(v::AMat, indices::AVec{Int}) = map(i -> cycle(v,i), indices) -Base.cycle(v, indices::AVec{Int}) = fill(v, length(indices)) +cycle(v::AVec, idx::Int) = v[mod1(idx, length(v))] +cycle(v::AMat, idx::Int) = size(v,1) == 1 ? v[1, mod1(idx, size(v,2))] : v[:, mod1(idx, size(v,2))] +cycle(v, idx::Int) = v -Base.cycle(grad::ColorGradient, idx::Int) = cycle(grad.colors, idx) -Base.cycle(grad::ColorGradient, indices::AVec{Int}) = cycle(grad.colors, indices) +cycle(v::AVec, indices::AVec{Int}) = map(i -> cycle(v,i), indices) +cycle(v::AMat, indices::AVec{Int}) = map(i -> cycle(v,i), indices) +cycle(v, indices::AVec{Int}) = fill(v, length(indices)) + +cycle(grad::ColorGradient, idx::Int) = cycle(grad.colors, idx) +cycle(grad::ColorGradient, indices::AVec{Int}) = cycle(grad.colors, indices) makevec(v::AVec) = v makevec{T}(v::T) = T[v] @@ -460,7 +469,7 @@ ok(tup::Tuple) = ok(tup...) # compute one side of a fill range from a ribbon function make_fillrange_side(y, rib) frs = zeros(length(y)) - for (i, (yi, ri)) in enumerate(zip(y, cycle(rib))) + for (i, (yi, ri)) in enumerate(zip(y, Base.cycle(rib))) frs[i] = yi + ri end frs @@ -490,6 +499,8 @@ zlims(sp_idx::Int = 1) = zlims(current(), sp_idx) # --------------------------------------------------------------- +makekw(; kw...) = KW(kw) + wraptuple(x::Tuple) = x wraptuple(x) = (x,) @@ -714,28 +725,28 @@ Base.push!(series::Series, xi, yi, zi) = (push_x!(series,xi); push_y!(series,yi) # ------------------------------------------------------- -function update!(series::Series; kw...) +function attr!(series::Series; kw...) d = KW(kw) preprocessArgs!(d) for (k,v) in d if haskey(_series_defaults, k) series[k] = v else - warn("unused key $k in series update") + warn("unused key $k in series attr") end end _series_updated(series[:subplot].plt, series) series end -function update!(sp::Subplot; kw...) +function attr!(sp::Subplot; kw...) d = KW(kw) preprocessArgs!(d) for (k,v) in d if haskey(_subplot_defaults, k) sp[k] = v else - warn("unused key $k in subplot update") + warn("unused key $k in subplot attr") end end sp diff --git a/test/imgcomp.jl b/test/imgcomp.jl index 9eb79021..71f94924 100644 --- a/test/imgcomp.jl +++ b/test/imgcomp.jl @@ -24,7 +24,7 @@ default(size=(500,300)) # TODO: use julia's Condition type and the wait() and notify() functions to initialize a Window, then wait() on a condition that # is referenced in a button press callback (the button clicked callback will call notify() on that condition) -const _current_plots_version = v"0.9.4" +const _current_plots_version = v"0.9.6" function image_comparison_tests(pkg::Symbol, idx::Int; debug = false, popup = isinteractive(), sigma = [1,1], eps = 1e-2) diff --git a/test/runtests.jl b/test/runtests.jl index eee6acbf..ca7f5e3e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,7 +67,7 @@ facts("UnicodePlots") do @fact backend() --> Plots.UnicodePlotsBackend() # lets just make sure it runs without error - @fact isa(plot(rand(10)), Plot) --> true + @fact isa(plot(rand(10)), Plots.Plot) --> true end @@ -75,7 +75,7 @@ end facts("Axes") do p = plot() axis = p.subplots[1][:xaxis] - @fact typeof(axis) --> Axis + @fact typeof(axis) --> Plots.Axis @fact Plots.discrete_value!(axis, "HI") --> (0.5, 1) @fact Plots.discrete_value!(axis, :yo) --> (1.5, 2) @fact extrema(axis) --> (0.5,1.5)