diff --git a/.travis.yml b/.travis.yml index 1253ee9a..bbeac50e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ os: - linux # - osx julia: - - 0.5 + - 0.6 matrix: allow_failures: - julia: nightly diff --git a/NEWS.md b/NEWS.md index c1c98498..930df8b0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,14 +3,261 @@ #### notes on release changes, ongoing development, and future planned work -- All new development should target 0.9! -- Minor version 0.8 is the last one to support Julia 0.4!! +- Minor version 0.17 is the last one to support Julia 0.6!! +- Minor version 0.11 is the last one to support Julia 0.5!! - Critical bugfixes only - - `backports` branch is for Julia 0.4 + - `backports` branch is for Julia 0.5 --- +## (current master) +- All new development should target Julia 0.7! + +## 0.17.4 +- fix thickness_scaling for pyplot + +## 0.17.3 +- Log-scale heatmap edge computation +- Fix size and dpi for GR and PyPlot +- Fix fillrange with line segments on PyPlot and Plotly +- fix flip for heatmap and image on GR +- New attributes for PGFPlots +- Widen axes for most series types and log scales +- Plotly: fix log scale with no ticks +- Fix axis flip on Plotly +- Fix hover and zcolor interaction in Plotly +- WebIO integration for PlotlyJS backend + +## 0.17.2 +- fix single subplot in plotly +- implement `(xyz)lims = :round` +- PyPlot: fix bg_legend = invisible() +- set fallback tick specification for axes with discrete values +- restructure of show methods + +## 0.17.1 +- Fix contour for PGFPlots +- 32Bit fix: Int64 -> Int +- Make series of shapes and segments toggle together in Plotly(JS) +- Fix marker arguments +- Fix processing order of series recipes +- Fix Plotly(JS) ribbon +- Contour plots with x,y in grid form on PyPlot + +## 0.17.0 +- Add GR dependency to make it the default backend +- Improve histogram2d bin estimation +- Allow vector arguments for certain series attributes and support line_z and fill_z on GR, PyPlot, Plotly(JS) and PGFPlots +- Automatic scientific notation for tick labels +- Allow to set the theme in PLOTS_DEFAULTS +- Implement plots_heatmap seriestype providing a Plots recipe for heatmaps + +## 0.16.0 +- fix 3D plotting in PyPlot +- Infinite objects + +## 0.15.1 + +- fix scientific notation for labels in GR +- fix labels with logscale +- fix image cropping with GR +- fix grouping of annotations +- fix annotations in Plotly +- allow saving notebook with plots as pdf from IJulia +- fix fillrange and ribbon for step recipes +- implement native ticks that respond to zoom +- fix bar plot with one bar +- contour labels and colorbar fixes +- interactive linked axis for PyPlot +- add `NamedTuple` syntax to group with named legend +- use bar recipe in Plotly +- implement categorical ticks + +## 0.15.0 + +- improve resolution of png output of GR with savefig() +- add check for ticks=nothing +- allow transparency in heatmaps +- fix line_z for GR +- fix legendcolor for pyplot +- fix pyplot ignoring alpha values of images +- don't let `abline!` change subplot limits +- update showtheme recipe + +## 0.14.2 + +- fix plotly bar lines bug +- allow passing multiple series to `ribbon` +- add a new example for `line_z` + +## 0.14.1 + +- Add linestyle argument to the legend +- Plotly: bar_width and stroke_width support for bar plots +- abline! does not change axis limits +- Fix default log scale ticks in GR backend +- Use the :fontsize keys so the scalefontsizes command works +- Prepare support for new PlotTheme type in PlotThemes + +## 0.14.0 + +- remove use of imagemagick; saving gifs now requires ffmpeg +- improvements to ffmpeg gif quality and speed +- overhaul of fonts, allows setting fonts in recipes and with magic arguments +- added `camera` attribute to control camera position for 3d plots +- added `showaxis` attribute to control which axes to display +- improvements of polar plots axes, and better backend consistency +- changed the 'spy' recipe back to using heatmap +- added `scatterpath` seriestype +- allow plotlyjs to save svg +- add `reset_defaults()` function to reset plot defaults +- update syntax to 0.6 +- make `fill = true` fill to 0 rather than to 1 +- use new `@df` syntax in StatPlots examples +- allow changing the color of legend box +- implement `title_location` for gr +- add `hline` marker to pgfplots - fixes errorbars +- pyplot legends now show marker types +- pyplot colorbars take font style from y axis +- pyplot tickmarks color the same as axis color +- allow setting linewidth for contour in gr +- allow legend to be outside plot area for pgfplots +- expand axis extrema for heatmap +- extendg grid lines to axis limits +- fix `line_z` for pyplot and gr +- fixed colorbar problem for flipped axes with gr +- fix marker_z for 3d plots in gr +- fix `weights` functionality for histograms +- fix gr annotations with colorbar +- fix aspect ratio in gr +- fix "hidden window" problem after savefig in gr +- fix pgfplots logscale ticks error +- fix pgfplots legends symbols +- fix axis linking for plotlyjs +- fix plotting of grayscale images + +## 0.13.1 + +- fix a bug when passing a vector of functions with no bounds (e.g. `plot([sin, cos])`) +- export `pct` and `px` from Plots.PlotMeasures + +## 0.13.0 + +- support `plotattributes` rather than `d` in recipes +- no longer export `w`, `h` and names from Measures.jl; use `using Plots.PlotMeasures` to get these names back +- `bar_width` now depends on the minimum distance between bars, not the mean +- better automatic x axis limits for plotting Functions +- `tick_direction` attribute now allows ticks to be on the inside of the plot border +- removed a bug where `p1 = plot(randn(10)); plot(p1, p2)` made `display(p1)` impossible +- allow `plot([])` to generate an empty plot +- add `origin` framestyle +- ensure finite bin number on histograms with only one unique value +- better automatic histogram bins for 2d histograms +- more informative error message on passing unsupported seriestype in a recipe +- allow grouping in user recipes +- GR now has `line_z` and `fill_z` attributes for determining the color of shapes and lines +- change GR default view angle for 3D plots to match that of PyPlot +- fix `clims` on GR +- fix `marker_z` for plotly backend +- implement `framestyle` for plotly +- fix logscale bug error for values < 1e-16 on pyplot +- fix an issue on pyplot where >1 colorbar would be shown if there was >1 series +- fix `writemime` for eps + +## 0.12.4 + +- added a new `framestyle` argument with choices: :box, :semi, :axes, :grid and :none +- changed the default bar width to 0.8 +- added working ribbon to plotly backend +- ensure that automatic ticks always generate 4 to 8 ticks +- group now groups keyword arguments of the same length as the input +- allow passing DateTime objects as ticks +- allow specifying the number of ticks as an integre +- fix bug on errorbars in gr +- fixed some but not all world age issues +- better margin with room for text +- added a `match` option for linecolor +- better error message un unsupported series types +- add a 'stride' keyword for the pyplot backend + +## 0.12.3 + +- new grid line style defaults +- `grid` is now an axis attribute and a magic argument: it is now possible to modify the grid line style, alpha and line width +- Enforce plot order in user recipes +- import `plot!` from RecipesBase +- GR no longer automatically handles _ and ^ in texts +- fix GR colorbar for scatter plots + +#### 0.12.2 + +- fix an issue with Juno/PlotlyJS compatibility on new installations +- fix markers not showing up in seriesrecipes using :scatter +- don't use pywrap in the pyplot backend +- improve the bottom margin for the gr backend + +#### 0.12.1 + +- fix deprecation warnings +- switch from FixedSizeArrays to StaticArrays.FixedSizeArrays +- drop FactCheck in tests +- remove julia 0.5 compliant uses of transpose operator +- fix GR heatmap bugs +- fix GR guide padding +- improve legend markers in GR +- add surface alpha for Plotly(JS) +- add fillrange to Plotly(JS) +- allow usage of Matplotlib 1.5 with PyPlot +- fix GLVisualize for julia 0.6 +- conform to changes in InspectDR + +#### 0.12.0 + +- 0.6 only + +#### 0.11.3 + +- add HDF5 backend +- GR replaces PyPlot as first-choice backend +- support for legend position in GR +- smaller markers in GR +- better viewport size in GR +- fix glvisualize support +- remove bug with three-argument method of `text` +- `legendtitle` attribute added +- add test for `spy` + +#### 0.11.0 + +- julia 0.6 compatibility +- matplotlib 2.0 compatibility +- add inspectdr backend +- improved histogram functionality: +- added a `:stephist` and `:scatterhist` series type as well as ``:barhist` (the default) +- support for log scale axes with histograms +- support for plotting `StatsBase.Histogram` +- allowing bins to be specified as `:sturges`, `:rice`, `:scott` or :fd +- allow `normalization` to be specified as :density (for unequal bins) or :pdf (sum to 1) +- add a `plotattr` function to access documentation for Plots attribute +- add `fill_z` attribute for pyplot +- add colorbar_title to plotlyjs +- enable standalone window for plotlyjs +- improved support for pgfplots, ticks rotation, clims, series_annotations +- restore colorbars for GR +- better axis labels for heatmap in GR +- better marker sizes in GR +- fix color representation in GR +- update GR legend +- fix image bug on GR +- fix glvisualize dependencies +- set dotted grid lines for pyplot +- several improvements to inspectdr +- improved tick positions for TimeType x axes +- support for improved color gradient capability in PlotUtils +- add a showlibrary recipe to display color libraries +- add a showgradient recipe to display color gradients +- add `vectorfield` as an alias for `quiver` +- use `PlotUtils.adaptedgrid` for functions -## 0.9 (current master/dev) #### 0.9.5 @@ -331,7 +578,7 @@ - z-axis keywords - 3D indexing overhaul: `push!`, `append!` support - matplotlib colormap constants (`:inferno` is the new default colormap for Plots) -- `typealias KW Dict{Symbol,Any}` used in place of splatting in many places +- `const KW = Dict{Symbol,Any}` used in place of splatting in many places - png generation for plotly backend using wkhtmltoimage - `normalize` and `weights` keywords - background/foreground subcategories for fine-tuning of looks diff --git a/README.md b/README.md index a47da192..65116717 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # Plots -[![Build Status](https://travis-ci.org/tbreloff/Plots.jl.svg?branch=master)](https://travis-ci.org/tbreloff/Plots.jl) +[![Build Status](https://travis-ci.org/JuliaPlots/Plots.jl.svg?branch=master)](https://travis-ci.org/JuliaPlots/Plots.jl) +[![Build status](https://ci.appveyor.com/api/projects/status/github/juliaplots/plots.jl?branch=master&svg=true)](https://ci.appveyor.com/project/mkborregaard/plots-jl) [![Join the chat at https://gitter.im/tbreloff/Plots.jl](https://badges.gitter.im/tbreloff/Plots.jl.svg)](https://gitter.im/tbreloff/Plots.jl?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -#### Author: Thomas Breloff (@tbreloff) +#### Created by Tom Breloff (@tbreloff) + +#### Maintained by the [JuliaPlot members](https://github.com/orgs/JuliaPlots/people) Plots is a plotting API and toolset. My goals with the package are: @@ -19,4 +22,4 @@ Plots is a plotting API and toolset. My goals with the package are: - **Lightweight**. Very few dependencies. - **Smart**. Attempts to figure out what you **want** it to do... not just what you **tell** it. -View the [full documentation](http://juliaplots.github.io). +View the [full documentation](http://docs.juliaplots.org/latest). diff --git a/REQUIRE b/REQUIRE index 67e5f781..c256c0a6 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,9 +1,16 @@ -julia 0.5 +julia 0.6 -RecipesBase -PlotUtils -PlotThemes +RecipesBase 0.2.3 +PlotUtils 0.4.1 +PlotThemes 0.1.3 Reexport -FixedSizeArrays +StaticArrays 0.5 +FixedPointNumbers 0.3 Measures Showoff +StatsBase 0.14.0 +JSON +NaNMath +Requires +Contour +GR 0.31.0 diff --git a/appveyor.yml b/appveyor.yml index 21481951..81d2e51f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,15 @@ environment: matrix: - - JULIAVERSION: "julialang/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe" - - JULIAVERSION: "julialang/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe" - - JULIAVERSION: "julianightlies/bin/winnt/x86/julia-latest-win32.exe" - - JULIAVERSION: "julianightlies/bin/winnt/x64/julia-latest-win64.exe" + - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" + - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" + - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" + - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" + +matrix: + allow_failures: + - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" #check and address + - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" + - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" notifications: - provider: Email @@ -12,13 +18,14 @@ notifications: on_build_status_changed: false install: + - ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12" # If there's a newer build queued for the same PR, cancel this one - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` throw "There are newer queued builds for this pull request, failing early." } # Download most recent Julia Windows binary - - ps: (new-object net.webclient).DownloadFile($("http://s3.amazonaws.com/"+$env:JULIAVERSION), "C:\projects\julia-binary.exe") + - ps: (new-object net.webclient).DownloadFile($env:JULIA_URL, "C:\projects\julia-binary.exe") # Run installer silently, output to C:\projects\julia - C:\projects\julia-binary.exe /S /D=C:\projects\julia diff --git a/src/Plots.jl b/src/Plots.jl index 3515e75b..ea7ff92d 100644 --- a/src/Plots.jl +++ b/src/Plots.jl @@ -1,14 +1,22 @@ -__precompile__(false) +__precompile__(true) module Plots using Reexport -using FixedSizeArrays + +import StaticArrays +using StaticArrays.FixedSizeArrays + @reexport using RecipesBase +import RecipesBase: plot, plot!, animate using Base.Meta @reexport using PlotUtils @reexport using PlotThemes import Showoff +import StatsBase +import JSON + +using Requires export grid, @@ -29,9 +37,6 @@ export with, twinx, - @userplot, - @shorthands, - pie, pie!, plot3d, @@ -50,6 +55,8 @@ export yflip!, xaxis!, yaxis!, + xgrid!, + ygrid!, xlims, ylims, @@ -99,15 +106,50 @@ export center, P2, P3, - BezierCurve + BezierCurve, + + plotattr + +# --------------------------------------------------------- + +import NaNMath # define functions that ignores NaNs. To overcome the destructive effects of https://github.com/JuliaLang/julia/pull/12563 +ignorenan_minimum(x::AbstractArray{F}) where {F<:AbstractFloat} = NaNMath.minimum(x) +ignorenan_minimum(x) = Base.minimum(x) +ignorenan_maximum(x::AbstractArray{F}) where {F<:AbstractFloat} = NaNMath.maximum(x) +ignorenan_maximum(x) = Base.maximum(x) +ignorenan_mean(x::AbstractArray{F}) where {F<:AbstractFloat} = NaNMath.mean(x) +ignorenan_mean(x) = Base.mean(x) +ignorenan_extrema(x::AbstractArray{F}) where {F<:AbstractFloat} = NaNMath.extrema(x) +ignorenan_extrema(x) = Base.extrema(x) + +# --------------------------------------------------------- + +# to cater for block matrices, Base.transpose is recursive. +# This makes it impossible to create row vectors of String and Symbol with the transpose operator. +# This solves this issue, internally in Plots at least. + + +# commented out on the insistence of the METADATA maintainers + +#Base.transpose(x::Symbol) = x +#Base.transpose(x::String) = x # --------------------------------------------------------- +import Measures +module PlotMeasures import Measures import Measures: Length, AbsoluteLength, Measure, BoundingBox, mm, cm, inch, pt, width, height, w, h -typealias BBox Measures.Absolute2DBox -export BBox, BoundingBox, mm, cm, inch, pt, px, pct, w, h +const BBox = Measures.Absolute2DBox +# allow pixels and percentages +const px = AbsoluteLength(0.254) +const pct = Length{:pct, Float64}(1.0) +export BBox, BoundingBox, mm, cm, inch, px, pct, pt, w, h +end + +using .PlotMeasures +import .PlotMeasures: Length, AbsoluteLength, Measure, width, height # --------------------------------------------------------- include("types.jl") @@ -115,7 +157,6 @@ include("utils.jl") include("components.jl") include("axes.jl") include("args.jl") -include("backends.jl") include("themes.jl") include("plot.jl") include("pipeline.jl") @@ -124,34 +165,31 @@ include("layouts.jl") include("subplots.jl") include("recipes.jl") include("animation.jl") -include("output.jl") include("examples.jl") include("arg_desc.jl") - +include("plotattr.jl") +include("backends.jl") +include("output.jl") # --------------------------------------------------------- -# define and export shorthand plotting method definitions -macro shorthands(funcname::Symbol) - funcname2 = Symbol(funcname, "!") - esc(quote - export $funcname, $funcname2 - $funcname(args...; kw...) = plot(args...; kw..., seriestype = $(quot(funcname))) - $funcname2(args...; kw...) = plot!(args...; kw..., seriestype = $(quot(funcname))) - end) -end - @shorthands scatter @shorthands bar @shorthands barh @shorthands histogram +@shorthands barhist +@shorthands stephist +@shorthands scatterhist @shorthands histogram2d @shorthands density @shorthands heatmap +@shorthands plots_heatmap @shorthands hexbin @shorthands sticks @shorthands hline @shorthands vline +@shorthands hspan +@shorthands vspan @shorthands ohlc @shorthands contour @shorthands contourf @@ -165,52 +203,86 @@ end @shorthands quiver @shorthands curves +"Plot a pie diagram" pie(args...; kw...) = plot(args...; kw..., seriestype = :pie, aspect_ratio = :equal, grid=false, xticks=nothing, yticks=nothing) pie!(args...; kw...) = plot!(args...; kw..., seriestype = :pie, aspect_ratio = :equal, grid=false, xticks=nothing, yticks=nothing) + +"Plot with seriestype :path3d" plot3d(args...; kw...) = plot(args...; kw..., seriestype = :path3d) plot3d!(args...; kw...) = plot!(args...; kw..., seriestype = :path3d) - +"Add title to an existing plot" title!(s::AbstractString; kw...) = plot!(; title = s, kw...) + +"Add xlabel to an existing plot" xlabel!(s::AbstractString; kw...) = plot!(; xlabel = s, kw...) + +"Add ylabel to an existing plot" ylabel!(s::AbstractString; kw...) = plot!(; ylabel = s, kw...) -xlims!{T<:Real,S<:Real}(lims::Tuple{T,S}; kw...) = plot!(; xlims = lims, kw...) -ylims!{T<:Real,S<:Real}(lims::Tuple{T,S}; kw...) = plot!(; ylims = lims, kw...) -zlims!{T<:Real,S<:Real}(lims::Tuple{T,S}; kw...) = plot!(; zlims = lims, kw...) + +"Set xlims for an existing plot" +xlims!(lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = plot!(; xlims = lims, kw...) + +"Set ylims for an existing plot" +ylims!(lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = plot!(; ylims = lims, kw...) + +"Set zlims for an existing plot" +zlims!(lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = plot!(; zlims = lims, kw...) + xlims!(xmin::Real, xmax::Real; kw...) = plot!(; xlims = (xmin,xmax), kw...) ylims!(ymin::Real, ymax::Real; kw...) = plot!(; ylims = (ymin,ymax), kw...) zlims!(zmin::Real, zmax::Real; kw...) = plot!(; zlims = (zmin,zmax), kw...) -xticks!{T<:Real}(v::AVec{T}; kw...) = plot!(; xticks = v, kw...) -yticks!{T<:Real}(v::AVec{T}; kw...) = plot!(; yticks = v, kw...) -xticks!{T<:Real,S<:AbstractString}( - ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(; xticks = (ticks,labels), kw...) -yticks!{T<:Real,S<:AbstractString}( - ticks::AVec{T}, labels::AVec{S}; kw...) = plot!(; yticks = (ticks,labels), kw...) + + +"Set xticks for an existing plot" +xticks!(v::AVec{T}; kw...) where {T<:Real} = plot!(; xticks = v, kw...) + +"Set yticks for an existing plot" +yticks!(v::AVec{T}; kw...) where {T<:Real} = plot!(; yticks = v, kw...) + +xticks!( +ticks::AVec{T}, labels::AVec{S}; kw...) where {T<:Real,S<:AbstractString} = plot!(; xticks = (ticks,labels), kw...) +yticks!( +ticks::AVec{T}, labels::AVec{S}; kw...) where {T<:Real,S<:AbstractString} = plot!(; yticks = (ticks,labels), kw...) + +"Add annotations to an existing plot" annotate!(anns...; kw...) = plot!(; annotation = anns, kw...) -annotate!{T<:Tuple}(anns::AVec{T}; kw...) = plot!(; annotation = anns, kw...) +annotate!(anns::AVec{T}; kw...) where {T<:Tuple} = plot!(; annotation = anns, kw...) + +"Flip the current plots' x axis" xflip!(flip::Bool = true; kw...) = plot!(; xflip = flip, kw...) + +"Flip the current plots' y axis" yflip!(flip::Bool = true; kw...) = plot!(; yflip = flip, kw...) + +"Specify x axis attributes for an existing plot" xaxis!(args...; kw...) = plot!(; xaxis = args, kw...) + +"Specify x axis attributes for an existing plot" yaxis!(args...; kw...) = plot!(; yaxis = args, kw...) +xgrid!(args...; kw...) = plot!(; xgrid = args, kw...) +ygrid!(args...; kw...) = plot!(; ygrid = 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, lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = plot!(plt; xlims = lims, kw...) + ylims!(plt::PlotOrSubplot, lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = plot!(plt; ylims = lims, kw...) + zlims!(plt::PlotOrSubplot, lims::Tuple{T,S}; kw...) where {T<:Real,S<:Real} = 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...) + xticks!(plt::PlotOrSubplot, ticks::AVec{T}; kw...) where {T<:Real} = plot!(plt; xticks = ticks, kw...) + yticks!(plt::PlotOrSubplot, ticks::AVec{T}; kw...) where {T<:Real} = plot!(plt; yticks = ticks, kw...) + xticks!(plt::PlotOrSubplot, + ticks::AVec{T}, labels::AVec{S}; kw...) where {T<:Real,S<:AbstractString} = plot!(plt; xticks = (ticks,labels), kw...) + yticks!(plt::PlotOrSubplot, + ticks::AVec{T}, labels::AVec{S}; kw...) where {T<:Real,S<:AbstractString} = plot!(plt; yticks = (ticks,labels), kw...) + xgrid!(plt::PlotOrSubplot, args...; kw...) = plot!(plt; xgrid = args, kw...) + ygrid!(plt::PlotOrSubplot, args...; kw...) = plot!(plt; ygrid = args, 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...) + annotate!(plt::PlotOrSubplot, anns::AVec{T}; kw...) where {T<:Tuple} = 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...) @@ -222,13 +294,14 @@ end const CURRENT_BACKEND = CurrentBackend(:none) -function __init__() - setup_ijulia() - setup_atom() - +# for compatibility with Requires.jl: +@init begin if isdefined(Main, :PLOTS_DEFAULTS) + if haskey(Main.PLOTS_DEFAULTS, :theme) + theme(Main.PLOTS_DEFAULTS[:theme]) + end for (k,v) in Main.PLOTS_DEFAULTS - default(k, v) + k == :theme || default(k, v) end end end diff --git a/src/animation.jl b/src/animation.jl index ee7428df..6c1f880f 100644 --- a/src/animation.jl +++ b/src/animation.jl @@ -1,5 +1,5 @@ - -immutable Animation +"Represents an animation object" +struct Animation dir::String frames::Vector{String} end @@ -9,7 +9,12 @@ function Animation() Animation(tmpdir, String[]) end -function frame{P<:AbstractPlot}(anim::Animation, plt::P=current()) +""" + frame(animation[, plot]) + +Add a plot (the current plot if not specified) to an existing animation +""" +function frame(anim::Animation, plt::P=current()) where P<:AbstractPlot i = length(anim.frames) + 1 filename = @sprintf("%06d.png", i) png(plt, joinpath(anim.dir, filename)) @@ -20,7 +25,7 @@ giffn() = (isijulia() ? "tmp.gif" : tempname()*".gif") movfn() = (isijulia() ? "tmp.mov" : tempname()*".mov") mp4fn() = (isijulia() ? "tmp.mp4" : tempname()*".mp4") -type FrameIterator +mutable struct FrameIterator itr every::Int kw @@ -49,46 +54,40 @@ end # ----------------------------------------------- "Wraps the location of an animated gif so that it can be displayed" -immutable AnimatedGif +struct AnimatedGif filename::String end file_extension(fn) = Base.Filesystem.splitext(fn)[2][2:end] 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...) +mov(anim::Animation, fn = movfn(); kw...) = buildanimation(anim.dir, fn, false; kw...) +mp4(anim::Animation, fn = mp4fn(); kw...) = buildanimation(anim.dir, fn, false; kw...) -const _imagemagick_initialized = Ref(false) -function buildanimation(animdir::AbstractString, fn::AbstractString; - fps::Integer = 20, loop::Integer = 0) +function buildanimation(animdir::AbstractString, fn::AbstractString, + is_animated_gif::Bool=true; + fps::Integer = 20, loop::Integer = 0, + variable_palette::Bool=false, + show_msg::Bool=true) 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 + + if is_animated_gif + if variable_palette + # generate a colorpalette for each frame for highest quality, but larger filesize + palette="palettegen=stats_mode=single[pal],[0:v][pal]paletteuse=new=1" + run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -lavfi "$palette" -y $fn`) + else + # generate a colorpalette first so ffmpeg does not have to guess it + run(`ffmpeg -v 0 -i $(animdir)/%06d.png -vf "palettegen=stats_mode=diff" -y "$(animdir)/palette.bmp"`) + # then apply the palette to get better results + run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -i "$(animdir)/palette.bmp" -lavfi "paletteuse=dither=sierra2_4a" -y $fn`) 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"`) + else + run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -pix_fmt yuv420p -y $fn`) end - info("Saved animation to ", fn) + show_msg && info("Saved animation to ", fn) AnimatedGif(fn) end @@ -117,6 +116,7 @@ function _animate(forloop::Expr, args...; callgif = false) # add the call to frame to the end of each iteration animsym = gensym("anim") countersym = gensym("counter") + freqassert = :() block = forloop.args[2] # create filter @@ -129,7 +129,7 @@ function _animate(forloop::Expr, args...; callgif = false) # filter every `freq` frames (starting with the first frame) @assert n == 2 freq = args[2] - @assert isa(freq, Integer) && freq > 0 + freqassert = :(@assert isa($freq, Integer) && $freq > 0) :(mod1($countersym, $freq) == 1) elseif args[1] == :when @@ -149,6 +149,7 @@ function _animate(forloop::Expr, args...; callgif = false) # full expression: esc(quote + $freqassert # if filtering, check frequency is an Integer > 0 $animsym = Animation() # init animation object $countersym = 1 # init iteration counter $forloop # for loop, saving a frame after each iteration diff --git a/src/arg_desc.jl b/src/arg_desc.jl index 4efbf1af..56e3719d 100644 --- a/src/arg_desc.jl +++ b/src/arg_desc.jl @@ -21,7 +21,7 @@ const _arg_desc = KW( :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 `: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.", +:bins => "Integer, NTuple{2,Integer}, AbstractVector or Symbol. Default is :auto (the Freedman-Diaconis rule). For histogram-types, defines the approximate number of bins to aim for, or the auto-binning algorithm to use (:sturges, :sqrt, :rice, :scott or :fd). For fine-grained control pass a Vector of break values, e.g. `linspace(extrema(x)..., 25)`", :smooth => "Bool. Add a regression line?", :group => "AbstractVector. Data is split into a separate series, one for each unique value in `group`.", :x => "Various. Input data. First Dimension", @@ -40,9 +40,10 @@ const _arg_desc = KW( :ribbon => "Number or AbstractVector. Creates a fillrange around the data points.", :quiver => "AbstractVector or 2-Tuple of vectors. The directional vectors U,V which specify velocity/gradient vectors for a quiver plot.", :arrow => "nothing (no arrows), Bool (if true, default arrows), Arrow object, or arg(s) that could be style or head length/widths. Defines arrowheads that should be displayed at the end of path line segments (just before a NaN and the last non-NaN point). Used in quiverplot, streamplot, or similar.", -:normalize => "Bool. Should normalize histogram types? Trying for area == 1.", +:normalize => "Bool or Symbol. Histogram normalization mode. Possible values are: false/:none (no normalization, default), true/:pdf (normalize to a discrete Probability Density Function, where the total area of the bins is 1), :probability (bin heights sum to 1) and :density (the area of each bin, rather than the height, is equal to the counts - useful for uneven bin sizes).", :weights => "AbstractVector. Used in histogram types for weighted counts.", :contours => "Bool. Add contours to the side-grids of 3D plots? Used in surface/wireframe.", +:contour_labels => "Bool. Show labels at the contour lines?", :match_dimensions => "Bool. For heatmap types... should the first dimension of a matrix (rows) correspond to the first dimension of the plot (x-axis)? The default is false, which matches the behavior of Matplotlib, Plotly, and others. Note: when passing a function for z, the function should still map `(x,y) -> z`.", :subplot => "Integer (subplot index) or Subplot object. The subplot that this series belongs to.", :series_annotations => "AbstractVector of String or PlotText. These are annotations which are mapped to data points/positions.", @@ -64,26 +65,37 @@ const _arg_desc = KW( :html_output_format => "Symbol. When writing html output, what is the format? `:png` and `:svg` are currently supported.", :inset_subplots => "nothing or vector of 2-tuple (parent,bbox). optionally pass a vector of (parent,bbox) tuples which are the parent layout and the relative bounding box of inset subplots", :dpi => "Number. Dots Per Inch of output figures", +:thickness_scaling => "Number. Scale for the thickness of all line elements like lines, borders, axes, grid lines, ... defaults to 1.", :display_type => "Symbol (`:auto`, `:gui`, or `:inline`). When supported, `display` will either open a GUI window or plot inline.", :extra_kwargs => "KW (Dict{Symbol,Any}). Pass a map of extra keyword args which may be specific to a backend.", +:fontfamily => "String or Symbol. Default font family for title, legend entries, tick labels and guides", # subplot args :title => "String. Subplot title.", :title_location => "Symbol. Position of subplot title. Values: `:left`, `:center`, `:right`", -:titlefont => "Font. Font of subplot title.", +:titlefontfamily => "String or Symbol. Font family of subplot title.", +:titlefontsize => "Integer. Font pointsize of subplot title.", +:titlefonthalign => "Symbol. Font horizontal alignment of subplot title: :hcenter, :left, :right or :center", +:titlefontvalign => "Symbol. Font vertical alignment of subplot title: :vcenter, :top, :bottom or :center", +:titlefontrotation => "Real. Font rotation of subplot title", +:titlefontcolor => "Color Type. Font color of subplot title", :background_color_subplot => "Color Type or `:match` (matches `:background_color`). Base background color of the subplot.", :background_color_legend => "Color Type or `:match` (matches `:background_color_subplot`). Background color of the legend.", :background_color_inside => "Color Type or `:match` (matches `:background_color_subplot`). Background color inside the plot area (under the grid).", :foreground_color_subplot => "Color Type or `:match` (matches `:foreground_color`). Base foreground color of the subplot.", :foreground_color_legend => "Color Type or `:match` (matches `:foreground_color_subplot`). Foreground color of the legend.", -:foreground_color_grid => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of grid lines.", :foreground_color_title => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of subplot title.", :color_palette => "Vector of colors (cycle through) or color gradient (generate list from gradient) or `:auto` (generate a color list using `Colors.distiguishable_colors` and custom seed colors chosen to contrast with the background). The color palette is a color list from which series colors are automatically chosen.", :legend => "Bool (show the legend?) or Symbol (legend position). Symbol values: `:none`, `:best`, `:right`, `:left`, `:top`, `:bottom`, `:inside`, `:legend`, `:topright`, `:topleft`, `:bottomleft`, `:bottomright` (note: only some may be supported in each backend)", +:legendfontfamily => "String or Symbol. Font family of legend entries.", +:legendfontsize => "Integer. Font pointsize of legend entries.", +:legendfonthalign => "Symbol. Font horizontal alignment of legend entries: :hcenter, :left, :right or :center", +:legendfontvalign => "Symbol. Font vertical alignment of legend entries: :vcenter, :top, :bottom or :center", +:legendfontrotation => "Real. Font rotation of legend entries", +:legendfontcolor => "Color Type. Font color of legend entries", :colorbar => "Bool (show the colorbar?) or Symbol (colorbar position). Symbol values: `:none`, `:best`, `:right`, `:left`, `:top`, `:bottom`, `:legend` (matches legend value) (note: only some may be supported in each backend)", :clims => "`:auto` or NTuple{2,Number}. Fixes the limits of the colorbar.", :legendfont => "Font. Font of legend items.", -: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. Plot area is resized so that 1 y-unit is the same size as `apect_ratio` x-units.", @@ -94,21 +106,40 @@ const _arg_desc = KW( :bottom_margin => "Measure (multiply by `mm`, `px`, etc) or `:match` (matches `:margin`). Specifies the extra padding on the bottom of the subplot.", :subplot_index => "Integer. Internal (not set by user). Specifies the index of this subplot in the Plot's `plt.subplot` list.", :colorbar_title => "String. Title of colorbar.", +:framestyle => "Symbol. Style of the axes frame. Choose from $(_allFramestyles)", +:camera => "NTuple{2, Real}. Sets the view angle (azimuthal, elevation) for 3D plots", # axis args :guide => "String. Axis guide (label).", -:lims => "NTuple{2,Number}. Force axis limits. Only finite values are used (you can set only the right limit with `xlims = (-Inf, 2)` for example).", +:lims => "NTuple{2,Number} or Symbol. Force axis limits. Only finite values are used (you can set only the right limit with `xlims = (-Inf, 2)` for example). `:round` widens the limit to the nearest round number ie. [0.1,3.6]=>[0.0,4.0]", :ticks => "Vector of numbers (set the tick values), Tuple of (tickvalues, ticklabels), or `:auto`", :scale => "Symbol. Scale of the axis: `:none`, `:ln`, `:log2`, `:log10`", :rotation => "Number. Degrees rotation of tick labels.", :flip => "Bool. Should we flip (reverse) the axis?", :formatter => "Function, :scientific, or :auto. A method which converts a number to a string for tick labeling.", -:tickfont => "Font. Font of axis tick labels.", -:guidefont => "Font. Font of axis guide (label).", +:tickfontfamily => "String or Symbol. Font family of tick labels.", +:tickfontsize => "Integer. Font pointsize of tick labels.", +:tickfonthalign => "Symbol. Font horizontal alignment of tick labels: :hcenter, :left, :right or :center", +:tickfontvalign => "Symbol. Font vertical alignment of tick labels: :vcenter, :top, :bottom or :center", +:tickfontrotation => "Real. Font rotation of tick labels", +:tickfontcolor => "Color Type. Font color of tick labels", +:guidefontfamily => "String or Symbol. Font family of axes guides.", +:guidefontsize => "Integer. Font pointsize of axes guides.", +:guidefonthalign => "Symbol. Font horizontal alignment of axes guides: :hcenter, :left, :right or :center", +:guidefontvalign => "Symbol. Font vertical alignment of axes guides: :vcenter, :top, :bottom or :center", +:guidefontrotation => "Real. Font rotation of axes guides", +:guidefontcolor => "Color Type. Font color of axes guides", :foreground_color_axis => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of axis ticks.", :foreground_color_border => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of plot area border (spines).", :foreground_color_text => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of tick labels.", :foreground_color_guide => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of axis guides (axis labels).", :mirror => "Bool. Switch the side of the tick labels (right or top).", - +:grid => "Bool, Symbol, String or `nothing`. Show the grid lines? `true`, `false`, `:show`, `:hide`, `:yes`, `:no`, `:x`, `:y`, `:z`, `:xy`, ..., `:all`, `:none`, `:off`", +:foreground_color_grid => "Color Type or `:match` (matches `:foreground_color_subplot`). Color of grid lines.", +:gridalpha => "Number in [0,1]. The alpha/opacity override for the grid lines.", +:gridstyle => "Symbol. Style of the grid lines. Choose from $(_allStyles)", +:gridlinewidth => "Number. Width of the grid lines (in pixels)", +:tick_direction => "Symbol. Direction of the ticks. `:in` or `:out`", +:showaxis => "Bool, Symbol or String. Show the axis. `true`, `false`, `:show`, `:hide`, `:yes`, `:no`, `:x`, `:y`, `:z`, `:xy`, ..., `:all`, `:off`", +:widen => "Bool. Widen the axis limits by a small factor to avoid cut-off markers and lines at the borders. Defaults to `true`.", ) diff --git a/src/args.jl b/src/args.jl index 8f82fec0..671485d0 100644 --- a/src/args.jl +++ b/src/args.jl @@ -35,7 +35,9 @@ const _3dTypes = [ ] const _allTypes = vcat([ :none, :line, :path, :steppre, :steppost, :sticks, :scatter, - :heatmap, :hexbin, :histogram, :histogram2d, :histogram3d, :density, :bar, :hline, :vline, + :heatmap, :hexbin, :barbins, :barhist, :histogram, :scatterbins, + :scatterhist, :stepbins, :stephist, :bins2d, :histogram2d, :histogram3d, + :density, :bar, :hline, :vline, :contour, :pie, :shape, :image ], _3dTypes) @@ -65,6 +67,7 @@ const _typeAliases = Dict{Symbol,Symbol}( :polygon => :shape, :box => :boxplot, :velocity => :quiver, + :vectorfield => :quiver, :gradient => :quiver, :img => :image, :imshow => :image, @@ -77,9 +80,13 @@ const _typeAliases = Dict{Symbol,Symbol}( add_non_underscore_aliases!(_typeAliases) -like_histogram(seriestype::Symbol) = seriestype in (:histogram, :density) -like_line(seriestype::Symbol) = seriestype in (:line, :path, :steppre, :steppost) -like_surface(seriestype::Symbol) = seriestype in (:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image) +const _histogram_like = [:histogram, :barhist, :barbins] +const _line_like = [:line, :path, :steppre, :steppost] +const _surface_like = [:contour, :contourf, :contour3d, :heatmap, :surface, :wireframe, :image] + +like_histogram(seriestype::Symbol) = seriestype in _histogram_like +like_line(seriestype::Symbol) = seriestype in _line_like +like_surface(seriestype::Symbol) = seriestype in _surface_like is3d(seriestype::Symbol) = seriestype in _3dTypes is3d(series::Series) = is3d(series.d) @@ -152,12 +159,75 @@ const _markerAliases = Dict{Symbol,Symbol}( :spike => :vline, ) +const _positionAliases = Dict{Symbol,Symbol}( + :top_left => :topleft, + :tl => :topleft, + :top_center => :topcenter, + :tc => :topcenter, + :top_right => :topright, + :tr => :topright, + :bottom_left => :bottomleft, + :bl => :bottomleft, + :bottom_center => :bottomcenter, + :bc => :bottomcenter, + :bottom_right => :bottomright, + :br => :bottomright, +) + const _allScales = [:identity, :ln, :log2, :log10, :asinh, :sqrt] +const _logScales = [:ln, :log2, :log10] +const _logScaleBases = Dict(:ln => e, :log2 => 2.0, :log10 => 10.0) const _scaleAliases = Dict{Symbol,Symbol}( :none => :identity, :log => :log10, ) +const _allGridSyms = [:x, :y, :z, + :xy, :xz, :yx, :yz, :zx, :zy, + :xyz, :xzy, :yxz, :yzx, :zxy, :zyx, + :all, :both, :on, :yes, :show, + :none, :off, :no, :hide] +const _allGridArgs = [_allGridSyms; string.(_allGridSyms); nothing] +hasgrid(arg::Void, letter) = false +hasgrid(arg::Bool, letter) = arg +function hasgrid(arg::Symbol, letter) + if arg in _allGridSyms + arg in (:all, :both, :on) || contains(string(arg), string(letter)) + else + warn("Unknown grid argument $arg; $(Symbol(letter, :grid)) was set to `true` instead.") + true + end +end +hasgrid(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) + +const _allShowaxisSyms = [:x, :y, :z, + :xy, :xz, :yx, :yz, :zx, :zy, + :xyz, :xzy, :yxz, :yzx, :zxy, :zyx, + :all, :both, :on, :yes, :show, + :off, :no, :hide] +const _allShowaxisArgs = [_allGridSyms; string.(_allGridSyms)] +showaxis(arg::Void, letter) = false +showaxis(arg::Bool, letter) = arg +function showaxis(arg::Symbol, letter) + if arg in _allGridSyms + arg in (:all, :both, :on, :yes) || contains(string(arg), string(letter)) + else + warn("Unknown showaxis argument $arg; $(Symbol(letter, :showaxis)) was set to `true` instead.") + true + end +end +showaxis(arg::AbstractString, letter) = hasgrid(Symbol(arg), letter) + +const _allFramestyles = [:box, :semi, :axes, :origin, :zerolines, :grid, :none] +const _framestyleAliases = Dict{Symbol, Symbol}( + :frame => :box, + :border => :box, + :on => :box, + :transparent => :semi, + :semitransparent => :semi, +) + +const _bar_width = 0.8 # ----------------------------------------------------------------------------- const _series_defaults = KW( @@ -167,7 +237,7 @@ const _series_defaults = KW( :seriestype => :path, :linestyle => :solid, :linewidth => :auto, - :linecolor => :match, + :linecolor => :auto, :linealpha => nothing, :fillrange => nothing, # ribbons, areas, etc :fillcolor => :match, @@ -180,7 +250,7 @@ const _series_defaults = KW( :markerstrokewidth => 1, :markerstrokecolor => :match, :markerstrokealpha => nothing, - :bins => 30, # number of bins for hists + :bins => :auto, # number of bins for hists :smooth => false, # regression line? :group => nothing, # groupby vector :x => nothing, @@ -202,6 +272,7 @@ const _series_defaults = KW( :normalize => false, # do we want a normalized histogram? :weights => nothing, # optional weights for histograms (1D and 2D) :contours => false, # add contours to 3d surface and wireframe plots + :contour_labels => false, :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? @@ -209,6 +280,7 @@ const _series_defaults = KW( :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 + :stride => (1,1), # array stride for wireframe/surface, the first element is the row stride and the second is the column stride. ) @@ -217,6 +289,7 @@ const _plot_defaults = KW( :background_color => colorant"white", # default for all backgrounds, :background_color_outside => :match, # background outside grid, :foreground_color => :auto, # default for all foregrounds, and title color, + :fontfamily => "sans-serif", :size => (600,400), :pos => (0,0), :window_title => "Plots.jl", @@ -228,6 +301,7 @@ const _plot_defaults = KW( :inset_subplots => nothing, # optionally pass a vector of (parent,bbox) tuples which are # the parent layout and the relative bounding box of inset subplots :dpi => DPI, # dots per inch for images, etc + :thickness_scaling => 1, :display_type => :auto, :extra_kwargs => KW(), ) @@ -236,20 +310,30 @@ const _plot_defaults = KW( const _subplot_defaults = KW( :title => "", :title_location => :center, # also :left or :right - :titlefont => font(14), + :fontfamily_subplot => :match, + :titlefontfamily => :match, + :titlefontsize => 14, + :titlefonthalign => :hcenter, + :titlefontvalign => :vcenter, + :titlefontrotation => 0.0, + :titlefontcolor => :match, :background_color_subplot => :match, # default for other bg colors... match takes plot default :background_color_legend => :match, # background of legend :background_color_inside => :match, # background inside grid :foreground_color_subplot => :match, # default for other fg colors... match takes plot default :foreground_color_legend => :match, # foreground of legend - :foreground_color_grid => :match, # grid color :foreground_color_title => :match, # title color :color_palette => :auto, :legend => :best, + :legendtitle => nothing, :colorbar => :legend, :clims => :auto, - :legendfont => font(8), - :grid => true, + :legendfontfamily => :match, + :legendfontsize => 8, + :legendfonthalign => :hcenter, + :legendfontvalign => :vcenter, + :legendfontrotation => 0.0, + :legendfontcolor => :match, :annotations => [], # annotation tuples... list of (x,y,annotation) :projection => :none, # can also be :polar or :3d :aspect_ratio => :none, # choose from :none or :equal @@ -260,6 +344,8 @@ const _subplot_defaults = KW( :bottom_margin => :match, :subplot_index => -1, :colorbar_title => "", + :framestyle => :axes, + :camera => (30,30), ) const _axis_defaults = KW( @@ -270,8 +356,18 @@ const _axis_defaults = KW( :rotation => 0, :flip => false, :link => [], - :tickfont => font(8), - :guidefont => font(11), + :tickfontfamily => :match, + :tickfontsize => 8, + :tickfonthalign => :hcenter, + :tickfontvalign => :vcenter, + :tickfontrotation => 0.0, + :tickfontcolor => :match, + :guidefontfamily => :match, + :guidefontsize => 11, + :guidefonthalign => :hcenter, + :guidefontvalign => :vcenter, + :guidefontrotation => 0.0, + :guidefontcolor => :match, :foreground_color_axis => :match, # axis border/tick colors, :foreground_color_border => :match, # plot area border/spines, :foreground_color_text => :match, # tick text color, @@ -279,6 +375,14 @@ const _axis_defaults = KW( :discrete_values => [], :formatter => :auto, :mirror => false, + :grid => true, + :foreground_color_grid => :match, # grid color + :gridalpha => 0.1, + :gridstyle => :solid, + :gridlinewidth => 0.5, + :tick_direction => :in, + :showaxis => true, + :widen => true, ) const _suppress_warnings = Set{Symbol}([ @@ -330,6 +434,15 @@ const _all_defaults = KW[ _axis_defaults_byletter ] +const _initial_defaults = deepcopy(_all_defaults) +const _initial_axis_defaults = deepcopy(_axis_defaults) + +# to be able to reset font sizes to initial values +const _initial_fontsizes = Dict(:titlefontsize => _subplot_defaults[:titlefontsize], + :legendfontsize => _subplot_defaults[:legendfontsize], + :tickfontsize => _axis_defaults[:tickfontsize], + :guidefontsize => _axis_defaults[:guidefontsize]) + const _all_args = sort(collect(union(map(keys, _all_defaults)...))) RecipesBase.is_key_supported(k::Symbol) = is_attr_supported(k) @@ -390,7 +503,7 @@ add_aliases(:foreground_color_title, :fg_title, :fgtitle, :fgcolor_title, :fg_co add_aliases(:foreground_color_axis, :fg_axis, :fgaxis, :fgcolor_axis, :fg_color_axis, :foreground_axis, :foreground_colour_axis, :fgcolour_axis, :fg_colour_axis, :axiscolor) add_aliases(:foreground_color_border, :fg_border, :fgborder, :fgcolor_border, :fg_color_border, :foreground_border, - :foreground_colour_border, :fgcolour_border, :fg_colour_border, :bordercolor, :border) + :foreground_colour_border, :fgcolour_border, :fg_colour_border, :bordercolor) add_aliases(:foreground_color_text, :fg_text, :fgtext, :fgcolor_text, :fg_color_text, :foreground_text, :foreground_colour_text, :fgcolour_text, :fg_colour_text, :textcolor) add_aliases(:foreground_color_guide, :fg_guide, :fgguide, :fgcolor_guide, :fg_color_guide, :foreground_guide, @@ -402,6 +515,7 @@ add_aliases(:linealpha, :la, :lalpha, :lα, :lineopacity, :lopacity) add_aliases(:markeralpha, :ma, :malpha, :mα, :markeropacity, :mopacity) add_aliases(:markerstrokealpha, :msa, :msalpha, :msα, :markerstrokeopacity, :msopacity) add_aliases(:fillalpha, :fa, :falpha, :fα, :fillopacity, :fopacity) +add_aliases(:gridalpha, :ga, :galpha, :gα, :gridopacity, :gopacity) # series attributes add_aliases(:seriestype, :st, :t, :typ, :linetype, :lt) @@ -434,6 +548,7 @@ add_aliases(:zticks, :ztick) add_aliases(:zrotation, :zrot, :zr) add_aliases(:fill_z, :fillz, :fz, :surfacecolor, :surfacecolour, :sc, :surfcolor, :surfcolour) add_aliases(:legend, :leg, :key) +add_aliases(:legendtitle, :legend_title, :labeltitle, :label_title, :leg_title, :key_title) add_aliases(:colorbar, :cb, :cbar, :colorkey) add_aliases(:clims, :clim, :cbarlims, :cbar_lims, :climits, :color_limits) add_aliases(:smooth, :regression, :reg) @@ -445,7 +560,7 @@ add_aliases(:color_palette, :palette) add_aliases(:overwrite_figure, :clf, :clearfig, :overwrite, :reuse) add_aliases(:xerror, :xerr, :xerrorbar) add_aliases(:yerror, :yerr, :yerrorbar, :err, :errorbar) -add_aliases(:quiver, :velocity, :quiver2d, :gradient) +add_aliases(:quiver, :velocity, :quiver2d, :gradient, :vectorfield) add_aliases(:normalize, :norm, :normed, :normalized) add_aliases(:aspect_ratio, :aspectratio, :axis_ratio, :axisratio, :ratio) add_aliases(:match_dimensions, :transpose, :transpose_z) @@ -456,7 +571,13 @@ add_aliases(:series_annotations, :series_ann, :seriesann, :series_anns, :seriesa add_aliases(:html_output_format, :format, :fmt, :html_format) add_aliases(:orientation, :direction, :dir) add_aliases(:inset_subplots, :inset, :floating) - +add_aliases(:stride, :wirefame_stride, :surface_stride, :surf_str, :str) +add_aliases(:gridlinewidth, :gridwidth, :grid_linewidth, :grid_width, :gridlw, :grid_lw) +add_aliases(:gridstyle, :grid_style, :gridlinestyle, :grid_linestyle, :grid_ls, :gridls) +add_aliases(:framestyle, :frame_style, :frame, :axesstyle, :axes_style, :boxstyle, :box_style, :box, :borderstyle, :border_style, :border) +add_aliases(:tick_direction, :tickdirection, :tick_dir, :tickdir, :tick_orientation, :tickorientation, :tick_or, :tickor) +add_aliases(:camera, :cam, :viewangle, :view_angle) +add_aliases(:contour_labels, :contourlabels, :clabels, :clabs) # add all pluralized forms to the _keyAliases dict for arg in keys(_series_defaults) @@ -475,7 +596,6 @@ end `default(; kw...)` will set the current default value for each key/value pair `default(d, key)` returns the key from d if it exists, otherwise `default(key)` """ - function default(k::Symbol) k = get(_keyAliases, k, k) for defaults in _all_defaults @@ -505,6 +625,8 @@ function default(k::Symbol, v) end function default(; kw...) + kw = KW(kw) + preprocessArgs!(kw) for (k,v) in kw default(k, v) end @@ -514,7 +636,10 @@ function default(d::KW, k::Symbol) get(d, k, default(k)) end - +function reset_defaults() + foreach(merge!, _all_defaults, _initial_defaults) + merge!(_axis_defaults, _initial_axis_defaults) +end # ----------------------------------------------------------------------------- @@ -617,6 +742,9 @@ function processFillArg(d::KW, arg) arg.color == nothing || (d[:fillcolor] = arg.color == :auto ? :auto : plot_color(arg.color)) arg.alpha == nothing || (d[:fillalpha] = arg.alpha) + elseif typeof(arg) <: Bool + d[:fillrange] = arg ? 0 : nothing + # fillrange function elseif allFunctions(arg) d[:fillrange] = arg @@ -625,6 +753,10 @@ function processFillArg(d::KW, arg) elseif allAlphas(arg) d[:fillalpha] = arg + # fillrange provided as vector or number + elseif typeof(arg) <: Union{AbstractArray{<:Real}, Real} + d[:fillrange] = arg + elseif !handleColors!(d, arg, :fillcolor) d[:fillrange] = arg @@ -633,6 +765,68 @@ function processFillArg(d::KW, arg) return end + +function processGridArg!(d::KW, arg, letter) + if arg in _allGridArgs || isa(arg, Bool) + d[Symbol(letter, :grid)] = hasgrid(arg, letter) + + elseif allStyles(arg) + d[Symbol(letter, :gridstyle)] = arg + + elseif typeof(arg) <: Stroke + arg.width == nothing || (d[Symbol(letter, :gridlinewidth)] = arg.width) + arg.color == nothing || (d[Symbol(letter, :foreground_color_grid)] = arg.color in (:auto, :match) ? :match : plot_color(arg.color)) + arg.alpha == nothing || (d[Symbol(letter, :gridalpha)] = arg.alpha) + arg.style == nothing || (d[Symbol(letter, :gridstyle)] = arg.style) + + # linealpha + elseif allAlphas(arg) + d[Symbol(letter, :gridalpha)] = arg + + # linewidth + elseif allReals(arg) + d[Symbol(letter, :gridlinewidth)] = arg + + # color + elseif !handleColors!(d, arg, Symbol(letter, :foreground_color_grid)) + warn("Skipped grid arg $arg.") + + end +end + +function processFontArg!(d::KW, fontname::Symbol, arg) + T = typeof(arg) + if T <: Font + d[Symbol(fontname, :family)] = arg.family + d[Symbol(fontname, :size)] = arg.pointsize + d[Symbol(fontname, :halign)] = arg.halign + d[Symbol(fontname, :valign)] = arg.valign + d[Symbol(fontname, :rotation)] = arg.rotation + d[Symbol(fontname, :color)] = arg.color + elseif arg == :center + d[Symbol(fontname, :halign)] = :hcenter + d[Symbol(fontname, :valign)] = :vcenter + elseif arg in (:hcenter, :left, :right) + d[Symbol(fontname, :halign)] = arg + elseif arg in (:vcenter, :top, :bottom) + d[Symbol(fontname, :valign)] = arg + elseif T <: Colorant + d[Symbol(fontname, :color)] = arg + elseif T <: Symbol || T <: AbstractString + try + d[Symbol(fontname, :color)] = parse(Colorant, string(arg)) + catch + d[Symbol(fontname, :family)] = string(arg) + end + elseif typeof(arg) <: Integer + d[Symbol(fontname, :size)] = arg + elseif typeof(arg) <: Real + d[Symbol(fontname, :rotation)] = convert(Float64, arg) + else + warn("Skipped font arg: $arg ($(typeof(arg)))") + end +end + _replace_markershape(shape::Symbol) = get(_markerAliases, shape, shape) _replace_markershape(shapes::AVec) = map(_replace_markershape, shapes) _replace_markershape(shape) = shape @@ -651,12 +845,13 @@ function preprocessArgs!(d::KW) replaceAliases!(d, _keyAliases) # clear all axis stuff - if haskey(d, :axis) && d[:axis] in (:none, nothing, false) - d[:ticks] = nothing - d[:foreground_color_border] = RGBA(0,0,0,0) - d[:grid] = false - delete!(d, :axis) - end + # if haskey(d, :axis) && d[:axis] in (:none, nothing, false) + # d[:ticks] = nothing + # d[:foreground_color_border] = RGBA(0,0,0,0) + # d[:foreground_color_axis] = RGBA(0,0,0,0) + # d[:grid] = false + # delete!(d, :axis) + # end # for letter in (:x, :y, :z) # asym = Symbol(letter, :axis) # if haskey(d, asym) || d[asym] in (:none, nothing, false) @@ -665,6 +860,13 @@ function preprocessArgs!(d::KW) # end # end + # handle axis args common to all axis + args = pop!(d, :axis, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + process_axis_arg!(d, arg, letter) + end + end # handle axis args for letter in (:x, :y, :z) asym = Symbol(letter, :axis) @@ -676,6 +878,48 @@ function preprocessArgs!(d::KW) end end + # handle grid args common to all axes + args = pop!(d, :grid, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + processGridArg!(d, arg, letter) + end + end + # handle individual axes grid args + for letter in (:x, :y, :z) + gridsym = Symbol(letter, :grid) + args = pop!(d, gridsym, ()) + for arg in wraptuple(args) + processGridArg!(d, arg, letter) + end + end + + # fonts + for fontname in (:titlefont, :legendfont) + args = pop!(d, fontname, ()) + for arg in wraptuple(args) + processFontArg!(d, fontname, arg) + end + end + # handle font args common to all axes + for fontname in (:tickfont, :guidefont) + args = pop!(d, fontname, ()) + for arg in wraptuple(args) + for letter in (:x, :y, :z) + processFontArg!(d, Symbol(letter, fontname), arg) + end + end + end + # handle individual axes font args + for letter in (:x, :y, :z) + for fontname in (:tickfont, :guidefont) + args = pop!(d, Symbol(letter, fontname), ()) + for arg in wraptuple(args) + processFontArg!(d, Symbol(letter, fontname), arg) + end + end + end + # handle line args for arg in wraptuple(pop!(d, :line, ())) processLineArg(d, arg) @@ -694,6 +938,9 @@ function preprocessArgs!(d::KW) delete!(d, :marker) if haskey(d, :markershape) d[:markershape] = _replace_markershape(d[:markershape]) + if d[:markershape] == :none && d[:seriestype] in (:scatter, :scatterbins, :scatterhist, :scatter3d) #the default should be :auto, not :none, so that :none can be set explicitly and would be respected + d[:markershape] = :circle + end elseif anymarker d[:markershape_to_add] = :circle # add it after _apply_recipe end @@ -737,6 +984,11 @@ function preprocessArgs!(d::KW) d[:colorbar] = convertLegendValue(d[:colorbar]) end + # framestyle + if haskey(d, :framestyle) && haskey(_framestyleAliases, d[:framestyle]) + d[:framestyle] = _framestyleAliases[d[:framestyle]] + end + # warnings for moved recipes st = get(d, :seriestype, :path) if st in (:boxplot, :violin, :density) && !isdefined(Main, :StatPlots) @@ -749,28 +1001,49 @@ end # ----------------------------------------------------------------------------- "A special type that will break up incoming data into groups, and allow for easier creation of grouped plots" -type GroupBy +mutable struct GroupBy groupLabels::Vector # length == numGroups groupIds::Vector{Vector{Int}} # list of indices for each group end # this is when given a vector-type of values to group by -function extractGroupArgs(v::AVec, args...) +function extractGroupArgs(v::AVec, args...; legendEntry = string) groupLabels = sort(collect(unique(v))) n = length(groupLabels) if n > 100 warn("You created n=$n groups... Is that intended?") end groupIds = Vector{Int}[filter(i -> v[i] == glab, 1:length(v)) for glab in groupLabels] - GroupBy(map(string, groupLabels), groupIds) + GroupBy(map(legendEntry, groupLabels), groupIds) end +legendEntryFromTuple(ns::Tuple) = join(ns, ' ') + +# this is when given a tuple of vectors of values to group by +function extractGroupArgs(vs::Tuple, args...) + isempty(vs) && return GroupBy([""], [1:size(args[1],1)]) + v = map(tuple, vs...) + extractGroupArgs(v, args...; legendEntry = legendEntryFromTuple) +end + +# allow passing NamedTuples for a named legend entry +@require NamedTuples begin + legendEntryFromTuple(ns::NamedTuples.NamedTuple) = + join(["$k = $v" for (k, v) in zip(keys(ns), values(ns))], ", ") + + function extractGroupArgs(vs::NamedTuples.NamedTuple, args...) + isempty(vs) && return GroupBy([""], [1:size(args[1],1)]) + NT = eval(:(NamedTuples.@NT($(keys(vs)...)))){map(eltype, vs)...} + v = map(NT, vs...) + extractGroupArgs(v, args...; legendEntry = legendEntryFromTuple) + end +end # expecting a mapping of "group label" to "group indices" -function extractGroupArgs{T, V<:AVec{Int}}(idxmap::Dict{T,V}, args...) +function extractGroupArgs(idxmap::Dict{T,V}, args...) where {T, V<:AVec{Int}} groupLabels = sortedkeys(idxmap) - groupIds = VecI[collect(idxmap[k]) for k in groupLabels] + groupIds = Vector{Int}[collect(idxmap[k]) for k in groupLabels] GroupBy(groupLabels, groupIds) end @@ -851,7 +1124,7 @@ function convertLegendValue(val::Symbol) :best elseif val in (:no, :none) :none - elseif val in (:right, :left, :top, :bottom, :inside, :best, :legend, :topright, :topleft, :bottomleft, :bottomright) + elseif val in (:right, :left, :top, :bottom, :inside, :best, :legend, :topright, :topleft, :bottomleft, :bottomright, :outertopright) val else error("Invalid symbol for legend: $val") @@ -859,7 +1132,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::Tuple{S,T}) where {S<:Real, T<:Real} = v convertLegendValue(v::AbstractArray) = map(convertLegendValue, v) # ----------------------------------------------------------------------------- @@ -928,12 +1201,17 @@ const _match_map = KW( :background_color_legend => :background_color_subplot, :background_color_inside => :background_color_subplot, :foreground_color_legend => :foreground_color_subplot, - :foreground_color_grid => :foreground_color_subplot, :foreground_color_title => :foreground_color_subplot, :left_margin => :margin, :top_margin => :margin, :right_margin => :margin, :bottom_margin => :margin, + :titlefontfamily => :fontfamily_subplot, + :legendfontfamily => :fontfamily_subplot, + :titlefontcolor => :foreground_color_subplot, + :legendfontcolor => :foreground_color_subplot, + :tickfontcolor => :foreground_color_text, + :guidefontcolor => :foreground_color_guide, ) # these can match values from the parent container (axis --> subplot --> plot) @@ -942,8 +1220,12 @@ const _match_map2 = KW( :foreground_color_subplot => :foreground_color, :foreground_color_axis => :foreground_color_subplot, :foreground_color_border => :foreground_color_subplot, + :foreground_color_grid => :foreground_color_subplot, :foreground_color_guide => :foreground_color_subplot, :foreground_color_text => :foreground_color_subplot, + :fontfamily_subplot => :fontfamily, + :tickfontfamily => :fontfamily_subplot, + :guidefontfamily => :fontfamily_subplot, ) # properly retrieve from plt.attr, passing `:match` to the correct key @@ -1048,11 +1330,9 @@ end function _update_subplot_periphery(sp::Subplot, anns::AVec) # 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) + newanns = [] + for ann in vcat(anns, sp[:annotations]) + append!(newanns, process_annotation(sp, ann...)) end sp.attr[:annotations] = newanns @@ -1076,7 +1356,6 @@ function _update_subplot_colors(sp::Subplot) # foreground colors color_or_nothing!(sp.attr, :foreground_color_subplot) color_or_nothing!(sp.attr, :foreground_color_legend) - color_or_nothing!(sp.attr, :foreground_color_grid) color_or_nothing!(sp.attr, :foreground_color_title) return end @@ -1128,6 +1407,7 @@ function _update_axis_colors(axis::Axis) color_or_nothing!(axis.d, :foreground_color_border) color_or_nothing!(axis.d, :foreground_color_guide) color_or_nothing!(axis.d, :foreground_color_text) + color_or_nothing!(axis.d, :foreground_color_grid) return end @@ -1164,24 +1444,26 @@ end # ----------------------------------------------------------------------------- +has_black_border_for_default(st) = error("The seriestype attribute only accepts Symbols, you passed the $(typeof(st)) $st.") +has_black_border_for_default(st::Function) = error("The seriestype attribute only accepts Symbols, you passed the function $st.") function has_black_border_for_default(st::Symbol) like_histogram(st) || st in (:hexbin, :bar, :shape) end # converts a symbol or string into a colorant (Colors.RGB), and assigns a color automatically -function getSeriesRGBColor(c, α, sp::Subplot, n::Int) +function getSeriesRGBColor(c, sp::Subplot, n::Int) if c == :auto c = autopick(sp[:color_palette], n) elseif isa(c, Int) c = autopick(sp[:color_palette], c) end - plot_color(c, α) + plot_color(c) end function ensure_gradient!(d::KW, csym::Symbol, asym::Symbol) if !isa(d[csym], ColorGradient) - d[csym] = cgrad(alpha = d[asym]) + d[csym] = typeof(d[asym]) <: AbstractVector ? cgrad() : cgrad(alpha = d[asym]) end end @@ -1193,26 +1475,19 @@ function _replace_linewidth(d::KW) end function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int) - pkg = plt.backend - globalIndex = d[:series_plotindex] - # add default values to our dictionary, being careful not to delete what we just added! for (k,v) in _series_defaults slice_arg!(d, d, k, v, commandIndex, false) end - # this is how many series belong to this subplot - # plotIndex = count(series -> series.d[:subplot] === sp && series.d[:primary], plt.series_list) - plotIndex = 0 - for series in sp.series_list - if series[:primary] - plotIndex += 1 - end - end - # plotIndex = count(series -> series[:primary], sp.series_list) - if get(d, :primary, true) - plotIndex += 1 - end + return d +end + + +function _update_series_attributes!(d::KW, plt::Plot, sp::Subplot) + pkg = plt.backend + globalIndex = d[:series_plotindex] + plotIndex = _series_index(d, sp) aliasesAndAutopick(d, :linestyle, _styleAliases, supported_styles(pkg), plotIndex) aliasesAndAutopick(d, :markershape, _markerAliases, supported_markers(pkg), plotIndex) @@ -1228,39 +1503,46 @@ function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int) end # update series color - d[:seriescolor] = getSeriesRGBColor(d[:seriescolor], d[:seriesalpha], sp, plotIndex) + d[:seriescolor] = getSeriesRGBColor.(d[:seriescolor], sp, plotIndex) # update other colors for s in (:line, :marker, :fill) csym, asym = Symbol(s,:color), Symbol(s,:alpha) - d[csym] = if d[csym] == :match - plot_color(if has_black_border_for_default(d[:seriestype]) && s == :line + d[csym] = if d[csym] == :auto + plot_color.(if has_black_border_for_default(d[:seriestype]) && s == :line sp[:foreground_color_subplot] else d[:seriescolor] - end, d[asym]) + end) + elseif d[csym] == :match + plot_color.(d[:seriescolor]) else - getSeriesRGBColor(d[csym], d[asym], sp, plotIndex) + getSeriesRGBColor.(d[csym], sp, plotIndex) end end # update markerstrokecolor d[:markerstrokecolor] = if d[:markerstrokecolor] == :match - plot_color(sp[:foreground_color_subplot], d[:markerstrokealpha]) + plot_color(sp[:foreground_color_subplot]) + elseif d[:markerstrokecolor] == :auto + getSeriesRGBColor.(d[:markercolor], sp, plotIndex) else - getSeriesRGBColor(d[:markerstrokecolor], d[:markerstrokealpha], sp, plotIndex) + getSeriesRGBColor.(d[:markerstrokecolor], sp, plotIndex) end - # if marker_z or line_z are set, ensure we have a gradient + # if marker_z, fill_z or line_z are set, ensure we have a gradient if d[:marker_z] != nothing ensure_gradient!(d, :markercolor, :markeralpha) end if d[:line_z] != nothing ensure_gradient!(d, :linecolor, :linealpha) end + if d[:fill_z] != nothing + ensure_gradient!(d, :fillcolor, :fillalpha) + end # scatter plots don't have a line, but must have a shape - if d[:seriestype] in (:scatter, :scatter3d) + if d[:seriestype] in (:scatter, :scatterbins, :scatterhist, :scatter3d) d[:linewidth] = 0 if d[:markershape] == :none d[:markershape] = :circle @@ -1275,3 +1557,19 @@ function _add_defaults!(d::KW, plt::Plot, sp::Subplot, commandIndex::Int) _replace_linewidth(d) d end + +function _series_index(d, sp) + idx = 0 + for series in series_list(sp) + if series[:primary] + idx += 1 + end + if series == d + return idx + end + end + if get(d, :primary, true) + idx += 1 + end + return idx +end diff --git a/src/axes.jl b/src/axes.jl index b9840e7a..1410b28b 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -70,13 +70,16 @@ function process_axis_arg!(d::KW, arg, letter = "") elseif arg == nothing d[Symbol(letter,:ticks)] = [] + elseif T <: Bool || arg in _allShowaxisArgs + d[Symbol(letter,:showaxis)] = showaxis(arg, letter) + elseif typeof(arg) <: Number d[Symbol(letter,:rotation)] = arg elseif typeof(arg) <: Function d[Symbol(letter,:formatter)] = arg - else + elseif !handleColors!(d, arg, Symbol(letter, :foreground_color_axis)) warn("Skipped $(letter)axis arg $arg") end @@ -118,7 +121,7 @@ Base.show(io::IO, axis::Axis) = dumpdict(axis.d, "Axis", true) # Base.getindex(axis::Axis, k::Symbol) = getindex(axis.d, k) Base.setindex!(axis::Axis, v, ks::Symbol...) = setindex!(axis.d, v, ks...) Base.haskey(axis::Axis, k::Symbol) = haskey(axis.d, k) -Base.extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) +ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax)) const _scale_funcs = Dict{Symbol,Function}( @@ -156,16 +159,52 @@ function optimal_ticks_and_labels(axis::Axis, ticks = nothing) scale = axis[:scale] sf = scalefunc(scale) + # If the axis input was a Date or DateTime use a special logic to find + # "round" Date(Time)s as ticks + # This bypasses the rest of optimal_ticks_and_labels, because + # optimize_datetime_ticks returns ticks AND labels: the label format (Date + # or DateTime) is chosen based on the time span between amin and amax + # rather than on the input format + # TODO: maybe: non-trivial scale (:ln, :log2, :log10) for date/datetime + if ticks == nothing && scale == :identity + if axis[:formatter] == dateformatter + # optimize_datetime_ticks returns ticks and labels(!) based on + # integers/floats corresponding to the DateTime type. Thus, the axes + # limits, which resulted from converting the Date type to integers, + # are converted to 'DateTime integers' (actually floats) before + # being passed to optimize_datetime_ticks. + # (convert(Int, convert(DateTime, convert(Date, i))) == 87600000*i) + ticks, labels = optimize_datetime_ticks(864e5 * amin, 864e5 * amax; + k_min = 2, k_max = 4) + # Now the ticks are converted back to floats corresponding to Dates. + return ticks / 864e5, labels + elseif axis[:formatter] == datetimeformatter + return optimize_datetime_ticks(amin, amax; k_min = 2, k_max = 4) + end + end + # get a list of well-laid-out ticks - scaled_ticks = if ticks == nothing - optimize_ticks( + if ticks == nothing + scaled_ticks = optimize_ticks( sf(amin), sf(amax); - k_min = 5, # minimum number of ticks + k_min = 4, # minimum number of ticks k_max = 8, # maximum number of ticks )[1] + elseif typeof(ticks) <: Int + scaled_ticks, viewmin, viewmax = optimize_ticks( + sf(amin), + sf(amax); + k_min = ticks, # minimum number of ticks + k_max = ticks, # maximum number of ticks + k_ideal = ticks, + # `strict_span = false` rewards cases where the span of the + # chosen ticks is not too much bigger than amin - amax: + strict_span = false, + ) + axis[:lims] = map(invscalefunc(scale), (viewmin, viewmax)) else - map(sf, filter(t -> amin <= t <= amax, ticks)) + scaled_ticks = map(sf, (filter(t -> amin <= t <= amax, ticks))) end unscaled_ticks = map(invscalefunc(scale), scaled_ticks) @@ -173,12 +212,20 @@ function optimal_ticks_and_labels(axis::Axis, ticks = nothing) formatter = axis[:formatter] if formatter == :auto # the default behavior is to make strings of the scaled values and then apply the labelfunc + map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, :auto)) + elseif formatter == :plain + # Leave the numbers in plain format map(labelfunc(scale, backend()), Showoff.showoff(scaled_ticks, :plain)) elseif formatter == :scientific Showoff.showoff(unscaled_ticks, :scientific) else # there was an override for the formatter... use that on the unscaled ticks map(formatter, unscaled_ticks) + # if the formatter left us with numbers, still apply the default formatter + # However it leave us with the problem of unicode number decoding by the backend + # if eltype(unscaled_ticks) <: Number + # Showoff.showoff(unscaled_ticks, :auto) + # end end else # no finite ticks to show... @@ -192,20 +239,39 @@ end # return (continuous_values, discrete_values) for the ticks on this axis function get_ticks(axis::Axis) - ticks = axis[:ticks] + ticks = _transform_ticks(axis[:ticks]) ticks in (nothing, false) && return nothing + # treat :native ticks as :auto + ticks = ticks == :native ? :auto : ticks + dvals = axis[:discrete_values] - cv, dv = if !isempty(dvals) && ticks == :auto - # discrete ticks... - axis[:continuous_values], dvals - elseif ticks == :auto - # compute optimal ticks and labels - optimal_ticks_and_labels(axis) - elseif typeof(ticks) <: AVec - # override ticks, but get the labels - optimal_ticks_and_labels(axis, ticks) - elseif typeof(ticks) <: NTuple{2} + cv, dv = if typeof(ticks) <: Symbol + if !isempty(dvals) + # discrete ticks... + n = length(dvals) + rng = if ticks == :auto + Int[round(Int,i) for i in linspace(1, n, 15)] + else # if ticks == :all + 1:n + end + axis[:continuous_values][rng], dvals[rng] + elseif ispolar(axis.sps[1]) && axis[:letter] == :x + #force theta axis to be full circle + (collect(0:pi/4:7pi/4), string.(0:45:315)) + else + # compute optimal ticks and labels + optimal_ticks_and_labels(axis) + end + elseif typeof(ticks) <: Union{AVec, Int} + if !isempty(dvals) && typeof(ticks) <: Int + rng = Int[round(Int,i) for i in linspace(1, length(dvals), ticks)] + axis[:continuous_values][rng], dvals[rng] + else + # override ticks, but get the labels + optimal_ticks_and_labels(axis, ticks) + end + elseif typeof(ticks) <: NTuple{2, Any} # assuming we're passed (ticks, labels) ticks else @@ -213,15 +279,13 @@ function get_ticks(axis::Axis) end # @show ticks dvals cv dv - # TODO: better/smarter cutoff values for sampling ticks - if length(cv) > 30 - rng = Int[round(Int,i) for i in linspace(1, length(cv), 15)] - cv[rng], dv[rng] - else - cv, dv - end + return cv, dv end +_transform_ticks(ticks) = ticks +_transform_ticks(ticks::AbstractArray{T}) where T <: Dates.TimeType = Dates.value.(ticks) +_transform_ticks(ticks::NTuple{2, Any}) = (_transform_ticks(ticks[1]), ticks[2]) + # ------------------------------------------------------------------------- @@ -236,8 +300,8 @@ end function expand_extrema!(ex::Extrema, v::Number) - ex.emin = min(v, ex.emin) - ex.emax = max(v, ex.emax) + ex.emin = isfinite(v) ? min(v, ex.emin) : ex.emin + ex.emax = isfinite(v) ? max(v, ex.emax) : ex.emax ex end @@ -250,13 +314,13 @@ expand_extrema!(axis::Axis, ::Void) = axis[:extrema] expand_extrema!(axis::Axis, ::Bool) = axis[:extrema] -function expand_extrema!{MIN<:Number,MAX<:Number}(axis::Axis, v::Tuple{MIN,MAX}) +function expand_extrema!(axis::Axis, v::Tuple{MIN,MAX}) where {MIN<:Number,MAX<:Number} ex = axis[:extrema] - ex.emin = min(v[1], ex.emin) - ex.emax = max(v[2], ex.emax) + ex.emin = isfinite(v[1]) ? min(v[1], ex.emin) : ex.emin + ex.emax = isfinite(v[2]) ? max(v[2], ex.emax) : ex.emax ex end -function expand_extrema!{N<:Number}(axis::Axis, v::AVec{N}) +function expand_extrema!(axis::Axis, v::AVec{N}) where N<:Number ex = axis[:extrema] for vi in v expand_extrema!(ex, vi) @@ -275,6 +339,9 @@ function expand_extrema!(sp::Subplot, d::KW) else letter == :x ? :y : letter == :y ? :x : :z end] + if letter != :z && d[:seriestype] == :straightline && any(series[:seriestype] != :straightline for series in series_list(sp)) && data[1] != data[2] + data = [NaN] + end axis = sp[Symbol(letter, "axis")] if isa(data, Volume) @@ -307,7 +374,7 @@ function expand_extrema!(sp::Subplot, d::KW) if fr == nothing && d[:seriestype] == :bar fr = 0.0 end - if fr != nothing + if fr != nothing && !all3D(d) axis = sp.attr[vert ? :yaxis : :xaxis] if typeof(fr) <: Tuple for fri in fr @@ -325,13 +392,22 @@ function expand_extrema!(sp::Subplot, d::KW) bw = d[:bar_width] if bw == nothing - bw = d[:bar_width] = mean(diff(data)) + bw = d[:bar_width] = _bar_width * ignorenan_minimum(filter(x->x>0,diff(sort(data)))) end axis = sp.attr[Symbol(dsym, :axis)] - expand_extrema!(axis, maximum(data) + 0.5maximum(bw)) - expand_extrema!(axis, minimum(data) - 0.5minimum(bw)) + expand_extrema!(axis, ignorenan_maximum(data) + 0.5maximum(bw)) + expand_extrema!(axis, ignorenan_minimum(data) - 0.5minimum(bw)) end + # expand for heatmaps + if d[:seriestype] == :heatmap + for letter in (:x, :y) + data = d[letter] + axis = sp[Symbol(letter, "axis")] + scale = get(d, Symbol(letter, "scale"), :identity) + expand_extrema!(axis, heatmap_edges(data, scale)) + end + end end function expand_extrema!(sp::Subplot, xmin, xmax, ymin, ymax) @@ -342,21 +418,23 @@ end # ------------------------------------------------------------------------- # push the limits out slightly -function widen(lmin, lmax) - span = lmax - lmin - # eps = max(1e-16, min(1e-2span, 1e-10)) - eps = max(1e-16, 0.03span) - lmin-eps, lmax+eps +function widen(lmin, lmax, scale = :identity) + f, invf = scalefunc(scale), invscalefunc(scale) + span = f(lmax) - f(lmin) + # eps = NaNMath.max(1e-16, min(1e-2span, 1e-10)) + eps = NaNMath.max(1e-16, 0.03span) + invf(f(lmin)-eps), invf(f(lmax)+eps) end -# figure out if widening is a good idea. if there's a scale set it's too tricky, -# so lazy out and don't widen +# figure out if widening is a good idea. +const _widen_seriestypes = (:line, :path, :steppre, :steppost, :sticks, :scatter, :barbins, :barhist, :histogram, :scatterbins, :scatterhist, :stepbins, :stephist, :bins2d, :histogram2d, :bar, :shape, :path3d, :scatter3d) + function default_should_widen(axis::Axis) should_widen = false - if axis[:scale] == :identity && !is_2tuple(axis[:lims]) + if !is_2tuple(axis[:lims]) for sp in axis.sps for series in series_list(sp) - if series.d[:seriestype] in (:scatter,) || series.d[:markershape] != :none + if series.d[:seriestype] in _widen_seriestypes should_widen = true end end @@ -365,6 +443,13 @@ function default_should_widen(axis::Axis) should_widen end +function round_limits(amin,amax) + scale = 10^(1-round(log10(amax - amin))) + amin = floor(amin*scale)/scale + amax = ceil(amax*scale)/scale + amin, amax +end + # using the axis extrema and limit overrides, return the min/max value for this axis function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis)) ex = axis[:extrema] @@ -384,8 +469,19 @@ function axis_limits(axis::Axis, should_widen::Bool = default_should_widen(axis) if !isfinite(amin) && !isfinite(amax) amin, amax = 0.0, 1.0 end - if should_widen - widen(amin, amax) + if ispolar(axis.sps[1]) + if axis[:letter] == :x + amin, amax = 0, 2pi + elseif lims == :auto + #widen max radius so ticks dont overlap with theta axis + amin, amax + 0.1 * abs(amax - amin) + else + amin, amax + end + elseif should_widen && axis[:widen] + widen(amin, amax, axis[:scale]) + elseif lims == :round + round_limits(amin,amax) else amin, amax end @@ -401,7 +497,7 @@ function discrete_value!(axis::Axis, dv) # @show axis[:discrete_map], axis[:discrete_values], dv if cv_idx == -1 ex = axis[:extrema] - cv = max(0.5, ex.emax + 1.0) + cv = NaNMath.max(0.5, ex.emax + 1.0) expand_extrema!(axis, cv) push!(axis[:discrete_values], dv) push!(axis[:continuous_values], cv) @@ -466,38 +562,94 @@ function axis_drawing_info(sp::Subplot) ymin, ymax = axis_limits(yaxis) xticks = get_ticks(xaxis) yticks = get_ticks(yaxis) - spine_segs = Segments(2) - grid_segs = Segments(2) + xaxis_segs = Segments(2) + yaxis_segs = Segments(2) + xtick_segs = Segments(2) + ytick_segs = Segments(2) + xgrid_segs = Segments(2) + ygrid_segs = Segments(2) + xborder_segs = Segments(2) + yborder_segs = Segments(2) - if !(xaxis[:ticks] in (nothing, false)) - f = scalefunc(yaxis[:scale]) - invf = invscalefunc(yaxis[:scale]) - t1 = invf(f(ymin) + 0.015*(f(ymax)-f(ymin))) - t2 = invf(f(ymax) - 0.015*(f(ymax)-f(ymin))) + if sp[:framestyle] != :none + # xaxis + if xaxis[:showaxis] + if sp[:framestyle] != :grid + y1, y2 = if sp[:framestyle] in (:origin, :zerolines) + 0.0, 0.0 + else + xor(xaxis[:mirror], yaxis[:flip]) ? (ymax, ymin) : (ymin, ymax) + end + push!(xaxis_segs, (xmin, y1), (xmax, y1)) + # don't show the 0 tick label for the origin framestyle + if sp[:framestyle] == :origin && !(xticks in (nothing,false)) && length(xticks) > 1 + showticks = xticks[1] .!= 0 + xticks = (xticks[1][showticks], xticks[2][showticks]) + end + end + sp[:framestyle] in (:semi, :box) && push!(xborder_segs, (xmin, y2), (xmax, y2)) # top spine + end + if !(xaxis[:ticks] in (nothing, false)) + f = scalefunc(yaxis[:scale]) + invf = invscalefunc(yaxis[:scale]) + ticks_in = xaxis[:tick_direction] == :out ? -1 : 1 + t1 = invf(f(ymin) + 0.015 * (f(ymax) - f(ymin)) * ticks_in) + t2 = invf(f(ymax) - 0.015 * (f(ymax) - f(ymin)) * ticks_in) + t3 = invf(f(0) + 0.015 * (f(ymax) - f(ymin)) * ticks_in) - push!(spine_segs, (xmin,ymin), (xmax,ymin)) # bottom spine - # push!(spine_segs, (xmin,ymax), (xmax,ymax)) # top spine - for xtick in xticks[1] - push!(spine_segs, (xtick, ymin), (xtick, t1)) # bottom tick - push!(grid_segs, (xtick, t1), (xtick, t2)) # vertical grid - # push!(spine_segs, (xtick, ymax), (xtick, t2)) # top tick + for xtick in xticks[1] + if xaxis[:showaxis] + tick_start, tick_stop = if sp[:framestyle] == :origin + (0, t3) + else + xor(xaxis[:mirror], yaxis[:flip]) ? (ymax, t2) : (ymin, t1) + end + push!(xtick_segs, (xtick, tick_start), (xtick, tick_stop)) # bottom tick + end + # sp[:draw_axes_border] && push!(xaxis_segs, (xtick, ymax), (xtick, t2)) # top tick + xaxis[:grid] && push!(xgrid_segs, (xtick, ymin), (xtick, ymax)) # vertical grid + end + end + + # yaxis + if yaxis[:showaxis] + if sp[:framestyle] != :grid + x1, x2 = if sp[:framestyle] in (:origin, :zerolines) + 0.0, 0.0 + else + xor(yaxis[:mirror], xaxis[:flip]) ? (xmax, xmin) : (xmin, xmax) + end + push!(yaxis_segs, (x1, ymin), (x1, ymax)) + # don't show the 0 tick label for the origin framestyle + if sp[:framestyle] == :origin && !(yticks in (nothing,false)) && length(yticks) > 1 + showticks = yticks[1] .!= 0 + yticks = (yticks[1][showticks], yticks[2][showticks]) + end + end + sp[:framestyle] in (:semi, :box) && push!(yborder_segs, (x2, ymin), (x2, ymax)) # right spine + end + if !(yaxis[:ticks] in (nothing, false)) + f = scalefunc(xaxis[:scale]) + invf = invscalefunc(xaxis[:scale]) + ticks_in = yaxis[:tick_direction] == :out ? -1 : 1 + t1 = invf(f(xmin) + 0.015 * (f(xmax) - f(xmin)) * ticks_in) + t2 = invf(f(xmax) - 0.015 * (f(xmax) - f(xmin)) * ticks_in) + t3 = invf(f(0) + 0.015 * (f(xmax) - f(xmin)) * ticks_in) + + for ytick in yticks[1] + if yaxis[:showaxis] + tick_start, tick_stop = if sp[:framestyle] == :origin + (0, t3) + else + xor(yaxis[:mirror], xaxis[:flip]) ? (xmax, t2) : (xmin, t1) + end + push!(ytick_segs, (tick_start, ytick), (tick_stop, ytick)) # left tick + end + # sp[:draw_axes_border] && push!(yaxis_segs, (xmax, ytick), (t2, ytick)) # right tick + yaxis[:grid] && push!(ygrid_segs, (xmin, ytick), (xmax, ytick)) # horizontal grid + end end end - if !(yaxis[:ticks] in (nothing, false)) - f = scalefunc(xaxis[:scale]) - invf = invscalefunc(xaxis[:scale]) - t1 = invf(f(xmin) + 0.015*(f(xmax)-f(xmin))) - t2 = invf(f(xmax) - 0.015*(f(xmax)-f(xmin))) - - push!(spine_segs, (xmin,ymin), (xmin,ymax)) # left spine - # push!(spine_segs, (xmax,ymin), (xmax,ymax)) # right spine - for ytick in yticks[1] - push!(spine_segs, (xmin, ytick), (t1, ytick)) # left tick - push!(grid_segs, (t1, ytick), (t2, ytick)) # horizontal grid - # push!(spine_segs, (xmax, ytick), (t2, ytick)) # right tick - end - end - - xticks, yticks, spine_segs, grid_segs + xticks, yticks, xaxis_segs, yaxis_segs, xtick_segs, ytick_segs, xgrid_segs, ygrid_segs, xborder_segs, yborder_segs end diff --git a/src/backends.jl b/src/backends.jl index 779b91cc..94199b00 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -1,12 +1,15 @@ -immutable NoBackend <: AbstractBackend end +struct NoBackend <: AbstractBackend end const _backendType = Dict{Symbol, DataType}(:none => NoBackend) const _backendSymbol = Dict{DataType, Symbol}(NoBackend => :none) const _backends = Symbol[] const _initialized_backends = Set{Symbol}() +"Returns a list of supported backends" backends() = _backends + +"Returns the name of the current backend" backend_name() = CURRENT_BACKEND.sym _backend_instance(sym::Symbol) = haskey(_backendType, sym) ? _backendType[sym]() : error("Unsupported backend $sym") @@ -15,7 +18,7 @@ macro init_backend(s) sym = Symbol(str) T = Symbol(string(s) * "Backend") esc(quote - immutable $T <: AbstractBackend end + struct $T <: AbstractBackend end export $sym $sym(; kw...) = (default(; kw...); backend(Symbol($str))) backend_name(::$T) = Symbol($str) @@ -48,8 +51,8 @@ _series_updated(plt::Plot, series::Series) = nothing _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 +title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt +guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt "Returns the (width,height) of a text label." function text_size(lablen::Int, sz::Number, rot::Number = 0) @@ -90,7 +93,7 @@ function tick_padding(axis::Axis) # hgt # get the height of the rotated label - text_size(longest_label, axis[:tickfont].pointsize, rot)[2] + text_size(longest_label, axis[:tickfontsize], rot)[2] end end @@ -120,7 +123,7 @@ _update_plot_object(plt::Plot) = nothing # --------------------------------------------------------- -type CurrentBackend +mutable struct CurrentBackend sym::Symbol pkg::AbstractBackend end @@ -148,7 +151,7 @@ function pickDefaultBackend() # the ordering/inclusion of this package list is my semi-arbitrary guess at # which one someone will want to use if they have the package installed...accounting for # features, speed, and robustness - for pkgstr in ("PyPlot", "GR", "PlotlyJS", "Immerse", "Gadfly", "UnicodePlots") + for pkgstr in ("GR", "PyPlot", "PlotlyJS", "PGFPlots", "UnicodePlots", "InspectDR", "GLVisualize") if Pkg.installed(pkgstr) != nothing return backend(Symbol(lowercase(pkgstr))) end @@ -277,6 +280,7 @@ end @init_backend GLVisualize @init_backend PGFPlots @init_backend InspectDR +@init_backend HDF5 # --------------------------------------------------------- diff --git a/src/backends/glvisualize.jl b/src/backends/glvisualize.jl index f1e40181..b8a4ed04 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 @@ -7,9 +7,12 @@ TODO * polar plots * labes and axis * fix units in all visuals (e.g dotted lines, marker scale, surfaces) - * why is there so little unicode supported in the font!??!? =# +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "glvisualize.jl")) +end + const _glvisualize_attr = merge_with_base_supported([ :annotations, :background_color_legend, :background_color_inside, :background_color_outside, @@ -21,11 +24,15 @@ const _glvisualize_attr = merge_with_base_supported([ :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :fillrange, :fillcolor, :fillalpha, :bins, :bar_width, :bar_edges, :bar_position, - :title, :title_location, :titlefont, + :title, :title_location, :window_title, :guide, :lims, :ticks, :scale, :flip, :rotation, - :tickfont, :guidefont, :legendfont, - :grid, :legend, :colorbar, + :titlefontsize, :titlefontcolor, + :legendfontsize, :legendfontcolor, + :tickfontsize, + :guidefontsize, :guidefontcolor, + :grid, :gridalpha, :gridstyle, :gridlinewidth, + :legend, :colorbar, :marker_z, :line_z, :levels, @@ -39,10 +46,12 @@ const _glvisualize_attr = merge_with_base_supported([ :clims, :inset_subplots, :dpi, - :hover + :hover, + :framestyle, + :tick_direction, ]) const _glvisualize_seriestype = [ - :path, :shape, + :path, :shape, :straightline, :scatter, :hexbin, :bar, :boxplot, :heatmap, :image, :volume, @@ -59,7 +68,7 @@ const _glvisualize_scale = [:identity, :ln, :log2, :log10] function _initialize_backend(::GLVisualizeBackend; kw...) @eval begin import GLVisualize, GeometryTypes, Reactive, GLAbstraction, GLWindow, Contour - import GeometryTypes: Point2f0, Point3f0, Vec2f0, Vec3f0, GLNormalMesh, SimpleRectangle + import GeometryTypes: Point2f0, Point3f0, Vec2f0, Vec3f0, GLNormalMesh, SimpleRectangle, Point, Vec import FileIO, Images export GLVisualize import Reactive: Signal @@ -67,10 +76,9 @@ function _initialize_backend(::GLVisualizeBackend; kw...) import GLVisualize: visualize import Plots.GL import UnicodeFun - Plots.slice_arg(img::Images.AbstractImage, idx::Int) = img + Plots.slice_arg(img::Matrix{C}, idx::Int) where {C<:Colorant} = img is_marker_supported(::GLVisualizeBackend, shape::GLVisualize.AllPrimitives) = true - is_marker_supported{Img<:Images.AbstractImage}(::GLVisualizeBackend, shape::Union{Vector{Img}, Img}) = true - is_marker_supported{C<:Colorant}(::GLVisualizeBackend, shape::Union{Vector{Matrix{C}}, Matrix{C}}) = true + is_marker_supported(::GLVisualizeBackend, shape::Union{Vector{Matrix{C}}, Matrix{C}}) where {C<:Colorant} = true is_marker_supported(::GLVisualizeBackend, shape::Shape) = true const GL = Plots end @@ -78,14 +86,9 @@ 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 (until Pkg3 is usable): - - Pkg.clone("https://github.com/tbreloff/MetaPkg.jl") - using MetaPkg - meta_checkout("MetaGL") - - See the MetaPkg readme for details... + if !Plots.is_installed("GLVisualize") + Pkg.add("GLVisualize") + end """ end @@ -99,46 +102,6 @@ end # 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) - close_child_signals!(screen) - empty!(screen) - empty!(screen.cameras) - for (k, s) in screen.inputs - empty!(s.actions) - end - empty!(screen) - else - for del_signal in _glplot_deletes - push!(del_signal, true) # trigger delete - end - empty!(_glplot_deletes) - end - nothing -end -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 @@ -155,38 +118,36 @@ function get_plot_screen(screen, name, result = []) end function create_window(plt::Plot{GLVisualizeBackend}, visible) - name = Symbol("Plots.jl") + 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", + "Plots", resolution = plt[:size], visible = visible ) - @async GLWindow.waiting_renderloop(parent_screen) + @async GLWindow.renderloop(parent_screen) + GLVisualize.add_screen(parent_screen) end # now lets get ourselves a permanent Plotting screen - plot_screens = get_plot_screen(GLVisualize.get_screens(), name) + plot_screens = get_plot_screen(GLVisualize.current_screen(), 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") + error("multiple Plot screens. Please don't use any screen with the name $name") end # Since we own this window, we can do deep cleansing - empty_screen!(screen) + empty!(screen) plt.o = screen GLWindow.set_visibility!(screen, visible) resize!(screen, plt[:size]...) @@ -221,12 +182,12 @@ function gl_marker(shape) shape end function gl_marker(shape::Shape) - points = Point2f0[Vec{2,Float32}(p) for p in zip(shape.x, shape.y)] + points = Point2f0[GeometryTypes.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 + map!(p -> ((p - origin) ./ width) - 0.5f0, points, points) # normalize and center GeometryTypes.GLNormalMesh(points) end # create a marker/shape type @@ -260,13 +221,13 @@ 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(::Type{T}, vec::T) where {T <: StaticArrays.StaticVector} = vec +to_vec(::Type{T}, s::Number) where {T <: StaticArrays.StaticVector} = 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(::Type{T}, vec::StaticArrays.StaticVector{3}) where {T <: StaticArrays.StaticVector{2}} = T(vec[1], vec[2]) +to_vec(::Type{T}, vec::StaticArrays.StaticVector{2}) where {T <: StaticArrays.StaticVector{3}} = T(vec[1], vec[2], 0) -to_vec{T <: FixedVector}(::Type{T}, vecs::AbstractVector) = map(x-> to_vec(T, x), vecs) +to_vec(::Type{T}, vecs::AbstractVector) where {T <: StaticArrays.StaticVector} = map(x-> to_vec(T, x), vecs) function extract_marker(d, kw_args) dim = Plots.is3d(d) ? 3 : 2 @@ -321,15 +282,21 @@ end function extract_surface(d) map(_extract_surface, (d[:x], d[:y], d[:z])) end -function topoints{P}(::Type{P}, array) - P[x for x in zip(array...)] +function topoints(::Type{P}, array) where P + [P(x) for x in zip(array...)] end function extract_points(d) dim = is3d(d) ? 3 : 2 - array = (d[:x], d[:y], d[:z])[1:dim] + array = if d[:seriestype] == :straightline + straightline_data(d) + elseif d[:seriestype] == :shape + shape_data(d) + else + (d[:x], d[:y], d[:z])[1:dim] + end topoints(Point{dim, Float32}, array) end -function make_gradient{C <: Colorant}(grad::Vector{C}) +function make_gradient(grad::Vector{C}) where C <: Colorant grad end function make_gradient(grad::ColorGradient) @@ -352,7 +319,7 @@ function extract_any_color(d, kw_args) kw_args[:color_norm] = Vec2f0(clims) end elseif clims == :auto - kw_args[:color_norm] = Vec2f0(extrema(d[:y])) + kw_args[:color_norm] = Vec2f0(ignorenan_extrema(d[:y])) end end else @@ -363,7 +330,7 @@ function extract_any_color(d, kw_args) kw_args[:color_norm] = Vec2f0(clims) end elseif clims == :auto - kw_args[:color_norm] = Vec2f0(extrema(d[:y])) + kw_args[:color_norm] = Vec2f0(ignorenan_extrema(d[:y])) else error("Unsupported limits: $clims") end @@ -375,7 +342,7 @@ 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] = Float32(d[:linewidth] * 3) end end @@ -384,7 +351,7 @@ function extract_color(d, sym) end gl_color(c::PlotUtils.ColorGradient) = c.colors -gl_color{T<:Colorant}(c::Vector{T}) = c +gl_color(c::Vector{T}) where {T<:Colorant} = c gl_color(c::RGBA{Float32}) = c gl_color(c::Colorant) = RGBA{Float32}(c) @@ -415,14 +382,14 @@ end dist(a, b) = abs(a-b) -mindist(x, a, b) = min(dist(a, x), dist(b, x)) +mindist(x, a, b) = NaNMath.min(dist(a, x), dist(b, x)) function gappy(x, ps) n = length(ps) x <= first(ps) && return first(ps) - x for j=1:(n-1) p0 = ps[j] - p1 = ps[min(j+1, n)] + p1 = ps[NaNMath.min(j+1, n)] if p0 <= x && p1 >= x return mindist(x, p0, p1) * (isodd(j) ? 1 : -1) end @@ -443,7 +410,7 @@ function extract_linestyle(d, kw_args) haskey(d, :linestyle) || return ls = d[:linestyle] lw = d[:linewidth] - kw_args[:thickness] = lw + kw_args[:thickness] = Float32(lw) if ls == :dash points = [0.0, lw, 2lw, 3lw, 4lw] insert_pattern!(points, kw_args) @@ -530,7 +497,7 @@ 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 = ignorenan_extrema(d[:x]); ymin, ymax = ignorenan_extrema(d[:y]) kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(xmin, ymin, xmax-xmin, ymax-ymin) nothing end @@ -557,7 +524,7 @@ function extract_colornorm(d, kw_args) else d[:y] end - kw_args[:color_norm] = Vec2f0(extrema(z)) + kw_args[:color_norm] = Vec2f0(ignorenan_extrema(z)) kw_args[:intensity] = map(Float32, collect(z)) end end @@ -615,7 +582,7 @@ function draw_grid_lines(sp, grid_segs, thickness, style, model, color) ) d = Dict( :linestyle => style, - :linewidth => thickness, + :linewidth => Float32(thickness), :linecolor => color ) Plots.extract_linestyle(d, kw_args) @@ -624,8 +591,10 @@ end function align_offset(startpos, lastpos, atlas, rscale, font, align) xscale, yscale = GLVisualize.glyph_scale!('X', rscale) - xmove = (lastpos-startpos)[1]+xscale - if align == :top + xmove = (lastpos-startpos)[1] + xscale + if isa(align, GeometryTypes.Vec) + return -Vec2f0(xmove, yscale) .* align + elseif align == :top return -Vec2f0(xmove/2f0, yscale) elseif align == :right return -Vec2f0(xmove, yscale/2f0) @@ -634,11 +603,6 @@ function align_offset(startpos, lastpos, atlas, rscale, font, align) end end -function align_offset(startpos, lastpos, atlas, rscale, font, align::Vec) - 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 @@ -654,10 +618,10 @@ end pointsize(font) = font.pointsize * 2 function draw_ticks( - axis, ticks, isx, lims, m, text = "", + axis, ticks, isx, isorigin, lims, m, text = "", positions = Point2f0[], offsets=Vec2f0[] ) - sz = pointsize(axis[:tickfont]) + sz = pointsize(tickfont(axis)) atlas = GLVisualize.get_texture_atlas() font = GLVisualize.defaultfont() @@ -672,7 +636,11 @@ function draw_ticks( for (cv, dv) in zip(ticks...) x, y = cv, lims[1] - xy = isx ? (x, y) : (y, x) + xy = if isorigin + isx ? (x, 0) : (0, x) + else + isx ? (x, y) : (y, x) + end _pos = m * GeometryTypes.Vec4f0(xy[1], xy[2], 0, 1) startpos = Point2f0(_pos[1], _pos[2]) - axis_gap str = string(dv) @@ -686,7 +654,7 @@ function draw_ticks( 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 + map!(position, position) do pos pos .+ alignoff end append!(positions, position) @@ -697,7 +665,7 @@ function draw_ticks( text, positions, offsets end -function text(position, text, kw_args) +function glvisualize_text(position, text, kw_args) text_align = alignment2num(text.font) startpos = Vec2f0(position) atlas = GLVisualize.get_texture_atlas() @@ -708,7 +676,7 @@ function text(position, text, kw_args) offset = GLVisualize.calc_offset(text.str, rscale, font, atlas) alignoff = align_offset(startpos, last(position), atlas, rscale, font, text_align) - map!(position) do pos + map!(position, position) do pos pos .+ alignoff end kw_args[:position] = position @@ -728,72 +696,122 @@ function text_model(font, pivot) end end function gl_draw_axes_2d(sp::Plots.Subplot{Plots.GLVisualizeBackend}, model, area) - xticks, yticks, spine_segs, grid_segs = Plots.axis_drawing_info(sp) + xticks, yticks, xspine_segs, yspine_segs, xtick_segs, ytick_segs, xgrid_segs, ygrid_segs, xborder_segs, yborder_segs = Plots.axis_drawing_info(sp) xaxis = sp[:xaxis]; yaxis = sp[:yaxis] - c = Colors.color(Plots.gl_color(sp[:foreground_color_grid])) + xgc = Colors.color(Plots.gl_color(xaxis[:foreground_color_grid])) + ygc = Colors.color(Plots.gl_color(yaxis[:foreground_color_grid])) axis_vis = [] - if sp[:grid] - grid = draw_grid_lines(sp, grid_segs, 1f0, :dot, model, RGBA(c, 0.3f0)) + if xaxis[:grid] + grid = draw_grid_lines(sp, xgrid_segs, xaxis[:gridlinewidth], xaxis[:gridstyle], model, RGBA(xgc, xaxis[:gridalpha])) push!(axis_vis, grid) end - if alpha(xaxis[:foreground_color_border]) > 0 - spine = draw_grid_lines(sp, spine_segs, 1f0, :solid, model, RGBA(c, 1.0f0)) + if yaxis[:grid] + grid = draw_grid_lines(sp, ygrid_segs, yaxis[:gridlinewidth], yaxis[:gridstyle], model, RGBA(ygc, yaxis[:gridalpha])) + push!(axis_vis, grid) + end + + xac = Colors.color(Plots.gl_color(xaxis[:foreground_color_axis])) + yac = Colors.color(Plots.gl_color(yaxis[:foreground_color_axis])) + if alpha(xaxis[:foreground_color_axis]) > 0 + spine = draw_grid_lines(sp, xspine_segs, 1f0, :solid, model, RGBA(xac, 1.0f0)) push!(axis_vis, spine) end + if alpha(yaxis[:foreground_color_axis]) > 0 + spine = draw_grid_lines(sp, yspine_segs, 1f0, :solid, model, RGBA(yac, 1.0f0)) + push!(axis_vis, spine) + end + if sp[:framestyle] in (:zerolines, :grid) + if alpha(xaxis[:foreground_color_grid]) > 0 + spine = draw_grid_lines(sp, xtick_segs, 1f0, :solid, model, RGBA(xgc, xaxis[:gridalpha])) + push!(axis_vis, spine) + end + if alpha(yaxis[:foreground_color_grid]) > 0 + spine = draw_grid_lines(sp, ytick_segs, 1f0, :solid, model, RGBA(ygc, yaxis[:gridalpha])) + push!(axis_vis, spine) + end + else + if alpha(xaxis[:foreground_color_axis]) > 0 + spine = draw_grid_lines(sp, xtick_segs, 1f0, :solid, model, RGBA(xac, 1.0f0)) + push!(axis_vis, spine) + end + if alpha(yaxis[:foreground_color_axis]) > 0 + spine = draw_grid_lines(sp, ytick_segs, 1f0, :solid, model, RGBA(yac, 1.0f0)) + push!(axis_vis, spine) + end + end fcolor = Plots.gl_color(xaxis[:foreground_color_axis]) xlim = Plots.axis_limits(xaxis) ylim = Plots.axis_limits(yaxis) - if !(xaxis[:ticks] in (nothing, false, :none)) + if !(xaxis[:ticks] in (nothing, false, :none)) && !(sp[:framestyle] == :none) && xaxis[:showaxis] 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 - ) + t, positions, offsets = draw_ticks(xaxis, xticks, true, sp[:framestyle] == :origin, ylim, m) end kw_args = Dict{Symbol, Any}( :position => map(x-> x[2], ticklabels), :offset => map(last, ticklabels), :color => fcolor, - :relative_scale => pointsize(xaxis[:tickfont]), + :relative_scale => pointsize(tickfont(xaxis)), :scale_primitive => false ) push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args)) end + if !(yaxis[:ticks] in (nothing, false, :none)) && !(sp[:framestyle] == :none) && yaxis[:showaxis] + ticklabels = map(model) do m + mirror = yaxis[:mirror] + t, positions, offsets = draw_ticks(yaxis, yticks, false, sp[:framestyle] == :origin, xlim, m) + end + kw_args = Dict{Symbol, Any}( + :position => map(x-> x[2], ticklabels), + :offset => map(last, ticklabels), + :color => fcolor, + :relative_scale => pointsize(tickfont(xaxis)), + :scale_primitive => false + ) + push!(axis_vis, visualize(map(first, ticklabels), Style(:default), kw_args)) + end + + xbc = Colors.color(Plots.gl_color(xaxis[:foreground_color_border])) + ybc = Colors.color(Plots.gl_color(yaxis[:foreground_color_border])) + intensity = sp[:framestyle] == :semi ? 0.5f0 : 1.0f0 + if sp[:framestyle] in (:box, :semi) + xborder = draw_grid_lines(sp, xborder_segs, intensity, :solid, model, RGBA(xbc, intensity)) + yborder = draw_grid_lines(sp, yborder_segs, intensity, :solid, model, RGBA(ybc, intensity)) + push!(axis_vis, xborder, yborder) + 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) + tf = titlefont(sp) + font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, tf.rotation, tf.color) 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)) + push!(axis_vis, glvisualize_text(xy, t, kw)) end if xaxis[:guide] != "" - tf = xaxis[:guidefont]; color = gl_color(xaxis[:foreground_color_guide]) + tf = guidefont(xaxis) xy = Point2f0(area.w/2, - pointsize(tf)/2) - font = Plots.Font(tf.family, tf.pointsize, :hcenter, :bottom, tf.rotation, color) + font = Plots.Font(tf.family, tf.pointsize, :hcenter, :bottom, tf.rotation, tf.color) 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)) + push!(axis_vis, glvisualize_text(xy, t, kw)) end if yaxis[:guide] != "" - tf = yaxis[:guidefont]; color = gl_color(yaxis[:foreground_color_guide]) - font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, 90f0, color) + tf = guidefont(yaxis) + font = Plots.Font(tf.family, tf.pointsize, :hcenter, :top, 90f0, tf.color) 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) - push!(axis_vis, text(xy, t, kw)) + push!(axis_vis, glvisualize_text(xy, t, kw)) end axis_vis @@ -829,9 +847,9 @@ function gl_bar(d, kw_args) # compute half-width of bars bw = nothing hw = if bw == nothing - mean(diff(x)) + ignorenan_mean(diff(x)) else - Float64[cycle(bw,i)*0.5 for i=1:length(x)] + Float64[_cycle(bw,i)*0.5 for i=1:length(x)] end # make fillto a vector... default fills to 0 @@ -840,12 +858,12 @@ function gl_bar(d, kw_args) fillto = 0 end # create the bar shapes by adding x/y segments - positions, scales = Array(Point2f0, ny), Array(Vec2f0, ny) + positions, scales = Array{Point2f0}(ny), Array{Vec2f0}(ny) m = Reactive.value(kw_args[:model]) sx, sy = m[1,1], m[2,2] for i=1:ny center = x[i] - hwi = abs(cycle(hw,i)); yi = y[i]; fi = cycle(fillto,i) + hwi = abs(_cycle(hw,i)); yi = y[i]; fi = _cycle(fillto,i) if Plots.isvertical(d) sz = (hwi*sx, yi*sy) else @@ -881,7 +899,7 @@ function gl_boxplot(d, kw_args) sx, sy = m[1,1], m[2,2] for (i,glabel) in enumerate(glabels) # filter y - values = y[filter(i -> cycle(x,i) == glabel, 1:length(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 @@ -894,7 +912,7 @@ function gl_boxplot(d, kw_args) # make the shape center = Plots.discrete_value!(d[:subplot][:xaxis], glabel)[1] - hw = d[:bar_width] == nothing ? Plots._box_halfwidth*2 : cycle(d[:bar_width], i) + hw = d[:bar_width] == nothing ? Plots._box_halfwidth*2 : _cycle(d[:bar_width], i) l, m, r = center - hw/2, center, center + hw/2 # internal nodes for notches @@ -912,7 +930,7 @@ function gl_boxplot(d, kw_args) end # change q1 and q5 to show outliers # using maximum and minimum values inside the limits - q1, q5 = extrema(inside) + q1, q5 = ignorenan_extrema(inside) end # Box if notch @@ -991,9 +1009,9 @@ function scale_for_annotations!(series::Series, scaletype::Symbol = :pixels) # 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)) + offsets = Array{Vec2f0}(length(anns.strs)) series[:markersize] = map(1:length(anns.strs)) do i - str = cycle(anns.strs, i) + str = _cycle(anns.strs, i) # get the width and height of the string (in mm) sw, sh = text_size(str, anns.font.pointsize) @@ -1090,7 +1108,7 @@ function _display(plt::Plot{GLVisualizeBackend}, visible = true) kw_args[:stroke_width] = Float32(d[:linewidth]/100f0) end vis = GL.gl_surface(x, y, z, kw_args) - elseif (st in (:path, :path3d)) && d[:linewidth] > 0 + elseif (st in (:path, :path3d, :straightline)) && d[:linewidth] > 0 kw = copy(kw_args) points = Plots.extract_points(d) extract_linestyle(d, kw) @@ -1106,7 +1124,7 @@ function _display(plt::Plot{GLVisualizeBackend}, visible = true) kw = copy(kw_args) fr = d[:fillrange] 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]) + vcat(points, Point2f0[(points[i][1], _cycle(fr, i)) for i=length(points):-1:1]) else points end @@ -1141,8 +1159,7 @@ function _display(plt::Plot{GLVisualizeBackend}, visible = true) vis = gl_bar(d, kw_args) elseif st == :image extract_extrema(d, kw_args) - z = transpose_z(series, d[:z].surf, false) - vis = GL.gl_image(z, kw_args) + vis = GL.gl_image(d[:z].surf, kw_args) elseif st == :boxplot extract_c(d, kw_args, :fill) vis = gl_boxplot(d, kw_args) @@ -1171,9 +1188,9 @@ function _display(plt::Plot{GLVisualizeBackend}, visible = true) 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) + x, y = Reactive.value(model_m) * GeometryTypes.Vec{4, Float32}(x, y, 0, 1) extract_font(font, txt_args) - t = text(Point2f0(x, y), PlotText(str, font), txt_args) + t = glvisualize_text(Point2f0(x, y), PlotText(str, font), txt_args) GLVisualize._view(t, sp_screen, camera = :perspective) end @@ -1182,7 +1199,7 @@ function _display(plt::Plot{GLVisualizeBackend}, visible = true) if _3d GLAbstraction.center!(sp_screen) end - Reactive.post_empty() + GLAbstraction.post_empty() yield() end end @@ -1197,21 +1214,18 @@ function _show(io::IO, ::MIME"image/png", plt::Plot{GLVisualizeBackend}) GLWindow.render_frame(GLWindow.rootscreen(plt.o)) GLWindow.swapbuffers(plt.o) buff = GLWindow.screenbuffer(plt.o) - png = Images.Image(map(RGB{U8}, buff), - colorspace = "sRGB", - spatialorder = ["y", "x"] - ) + png = map(RGB{U8}, buff) FileIO.save(FileIO.Stream(FileIO.DataFormat{:PNG}, io), png) end function gl_image(img, kw_args) rect = kw_args[:primitive] - kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(rect.x, rect.y, rect.h, rect.w) # seems to be flipped + kw_args[:primitive] = GeometryTypes.SimpleRectangle{Float32}(rect.x, rect.y, rect.w, rect.h) visualize(img, Style(:default), kw_args) end -function handle_segment{P}(lines, line_segments, points::Vector{P}, segment) +function handle_segment(lines, line_segments, points::Vector{P}, segment) where P (isempty(segment) || length(segment) < 2) && return if length(segment) == 2 append!(line_segments, view(points, segment)) @@ -1280,7 +1294,7 @@ function gl_scatter(points, kw_args) 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) + 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 end end @@ -1342,7 +1356,7 @@ function gl_surface(x,y,z, kw_args) end color = get(kw_args, :stroke_color, RGBA{Float32}(0,0,0,1)) kw_args[:color] = color - kw_args[:thickness] = get(kw_args, :stroke_width, 1f0) + kw_args[:thickness] = Float32(get(kw_args, :stroke_width, 1f0)) kw_args[:indices] = faces delete!(kw_args, :stroke_color) delete!(kw_args, :stroke_width) @@ -1358,8 +1372,8 @@ 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)] + I = GLVisualize.Intensity{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 @@ -1367,7 +1381,7 @@ function gl_contour(x, y, z, kw_args) 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))) + zmin, zmax = get(kw_args, :limits, Vec2f0(ignorenan_extrema(z))) cmap = get(kw_args, :color_map, get(kw_args, :color, RGBA{Float32}(0,0,0,1))) colors = RGBA{Float32}[] for c in levels.contours @@ -1388,10 +1402,10 @@ end function gl_heatmap(x,y,z, kw_args) - get!(kw_args, :color_norm, Vec2f0(extrema(z))) + get!(kw_args, :color_norm, Vec2f0(ignorenan_extrema(z))) get!(kw_args, :color_map, Plots.make_gradient(cgrad())) delete!(kw_args, :intensity) - I = GLVisualize.Intensity{1, Float32} + I = GLVisualize.Intensity{Float32} heatmap = I[z[j,i] for i=1:size(z, 2), j=1:size(z, 1)] tex = GLAbstraction.Texture(heatmap, minfilter=:nearest) kw_args[:stroke_width] = 0f0 @@ -1422,6 +1436,8 @@ function label_scatter(d, w, ho) color = get(kw, :color, nothing) kw[:color] = isa(color, Array) ? first(color) : color end + strcolor = get(kw, :stroke_color, RGBA{Float32}(0,0,0,0)) + kw[:stroke_color] = isa(strcolor, Array) ? first(strcolor) : strcolor p = get(kw, :primitive, GeometryTypes.Circle) if isa(p, GLNormalMesh) bb = GeometryTypes.AABB{Float32}(GeometryTypes.vertices(p)) @@ -1436,6 +1452,9 @@ function label_scatter(d, w, ho) kw[:scale] = Vec3f0(w/2) delete!(kw, :offset) end + if isa(p, Array) + kw[:primitive] = GeometryTypes.Circle + end GL.gl_scatter(Point2f0[(w/2, ho)], kw) end @@ -1447,7 +1466,7 @@ function make_label(sp, series, i) d = series.d st = d[:seriestype] kw_args = KW() - if (st in (:path, :path3d)) && d[:linewidth] > 0 + if (st in (:path, :path3d, :straightline)) && d[:linewidth] > 0 points = Point2f0[(0, ho), (w, ho)] kw = KW() extract_linestyle(d, kw) @@ -1473,14 +1492,13 @@ function make_label(sp, series, 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) + ft = legendfont(sp) + font = Plots.Font(ft.family, ft.pointsize, :left, :bottom, 0.0, ft.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)) + push!(result, glvisualize_text(xy, t, kw)) GLAbstraction.Context(result...), i end diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 4fc31a90..483e717f 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -3,6 +3,9 @@ # significant contributions by @jheinen +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "gr.jl")) +end const _gr_attr = merge_with_base_supported([ :annotations, @@ -19,9 +22,17 @@ const _gr_attr = merge_with_base_supported([ :layout, :title, :window_title, :guide, :lims, :ticks, :scale, :flip, - :tickfont, :guidefont, :legendfont, - :grid, :legend, :colorbar, - :marker_z, :levels, + :titlefontfamily, :titlefontsize, :titlefonthalign, :titlefontvalign, + :titlefontrotation, :titlefontcolor, + :legendfontfamily, :legendfontsize, :legendfonthalign, :legendfontvalign, + :legendfontrotation, :legendfontcolor, + :tickfontfamily, :tickfontsize, :tickfonthalign, :tickfontvalign, + :tickfontrotation, :tickfontcolor, + :guidefontfamily, :guidefontsize, :guidefonthalign, :guidefontvalign, + :guidefontrotation, :guidefontcolor, + :grid, :gridalpha, :gridstyle, :gridlinewidth, + :legend, :legendtitle, :colorbar, + :fill_z, :line_z, :marker_z, :levels, :ribbon, :quiver, :orientation, :overwrite_figure, @@ -31,9 +42,13 @@ const _gr_attr = merge_with_base_supported([ :inset_subplots, :bar_width, :arrow, + :framestyle, + :tick_direction, + :camera, + :contour_labels, ]) const _gr_seriestype = [ - :path, :scatter, + :path, :scatter, :straightline, :heatmap, :pie, :image, :contour, :path3d, :scatter3d, :surface, :wireframe, :shape @@ -76,6 +91,8 @@ const gr_markertype = KW( :diamond => -13, :utriangle => -3, :dtriangle => -5, + :ltriangle => -18, + :rtriangle => -17, :pentagon => -21, :hexagon => -22, :heptagon => -23, @@ -118,14 +135,16 @@ const gr_font_family = Dict( # -------------------------------------------------------------------------------------- function gr_getcolorind(c) - GR.settransparency(float(alpha(c))) + gr_set_transparency(float(alpha(c))) convert(Int, GR.inqcolorfromrgb(red(c), green(c), blue(c))) end -gr_set_linecolor(c) = GR.setlinecolorind(gr_getcolorind(cycle(c,1))) -gr_set_fillcolor(c) = GR.setfillcolorind(gr_getcolorind(cycle(c,1))) -gr_set_markercolor(c) = GR.setmarkercolorind(gr_getcolorind(cycle(c,1))) -gr_set_textcolor(c) = GR.settextcolorind(gr_getcolorind(cycle(c,1))) +gr_set_linecolor(c) = GR.setlinecolorind(gr_getcolorind(_cycle(c,1))) +gr_set_fillcolor(c) = GR.setfillcolorind(gr_getcolorind(_cycle(c,1))) +gr_set_markercolor(c) = GR.setmarkercolorind(gr_getcolorind(_cycle(c,1))) +gr_set_textcolor(c) = GR.settextcolorind(gr_getcolorind(_cycle(c,1))) +gr_set_transparency(α::Real) = GR.settransparency(clamp(α, 0, 1)) +function gr_set_transparency(::Void) end # -------------------------------------------------------------------------------------- @@ -172,55 +191,89 @@ function gr_polyline(x, y, func = GR.polyline; arrowside=:none) end end +gr_inqtext(x, y, s::Symbol) = gr_inqtext(x, y, string(s)) + function gr_inqtext(x, y, s) if length(s) >= 2 && s[1] == '$' && s[end] == '$' GR.inqtextext(x, y, s[2:end-1]) - elseif search(s, '\\') != 0 || search(s, '_') != 0 || search(s, '^') != 0 + elseif search(s, '\\') != 0 || contains(s, "10^{") GR.inqtextext(x, y, s) else GR.inqtext(x, y, s) end end +gr_text(x, y, s::Symbol) = gr_text(x, y, string(s)) + function gr_text(x, y, s) if length(s) >= 2 && s[1] == '$' && s[end] == '$' GR.mathtex(x, y, s[2:end-1]) - elseif search(s, '\\') != 0 || search(s, '_') != 0 || search(s, '^') != 0 + elseif search(s, '\\') != 0 || contains(s, "10^{") GR.textext(x, y, s) else GR.text(x, y, s) end end -function gr_polaraxes(rmin, rmax) +function gr_polaraxes(rmin::Real, rmax::Real, sp::Subplot) GR.savestate() - GR.setlinetype(GR.LINETYPE_SOLID) - GR.setlinecolorind(88) - tick = 0.5 * GR.tick(rmin, rmax) - n = round(Int, (rmax - rmin) / tick + 0.5) - for i in 0:n - r = float(i) / n - if i % 2 == 0 - GR.setlinecolorind(88) - if i > 0 - GR.drawarc(-r, r, -r, r, 0, 359) - end - GR.settextalign(GR.TEXT_HALIGN_LEFT, GR.TEXT_VALIGN_HALF) - x, y = GR.wctondc(0.05, r) - GR.text(x, y, string(signif(rmin + i * tick, 12))) - else - GR.setlinecolorind(90) - GR.drawarc(-r, r, -r, r, 0, 359) + xaxis = sp[:xaxis] + yaxis = sp[:yaxis] + + α = 0:45:315 + a = α .+ 90 + sinf = sind.(a) + cosf = cosd.(a) + rtick_values, rtick_labels = get_ticks(yaxis) + if yaxis[:formatter] in (:scientific, :auto) && yaxis[:ticks] in (:auto, :native) + rtick_labels = convert_sci_unicode.(rtick_labels) + end + + #draw angular grid + if xaxis[:grid] + gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) + gr_set_transparency(xaxis[:gridalpha]) + for i in 1:length(α) + GR.polyline([sinf[i], 0], [cosf[i], 0]) end end - for α in 0:45:315 - a = α + 90 - sinf = sin(a * pi / 180) - cosf = cos(a * pi / 180) - GR.polyline([sinf, 0], [cosf, 0]) - GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_HALF) - x, y = GR.wctondc(1.1 * sinf, 1.1 * cosf) - GR.textext(x, y, string(α, "^o")) + + #draw radial grid + if yaxis[:grid] + gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) + gr_set_transparency(yaxis[:gridalpha]) + for i in 1:length(rtick_values) + r = (rtick_values[i] - rmin) / (rmax - rmin) + if r <= 1.0 && r >= 0.0 + GR.drawarc(-r, r, -r, r, 0, 359) + end + end + GR.drawarc(-1, 1, -1, 1, 0, 359) + end + + #prepare to draw ticks + gr_set_transparency(1) + GR.setlinecolorind(90) + GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_HALF) + + #draw angular ticks + if xaxis[:showaxis] + GR.drawarc(-1, 1, -1, 1, 0, 359) + for i in 1:length(α) + x, y = GR.wctondc(1.1 * sinf[i], 1.1 * cosf[i]) + GR.textext(x, y, string((360-α[i])%360, "^o")) + end + end + + #draw radial ticks + if yaxis[:showaxis] + for i in 1:length(rtick_values) + r = (rtick_values[i] - rmin) / (rmax - rmin) + if r <= 1.0 && r >= 0.0 + x, y = GR.wctondc(0.05, r) + gr_text(x, y, _cycle(rtick_labels, i)) + end + end end GR.restorestate() end @@ -256,13 +309,15 @@ function gr_fill_viewport(vp::AVec{Float64}, c) end -normalize_zvals(zv::Void) = zv -function normalize_zvals(zv::AVec) - vmin, vmax = extrema(zv) +normalize_zvals(args...) = nothing +function normalize_zvals(zv::AVec, clims::NTuple{2, <:Real}) + vmin, vmax = ignorenan_extrema(zv) + isfinite(clims[1]) && (vmin = clims[1]) + isfinite(clims[2]) && (vmax = clims[2]) if vmin == vmax zeros(length(zv)) else - (zv - vmin) ./ (vmax - vmin) + clamp.((zv - vmin) ./ (vmax - vmin), 0, 1) end end @@ -273,17 +328,19 @@ function gr_draw_marker(xi, yi, msize, shape::Shape) sx, sy = coords(shape) # convert to ndc coords (percentages of window) GR.selntran(0) + w, h = gr_plot_size + f = msize / (w + h) xi, yi = GR.wctondc(xi, yi) - 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.fillarea(xi .+ sx .* f, + yi .+ sy .* f) GR.selntran(1) end # draw ONE symbol marker function gr_draw_marker(xi, yi, msize::Number, shape::Symbol) GR.setmarkertype(gr_markertype[shape]) - GR.setmarkersize(0.3msize) + w, h = gr_plot_size + GR.setmarkersize(0.3msize / ((w + h) * 0.001)) GR.polymarker([xi], [yi]) end @@ -293,46 +350,40 @@ function gr_draw_markers(series::Series, x, y, msize, mz) shapes = series[:markershape] if shapes != :none for i=1:length(x) - msi = cycle(msize, i) - shape = cycle(shapes, i) + 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 # draw a filled in shape, slightly bigger, to estimate a stroke if series[:markerstrokewidth] > 0 - cfunc(cycle(series[:markerstrokecolor], i)) #, series[:markerstrokealpha]) + cfunc(get_markerstrokecolor(series, i)) + gr_set_transparency(get_markerstrokealpha(series, i)) gr_draw_marker(x[i], y[i], msi + series[:markerstrokewidth], shape) end - # draw the shape - if mz == nothing - cfunc(cycle(series[:markercolor], i)) #, series[:markeralpha]) - else - # pick a color from the pre-loaded gradient - ci = round(Int, 1000 + cycle(mz, i) * 255) - cfuncind(ci) - GR.settransparency(_gr_gradient_alpha[ci-999]) + # draw the shape - don't draw filled area if marker shape is 1D + if !(shape in (:hline, :vline, :+, :x)) + cfunc(get_markercolor(series, i)) + gr_set_transparency(get_markeralpha(series, i)) + gr_draw_marker(x[i], y[i], msi, shape) end - gr_draw_marker(x[i], y[i], msi, shape) end end end -function gr_draw_markers(series::Series, x, y) +function gr_draw_markers(series::Series, x, y, clims) isempty(x) && return - mz = normalize_zvals(series[:marker_z]) + mz = normalize_zvals(series[:marker_z], clims) GR.setfillintstyle(GR.INTSTYLE_SOLID) gr_draw_markers(series, x, y, series[:markersize], mz) - if mz != nothing - gr_colorbar(series[:subplot]) - end end # --------------------------------------------------------- -function gr_set_line(w, style, c) #, a) +function gr_set_line(lw, style, c) #, a) GR.setlinetype(gr_linetype[style]) - GR.setlinewidth(w) + w, h = gr_plot_size + GR.setlinewidth(_gr_thickness_scaling[1] * max(0, lw / ((w + h) * 0.001))) gr_set_linecolor(c) #, a) end @@ -344,7 +395,8 @@ function gr_set_fill(c) #, a) end # this stores the conversion from a font pointsize to "percentage of window height" (which is what GR uses) -const _gr_point_mult = zeros(1) +const _gr_point_mult = 0.0018 * ones(1) +const _gr_thickness_scaling = ones(1) # set the font attributes... assumes _gr_point_mult has been populated already function gr_set_font(f::Font; halign = f.halign, valign = f.valign, @@ -375,22 +427,33 @@ end const viewport_plotarea = zeros(4) # the size of the current plot in pixels -const gr_plot_size = zeros(2) +const gr_plot_size = [600.0, 400.0] -function gr_viewport_from_bbox(bb::BoundingBox, w, h, viewport_canvas) +function gr_viewport_from_bbox(sp::Subplot{GRBackend}, bb::BoundingBox, w, h, viewport_canvas) viewport = zeros(4) viewport[1] = viewport_canvas[2] * (left(bb) / w) viewport[2] = viewport_canvas[2] * (right(bb) / w) viewport[3] = viewport_canvas[4] * (1.0 - bottom(bb) / h) viewport[4] = viewport_canvas[4] * (1.0 - top(bb) / h) + if is3d(sp) + vp = viewport[:] + extent = min(vp[2] - vp[1], vp[4] - vp[3]) + viewport[1] = 0.5 * (vp[1] + vp[2] - extent) + viewport[2] = 0.5 * (vp[1] + vp[2] + extent) + viewport[3] = 0.5 * (vp[3] + vp[4] - extent) + viewport[4] = 0.5 * (vp[3] + vp[4] + extent) + end + if hascolorbar(sp) + viewport[2] -= 0.1 + end viewport end # change so we're focused on the viewport area function gr_set_viewport_cmap(sp::Subplot) GR.setviewport( - viewport_plotarea[2] + (is3d(sp) ? 0.04 : 0.02), - viewport_plotarea[2] + (is3d(sp) ? 0.07 : 0.05), + viewport_plotarea[2] + (is3d(sp) ? 0.07 : 0.02), + viewport_plotarea[2] + (is3d(sp) ? 0.10 : 0.05), viewport_plotarea[3], viewport_plotarea[4] ) @@ -411,33 +474,56 @@ function gr_set_viewport_polar() ymax -= 0.05 * (xmax - xmin) xcenter = 0.5 * (xmin + xmax) ycenter = 0.5 * (ymin + ymax) - r = 0.5 * min(xmax - xmin, ymax - ymin) + r = 0.5 * NaNMath.min(xmax - xmin, ymax - ymin) GR.setviewport(xcenter -r, xcenter + r, ycenter - r, ycenter + r) GR.setwindow(-1, 1, -1, 1) r end # add the colorbar -function gr_colorbar(sp::Subplot) - if sp[:colorbar] != :none - gr_set_viewport_cmap(sp) - GR.colormap() - gr_set_viewport_plotarea() - end +function gr_colorbar(sp::Subplot, clims) + xmin, xmax = gr_xy_axislims(sp)[1:2] + gr_set_viewport_cmap(sp) + l = zeros(Int32, 1, 256) + l[1,:] = Int[round(Int, _i) for _i in linspace(1000, 1255, 256)] + GR.setscale(0) + GR.setwindow(xmin, xmax, clims[1], clims[2]) + GR.cellarray(xmin, xmax, clims[2], clims[1], 1, length(l), l) + ztick = 0.5 * GR.tick(clims[1], clims[2]) + GR.axes(0, ztick, xmax, clims[1], 0, 1, 0.005) + gr_set_viewport_plotarea() end gr_view_xcenter() = 0.5 * (viewport_plotarea[1] + viewport_plotarea[2]) 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 +function gr_legend_pos(s::Symbol,w,h) + str = string(s) + if str == "best" + str = "topright" + end + if contains(str,"right") + xpos = viewport_plotarea[2] - 0.05 - w + elseif contains(str,"left") + xpos = viewport_plotarea[1] + 0.11 + else + xpos = (viewport_plotarea[2]-viewport_plotarea[1])/2 - w/2 +.04 + end + if contains(str,"top") + ypos = viewport_plotarea[4] - 0.06 + elseif contains(str,"bottom") + ypos = viewport_plotarea[3] + h + 0.06 + else + ypos = (viewport_plotarea[4]-viewport_plotarea[3])/2 + h/2 + end + (xpos,ypos) end +function gr_legend_pos(v::Tuple{S,T},w,h) where {S<:Real, T<:Real} + xpos = v[1] * (viewport_plotarea[2] - viewport_plotarea[1]) + viewport_plotarea[1] + ypos = v[2] * (viewport_plotarea[4] - viewport_plotarea[3]) + viewport_plotarea[3] + (xpos,ypos) +end # -------------------------------------------------------------------------------------- @@ -454,9 +540,12 @@ function gr_set_gradient(c) end # this is our new display func... set up the viewport_canvas, compute bounding boxes, and display each subplot -function gr_display(plt::Plot) +function gr_display(plt::Plot, fmt="") GR.clearws() + _gr_thickness_scaling[1] = plt[:thickness_scaling] + dpi_factor = plt[:dpi] / Plots.DPI + # collect some monitor/display sizes in meters and pixels display_width_meters, display_height_meters, display_width_px, display_height_px = GR.inqdspsize() display_width_ratio = display_width_meters / display_width_px @@ -468,14 +557,14 @@ function gr_display(plt::Plot) gr_plot_size[:] = [w, h] if w > h ratio = float(h) / w - msize = display_width_ratio * w + msize = display_width_ratio * w * dpi_factor GR.setwsviewport(0, msize, 0, msize * ratio) GR.setwswindow(0, 1, 0, ratio) viewport_canvas[3] *= ratio viewport_canvas[4] *= ratio else ratio = float(w) / h - msize = display_height_ratio * h + msize = display_height_ratio * h * dpi_factor GR.setwsviewport(0, msize * ratio, 0, msize) GR.setwswindow(0, ratio, 0, 1) viewport_canvas[1] *= ratio @@ -487,7 +576,7 @@ function gr_display(plt::Plot) # update point mult px_per_pt = px / pt - _gr_point_mult[1] = 1.5 * px_per_pt / max(h,w) + _gr_point_mult[1] = 1.5 * _gr_thickness_scaling[1] * px_per_pt / max(h,w) # subplots: for sp in plt.subplots @@ -498,12 +587,96 @@ function gr_display(plt::Plot) end +function gr_set_xticks_font(sp) + flip = sp[:yaxis][:flip] + mirror = sp[:xaxis][:mirror] + gr_set_font(tickfont(sp[:xaxis]), + halign = (:left, :hcenter, :right)[sign(sp[:xaxis][:rotation]) + 2], + valign = (mirror ? :bottom : :top), + rotation = sp[:xaxis][:rotation]) + return flip, mirror +end + + +function gr_set_yticks_font(sp) + flip = sp[:xaxis][:flip] + mirror = sp[:yaxis][:mirror] + gr_set_font(tickfont(sp[:yaxis]), + halign = (mirror ? :left : :right), + valign = (:top, :vcenter, :bottom)[sign(sp[:yaxis][:rotation]) + 2], + rotation = sp[:yaxis][:rotation]) + return flip, mirror +end + +function gr_get_ticks_size(ticks, i) + GR.savestate() + GR.selntran(0) + l = 0.0 + for (cv, dv) in zip(ticks...) + tb = gr_inqtext(0, 0, string(dv))[i] + tb_min, tb_max = extrema(tb) + l = max(l, tb_max - tb_min) + end + GR.restorestate() + return l +end + +function _update_min_padding!(sp::Subplot{GRBackend}) + dpi = sp.plt[:thickness_scaling] + if !haskey(ENV, "GKSwstype") + if isijulia() || (isdefined(Main, :Juno) && Juno.isactive()) + ENV["GKSwstype"] = "svg" + end + end + # Add margin given by the user + leftpad = 4mm + sp[:left_margin] + toppad = 2mm + sp[:top_margin] + rightpad = 4mm + sp[:right_margin] + bottompad = 2mm + sp[:bottom_margin] + # Add margin for title + if sp[:title] != "" + toppad += 5mm + end + # Add margin for x and y ticks + xticks, yticks = axis_drawing_info(sp)[1:2] + if !(xticks in (nothing, false, :none)) + flip, mirror = gr_set_xticks_font(sp) + l = gr_get_ticks_size(xticks, 2) + if mirror + toppad += 1mm + gr_plot_size[2] * l * px + else + bottompad += 1mm + gr_plot_size[2] * l * px + end + end + if !(yticks in (nothing, false, :none)) + flip, mirror = gr_set_yticks_font(sp) + l = gr_get_ticks_size(yticks, 1) + if mirror + rightpad += 1mm + gr_plot_size[1] * l * px + else + leftpad += 1mm + gr_plot_size[1] * l * px + end + end + # Add margin for x label + if sp[:xaxis][:guide] != "" + bottompad += 4mm + end + # Add margin for y label + if sp[:yaxis][:guide] != "" + leftpad += 4mm + end + sp.minpad = Tuple(dpi * [leftpad, toppad, rightpad, bottompad]) +end + function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) + _update_min_padding!(sp) + # the viewports for this subplot - viewport_subplot = gr_viewport_from_bbox(bbox(sp), w, h, viewport_canvas) - viewport_plotarea[:] = gr_viewport_from_bbox(plotarea(sp), w, h, viewport_canvas) + viewport_subplot = gr_viewport_from_bbox(sp, bbox(sp), w, h, viewport_canvas) + viewport_plotarea[:] = gr_viewport_from_bbox(sp, plotarea(sp), w, h, viewport_canvas) # get data limits data_lims = gr_xy_axislims(sp) + xy_lims = data_lims ratio = sp[:aspect_ratio] if ratio != :none @@ -532,27 +705,30 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # reduced from before... set some flags based on the series in this subplot # TODO: can these be generic flags? outside_ticks = false - cmap = false - draw_axes = true + cmap = hascolorbar(sp) + draw_axes = sp[:framestyle] != :none # axes_2d = true for series in series_list(sp) st = series[:seriestype] - if st in (:contour, :surface, :heatmap) || series[:marker_z] != nothing - cmap = true - end if st == :pie draw_axes = false end if st == :heatmap outside_ticks = true + for ax in (sp[:xaxis], sp[:yaxis]) + v = series[ax[:letter]] + if diff(collect(extrema(diff(v))))[1] > 1e-6*std(v) + warn("GR: heatmap only supported with equally spaced data.") + end + end + x, y = heatmap_edges(series[:x], sp[:xaxis][:scale]), heatmap_edges(series[:y], sp[:yaxis][:scale]) + xy_lims = x[1], x[end], y[1], y[end] + expand_extrema!(sp[:xaxis], x) + expand_extrema!(sp[:yaxis], y) + data_lims = gr_xy_axislims(sp) end end - if cmap && sp[:colorbar] != :none - # note: add extra midpadding on the right for the colorbar - viewport_plotarea[2] -= 0.1 - end - # set our plot area view gr_set_viewport_plotarea() @@ -595,9 +771,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) end # draw the axes - gr_set_font(xaxis[:tickfont]) - gr_set_textcolor(xaxis[:foreground_color_text]) - GR.setlinewidth(1) + gr_set_font(tickfont(xaxis)) + GR.setlinewidth(sp.plt[:thickness_scaling]) if is3d(sp) zmin, zmax = gr_lims(zaxis, true) @@ -606,125 +781,190 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) isfinite(clims[1]) && (zmin = clims[1]) isfinite(clims[2]) && (zmax = clims[2]) end - GR.setspace(zmin, zmax, 40, 70) + GR.setspace(zmin, zmax, round.(Int, sp[:camera])...) xtick = GR.tick(xmin, xmax) / 2 ytick = GR.tick(ymin, ymax) / 2 ztick = GR.tick(zmin, zmax) / 2 ticksize = 0.01 * (viewport_plotarea[2] - viewport_plotarea[1]) - # GR.setlinetype(GR.LINETYPE_DOTTED) - if sp[:grid] - GR.grid3d(xtick, 0, ztick, xmin, ymax, zmin, 2, 0, 2) + if xaxis[:grid] + gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) + gr_set_transparency(xaxis[:gridalpha]) + GR.grid3d(xtick, 0, 0, xmin, ymax, zmin, 2, 0, 0) + end + if yaxis[:grid] + gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) + gr_set_transparency(yaxis[:gridalpha]) GR.grid3d(0, ytick, 0, xmin, ymax, zmin, 0, 2, 0) end + if zaxis[:grid] + gr_set_line(zaxis[:gridlinewidth], zaxis[:gridstyle], zaxis[:foreground_color_grid]) + gr_set_transparency(zaxis[:gridalpha]) + GR.grid3d(0, 0, ztick, xmin, ymax, zmin, 0, 0, 2) + end + gr_set_line(1, :solid, xaxis[:foreground_color_axis]) + gr_set_transparency(1) GR.axes3d(xtick, 0, ztick, xmin, ymin, zmin, 2, 0, 2, -ticksize) GR.axes3d(0, ytick, 0, xmax, ymin, zmin, 0, 2, 0, ticksize) elseif ispolar(sp) r = gr_set_viewport_polar() - rmin, rmax = GR.adjustrange(minimum(r), maximum(r)) - # rmin, rmax = axis_limits(sp[:yaxis]) - gr_polaraxes(rmin, rmax) + #rmin, rmax = GR.adjustrange(ignorenan_minimum(r), ignorenan_maximum(r)) + rmin, rmax = axis_limits(sp[:yaxis]) + gr_polaraxes(rmin, rmax, sp) elseif draw_axes if xmax > xmin && ymax > ymin GR.setwindow(xmin, xmax, ymin, ymax) end - xticks, yticks, spine_segs, grid_segs = axis_drawing_info(sp) + xticks, yticks, xspine_segs, yspine_segs, xtick_segs, ytick_segs, xgrid_segs, ygrid_segs, xborder_segs, yborder_segs = axis_drawing_info(sp) # @show xticks yticks #spine_segs grid_segs # draw the grid lines - if sp[:grid] + if xaxis[:grid] # gr_set_linecolor(sp[:foreground_color_grid]) # GR.grid(xtick, ytick, 0, 0, majorx, majory) - gr_set_line(1, :dot, sp[:foreground_color_grid]) - GR.settransparency(0.5) - gr_polyline(coords(grid_segs)...) + gr_set_line(xaxis[:gridlinewidth], xaxis[:gridstyle], xaxis[:foreground_color_grid]) + gr_set_transparency(xaxis[:gridalpha]) + gr_polyline(coords(xgrid_segs)...) end - GR.settransparency(1.0) + if yaxis[:grid] + gr_set_line(yaxis[:gridlinewidth], yaxis[:gridstyle], yaxis[:foreground_color_grid]) + gr_set_transparency(yaxis[:gridalpha]) + gr_polyline(coords(ygrid_segs)...) + end + gr_set_transparency(1.0) - # spine (border) and tick marks - gr_set_line(1, :solid, sp[:xaxis][:foreground_color_axis]) - gr_polyline(coords(spine_segs)...) + # axis lines + if xaxis[:showaxis] + gr_set_line(1, :solid, xaxis[:foreground_color_axis]) + GR.setclip(0) + gr_polyline(coords(xspine_segs)...) + end + if yaxis[:showaxis] + gr_set_line(1, :solid, yaxis[:foreground_color_axis]) + GR.setclip(0) + gr_polyline(coords(yspine_segs)...) + end + GR.setclip(1) - if !(xticks in (nothing, false)) + # axis ticks + if xaxis[:showaxis] + if sp[:framestyle] in (:zerolines, :grid) + gr_set_line(1, :solid, xaxis[:foreground_color_grid]) + gr_set_transparency(xaxis[:gridalpha]) + else + gr_set_line(1, :solid, xaxis[:foreground_color_axis]) + end + GR.setclip(0) + gr_polyline(coords(xtick_segs)...) + end + if yaxis[:showaxis] + if sp[:framestyle] in (:zerolines, :grid) + gr_set_line(1, :solid, yaxis[:foreground_color_grid]) + gr_set_transparency(yaxis[:gridalpha]) + else + gr_set_line(1, :solid, yaxis[:foreground_color_axis]) + end + GR.setclip(0) + gr_polyline(coords(ytick_segs)...) + end + GR.setclip(1) + + # tick marks + if !(xticks in (:none, nothing, false)) && xaxis[:showaxis] # x labels - flip = sp[:yaxis][:flip] - mirror = sp[:xaxis][:mirror] - gr_set_font(sp[:xaxis][:tickfont], - valign = (mirror ? :bottom : :top), - color = sp[:xaxis][:foreground_color_axis], - rotation = sp[:xaxis][:rotation]) + flip, mirror = gr_set_xticks_font(sp) for (cv, dv) in zip(xticks...) # use xor ($) to get the right y coords - xi, yi = GR.wctondc(cv, (flip $ mirror) ? ymax : ymin) + xi, yi = GR.wctondc(cv, sp[:framestyle] == :origin ? 0 : xor(flip, mirror) ? ymax : ymin) # @show cv dv ymin xi yi flip mirror (flip $ mirror) - gr_text(xi, yi + (mirror ? 1 : -1) * 2e-3, string(dv)) + if xaxis[:ticks] in (:auto, :native) + # ensure correct dispatch in gr_text for automatic log ticks + if xaxis[:scale] in _logScales + dv = string(dv, "\\ ") + elseif xaxis[:formatter] in (:scientific, :auto) + dv = convert_sci_unicode(dv) + end + end + gr_text(xi, yi + (mirror ? 1 : -1) * 5e-3 * (xaxis[:tick_direction] == :out ? 1.5 : 1.0), string(dv)) end end - if !(yticks in (nothing, false)) + if !(yticks in (:none, nothing, false)) && yaxis[:showaxis] # y labels - flip = sp[:xaxis][:flip] - mirror = sp[:yaxis][:mirror] - gr_set_font(sp[:yaxis][:tickfont], - halign = (mirror ? :left : :right), - color = sp[:yaxis][:foreground_color_axis], - rotation = sp[:yaxis][:rotation]) + flip, mirror = gr_set_yticks_font(sp) for (cv, dv) in zip(yticks...) # use xor ($) to get the right y coords - xi, yi = GR.wctondc((flip $ mirror) ? xmax : xmin, cv) + xi, yi = GR.wctondc(sp[:framestyle] == :origin ? 0 : xor(flip, mirror) ? xmax : xmin, cv) # @show cv dv xmin xi yi - gr_text(xi + (mirror ? 1 : -1) * 2e-3, yi, string(dv)) + if yaxis[:ticks] in (:auto, :native) + # ensure correct dispatch in gr_text for automatic log ticks + if yaxis[:scale] in _logScales + dv = string(dv, "\\ ") + elseif yaxis[:formatter] in (:scientific, :auto) + dv = convert_sci_unicode(dv) + end + end + gr_text(xi + (mirror ? 1 : -1) * 1e-2 * (yaxis[:tick_direction] == :out ? 1.5 : 1.0), yi, string(dv)) end end - # window_diag = sqrt(gr_view_xdiff()^2 + gr_view_ydiff()^2) - # ticksize = 0.0075 * window_diag - # if outside_ticks - # ticksize = -ticksize - # end - # # TODO: this should be done for each axis separately - # gr_set_linecolor(xaxis[:foreground_color_axis]) - - # x1, x2 = xaxis[:flip] ? (xmax,xmin) : (xmin,xmax) - # y1, y2 = yaxis[:flip] ? (ymax,ymin) : (ymin,ymax) - # GR.axes(xtick, ytick, x1, y1, 1, 1, ticksize) - # GR.axes(xtick, ytick, x2, y2, -1, -1, -ticksize) + # border + intensity = sp[:framestyle] == :semi ? 0.5 : 1.0 + if sp[:framestyle] in (:box, :semi) + gr_set_line(intensity, :solid, xaxis[:foreground_color_border]) + gr_set_transparency(intensity) + gr_polyline(coords(xborder_segs)...) + gr_set_line(intensity, :solid, yaxis[:foreground_color_border]) + gr_set_transparency(intensity) + gr_polyline(coords(yborder_segs)...) + end end # end # add the guides GR.savestate() if sp[:title] != "" - gr_set_font(sp[:titlefont]) - GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_TOP) - gr_set_textcolor(sp[:foreground_color_title]) - gr_text(gr_view_xcenter(), viewport_subplot[4], sp[:title]) + gr_set_font(titlefont(sp)) + loc = sp[:title_location] + if loc == :left + xpos = viewport_plotarea[1] + halign = GR.TEXT_HALIGN_LEFT + elseif loc == :right + xpos = viewport_plotarea[2] + halign = GR.TEXT_HALIGN_RIGHT + else + xpos = gr_view_xcenter() + halign = GR.TEXT_HALIGN_CENTER + end + GR.settextalign(halign, GR.TEXT_VALIGN_TOP) + gr_text(xpos, viewport_subplot[4], sp[:title]) end if xaxis[:guide] != "" - gr_set_font(xaxis[:guidefont]) + gr_set_font(guidefont(xaxis)) GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_BOTTOM) - gr_set_textcolor(xaxis[:foreground_color_guide]) gr_text(gr_view_xcenter(), viewport_subplot[3], xaxis[:guide]) end if yaxis[:guide] != "" - gr_set_font(yaxis[:guidefont]) + gr_set_font(guidefont(yaxis)) GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_TOP) GR.setcharup(-1, 0) - gr_set_textcolor(yaxis[:foreground_color_guide]) gr_text(viewport_subplot[1], gr_view_ycenter(), yaxis[:guide]) end GR.restorestate() - gr_set_font(xaxis[:tickfont]) + gr_set_font(tickfont(xaxis)) # this needs to be here to point the colormap to the right indices GR.setcolormap(1000 + GR.COLORMAP_COOLWARM) + # calculate the colorbar limits once for a subplot + clims = get_clims(sp) + for (idx, series) in enumerate(series_list(sp)) st = series[:seriestype] @@ -733,6 +973,10 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) gr_set_gradient(series[:fillcolor]) #, series[:fillalpha]) elseif series[:marker_z] != nothing series[:markercolor] = gr_set_gradient(series[:markercolor]) + elseif series[:line_z] != nothing + series[:linecolor] = gr_set_gradient(series[:linecolor]) + elseif series[:fill_z] != nothing + series[:fillcolor] = gr_set_gradient(series[:fillcolor]) end GR.savestate() @@ -757,10 +1001,6 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) # 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 z = vec(transpose_z(series, z.surf, false)) elseif ispolar(sp) if frng != nothing @@ -769,58 +1009,67 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) x, y = convert_to_polar(x, y, (rmin, rmax)) end - if st in (:path, :scatter) + if st == :straightline + x, y = straightline_data(series) + end + + if st in (:path, :scatter, :straightline) if length(x) > 1 + lz = series[:line_z] + segments = iter_segments(series) # do area fill if frng != nothing GR.setfillintstyle(GR.INTSTYLE_SOLID) fr_from, fr_to = (is_2tuple(frng) ? frng : (y, frng)) - for (i,rng) in enumerate(iter_segments(series[:x], series[:y])) - if length(rng) > 1 - gr_set_fillcolor(cycle(series[:fillcolor], i)) - fx = cycle(x, vcat(rng, reverse(rng))) - fy = vcat(cycle(fr_from,rng), cycle(fr_to,reverse(rng))) - # @show i rng fx fy - GR.fillarea(fx, fy) - end + for (i, rng) in enumerate(segments) + gr_set_fillcolor(get_fillcolor(series, i)) + fx = _cycle(x, vcat(rng, reverse(rng))) + fy = vcat(_cycle(fr_from,rng), _cycle(fr_to,reverse(rng))) + gr_set_transparency(get_fillalpha(series, i)) + GR.fillarea(fx, fy) end end # draw the line(s) - if st == :path - gr_set_line(series[:linewidth], series[:linestyle], series[:linecolor]) #, series[:linealpha]) - arrowside = isa(series[:arrow], Arrow) ? series[:arrow].side : :none - gr_polyline(x, y; arrowside = arrowside) + if st in (:path, :straightline) + for (i, rng) in enumerate(segments) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) #, series[:linealpha]) + gr_set_transparency(get_linealpha(series, i)) + arrowside = isa(series[:arrow], Arrow) ? series[:arrow].side : :none + gr_polyline(x[rng], y[rng]; arrowside = arrowside) + end end end if series[:markershape] != :none - gr_draw_markers(series, x, y) + gr_draw_markers(series, x, y, clims) end elseif st == :contour - zmin, zmax = gr_lims(zaxis, false) - clims = sp[:clims] - if is_2tuple(clims) - isfinite(clims[1]) && (zmin = clims[1]) - isfinite(clims[2]) && (zmax = clims[2]) - end - if typeof(series[:levels]) <: Array + zmin, zmax = clims + GR.setspace(zmin, zmax, 0, 90) + if typeof(series[:levels]) <: AbstractArray h = series[:levels] else - h = linspace(zmin, zmax, series[:levels]) + h = series[:levels] > 1 ? linspace(zmin, zmax, series[:levels]) : [(zmin + zmax) / 2] end - GR.setspace(zmin, zmax, 0, 90) if series[:fillrange] != nothing GR.surface(x, y, z, GR.OPTION_CELL_ARRAY) else - GR.contour(x, y, h, z, 1000) + GR.setlinetype(gr_linetype[get_linestyle(series)]) + GR.setlinewidth(max(0, get_linewidth(series) / (sum(gr_plot_size) * 0.001))) + if plot_color(series[:linecolor]) == [plot_color(:black)] + GR.contour(x, y, h, z, 0 + (series[:contour_labels] == true ? 1 : 0)) + else + GR.contour(x, y, h, z, 1000 + (series[:contour_labels] == true ? 1 : 0)) + end end # create the colorbar of contour levels - if sp[:colorbar] != :none + if cmap + gr_set_line(1, :solid, yaxis[:foreground_color_axis]) gr_set_viewport_cmap(sp) - l = round(Int32, 1000 + (h - minimum(h)) / (maximum(h) - minimum(h)) * 255) + l = (length(h) > 1) ? round.(Int32, 1000 + (h - ignorenan_minimum(h)) / (ignorenan_maximum(h) - ignorenan_minimum(h)) * 255) : Int32[1000, 1255] GR.setwindow(xmin, xmax, zmin, zmax) GR.cellarray(xmin, xmax, zmax, zmin, 1, length(l), l) ztick = 0.5 * GR.tick(zmin, zmax) @@ -839,37 +1088,42 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) GR.setfillcolorind(0) GR.surface(x, y, z, GR.OPTION_FILLED_MESH) end - cmap && gr_colorbar(sp) elseif st == :heatmap - 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 + xmin, xmax, ymin, ymax = xy_lims + zmin, zmax = clims + m, n = length(x), length(y) + xinds = sort(1:m, rev = xaxis[:flip]) + yinds = sort(1:n, rev = yaxis[:flip]) + z = reshape(reshape(z, m, n)[xinds, yinds], m*n) + GR.setspace(zmin, zmax, 0, 90) grad = isa(series[:fillcolor], ColorGradient) ? series[:fillcolor] : cgrad() - colors = [grad[clamp((zi-zmin) / (zmax-zmin), 0, 1)] for zi=z] + colors = [plot_color(grad[clamp((zi-zmin) / (zmax-zmin), 0, 1)], series[:fillalpha]) 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) + w, h = length(x), length(y) + GR.drawimage(xmin, xmax, ymax, ymin, w, h, rgba) elseif st in (:path3d, :scatter3d) # draw path if st == :path3d if length(x) > 1 - gr_set_line(series[:linewidth], series[:linestyle], series[:linecolor]) #, series[:linealpha]) - GR.polyline3d(x, y, z) + lz = series[:line_z] + segments = iter_segments(series) + for (i, rng) in enumerate(segments) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) #, series[:linealpha]) + gr_set_transparency(get_linealpha(series, i)) + GR.polyline3d(x[rng], y[rng], z[rng]) + end end end # draw markers if st == :scatter3d || series[:markershape] != :none x2, y2 = unzip(map(GR.wc3towc, x, y, z)) - gr_draw_markers(series, x2, y2) + gr_draw_markers(series, x2, y2, clims) end # TODO: replace with pie recipe @@ -877,7 +1131,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) GR.selntran(0) GR.setfillintstyle(GR.INTSTYLE_SOLID) xmin, xmax, ymin, ymax = viewport_plotarea - ymax -= 0.05 * (xmax - xmin) + ymax -= 0.1 * (xmax - xmin) xcenter = 0.5 * (xmin + xmax) ycenter = 0.5 * (ymin + ymax) if xmax - xmin > ymax - ymin @@ -921,30 +1175,37 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) GR.selntran(1) elseif st == :shape - for (i,rng) in enumerate(iter_segments(series[:x], series[:y])) + x, y = shape_data(series) + for (i,rng) in enumerate(iter_segments(x, y)) if length(rng) > 1 # connect to the beginning rng = vcat(rng, rng[1]) # get the segments - x, y = series[:x][rng], series[:y][rng] + xseg, yseg = x[rng], y[rng] # draw the interior - gr_set_fill(cycle(series[:fillcolor], i)) - GR.fillarea(x, y) + gr_set_fill(get_fillcolor(series, i)) + gr_set_transparency(get_fillalpha(series, i)) + GR.fillarea(xseg, yseg) # draw the shapes - gr_set_line(series[:linewidth], :solid, cycle(series[:linecolor], i)) - GR.polyline(x, y) + gr_set_line(get_linewidth(series, i), get_linestyle(series, i), get_linecolor(series, i)) + gr_set_transparency(get_linealpha(series, i)) + GR.polyline(xseg, yseg) end end elseif st == :image - z = transpose_z(series, series[:z].surf, true) - h, w = size(z) + z = transpose_z(series, series[:z].surf, true)' + w, h = length(x), length(y) + xinds = sort(1:w, rev = xaxis[:flip]) + yinds = sort(1:h, rev = yaxis[:flip]) + z = z[xinds, yinds] + xmin, xmax = ignorenan_extrema(series[:x]); ymin, ymax = ignorenan_extrema(series[:y]) if eltype(z) <: Colors.AbstractGray - grey = round(UInt8, float(z) * 255) + grey = round.(UInt8, float(z) * 255) rgba = map(c -> UInt32( 0xff000000 + Int(c)<<16 + Int(c)<<8 + Int(c) ), grey) else rgba = map(c -> UInt32( round(Int, alpha(c) * 255) << 24 + @@ -962,6 +1223,13 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) gr_text(GR.wctondc(xi, yi)..., str) end + # draw the colorbar + if cmap && st != :contour # special colorbar with steps is drawn for contours + gr_set_line(1, :solid, yaxis[:foreground_color_axis]) + gr_set_transparency(1) + gr_colorbar(sp, clims) + end + GR.restorestate() end @@ -971,10 +1239,15 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) GR.savestate() GR.selntran(0) GR.setscale(0) - gr_set_font(sp[:legendfont]) + gr_set_font(legendfont(sp)) w = 0 i = 0 n = 0 + if sp[:legendtitle] != nothing + tbx, tby = gr_inqtext(0, 0, string(sp[:legendtitle])) + w = tbx[3] - tbx[1] + n += 1 + end for series in series_list(sp) should_add_to_legend(series) || continue n += 1 @@ -984,38 +1257,55 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) else lab = series[:label] end - tbx, tby = gr_inqtext(0, 0, lab) + tbx, tby = gr_inqtext(0, 0, string(lab)) w = max(w, tbx[3] - tbx[1]) end if w > 0 - xpos = viewport_plotarea[2] - 0.05 - w - ypos = viewport_plotarea[4] - 0.06 - dy = _gr_point_mult[1] * sp[:legendfont].pointsize * 1.75 + dy = _gr_point_mult[1] * sp[:legendfontsize] * 1.75 + h = dy*n + (xpos,ypos) = gr_legend_pos(sp[:legend],w,h) GR.setfillintstyle(GR.INTSTYLE_SOLID) gr_set_fillcolor(sp[:background_color_legend]) GR.fillrect(xpos - 0.08, xpos + w + 0.02, ypos + dy, ypos - dy * n) - GR.setlinetype(1) - GR.setlinewidth(1) + gr_set_line(1, :solid, sp[:foreground_color_legend]) GR.drawrect(xpos - 0.08, xpos + w + 0.02, ypos + dy, ypos - dy * n) i = 0 + if sp[:legendtitle] != nothing + GR.settextalign(GR.TEXT_HALIGN_CENTER, GR.TEXT_VALIGN_HALF) + gr_set_textcolor(sp[:legendfontcolor]) + gr_set_transparency(1) + gr_text(xpos - 0.03 + 0.5*w, ypos, string(sp[:legendtitle])) + ypos -= dy + end for series in series_list(sp) should_add_to_legend(series) || continue st = series[:seriestype] - gr_set_line(series[:linewidth], series[:linestyle], series[:linecolor]) #, series[:linealpha]) - if st == :path - GR.polyline([xpos - 0.07, xpos - 0.01], [ypos, ypos]) - elseif st == :shape - gr_set_fill(series[:fillcolor]) #, series[:fillalpha]) + gr_set_line(get_linewidth(series), get_linestyle(series), get_linecolor(series)) #, series[:linealpha]) + + if (st == :shape || series[:fillrange] != nothing) && series[:ribbon] == nothing + gr_set_fill(get_fillcolor(series)) #, series[:fillalpha]) l, r = xpos-0.07, xpos-0.01 b, t = ypos-0.4dy, ypos+0.4dy x = [l, r, r, l, l] y = [b, b, t, t, b] + gr_set_transparency(get_fillalpha(series)) gr_polyline(x, y, GR.fillarea) - gr_polyline(x, y) + gr_set_transparency(get_linealpha(series)) + gr_set_line(get_linewidth(series), get_linestyle(series), get_linecolor(series)) + st == :shape && gr_polyline(x, y) + end + + if st in (:path, :straightline) + gr_set_transparency(get_linealpha(series)) + if series[:fillrange] == nothing || series[:ribbon] != nothing + GR.polyline([xpos - 0.07, xpos - 0.01], [ypos, ypos]) + else + GR.polyline([xpos - 0.07, xpos - 0.01], [ypos+0.4dy, ypos+0.4dy]) + end end if series[:markershape] != :none - gr_draw_markers(series, xpos-[0.06,0.02], [ypos,ypos], 10, nothing) + gr_draw_markers(series, xpos - .035, ypos, 6, nothing) end if typeof(series[:label]) <: Array @@ -1025,8 +1315,8 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) lab = series[:label] end GR.settextalign(GR.TEXT_HALIGN_LEFT, GR.TEXT_VALIGN_HALF) - gr_set_textcolor(sp[:foreground_color_legend]) - gr_text(xpos, ypos, lab) + gr_set_textcolor(sp[:legendfontcolor]) + gr_text(xpos, ypos, string(lab)) ypos -= dy end end @@ -1046,7 +1336,7 @@ function gr_display(sp::Subplot{GRBackend}, w, h, viewport_canvas) end end for ann in sp[:annotations] - x, y, val = ann + x, y, val = locate_annotation(sp, ann...) x, y = if is3d(sp) # GR.wc3towc(x, y, z) else @@ -1084,12 +1374,18 @@ for (mime, fmt) in _gr_mimeformats @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GRBackend}) GR.emergencyclosegks() filepath = tempname() * "." * $fmt + env = get(ENV, "GKSwstype", "0") ENV["GKSwstype"] = $fmt ENV["GKS_FILEPATH"] = filepath - gr_display(plt) + gr_display(plt, $fmt) GR.emergencyclosegks() write(io, readstring(filepath)) rm(filepath) + if env != "0" + ENV["GKSwstype"] = env + else + pop!(ENV,"GKSwstype") + end end end diff --git a/src/backends/hdf5.jl b/src/backends/hdf5.jl new file mode 100644 index 00000000..a82a277f --- /dev/null +++ b/src/backends/hdf5.jl @@ -0,0 +1,665 @@ +#HDF5 Plots: Save/replay plots to/from HDF5 +#------------------------------------------------------------------------------- + +#==Usage +=============================================================================== +Write to .hdf5 file using: + p = plot(...) + Plots.hdf5plot_write(p, "plotsave.hdf5") + +Read from .hdf5 file using: + pyplot() #Must first select backend + pread = Plots.hdf5plot_read("plotsave.hdf5") + display(pread) +==# + + +#==TODO +=============================================================================== + 1. Support more features + - SeriesAnnotations & GridLayout known to be missing. + 3. Improve error handling. + - Will likely crash if file format is off. + 2. Save data in a folder parallel to "plot". + - Will make it easier for users to locate data. + - Use HDF5 reference to link data? + 3. Develop an actual versioned file format. + - Should have some form of backward compatibility. + - Should be reliable for archival purposes. +==# + +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "hdf5.jl")) +end + +import FixedPointNumbers: N0f8 #In core Julia + +#Dispatch types: +struct HDF5PlotNative; end #Indentifies a data element that can natively be handled by HDF5 +struct HDF5CTuple; end #Identifies a "complex" tuple structure + +mutable struct HDF5Plot_PlotRef + ref::Union{Plot, Void} +end + + +#==Useful constants +===============================================================================# +const _hdf5_plotroot = "plot" +const _hdf5_dataroot = "data" #TODO: Eventually move data to different root (easier to locate)? +const _hdf5plot_datatypeid = "TYPE" #Attribute identifying type +const _hdf5plot_countid = "COUNT" #Attribute for storing count + +#Dict has problems using "Types" as keys. Initialize in "_initialize_backend": +const HDF5PLOT_MAP_STR2TELEM = Dict{String, Type}() +const HDF5PLOT_MAP_TELEM2STR = Dict{Type, String}() + +#Don't really like this global variable... Very hacky +const HDF5PLOT_PLOTREF = HDF5Plot_PlotRef(nothing) + +#Simple sub-structures that can just be written out using _hdf5plot_gwritefields: +const HDF5PLOT_SIMPLESUBSTRUCT = Union{Font, BoundingBox, + GridLayout, RootLayout, ColorGradient, SeriesAnnotations, PlotText +} + + +#== +===============================================================================# + +const _hdf5_attr = merge_with_base_supported([ + :annotations, + :background_color_legend, :background_color_inside, :background_color_outside, + :foreground_color_grid, :foreground_color_legend, :foreground_color_title, + :foreground_color_axis, :foreground_color_border, :foreground_color_guide, :foreground_color_text, + :label, + :linecolor, :linestyle, :linewidth, :linealpha, + :markershape, :markercolor, :markersize, :markeralpha, + :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, + :fillrange, :fillcolor, :fillalpha, + :bins, :bar_width, :bar_edges, :bar_position, + :title, :title_location, :titlefont, + :window_title, + :guide, :lims, :ticks, :scale, :flip, :rotation, + :tickfont, :guidefont, :legendfont, + :grid, :legend, :colorbar, + :marker_z, :line_z, :fill_z, + :levels, + :ribbon, :quiver, :arrow, + :orientation, + :overwrite_figure, + :polar, + :normalize, :weights, + :contours, :aspect_ratio, + :match_dimensions, + :clims, + :inset_subplots, + :dpi, + :colorbar_title, + ]) +const _hdf5_seriestype = [ + :path, :steppre, :steppost, :shape, :straightline, + :scatter, :hexbin, #:histogram2d, :histogram, + # :bar, + :heatmap, :pie, :image, + :contour, :contour3d, :path3d, :scatter3d, :surface, :wireframe + ] +const _hdf5_style = [:auto, :solid, :dash, :dot, :dashdot] +const _hdf5_marker = vcat(_allMarkers, :pixel) +const _hdf5_scale = [:identity, :ln, :log2, :log10] +is_marker_supported(::HDF5Backend, shape::Shape) = true + +function add_backend_string(::HDF5Backend) + """ + if !Plots.is_installed("HDF5") + Pkg.add("HDF5") + end + """ +end + + +#==Helper functions +===============================================================================# + +_hdf5_plotelempath(subpath::String) = "$_hdf5_plotroot/$subpath" +_hdf5_datapath(subpath::String) = "$_hdf5_dataroot/$subpath" +_hdf5_map_str2telem(k::String) = HDF5PLOT_MAP_STR2TELEM[k] +_hdf5_map_str2telem(v::Vector) = HDF5PLOT_MAP_STR2TELEM[v[1]] + +function _hdf5_merge!(dest::Dict, src::Dict) + for (k, v) in src + if isa(v, Axis) + _hdf5_merge!(dest[k].d, v.d) + else + dest[k] = v + end + end + return +end + + +#== +===============================================================================# + +function _initialize_backend(::HDF5Backend) + @eval begin + import HDF5 + export HDF5 + if length(HDF5PLOT_MAP_TELEM2STR) < 1 + #Possible element types of high-level data types: + const telem2str = Dict{String, Type}( + "NATIVE" => HDF5PlotNative, + "VOID" => Void, + "BOOL" => Bool, + "SYMBOL" => Symbol, + "TUPLE" => Tuple, + "CTUPLE" => HDF5CTuple, #Tuple of complex structures + "RGBA" => ARGB{N0f8}, + "EXTREMA" => Extrema, + "LENGTH" => Length, + "ARRAY" => Array, #Dict won't allow Array to be key in HDF5PLOT_MAP_TELEM2STR + + #Sub-structure types: + "FONT" => Font, + "BOUNDINGBOX" => BoundingBox, + "GRIDLAYOUT" => GridLayout, + "ROOTLAYOUT" => RootLayout, + "SERIESANNOTATIONS" => SeriesAnnotations, +# "PLOTTEXT" => PlotText, + "COLORGRADIENT" => ColorGradient, + "AXIS" => Axis, + "SURFACE" => Surface, + "SUBPLOT" => Subplot, + "NULLABLE" => Nullable, + ) + merge!(HDF5PLOT_MAP_STR2TELEM, telem2str) + merge!(HDF5PLOT_MAP_TELEM2STR, Dict{Type, String}(v=>k for (k,v) in HDF5PLOT_MAP_STR2TELEM)) + end + end +end + +# --------------------------------------------------------------------------- + +# Create the window/figure for this backend. +function _create_backend_figure(plt::Plot{HDF5Backend}) + #Do nothing +end + +# --------------------------------------------------------------------------- + +# # this is called early in the pipeline, use it to make the plot current or something +# function _prepare_plot_object(plt::Plot{HDF5Backend}) +# end + +# --------------------------------------------------------------------------- + +# Set up the subplot within the backend object. +function _initialize_subplot(plt::Plot{HDF5Backend}, sp::Subplot{HDF5Backend}) + #Do nothing +end + +# --------------------------------------------------------------------------- + +# Add one series to the underlying backend object. +# Called once per series +# NOTE: Seems to be called when user calls plot()... even if backend +# plot, sp.o has not yet been constructed... +function _series_added(plt::Plot{HDF5Backend}, series::Series) + #Do nothing +end + +# --------------------------------------------------------------------------- + +# When series data is added/changed, this callback can do dynamic updates to the backend object. +# note: if the backend rebuilds the plot from scratch on display, then you might not do anything here. +function _series_updated(plt::Plot{HDF5Backend}, series::Series) + #Do nothing +end + +# --------------------------------------------------------------------------- + +# called just before updating layout bounding boxes... in case you need to prep +# for the calcs +function _before_layout_calcs(plt::Plot{HDF5Backend}) + #Do nothing +end + +# ---------------------------------------------------------------- + +# Set the (left, top, right, bottom) minimum padding around the plot area +# to fit ticks, tick labels, guides, colorbars, etc. +function _update_min_padding!(sp::Subplot{HDF5Backend}) + #Do nothing +end + +# ---------------------------------------------------------------- + +# Override this to update plot items (title, xlabel, etc), and add annotations (d[:annotations]) +function _update_plot_object(plt::Plot{HDF5Backend}) + #Do nothing +end + +# ---------------------------------------------------------------- + +# Display/show the plot (open a GUI window, or browser page, for example). +function _display(plt::Plot{HDF5Backend}) + msg = "HDF5 interface does not support `display()` function." + msg *= "\nUse `Plots.hdf5plot_write(::String)` method to write to .HDF5 \"plot\" file instead." + warn(msg) + return +end + + +#==HDF5 write functions +===============================================================================# + +function _hdf5plot_writetype(grp, k::String, tstr::Array{String}) + d = HDF5.d_open(grp, k) + HDF5.a_write(d, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_writetype(grp, k::String, T::Type) + tstr = HDF5PLOT_MAP_TELEM2STR[T] + d = HDF5.d_open(grp, k) + HDF5.a_write(d, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_overwritetype(grp, k::String, T::Type) + tstr = HDF5PLOT_MAP_TELEM2STR[T] + d = HDF5.d_open(grp, k) + HDF5.a_delete(d, _hdf5plot_datatypeid) + HDF5.a_write(d, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_writetype(grp, T::Type) #Write directly to group + tstr = HDF5PLOT_MAP_TELEM2STR[T] + HDF5.a_write(grp, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_overwritetype(grp, T::Type) #Write directly to group + tstr = HDF5PLOT_MAP_TELEM2STR[T] + HDF5.a_delete(grp, _hdf5plot_datatypeid) + HDF5.a_write(grp, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_writetype(grp, ::Type{Array{T}}) where T<:Any + tstr = HDF5PLOT_MAP_TELEM2STR[Array] #ANY + HDF5.a_write(grp, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_writetype(grp, ::Type{T}) where T<:BoundingBox + tstr = HDF5PLOT_MAP_TELEM2STR[BoundingBox] + HDF5.a_write(grp, _hdf5plot_datatypeid, tstr) +end +function _hdf5plot_writecount(grp, n::Int) #Write directly to group + HDF5.a_write(grp, _hdf5plot_countid, n) +end +function _hdf5plot_gwritefields(grp, k::String, v) + grp = HDF5.g_create(grp, k) + for _k in fieldnames(v) + _v = getfield(v, _k) + kstr = string(_k) + _hdf5plot_gwrite(grp, kstr, _v) + end + _hdf5plot_writetype(grp, typeof(v)) + return +end + +# Write data +# ---------------------------------------------------------------- + +function _hdf5plot_gwrite(grp, k::String, v) #Default + grp[k] = v + _hdf5plot_writetype(grp, k, HDF5PlotNative) +end +function _hdf5plot_gwrite(grp, k::String, v::Array{T}) where T<:Number #Default for arrays + grp[k] = v + _hdf5plot_writetype(grp, k, HDF5PlotNative) +end +#= +function _hdf5plot_gwrite(grp, k::String, v::Array{Any}) +# @show grp, k + warn("Cannot write Array: $k=$v") +end +=# +function _hdf5plot_gwrite(grp, k::String, v::Void) + grp[k] = 0 + _hdf5plot_writetype(grp, k, Void) +end +function _hdf5plot_gwrite(grp, k::String, v::Bool) + grp[k] = Int(v) + _hdf5plot_writetype(grp, k, Bool) +end +function _hdf5plot_gwrite(grp, k::String, v::Symbol) + grp[k] = string(v) + _hdf5plot_writetype(grp, k, Symbol) +end +function _hdf5plot_gwrite(grp, k::String, v::Tuple) + varr = [v...] + elt = eltype(varr) +# if isleaftype(elt) + + _hdf5plot_gwrite(grp, k, varr) + if elt <: Number + #We just wrote a simple dataset + _hdf5plot_overwritetype(grp, k, Tuple) + else #Used a more complex scheme (using subgroups): + _hdf5plot_overwritetype(grp[k], HDF5CTuple) + end + #NOTE: _hdf5plot_overwritetype overwrites "Array" type with "Tuple". +end +function _hdf5plot_gwrite(grp, k::String, d::Dict) +# warn("Cannot write dict: $k=$d") +end +function _hdf5plot_gwrite(grp, k::String, v::Range) + _hdf5plot_gwrite(grp, k, collect(v)) #For now +end +function _hdf5plot_gwrite(grp, k::String, v::ARGB{N0f8}) + grp[k] = [v.r.i, v.g.i, v.b.i, v.alpha.i] + _hdf5plot_writetype(grp, k, ARGB{N0f8}) +end +function _hdf5plot_gwrite(grp, k::String, v::Colorant) + _hdf5plot_gwrite(grp, k, ARGB{N0f8}(v)) +end +#Custom vector (when not using simple numeric type): +function _hdf5plot_gwritearray(grp, k::String, v::Array{T}) where T + if "annotations" == k; + return #Hack. Does not yet support annotations. + end + + vgrp = HDF5.g_create(grp, k) + _hdf5plot_writetype(vgrp, Array) #ANY + sz = size(v) + + for iter in eachindex(v) + coord = ind2sub(sz, iter) + elem = v[iter] + idxstr = join(coord, "_") + _hdf5plot_gwrite(vgrp, "v$idxstr", v[iter]) + end + + _hdf5plot_gwrite(vgrp, "dim", [sz...]) + return +end +_hdf5plot_gwrite(grp, k::String, v::Array) = + _hdf5plot_gwritearray(grp, k, v) +function _hdf5plot_gwrite(grp, k::String, v::Extrema) + grp[k] = [v.emin, v.emax] + _hdf5plot_writetype(grp, k, Extrema) +end +function _hdf5plot_gwrite(grp, k::String, v::Length{T}) where T + grp[k] = v.value + _hdf5plot_writetype(grp, k, [HDF5PLOT_MAP_TELEM2STR[Length], string(T)]) +end + +# Write more complex structures: +# ---------------------------------------------------------------- + +function _hdf5plot_gwrite(grp, k::String, v::Plot) + #Don't write plot references +end +function _hdf5plot_gwrite(grp, k::String, v::HDF5PLOT_SIMPLESUBSTRUCT) + _hdf5plot_gwritefields(grp, k, v) + return +end +function _hdf5plot_gwrite(grp, k::String, v::Axis) + grp = HDF5.g_create(grp, k) + for (_k, _v) in v.d + kstr = string(_k) + _hdf5plot_gwrite(grp, kstr, _v) + end + _hdf5plot_writetype(grp, Axis) + return +end +function _hdf5plot_gwrite(grp, k::String, v::Surface) + grp = HDF5.g_create(grp, k) + _hdf5plot_gwrite(grp, "data2d", v.surf) + _hdf5plot_writetype(grp, Surface) +end +#TODO: "Properly" support Nullable using _hdf5plot_writetype? +function _hdf5plot_gwrite(grp, k::String, v::Nullable) + if isnull(v) + _hdf5plot_gwrite(grp, k, nothing) + else + _hdf5plot_gwrite(grp, k, v.value) + end + return +end + +function _hdf5plot_gwrite(grp, k::String, v::SeriesAnnotations) + #Currently no support for SeriesAnnotations + return +end +function _hdf5plot_gwrite(grp, k::String, v::Subplot) + grp = HDF5.g_create(grp, k) + _hdf5plot_gwrite(grp, "index", v[:subplot_index]) + _hdf5plot_writetype(grp, Subplot) + return +end +function _hdf5plot_write(grp, d::Dict) + for (k, v) in d + kstr = string(k) + _hdf5plot_gwrite(grp, kstr, v) + end + return +end + +# Write main plot structures: +# ---------------------------------------------------------------- + +function _hdf5plot_write(sp::Subplot{HDF5Backend}, subpath::String, f) + f = f::HDF5.HDF5File #Assert + grp = HDF5.g_create(f, _hdf5_plotelempath("$subpath/attr")) + _hdf5plot_write(grp, sp.attr) + grp = HDF5.g_create(f, _hdf5_plotelempath("$subpath/series_list")) + _hdf5plot_writecount(grp, length(sp.series_list)) + for (i, series) in enumerate(sp.series_list) + grp = HDF5.g_create(f, _hdf5_plotelempath("$subpath/series_list/series$i")) + _hdf5plot_write(grp, series.d) + end + + return +end + +function _hdf5plot_write(plt::Plot{HDF5Backend}, f) + f = f::HDF5.HDF5File #Assert + + grp = HDF5.g_create(f, _hdf5_plotelempath("attr")) + _hdf5plot_write(grp, plt.attr) + + grp = HDF5.g_create(f, _hdf5_plotelempath("subplots")) + _hdf5plot_writecount(grp, length(plt.subplots)) + + for (i, sp) in enumerate(plt.subplots) + _hdf5plot_write(sp, "subplots/subplot$i", f) + end + + return +end +function hdf5plot_write(plt::Plot{HDF5Backend}, path::AbstractString) + HDF5.h5open(path, "w") do file + _hdf5plot_write(plt, file) + end +end +hdf5plot_write(path::AbstractString) = hdf5plot_write(current(), path) + + +#==HDF5 playback (read) functions +===============================================================================# + +function _hdf5plot_readcount(grp) #Read directly from group + return HDF5.a_read(grp, _hdf5plot_countid) +end + +_hdf5plot_convert(T::Type{HDF5PlotNative}, v) = v +_hdf5plot_convert(T::Type{Void}, v) = nothing +_hdf5plot_convert(T::Type{Bool}, v) = (v!=0) +_hdf5plot_convert(T::Type{Symbol}, v) = Symbol(v) +_hdf5plot_convert(T::Type{Tuple}, v) = tuple(v...) #With Vector{T<:Number} +function _hdf5plot_convert(T::Type{ARGB{N0f8}}, v) + r, g, b, a = reinterpret(N0f8, v) + return Colors.ARGB{N0f8}(r, g, b, a) +end +_hdf5plot_convert(T::Type{Extrema}, v) = Extrema(v[1], v[2]) + +# Read data structures: +# ---------------------------------------------------------------- + +function _hdf5plot_read(grp, k::String, T::Type, dtid) + v = HDF5.d_read(grp, k) + return _hdf5plot_convert(T, v) +end +function _hdf5plot_read(grp, k::String, T::Type{Length}, dtid::Vector) + v = HDF5.d_read(grp, k) + TU = Symbol(dtid[2]) + T = typeof(v) + return Length{TU,T}(v) +end + +# Read more complex data structures: +# ---------------------------------------------------------------- +function _hdf5plot_read(grp, k::String, T::Type{Font}, dtid) + grp = HDF5.g_open(grp, k) + + family = _hdf5plot_read(grp, "family") + pointsize = _hdf5plot_read(grp, "pointsize") + halign = _hdf5plot_read(grp, "halign") + valign = _hdf5plot_read(grp, "valign") + rotation = _hdf5plot_read(grp, "rotation") + color = _hdf5plot_read(grp, "color") + return Font(family, pointsize, halign, valign, rotation, color) +end +function _hdf5plot_read(grp, k::String, T::Type{Array}, dtid) #ANY + grp = HDF5.g_open(grp, k) + sz = _hdf5plot_read(grp, "dim") + if [0] == sz; return []; end + sz = tuple(sz...) + result = Array{Any}(sz) + + for iter in eachindex(result) + coord = ind2sub(sz, iter) + idxstr = join(coord, "_") + result[iter] = _hdf5plot_read(grp, "v$idxstr") + end + + #Hack: Implicitly make Julia detect element type. + # (Should probably write it explicitly to file) + result = [result[iter] for iter in eachindex(result)] #Potentially make more specific + return reshape(result, sz) +end +function _hdf5plot_read(grp, k::String, T::Type{HDF5CTuple}, dtid) + v = _hdf5plot_read(grp, k, Array, dtid) + return tuple(v...) +end +function _hdf5plot_read(grp, k::String, T::Type{ColorGradient}, dtid) + grp = HDF5.g_open(grp, k) + + colors = _hdf5plot_read(grp, "colors") + values = _hdf5plot_read(grp, "values") + return ColorGradient(colors, values) +end +function _hdf5plot_read(grp, k::String, T::Type{BoundingBox}, dtid) + grp = HDF5.g_open(grp, k) + + x0 = _hdf5plot_read(grp, "x0") + a = _hdf5plot_read(grp, "a") + return BoundingBox(x0, a) +end +_hdf5plot_read(grp, k::String, T::Type{RootLayout}, dtid) = RootLayout() +function _hdf5plot_read(grp, k::String, T::Type{GridLayout}, dtid) + grp = HDF5.g_open(grp, k) + +# parent = _hdf5plot_read(grp, "parent") +parent = RootLayout() + minpad = _hdf5plot_read(grp, "minpad") + bbox = _hdf5plot_read(grp, "bbox") + grid = _hdf5plot_read(grp, "grid") + widths = _hdf5plot_read(grp, "widths") + heights = _hdf5plot_read(grp, "heights") + attr = KW() #TODO support attr: _hdf5plot_read(grp, "attr") + + return GridLayout(parent, minpad, bbox, grid, widths, heights, attr) +end +function _hdf5plot_read(grp, k::String, T::Type{Axis}, dtid) + grp = HDF5.g_open(grp, k) + kwlist = KW() + _hdf5plot_read(grp, kwlist) + return Axis([], kwlist) +end +function _hdf5plot_read(grp, k::String, T::Type{Surface}, dtid) + grp = HDF5.g_open(grp, k) + data2d = _hdf5plot_read(grp, "data2d") + return Surface(data2d) +end +function _hdf5plot_read(grp, k::String, T::Type{Subplot}, dtid) + grp = HDF5.g_open(grp, k) + idx = _hdf5plot_read(grp, "index") + return HDF5PLOT_PLOTREF.ref.subplots[idx] +end +function _hdf5plot_read(grp, k::String) + dtid = HDF5.a_read(grp[k], _hdf5plot_datatypeid) + T = _hdf5_map_str2telem(dtid) #expect exception + return _hdf5plot_read(grp, k, T, dtid) +end + +#Read in values in group to populate d: +function _hdf5plot_read(grp, d::Dict) + gnames = names(grp) + for k in gnames + try + v = _hdf5plot_read(grp, k) + d[Symbol(k)] = v + catch e + @show e + @show grp + warn("Could not read field $k") + end + end + return +end + +# Read main plot structures: +# ---------------------------------------------------------------- + +function _hdf5plot_read(sp::Subplot, subpath::String, f) + f = f::HDF5.HDF5File #Assert + + grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/attr")) + kwlist = KW() + _hdf5plot_read(grp, kwlist) + _hdf5_merge!(sp.attr, kwlist) + + grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/series_list")) + nseries = _hdf5plot_readcount(grp) + + for i in 1:nseries + grp = HDF5.g_open(f, _hdf5_plotelempath("$subpath/series_list/series$i")) + kwlist = KW() + _hdf5plot_read(grp, kwlist) + plot!(sp, kwlist[:x], kwlist[:y]) #Add data & create data structures + _hdf5_merge!(sp.series_list[end].d, kwlist) + end + + return +end + +function _hdf5plot_read(plt::Plot, f) + f = f::HDF5.HDF5File #Assert + #Assumpltion: subplots are already allocated (plt.subplots) + + HDF5PLOT_PLOTREF.ref = plt #Used when reading "layout" + grp = HDF5.g_open(f, _hdf5_plotelempath("attr")) + _hdf5plot_read(grp, plt.attr) + + for (i, sp) in enumerate(plt.subplots) + _hdf5plot_read(sp, "subplots/subplot$i", f) + end + + return +end + +function hdf5plot_read(path::AbstractString) + plt = nothing + HDF5.h5open(path, "r") do file + grp = HDF5.g_open(file, _hdf5_plotelempath("subplots")) + n = _hdf5plot_readcount(grp) + plt = plot(layout=n) #Get reference to a new plot + _hdf5plot_read(plt, file) + end + return plt +end + +#Last line diff --git a/src/backends/inspectdr.jl b/src/backends/inspectdr.jl index fea44064..8cc2a22c 100644 --- a/src/backends/inspectdr.jl +++ b/src/backends/inspectdr.jl @@ -13,12 +13,17 @@ Add in functionality to Plots.jl: :aspect_ratio, =# +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "inspectdr.jl")) +end + # --------------------------------------------------------------------------- #TODO: remove features const _inspectdr_attr = merge_with_base_supported([ :annotations, :background_color_legend, :background_color_inside, :background_color_outside, - :foreground_color_grid, :foreground_color_legend, :foreground_color_title, + # :foreground_color_grid, + :foreground_color_legend, :foreground_color_title, :foreground_color_axis, :foreground_color_border, :foreground_color_guide, :foreground_color_text, :label, :linecolor, :linestyle, :linewidth, :linealpha, @@ -27,10 +32,13 @@ const _inspectdr_attr = merge_with_base_supported([ :markerstrokestyle, #Causes warning not to have it... what is this? :fillcolor, :fillalpha, #:fillrange, # :bins, :bar_width, :bar_edges, :bar_position, - :title, :title_location, :titlefont, + :title, :title_location, :window_title, :guide, :lims, :scale, #:ticks, :flip, :rotation, - :tickfont, :guidefont, :legendfont, + :titlefontfamily, :titlefontsize, :titlefontcolor, + :legendfontfamily, :legendfontsize, :legendfontcolor, + :tickfontfamily, :tickfontsize, :tickfontcolor, + :guidefontfamily, :guidefontsize, :guidefontcolor, :grid, :legend, #:colorbar, # :marker_z, # :line_z, @@ -38,7 +46,7 @@ const _inspectdr_attr = merge_with_base_supported([ # :ribbon, :quiver, :arrow, # :orientation, :overwrite_figure, -# :polar, + :polar, # :normalize, :weights, # :contours, :aspect_ratio, :match_dimensions, @@ -49,7 +57,7 @@ const _inspectdr_attr = merge_with_base_supported([ ]) const _inspectdr_style = [:auto, :solid, :dash, :dot, :dashdot] const _inspectdr_seriestype = [ - :path, :scatter, :shape #, :steppre, :steppost + :path, :scatter, :shape, :straightline, #, :steppre, :steppost ] #see: _allMarkers, _shape_keys const _inspectdr_marker = Symbol[ @@ -66,6 +74,9 @@ const _inspectdr_scale = [:identity, :ln, :log2, :log10] is_marker_supported(::InspectDRBackend, shape::Shape) = true +_inspectdr_to_pixels(bb::BoundingBox) = + InspectDR.BoundingBox(to_pixels(left(bb)), to_pixels(right(bb)), to_pixels(top(bb)), to_pixels(bottom(bb))) + #Do we avoid Map to avoid possible pre-comile issues? function _inspectdr_mapglyph(s::Symbol) s == :rect && return :square @@ -125,16 +136,17 @@ end # --------------------------------------------------------------------------- -function _inspectdr_getscale(s::Symbol) +function _inspectdr_getscale(s::Symbol, yaxis::Bool) #TODO: Support :asinh, :sqrt + kwargs = yaxis ? (:tgtmajor=>8, :tgtminor=>2) : () #More grid lines on y-axis if :log2 == s - return InspectDR.AxisScale(:log2) + return InspectDR.AxisScale(:log2; kwargs...) elseif :log10 == s - return InspectDR.AxisScale(:log10) + return InspectDR.AxisScale(:log10; kwargs...) elseif :ln == s - return InspectDR.AxisScale(:ln) + return InspectDR.AxisScale(:ln; kwargs...) else #identity - return InspectDR.AxisScale(:lin) + return InspectDR.AxisScale(:lin; kwargs...) end end @@ -158,7 +170,7 @@ function _initialize_backend(::InspectDRBackend; kw...) 2*InspectDR.GLYPH_SQUARE.x, InspectDR.GLYPH_SQUARE.y ) - type InspecDRPlotRef + mutable struct InspecDRPlotRef mplot::Union{Void, InspectDR.Multiplot} gui::Union{Void, InspectDR.GtkPlot} end @@ -167,7 +179,7 @@ function _initialize_backend(::InspectDRBackend; kw...) _inspectdr_getmplot(r::InspecDRPlotRef) = r.mplot _inspectdr_getgui(::Any) = nothing - _inspectdr_getgui(gplot::InspectDR.GtkPlot) = (gplot.destroyed? nothing: gplot) + _inspectdr_getgui(gplot::InspectDR.GtkPlot) = (gplot.destroyed ? nothing : gplot) _inspectdr_getgui(r::InspecDRPlotRef) = _inspectdr_getgui(r.gui) end end @@ -209,14 +221,10 @@ end # Set up the subplot within the backend object. function _initialize_subplot(plt::Plot{InspectDRBackend}, sp::Subplot{InspectDRBackend}) plot = sp.o - #Don't do anything without a "subplot" object: Will process later. if nothing == plot; return; end plot.data = [] - plot.markers = [] #Clear old markers - plot.atext = [] #Clear old annotation - plot.apline = [] #Clear old poly lines - + plot.userannot = [] #Clear old markers/text annotation/polyline "annotation" return plot end @@ -234,8 +242,18 @@ function _series_added(plt::Plot{InspectDRBackend}, series::Series) #Don't do anything without a "subplot" object: Will process later. if nothing == plot; return; end - _vectorize(v) = isa(v, Vector)? v: collect(v) #InspectDR only supports vectors - x = _vectorize(series[:x]); y = _vectorize(series[:y]) + _vectorize(v) = isa(v, Vector) ? v : collect(v) #InspectDR only supports vectors + x, y = if st == :straightline + straightline_data(series) + else + _vectorize(series[:x]), _vectorize(series[:y]) + end + + #No support for polar grid... but can still perform polar transformation: + if ispolar(sp) + Θ = x; r = y + x = r.*cos.(Θ); y = r.*sin.(Θ) + end # doesn't handle mismatched x/y - wrap data (pyplot behaviour): nx = length(x); ny = length(y) @@ -254,28 +272,29 @@ For st in :shape: =# if st in (:shape,) + x, y = shape_data(series) nmax = 0 for (i,rng) in enumerate(iter_segments(x, y)) nmax = i if length(rng) > 1 linewidth = series[:linewidth] - linecolor = _inspectdr_mapcolor(cycle(series[:linecolor], i)) - fillcolor = _inspectdr_mapcolor(cycle(series[:fillcolor], i)) + linecolor = _inspectdr_mapcolor(_cycle(series[:linecolor], i)) + fillcolor = _inspectdr_mapcolor(_cycle(series[:fillcolor], i)) line = InspectDR.line( style=:solid, width=linewidth, color=linecolor ) apline = InspectDR.PolylineAnnotation( x[rng], y[rng], line=line, fillcolor=fillcolor ) - push!(plot.apline, apline) + InspectDR.add(plot, apline) end end - i = (nmax >= 2? div(nmax, 2): nmax) #Must pick one set of colors for legend + i = (nmax >= 2 ? div(nmax, 2) : nmax) #Must pick one set of colors for legend if i > 1 #Add dummy waveform for legend entry: linewidth = series[:linewidth] - linecolor = _inspectdr_mapcolor(cycle(series[:linecolor], i)) - fillcolor = _inspectdr_mapcolor(cycle(series[:fillcolor], i)) + linecolor = _inspectdr_mapcolor(_cycle(series[:linecolor], i)) + fillcolor = _inspectdr_mapcolor(_cycle(series[:fillcolor], i)) wfrm = InspectDR.add(plot, Float64[], Float64[], id=series[:label]) wfrm.line = InspectDR.line( style=:none, width=linewidth, #linewidth affects glyph @@ -285,11 +304,11 @@ For st in :shape: color = linecolor, fillcolor = fillcolor ) end - elseif st in (:path, :scatter) #, :steppre, :steppost) + elseif st in (:path, :scatter, :straightline) #, :steppre, :steppost) #NOTE: In Plots.jl, :scatter plots have 0-linewidths (I think). linewidth = series[:linewidth] #More efficient & allows some support for markerstrokewidth: - _style = (0==linewidth? :none: series[:linestyle]) + _style = (0==linewidth ? :none : series[:linestyle]) wfrm = InspectDR.add(plot, x, y, id=series[:label]) wfrm.line = InspectDR.line( style = _style, @@ -328,48 +347,60 @@ end # --------------------------------------------------------------------------- function _inspectdr_setupsubplot(sp::Subplot{InspectDRBackend}) - const gridon = InspectDR.grid(vmajor=true, hmajor=true) - const gridoff = InspectDR.grid() const plot = sp.o + const strip = plot.strips[1] #Only 1 strip supported with Plots.jl xaxis = sp[:xaxis]; yaxis = sp[:yaxis] - xscale = _inspectdr_getscale(xaxis[:scale]) - yscale = _inspectdr_getscale(yaxis[:scale]) - plot.axes = InspectDR.AxesRect(xscale, yscale) + xgrid_show = xaxis[:grid] + ygrid_show = yaxis[:grid] + + strip.grid = InspectDR.GridRect( + vmajor=xgrid_show, # vminor=xgrid_show, + hmajor=ygrid_show, # hminor=ygrid_show, + ) + + plot.xscale = _inspectdr_getscale(xaxis[:scale], false) + strip.yscale = _inspectdr_getscale(yaxis[:scale], true) xmin, xmax = axis_limits(xaxis) ymin, ymax = axis_limits(yaxis) - plot.ext = InspectDR.PExtents2D() #reset - plot.ext_full = InspectDR.PExtents2D(xmin, xmax, ymin, ymax) + if ispolar(sp) + #Plots.jl appears to give (xmin,xmax) ≜ (Θmin,Θmax) & (ymin,ymax) ≜ (rmin,rmax) + rmax = NaNMath.max(abs(ymin), abs(ymax)) + xmin, xmax = -rmax, rmax + ymin, ymax = -rmax, rmax + end + plot.xext = InspectDR.PExtents1D() #reset + strip.yext = InspectDR.PExtents1D() #reset + plot.xext_full = InspectDR.PExtents1D(xmin, xmax) + strip.yext_full = InspectDR.PExtents1D(ymin, ymax) a = plot.annotation a.title = sp[:title] - a.xlabel = xaxis[:guide]; a.ylabel = yaxis[:guide] + a.xlabel = xaxis[:guide]; a.ylabels = [yaxis[:guide]] l = plot.layout - l.framedata.fillcolor = _inspectdr_mapcolor(sp[:background_color_inside]) - l.framedata.line.color = _inspectdr_mapcolor(xaxis[:foreground_color_axis]) - l.fnttitle = InspectDR.Font(sp[:titlefont].family, - _inspectdr_mapptsize(sp[:titlefont].pointsize), - color = _inspectdr_mapcolor(sp[:foreground_color_title]) + l[:frame_canvas].fillcolor = _inspectdr_mapcolor(sp[:background_color_subplot]) + l[:frame_data].fillcolor = _inspectdr_mapcolor(sp[:background_color_inside]) + l[:frame_data].line.color = _inspectdr_mapcolor(xaxis[:foreground_color_axis]) + l[:font_title] = InspectDR.Font(sp[:titlefontfamily], + _inspectdr_mapptsize(sp[:titlefontsize]), + color = _inspectdr_mapcolor(sp[:titlefontcolor]) ) #Cannot independently control fonts of axes with InspectDR: - l.fntaxlabel = InspectDR.Font(xaxis[:guidefont].family, - _inspectdr_mapptsize(xaxis[:guidefont].pointsize), - color = _inspectdr_mapcolor(xaxis[:foreground_color_guide]) + l[:font_axislabel] = InspectDR.Font(xaxis[:guidefontfamily], + _inspectdr_mapptsize(xaxis[:guidefontsize]), + color = _inspectdr_mapcolor(xaxis[:guidefontcolor]) ) - l.fntticklabel = InspectDR.Font(xaxis[:tickfont].family, - _inspectdr_mapptsize(xaxis[:tickfont].pointsize), - color = _inspectdr_mapcolor(xaxis[:foreground_color_text]) + l[:font_ticklabel] = InspectDR.Font(xaxis[:tickfontfamily], + _inspectdr_mapptsize(xaxis[:tickfontsize]), + color = _inspectdr_mapcolor(xaxis[:tickfontcolor]) ) - #No independent control of grid??? - l.grid = sp[:grid]? gridon: gridoff - leg = l.legend - leg.enabled = (sp[:legend] != :none) - #leg.width = 150 #TODO: compute??? - leg.font = InspectDR.Font(sp[:legendfont].family, - _inspectdr_mapptsize(sp[:legendfont].pointsize), - color = _inspectdr_mapcolor(sp[:foreground_color_legend]) + l[:enable_legend] = (sp[:legend] != :none) + #l[:halloc_legend] = 150 #TODO: compute??? + l[:font_legend] = InspectDR.Font(sp[:legendfontfamily], + _inspectdr_mapptsize(sp[:legendfontsize]), + color = _inspectdr_mapcolor(sp[:legendfontcolor]) ) - leg.frame.fillcolor = _inspectdr_mapcolor(sp[:background_color_legend]) + l[:frame_legend].fillcolor = _inspectdr_mapcolor(sp[:background_color_legend]) end # called just before updating layout bounding boxes... in case you need to prep @@ -378,6 +409,13 @@ function _before_layout_calcs(plt::Plot{InspectDRBackend}) const mplot = _inspectdr_getmplot(plt.o) if nothing == mplot; return; end + mplot.title = plt[:plot_title] + if "" == mplot.title + #Don't use window_title... probably not what you want. + #mplot.title = plt[:window_title] + end + mplot.layout[:frame].fillcolor = _inspectdr_mapcolor(plt[:background_color_outside]) + resize!(mplot.subplots, length(plt.subplots)) nsubplots = length(plt.subplots) for (i, sp) in enumerate(plt.subplots) @@ -385,30 +423,28 @@ function _before_layout_calcs(plt::Plot{InspectDRBackend}) mplot.subplots[i] = InspectDR.Plot2D() end sp.o = mplot.subplots[i] + plot = sp.o _initialize_subplot(plt, sp) _inspectdr_setupsubplot(sp) - sp.o.layout.frame.fillcolor = - _inspectdr_mapcolor(plt[:background_color_outside]) - # add the annotations for ann in sp[:annotations] - _inspectdr_add_annotations(mplot.subplots[i], ann...) + _inspectdr_add_annotations(plot, ann...) end end #Do not yet support absolute plot positionning. #Just try to make things look more-or less ok: if nsubplots <= 1 - mplot.ncolumns = 1 + mplot.layout[:ncolumns] = 1 elseif nsubplots <= 4 - mplot.ncolumns = 2 + mplot.layout[:ncolumns] = 2 elseif nsubplots <= 6 - mplot.ncolumns = 3 + mplot.layout[:ncolumns] = 3 elseif nsubplots <= 12 - mplot.ncolumns = 4 + mplot.layout[:ncolumns] = 4 else - mplot.ncolumns = 5 + mplot.layout[:ncolumns] = 5 end for series in plt.series_list @@ -422,8 +458,19 @@ end # Set the (left, top, right, bottom) minimum padding around the plot area # to fit ticks, tick labels, guides, colorbars, etc. function _update_min_padding!(sp::Subplot{InspectDRBackend}) - sp.minpad = (20mm, 5mm, 2mm, 10mm) - #TODO: Add support for padding. + plot = sp.o + if !isa(plot, InspectDR.Plot2D); return sp.minpad; end + #Computing plotbounds with 0-BoundingBox returns required padding: + bb = InspectDR.plotbounds(plot.layout.values, InspectDR.BoundingBox(0,0,0,0)) + #NOTE: plotbounds always pads for titles, legends, etc. even if not in use. + #TODO: possibly zero-out items not in use?? + + # add in the user-specified margin to InspectDR padding: + leftpad = abs(bb.xmin)*px + sp[:left_margin] + toppad = abs(bb.ymin)*px + sp[:top_margin] + rightpad = abs(bb.xmax)*px + sp[:right_margin] + bottompad = abs(bb.ymax)*px + sp[:bottom_margin] + sp.minpad = (leftpad, toppad, rightpad, bottompad) end # ---------------------------------------------------------------- @@ -432,6 +479,13 @@ end function _update_plot_object(plt::Plot{InspectDRBackend}) mplot = _inspectdr_getmplot(plt.o) if nothing == mplot; return; end + + for (i, sp) in enumerate(plt.subplots) + graphbb = _inspectdr_to_pixels(plotarea(sp)) + plot = mplot.subplots[i] + plot.plotbb = InspectDR.plotbounds(plot.layout.values, graphbb) + end + gplot = _inspectdr_getgui(plt.o) if nothing == gplot; return; end @@ -452,22 +506,23 @@ const _inspectdr_mimeformats_nodpi = Dict( # "application/postscript" => "ps", #TODO: support once Cairo supports PSSurface "application/pdf" => "pdf" ) -_inspectdr_show(io::IO, mime::MIME, ::Void) = +_inspectdr_show(io::IO, mime::MIME, ::Void, w, h) = throw(ErrorException("Cannot show(::IO, ...) plot - not yet generated")) -_inspectdr_show(io::IO, mime::MIME, mplot) = show(io, mime, mplot) +function _inspectdr_show(io::IO, mime::MIME, mplot, w, h) + InspectDR._show(io, mime, mplot, Float64(w), Float64(h)) +end for (mime, fmt) in _inspectdr_mimeformats_dpi @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{InspectDRBackend}) dpi = plt[:dpi]#TODO: support - _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o)) + _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o), plt[:size]...) end end for (mime, fmt) in _inspectdr_mimeformats_nodpi @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{InspectDRBackend}) - _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o)) + _inspectdr_show(io, mime, _inspectdr_getmplot(plt.o), plt[:size]...) end end -_show(io::IO, mime::MIME"text/plain", plt::Plot{InspectDRBackend}) = nothing #Don't show # ---------------------------------------------------------------- diff --git a/src/backends/pgfplots.jl b/src/backends/pgfplots.jl index b42e8629..aae1a356 100644 --- a/src/backends/pgfplots.jl +++ b/src/backends/pgfplots.jl @@ -2,13 +2,18 @@ # significant contributions by: @pkofod +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "pgfplots.jl")) +end + const _pgfplots_attr = merge_with_base_supported([ - # :annotations, - # :background_color_legend, + :annotations, + :background_color_legend, :background_color_inside, # :background_color_outside, - # :foreground_color_legend, :foreground_color_grid, :foreground_color_axis, - # :foreground_color_text, :foreground_color_border, + # :foreground_color_legend, + :foreground_color_grid, :foreground_color_axis, + :foreground_color_text, :foreground_color_border, :label, :seriescolor, :seriesalpha, :linecolor, :linestyle, :linewidth, :linealpha, @@ -22,19 +27,23 @@ const _pgfplots_attr = merge_with_base_supported([ :guide, :lims, :ticks, :scale, :flip, :rotation, :tickfont, :guidefont, :legendfont, :grid, :legend, - # :colorbar, - # :marker_z, :levels, + :colorbar, + :fill_z, :line_z, :marker_z, :levels, # :ribbon, :quiver, :arrow, # :orientation, # :overwrite_figure, - # :polar, + :polar, # :normalize, :weights, :contours, :aspect_ratio, # :match_dimensions, + :tick_direction, + :framestyle, + :camera, + :contour_labels, ]) -const _pgfplots_seriestype = [:path, :path3d, :scatter, :steppre, :stepmid, :steppost, :histogram2d, :ysticks, :xsticks, :contour] +const _pgfplots_seriestype = [:path, :path3d, :scatter, :steppre, :stepmid, :steppost, :histogram2d, :ysticks, :xsticks, :contour, :shape, :straightline,] const _pgfplots_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] -const _pgfplots_marker = [:none, :auto, :circle, :rect, :diamond, :utriangle, :dtriangle, :cross, :xcross, :star5, :pentagon] #vcat(_allMarkers, Shape) +const _pgfplots_marker = [:none, :auto, :circle, :rect, :diamond, :utriangle, :dtriangle, :cross, :xcross, :star5, :pentagon, :hline] #vcat(_allMarkers, Shape) const _pgfplots_scale = [:identity, :ln, :log2, :log10] @@ -79,6 +88,7 @@ const _pgfplots_markers = KW( :star6 => "asterisk", :diamond => "diamond*", :pentagon => "pentagon*", + :hline => "-" ) const _pgfplots_legend_pos = KW( @@ -86,6 +96,7 @@ const _pgfplots_legend_pos = KW( :bottomright => "south east", :topright => "north east", :topleft => "north west", + :outertopright => "outer north east", ) @@ -98,71 +109,131 @@ const _pgf_series_extrastyle = KW( :xsticks => "xcomb", ) +# PGFPlots uses the anchors to define orientations for example to align left +# one needs to use the right edge as anchor +const _pgf_annotation_halign = KW( + :center => "", + :left => "right", + :right => "left" +) + +const _pgf_framestyles = [:box, :axes, :origin, :zerolines, :grid, :none] +const _pgf_framestyle_defaults = Dict(:semi => :box) +function pgf_framestyle(style::Symbol) + if style in _pgf_framestyles + return style + else + default_style = get(_pgf_framestyle_defaults, style, :axes) + warn("Framestyle :$style is not (yet) supported by the PGFPlots backend. :$default_style was cosen instead.") + default_style + end +end + # -------------------------------------------------------------------------------------- # takes in color,alpha, and returns color and alpha appropriate for pgf style -function pgf_color(c) +function pgf_color(c::Colorant) cstr = @sprintf("{rgb,1:red,%.8f;green,%.8f;blue,%.8f}", red(c), green(c), blue(c)) cstr, alpha(c) end -function pgf_fillstyle(d::KW) - cstr,a = pgf_color(d[:fillcolor]) +function pgf_color(grad::ColorGradient) + # Can't handle ColorGradient here, fallback to defaults. + cstr = @sprintf("{rgb,1:red,%.8f;green,%.8f;blue,%.8f}", 0.0, 0.60560316,0.97868012) + cstr, 1 +end + +# Generates a colormap for pgfplots based on a ColorGradient +function pgf_colormap(grad::ColorGradient) + join(map(grad.colors) do c + @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c),blue(c)) + end,", ") +end + +pgf_thickness_scaling(plt::Plot) = plt[:thickness_scaling] +pgf_thickness_scaling(sp::Subplot) = pgf_thickness_scaling(sp.plt) +pgf_thickness_scaling(series) = pgf_thickness_scaling(series[:subplot]) + +function pgf_fillstyle(d, i = 1) + cstr,a = pgf_color(get_fillcolor(d, i)) + fa = get_fillalpha(d, i) + if fa != nothing + a = fa + end "fill = $cstr, fill opacity=$a" end -function pgf_linestyle(d::KW) - cstr,a = pgf_color(d[:linecolor]) +function pgf_linestyle(linewidth::Real, color, α = 1, linestyle = "solid") + cstr, a = pgf_color(plot_color(color, α)) """ color = $cstr, - draw opacity=$a, - line width=$(d[:linewidth]), - $(get(_pgfplots_linestyles, d[:linestyle], "solid"))""" + draw opacity = $a, + line width = $linewidth, + $(get(_pgfplots_linestyles, linestyle, "solid"))""" end -function pgf_marker(d::KW) - shape = d[:markershape] - cstr, a = pgf_color(d[:markercolor]) - cstr_stroke, a_stroke = pgf_color(d[:markerstrokecolor]) +function pgf_linestyle(d, i = 1) + lw = pgf_thickness_scaling(d) * get_linewidth(d, i) + lc = get_linecolor(d, i) + la = get_linealpha(d, i) + ls = get_linestyle(d, i) + return pgf_linestyle(lw, lc, la, ls) +end + +function pgf_font(fontsize, thickness_scaling = 1, font = "\\selectfont") + fs = fontsize * thickness_scaling + return string("{\\fontsize{", fs, " pt}{", 1.3fs, " pt}", font, "}") +end + +function pgf_marker(d, i = 1) + shape = _cycle(d[:markershape], i) + cstr, a = pgf_color(plot_color(get_markercolor(d, i), get_markeralpha(d, i))) + cstr_stroke, a_stroke = pgf_color(plot_color(get_markerstrokecolor(d, i), get_markerstrokealpha(d, i))) """ mark = $(get(_pgfplots_markers, shape, "*")), - mark size = $(0.5 * d[:markersize]), + mark size = $(pgf_thickness_scaling(d) * 0.5 * _cycle(d[:markersize], i)), mark options = { color = $cstr_stroke, draw opacity = $a_stroke, fill = $cstr, fill opacity = $a, - line width = $(d[:markerstrokewidth]), + line width = $(pgf_thickness_scaling(d) * _cycle(d[:markerstrokewidth], i)), rotate = $(shape == :dtriangle ? 180 : 0), - $(get(_pgfplots_linestyles, d[:markerstrokestyle], "solid")) + $(get(_pgfplots_linestyles, _cycle(d[:markerstrokestyle], i), "solid")) }""" end +function pgf_add_annotation!(o, x, y, val, thickness_scaling = 1) + # Construct the style string. + # Currently supports color and orientation + cstr,a = pgf_color(val.font.color) + push!(o, PGFPlots.Plots.Node(val.str, # Annotation Text + x, y, + style=""" + $(get(_pgf_annotation_halign,val.font.halign,"")), + color=$cstr, draw opacity=$(convert(Float16,a)), + rotate=$(val.font.rotation), + font=$(pgf_font(val.font.pointsize, thickness_scaling)) + """)) +end + # -------------------------------------------------------------------------------------- function pgf_series(sp::Subplot, series::Series) d = series.d st = d[:seriestype] - style = [] - kw = KW() - - push!(style, pgf_linestyle(d)) - push!(style, pgf_marker(d)) - - if d[:fillrange] != nothing - push!(style, pgf_fillstyle(d)) - end - - # add to legend? - if sp[:legend] != :none && should_add_to_legend(series) - kw[:legendentry] = d[:label] - else - push!(style, "forget plot") - end + series_collection = PGFPlots.Plot[] # function args - args = if st == :contour + args = if st == :contour d[:z].surf, d[:x], d[:y] elseif is3d(st) d[:x], d[:y], d[:z] + elseif st == :straightline + straightline_data(series) + elseif st == :shape + shape_data(series) + elseif ispolar(sp) + theta, r = filter_radial_data(d[:x], d[:y], axis_limits(sp[:yaxis])) + rad2deg.(theta), r else d[:x], d[:y] end @@ -173,34 +244,131 @@ function pgf_series(sp::Subplot, series::Series) else a end, args) - # for (i,a) in enumerate(args) - # if typeof(a) <: AbstractVector && typeof(a) != Vector - # args[i] = collect(a) - # end - # end - # include additional style, then add to the kw + if st in (:contour, :histogram2d) + style = [] + kw = KW() + push!(style, pgf_linestyle(d)) + push!(style, pgf_marker(d)) + push!(style, "forget plot") + + kw[:style] = join(style, ',') + func = if st == :histogram2d + PGFPlots.Histogram2 + else + kw[:labels] = series[:contour_labels] + kw[:levels] = series[:levels] + PGFPlots.Contour + end + push!(series_collection, func(args...; kw...)) + + else + # series segments + segments = iter_segments(series) + for (i, rng) in enumerate(segments) + style = [] + kw = KW() + push!(style, pgf_linestyle(d, i)) + push!(style, pgf_marker(d, i)) + + if st == :shape + push!(style, pgf_fillstyle(d, i)) + end + + # add to legend? + if i == 1 && sp[:legend] != :none && should_add_to_legend(series) + if d[:fillrange] != nothing + push!(style, "forget plot") + push!(series_collection, pgf_fill_legend_hack(d, args)) + else + kw[:legendentry] = d[:label] + if st == :shape # || d[:fillrange] != nothing + push!(style, "area legend") + end + end + else + push!(style, "forget plot") + end + + seg_args = (arg[rng] for arg in args) + + # include additional style, then add to the kw + if haskey(_pgf_series_extrastyle, st) + push!(style, _pgf_series_extrastyle[st]) + end + kw[:style] = join(style, ',') + + # add fillrange + if series[:fillrange] != nothing && st != :shape + push!(series_collection, pgf_fillrange_series(series, i, _cycle(series[:fillrange], rng), seg_args...)) + end + + # build/return the series object + func = if st == :path3d + PGFPlots.Linear3 + elseif st == :scatter + PGFPlots.Scatter + else + PGFPlots.Linear + end + push!(series_collection, func(seg_args...; kw...)) + end + end + series_collection +end + +function pgf_fillrange_series(series, i, fillrange, args...) + st = series[:seriestype] + style = [] + kw = KW() + push!(style, "line width = 0") + push!(style, "draw opacity = 0") + push!(style, pgf_fillstyle(series, i)) + push!(style, pgf_marker(series, i)) + push!(style, "forget plot") if haskey(_pgf_series_extrastyle, st) push!(style, _pgf_series_extrastyle[st]) end kw[:style] = join(style, ',') + func = is3d(series) ? PGFPlots.Linear3 : PGFPlots.Linear + return func(pgf_fillrange_args(fillrange, args...)...; kw...) +end - # build/return the series object +function pgf_fillrange_args(fillrange, x, y) + n = length(x) + x_fill = [x; x[n:-1:1]; x[1]] + y_fill = [y; _cycle(fillrange, n:-1:1); y[1]] + return x_fill, y_fill +end + +function pgf_fillrange_args(fillrange, x, y, z) + n = length(x) + x_fill = [x; x[n:-1:1]; x[1]] + y_fill = [y; y[n:-1:1]; x[1]] + z_fill = [z; _cycle(fillrange, n:-1:1); z[1]] + return x_fill, y_fill, z_fill +end + +function pgf_fill_legend_hack(d, args) + style = [] + kw = KW() + push!(style, pgf_linestyle(d, 1)) + push!(style, pgf_marker(d, 1)) + push!(style, pgf_fillstyle(d, 1)) + push!(style, "area legend") + kw[:legendentry] = d[:label] + kw[:style] = join(style, ',') + st = d[:seriestype] func = if st == :path3d PGFPlots.Linear3 elseif st == :scatter PGFPlots.Scatter - elseif st == :histogram2d - PGFPlots.Histogram2 - elseif st == :contour - PGFPlots.Contour else PGFPlots.Linear end - func(args...; kw...) + return func(([arg[1]] for arg in args)...; kw...) end - # ---------------------------------------------------------------- function pgf_axis(sp::Subplot, letter) @@ -208,9 +376,19 @@ function pgf_axis(sp::Subplot, letter) style = [] kw = KW() + # turn off scaled ticks + push!(style, "scaled $(letter) ticks = false") + + # set to supported framestyle + framestyle = pgf_framestyle(sp[:framestyle]) + # axis guide kw[Symbol(letter,:label)] = axis[:guide] + # Add label font + cstr, α = pgf_color(plot_color(axis[:guidefontcolor])) + push!(style, string(letter, "label style = {font = ", pgf_font(axis[:guidefontsize], pgf_thickness_scaling(sp)), ", color = ", cstr, ", draw opacity = ", α, ", rotate = ", axis[:guidefontrotation], "}")) + # flip/reverse? axis[:flip] && push!(style, "$letter dir=reverse") @@ -222,22 +400,74 @@ function pgf_axis(sp::Subplot, letter) end # ticks on or off - if axis[:ticks] in (nothing, false) + if axis[:ticks] in (nothing, false, :none) || framestyle == :none push!(style, "$(letter)majorticks=false") end + # grid on or off + if axis[:grid] && framestyle != :none + push!(style, "$(letter)majorgrids = true") + else + push!(style, "$(letter)majorgrids = false") + end + # limits # TODO: support zlims if letter != :z - lims = axis_limits(axis) + lims = ispolar(sp) && letter == :x ? rad2deg.(axis_limits(axis)) : axis_limits(axis) kw[Symbol(letter,:min)] = lims[1] kw[Symbol(letter,:max)] = lims[2] end - if !(axis[:ticks] in (nothing, false, :none, :auto)) + if !(axis[:ticks] in (nothing, false, :none, :native)) && framestyle != :none ticks = get_ticks(axis) - push!(style, string(letter, "tick = {", join(ticks[1],","), "}")) - push!(style, string(letter, "ticklabels = {", join(ticks[2],","), "}")) + #pgf plot ignores ticks with angle below 90 when xmin = 90 so shift values + tick_values = ispolar(sp) && letter == :x ? [rad2deg.(ticks[1])[3:end]..., 360, 405] : ticks[1] + push!(style, string(letter, "tick = {", join(tick_values,","), "}")) + if axis[:showaxis] && axis[:scale] in (:ln, :log2, :log10) && axis[:ticks] == :auto + # wrap the power part of label with } + tick_labels = String[begin + base, power = split(label, "^") + power = string("{", power, "}") + string(base, "^", power) + end for label in ticks[2]] + push!(style, string(letter, "ticklabels = {\$", join(tick_labels,"\$,\$"), "\$}")) + elseif axis[:showaxis] + tick_labels = ispolar(sp) && letter == :x ? [ticks[2][3:end]..., "0", "45"] : ticks[2] + if axis[:formatter] in (:scientific, :auto) + tick_labels = string.("\$", convert_sci_unicode.(tick_labels), "\$") + tick_labels = replace.(tick_labels, "×", "\\times") + end + push!(style, string(letter, "ticklabels = {", join(tick_labels,","), "}")) + else + push!(style, string(letter, "ticklabels = {}")) + end + push!(style, string(letter, "tick align = ", (axis[:tick_direction] == :out ? "outside" : "inside"))) + cstr, α = pgf_color(plot_color(axis[:tickfontcolor])) + push!(style, string(letter, "ticklabel style = {font = ", pgf_font(axis[:tickfontsize], pgf_thickness_scaling(sp)), ", color = ", cstr, ", draw opacity = ", α, ", rotate = ", axis[:tickfontrotation], "}")) + push!(style, string(letter, " grid style = {", pgf_linestyle(pgf_thickness_scaling(sp) * axis[:gridlinewidth], axis[:foreground_color_grid], axis[:gridalpha], axis[:gridstyle]), "}")) + end + + # framestyle + if framestyle in (:axes, :origin) + axispos = framestyle == :axes ? "left" : "middle" + # the * after lines disables the arrows at the axes + push!(style, string("axis lines* = ", axispos)) + end + + if framestyle == :zerolines + push!(style, string("extra ", letter, " ticks = 0")) + push!(style, string("extra ", letter, " tick labels = ")) + push!(style, string("extra ", letter, " tick style = {grid = major, major grid style = {", pgf_linestyle(pgf_thickness_scaling(sp), axis[:foreground_color_axis], 1.0), "}}")) + end + + if !axis[:showaxis] + push!(style, "separate axis lines") + end + if !axis[:showaxis] || framestyle in (:zerolines, :grid, :none) + push!(style, string(letter, " axis line style = {draw opacity = 0}")) + else + push!(style, string(letter, " axis line style = {", pgf_linestyle(pgf_thickness_scaling(sp), axis[:foreground_color_axis], 1.0), "}")) end # return the style list and KW args @@ -249,8 +479,12 @@ end function _update_plot_object(plt::Plot{PGFPlotsBackend}) plt.o = PGFPlots.Axis[] + # Obtain the total height of the plot by extracting the maximal bottom + # coordinate from the bounding box. + total_height = bottom(bbox(plt.layout)) + for sp in plt.subplots - # first build the PGFPlots.Axis object + # first build the PGFPlots.Axis object style = ["unbounded coords=jump"] kw = KW() @@ -265,10 +499,12 @@ function _update_plot_object(plt::Plot{PGFPlotsBackend}) # bounding box values are in mm # note: bb origin is top-left, pgf is bottom-left + # A round on 2 decimal places should be enough precision for 300 dpi + # plots. bb = bbox(sp) push!(style, """ xshift = $(left(bb).value)mm, - yshift = $((height(bb) - (bottom(bb))).value)mm, + yshift = $(round((total_height - (bottom(bb))).value,2))mm, axis background/.style={fill=$(pgf_color(sp[:background_color_inside])[1])} """) kw[:width] = "$(width(bb).value)mm" @@ -276,9 +512,10 @@ function _update_plot_object(plt::Plot{PGFPlotsBackend}) if sp[:title] != "" kw[:title] = "$(sp[:title])" + cstr, α = pgf_color(plot_color(sp[:titlefontcolor])) + push!(style, string("title style = {font = ", pgf_font(sp[:titlefontsize], pgf_thickness_scaling(sp)), ", color = ", cstr, ", draw opacity = ", α, ", rotate = ", sp[:titlefontrotation], "}")) end - sp[:grid] && push!(style, "grid = major") if sp[:aspect_ratio] in (1, :equal) kw[:axisEqual] = "true" end @@ -287,20 +524,76 @@ function _update_plot_object(plt::Plot{PGFPlotsBackend}) if haskey(_pgfplots_legend_pos, legpos) kw[:legendPos] = _pgfplots_legend_pos[legpos] end + cstr, a = pgf_color(plot_color(sp[:background_color_legend])) + push!(style, string("legend style = {", pgf_linestyle(pgf_thickness_scaling(sp), sp[:foreground_color_legend], 1.0, "solid"), ",", "fill = $cstr,", "font = ", pgf_font(sp[:legendfontsize], pgf_thickness_scaling(sp)), "}")) - o = PGFPlots.Axis(; style = style, kw...) + if any(s[:seriestype] == :contour for s in series_list(sp)) + kw[:view] = "{0}{90}" + kw[:colorbar] = !(sp[:colorbar] in (:none, :off, :hide, false)) + elseif is3d(sp) + azim, elev = sp[:camera] + kw[:view] = "{$(azim)}{$(elev)}" + end + + axisf = PGFPlots.Axis + if sp[:projection] == :polar + axisf = PGFPlots.PolarAxis + #make radial axis vertical + kw[:xmin] = 90 + kw[:xmax] = 450 + end + + # Search series for any gradient. In case one series uses a gradient set + # the colorbar and colomap. + # The reasoning behind doing this on the axis level is that pgfplots + # colorbar seems to only works on axis level and needs the proper colormap for + # correctly displaying it. + # It's also possible to assign the colormap to the series itself but + # then the colormap needs to be added twice, once for the axis and once for the + # series. + # As it is likely that all series within the same axis use the same + # colormap this should not cause any problem. + for series in series_list(sp) + for col in (:markercolor, :fillcolor, :linecolor) + if typeof(series.d[col]) == ColorGradient + push!(style,"colormap={plots}{$(pgf_colormap(series.d[col]))}") + + if sp[:colorbar] == :none + kw[:colorbar] = "false" + else + kw[:colorbar] = "true" + end + # goto is needed to break out of col and series for + @goto colorbar_end + end + end + end + @label colorbar_end + + o = axisf(; style = join(style, ","), kw...) # add the series object to the PGFPlots.Axis for series in series_list(sp) - push!(o, pgf_series(sp, series)) + push!.(o, pgf_series(sp, series)) + + # add series annotations + anns = series[:series_annotations] + for (xi,yi,str,fnt) in EachAnn(anns, series[:x], series[:y]) + pgf_add_annotation!(o, xi, yi, PlotText(str, fnt), pgf_thickness_scaling(series)) + end end + # add the annotations + for ann in sp[:annotations] + pgf_add_annotation!(o, locate_annotation(sp, ann...)..., pgf_thickness_scaling(sp)) + end + + # add the PGFPlots.Axis to the list push!(plt.o, o) end end - function _show(io::IO, mime::MIME"image/svg+xml", plt::Plot{PGFPlotsBackend}) show(io, mime, plt.o) end diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index bbdd29b9..333a4276 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -1,11 +1,15 @@ # https://plot.ly/javascript/getting-started +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "plotly.jl")) +end + const _plotly_attr = merge_with_base_supported([ :annotations, :background_color_legend, :background_color_inside, :background_color_outside, :foreground_color_legend, :foreground_color_guide, - # :foreground_color_grid, :foreground_color_axis, + :foreground_color_grid, :foreground_color_axis, :foreground_color_text, :foreground_color_border, :foreground_color_title, :label, @@ -15,12 +19,18 @@ const _plotly_attr = merge_with_base_supported([ :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, :markerstrokestyle, :fillrange, :fillcolor, :fillalpha, :bins, - :title, :title_location, :titlefont, + :title, :title_location, + :titlefontfamily, :titlefontsize, :titlefonthalign, :titlefontvalign, + :titlefontcolor, + :legendfontfamily, :legendfontsize, :legendfontcolor, + :tickfontfamily, :tickfontsize, :tickfontcolor, + :guidefontfamily, :guidefontsize, :guidefontcolor, :window_title, :guide, :lims, :ticks, :scale, :flip, :rotation, :tickfont, :guidefont, :legendfont, - :grid, :legend, :colorbar, - :marker_z, :fill_z, :levels, + :grid, :gridalpha, :gridlinewidth, + :legend, :colorbar, :colorbar_title, + :marker_z, :fill_z, :line_z, :levels, :ribbon, :quiver, :orientation, # :overwrite_figure, @@ -31,11 +41,17 @@ const _plotly_attr = merge_with_base_supported([ :hover, :inset_subplots, :bar_width, + :clims, + :framestyle, + :tick_direction, + :camera, + :contour_labels, ]) const _plotly_seriestype = [ - :path, :scatter, :bar, :pie, :heatmap, + :path, :scatter, :pie, :heatmap, :contour, :surface, :wireframe, :path3d, :scatter3d, :shape, :scattergl, + :straightline ] const _plotly_style = [:auto, :solid, :dash, :dot, :dashdot] const _plotly_marker = [ @@ -45,6 +61,17 @@ const _plotly_marker = [ const _plotly_scale = [:identity, :log10] is_subplot_supported(::PlotlyBackend) = true # is_string_supported(::PlotlyBackend) = true +const _plotly_framestyles = [:box, :axes, :zerolines, :grid, :none] +const _plotly_framestyle_defaults = Dict(:semi => :box, :origin => :zerolines) +function _plotly_framestyle(style::Symbol) + if style in _plotly_framestyles + return style + else + default_style = get(_plotly_framestyle_defaults, style, :axes) + warn("Framestyle :$style is not supported by Plotly and PlotlyJS. :$default_style was cosen instead.") + default_style + end +end # -------------------------------------------------------------------------------------- @@ -61,8 +88,6 @@ const _plotly_js_path_remote = "https://cdn.plot.ly/plotly-latest.min.js" function _initialize_backend(::PlotlyBackend; kw...) @eval begin - import JSON - _js_code = open(readstring, _plotly_js_path, "r") # borrowed from https://github.com/plotly/plotly.py/blob/2594076e29584ede2d09f2aa40a8a195b3f3fc66/plotly/offline/offline.py#L64-L71 c/o @spencerlyon2 @@ -106,7 +131,7 @@ const _plotly_legend_pos = KW( ) plotly_legend_pos(pos::Symbol) = get(_plotly_legend_pos, pos, [1.,1.]) -plotly_legend_pos{S<:Real, T<:Real}(v::Tuple{S,T}) = v +plotly_legend_pos(v::Tuple{S,T}) where {S<:Real, T<:Real} = v function plotly_font(font::Font, color = font.color) KW( @@ -122,7 +147,7 @@ function plotly_annotation_dict(x, y, val; xref="paper", yref="paper") :text => val, :xref => xref, :x => x, - :yref => xref, + :yref => yref, :y => y, :showarrow => false, ) @@ -208,43 +233,57 @@ function plotly_domain(sp::Subplot, letter) end -function plotly_axis(axis::Axis, sp::Subplot) +function plotly_axis(plt::Plot, axis::Axis, sp::Subplot) letter = axis[:letter] + framestyle = sp[:framestyle] ax = KW( + :visible => framestyle != :none, :title => axis[:guide], - :showgrid => sp[:grid], - :zeroline => false, - :ticks => "inside", + :showgrid => axis[:grid], + :gridcolor => rgba_string(plot_color(axis[:foreground_color_grid], axis[:gridalpha])), + :gridwidth => axis[:gridlinewidth], + :zeroline => framestyle == :zerolines, + :zerolinecolor => rgba_string(axis[:foreground_color_axis]), + :showline => framestyle in (:box, :axes) && axis[:showaxis], + :linecolor => rgba_string(plot_color(axis[:foreground_color_axis])), + :ticks => axis[:tick_direction] == :out ? "outside" : "inside", + :mirror => framestyle == :box, + :showticklabels => axis[:showaxis], ) if letter in (:x,:y) ax[:domain] = plotly_domain(sp, letter) - ax[:anchor] = "$(letter==:x ? :y : :x)$(plotly_subplot_index(sp))" + if is3d(sp) + # don't link 3d axes for synchronized interactivity + x_idx = y_idx = sp[:subplot_index] + else + x_idx, y_idx = plotly_link_indicies(plt, sp) + end + ax[:anchor] = "$(letter==:x ? "y$(y_idx)" : "x$(x_idx)")" end ax[:tickangle] = -axis[:rotation] + ax[:type] = plotly_scale(axis[:scale]) + lims = axis_limits(axis) - if !(axis[:ticks] in (nothing, :none)) - ax[:titlefont] = plotly_font(axis[:guidefont], axis[:foreground_color_guide]) - ax[:type] = plotly_scale(axis[:scale]) - ax[:tickfont] = plotly_font(axis[:tickfont], axis[:foreground_color_text]) - ax[:tickcolor] = rgba_string(axis[:foreground_color_border]) - ax[:linecolor] = rgba_string(axis[:foreground_color_border]) + if axis[:ticks] != :native || axis[:lims] != :auto + ax[:range] = map(scalefunc(axis[:scale]), lims) + end - # lims - lims = axis[:lims] - if lims != :auto && limsType(lims) == :limits - ax[:range] = map(scalefunc(axis[:scale]), lims) - end + if !(axis[:ticks] in (nothing, :none, false)) + ax[:titlefont] = plotly_font(guidefont(axis)) + ax[:tickfont] = plotly_font(tickfont(axis)) + ax[:tickcolor] = framestyle in (:zerolines, :grid) || !axis[:showaxis] ? rgba_string(invisible()) : rgb_string(axis[:foreground_color_axis]) + ax[:linecolor] = rgba_string(axis[:foreground_color_axis]) # flip if axis[:flip] - ax[:autorange] = "reversed" + ax[:range] = reverse(ax[:range]) end # ticks - ticks = get_ticks(axis) - if ticks != :auto + if axis[:ticks] != :native + ticks = get_ticks(axis) ttype = ticksType(ticks) if ttype == :ticks ax[:tickmode] = "array" @@ -259,6 +298,23 @@ function plotly_axis(axis::Axis, sp::Subplot) ax[:showgrid] = false end + + ax +end + +function plotly_polaraxis(axis::Axis) + ax = KW( + :visible => axis[:showaxis], + :showline => axis[:grid], + ) + + if axis[:letter] == :x + ax[:range] = rad2deg.(axis_limits(axis)) + else + ax[:range] = axis_limits(axis) + ax[:orientation] = -90 + end + ax end @@ -268,14 +324,15 @@ function plotly_layout(plt::Plot) w, h = plt[:size] d_out[:width], d_out[:height] = w, h d_out[:paper_bgcolor] = rgba_string(plt[:background_color_outside]) - d_out[:margin] = KW(:l=>0, :b=>0, :r=>0, :t=>20) + d_out[:margin] = KW(:l=>0, :b=>20, :r=>0, :t=>20) d_out[:annotations] = KW[] + multiple_subplots = length(plt.subplots) > 1 + for sp in plt.subplots - spidx = plotly_subplot_index(sp) - - + spidx = multiple_subplots ? sp[:subplot_index] : "" + x_idx, y_idx = multiple_subplots ? plotly_link_indicies(plt, sp) : ("", "") # add an annotation for the title... positioned horizontally relative to plotarea, # but vertically just below the top of the subplot bounding box if sp[:title] != "" @@ -289,22 +346,40 @@ function plotly_layout(plt::Plot) 0.5 * (left(bb) + right(bb)) end titlex, titley = xy_mm_to_pcts(xmm, top(bbox(sp)), w*px, h*px) - titlefont = font(sp[:titlefont], :top, sp[:foreground_color_title]) - push!(d_out[:annotations], plotly_annotation_dict(titlex, titley, text(sp[:title], titlefont))) + title_font = font(titlefont(sp), :top) + push!(d_out[:annotations], plotly_annotation_dict(titlex, titley, text(sp[:title], title_font))) end d_out[:plot_bgcolor] = rgba_string(sp[:background_color_inside]) + # set to supported framestyle + sp[:framestyle] = _plotly_framestyle(sp[:framestyle]) + # if any(is3d, seriesargs) if is3d(sp) + azim = sp[:camera][1] - 90 #convert azimuthal to match GR behaviour + theta = 90 - sp[:camera][2] #spherical coordinate angle from z axis d_out[:scene] = KW( - Symbol("xaxis$spidx") => plotly_axis(sp[:xaxis], sp), - Symbol("yaxis$spidx") => plotly_axis(sp[:yaxis], sp), - Symbol("zaxis$spidx") => plotly_axis(sp[:zaxis], sp), + Symbol("xaxis$(spidx)") => plotly_axis(plt, sp[:xaxis], sp), + Symbol("yaxis$(spidx)") => plotly_axis(plt, sp[:yaxis], sp), + Symbol("zaxis$(spidx)") => plotly_axis(plt, sp[:zaxis], sp), + + #2.6 multiplier set camera eye such that whole plot can be seen + :camera => KW( + :eye => KW( + :x => cosd(azim)*sind(theta)*2.6, + :y => sind(azim)*sind(theta)*2.6, + :z => cosd(theta)*2.6, + ), + ), ) + elseif ispolar(sp) + d_out[Symbol("angularaxis$(spidx)")] = plotly_polaraxis(sp[:xaxis]) + d_out[Symbol("radialaxis$(spidx)")] = plotly_polaraxis(sp[:yaxis]) else - d_out[Symbol("xaxis$spidx")] = plotly_axis(sp[:xaxis], sp) - d_out[Symbol("yaxis$spidx")] = plotly_axis(sp[:yaxis], sp) + d_out[Symbol("xaxis$(x_idx)")] = plotly_axis(plt, sp[:xaxis], sp) + # don't allow yaxis to be reupdated/reanchored in a linked subplot + spidx == y_idx ? d_out[Symbol("yaxis$(y_idx)")] = plotly_axis(plt, sp[:yaxis], sp) : nothing end # legend @@ -314,15 +389,17 @@ function plotly_layout(plt::Plot) 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]), + :font => plotly_font(legendfont(sp)), + :tracegroupgap => 0, :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]]) - + for ann in sp[:annotations] + append!(d_out[:annotations], KW[plotly_annotation_dict(locate_annotation(sp, ann...)...; xref = "x$(x_idx)", yref = "y$(y_idx)")]) + end # series_annotations for series in series_list(sp) anns = series[:series_annotations] @@ -330,7 +407,7 @@ function plotly_layout(plt::Plot) push!(d_out[:annotations], plotly_annotation_dict( xi, yi, - PlotText(str,fnt); xref = "x$spidx", yref = "y$spidx") + PlotText(str,fnt); xref = "x$(x_idx)", yref = "y$(y_idx)") ) end end @@ -366,9 +443,17 @@ end function plotly_colorscale(grad::ColorGradient, α) - [[grad.values[i], rgb_string(grad.colors[i])] for i in 1:length(grad.colors)] + [[grad.values[i], rgba_string(plot_color(grad.colors[i], α))] for i in 1:length(grad.colors)] end plotly_colorscale(c, α) = plotly_colorscale(cgrad(alpha=α), α) +function plotly_colorscale(c::AbstractVector{<:RGBA}, α) + if length(c) == 1 + return [[0.0, rgba_string(plot_color(c[1], α))], [1.0, rgba_string(plot_color(c[1], α))]] + else + vals = linspace(0.0, 1.0, length(c)) + return [[vals[i], rgba_string(plot_color(c[i], α))] for i in eachindex(c)] + end +end # plotly_colorscale(c, alpha = nothing) = plotly_colorscale(cgrad(), alpha) @@ -383,9 +468,15 @@ const _plotly_markers = KW( :hline => "line-ew", ) -function plotly_subplot_index(sp::Subplot) - spidx = sp[:subplot_index] - spidx == 1 ? "" : spidx +# find indicies of axes to which the supblot links to +function plotly_link_indicies(plt::Plot, sp::Subplot) + if plt[:link] in (:x, :y, :both) + x_idx = sp[:xaxis].sps[1][:subplot_index] + y_idx = sp[:yaxis].sps[1][:subplot_index] + else + x_idx = y_idx = sp[:subplot_index] + end + x_idx, y_idx end @@ -400,14 +491,55 @@ function plotly_close_shapes(x, y) nanvcat(xs), nanvcat(ys) end -plotly_data(v) = collect(v) +function plotly_data(series::Series, letter::Symbol, data) + axis = series[:subplot][Symbol(letter, :axis)] + + data = if axis[:ticks] == :native && data != nothing + plotly_native_data(axis, data) + else + data + end + + if series[:seriestype] in (:heatmap, :contour, :surface, :wireframe) + plotly_surface_data(series, data) + else + plotly_data(data) + end +end +plotly_data(v) = v != nothing ? collect(v) : v plotly_data(surf::Surface) = surf.surf -plotly_data{R<:Rational}(v::AbstractArray{R}) = float(v) +plotly_data(v::AbstractArray{R}) where {R<:Rational} = float(v) plotly_surface_data(series::Series, a::AbstractVector) = a plotly_surface_data(series::Series, a::AbstractMatrix) = transpose_z(series, a, false) plotly_surface_data(series::Series, a::Surface) = plotly_surface_data(series, a.surf) +function plotly_native_data(axis::Axis, data::AbstractArray) + if !isempty(axis[:discrete_values]) + construct_categorical_data(data, axis) + elseif axis[:formatter] in (datetimeformatter, dateformatter, timeformatter) + plotly_convert_to_datetime(data, axis[:formatter]) + else + data + end +end +plotly_native_data(axis::Axis, a::Surface) = Surface(plotly_native_data(axis, a.surf)) + +function plotly_convert_to_datetime(x::AbstractArray, formatter::Function) + if formatter == datetimeformatter + map(xi -> replace(formatter(xi), "T", " "), x) + elseif formatter == dateformatter + map(xi -> string(formatter(xi), " 00:00:00"), x) + elseif formatter == timeformatter + map(xi -> string(Dates.Date(Dates.now()), " ", formatter(xi)), x) + else + error("Invalid DateTime formatter. Expected Plots.datetime/date/time formatter but got $formatter") + end +end +#ensures that a gradient is called if a single color is supplied where a gradient is needed (e.g. if a series recipe defines marker_z) +as_gradient(grad::ColorGradient, α) = grad +as_gradient(grad, α) = cgrad(alpha = α) + # get a dictionary representing the series params (d is the Plots-dict, d_out is the Plotly-dict) function plotly_series(plt::Plot, series::Series) st = series[:seriestype] @@ -419,79 +551,77 @@ function plotly_series(plt::Plot, series::Series) d_out = KW() # these are the axes that the series should be mapped to - spidx = plotly_subplot_index(sp) - d_out[:xaxis] = "x$spidx" - d_out[:yaxis] = "y$spidx" + x_idx, y_idx = plotly_link_indicies(plt, sp) + d_out[:xaxis] = "x$(x_idx)" + d_out[:yaxis] = "y$(y_idx)" d_out[:showlegend] = should_add_to_legend(series) - x, y = plotly_data(series[:x]), plotly_data(series[:y]) + if st == :straightline + x, y = straightline_data(series) + z = series[:z] + else + x, y, z = series[:x], series[:y], series[:z] + end + + x, y, z = (plotly_data(series, letter, data) + for (letter, data) in zip((:x, :y, :z), (x, y, z)) + ) + d_out[:name] = series[:label] isscatter = st in (:scatter, :scatter3d, :scattergl) hasmarker = isscatter || series[:markershape] != :none - hasline = st in (:path, :path3d) + hasline = st in (:path, :path3d, :straightline) + hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && + (isa(series[:fillrange], AbstractVector) || isa(series[:fillrange], Tuple)) - # for surface types, set the data - if st in (:heatmap, :contour, :surface, :wireframe) - for letter in [:x,:y,:z] - d_out[letter] = plotly_surface_data(series, series[letter]) - end + d_out[:colorbar] = KW(:title => sp[:colorbar_title]) + + clims = sp[:clims] + if is_2tuple(clims) + d_out[:zmin], d_out[:zmax] = clims end # set the "type" - if st in (:path, :scatter, :scattergl) - d_out[:type] = st==:scattergl ? "scattergl" : "scatter" - d_out[:mode] = if hasmarker - hasline ? "lines+markers" : "markers" - else - hasline ? "lines" : "none" - end - if series[:fillrange] == true || series[:fillrange] == 0 - d_out[:fill] = "tozeroy" - d_out[:fillcolor] = rgba_string(series[:fillcolor]) - elseif !(series[:fillrange] in (false, nothing)) - warn("fillrange ignored... plotly only supports filling to zero. fillrange: $(series[:fillrange])") - end - d_out[:x], d_out[:y] = x, y - - elseif st == :bar - d_out[:type] = "bar" - 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])) + if st in (:path, :scatter, :scattergl, :straightline, :path3d, :scatter3d) + return plotly_series_segments(series, d_out, x, y, z) elseif st == :heatmap + x = heatmap_edges(x, sp[:xaxis][:scale]) + y = heatmap_edges(y, sp[:yaxis][:scale]) d_out[:type] = "heatmap" - # d_out[:x], d_out[:y], d_out[:z] = series[:x], series[:y], transpose_z(series, series[:z].surf, false) + d_out[:x], d_out[:y], d_out[:z] = x, y, z d_out[:colorscale] = plotly_colorscale(series[:fillcolor], series[:fillalpha]) + d_out[:showscale] = hascolorbar(sp) elseif st == :contour d_out[:type] = "contour" - # d_out[:x], d_out[:y], d_out[:z] = series[:x], series[:y], transpose_z(series, series[:z].surf, false) + d_out[:x], d_out[:y], d_out[:z] = x, y, z # d_out[:showscale] = series[:colorbar] != :none d_out[:ncontours] = series[:levels] - d_out[:contours] = KW(:coloring => series[:fillrange] != nothing ? "fill" : "lines") + d_out[:contours] = KW(:coloring => series[:fillrange] != nothing ? "fill" : "lines", :showlabels => series[:contour_labels] == true) d_out[:colorscale] = plotly_colorscale(series[:linecolor], series[:linealpha]) + d_out[:showscale] = hascolorbar(sp) elseif st in (:surface, :wireframe) d_out[:type] = "surface" - # d_out[:x], d_out[:y], d_out[:z] = series[:x], series[:y], transpose_z(series, series[:z].surf, false) + d_out[:x], d_out[:y], d_out[:z] = x, y, z if st == :wireframe d_out[:hidesurface] = true wirelines = KW( :show => true, - :color => rgba_string(series[:linecolor]), + :color => rgba_string(plot_color(series[:linecolor], series[:linealpha])), :highlightwidth => series[:linewidth], ) d_out[:contours] = KW(:x => wirelines, :y => wirelines, :z => wirelines) + d_out[:showscale] = false else d_out[:colorscale] = plotly_colorscale(series[:fillcolor], series[:fillalpha]) + d_out[:opacity] = series[:fillalpha] if series[:fill_z] != nothing d_out[:surfacecolor] = plotly_surface_data(series, series[:fill_z]) end + d_out[:showscale] = hascolorbar(sp) end elseif st == :pie @@ -500,16 +630,6 @@ function plotly_series(plt::Plot, series::Series) d_out[:values] = y d_out[:hoverinfo] = "label+percent+name" - elseif st in (:path3d, :scatter3d) - d_out[:type] = "scatter3d" - d_out[:mode] = if hasmarker - hasline ? "lines+markers" : "markers" - else - hasline ? "lines" : "none" - end - d_out[:x], d_out[:y] = x, y - d_out[:z] = plotly_data(series[:z]) - else warn("Plotly: seriestype $st isn't supported.") return KW() @@ -517,98 +637,235 @@ function plotly_series(plt::Plot, series::Series) # add "marker" if hasmarker + inds = eachindex(x) d_out[:marker] = KW( :symbol => get(_plotly_markers, series[:markershape], string(series[:markershape])), # :opacity => series[:markeralpha], - :size => 2 * series[:markersize], - # :color => rgba_string(series[:markercolor]), + :size => 2 * _cycle(series[:markersize], inds), + :color => rgba_string.(plot_color.(get_markercolor.(series, inds), get_markeralpha.(series, inds))), :line => KW( - :color => rgba_string(series[:markerstrokecolor]), - :width => series[:markerstrokewidth], + :color => rgba_string.(plot_color.(get_markerstrokecolor.(series, inds), get_markerstrokealpha.(series, inds))), + :width => _cycle(series[:markerstrokewidth], inds), ), ) - - # gotta hack this (for now?) since plotly can't handle rgba values inside the gradient - d_out[:marker][:color] = if series[:marker_z] == nothing - rgba_string(series[:markercolor]) - else - # grad = ColorGradient(series[:markercolor], alpha=series[:markeralpha]) - grad = series[:markercolor] - zmin, zmax = extrema(series[:marker_z]) - [rgba_string(grad[(zi - zmin) / (zmax - zmin)]) for zi in series[:marker_z]] - end - end - - # add "line" - if hasline - d_out[:line] = KW( - :color => rgba_string(series[:linecolor]), - :width => series[:linewidth], - :shape => if st == :steppre - "vh" - elseif st == :steppost - "hv" - else - "linear" - end, - :dash => string(series[:linestyle]), - # :dash => "solid", - ) end plotly_polar!(d_out, series) plotly_hover!(d_out, series[:hover]) - [d_out] + return [d_out] end function plotly_series_shapes(plt::Plot, series::Series) - d_outs = [] + segments = iter_segments(series) + d_outs = Vector{KW}(length(segments)) # TODO: create a d_out for each polygon # x, y = series[:x], series[:y] # these are the axes that the series should be mapped to - spidx = plotly_subplot_index(series[:subplot]) - base_d = KW() - base_d[:xaxis] = "x$spidx" - base_d[:yaxis] = "y$spidx" - base_d[:name] = series[:label] - # base_d[:legendgroup] = series[:label] + x_idx, y_idx = plotly_link_indicies(plt, series[:subplot]) + d_base = KW( + :xaxis => "x$(x_idx)", + :yaxis => "y$(y_idx)", + :name => series[:label], + :legendgroup => series[:label], + ) - x, y = plotly_data(series[:x]), plotly_data(series[:y]) - for (i,rng) in enumerate(iter_segments(x,y)) + x, y = (plotly_data(series, letter, data) + for (letter, data) in zip((:x, :y), shape_data(series)) + ) + + for (i,rng) in enumerate(segments) length(rng) < 2 && continue # to draw polygons, we actually draw lines with fill - d_out = merge(base_d, KW( + d_out = merge(d_base, KW( :type => "scatter", :mode => "lines", :x => vcat(x[rng], x[rng[1]]), :y => vcat(y[rng], y[rng[1]]), :fill => "tozeroy", - :fillcolor => rgba_string(cycle(series[:fillcolor], i)), + :fillcolor => rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))), )) if series[:markerstrokewidth] > 0 d_out[:line] = KW( - :color => rgba_string(cycle(series[:linecolor], i)), - :width => series[:linewidth], - :dash => string(series[:linestyle]), + :color => rgba_string(plot_color(get_linecolor(series, i), get_linealpha(series, i))), + :width => get_linewidth(series, i), + :dash => string(get_linestyle(series, i)), ) end d_out[:showlegend] = i==1 ? should_add_to_legend(series) : false plotly_polar!(d_out, series) - plotly_hover!(d_out, cycle(series[:hover], i)) - push!(d_outs, d_out) + plotly_hover!(d_out, _cycle(series[:hover], i)) + d_outs[i] = d_out + end + if series[:fill_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :fill)) + elseif series[:line_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :line)) + elseif series[:marker_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :marker)) end d_outs end +function plotly_series_segments(series::Series, d_base::KW, x, y, z) + st = series[:seriestype] + sp = series[:subplot] + isscatter = st in (:scatter, :scatter3d, :scattergl) + hasmarker = isscatter || series[:markershape] != :none + hasline = st in (:path, :path3d, :straightline) + hasfillrange = st in (:path, :scatter, :scattergl, :straightline) && + (isa(series[:fillrange], AbstractVector) || isa(series[:fillrange], Tuple)) + + segments = iter_segments(series) + d_outs = Vector{KW}((hasfillrange ? 2 : 1 ) * length(segments)) + + for (i,rng) in enumerate(segments) + !isscatter && length(rng) < 2 && continue + + d_out = deepcopy(d_base) + d_out[:showlegend] = i==1 ? should_add_to_legend(series) : false + d_out[:legendgroup] = series[:label] + + # set the type + if st in (:path, :scatter, :scattergl, :straightline) + d_out[:type] = st==:scattergl ? "scattergl" : "scatter" + d_out[:mode] = if hasmarker + hasline ? "lines+markers" : "markers" + else + hasline ? "lines" : "none" + end + if series[:fillrange] == true || series[:fillrange] == 0 || isa(series[:fillrange], Tuple) + d_out[:fill] = "tozeroy" + d_out[:fillcolor] = rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))) + elseif typeof(series[:fillrange]) <: Union{AbstractVector{<:Real}, Real} + d_out[:fill] = "tonexty" + d_out[:fillcolor] = rgba_string(plot_color(get_fillcolor(series, i), get_fillalpha(series, i))) + elseif !(series[:fillrange] in (false, nothing)) + warn("fillrange ignored... plotly only supports filling to zero and to a vector of values. fillrange: $(series[:fillrange])") + end + d_out[:x], d_out[:y] = x[rng], y[rng] + + elseif st in (:path3d, :scatter3d) + d_out[:type] = "scatter3d" + d_out[:mode] = if hasmarker + hasline ? "lines+markers" : "markers" + else + hasline ? "lines" : "none" + end + d_out[:x], d_out[:y], d_out[:z] = x[rng], y[rng], z[rng] + end + + # add "marker" + if hasmarker + d_out[:marker] = KW( + :symbol => get(_plotly_markers, _cycle(series[:markershape], i), string(_cycle(series[:markershape], i))), + # :opacity => series[:markeralpha], + :size => 2 * _cycle(series[:markersize], i), + :color => rgba_string(plot_color(get_markercolor(series, i), get_markeralpha(series, i))), + :line => KW( + :color => rgba_string(plot_color(get_markerstrokecolor(series, i), get_markerstrokealpha(series, i))), + :width => _cycle(series[:markerstrokewidth], i), + ), + ) + end + + # add "line" + if hasline + d_out[:line] = KW( + :color => rgba_string(plot_color(get_linecolor(series, i), get_linealpha(series, i))), + :width => get_linewidth(series, i), + :shape => if st == :steppre + "vh" + elseif st == :steppost + "hv" + else + "linear" + end, + :dash => string(get_linestyle(series, i)), + ) + end + + plotly_polar!(d_out, series) + plotly_hover!(d_out, _cycle(series[:hover], rng)) + + if hasfillrange + # if hasfillrange is true, return two dictionaries (one for original + # series, one for series being filled to) instead of one + d_out_fillrange = deepcopy(d_out) + d_out_fillrange[:showlegend] = false + # if fillrange is provided as real or tuple of real, expand to array + if typeof(series[:fillrange]) <: Real + series[:fillrange] = fill(series[:fillrange], length(rng)) + elseif typeof(series[:fillrange]) <: Tuple + f1 = typeof(series[:fillrange][1]) <: Real ? fill(series[:fillrange][1], length(rng)) : series[:fillrange][1][rng] + f2 = typeof(series[:fillrange][2]) <: Real ? fill(series[:fillrange][2], length(rng)) : series[:fillrange][2][rng] + series[:fillrange] = (f1, f2) + end + if isa(series[:fillrange], AbstractVector) + d_out_fillrange[:y] = series[:fillrange][rng] + delete!(d_out_fillrange, :fill) + delete!(d_out_fillrange, :fillcolor) + else + # if fillrange is a tuple with upper and lower limit, d_out_fillrange + # is the series that will do the filling + fillrng = Tuple(series[:fillrange][i][rng] for i in 1:2) + d_out_fillrange[:x], d_out_fillrange[:y] = concatenate_fillrange(x[rng], fillrng) + d_out_fillrange[:line][:width] = 0 + delete!(d_out, :fill) + delete!(d_out, :fillcolor) + end + + d_outs[(2 * i - 1):(2 * i)] = [d_out_fillrange, d_out] + else + d_outs[i] = d_out + end + end + + if series[:line_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :line)) + elseif series[:fill_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :fill)) + elseif series[:marker_z] != nothing + push!(d_outs, plotly_colorbar_hack(series, d_base, :marker)) + end + + d_outs +end + +function plotly_colorbar_hack(series::Series, d_base::KW, sym::Symbol) + d_out = deepcopy(d_base) + cmin, cmax = get_clims(series[:subplot]) + d_out[:showlegend] = false + d_out[:type] = is3d(series) ? :scatter3d : :scatter + d_out[:hoverinfo] = :none + d_out[:mode] = :markers + d_out[:x], d_out[:y] = [series[:x][1]], [series[:y][1]] + if is3d(series) + d_out[:z] = [series[:z][1]] + end + # zrange = zmax == zmin ? 1 : zmax - zmin # if all marker_z values are the same, plot all markers same color (avoids division by zero in next line) + d_out[:marker] = KW( + :size => 0, + :opacity => 0, + :color => [0.5], + :cmin => cmin, + :cmax => cmax, + :colorscale => plotly_colorscale(series[Symbol("$(sym)color")], 1), + :showscale => hascolorbar(series[:subplot]), + ) + return d_out +end + + function plotly_polar!(d_out::KW, series::Series) # convert polar plots x/y to theta/radius if ispolar(series[:subplot]) - d_out[:t] = rad2deg(pop!(d_out, :x)) - d_out[:r] = pop!(d_out, :y) + theta, r = filter_radial_data(pop!(d_out, :x), pop!(d_out, :y), axis_limits(series[:subplot][:yaxis])) + d_out[:t] = rad2deg.(theta) + d_out[:r] = r end end @@ -623,21 +880,23 @@ function plotly_hover!(d_out::KW, hover) end # get a list of dictionaries, each representing the series params -function plotly_series_json(plt::Plot) +function plotly_series(plt::Plot) slist = [] for series in plt.series_list append!(slist, plotly_series(plt, series)) end - JSON.json(slist) - # JSON.json(map(series -> plotly_series(plt, series), plt.series_list)) + slist end +# get json string for a list of dictionaries, each representing the series params +plotly_series_json(plt::Plot) = JSON.json(plotly_series(plt)) + # ---------------------------------------------------------------- const _use_remote = Ref(false) function html_head(plt::Plot{PlotlyBackend}) - jsfilename = _use_remote[] ? _plotly_js_path_remote : _plotly_js_path + jsfilename = _use_remote[] ? _plotly_js_path_remote : ("file://" * _plotly_js_path) # "" "" end @@ -669,12 +928,7 @@ end # ---------------------------------------------------------------- -function _show(io::IO, ::MIME"image/png", plt::Plot{PlotlyBackend}) - # 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}) +function _show(io::IO, ::MIME"text/html", plt::Plot{PlotlyBackend}) write(io, html_head(plt) * html_body(plt)) end diff --git a/src/backends/plotlyjs.jl b/src/backends/plotlyjs.jl index 6d4ad145..e2883220 100644 --- a/src/backends/plotlyjs.jl +++ b/src/backends/plotlyjs.jl @@ -1,3 +1,6 @@ +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "plotlyjs.jl")) +end # https://github.com/spencerlyon2/PlotlyJS.jl @@ -85,7 +88,7 @@ end # ---------------------------------------------------------------- -function _show(io::IO, ::MIME"image/svg+xml", plt::Plot{PlotlyJSBackend}) +function _show(io::IO, ::MIME"text/html", plt::Plot{PlotlyJSBackend}) if isijulia() && !_use_remote[] write(io, PlotlyJS.html_body(PlotlyJS.JupyterPlot(plt.o))) else @@ -98,14 +101,31 @@ function plotlyjs_save_hack(io::IO, plt::Plot{PlotlyJSBackend}, ext::String) PlotlyJS.savefig(plt.o, tmpfn) write(io, read(open(tmpfn))) end +_show(io::IO, ::MIME"image/svg+xml", plt::Plot{PlotlyJSBackend}) = plotlyjs_save_hack(io, plt, "svg") _show(io::IO, ::MIME"image/png", plt::Plot{PlotlyJSBackend}) = plotlyjs_save_hack(io, plt, "png") _show(io::IO, ::MIME"application/pdf", plt::Plot{PlotlyJSBackend}) = plotlyjs_save_hack(io, plt, "pdf") _show(io::IO, ::MIME"image/eps", plt::Plot{PlotlyJSBackend}) = plotlyjs_save_hack(io, plt, "eps") -function _display(plt::Plot{PlotlyJSBackend}) - display(plt.o) +function write_temp_html(plt::Plot{PlotlyJSBackend}) + filename = string(tempname(), ".html") + savefig(plt, filename) + filename end +function _display(plt::Plot{PlotlyJSBackend}) + if get(ENV, "PLOTS_USE_ATOM_PLOTPANE", true) in (true, 1, "1", "true", "yes") + display(plt.o) + else + standalone_html_window(plt) + end +end + +@require WebIO begin + function WebIO.render(plt::Plot{PlotlyJSBackend}) + prepare_output(plt) + WebIO.render(plt.o) + end +end function closeall(::PlotlyJSBackend) if !isplotnull() && isa(current().o, PlotlyJS.SyncPlot) diff --git a/src/backends/pyplot.jl b/src/backends/pyplot.jl index 84ea6718..92f33fd5 100644 --- a/src/backends/pyplot.jl +++ b/src/backends/pyplot.jl @@ -1,6 +1,9 @@ # https://github.com/stevengj/PyPlot.jl +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "pyplot.jl")) +end const _pyplot_attr = merge_with_base_supported([ :annotations, @@ -16,8 +19,12 @@ const _pyplot_attr = merge_with_base_supported([ :title, :title_location, :titlefont, :window_title, :guide, :lims, :ticks, :scale, :flip, :rotation, - :tickfont, :guidefont, :legendfont, - :grid, :legend, :colorbar, + :titlefontfamily, :titlefontsize, :titlefontcolor, + :legendfontfamily, :legendfontsize, :legendfontcolor, + :tickfontfamily, :tickfontsize, :tickfontcolor, + :guidefontfamily, :guidefontsize, :guidefontcolor, + :grid, :gridalpha, :gridstyle, :gridlinewidth, + :legend, :legendtitle, :colorbar, :marker_z, :line_z, :fill_z, :levels, :ribbon, :quiver, :arrow, @@ -31,9 +38,14 @@ const _pyplot_attr = merge_with_base_supported([ :inset_subplots, :dpi, :colorbar_title, + :stride, + :framestyle, + :tick_direction, + :camera, + :contour_labels, ]) const _pyplot_seriestype = [ - :path, :steppre, :steppost, :shape, + :path, :steppre, :steppost, :shape, :straightline, :scatter, :hexbin, #:histogram2d, :histogram, # :bar, :heatmap, :pie, :image, @@ -55,6 +67,8 @@ function add_backend_string(::PyPlotBackend) withenv("PYTHON" => "") do Pkg.build("PyPlot") end + + # now restart julia! """ end @@ -68,22 +82,30 @@ function _initialize_backend(::PyPlotBackend) append!(Base.Multimedia.displays, otherdisplays) export PyPlot - const pycolors = PyPlot.pywrap(PyPlot.pyimport("matplotlib.colors")) - const pypath = PyPlot.pywrap(PyPlot.pyimport("matplotlib.path")) - const mplot3d = PyPlot.pywrap(PyPlot.pyimport("mpl_toolkits.mplot3d")) - const pypatches = PyPlot.pywrap(PyPlot.pyimport("matplotlib.patches")) - const pyfont = PyPlot.pywrap(PyPlot.pyimport("matplotlib.font_manager")) - const pyticker = PyPlot.pywrap(PyPlot.pyimport("matplotlib.ticker")) - const pycmap = PyPlot.pywrap(PyPlot.pyimport("matplotlib.cm")) - const pynp = PyPlot.pywrap(PyPlot.pyimport("numpy")) - pynp.seterr(invalid="ignore") - const pytransforms = PyPlot.pywrap(PyPlot.pyimport("matplotlib.transforms")) - const pycollections = PyPlot.pywrap(PyPlot.pyimport("matplotlib.collections")) - const pyart3d = PyPlot.pywrap(PyPlot.pyimport("mpl_toolkits.mplot3d.art3d")) - end + const pycolors = PyPlot.pyimport("matplotlib.colors") + const pypath = PyPlot.pyimport("matplotlib.path") + const mplot3d = PyPlot.pyimport("mpl_toolkits.mplot3d") + const pypatches = PyPlot.pyimport("matplotlib.patches") + const pyfont = PyPlot.pyimport("matplotlib.font_manager") + const pyticker = PyPlot.pyimport("matplotlib.ticker") + const pycmap = PyPlot.pyimport("matplotlib.cm") + const pynp = PyPlot.pyimport("numpy") + pynp["seterr"](invalid="ignore") + const pytransforms = PyPlot.pyimport("matplotlib.transforms") + const pycollections = PyPlot.pyimport("matplotlib.collections") + const pyart3d = PyPlot.art3D - # we don't want every command to update the figure - PyPlot.ioff() + # "support" matplotlib v1.5 + const set_facecolor_sym = if PyPlot.version < v"2" + warn("You are using Matplotlib $(PyPlot.version), which is no longer officialy supported by the Plots community. To ensure smooth Plots.jl integration update your Matplotlib library to a version >= 2.0.0") + :set_axis_bgcolor + else + :set_facecolor + end + + # we don't want every command to update the figure + PyPlot.ioff() + end end # -------------------------------------------------------------------------------------- @@ -99,7 +121,7 @@ end # function py_colormap(c::ColorGradient, α=nothing) # pyvals = [(v, py_color(getColorZ(c, v), α)) for v in c.values] -# pycolors.pymember("LinearSegmentedColormap")[:from_list]("tmp", pyvals) +# pycolors["LinearSegmentedColormap"][:from_list]("tmp", pyvals) # end # # convert vectors and ColorVectors to standard ColorGradients @@ -110,13 +132,15 @@ end # # anything else just gets a bluesred gradient # py_colormap(c, α=nothing) = py_colormap(default_gradient(), α) +py_color(s) = py_color(parse(Colorant, string(s))) py_color(c::Colorant) = (red(c), green(c), blue(c), alpha(c)) py_color(cs::AVec) = map(py_color, cs) py_color(grad::ColorGradient) = py_color(grad.colors) +py_color(c::Colorant, α) = py_color(plot_color(c, α)) function py_colormap(grad::ColorGradient) pyvals = [(z, py_color(grad[z])) for z in grad.values] - cm = 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 @@ -125,7 +149,7 @@ py_colormap(c) = py_colormap(cgrad()) function py_shading(c, z) cmap = py_colormap(c) - ls = pycolors.pymember("LightSource")(270,45) + ls = pycolors["LightSource"](270,45) ls[:shade](z, cmap, vert_exag=0.1, blend_mode="soft") end @@ -149,7 +173,7 @@ function py_marker(marker::Shape) mat[i,2] = y[i] end mat[n+1,:] = mat[1,:] - pypath.pymember("Path")(mat) + pypath["Path"](mat) end const _path_MOVETO = UInt8(1) @@ -175,7 +199,7 @@ const _path_CLOSEPOLY = UInt8(79) # lastnan = nan # end # codes[n+1] = _path_CLOSEPOLY -# pypath.pymember("Path")(mat, codes) +# pypath["Path"](mat, codes) # end # get the marker shape @@ -193,6 +217,8 @@ function py_marker(marker::Symbol) marker == :hexagon && return "h" marker == :octagon && return "8" marker == :pixel && return "," + marker == :hline && return "_" + marker == :vline && return "|" haskey(_shapes, marker) && return py_marker(_shapes[marker]) warn("Unknown marker $marker") @@ -217,16 +243,22 @@ function py_stepstyle(seriestype::Symbol) return "default" end +function py_fillstepstyle(seriestype::Symbol) + seriestype == :steppost && return "post" + seriestype == :steppre && return "pre" + return nothing +end + # # untested... return a FontProperties object from a Plots.Font # function py_font(font::Font) -# pyfont.pymember("FontProperties")( +# pyfont["FontProperties"]( # family = font.family, # size = font.size # ) # end function get_locator_and_formatter(vals::AVec) - pyticker.pymember("FixedLocator")(1:length(vals)), pyticker.pymember("FixedFormatter")(vals) + pyticker["FixedLocator"](1:length(vals)), pyticker["FixedFormatter"](vals) end function add_pyfixedformatter(cbar, vals::AVec) @@ -248,9 +280,9 @@ function labelfunc(scale::Symbol, backend::PyPlotBackend) 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) + # pynp["ma"][:masked_invalid](z))) + PyCall.pycall(pynp["ma"][:masked_invalid], Any, z) + # pynp["ma"][:masked_where](pynp["isnan"](z),z) end # --------------------------------------------------------------------------- @@ -362,15 +394,15 @@ function py_bbox_title(ax) bb end -function py_dpi_scale(plt::Plot{PyPlotBackend}, ptsz) - ptsz * plt[:dpi] / DPI +function py_thickness_scale(plt::Plot{PyPlotBackend}, ptsz) + ptsz * plt[:thickness_scaling] end # --------------------------------------------------------------------------- # Create the window/figure for this backend. function _create_backend_figure(plt::Plot{PyPlotBackend}) - w,h = map(px2inch, plt[:size]) + w,h = map(px2inch, Tuple(s * plt[:dpi] / Plots.DPI for s in plt[:size])) # # reuse the current figure? fig = if plt[:overwrite_figure] @@ -422,14 +454,24 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # ax = getAxis(plt, series) x, y, z = series[:x], series[:y], series[:z] + if st == :straightline + x, y = straightline_data(series) + elseif st == :shape + x, y = shape_data(series) + end xyargs = (st in _3dTypes ? (x,y,z) : (x,y)) # handle zcolor and get c/cmap - extrakw = KW() + needs_colorbar = hascolorbar(sp) + extrakw = if needs_colorbar || is_2tuple(sp[:clims]) + vmin, vmax = get_clims(sp) + KW(:vmin => vmin, :vmax => vmax) + else + KW() + end # holds references to any python object representing the matplotlib series handles = [] - needs_colorbar = false discrete_colorbar_values = nothing @@ -450,68 +492,52 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # for each plotting command, optionally build and add a series handle to the list # line plot - if st in (:path, :path3d, :steppre, :steppost) - if series[:linewidth] > 0 - if series[:line_z] == nothing - handle = ax[:plot](xyargs...; - label = series[:label], - zorder = series[:series_plotindex], - color = py_linecolor(series), - linewidth = py_dpi_scale(plt, series[:linewidth]), - linestyle = py_linestyle(st, series[:linestyle]), - solid_capstyle = "round", - drawstyle = py_stepstyle(st) - )[1] - push!(handles, handle) - - else - # multicolored line segments - n = length(x) - 1 - # segments = Array(Any,n) - segments = [] - kw = KW( - :label => series[:label], - :zorder => plt.n, - :cmap => py_linecolormap(series), - :linewidth => py_dpi_scale(plt, series[:linewidth]), - :linestyle => py_linestyle(st, series[:linestyle]) - ) - clims = sp[:clims] - if is_2tuple(clims) - extrakw = KW() - isfinite(clims[1]) && (extrakw[:vmin] = clims[1]) - isfinite(clims[2]) && (extrakw[:vmax] = clims[2]) - kw[:norm] = pycolors.Normalize(; extrakw...) + if st in (:path, :path3d, :steppre, :steppost, :straightline) + if maximum(series[:linewidth]) > 0 + segments = iter_segments(series) + # TODO: check LineCollection alternative for speed + # if length(segments) > 1 && (any(typeof(series[attr]) <: AbstractVector for attr in (:fillcolor, :fillalpha)) || series[:fill_z] != nothing) && !(typeof(series[:linestyle]) <: AbstractVector) + # # multicolored line segments + # n = length(segments) + # # segments = Array(Any,n) + # segments = [] + # kw = KW( + # :label => series[:label], + # :zorder => plt.n, + # :cmap => py_linecolormap(series), + # :linewidths => py_thickness_scale(plt, get_linewidth.(series, 1:n)), + # :linestyle => py_linestyle(st, get_linestyle.(series)), + # :norm => pycolors["Normalize"](; extrakw...) + # ) + # lz = _cycle(series[:line_z], 1:n) + # handle = if is3d(st) + # line_segments = [[(x[j], y[j], z[j]) for j in rng] for rng in segments] + # lc = pyart3d["Line3DCollection"](line_segments; kw...) + # lc[:set_array](lz) + # ax[:add_collection3d](lc, zs=z) #, zdir='y') + # lc + # else + # line_segments = [[(x[j], y[j]) for j in rng] for rng in segments] + # lc = pycollections["LineCollection"](line_segments; kw...) + # lc[:set_array](lz) + # ax[:add_collection](lc) + # lc + # end + # push!(handles, handle) + # else + for (i, rng) in enumerate(iter_segments(series)) + handle = ax[:plot]((arg[rng] for arg in xyargs)...; + label = i == 1 ? series[:label] : "", + zorder = series[:series_plotindex], + color = py_color(get_linecolor(series, i), get_linealpha(series, i)), + linewidth = py_thickness_scale(plt, get_linewidth(series, i)), + linestyle = py_linestyle(st, get_linestyle(series, i)), + solid_capstyle = "round", + drawstyle = py_stepstyle(st) + )[1] + push!(handles, handle) end - lz = collect(series[:line_z]) - handle = if is3d(st) - for rng in iter_segments(x, y, z) - length(rng) < 2 && continue - push!(segments, [(cycle(x,i),cycle(y,i),cycle(z,i)) for i in rng]) - end - # for i=1:n - # segments[i] = [(cycle(x,i), cycle(y,i), cycle(z,i)), (cycle(x,i+1), cycle(y,i+1), cycle(z,i+1))] - # end - lc = pyart3d.Line3DCollection(segments; kw...) - lc[:set_array](lz) - ax[:add_collection3d](lc, zs=z) #, zdir='y') - lc - else - for rng in iter_segments(x, y) - length(rng) < 2 && continue - push!(segments, [(cycle(x,i),cycle(y,i)) for i in rng]) - end - # for i=1:n - # segments[i] = [(cycle(x,i), cycle(y,i)), (cycle(x,i+1), cycle(y,i+1))] - # end - lc = pycollections.LineCollection(segments; kw...) - lc[:set_array](lz) - ax[:add_collection](lc) - lc - end - push!(handles, handle) - needs_colorbar = true - end + # end a = series[:arrow] if a != nothing && !is3d(st) # TODO: handle 3d later @@ -524,8 +550,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) :shrinkB => 0, :edgecolor => py_linecolor(series), :facecolor => py_linecolor(series), - :linewidth => py_dpi_scale(plt, series[:linewidth]), - :linestyle => py_linestyle(st, series[:linestyle]), + :linewidth => py_thickness_scale(plt, get_linewidth(series)), + :linestyle => py_linestyle(st, get_linestyle(series)), ) add_arrows(x, y) do xyprev, xy ax[:annotate]("", @@ -544,19 +570,12 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) if series[:markershape] != :none && st in (:path, :scatter, :path3d, :scatter3d, :steppre, :steppost, :bar) - extrakw = KW() - if series[:marker_z] == nothing - extrakw[:c] = py_color_fix(py_markercolor(series), x) + markercolor = if any(typeof(series[arg]) <: AVec for arg in (:markercolor, :markeralpha)) || series[:marker_z] != nothing + py_color(plot_color.(get_markercolor.(series, eachindex(x)), get_markeralpha.(series, eachindex(x)))) else - extrakw[:c] = convert(Vector{Float64}, series[:marker_z]) - extrakw[:cmap] = py_markercolormap(series) - clims = sp[:clims] - if is_2tuple(clims) - isfinite(clims[1]) && (extrakw[:vmin] = clims[1]) - isfinite(clims[2]) && (extrakw[:vmax] = clims[2]) - end - needs_colorbar = true + py_color(plot_color(series[:markercolor], series[:markeralpha])) end + extrakw[:c] = py_color_fix(markercolor, x) xyargs = if st == :bar && !isvertical(series) (y, x) else @@ -570,19 +589,15 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) x,y = xyargs shapes = series[:markershape] msc = py_markerstrokecolor(series) - lw = py_dpi_scale(plt, series[:markerstrokewidth]) + lw = py_thickness_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 + extrakw[:c] = _cycle(markercolor, i) - push!(handle, ax[:scatter](cycle(x,i), cycle(y,i); + 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), + marker = py_marker(_cycle(shapes,i)), + s = py_thickness_scale(plt, _cycle(series[:markersize],i) .^ 2), edgecolors = msc, linewidths = lw, extrakw... @@ -595,9 +610,9 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) label = series[:label], zorder = series[:series_plotindex] + 0.5, marker = py_marker(series[:markershape]), - s = py_dpi_scale(plt, series[:markersize] .^ 2), + s = py_thickness_scale(plt, series[:markersize] .^ 2), edgecolors = py_markerstrokecolor(series), - linewidths = py_dpi_scale(plt, series[:markerstrokewidth]), + linewidths = py_thickness_scale(plt, series[:markerstrokewidth]), extrakw... ) push!(handles, handle) @@ -605,47 +620,48 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) end if st == :hexbin - clims = sp[:clims] - if is_2tuple(clims) - isfinite(clims[1]) && (extrakw[:vmin] = clims[1]) - isfinite(clims[2]) && (extrakw[:vmax] = clims[2]) - end handle = ax[:hexbin](x, y; label = series[:label], zorder = series[:series_plotindex], gridsize = series[:bins], - linewidths = py_dpi_scale(plt, series[:linewidth]), + linewidths = py_thickness_scale(plt, series[:linewidth]), edgecolors = py_linecolor(series), cmap = py_fillcolormap(series), # applies to the pcolorfast object extrakw... ) push!(handles, handle) - needs_colorbar = true end if st in (:contour, :contour3d) z = transpose_z(series, z.surf) - needs_colorbar = true - - clims = sp[:clims] - if is_2tuple(clims) - isfinite(clims[1]) && (extrakw[:vmin] = clims[1]) - isfinite(clims[2]) && (extrakw[:vmax] = clims[2]) + if typeof(x)<:Plots.Surface + x = Plots.transpose_z(series, x.surf) + end + if typeof(y)<:Plots.Surface + y = Plots.transpose_z(series, y.surf) end if st == :contour3d extrakw[:extend3d] = true end + if typeof(series[:linecolor]) <: AbstractArray + extrakw[:colors] = py_color.(series[:linecolor]) + else + extrakw[:cmap] = py_linecolormap(series) + end + # contour lines handle = ax[:contour](x, y, z, levelargs...; label = series[:label], zorder = series[:series_plotindex], - linewidths = py_dpi_scale(plt, series[:linewidth]), + linewidths = py_thickness_scale(plt, series[:linewidth]), linestyles = py_linestyle(st, series[:linestyle]), - cmap = py_linecolormap(series), extrakw... ) + if series[:contour_labels] == true + PyPlot.clabel(handle, handle[:levels]) + end push!(handles, handle) # contour fills @@ -653,7 +669,6 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) handle = ax[:contourf](x, y, z, levelargs...; label = series[:label], zorder = series[:series_plotindex] + 0.5, - cmap = py_fillcolormap(series), extrakw... ) push!(handles, handle) @@ -676,14 +691,13 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) else extrakw[:cmap] = py_fillcolormap(series) end - needs_colorbar = true end handle = ax[st == :surface ? :plot_surface : :plot_wireframe](x, y, z; label = series[:label], zorder = series[:series_plotindex], - rstride = 1, - cstride = 1, - linewidth = py_dpi_scale(plt, series[:linewidth]), + rstride = series[:stride][1], + cstride = series[:stride][2], + linewidth = py_thickness_scale(plt, series[:linewidth]), edgecolor = py_linecolor(series), extrakw... ) @@ -692,21 +706,16 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # contours on the axis planes if series[:contours] for (zdir,mat) in (("x",x), ("y",y), ("z",z)) - offset = (zdir == "y" ? maximum : minimum)(mat) + offset = (zdir == "y" ? ignorenan_maximum : ignorenan_minimum)(mat) handle = ax[:contourf](x, y, z, levelargs...; zdir = zdir, cmap = py_fillcolormap(series), - offset = (zdir == "y" ? maximum : minimum)(mat) # where to draw the contour plane + offset = (zdir == "y" ? ignorenan_maximum : ignorenan_minimum)(mat) # where to draw the contour plane ) push!(handles, handle) - needs_colorbar = true end end - # no colorbar if we are creating a surface LightSource - if haskey(extrakw, :facecolors) - needs_colorbar = false - end elseif typeof(z) <: AbstractVector # tri-surface plot (http://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html#tri-surface-plots) @@ -719,12 +728,11 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) label = series[:label], zorder = series[:series_plotindex], cmap = py_fillcolormap(series), - linewidth = py_dpi_scale(plt, series[:linewidth]), + linewidth = py_thickness_scale(plt, series[:linewidth]), edgecolor = py_linecolor(series), extrakw... ) push!(handles, handle) - needs_colorbar = true else error("Unsupported z type $(typeof(z)) for seriestype=$st") end @@ -732,11 +740,12 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) if st == :image # @show typeof(z) + xmin, xmax = ignorenan_extrema(series[:x]); ymin, ymax = ignorenan_extrema(series[:y]) img = Array(transpose_z(series, z.surf)) z = if eltype(img) <: Colors.AbstractGray float(img) elseif eltype(img) <: Colorant - map(c -> Float64[red(c),green(c),blue(c)], img) + map(c -> Float64[red(c),green(c),blue(c),alpha(c)], img) else z # hopefully it's in a data format that will "just work" with imshow end @@ -744,7 +753,8 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) zorder = series[:series_plotindex], cmap = py_colormap([:black, :white]), vmin = 0.0, - vmax = 1.0 + vmax = 1.0, + extent = (xmin, xmax, ymax, ymin) ) push!(handles, handle) @@ -755,7 +765,7 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) end if st == :heatmap - x, y, z = heatmap_edges(x), heatmap_edges(y), transpose_z(series, z.surf) + x, y, z = heatmap_edges(x, sp[:xaxis][:scale]), heatmap_edges(y, sp[:yaxis][:scale]), transpose_z(series, z.surf) expand_extrema!(sp[:xaxis], x) expand_extrema!(sp[:yaxis], y) @@ -764,34 +774,30 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) discrete_colorbar_values = dvals end - clims = sp[:clims] - 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, py_mask_nans(z); label = series[:label], zorder = series[:series_plotindex], cmap = py_fillcolormap(series), + alpha = series[:fillalpha], # edgecolors = (series[:linewidth] > 0 ? py_linecolor(series) : "face"), extrakw... ) push!(handles, handle) - needs_colorbar = true end if st == :shape handle = [] - for (i,rng) in enumerate(iter_segments(x, y)) + for (i, rng) in enumerate(iter_segments(series)) if length(rng) > 1 - path = pypath.pymember("Path")(hcat(x[rng], y[rng])) - patches = pypatches.pymember("PathPatch")( + path = pypath["Path"](hcat(x[rng], y[rng])) + patches = pypatches["PathPatch"]( path; label = series[:label], zorder = series[:series_plotindex], - edgecolor = py_color(cycle(series[:linecolor], i)), - facecolor = py_color(cycle(series[:fillcolor], i)), - linewidth = py_dpi_scale(plt, series[:linewidth]), + edgecolor = py_color(get_linecolor(series, i), get_linealpha(series, i)), + facecolor = py_color(get_fillcolor(series, i), get_fillalpha(series, i)), + linewidth = py_thickness_scale(plt, get_linewidth(series, i)), + linestyle = py_linestyle(st, get_linestyle(series, i)), fill = true ) push!(handle, ax[:add_patch](patches)) @@ -820,51 +826,29 @@ function py_add_series(plt::Plot{PyPlotBackend}, series::Series) # # smoothing # handleSmooth(plt, ax, series, series[:smooth]) - # add the colorbar legend - if needs_colorbar && sp[:colorbar] != :none - # add keyword args for a discrete colorbar - handle = handles[end] - kw = KW() - if discrete_colorbar_values != nothing - locator, formatter = get_locator_and_formatter(discrete_colorbar_values) - # kw[:values] = 1:length(discrete_colorbar_values) - kw[:values] = sp[:zaxis][:continuous_values] - kw[:ticks] = locator - kw[:format] = formatter - kw[:boundaries] = vcat(0, kw[:values] + 0.5) - end - - # create and store the colorbar object (handle) and the axis that it is drawn on. - # note: the colorbar axis is positioned independently from the subplot axis - fig = plt.o - cbax = fig[:add_axes]([0.8,0.1,0.03,0.8], label = string(gensym())) - cb = fig[:colorbar](handle; cax = cbax, kw...) - cb[:set_label](sp[:colorbar_title]) - sp.attr[:cbar_handle] = cb - sp.attr[:cbar_ax] = cbax - end - # handle area filling fillrange = series[:fillrange] if fillrange != nothing && st != :contour - f, dim1, dim2 = if isvertical(series) - :fill_between, x, y - else - :fill_betweenx, y, x - end - n = length(dim1) - args = if typeof(fillrange) <: Union{Real, AVec} - dim1, expand_data(fillrange, n), dim2 - elseif is_2tuple(fillrange) - dim1, expand_data(fillrange[1], n), expand_data(fillrange[2], n) - end + for (i, rng) in enumerate(iter_segments(series)) + f, dim1, dim2 = if isvertical(series) + :fill_between, x[rng], y[rng] + else + :fill_betweenx, y[rng], x[rng] + end + n = length(dim1) + args = if typeof(fillrange) <: Union{Real, AVec} + dim1, _cycle(fillrange, rng), dim2 + elseif is_2tuple(fillrange) + dim1, _cycle(fillrange[1], rng), _cycle(fillrange[2], rng) + end - handle = ax[f](args...; - zorder = series[:series_plotindex], - facecolor = py_fillcolor(series), - linewidths = 0 - ) - push!(handles, handle) + handle = ax[f](args..., trues(n), false, py_fillstepstyle(st); + zorder = series[:series_plotindex], + facecolor = py_color(get_fillcolor(series, i), get_fillalpha(series, i)), + linewidths = 0 + ) + push!(handles, handle) + end end # this is all we need to add the series_annotations text @@ -913,14 +897,14 @@ function py_compute_axis_minval(axis::Axis) for series in series_list(sp) v = series.d[axis[:letter]] if !isempty(v) - minval = min(minval, minimum(abs(v))) + minval = NaNMath.min(minval, ignorenan_minimum(abs.(v))) end end end # now if the axis limits go to a smaller abs value, use that instead vmin, vmax = axis_limits(axis) - minval = min(minval, abs(vmin), abs(vmax)) + minval = NaNMath.min(minval, abs(vmin), abs(vmax)) minval end @@ -941,23 +925,24 @@ function py_set_scale(ax, axis::Axis) elseif scale == :log10 10 end - kw[Symbol(:linthresh,letter)] = max(1e-16, py_compute_axis_minval(axis)) + kw[Symbol(:linthresh,letter)] = NaNMath.min(1e-16, py_compute_axis_minval(axis)) "symlog" end func(arg; kw...) end -function py_set_axis_colors(ax, a::Axis) +function py_set_axis_colors(sp, ax, a::Axis) for (loc, spine) in ax[:spines] spine[:set_color](py_color(a[:foreground_color_border])) end axissym = Symbol(a[:letter], :axis) if haskey(ax, axissym) + tickcolor = sp[:framestyle] in (:zerolines, :grid) ? py_color(plot_color(a[:foreground_color_grid], a[:gridalpha])) : py_color(a[:foreground_color_axis]) ax[:tick_params](axis=string(a[:letter]), which="both", - colors=py_color(a[:foreground_color_axis]), - labelcolor=py_color(a[:foreground_color_text])) - ax[axissym][:label][:set_color](py_color(a[:foreground_color_guide])) + colors=tickcolor, + labelcolor=py_color(a[:tickfontcolor])) + ax[axissym][:label][:set_color](py_color(a[:guidefontcolor])) end end @@ -971,9 +956,9 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) fig = plt.o fig[:clear]() dpi = plt[:dpi] - fig[:set_size_inches](w/dpi, h/dpi, forward = true) - fig[:set_facecolor](py_color(plt[:background_color_outside])) - fig[:set_dpi](dpi) + fig[:set_size_inches](w/DPI, h/DPI, forward = true) + fig[set_facecolor_sym](py_color(plt[:background_color_outside])) + fig[:set_dpi](plt[:dpi]) # resize the window PyPlot.plt[:get_current_fig_manager]()[:resize](w, h) @@ -997,7 +982,7 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) # add the annotations for ann in sp[:annotations] - py_add_annotations(sp, ann...) + py_add_annotations(sp, locate_annotation(sp, ann...)...) end # title @@ -1011,12 +996,84 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) :title end ax[func][:set_text](sp[:title]) - ax[func][:set_fontsize](py_dpi_scale(plt, sp[:titlefont].pointsize)) - ax[func][:set_family](sp[:titlefont].family) - ax[func][:set_color](py_color(sp[:foreground_color_title])) + ax[func][:set_fontsize](py_thickness_scale(plt, sp[:titlefontsize])) + ax[func][:set_family](sp[:titlefontfamily]) + ax[func][:set_color](py_color(sp[:titlefontcolor])) # ax[:set_title](sp[:title], loc = loc) end + # add the colorbar legend + if hascolorbar(sp) + # add keyword args for a discrete colorbar + slist = series_list(sp) + colorbar_series = slist[findfirst(hascolorbar.(slist))] + handle = colorbar_series[:serieshandle][end] + kw = KW() + if !isempty(sp[:zaxis][:discrete_values]) && colorbar_series[:seriestype] == :heatmap + locator, formatter = get_locator_and_formatter(sp[:zaxis][:discrete_values]) + # kw[:values] = 1:length(sp[:zaxis][:discrete_values]) + kw[:values] = sp[:zaxis][:continuous_values] + kw[:ticks] = locator + kw[:format] = formatter + kw[:boundaries] = vcat(0, kw[:values] + 0.5) + elseif any(colorbar_series[attr] != nothing for attr in (:line_z, :fill_z, :marker_z)) + cmin, cmax = get_clims(sp) + norm = pycolors[:Normalize](vmin = cmin, vmax = cmax) + f = if colorbar_series[:line_z] != nothing + py_linecolormap + elseif colorbar_series[:fill_z] != nothing + py_fillcolormap + else + py_markercolormap + end + cmap = pycmap[:ScalarMappable](norm = norm, cmap = f(colorbar_series)) + cmap[:set_array]([]) + handle = cmap + end + + # create and store the colorbar object (handle) and the axis that it is drawn on. + # note: the colorbar axis is positioned independently from the subplot axis + fig = plt.o + cbax = fig[:add_axes]([0.8,0.1,0.03,0.8], label = string(gensym())) + cb = fig[:colorbar](handle; cax = cbax, kw...) + cb[:set_label](sp[:colorbar_title],size=py_thickness_scale(plt, sp[:yaxis][:guidefontsize]),family=sp[:yaxis][:guidefontfamily], color = py_color(sp[:yaxis][:guidefontcolor])) + for lab in cb[:ax][:yaxis][:get_ticklabels]() + lab[:set_fontsize](py_thickness_scale(plt, sp[:yaxis][:tickfontsize])) + lab[:set_family](sp[:yaxis][:tickfontfamily]) + lab[:set_color](py_color(sp[:yaxis][:tickfontcolor])) + end + sp.attr[:cbar_handle] = cb + sp.attr[:cbar_ax] = cbax + end + + # framestyle + if !ispolar(sp) && !is3d(sp) + ax[:spines]["left"][:set_linewidth](py_thickness_scale(plt, 1)) + ax[:spines]["bottom"][:set_linewidth](py_thickness_scale(plt, 1)) + if sp[:framestyle] == :semi + intensity = 0.5 + ax[:spines]["right"][:set_alpha](intensity) + ax[:spines]["top"][:set_alpha](intensity) + ax[:spines]["right"][:set_linewidth](py_thickness_scale(plt, intensity)) + ax[:spines]["top"][:set_linewidth](py_thickness_scale(plt, intensity)) + elseif sp[:framestyle] in (:axes, :origin) + ax[:spines]["right"][:set_visible](false) + ax[:spines]["top"][:set_visible](false) + if sp[:framestyle] == :origin + ax[:spines]["bottom"][:set_position]("zero") + ax[:spines]["left"][:set_position]("zero") + end + elseif sp[:framestyle] in (:grid, :none, :zerolines) + for (loc, spine) in ax[:spines] + spine[:set_visible](false) + end + if sp[:framestyle] == :zerolines + ax[:axhline](y = 0, color = py_color(sp[:xaxis][:foreground_color_axis]), lw = py_thickness_scale(plt, 0.75)) + ax[:axvline](x = 0, color = py_color(sp[:yaxis][:foreground_color_axis]), lw = py_thickness_scale(plt, 0.75)) + end + end + end + # axis attributes for letter in (:x, :y, :z) axissym = Symbol(letter, :axis) @@ -1030,25 +1087,64 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) pyaxis[Symbol(:tick_, pos)]() # the tick labels end py_set_scale(ax, axis) - py_set_lims(ax, axis) - py_set_ticks(ax, get_ticks(axis), letter) + axis[:ticks] != :native ? py_set_lims(ax, axis) : nothing + if ispolar(sp) && letter == :y + ax[:set_rlabel_position](90) + end + ticks = sp[:framestyle] == :none ? nothing : get_ticks(axis) + # don't show the 0 tick label for the origin framestyle + if sp[:framestyle] == :origin && length(ticks) > 1 + ticks[2][ticks[1] .== 0] = "" + end + axis[:ticks] != :native ? py_set_ticks(ax, ticks, letter) : nothing + pyaxis[:set_tick_params](direction = axis[:tick_direction] == :out ? "out" : "in") ax[Symbol("set_", letter, "label")](axis[:guide]) if get(axis.d, :flip, false) ax[Symbol("invert_", letter, "axis")]() end - pyaxis[:label][:set_fontsize](py_dpi_scale(plt, axis[:guidefont].pointsize)) - pyaxis[:label][:set_family](axis[:guidefont].family) + pyaxis[:label][:set_fontsize](py_thickness_scale(plt, axis[:guidefontsize])) + pyaxis[:label][:set_family](axis[:guidefontfamily]) for lab in ax[Symbol("get_", letter, "ticklabels")]() - lab[:set_fontsize](py_dpi_scale(plt, axis[:tickfont].pointsize)) - lab[:set_family](axis[:tickfont].family) + lab[:set_fontsize](py_thickness_scale(plt, axis[:tickfontsize])) + lab[:set_family](axis[:tickfontfamily]) lab[:set_rotation](axis[:rotation]) end - if sp[:grid] - fgcolor = py_color(sp[:foreground_color_grid]) - pyaxis[:grid](true, color = fgcolor) + if axis[:grid] && !(ticks in (:none, nothing, false)) + fgcolor = py_color(axis[:foreground_color_grid]) + pyaxis[:grid](true, + color = fgcolor, + linestyle = py_linestyle(:line, axis[:gridstyle]), + linewidth = py_thickness_scale(plt, axis[:gridlinewidth]), + alpha = axis[:gridalpha]) ax[:set_axisbelow](true) + else + pyaxis[:grid](false) end - py_set_axis_colors(ax, axis) + py_set_axis_colors(sp, ax, axis) + end + + # showaxis + if !sp[:xaxis][:showaxis] + kw = KW() + for dir in (:top, :bottom) + if ispolar(sp) + ax[:spines]["polar"][:set_visible](false) + else + ax[:spines][string(dir)][:set_visible](false) + end + kw[dir] = kw[Symbol(:label,dir)] = "off" + end + ax[:xaxis][:set_tick_params](; which="both", kw...) + end + if !sp[:yaxis][:showaxis] + kw = KW() + for dir in (:left, :right) + if !ispolar(sp) + ax[:spines][string(dir)][:set_visible](false) + end + kw[dir] = kw[Symbol(:label,dir)] = "off" + end + ax[:yaxis][:set_tick_params](; which="both", kw...) end # aspect ratio @@ -1057,11 +1153,23 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend}) ax[:set_aspect](isa(aratio, Symbol) ? string(aratio) : aratio, anchor = "C") end + #camera/view angle + if is3d(sp) + #convert azimuthal to match GR behaviour + #view_init(elevation, azimuthal) so reverse :camera args + ax[:view_init]((sp[:camera].-(90,0))[end:-1:1]...) + end + # legend py_add_legend(plt, sp, ax) # this sets the bg color inside the grid - ax[:set_axis_bgcolor](py_color(sp[:background_color_inside])) + ax[set_facecolor_sym](py_color(sp[:background_color_inside])) + + # link axes + x_ax_link, y_ax_link = sp[:xaxis].sps[1].o, sp[:yaxis].sps[1].o + ax != x_ax_link && ax[:get_shared_x_axes]()[:join](ax, sp[:xaxis].sps[1].o) + ax != y_ax_link && ax[:get_shared_y_axes]()[:join](ax, sp[:yaxis].sps[1].o) end py_drawfig(fig) end @@ -1093,7 +1201,7 @@ function _update_min_padding!(sp::Subplot{PyPlotBackend}) # optionally add the width of colorbar labels and colorbar to rightpad if haskey(sp.attr, :cbar_ax) bb = py_bbox(sp.attr[:cbar_handle][:ax][:get_yticklabels]()) - sp.attr[:cbar_width] = _cbar_width + width(bb) + 1mm + (sp[:colorbar_title] == "" ? 0px : 30px) + sp.attr[:cbar_width] = _cbar_width + width(bb) + 2.3mm + (sp[:colorbar_title] == "" ? 0px : 30px) rightpad = rightpad + sp.attr[:cbar_width] end @@ -1103,7 +1211,9 @@ function _update_min_padding!(sp::Subplot{PyPlotBackend}) rightpad += sp[:right_margin] bottompad += sp[:bottom_margin] - sp.minpad = (leftpad, toppad, rightpad, bottompad) + dpi_factor = Plots.DPI / sp.plt[:dpi] + + sp.minpad = Tuple(dpi_factor .* [leftpad, toppad, rightpad, bottompad]) end @@ -1124,7 +1234,7 @@ function py_add_annotations(sp::Subplot{PyPlotBackend}, x, y, val::PlotText) horizontalalignment = val.font.halign == :hcenter ? "center" : string(val.font.halign), verticalalignment = val.font.valign == :vcenter ? "center" : string(val.font.valign), rotation = val.font.rotation * 180 / π, - size = py_dpi_scale(sp.plt, val.font.pointsize), + size = py_thickness_scale(sp.plt, val.font.pointsize), zorder = 999 ) end @@ -1151,10 +1261,21 @@ function py_add_legend(plt::Plot, sp::Subplot, ax) for series in series_list(sp) if should_add_to_legend(series) # add a line/marker and a label - push!(handles, if series[:seriestype] == :shape + push!(handles, if series[:seriestype] == :shape || series[:fillrange] != nothing + pypatches[:Patch]( + edgecolor = py_color(get_linecolor(series), get_linealpha(series)), + facecolor = py_color(get_fillcolor(series), get_fillalpha(series)), + linewidth = py_thickness_scale(plt, clamp(get_linewidth(series), 0, 5)), + linestyle = py_linestyle(series[:seriestype], get_linestyle(series)) + ) + elseif series[:seriestype] in (:path, :straightline) PyPlot.plt[:Line2D]((0,1),(0,0), - color = py_color(cycle(series[:fillcolor],1)), - linewidth = py_dpi_scale(plt, 4) + color = py_color(get_linecolor(series), get_linealpha(series)), + linewidth = py_thickness_scale(plt, clamp(get_linewidth(series), 0, 5)), + linestyle = py_linestyle(:path, get_linestyle(series)), + marker = py_marker(series[:markershape]), + markeredgecolor = py_color(get_markerstrokecolor(series), get_markerstrokealpha(series)), + markerfacecolor = series[:marker_z] == nothing ? py_color(get_markercolor(series), get_markeralpha(series)) : py_color(series[:markercolor][0.5]) ) else series[:serieshandle][1] @@ -1169,21 +1290,19 @@ function py_add_legend(plt::Plot, sp::Subplot, ax) labels, loc = get(_pyplot_legend_pos, leg, "best"), scatterpoints = 1, - fontsize = py_dpi_scale(plt, sp[:legendfont].pointsize) - # family = sp[:legendfont].family - # framealpha = 0.6 + fontsize = py_thickness_scale(plt, sp[:legendfontsize]), + facecolor = py_color(sp[:background_color_legend]), + edgecolor = py_color(sp[:foreground_color_legend]), + framealpha = alpha(plot_color(sp[:background_color_legend])), ) - leg[:set_zorder](1000) - - fgcolor = py_color(sp[:foreground_color_legend]) - for txt in leg[:get_texts]() - PyPlot.plt[:setp](txt, color = fgcolor, family = sp[:legendfont].family) - end - - # set some legend properties frame = leg[:get_frame]() - frame[:set_facecolor](py_color(sp[:background_color_legend])) - frame[:set_edgecolor](fgcolor) + frame[:set_linewidth](py_thickness_scale(plt, 1)) + leg[:set_zorder](1000) + sp[:legendtitle] != nothing && leg[:set_title](sp[:legendtitle]) + + for txt in leg[:get_texts]() + PyPlot.plt[:setp](txt, color = py_color(sp[:legendfontcolor]), family = sp[:legendfontfamily]) + end end end end @@ -1206,7 +1325,9 @@ function _update_plot_object(plt::Plot{PyPlotBackend}) if haskey(sp.attr, :cbar_ax) cbw = sp.attr[:cbar_width] # this is the bounding box of just the colors of the colorbar (not labels) - cb_bbox = BoundingBox(right(sp.bbox)-cbw+1mm, top(sp.bbox)+2mm, _cbar_width-1mm, height(sp.bbox)-4mm) + ex = sp[:zaxis][:extrema] + has_toplabel = !(1e-7 < max(abs(ex.emax), abs(ex.emin)) < 1e7) + cb_bbox = BoundingBox(right(sp.bbox)-cbw+1mm, top(sp.bbox) + (has_toplabel ? 4mm : 2mm), _cbar_width-1mm, height(sp.bbox) - (has_toplabel ? 6mm : 4mm)) pcts = bbox_to_pcts(cb_bbox, figw, figh) sp.attr[:cbar_ax][:set_position](pcts) end diff --git a/src/backends/unicodeplots.jl b/src/backends/unicodeplots.jl index f5e34834..f644966d 100644 --- a/src/backends/unicodeplots.jl +++ b/src/backends/unicodeplots.jl @@ -1,6 +1,10 @@ # https://github.com/Evizero/UnicodePlots.jl +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "unicodeplots.jl")) +end + const _unicodeplots_attr = merge_with_base_supported([ :label, :legend, @@ -13,7 +17,7 @@ const _unicodeplots_attr = merge_with_base_supported([ :guide, :lims, ]) const _unicodeplots_seriestype = [ - :path, :scatter, + :path, :scatter, :straightline, # :bar, :shape, :histogram2d, @@ -138,7 +142,7 @@ function addUnicodeSeries!(o, d::KW, addlegend::Bool, xlim, ylim) return end - if st == :path + if st in (:path, :straightline) func = UnicodePlots.lineplot! elseif st == :scatter || d[:markershape] != :none func = UnicodePlots.scatterplot! @@ -151,14 +155,20 @@ function addUnicodeSeries!(o, d::KW, addlegend::Bool, xlim, ylim) end # get the series data and label - x, y = [collect(float(d[s])) for s in (:x, :y)] + x, y = if st == :straightline + straightline_data(d) + elseif st == :shape + shape_data(series) + else + [collect(float(d[s])) for s in (:x, :y)] + end label = addlegend ? d[:label] : "" # if we happen to pass in allowed color symbols, great... otherwise let UnicodePlots decide color = d[:linecolor] in UnicodePlots.color_cycle ? d[:linecolor] : :auto # add the series - x, y = Plots.unzip(collect(filter(xy->isfinite(xy[1])&&isfinite(xy[2]), zip(x,y)))) + x, y = Plots.unzip(collect(Base.Iterators.filter(xy->isfinite(xy[1])&&isfinite(xy[2]), zip(x,y)))) func(o, x, y; color = color, name = label) end @@ -202,7 +212,7 @@ end function _show(io::IO, ::MIME"text/plain", plt::Plot{UnicodePlotsBackend}) unicodeplots_rebuild(plt) - map(show, plt.o) + foreach(x -> show(io, x), plt.o) nothing end diff --git a/src/backends/web.jl b/src/backends/web.jl index 2fd4ae6e..e38ad782 100644 --- a/src/backends/web.jl +++ b/src/backends/web.jl @@ -2,7 +2,9 @@ # NOTE: backend should implement `html_body` and `html_head` # CREDIT: parts of this implementation were inspired by @joshday's PlotlyLocal.jl - +@require Revise begin + Revise.track(Plots, joinpath(Pkg.dir("Plots"), "src", "backends", "web.jl")) +end function standalone_html(plt::AbstractPlot; title::AbstractString = get(plt.attr, :window_title, "Plots.jl")) """ @@ -10,6 +12,7 @@ function standalone_html(plt::AbstractPlot; title::AbstractString = get(plt.attr $title + $(html_head(plt)) @@ -23,11 +26,11 @@ function open_browser_window(filename::AbstractString) @static if is_apple() return run(`open $(filename)`) end - @static if is_linux() + @static if is_linux() || is_bsd() # is_bsd() addition is as yet untested, but based on suggestion in https://github.com/JuliaPlots/Plots.jl/issues/681 return run(`xdg-open $(filename)`) end @static if is_windows() - return run(`$(ENV["COMSPEC"]) /c start $(filename)`) + return run(`$(ENV["COMSPEC"]) /c start "" "$(filename)"`) end warn("Unknown OS... cannot open browser window.") end diff --git a/src/components.jl b/src/components.jl index bbb2f9c2..2b1284ce 100644 --- a/src/components.jl +++ b/src/components.jl @@ -1,7 +1,7 @@ -typealias P2 FixedSizeArrays.Vec{2,Float64} -typealias P3 FixedSizeArrays.Vec{3,Float64} +const P2 = FixedSizeArrays.Vec{2,Float64} +const P3 = FixedSizeArrays.Vec{3,Float64} nanpush!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); push!(a, b)) nanappend!(a::AbstractVector{P2}, b) = (push!(a, P2(NaN,NaN)); append!(a, b)) @@ -11,7 +11,7 @@ compute_angle(v::P2) = (angle = atan2(v[2], v[1]); angle < 0 ? 2π - angle : ang # ------------------------------------------------------------- -immutable Shape +struct Shape x::Vector{Float64} y::Vector{Float64} # function Shape(x::AVec, y::AVec) @@ -22,6 +22,13 @@ immutable Shape # end # end end + +""" + Shape(x, y) + Shape(vertices) + +Construct a polygon to be plotted +""" Shape(verts::AVec) = Shape(unzip(verts)...) Shape(s::Shape) = deepcopy(s) @@ -32,6 +39,7 @@ vertices(shape::Shape) = collect(zip(shape.x, shape.y)) #deprecated @deprecate shape_coords coords +"return the vertex points from a Shape or Segments object" function coords(shape::Shape) shape.x, shape.y end @@ -156,6 +164,7 @@ Shape(k::Symbol) = deepcopy(_shapes[k]) # uses the centroid calculation from https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon +"return the centroid of a Shape" function center(shape::Shape) x, y = coords(shape) n = length(x) @@ -174,7 +183,7 @@ function center(shape::Shape) Cx / 6A, Cy / 6A end -function Base.scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) +function scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) sx, sy = coords(shape) cx, cy = c for i=1:length(sx) @@ -184,11 +193,12 @@ function Base.scale!(shape::Shape, x::Real, y::Real = x, c = center(shape)) shape end -function Base.scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) +function scale(shape::Shape, x::Real, y::Real = x, c = center(shape)) shapecopy = deepcopy(shape) scale!(shapecopy, x, y, c) end +"translate a Shape in space" function translate!(shape::Shape, x::Real, y::Real = x) sx, sy = coords(shape) for i=1:length(sx) @@ -227,6 +237,7 @@ function rotate!(shape::Shape, Θ::Real, c = center(shape)) shape end +"rotate an object in space" function rotate(shape::Shape, Θ::Real, c = center(shape)) shapecopy = deepcopy(shape) rotate!(shapecopy, Θ, c) @@ -235,7 +246,7 @@ end # ----------------------------------------------------------------------- -type Font +mutable struct Font family::AbstractString pointsize::Int halign::Symbol @@ -294,23 +305,50 @@ end function scalefontsize(k::Symbol, factor::Number) f = default(k) - f.pointsize = round(Int, factor * f.pointsize) + f = round(Int, factor * f) default(k, f) end + +""" + scalefontsizes(factor::Number) + +Scales all **current** font sizes by `factor`. For example `scalefontsizes(1.1)` increases all current font sizes by 10%. To reset to initial sizes, use `scalefontsizes()` +""" function scalefontsizes(factor::Number) - for k in (:titlefont, :guidefont, :tickfont, :legendfont) + for k in (:titlefontsize, :guidefontsize, :tickfontsize, :legendfontsize) scalefontsize(k, factor) end end +""" + scalefontsizes() + +Resets font sizes to initial default values. +""" +function scalefontsizes() + for k in (:titlefontsize, :guidefontsize, :tickfontsize, :legendfontsize) + f = default(k) + if k in keys(_initial_fontsizes) + factor = f / _initial_fontsizes[k] + scalefontsize(k, 1.0/factor) + end + end +end + "Wrap a string with font info" -immutable PlotText +struct PlotText str::AbstractString font::Font end PlotText(str) = PlotText(string(str), font()) +""" + text(string, args...) + +Create a PlotText object wrapping a string with font info, for plot annotations +""" text(t::PlotText) = t +text(t::PlotText, font::Font) = PlotText(t.str, font) text(str::AbstractString, f::Font) = PlotText(str, f) function text(str, args...) PlotText(string(str), font(args...)) @@ -322,13 +360,18 @@ Base.length(t::PlotText) = length(t.str) # ----------------------------------------------------------------------- -immutable Stroke +struct Stroke width color alpha style end +""" + stroke(args...; alpha = nothing) + +Define the properties of the stroke used in plotting lines +""" function stroke(args...; alpha = nothing) width = 1 color = :black @@ -359,7 +402,7 @@ function stroke(args...; alpha = nothing) end -immutable Brush +struct Brush size # fillrange, markersize, or any other sizey attribute color alpha @@ -392,7 +435,7 @@ end # ----------------------------------------------------------------------- -type SeriesAnnotations +mutable struct SeriesAnnotations strs::AbstractVector # the labels/names font::Font baseshape::Nullable @@ -446,7 +489,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) msw,msh = anns.scalefactor msize = Float64[] shapes = Shape[begin - str = cycle(anns.strs,i) + str = _cycle(anns.strs,i) # get the width and height of the string (in mm) sw, sh = text_size(str, anns.font.pointsize) @@ -462,7 +505,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) # 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) + 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 @@ -471,7 +514,7 @@ function series_annotations_shapes!(series::Series, scaletype::Symbol = :pixels) return end -type EachAnn +mutable struct EachAnn anns x y @@ -479,13 +522,13 @@ 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) + 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) + ((_cycle(ea.x,i), _cycle(ea.y,i), str, fnt), i+1) end annotations(::Void) = [] @@ -493,24 +536,72 @@ annotations(anns::AVec) = anns annotations(anns) = Any[anns] annotations(sa::SeriesAnnotations) = sa +# Expand arrays of coordinates, positions and labels into induvidual annotations +# and make sure labels are of type PlotText +function process_annotation(sp::Subplot, xs, ys, labs, font = font()) + anns = [] + labs = makevec(labs) + for i in 1:max(length(xs), length(ys), length(labs)) + x, y, lab = _cycle(xs, i), _cycle(ys, i), _cycle(labs, i) + if lab == :auto + alphabet = "abcdefghijklmnopqrstuvwxyz" + push!(anns, (x, y, text(string("(", alphabet[sp[:subplot_index]], ")"), font))) + else + push!(anns, (x, y, isa(lab, PlotText) ? lab : text(lab, font))) + end + end + anns +end +function process_annotation(sp::Subplot, positions::Union{AVec{Symbol},Symbol}, labs, font = font()) + anns = [] + positions, labs = makevec(positions), makevec(labs) + for i in 1:max(length(positions), length(labs)) + pos, lab = _cycle(positions, i), _cycle(labs, i) + pos = get(_positionAliases, pos, pos) + if lab == :auto + alphabet = "abcdefghijklmnopqrstuvwxyz" + push!(anns, (pos, text(string("(", alphabet[sp[:subplot_index]], ")"), font))) + else + push!(anns, (pos, isa(lab, PlotText) ? lab : text(lab, font))) + end + end + anns +end + +# Give each annotation coordinates based on specified position +function locate_annotation(sp::Subplot, pos::Symbol, lab::PlotText) + position_multiplier = Dict{Symbol, Tuple{Float64,Float64}}( + :topleft => (0.1, 0.9), + :topcenter => (0.5, 0.9), + :topright => (0.9, 0.9), + :bottomleft => (0.1, 0.1), + :bottomcenter => (0.5, 0.1), + :bottomright => (0.9, 0.1), + ) + xmin, xmax = ignorenan_extrema(sp[:xaxis]) + ymin, ymax = ignorenan_extrema(sp[:yaxis]) + x, y = (xmin, ymin).+ position_multiplier[pos].* (xmax - xmin, ymax - ymin) + (x, y, lab) +end +locate_annotation(sp::Subplot, x, y, label::PlotText) = (x, y, label) # ----------------------------------------------------------------------- "type which represents z-values for colors and sizes (and anything else that might come up)" -immutable ZValues +struct ZValues values::Vector{Float64} zrange::Tuple{Float64,Float64} end -function zvalues{T<:Real}(values::AVec{T}, zrange::Tuple{T,T} = (minimum(values), maximum(values))) +function zvalues(values::AVec{T}, zrange::Tuple{T,T} = (ignorenan_minimum(values), ignorenan_maximum(values))) where T<:Real ZValues(collect(float(values)), map(Float64, zrange)) end # ----------------------------------------------------------------------- -abstract AbstractSurface +abstract type AbstractSurface end "represents a contour or surface mesh" -immutable Surface{M<:AMat} <: AbstractSurface +struct Surface{M<:AMat} <: AbstractSurface surf::M end @@ -521,8 +612,8 @@ Base.Array(surf::Surface) = surf.surf for f in (:length, :size) @eval Base.$f(surf::Surface, args...) = $f(surf.surf, args...) end -Base.copy(surf::Surface) = Surface{typeof(surf.surf)}(copy(surf.surf)) -Base.eltype{T}(surf::Surface{T}) = eltype(T) +Base.copy(surf::Surface) = Surface(copy(surf.surf)) +Base.eltype(surf::Surface{T}) where {T} = eltype(T) function expand_extrema!(a::Axis, surf::Surface) ex = a[:extrema] @@ -533,7 +624,7 @@ function expand_extrema!(a::Axis, surf::Surface) end "For the case of representing a surface as a function of x/y... can possibly avoid allocations." -immutable SurfaceFunction <: AbstractSurface +struct SurfaceFunction <: AbstractSurface f::Function end @@ -543,19 +634,19 @@ end # # I don't want to clash with ValidatedNumerics, but this would be nice: # ..(a::T, b::T) = (a,b) -immutable Volume{T} +struct Volume{T} v::Array{T,3} x_extents::Tuple{T,T} y_extents::Tuple{T,T} z_extents::Tuple{T,T} end -default_extents{T}(::Type{T}) = (zero(T), one(T)) +default_extents(::Type{T}) where {T} = (zero(T), one(T)) -function Volume{T}(v::Array{T,3}, - x_extents = default_extents(T), - y_extents = default_extents(T), - z_extents = default_extents(T)) +function Volume(v::Array{T,3}, + x_extents = default_extents(T), + y_extents = default_extents(T), + z_extents = default_extents(T)) where T Volume(v, x_extents, y_extents, z_extents) end @@ -563,19 +654,25 @@ Base.Array(vol::Volume) = vol.v for f in (:length, :size) @eval Base.$f(vol::Volume, args...) = $f(vol.v, args...) end -Base.copy{T}(vol::Volume{T}) = Volume{T}(copy(vol.v), vol.x_extents, vol.y_extents, vol.z_extents) -Base.eltype{T}(vol::Volume{T}) = T +Base.copy(vol::Volume{T}) where {T} = Volume{T}(copy(vol.v), vol.x_extents, vol.y_extents, vol.z_extents) +Base.eltype(vol::Volume{T}) where {T} = T # ----------------------------------------------------------------------- # style is :open or :closed (for now) -immutable Arrow +struct Arrow style::Symbol side::Symbol # :head (default), :tail, or :both headlength::Float64 headwidth::Float64 end +""" + arrow(args...) + +Define arrowheads to apply to lines - args are `style` (`:open` or `:closed`), +`side` (`:head`, `:tail` or `:both`), `headlength` and `headwidth` +""" function arrow(args...) style = :simple side = :head @@ -625,14 +722,14 @@ end # ----------------------------------------------------------------------- "Represents data values with formatting that should apply to the tick labels." -immutable Formatted{T} +struct Formatted{T} data::T formatter::Function end # ----------------------------------------------------------------------- - -type BezierCurve{T <: FixedSizeArrays.Vec} +"create a BezierCurve for plotting" +mutable struct BezierCurve{T <: FixedSizeArrays.Vec} control_points::Vector{T} end @@ -645,8 +742,8 @@ function (bc::BezierCurve)(t::Real) 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) +# mean(x::Real, y::Real) = 0.5*(x+y) #commented out as I cannot see this used anywhere and it overwrites a Base method with different functionality +# mean{N,T<:Real}(ps::FixedSizeArrays.Vec{N,T}...) = sum(ps) / length(ps) # I also could not see this used anywhere, and it's type piracy - implementing a NaNMath version for this would just involve converting to a standard array @deprecate curve_points coords @@ -659,7 +756,7 @@ function directed_curve(args...; kw...) end function extrema_plus_buffer(v, buffmult = 0.2) - vmin,vmax = extrema(v) + vmin,vmax = ignorenan_extrema(v) vdiff = vmax-vmin buffer = vdiff * buffmult vmin - buffer, vmax + buffer diff --git a/src/deprecated/backends/gadfly.jl b/src/deprecated/backends/gadfly.jl index bb501db2..53453b9e 100644 --- a/src/deprecated/backends/gadfly.jl +++ b/src/deprecated/backends/gadfly.jl @@ -558,7 +558,7 @@ function createGadflyAnnotationObject(x, y, txt::PlotText) )) end -function _add_annotations{X,Y,V}(plt::Plot{GadflyBackend}, anns::AVec{Tuple{X,Y,V}}) +function _add_annotations(plt::Plot{GadflyBackend}, anns::AVec{Tuple{X,Y,V}}) where {X,Y,V} for ann in anns push!(plt.o.guides, createGadflyAnnotationObject(ann...)) end @@ -614,7 +614,7 @@ function getxy(plt::Plot{GadflyBackend}, i::Integer) mapping[:x], mapping[:y] end -function setxy!{X,Y}(plt::Plot{GadflyBackend}, xy::Tuple{X,Y}, i::Integer) +function setxy!(plt::Plot{GadflyBackend}, xy::Tuple{X,Y}, i::Integer) where {X,Y} for mapping in getGadflyMappings(plt, i) mapping[:x], mapping[:y] = xy end @@ -677,7 +677,7 @@ setGadflyDisplaySize(plt::Plot) = setGadflyDisplaySize(plt.attr[:size]...) # ------------------------------------------------------------------------- -function doshow{P<:Union{GadflyBackend,ImmerseBackend}}(io::IO, func, plt::AbstractPlot{P}) +function doshow(io::IO, func, plt::AbstractPlot{P}) where P<:Union{GadflyBackend,ImmerseBackend} gplt = getGadflyContext(plt) setGadflyDisplaySize(plt) Gadfly.draw(func(io, Compose.default_graphic_width, Compose.default_graphic_height), gplt) @@ -692,7 +692,7 @@ getGadflyWriteFunc(::MIME"application/x-tex") = Gadfly.PGF getGadflyWriteFunc(m::MIME) = error("Unsupported in Gadfly/Immerse: ", m) for mime in (MIME"image/png", MIME"image/svg+xml", MIME"application/pdf", MIME"application/postscript", MIME"application/x-tex") - @eval function Base.show{P<:Union{GadflyBackend,ImmerseBackend}}(io::IO, ::$mime, plt::AbstractPlot{P}) + @eval function Base.show(io::IO, ::$mime, plt::AbstractPlot{P}) where P<:Union{GadflyBackend,ImmerseBackend} func = getGadflyWriteFunc($mime()) doshow(io, func, plt) end diff --git a/src/deprecated/backends/gadfly_shapes.jl b/src/deprecated/backends/gadfly_shapes.jl index 8ee1b9ef..1a818993 100644 --- a/src/deprecated/backends/gadfly_shapes.jl +++ b/src/deprecated/backends/gadfly_shapes.jl @@ -2,7 +2,7 @@ # Geometry which displays arbitrary shapes at given (x, y) positions. # note: vertices is a list of shapes -immutable ShapeGeometry <: Gadfly.GeometryElement +struct ShapeGeometry <: Gadfly.GeometryElement vertices::AbstractVector #{Tuple{Float64,Float64}} tag::Symbol @@ -84,7 +84,7 @@ function make_polygon(geom::ShapeGeometry, xs::AbstractArray, ys::AbstractArray, x = Compose.x_measure(xs[mod1(i, length(xs))]) y = Compose.y_measure(ys[mod1(i, length(ys))]) r = rs[mod1(i, length(rs))] - polys[i] = T[(x + r * sx, y + r * sy) for (sx,sy) in cycle(geom.vertices, i)] + polys[i] = T[(x + r * sx, y + r * sy) for (sx,sy) in _cycle(geom.vertices, i)] end Gadfly.polygon(polys, geom.tag) end diff --git a/src/deprecated/backends/immerse.jl b/src/deprecated/backends/immerse.jl index b8679cf3..58950480 100644 --- a/src/deprecated/backends/immerse.jl +++ b/src/deprecated/backends/immerse.jl @@ -61,7 +61,7 @@ end # ---------------------------------------------------------------- -function _add_annotations{X,Y,V}(plt::Plot{ImmerseBackend}, anns::AVec{Tuple{X,Y,V}}) +function _add_annotations(plt::Plot{ImmerseBackend}, anns::AVec{Tuple{X,Y,V}}) where {X,Y,V} for ann in anns push!(getGadflyContext(plt).guides, createGadflyAnnotationObject(ann...)) end @@ -76,7 +76,7 @@ function getxy(plt::Plot{ImmerseBackend}, i::Integer) mapping[:x], mapping[:y] end -function setxy!{X,Y}(plt::Plot{ImmerseBackend}, xy::Tuple{X,Y}, i::Integer) +function setxy!(plt::Plot{ImmerseBackend}, xy::Tuple{X,Y}, i::Integer) where {X,Y} for mapping in getGadflyMappings(plt, i) mapping[:x], mapping[:y] = xy end diff --git a/src/deprecated/backends/qwt.jl b/src/deprecated/backends/qwt.jl index 2173c599..6bffd322 100644 --- a/src/deprecated/backends/qwt.jl +++ b/src/deprecated/backends/qwt.jl @@ -218,7 +218,7 @@ function createQwtAnnotation(plt::Plot, x, y, val::AbstractString) marker[:attach](plt.o.widget) end -function _add_annotations{X,Y,V}(plt::Plot{QwtBackend}, anns::AVec{Tuple{X,Y,V}}) +function _add_annotations(plt::Plot{QwtBackend}, anns::AVec{Tuple{X,Y,V}}) where {X,Y,V} for ann in anns createQwtAnnotation(plt, ann...) end @@ -233,7 +233,7 @@ function getxy(plt::Plot{QwtBackend}, i::Int) series.x, series.y end -function setxy!{X,Y}(plt::Plot{QwtBackend}, xy::Tuple{X,Y}, i::Integer) +function setxy!(plt::Plot{QwtBackend}, xy::Tuple{X,Y}, i::Integer) where {X,Y} series = plt.o.lines[i] series.x, series.y = xy plt diff --git a/src/deprecated/backends/winston.jl b/src/deprecated/backends/winston.jl index 8e7b97c7..22694be8 100644 --- a/src/deprecated/backends/winston.jl +++ b/src/deprecated/backends/winston.jl @@ -217,7 +217,7 @@ function createWinstonAnnotationObject(plt::Plot{WinstonBackend}, x, y, val::Abs Winston.text(x, y, val) end -function _add_annotations{X,Y,V}(plt::Plot{WinstonBackend}, anns::AVec{Tuple{X,Y,V}}) +function _add_annotations(plt::Plot{WinstonBackend}, anns::AVec{Tuple{X,Y,V}}) where {X,Y,V} for ann in anns createWinstonAnnotationObject(plt, ann...) end diff --git a/src/deprecated/colors.jl b/src/deprecated/colors.jl index ebc75222..8247529a 100644 --- a/src/deprecated/colors.jl +++ b/src/deprecated/colors.jl @@ -1,5 +1,5 @@ -abstract ColorScheme +abstract type ColorScheme end Base.getindex(scheme::ColorScheme, i::Integer) = getColor(scheme, i) @@ -34,9 +34,9 @@ getColorVector(scheme::ColorScheme) = [getColor(scheme)] colorscheme(scheme::ColorScheme) = scheme colorscheme(s::AbstractString; kw...) = colorscheme(Symbol(s); kw...) colorscheme(s::Symbol; kw...) = haskey(_gradients, s) ? ColorGradient(s; kw...) : ColorWrapper(convertColor(s); kw...) -colorscheme{T<:Real}(s::Symbol, vals::AVec{T}; kw...) = ColorGradient(s, vals; kw...) +colorscheme(s::Symbol, vals::AVec{T}; kw...) where {T<:Real} = ColorGradient(s, vals; kw...) colorscheme(cs::AVec, vs::AVec; kw...) = ColorGradient(cs, vs; kw...) -colorscheme{T<:Colorant}(cs::AVec{T}; kw...) = ColorGradient(cs; kw...) +colorscheme(cs::AVec{T}; kw...) where {T<:Colorant} = ColorGradient(cs; kw...) colorscheme(f::Function; kw...) = ColorFunction(f; kw...) colorscheme(v::AVec; kw...) = ColorVector(v; kw...) colorscheme(m::AMat; kw...) = size(m,1) == 1 ? map(c->colorscheme(c; kw...), m) : [colorscheme(m[:,i]; kw...) for i in 1:size(m,2)]' @@ -98,7 +98,7 @@ const _gradients = KW( :lighttest => map(c -> lighten(c, 0.3), _testColors), ) -function register_gradient_colors{C<:Colorant}(name::Symbol, colors::AVec{C}) +function register_gradient_colors(name::Symbol, colors::AVec{C}) where C<:Colorant _gradients[name] = colors end @@ -109,11 +109,11 @@ default_gradient() = ColorGradient(:inferno) # -------------------------------------------------------------- "Continuous gradient between values. Wraps a list of bounding colors and the values they represent." -immutable ColorGradient <: ColorScheme +struct ColorGradient <: ColorScheme colors::Vector values::Vector - function ColorGradient{S<:Real}(cs::AVec, vals::AVec{S} = linspace(0, 1, length(cs)); alpha = nothing) + function ColorGradient(cs::AVec, vals::AVec{S} = linspace(0, 1, length(cs)); alpha = nothing) where S<:Real if length(cs) == length(vals) return new(convertColor(cs,alpha), collect(vals)) end @@ -138,7 +138,7 @@ Base.getindex(cs::ColorGradient, z::Number) = getColorZ(cs, z) # create a gradient from a symbol (blues, reds, etc) and vector of boundary values -function ColorGradient{T<:Real}(s::Symbol, vals::AVec{T} = 0:0; kw...) +function ColorGradient(s::Symbol, vals::AVec{T} = 0:0; kw...) where T<:Real haskey(_gradients, s) || error("Invalid gradient symbol. Choose from: ", sort(collect(keys(_gradients)))) cs = _gradients[s] if vals == 0:0 @@ -208,7 +208,7 @@ end # -------------------------------------------------------------- "Wraps a function, taking an index and returning a Colorant" -immutable ColorFunction <: ColorScheme +struct ColorFunction <: ColorScheme f::Function end @@ -217,7 +217,7 @@ getColor(scheme::ColorFunction, idx::Int) = scheme.f(idx) # -------------------------------------------------------------- "Wraps a function, taking an z-value and returning a Colorant" -immutable ColorZFunction <: ColorScheme +struct ColorZFunction <: ColorScheme f::Function end @@ -226,7 +226,7 @@ getColorZ(scheme::ColorZFunction, z::Real) = scheme.f(z) # -------------------------------------------------------------- "Wraps a vector of colors... may be vector of Symbol/String/Colorant" -immutable ColorVector <: ColorScheme +struct ColorVector <: ColorScheme v::Vector{Colorant} ColorVector(v::AVec; alpha = nothing) = new(convertColor(v,alpha)) end @@ -238,7 +238,7 @@ getColorVector(scheme::ColorVector) = scheme.v # -------------------------------------------------------------- "Wraps a single color" -immutable ColorWrapper <: ColorScheme +struct ColorWrapper <: ColorScheme c::RGBA ColorWrapper(c::Colorant; alpha = nothing) = new(convertColor(c, alpha)) end @@ -347,8 +347,8 @@ function get_color_palette(palette, bgcolor::Union{Colorant,ColorWrapper}, numco RGBA[getColorZ(grad, z) for z in zrng] end -function get_color_palette{C<:Colorant}(palette::Vector{C}, - bgcolor::Union{Colorant,ColorWrapper}, numcolors::Integer) +function get_color_palette(palette::Vector{C}, +bgcolor::Union{Colorant,ColorWrapper}, numcolors::Integer) where C<:Colorant palette end diff --git a/src/deprecated/series_args.jl b/src/deprecated/series_args.jl index 7d491290..b5b02594 100644 --- a/src/deprecated/series_args.jl +++ b/src/deprecated/series_args.jl @@ -5,21 +5,21 @@ # This should cut down on boilerplate code and allow more focused dispatch on type # note: returns meta information... mainly for use with automatic labeling from DataFrames for now -typealias FuncOrFuncs @compat(Union{Function, AVec{Function}}) +const FuncOrFuncs = Union{Function, AVec{Function}} all3D(d::KW) = trueOrAllTrue(st -> st in (:contour, :contourf, :heatmap, :surface, :wireframe, :contour3d, :image), get(d, :seriestype, :none)) # missing -convertToAnyVector(v::@compat(Void), d::KW) = Any[nothing], nothing +convertToAnyVector(v::Void, d::KW) = Any[nothing], nothing # fixed number of blank series convertToAnyVector(n::Integer, d::KW) = Any[zeros(0) for i in 1:n], nothing # numeric vector -convertToAnyVector{T<:Number}(v::AVec{T}, d::KW) = Any[v], nothing +convertToAnyVector(v::AVec{T}, d::KW) where {T<:Number} = Any[v], nothing # string vector -convertToAnyVector{T<:@compat(AbstractString)}(v::AVec{T}, d::KW) = Any[v], nothing +convertToAnyVector(v::AVec{T}, d::KW) where {T<:AbstractString} = Any[v], nothing function convertToAnyVector(v::AMat, d::KW) if all3D(d) @@ -39,7 +39,7 @@ convertToAnyVector(s::Surface, d::KW) = Any[s], nothing # convertToAnyVector(v::AVec{OHLC}, d::KW) = Any[v], nothing # dates -convertToAnyVector{D<:Union{Date,DateTime}}(dts::AVec{D}, d::KW) = Any[dts], nothing +convertToAnyVector(dts::AVec{D}, d::KW) where {D<:Union{Date,DateTime}} = Any[dts], nothing # list of things (maybe other vectors, functions, or something else) function convertToAnyVector(v::AVec, d::KW) diff --git a/src/examples.jl b/src/examples.jl index 96872851..57ed7b3e 100644 --- a/src/examples.jl +++ b/src/examples.jl @@ -1,7 +1,7 @@ """ Holds all data needed for a documentation example... header, description, and plotting expression (Expr) """ -type PlotExample +mutable struct PlotExample header::AbstractString desc::AbstractString exprs::Vector{Expr} @@ -18,7 +18,14 @@ PlotExample("Lines", ), PlotExample("Functions, adding data, and animations", - "Plot multiple functions. You can also put the function first, or use the form `plot(f, xmin, xmax)` where f is a Function or AbstractVector{Function}.\n\nGet series data: `x, y = plt[i]`. Set series data: `plt[i] = (x,y)`. Add to the series with `push!`/`append!`.\n\nEasily build animations. (`convert` or `ffmpeg` must be available to generate the animation.) Use command `gif(anim, filename, fps=15)` to save the animation.", +""" +Plot multiple functions. You can also put the function first, or use the form `plot(f, +xmin, xmax)` where f is a Function or AbstractVector{Function}.\n\nGet series data: +`x, y = plt[i]`. Set series data: `plt[i] = (x,y)`. Add to the series with +`push!`/`append!`.\n\nEasily build animations. (`convert` or `ffmpeg` must be available +to generate the animation.) Use command `gif(anim, filename, fps=15)` to save the +animation. +""", [:(begin p = plot([sin,cos], zeros(0), leg=false) anim = Animation() @@ -37,23 +44,35 @@ PlotExample("Parametric plots", ), 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.", +""" +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, α=0.6) - scatter!(y, zcolor=abs(y-.5), m=(:heat,0.8,stroke(1,:green)), ms=10*abs(y-0.5)+4, lab="grad") +y = rand(100) +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)] ), PlotExample("Global", - "Change the guides/background/limits/ticks. Convenience args `xaxis` and `yaxis` allow you to pass a tuple or value which will be mapped to the relevant args automatically. The `xaxis` below will be replaced with `xlabel` and `xlims` args automatically during the preprocessing step. You can also use shorthand functions: `title!`, `xaxis!`, `yaxis!`, `xlabel!`, `ylabel!`, `xlims!`, `ylims!`, `xticks!`, `yticks!`", +""" +Change the guides/background/limits/ticks. Convenience args `xaxis` and `yaxis` allow +you to pass a tuple or value which will be mapped to the relevant args automatically. +The `xaxis` below will be replaced with `xlabel` and `xlims` args automatically during +the preprocessing step. You can also use shorthand functions: `title!`, `xaxis!`, +`yaxis!`, `xlabel!`, `ylabel!`, `xlims!`, `ylims!`, `xticks!`, `yticks!` +""", [:(begin - y = rand(20,3) - plot(y, xaxis=("XLABEL",(-5,30),0:2:20,:flip), background_color = RGB(0.2,0.2,0.2), leg=false) - hline!(mean(y,1)+rand(1,3), line=(4,:dash,0.6,[:lightgreen :green :darkgreen])) - vline!([5,10]) - title!("TITLE") - yaxis!("YLABEL", :log10) +y = rand(20,3) +plot(y, xaxis=("XLABEL",(-5,30),0:2:20,:flip), background_color = RGB(0.2,0.2,0.2), + leg=false) +hline!(mean(y,1)+rand(1,3), line=(4,:dash,0.6,[:lightgreen :green :darkgreen])) +vline!([5,10]) +title!("TITLE") +yaxis!("YLABEL", :log10) end)] ), @@ -66,14 +85,21 @@ PlotExample("Global", PlotExample("Images", "Plot an image. y-axis is set to flipped", [:(begin - import Images - img = Images.load(Pkg.dir("PlotReferenceImages","Plots","pyplot","0.7.0","ref1.png")) + import FileIO + img = FileIO.load(Pkg.dir("PlotReferenceImages","Plots","pyplot","0.7.0","ref1.png")) plot(img) end)] ), PlotExample("Arguments", - "Plot multiple series with different numbers of points. Mix arguments that apply to all series (marker/markersize) with arguments unique to each series (colors). Special arguments `line`, `marker`, and `fill` will automatically figure out what arguments to set (for example, we are setting the `linestyle`, `linewidth`, and `color` arguments with `line`.) Note that we pass a matrix of colors, and this applies the colors to each series.", +""" +Plot multiple series with different numbers of points. Mix arguments that apply to all +series (marker/markersize) with arguments unique to each series (colors). Special +arguments `line`, `marker`, and `fill` will automatically figure out what arguments to +set (for example, we are setting the `linestyle`, `linewidth`, and `color` arguments with +`line`.) Note that we pass a matrix of colors, and this applies the colors to each +series. +""", [:(begin ys = Vector[rand(10), rand(20)] plot(ys, color=[:black :orange], line=(:dot,4), marker=([:hex :d],12,0.8,stroke(3,:gray))) @@ -115,20 +141,23 @@ PlotExample("Line types", PlotExample("Line styles", "", [:(begin - styles = filter(s -> s in Plots.supported_styles(), [:solid, :dash, :dot, :dashdot, :dashdotdot])' - n = length(styles) - y = cumsum(randn(20,n),1) - plot(y, line = (5, styles), label = map(string,styles)) - end)] +styles = filter(s -> s in Plots.supported_styles(), + [:solid, :dash, :dot, :dashdot, :dashdotdot]) +styles = reshape(styles, 1, length(styles)) # Julia 0.6 unfortunately gives an error when transposing symbol vectors +n = length(styles) +y = cumsum(randn(20,n),1) +plot(y, line = (5, styles), label = map(string,styles), legendtitle = "linestyle") + end)] ), PlotExample("Marker types", "", [:(begin - markers = filter(m -> m in Plots.supported_markers(), Plots._shape_keys)' + markers = filter(m -> m in Plots.supported_markers(), Plots._shape_keys) + markers = reshape(markers, 1, length(markers)) n = length(markers) x = linspace(0,10,n+2)[2:end-1] - y = repmat(reverse(x)', n, 1) + y = repmat(reshape(reverse(x),1,:), n, 1) scatter(x, y, m=(8,:auto), lab=map(string,markers), bg=:linen, xlim=(0,10), ylim=(0,10)) end)] ), @@ -143,24 +172,30 @@ PlotExample("Bar", PlotExample("Histogram", "", [:(begin - histogram(randn(1000), nbins=20) + histogram(randn(1000), bins = :scott, weights = repeat(1:5, outer = 200)) end)] ), PlotExample("Subplots", - """ - Use the `layout` keyword, and optionally the convenient `@layout` macro to generate arbitrarily complex subplot layouts. - """, +""" +Use the `layout` keyword, and optionally the convenient `@layout` macro to generate +arbitrarily complex subplot layouts. +""", [:(begin - l = @layout([a{0.1h}; b [c;d e]]) - plot(randn(100,5), layout=l, t=[:line :histogram :scatter :steppre :bar], leg=false, ticks=nothing, border=false) +l = @layout([a{0.1h}; b [c;d e]]) +plot(randn(100,5), layout=l, t=[:line :histogram :scatter :steppre :bar], leg=false, + ticks=nothing, border=:none) end)] ), PlotExample("Adding to subplots", - "Note here the automatic grid layout, as well as the order in which new series are added to the plots.", +""" +Note here the automatic grid layout, as well as the order in which new series are added +to the plots. +""", [:(begin - plot(Plots.fakedata(100,10), layout=4, palette=[:grays :blues :heat :lightrainbow], bg_inside=[:orange :pink :darkblue :black]) +plot(Plots.fakedata(100,10), layout=4, palette=[:grays :blues :heat :lightrainbow], + bg_inside=[:orange :pink :darkblue :black]) end)] ), @@ -173,48 +208,68 @@ PlotExample("", ), PlotExample("Open/High/Low/Close", - "Create an OHLC chart. Pass in a list of (open,high,low,close) tuples as your `y` argument. This uses recipes to first convert the tuples to OHLC objects, and subsequently create a :path series with the appropriate line segments.", +""" +Create an OHLC chart. Pass in a list of (open,high,low,close) tuples as your `y` +argument. This uses recipes to first convert the tuples to OHLC objects, and +subsequently create a :path series with the appropriate line segments. +""", [:(begin - n=20 - hgt=rand(n)+1 - bot=randn(n) - openpct=rand(n) - closepct=rand(n) - y = OHLC[(openpct[i]*hgt[i]+bot[i], bot[i]+hgt[i], bot[i], closepct[i]*hgt[i]+bot[i]) for i in 1:n] - ohlc(y) +n=20 +hgt=rand(n)+1 +bot=randn(n) +openpct=rand(n) +closepct=rand(n) +y = OHLC[(openpct[i]*hgt[i]+bot[i], bot[i]+hgt[i], bot[i], + closepct[i]*hgt[i]+bot[i]) for i in 1:n] +ohlc(y) end)] ), PlotExample("Annotations", - "The `annotations` keyword is used for text annotations in data-coordinates. Pass in a tuple (x,y,text) or a vector of annotations. `annotate!(ann)` is shorthand for `plot!(; annotation=ann)`. Series annotations are used for annotating individual data points. They require only the annotation... x/y values are computed. A `PlotText` object can be build with the method `text(string, attr...)`, which wraps font and color attributes.", +""" +The `annotations` keyword is used for text annotations in data-coordinates. Pass in a +tuple (x,y,text) or a vector of annotations. `annotate!(ann)` is shorthand for `plot!(; +annotation=ann)`. Series annotations are used for annotating individual data points. +They require only the annotation... x/y values are computed. A `PlotText` object can be +build with the method `text(string, attr...)`, which wraps font and color attributes. +""", [:(begin - y = rand(10) - plot(y, annotations = (3,y[3],text("this is #3",:left)), leg=false) - annotate!([(5, y[5], text("this is #5",16,:red,:center)), (10, y[10], text("this is #10",:right,20,"courier"))]) - scatter!(linspace(2,8,6), rand(6), marker=(50,0.2,:orange), series_annotations = ["series","annotations","map","to","series",text("data",:green)]) +y = rand(10) +plot(y, annotations = (3,y[3],text("this is #3",:left)), leg=false) +annotate!([(5, y[5], text("this is #5",16,:red,:center)), + (10, y[10], text("this is #10",:right,20,"courier"))]) +scatter!(linspace(2,8,6), rand(6), marker=(50,0.2,:orange), + series_annotations = ["series","annotations","map","to","series", + text("data",:green)]) end)] ), PlotExample("Custom Markers", - "A `Plots.Shape` is a light wrapper around vertices of a polygon. For supported backends, pass arbitrary polygons as the marker shapes. Note: The center is (0,0) and the size is expected to be rougly the area of the unit circle.", +"""A `Plots.Shape` is a light wrapper around vertices of a polygon. For supported +backends, pass arbitrary polygons as the marker shapes. Note: The center is (0,0) and +the size is expected to be rougly the area of the unit circle. +""", [:(begin - verts = [(-1.0,1.0),(-1.28,0.6),(-0.2,-1.4),(0.2,-1.4),(1.28,0.6),(1.0,1.0), - (-1.0,1.0),(-0.2,-0.6),(0.0,-0.2),(-0.4,0.6),(1.28,0.6),(0.2,-1.4), - (-0.2,-1.4),(0.6,0.2),(-0.2,0.2),(0.0,-0.2),(0.2,0.2),(-0.2,-0.6)] - x = 0.1:0.2:0.9 - y = 0.7rand(5)+0.15 - plot(x, y, line = (3,:dash,:lightblue), marker = (Shape(verts),30,RGBA(0,0,0,0.2)), - bg=:pink, fg=:darkblue, xlim = (0,1), ylim=(0,1), leg=false) +verts = [(-1.0,1.0),(-1.28,0.6),(-0.2,-1.4),(0.2,-1.4),(1.28,0.6),(1.0,1.0), + (-1.0,1.0),(-0.2,-0.6),(0.0,-0.2),(-0.4,0.6),(1.28,0.6),(0.2,-1.4), + (-0.2,-1.4),(0.6,0.2),(-0.2,0.2),(0.0,-0.2),(0.2,0.2),(-0.2,-0.6)] +x = 0.1:0.2:0.9 +y = 0.7rand(5)+0.15 +plot(x, y, line = (3,:dash,:lightblue), marker = (Shape(verts),30,RGBA(0,0,0,0.2)), + bg=:pink, fg=:darkblue, xlim = (0,1), ylim=(0,1), leg=false) end)] ), PlotExample("Contours", - "Any value for fill works here. We first build a filled contour from a function, then an unfilled contour from a matrix.", +""" +Any value for fill works here. We first build a filled contour from a function, then an +unfilled contour from a matrix. +""", [:(begin x = 1:0.5:20 y = 1:0.5:10 f(x,y) = (3x+y^2)*abs(sin(x)+cos(y)) - X = repmat(x', length(y), 1) + X = repmat(reshape(x,1,:), length(y), 1) Y = repmat(y, 1, length(x)) Z = map(f, X, Y) p1 = contour(x, y, f, fill=true) @@ -250,7 +305,7 @@ PlotExample("DataFrames", [:(begin import RDatasets iris = RDatasets.dataset("datasets", "iris") - scatter(iris, :SepalLength, :SepalWidth, group=:Species, + @df iris scatter(:SepalLength, :SepalWidth, group=:Species, title = "My awesome plot", xlabel = "Length", ylabel = "Width", marker = (0.5, [:cross :hex :star7], 12), bg=RGB(.2,.2,.2)) end)] @@ -260,7 +315,8 @@ PlotExample("Groups and Subplots", "", [:(begin group = rand(map(i->"group $i",1:4),100) - plot(rand(100), layout=@layout([a b;c]), group=group, linetype=[:bar :scatter :steppre]) + plot(rand(100), layout=@layout([a b;c]), group=group, + linetype=[:bar :scatter :steppre], linecolor = :match) end)] ), @@ -268,7 +324,7 @@ PlotExample("Polar Plots", "", [:(begin Θ = linspace(0,1.5π,100) - r = abs(0.1randn(100)+sin(3Θ)) + r = abs.(0.1randn(100)+sin.(3Θ)) plot(Θ, r, proj=:polar, m=2) end)] ), @@ -278,7 +334,7 @@ PlotExample("Heatmap, categorical axes, and aspect_ratio", [:(begin xs = [string("x",i) for i=1:10] ys = [string("y",i) for i=1:4] - z = float((1:4)*(1:10)') + z = float((1:4)*reshape(1:10,1,:)) heatmap(xs, ys, z, aspect_ratio=1) end)] ), @@ -286,9 +342,10 @@ PlotExample("Heatmap, categorical axes, and aspect_ratio", PlotExample("Layouts, margins, label rotation, title location", "", [:(begin + using Plots.PlotMeasures # for Measures, e.g. mm and px plot(rand(100,6),layout=@layout([a b; c]),title=["A" "B" "C"], title_location=:left, left_margin=[20mm 0mm], - bottom_margin=50px, xrotation=60) + bottom_margin=10px, xrotation=60) end)] ), @@ -297,10 +354,83 @@ PlotExample("Boxplot and Violin series recipes", [:(begin import RDatasets singers = RDatasets.dataset("lattice", "singer") - violin(singers, :VoicePart, :Height, line = 0, fill = (0.2, :blue)) - boxplot!(singers, :VoicePart, :Height, line = (2,:black), fill = (0.3, :orange)) + @df singers violin(:VoicePart, :Height, line = 0, fill = (0.2, :blue)) + @df singers boxplot!(:VoicePart, :Height, line = (2,:black), fill = (0.3, :orange)) end)] -) +), + +PlotExample("Animation with subplots", + "The `layout` macro can be used to create an animation with subplots.", + [:(begin + l = @layout([[a; b] c]) + p = plot(plot([sin,cos],1,leg=false), + scatter([atan,cos],1,leg=false), + plot(log,1,xlims=(1,10π),ylims=(0,5),leg=false),layout=l) + + anim = Animation() + for x = linspace(1,10π,100) + plot(push!(p,x,Float64[sin(x),cos(x),atan(x),cos(x),log(x)])) + frame(anim) + end + end)] +), + +PlotExample("Spy", +""" +For a matrix `mat` with unique nonzeros `spy(mat)` returns a colorless plot. If `mat` has +various different nonzero values, a colorbar is added. The colorbar can be disabled with +`legend = nothing`. +""", + [:(begin + a = spdiagm((ones(50), ones(49), ones(49), ones(40), ones(40)),(0, 1, -1, 10, -10)) + b = spdiagm((1:50, 1:49, 1:49, 1:40, 1:40),(0, 1, -1, 10, -10)) + plot(spy(a), spy(b), title = ["Unique nonzeros" "Different nonzeros"]) + end)] +), + +PlotExample("Magic grid argument", +""" +The grid lines can be modified individually for each axis with the magic `grid` argument. +""", + [:(begin + x = rand(10) + p1 = plot(x, title = "Default looks") + p2 = plot(x, grid = (:y, :olivedrab, :dot, 1, 0.9), title = "Modified y grid") + p3 = plot(deepcopy(p2), title = "Add x grid") + xgrid!(p3, :on, :cadetblue, 2, :dashdot, 0.4) + plot(p1, p2, p3, layout = (1, 3), label = "", fillrange = 0, fillalpha = 0.3) + end)] +), + +PlotExample("Framestyle", +""" +The style of the frame/axes of a (sub)plot can be changed with the `framestyle` +attribute. The default framestyle is `:axes`. +""", + [:(begin + scatter(fill(randn(10), 6), fill(randn(10), 6), + framestyle = [:box :semi :origin :zerolines :grid :none], + title = [":box" ":semi" ":origin" ":zerolines" ":grid" ":none"], + color = RowVector(1:6), layout = 6, label = "", markerstrokewidth = 0, + ticks = -2:2) + end)] +), + +PlotExample("Lines and markers with varying colors", +""" +You can use the `line_z` and `marker_z` properties to associate a color with +each line segment or marker in the plot. +""", + [:(begin + t = linspace(0, 1, 100) + θ = 6π .* t + x = t .* cos.(θ) + y = t .* sin.(θ) + p1 = plot(x, y, line_z=t, linewidth=3, legend=false) + p2 = scatter(x, y, marker_z=t, color=:bluesreds, legend=false) + plot(p1, p2) + end)] +), ] @@ -321,6 +451,13 @@ function test_examples(pkgname::Symbol, idx::Int; debug = false, disp = true) end # generate all plots and create a dict mapping idx --> plt +""" +test_examples(pkgname[, idx]; debug = false, disp = true, sleep = nothing, + skip = [], only = nothing + +Run the `idx` test example for a given backend, or all examples if `idx` +is not specified. +""" function test_examples(pkgname::Symbol; debug = false, disp = true, sleep = nothing, skip = [], only = nothing) Plots._debugMode.on = debug diff --git a/src/layouts.jl b/src/layouts.jl index a5814ba1..63924374 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -1,23 +1,19 @@ # NOTE: (0,0) is the top-left !!! -# allow pixels and percentages -const px = AbsoluteLength(0.254) -const pct = Length{:pct, Float64}(1.0) - to_pixels(m::AbsoluteLength) = m.value / 0.254 const _cbar_width = 5mm -Base.:.*(m::Measure, n::Number) = m * n -Base.:.*(n::Number, m::Measure) = m * n +Base.broadcast(::typeof(Base.:.*), m::Measure, n::Number) = m * n +Base.broadcast(::typeof(Base.:.*), m::Number, n::Measure) = m * n Base.:-(m::Measure, a::AbstractArray) = map(ai -> m - ai, a) Base.:-(a::AbstractArray, m::Measure) = map(ai -> ai - m, a) Base.zero(::Type{typeof(mm)}) = 0mm Base.one(::Type{typeof(mm)}) = 1mm Base.typemin(::typeof(mm)) = -Inf*mm Base.typemax(::typeof(mm)) = Inf*mm -Base.convert{F<:AbstractFloat}(::Type{F}, l::AbsoluteLength) = convert(F, l.value) +Base.convert(::Type{F}, l::AbsoluteLength) where {F<:AbstractFloat} = convert(F, l.value) # TODO: these are unintuitive and may cause tricky bugs # Base.:+(m1::AbsoluteLength, m2::Length{:pct}) = AbsoluteLength(m1.value * (1 + m2.value)) @@ -99,7 +95,7 @@ end # ----------------------------------------------------------- # points combined by x/y, pct, and length -type MixedMeasures +mutable struct MixedMeasures xy::Float64 pct::Float64 len::AbsoluteLength @@ -133,7 +129,12 @@ make_measure_hor(m::Measure) = m make_measure_vert(n::Number) = n * h make_measure_vert(m::Measure) = m +""" + bbox(x, y, w, h [,originargs...]) + bbox(layout) +Create a bounding box for plotting +""" function bbox(x, y, w, h, oarg1::Symbol, originargs::Symbol...) oargs = vcat(oarg1, originargs...) orighor = :left @@ -215,7 +216,7 @@ bottompad(layout::AbstractLayout) = 0mm # RootLayout # this is the parent of the top-level layout -immutable RootLayout <: AbstractLayout end +struct RootLayout <: AbstractLayout end Base.parent(::RootLayout) = nothing parent_bbox(::RootLayout) = defaultbox @@ -225,7 +226,7 @@ bbox(::RootLayout) = defaultbox # EmptyLayout # contains blank space -type EmptyLayout <: AbstractLayout +mutable struct EmptyLayout <: AbstractLayout parent::AbstractLayout bbox::BoundingBox attr::KW # store label, width, and height for initialization @@ -243,7 +244,7 @@ _update_min_padding!(layout::EmptyLayout) = nothing # GridLayout # nested, gridded layout with optional size percentages -type GridLayout <: AbstractLayout +mutable struct GridLayout <: AbstractLayout parent::AbstractLayout minpad::Tuple # leftpad, toppad, rightpad, bottompad bbox::BoundingBox @@ -253,6 +254,13 @@ type GridLayout <: AbstractLayout attr::KW end +""" + grid(args...; kw...) + +Create a grid layout for subplots. `args` specify the dimensions, e.g. +`grid(3,2, widths = (0.6,04))` creates a grid with three rows and two +columns of different width. +""" grid(args...; kw...) = GridLayout(args...; kw...) function GridLayout(dims...; @@ -473,12 +481,12 @@ function layout_args(n::Integer) GridLayout(nr, nc), n end -function layout_args{I<:Integer}(sztup::NTuple{2,I}) +function layout_args(sztup::NTuple{2,I}) where I<:Integer nr, nc = sztup GridLayout(nr, nc), nr*nc end -function layout_args{I<:Integer}(sztup::NTuple{3,I}) +function layout_args(sztup::NTuple{3,I}) where I<:Integer n, nr, nc = sztup nr, nc = compute_gridsize(n, nr, nc) GridLayout(nr, nc), n @@ -704,7 +712,7 @@ function link_axes!(axes::Axis...) a1 = axes[1] for i=2:length(axes) a2 = axes[i] - expand_extrema!(a1, extrema(a2)) + expand_extrema!(a1, ignorenan_extrema(a2)) for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) a2[k] = a1[k] end diff --git a/src/output.jl b/src/output.jl index ee9972ab..1f9da687 100644 --- a/src/output.jl +++ b/src/output.jl @@ -39,7 +39,7 @@ ps(fn::AbstractString) = ps(current(), fn) function eps(plt::Plot, fn::AbstractString) fn = addExtension(fn, "eps") io = open(fn, "w") - writemime(io, MIME("image/eps"), plt) + show(io, MIME("image/eps"), plt) close(io) end eps(fn::AbstractString) = eps(current(), fn) @@ -97,6 +97,13 @@ function addExtension(fn::AbstractString, ext::AbstractString) end end +""" + savefig([plot,] filename) + +Save a Plot (the current plot if `plot` is not passed) to file. The file +type is inferred from the file extension. All backends support png and pdf +file types, some also support svg, ps, eps, html and tex. +""" function savefig(plt::Plot, fn::AbstractString) # get the extension @@ -119,7 +126,11 @@ savefig(fn::AbstractString) = savefig(current(), fn) # --------------------------------------------------------- +""" + gui([plot]) +Display a plot using the backends' gui window +""" gui(plt::Plot = current()) = display(PlotsDisplay(), plt) # IJulia only... inline display @@ -146,25 +157,16 @@ end # --------------------------------------------------------- -const _mimeformats = Dict( - "application/eps" => "eps", - "image/eps" => "eps", - "application/pdf" => "pdf", - "image/png" => "png", - "application/postscript" => "ps", - "image/svg+xml" => "svg", - "text/plain" => "txt", - "application/x-tex" => "tex", -) - const _best_html_output_type = KW( :pyplot => :png, :unicodeplots => :txt, - :glvisualize => :png + :glvisualize => :png, + :plotlyjs => :html, + :plotly => :html ) # a backup for html... passes to svg or png depending on the html_output_format arg -function Base.show(io::IO, ::MIME"text/html", plt::Plot) +function _show(io::IO, ::MIME"text/html", plt::Plot) output_type = Symbol(plt.attr[:html_output_format]) if output_type == :auto output_type = get(_best_html_output_type, backend_name(plt.backend), :svg) @@ -178,35 +180,43 @@ function Base.show(io::IO, ::MIME"text/html", plt::Plot) elseif output_type == :txt show(io, MIME("text/plain"), plt) else - error("only png or svg allowed. got: $output_type") + error("only png or svg allowed. got: $(repr(output_type))") end end -function _show{B}(io::IO, m, plt::Plot{B}) - # Base.show_backtrace(STDOUT, backtrace()) - warn("_show is not defined for this backend. m=", string(m)) +# delegate mimewritable (showable on julia 0.7) to _show instead +function Base.mimewritable(m::M, plt::P) where {M<:MIME, P<:Plot} + return method_exists(_show, Tuple{IO, M, P}) end + function _display(plt::Plot) warn("_display is not defined for this backend.") end # for writing to io streams... first prepare, then callback -for mime in keys(_mimeformats) - @eval function Base.show{B}(io::IO, m::MIME{Symbol($mime)}, plt::Plot{B}) +for mime in ("text/plain", "text/html", "image/png", "image/eps", "image/svg+xml", + "application/eps", "application/pdf", "application/postscript", + "application/x-tex") + @eval function Base.show(io::IO, m::MIME{Symbol($mime)}, plt::Plot) prepare_output(plt) _show(io, m, plt) end end +# default text/plain for all backends +_show(io::IO, ::MIME{Symbol("text/plain")}, plt::Plot) = show(io, plt) + +"Close all open gui windows of the current backend" closeall() = closeall(backend()) # --------------------------------------------------------- # A backup, if no PNG generation is defined, is to try to make a PDF and use FileIO to convert +const PDFBackends = Union{PGFPlotsBackend,PlotlyJSBackend,PyPlotBackend,InspectDRBackend,GRBackend} if is_installed("FileIO") @eval import FileIO - function _show(io::IO, ::MIME"image/png", plt::Plot) + function _show(io::IO, ::MIME"image/png", plt::Plot{<:PDFBackends}) fn = tempname() # first save a pdf file @@ -220,14 +230,10 @@ if is_installed("FileIO") FileIO.save(pngfn, s) # now write from the file - write(io, readall(open(pngfn))) + write(io, readstring(open(pngfn))) end end - - - - # function html_output_format(fmt) # if fmt == "png" # @eval function Base.show(io::IO, ::MIME"text/html", plt::Plot) @@ -248,80 +254,108 @@ end # IJulia # --------------------------------------------------------- -const _ijulia_output = String["text/html"] +@require IJulia begin + if IJulia.inited -function setup_ijulia() - # override IJulia inline display - if isijulia() - @eval begin - import IJulia - export set_ijulia_output - function set_ijulia_output(mimestr::AbstractString) - # info("Setting IJulia output format to $mimestr") - global _ijulia_output - _ijulia_output[1] = mimestr - end - function IJulia.display_dict(plt::Plot) - global _ijulia_output - Dict{String, String}(_ijulia_output[1] => sprint(show, _ijulia_output[1], plt)) - end + """ + Add extra jupyter mimetypes to display_dict based on the plot backed. - # default text/plain passes to html... handles Interact issues - function Base.show(io::IO, m::MIME"text/plain", plt::Plot) - show(io, MIME("text/html"), plt) - end + The default is nothing, except for plotly based backends, where it + adds data for `application/vnd.plotly.v1+json` that is used in + frontends like jupyterlab and nteract. + """ + _extra_mime_info!(plt::Plot, out::Dict) = out + function _extra_mime_info!(plt::Plot{PlotlyJSBackend}, out::Dict) + out["application/vnd.plotly.v1+json"] = JSON.lower(plt.o) + out end - set_ijulia_output("text/html") + + function _extra_mime_info!(plt::Plot{PlotlyBackend}, out::Dict) + out["application/vnd.plotly.v1+json"] = Dict( + :data => plotly_series(plt), + :layout => plotly_layout(plt) + ) + out + end + + function IJulia.display_dict(plt::Plot) + output_type = Symbol(plt.attr[:html_output_format]) + if output_type == :auto + output_type = get(_best_html_output_type, backend_name(plt.backend), :svg) + end + out = Dict() + if output_type == :txt + mime = "text/plain" + out[mime] = sprint(show, MIME(mime), plt) + elseif output_type == :png + mime = "image/png" + out[mime] = base64encode(show, MIME(mime), plt) + elseif output_type == :svg + mime = "image/svg+xml" + out[mime] = sprint(show, MIME(mime), plt) + elseif output_type == :html + mime = "text/html" + out[mime] = sprint(show, MIME(mime), plt) + else + error("Unsupported output type $output_type") + end + _extra_mime_info!(plt, out) + out + end + + ENV["MPLBACKEND"] = "Agg" end end # --------------------------------------------------------- # Atom PlotPane # --------------------------------------------------------- +@require Juno begin + import Hiccup, Media -function setup_atom() - if isatom() - @eval import Atom, Media + if Juno.isactive() Media.media(Plot, Media.Plot) - # default text/plain so it doesn't complain - function Base.show{B}(io::IO, ::MIME"text/plain", plt::Plot{B}) - print(io, "Plot{$B}()") - end - - function Media.render(e::Atom.Editor, plt::Plot) - Media.render(e, nothing) + function Juno.render(e::Juno.Editor, plt::Plot) + Juno.render(e, nothing) end if get(ENV, "PLOTS_USE_ATOM_PLOTPANE", true) in (true, 1, "1", "true", "yes") - # this is like "display"... sends an html div with the plot to the PlotPane - function Media.render(pane::Atom.PlotPane, plt::Plot) + function Juno.render(pane::Juno.PlotPane, plt::Plot) # temporarily overwrite size to be Atom.plotsize sz = plt[:size] - plt[:size] = Juno.plotsize() - Media.render(pane, Atom.div(".fill", Atom.HTML(stringmime(MIME("text/html"), plt)))) + dpi = plt[:dpi] + thickness_scaling = plt[:thickness_scaling] + jsize = Juno.plotsize() + jsize[1] == 0 && (jsize[1] = 400) + jsize[2] == 0 && (jsize[2] = 500) + + scale = minimum(jsize[i] / sz[i] for i in 1:2) + plt[:size] = (s * scale for s in sz) + plt[:dpi] = Plots.DPI + plt[:thickness_scaling] *= scale + Juno.render(pane, HTML(stringmime(MIME("text/html"), plt))) plt[:size] = sz + plt[:dpi] = dpi + plt[:thickness_scaling] = thickness_scaling + end + # special handling for PlotlyJS + function Juno.render(pane::Juno.PlotPane, plt::Plot{PlotlyJSBackend}) + display(Plots.PlotsDisplay(), plt) end else - # - function Media.render(pane::Atom.PlotPane, plt::Plot) + function Juno.render(pane::Juno.PlotPane, plt::Plot) display(Plots.PlotsDisplay(), plt) s = "PlotPane turned off. Unset ENV[\"PLOTS_USE_ATOM_PLOTPANE\"] and restart Julia to enable it." - Media.render(pane, Atom.div(Atom.HTML(s))) + Juno.render(pane, HTML(s)) end end # special handling for plotly... use PlotsDisplay - function Media.render(pane::Atom.PlotPane, plt::Plot{PlotlyBackend}) + function Juno.render(pane::Juno.PlotPane, plt::Plot{PlotlyBackend}) display(Plots.PlotsDisplay(), plt) - s = "PlotPane turned off. The plotly and plotlyjs backends cannot render in the PlotPane due to javascript issues." - Media.render(pane, Atom.div(Atom.HTML(s))) - end - - # special handling for PlotlyJS to pass through to that render method - function Media.render(pane::Atom.PlotPane, plt::Plot{PlotlyJSBackend}) - Plots.prepare_output(plt) - Media.render(pane, plt.o) + s = "PlotPane turned off. The plotly backend cannot render in the PlotPane due to javascript issues. Plotlyjs is similar to plotly and is compatible with the plot pane." + Juno.render(pane, HTML(s)) end end end diff --git a/src/pipeline.jl b/src/pipeline.jl index 6c2e05c3..d1af402d 100644 --- a/src/pipeline.jl +++ b/src/pipeline.jl @@ -60,29 +60,26 @@ function _process_userrecipes(plt::Plot, d::KW, args) args = _preprocess_args(d, args, still_to_process) # for plotting recipes, swap out the args and update the parameter dictionary - # we are keeping a queue of series that still need to be processed. + # we are keeping a stack of series that still need to be processed. # each pass through the loop, we pop one off and apply the recipe. # the recipe will return a list a Series objects... the ones that are - # finished (no more args) get added to the kw_list, and the rest go into the queue - # for processing. + # finished (no more args) get added to the kw_list, the ones that are not + # are placed on top of the stack and are then processed further. kw_list = KW[] while !isempty(still_to_process) - # grab the first in line to be processed and pass it through apply_recipe - # to generate a list of RecipeData objects (data + attributes) + # grab the first in line to be processed and either add it to the kw_list or + # pass it through apply_recipe to generate a list of RecipeData objects (data + attributes) + # for further processing. next_series = shift!(still_to_process) - rd_list = RecipesBase.apply_recipe(next_series.d, next_series.args...) - for recipedata in rd_list - # recipedata should be of type RecipeData. if it's not then the inputs must not have been fully processed by recipes - if !(typeof(recipedata) <: RecipeData) - error("Inputs couldn't be processed... expected RecipeData but got: $recipedata") - end - - if isempty(recipedata.args) - _process_userrecipe(plt, kw_list, recipedata) - else - # args are non-empty, so there's still processing to do... add it back to the queue - push!(still_to_process, recipedata) - end + # recipedata should be of type RecipeData. if it's not then the inputs must not have been fully processed by recipes + if !(typeof(next_series) <: RecipeData) + error("Inputs couldn't be processed... expected RecipeData but got: $next_series") + end + if isempty(next_series.args) + _process_userrecipe(plt, kw_list, next_series) + else + rd_list = RecipesBase.apply_recipe(next_series.d, next_series.args...) + prepend!(still_to_process,rd_list) end end @@ -153,7 +150,7 @@ function _add_smooth_kw(kw_list::Vector{KW}, kw::KW) if get(kw, :smooth, false) x, y = kw[:x], kw[:y] β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y) - sx = [minimum(x), maximum(x)] + sx = [ignorenan_minimum(x), ignorenan_maximum(x)] sy = β * sx + α push!(kw_list, merge(copy(kw), KW( :seriestype => :path, @@ -213,7 +210,7 @@ function _plot_setup(plt::Plot, d::KW, kw_list::Vector{KW}) # TODO: init subplots here _update_plot_args(plt, d) if !plt.init - plt.o = _create_backend_figure(plt) + plt.o = Base.invokelatest(_create_backend_figure, plt) # create the layout and subplots from the inputs plt.layout, plt.subplots, plt.spmap = build_layout(plt.attr) @@ -262,12 +259,12 @@ function _subplot_setup(plt::Plot, d::KW, kw_list::Vector{KW}) for kw in kw_list # get the Subplot object to which the series belongs. sps = get(kw, :subplot, :auto) - sp = get_subplot(plt, cycle(sps == :auto ? plt.subplots : plt.subplots[sps], command_idx(kw_list,kw))) + sp = get_subplot(plt, _cycle(sps == :auto ? plt.subplots : plt.subplots[sps], command_idx(kw_list,kw))) kw[:subplot] = sp # extract subplot/axis attributes from kw and add to sp_attr attr = KW() - for (k,v) in kw + for (k,v) in collect(kw) if haskey(_subplot_defaults, k) || haskey(_axis_defaults_byletter, k) attr[k] = pop!(kw, k) end @@ -277,6 +274,13 @@ function _subplot_setup(plt::Plot, d::KW, kw_list::Vector{KW}) attr[Symbol(letter,k)] = v end end + for k in (:scale,), letter in (:x,:y,:z) + # Series recipes may need access to this information + lk = Symbol(letter,k) + if haskey(attr, lk) + kw[lk] = attr[lk] + end + end end sp_attrs[sp] = attr end @@ -297,7 +301,7 @@ end # getting ready to add the series... last update to subplot from anything # that might have been added during series recipes -function _prepare_subplot{T}(plt::Plot{T}, d::KW) +function _prepare_subplot(plt::Plot{T}, d::KW) where T st::Symbol = d[:seriestype] sp::Subplot{T} = d[:subplot] sp_idx = get_subplot_index(plt, sp) @@ -323,7 +327,7 @@ end function _override_seriestype_check(d::KW, st::Symbol) # do we want to override the series type? - if !is3d(st) + if !is3d(st) && !(st in (:contour,:contour3d)) z = d[:z] if !isa(z, Void) && (size(d[:x]) == size(d[:y]) == size(z)) st = (st == :scatter ? :scatter3d : :path3d) @@ -353,13 +357,17 @@ end function _expand_subplot_extrema(sp::Subplot, d::KW, st::Symbol) # adjust extrema and discrete info if st == :image - w, h = size(d[:z]) - expand_extrema!(sp[:xaxis], (0,w)) - expand_extrema!(sp[:yaxis], (0,h)) - sp[:yaxis].d[:flip] = true - elseif !(st in (:pie, :histogram, :histogram2d)) + xmin, xmax = ignorenan_extrema(d[:x]); ymin, ymax = ignorenan_extrema(d[:y]) + expand_extrema!(sp[:xaxis], (xmin, xmax)) + expand_extrema!(sp[:yaxis], (ymin, ymax)) + elseif !(st in (:pie, :histogram, :bins2d, :histogram2d)) expand_extrema!(sp, d) end + # expand for zerolines (axes through origin) + if sp[:framestyle] in (:origin, :zerolines) + expand_extrema!(sp[:xaxis], 0.0) + expand_extrema!(sp[:yaxis], 0.0) + end end function _add_the_series(plt, sp, d) @@ -390,6 +398,7 @@ function _process_seriesrecipe(plt::Plot, d::KW) sp = _prepare_subplot(plt, d) _prepare_annotations(sp, d) _expand_subplot_extrema(sp, d, st) + _update_series_attributes!(d, plt, sp) _add_the_series(plt, sp, d) else diff --git a/src/plot.jl b/src/plot.jl index 2e8063af..2b7ab389 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -1,11 +1,15 @@ -type CurrentPlot +mutable struct CurrentPlot nullableplot::Nullable{AbstractPlot} end const CURRENT_PLOT = CurrentPlot(Nullable{AbstractPlot}()) isplotnull() = isnull(CURRENT_PLOT.nullableplot) +""" + current() +Returns the Plot object for the current plot +""" function current() if isplotnull() error("No current plot/subplot") @@ -29,7 +33,7 @@ convertSeriesIndex(plt::Plot, n::Int) = n """ -The main plot command. Use `plot` to create a new plot object, and `plot!` to add to an existing one: +The main plot command. Use `plot` to create a new plot object, and `plot!` to add to an existing one: ``` plot(args...; kw...) # creates a new plot window, and sets it to be the current @@ -38,7 +42,9 @@ The main plot command. Use `plot` to create a new plot object, and `plot!` to a ``` There are lots of ways to pass in data, and lots of keyword arguments... just try it and it will likely work as expected. -When you pass in matrices, it splits by columns. See the documentation for more info. +When you pass in matrices, it splits by columns. To see the list of available attributes, use the `plotattr([attr])` +function, where `attr` is the symbol `:Series:`, `:Subplot:`, `:Plot` or `:Axis`. Pass any attribute to `plotattr` +as a String to look up its docstring; e.g. `plotattr("seriestype")`. """ # this creates a new plot with args/kw and sets it to be the current plot @@ -60,7 +66,7 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) # build our plot vector from the args n = length(plts_tail) + 1 - plts = Array(Plot, n) + plts = Array{Plot}(n) plts[1] = plt1 for (i,plt) in enumerate(plts_tail) plts[i+1] = plt @@ -80,7 +86,7 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) # TODO: replace this with proper processing from a merged user_attr KW # update plot args, first with existing plots, then override with d for p in plts - _update_plot_args(plt, p.attr) + _update_plot_args(plt, copy(p.attr)) plt.n += p.n end _update_plot_args(plt, d) @@ -96,8 +102,13 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) end end - # create the layout and initialize the subplots + # create the layout plt.layout, plt.subplots, plt.spmap = build_layout(layout, num_sp, copy(plts)) + + # do we need to link any axes together? + link_axes!(plt.layout, plt[:link]) + + # initialize the subplots cmdidx = 1 for (idx, sp) in enumerate(plt.subplots) _initialize_subplot(plt, sp) @@ -121,9 +132,6 @@ function plot(plt1::Plot, plts_tail::Plot...; kw...) _update_subplot_args(plt, sp, d, idx, false) end - # do we need to link any axes together? - link_axes!(plt.layout, plt[:link]) - # finish up current(plt) _do_plot_show(plt, get(d, :show, default(:show))) diff --git a/src/plotattr.jl b/src/plotattr.jl new file mode 100644 index 00000000..7313b2ce --- /dev/null +++ b/src/plotattr.jl @@ -0,0 +1,62 @@ + +const _attribute_defaults = Dict(:Series => _series_defaults, + :Subplot => _subplot_defaults, + :Plot => _plot_defaults, + :Axis => _axis_defaults) + +attrtypes() = join(keys(_attribute_defaults), ", ") +attributes(attrtype::Symbol) = sort(collect(keys(_attribute_defaults[attrtype]))) + +function lookup_aliases(attrtype, attribute) + attribute = Symbol(attribute) + attribute = in(attribute, keys(_keyAliases)) ? _keyAliases[attribute] : attribute + in(attribute, keys(_attribute_defaults[attrtype])) && return attribute + error("There is no attribute named $attribute in $attrtype") +end + +""" + plotattr([attr]) + +Look up the properties of a Plots attribute, or specify an attribute type. Call `plotattr()` for options. +The information is the same as that given on https://juliaplots.github.io/attributes/. +""" +function plotattr() + println("Specify an attribute type to get a list of supported attributes. Options are $(attrtypes())") +end + +function plotattr(attrtype::Symbol) + in(attrtype, keys(_attribute_defaults)) || error("Viable options are $(attrtypes())") + println("Defined $attrtype attributes are:\n$(join(attributes(attrtype), ", "))") +end + +function plotattr(attribute::AbstractString) + attribute = Symbol(attribute) + attribute = in(attribute, keys(_keyAliases)) ? _keyAliases[attribute] : attribute + for (k, v) in _attribute_defaults + if in(attribute, keys(v)) + return plotattr(k, "$attribute") + end + end + error("There is no attribute named $attribute") +end + +function plotattr(attrtype::Symbol, attribute::AbstractString) + in(attrtype, keys(_attribute_defaults)) || ArgumentError("`attrtype` must match one of $(attrtypes())") + + attribute = Symbol(lookup_aliases(attrtype, attribute)) + + desc = get(_arg_desc, attribute, "") + first_period_idx = findfirst(desc, '.') + typedesc = desc[1:first_period_idx-1] + desc = strip(desc[first_period_idx+1:end]) + als = keys(filter((_,v)->v==attribute, _keyAliases)) |> collect |> sort + als = join(map(string,als), ", ") + def = _attribute_defaults[attrtype][attribute] + + + # Looks up the different elements and plots them + println("$attribute ", typedesc == "" ? "" : "{$typedesc}", "\n", + als == "" ? "" : "$als\n", + "\n$desc\n", + "$(attrtype) attribute, ", def == "" ? "" : " default: $def") +end diff --git a/src/recipes.jl b/src/recipes.jl index 156bda3e..dfb1fd54 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -1,53 +1,4 @@ - - -""" -You can easily define your own plotting recipes with convenience methods: - -``` -@userplot type GroupHist - args -end - -@recipe function f(gh::GroupHist) - # set some attributes, add some series, using gh.args as input -end - -# now you can plot like: -grouphist(rand(1000,4)) -``` -""" -macro userplot(expr) - _userplot(expr) -end - -function _userplot(expr::Expr) - if expr.head != :type - errror("Must call userplot on a type/immutable expression. Got: $expr") - end - - typename = expr.args[2] - funcname = Symbol(lowercase(string(typename))) - funcname2 = Symbol(funcname, "!") - - # return a code block with the type definition and convenience plotting methods - esc(quote - $expr - export $funcname, $funcname2 - $funcname(args...; kw...) = plot($typename(args); kw...) - $funcname2(args...; kw...) = plot!($typename(args); kw...) - end) -end - -function _userplot(sym::Symbol) - _userplot(:(type $sym - args - end)) -end - - -# ---------------------------------------------------------------------------------- - const _series_recipe_deps = Dict() function series_recipe_dependencies(st::Symbol, deps::Symbol...) @@ -96,7 +47,7 @@ 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")) +RecipesBase.apply_recipe(d::KW, ::Type{T}, plt::AbstractPlot) where {T} = throw(MethodError("Unmatched plot recipe: $T")) # --------------------------------------------------------------------------- @@ -128,55 +79,101 @@ function hvline_limits(axis::Axis) end @recipe function f(::Type{Val{:hline}}, x, y, z) - xmin, xmax = hvline_limits(d[:subplot][:xaxis]) n = length(y) - newx = repmat(Float64[xmin, xmax, NaN], n) + newx = repmat(Float64[-1, 1, NaN], n) newy = vec(Float64[yi for i=1:3,yi=y]) x := newx y := newy - seriestype := :path + seriestype := :straightline () end -@deps hline path +@deps hline straightline @recipe function f(::Type{Val{:vline}}, x, y, z) - ymin, ymax = hvline_limits(d[:subplot][:yaxis]) n = length(y) newx = vec(Float64[yi for i=1:3,yi=y]) - newy = repmat(Float64[ymin, ymax, NaN], n) + newy = repmat(Float64[-1, 1, NaN], n) x := newx y := newy - seriestype := :path + seriestype := :straightline () end -@deps vline path +@deps vline straightline + +@recipe function f(::Type{Val{:hspan}}, x, y, z) + n = div(length(y), 2) + newx = repeat([-Inf, Inf, Inf, -Inf, NaN], outer = n) + newy = vcat([[y[2i-1], y[2i-1], y[2i], y[2i], NaN] for i in 1:n]...) + linewidth --> 0 + x := newx + y := newy + seriestype := :shape + () +end +@deps hspan shape + +@recipe function f(::Type{Val{:vspan}}, x, y, z) + n = div(length(y), 2) + newx = vcat([[y[2i-1], y[2i-1], y[2i], y[2i], NaN] for i in 1:n]...) + newy = repeat([-Inf, Inf, Inf, -Inf, NaN], outer = n) + linewidth --> 0 + x := newx + y := newy + seriestype := :shape + () +end +@deps vspan shape + +# --------------------------------------------------------------------------- +# path and scatter + +# create a path from steps +@recipe function f(::Type{Val{:scatterpath}}, x, y, z) + x := x + y := y + seriestype := :scatter + @series begin + seriestype := :path + label := "" + primary := false + () + end +() +end +@deps scatterpath path scatter + # --------------------------------------------------------------------------- # steps -function make_steps(x, y, st) +make_steps(x, st) = x +function make_steps(x::AbstractArray, st) n = length(x) - n == 0 && return zeros(0),zeros(0) - newx, newy = zeros(2n-1), zeros(2n-1) - for i=1:n - idx = 2i-1 + n == 0 && return zeros(0) + newx = zeros(2n - 1) + for i in 1:n + idx = 2i - 1 newx[idx] = x[i] - newy[idx] = y[i] if i > 1 - newx[idx-1] = x[st == :steppre ? i-1 : i] - newy[idx-1] = y[st == :steppre ? i : i-1] + newx[idx - 1] = x[st == :pre ? i : i - 1] end end - newx, newy + return newx end +make_steps(t::Tuple, st) = Tuple(make_steps(ti, st) for ti in t) + # create a path from steps @recipe function f(::Type{Val{:steppre}}, x, y, z) - d[:x], d[:y] = make_steps(x, y, :steppre) + plotattributes[:x] = make_steps(x, :post) + plotattributes[:y] = make_steps(y, :pre) seriestype := :path + # handle fillrange + plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :pre) + # create a secondary series for the markers - if d[:markershape] != :none + if plotattributes[:markershape] != :none @series begin seriestype := :scatter x := x @@ -193,11 +190,15 @@ end # create a path from steps @recipe function f(::Type{Val{:steppost}}, x, y, z) - d[:x], d[:y] = make_steps(x, y, :steppost) + plotattributes[:x] = make_steps(x, :pre) + plotattributes[:y] = make_steps(y, :post) seriestype := :path + # handle fillrange + plotattributes[:fillrange] = make_steps(plotattributes[:fillrange], :post) + # create a secondary series for the markers - if d[:markershape] != :none + if plotattributes[:markershape] != :none @series begin seriestype := :scatter x := x @@ -219,20 +220,20 @@ end # create vertical line segments from fill @recipe function f(::Type{Val{:sticks}}, x, y, z) n = length(x) - fr = d[:fillrange] + fr = plotattributes[:fillrange] if fr == nothing - yaxis = d[:subplot][:yaxis] + yaxis = plotattributes[:subplot][:yaxis] fr = if yaxis[:scale] == :identity 0.0 else - min(axis_limits(yaxis)[1], minimum(y)) + NaNMath.min(axis_limits(yaxis)[1], ignorenan_minimum(y)) end end newx, newy = zeros(3n), zeros(3n) for i=1:n rng = 3i-2:3i newx[rng] = [x[i], x[i], NaN] - newy[rng] = [cycle(fr,i), y[i], NaN] + newy[rng] = [_cycle(fr,i), y[i], NaN] end x := newx y := newy @@ -240,7 +241,7 @@ end seriestype := :path # create a secondary series for the markers - if d[:markershape] != :none + if plotattributes[:markershape] != :none @series begin seriestype := :scatter x := x @@ -273,7 +274,7 @@ end @recipe function f(::Type{Val{:curves}}, x, y, z; npoints = 30) args = z != nothing ? (x,y,z) : (x,y) newx, newy = zeros(0), zeros(0) - fr = d[:fillrange] + fr = plotattributes[:fillrange] newfr = fr != nothing ? zeros(0) : nothing newz = z != nothing ? zeros(0) : nothing # lz = d[:line_z] @@ -284,16 +285,16 @@ end for rng in iter_segments(args...) length(rng) < 2 && continue ts = linspace(0, 1, npoints) - nanappend!(newx, map(t -> bezier_value(cycle(x,rng), t), ts)) - nanappend!(newy, map(t -> bezier_value(cycle(y,rng), t), ts)) + nanappend!(newx, map(t -> bezier_value(_cycle(x,rng), t), ts)) + nanappend!(newy, map(t -> bezier_value(_cycle(y,rng), t), ts)) if z != nothing - nanappend!(newz, map(t -> bezier_value(cycle(z,rng), t), ts)) + nanappend!(newz, map(t -> bezier_value(_cycle(z,rng), t), ts)) end if fr != nothing - nanappend!(newfr, map(t -> bezier_value(cycle(fr,rng), t), ts)) + nanappend!(newfr, map(t -> bezier_value(_cycle(fr,rng), t), ts)) end # if lz != nothing - # lzrng = cycle(lz, rng) # the line_z's for this segment + # lzrng = _cycle(lz, rng) # the line_z's for this segment # push!(newlz, 0.0) # append!(newlz, map(t -> lzrng[1+floor(Int, t * (length(rng)-1))], ts)) # end @@ -323,10 +324,11 @@ end # create a bar plot as a filled step function @recipe function f(::Type{Val{:bar}}, x, y, z) - nx, ny = length(x), length(y) - axis = d[:subplot][isvertical(d) ? :xaxis : :yaxis] - cv = [discrete_value!(axis, xi)[1] for xi=x] - x = if nx == ny + procx, procy, xscale, yscale, baseline = _preprocess_barlike(plotattributes, x, y) + nx, ny = length(procx), length(procy) + axis = plotattributes[:subplot][isvertical(plotattributes) ? :xaxis : :yaxis] + cv = [discrete_value!(axis, xi)[1] for xi=procx] + procx = if nx == ny cv elseif nx == ny + 1 0.5diff(cv) + cv[1:end-1] @@ -335,35 +337,44 @@ end end # compute half-width of bars - bw = d[:bar_width] + bw = plotattributes[:bar_width] hw = if bw == nothing - 0.5mean(diff(x)) + if nx > 1 + 0.5*_bar_width*ignorenan_minimum(filter(x->x>0, diff(procx))) + else + 0.5 * _bar_width + end else - Float64[0.5cycle(bw,i) for i=1:length(x)] + Float64[0.5_cycle(bw,i) for i=1:length(procx)] end # make fillto a vector... default fills to 0 - fillto = d[:fillrange] + fillto = plotattributes[:fillrange] if fillto == nothing fillto = 0 end + if (yscale in _logScales) && !all(_is_positive, fillto) + fillto = map(x -> _is_positive(x) ? typeof(baseline)(x) : baseline, fillto) + end # create the bar shapes by adding x/y segments xseg, yseg = Segments(), Segments() for i=1:ny - center = x[i] - hwi = cycle(hw,i) - yi = y[i] - fi = cycle(fillto,i) - push!(xseg, center-hwi, center-hwi, center+hwi, center+hwi, center-hwi) - push!(yseg, yi, fi, fi, yi, yi) + yi = procy[i] + if !isnan(yi) + center = procx[i] + hwi = _cycle(hw,i) + fi = _cycle(fillto,i) + push!(xseg, center-hwi, center-hwi, center+hwi, center+hwi, center-hwi) + push!(yseg, yi, fi, fi, yi, yi) + end end # widen limits out a bit - expand_extrema!(axis, widen(extrema(xseg.pts)...)) + expand_extrema!(axis, widen(ignorenan_extrema(xseg.pts)...)) # switch back - if !isvertical(d) + if !isvertical(plotattributes) xseg, yseg = yseg, xseg end @@ -379,105 +390,360 @@ end @deps bar shape # --------------------------------------------------------------------------- -# Histograms - -# edges from number of bins -function calc_edges(v, bins::Integer) - vmin, vmax = extrema(v) - linspace(vmin, vmax, bins+1) -end - -# just pass through arrays -calc_edges(v, bins::AVec) = bins - -# find the bucket index of this value -function bucket_index(vi, edges) - for (i,e) in enumerate(edges) - if vi <= e - return max(1,i-1) +# Plots Heatmap +@recipe function f(::Type{Val{:plots_heatmap}}, x, y, z) + xe, ye = heatmap_edges(x), heatmap_edges(y) + m, n = size(z.surf) + x_pts, y_pts = fill(NaN, 6 * m * n), fill(NaN, 6 * m * n) + fz = zeros(m * n) + for i in 1:m # y + for j in 1:n # x + k = (j - 1) * m + i + inds = (6 * (k - 1) + 1):(6 * k - 1) + x_pts[inds] .= [xe[j], xe[j + 1], xe[j + 1], xe[j], xe[j]] + y_pts[inds] .= [ye[i], ye[i], ye[i + 1], ye[i + 1], ye[i]] + fz[k] = z.surf[i, j] end end - return length(edges)-1 + ensure_gradient!(plotattributes, :fillcolor, :fillalpha) + fill_z := fz + line_z := fz + x := x_pts + y := y_pts + z := nothing + seriestype := :shape + label := "" + widen --> false + () +end +@deps plots_heatmap shape + +# --------------------------------------------------------------------------- +# Histograms + +_bin_centers(v::AVec) = (v[1:end-1] + v[2:end]) / 2 + +_is_positive(x) = (x > 0) && !(x ≈ 0) + +_positive_else_nan(::Type{T}, x::Real) where {T} = _is_positive(x) ? T(x) : T(NaN) + +function _scale_adjusted_values(::Type{T}, V::AbstractVector, scale::Symbol) where T<:AbstractFloat + if scale in _logScales + [_positive_else_nan(T, x) for x in V] + else + [T(x) for x in V] + end end -function my_hist(v, bins; normed = false, weights = nothing) - edges = calc_edges(v, bins) - counts = zeros(length(edges)-1) - # add a weighted count - for (i,vi) in enumerate(v) - idx = bucket_index(vi, edges) - counts[idx] += (weights == nothing ? 1.0 : weights[i]) +function _binbarlike_baseline(min_value::T, scale::Symbol) where T<:Real + if (scale in _logScales) + !isnan(min_value) ? min_value / T(_logScaleBases[scale]^log10(2)) : T(1E-3) + else + zero(T) + end +end + + +function _preprocess_binbarlike_weights(::Type{T}, w, wscale::Symbol) where T<:AbstractFloat + w_adj = _scale_adjusted_values(T, w, wscale) + w_min = ignorenan_minimum(w_adj) + w_max = ignorenan_maximum(w_adj) + baseline = _binbarlike_baseline(w_min, wscale) + w_adj, baseline +end + +function _preprocess_barlike(d, x, y) + xscale = get(d, :xscale, :identity) + yscale = get(d, :yscale, :identity) + weights, baseline = _preprocess_binbarlike_weights(float(eltype(y)), y, yscale) + x, weights, xscale, yscale, baseline +end + +function _preprocess_binlike(d, x, y) + xscale = get(d, :xscale, :identity) + yscale = get(d, :yscale, :identity) + T = float(promote_type(eltype(x), eltype(y))) + edge = T.(x) + weights, baseline = _preprocess_binbarlike_weights(T, y, yscale) + edge, weights, xscale, yscale, baseline +end + + +@recipe function f(::Type{Val{:barbins}}, x, y, z) + edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, x, y) + if (plotattributes[:bar_width] == nothing) + bar_width := diff(edge) + end + x := _bin_centers(edge) + y := weights + seriestype := :bar + () +end +@deps barbins bar + + +@recipe function f(::Type{Val{:scatterbins}}, x, y, z) + edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, x, y) + xerror := diff(edge)/2 + x := _bin_centers(edge) + y := weights + seriestype := :scatter + () +end +@deps scatterbins scatter + + +function _stepbins_path(edge, weights, baseline::Real, xscale::Symbol, yscale::Symbol) + log_scale_x = xscale in _logScales + log_scale_y = yscale in _logScales + + nbins = length(linearindices(weights)) + if length(linearindices(edge)) != nbins + 1 + error("Edge vector must be 1 longer than weight vector") end - # normalize by bar area? - norm_denom = normed ? sum(diff(edges) .* counts) : 1.0 - if norm_denom == 0 - norm_denom = 1.0 + x = eltype(edge)[] + y = eltype(weights)[] + + it_e, it_w = start(edge), start(weights) + a, it_e = next(edge, it_e) + last_w = eltype(weights)(NaN) + i = 1 + while (!done(edge, it_e) && !done(edge, it_e)) + b, it_e = next(edge, it_e) + w, it_w = next(weights, it_w) + + if (log_scale_x && a ≈ 0) + a = b/_logScaleBases[xscale]^3 + end + + if isnan(w) + if !isnan(last_w) + push!(x, a) + push!(y, baseline) + end + else + if isnan(last_w) + push!(x, a) + push!(y, baseline) + end + push!(x, a) + push!(y, w) + push!(x, b) + push!(y, w) + end + + a = b + last_w = w + end + if (last_w != baseline) + push!(x, a) + push!(y, baseline) end - edges, counts ./ norm_denom + (x, y) +end + + +@recipe function f(::Type{Val{:stepbins}}, x, y, z) + axis = plotattributes[:subplot][Plots.isvertical(plotattributes) ? :xaxis : :yaxis] + + edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, x, y) + + xpts, ypts = _stepbins_path(edge, weights, baseline, xscale, yscale) + if !isvertical(plotattributes) + xpts, ypts = ypts, xpts + end + + # create a secondary series for the markers + if plotattributes[:markershape] != :none + @series begin + seriestype := :scatter + x := _bin_centers(edge) + y := weights + fillrange := nothing + label := "" + primary := false + () + end + markershape := :none + xerror := :none + yerror := :none + end + + x := xpts + y := ypts + seriestype := :path + () +end +Plots.@deps stepbins path + +wand_edges(x...) = (warn("Load the StatPlots package in order to use :wand bins. Defaulting to :auto", once = true); :auto) + +function _auto_binning_nbins(vs::NTuple{N,AbstractVector}, dim::Integer; mode::Symbol = :auto) where N + _cl(x) = ceil(Int, NaNMath.max(x, one(x))) + _iqr(v) = (q = quantile(v, 0.75) - quantile(v, 0.25); q > 0 ? q : oftype(q, 1)) + _span(v) = ignorenan_maximum(v) - ignorenan_minimum(v) + + n_samples = length(linearindices(first(vs))) + + # The nd estimator is the key to most automatic binning methods, and is modified for twodimensional histograms to include correlation + nd = n_samples^(1/(2+N)) + nd = N == 2 ? min(n_samples^(1/(2+N)), nd / (1-cor(first(vs), last(vs))^2)^(3//8)) : nd # the >2-dimensional case does not have a nice solution to correlations + + v = vs[dim] + + if mode == :auto + mode = :fd + end + + if mode == :sqrt # Square-root choice + _cl(sqrt(n_samples)) + elseif mode == :sturges # Sturges' formula + _cl(log2(n_samples) + 1) + elseif mode == :rice # Rice Rule + _cl(2 * nd) + elseif mode == :scott # Scott's normal reference rule + _cl(_span(v) / (3.5 * std(v) / nd)) + elseif mode == :fd # Freedman–Diaconis rule + _cl(_span(v) / (2 * _iqr(v) / nd)) + elseif mode == :wand + wand_edges(v) # this makes this function not type stable, but the type instability does not propagate + else + error("Unknown auto-binning mode $mode") + end +end + +_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::Integer) where {N} = StatsBase.histrange(vs[dim], binning, :left) +_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::Symbol) where {N} = _hist_edge(vs, dim, _auto_binning_nbins(vs, dim, mode = binning)) +_hist_edge(vs::NTuple{N,AbstractVector}, dim::Integer, binning::AbstractVector) where {N} = binning + +_hist_edges(vs::NTuple{N,AbstractVector}, binning::NTuple{N}) where {N} = + map(dim -> _hist_edge(vs, dim, binning[dim]), (1:N...)) + +_hist_edges(vs::NTuple{N,AbstractVector}, binning::Union{Integer, Symbol, AbstractVector}) where {N} = + map(dim -> _hist_edge(vs, dim, binning), (1:N...)) + +_hist_norm_mode(mode::Symbol) = mode +_hist_norm_mode(mode::Bool) = mode ? :pdf : :none + +function _make_hist(vs::NTuple{N,AbstractVector}, binning; normed = false, weights = nothing) where N + edges = _hist_edges(vs, binning) + h = float( weights == nothing ? + StatsBase.fit(StatsBase.Histogram, vs, edges, closed = :left) : + StatsBase.fit(StatsBase.Histogram, vs, StatsBase.Weights(weights), edges, closed = :left) + ) + normalize!(h, mode = _hist_norm_mode(normed)) end @recipe function f(::Type{Val{:histogram}}, x, y, z) - edges, counts = my_hist(y, d[:bins], - normed = d[:normalize], - weights = d[:weights]) - x := edges - y := counts - seriestype := :bar + seriestype := length(y) > 1e6 ? :stephist : :barhist () end -@deps histogram bar +@deps histogram barhist + +@recipe function f(::Type{Val{:barhist}}, x, y, z) + h = _make_hist((y,), plotattributes[:bins], normed = plotattributes[:normalize], weights = plotattributes[:weights]) + x := h.edges[1] + y := h.weights + seriestype := :barbins + () +end +@deps barhist barbins + +@recipe function f(::Type{Val{:stephist}}, x, y, z) + h = _make_hist((y,), plotattributes[:bins], normed = plotattributes[:normalize], weights = plotattributes[:weights]) + x := h.edges[1] + y := h.weights + seriestype := :stepbins + () +end +@deps stephist stepbins + +@recipe function f(::Type{Val{:scatterhist}}, x, y, z) + h = _make_hist((y,), plotattributes[:bins], normed = plotattributes[:normalize], weights = plotattributes[:weights]) + x := h.edges[1] + y := h.weights + seriestype := :scatterbins + () +end +@deps scatterhist scatterbins + + +@recipe function f(h::StatsBase.Histogram{T, 1, E}) where {T, E} + seriestype --> :barbins + + st_map = Dict( + :bar => :barbins, :scatter => :scatterbins, :step => :stepbins, + :steppost => :stepbins # :step can be mapped to :steppost in pre-processing + ) + seriestype := get(st_map, plotattributes[:seriestype], plotattributes[:seriestype]) + + if plotattributes[:seriestype] == :scatterbins + # Workaround, error bars currently not set correctly by scatterbins + edge, weights, xscale, yscale, baseline = _preprocess_binlike(plotattributes, h.edges[1], h.weights) + xerror --> diff(h.edges[1])/2 + seriestype := :scatter + (Plots._bin_centers(edge), weights) + else + (h.edges[1], h.weights) + end +end + + +@recipe function f(hv::AbstractVector{H}) where H <: StatsBase.Histogram + for h in hv + @series begin + h + end + end +end + # --------------------------------------------------------------------------- # Histogram 2D -# if tuple, map out bins, otherwise use the same for both -calc_edges_2d(x, y, bins) = calc_edges(x, bins), calc_edges(y, bins) -calc_edges_2d{X,Y}(x, y, bins::Tuple{X,Y}) = calc_edges(x, bins[1]), calc_edges(y, bins[2]) +@recipe function f(::Type{Val{:bins2d}}, x, y, z) + edge_x, edge_y, weights = x, y, z.surf -# the 2D version -function my_hist_2d(x, y, bins; normed = false, weights = nothing) - xedges, yedges = calc_edges_2d(x, y, bins) - counts = zeros(length(yedges)-1, length(xedges)-1) - - # add a weighted count - for i=1:length(x) - r = bucket_index(y[i], yedges) - c = bucket_index(x[i], xedges) - counts[r,c] += (weights == nothing ? 1.0 : weights[i]) + float_weights = float(weights) + if float_weights === weights + float_weights = deepcopy(float_weights) end - - # normalize to cubic area of the imaginary surface towers - norm_denom = normed ? sum((diff(yedges) * diff(xedges)') .* counts) : 1.0 - if norm_denom == 0 - norm_denom = 1.0 - end - - xedges, yedges, counts ./ norm_denom -end - -centers(v::AVec) = 0.5 * (v[1:end-1] + v[2:end]) - -@recipe function f(::Type{Val{:histogram2d}}, x, y, z) - xedges, yedges, counts = my_hist_2d(x, y, d[:bins], - normed = d[:normalize], - weights = d[:weights]) - for (i,c) in enumerate(counts) + for (i, c) in enumerate(float_weights) if c == 0 - counts[i] = NaN + float_weights[i] = NaN end end - x := centers(xedges) - y := centers(yedges) - z := Surface(counts) - linewidth := 0 + + x := Plots._bin_centers(edge_x) + y := Plots._bin_centers(edge_y) + z := Surface(float_weights) + + match_dimensions := true seriestype := :heatmap () end -@deps histogram2d heatmap +Plots.@deps bins2d heatmap + + +@recipe function f(::Type{Val{:histogram2d}}, x, y, z) + h = _make_hist((x, y), plotattributes[:bins], normed = plotattributes[:normalize], weights = plotattributes[:weights]) + x := h.edges[1] + y := h.edges[2] + z := Surface(h.weights) + seriestype := :bins2d + () +end +@deps histogram2d bins2d + + +@recipe function f(h::StatsBase.Histogram{T, 2, E}) where {T, E} + seriestype --> :bins2d + (h.edges[1], h.edges[2], Surface(h.weights)) +end # --------------------------------------------------------------------------- @@ -485,7 +751,7 @@ end @recipe function f(::Type{Val{:scatter3d}}, x, y, z) seriestype := :path3d - if d[:markershape] == :none + if plotattributes[:markershape] == :none markershape := :circle end linewidth := 0 @@ -526,12 +792,12 @@ end function error_coords(xorig, yorig, ebar) # init empty x/y, and zip errors if passed Tuple{Vector,Vector} - x, y = Array(float_extended_type(xorig), 0), Array(Float64, 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) - yi = cycle(yorig, i) - ebi = cycle(ebar, i) + xi = _cycle(xorig, i) + yi = _cycle(yorig, i) + ebi = _cycle(ebar, i) nanappend!(x, [xi, xi]) e1, e2 = if istuple(ebi) first(ebi), last(ebi) @@ -548,17 +814,17 @@ end # we will create a series of path segments, where each point represents one # side of an errorbar @recipe function f(::Type{Val{:yerror}}, x, y, z) - error_style!(d) + error_style!(plotattributes) markershape := :hline - d[:x], d[:y] = error_coords(d[:x], d[:y], error_zipit(d[:yerror])) + plotattributes[:x], plotattributes[:y] = error_coords(plotattributes[:x], plotattributes[:y], error_zipit(plotattributes[:yerror])) () end @deps yerror path @recipe function f(::Type{Val{:xerror}}, x, y, z) - error_style!(d) + error_style!(plotattributes) markershape := :vline - d[:y], d[:x] = error_coords(d[:y], d[:x], error_zipit(d[:xerror])) + plotattributes[:y], plotattributes[:x] = error_coords(plotattributes[:y], plotattributes[:x], error_zipit(plotattributes[:xerror])) () end @deps xerror path @@ -584,11 +850,11 @@ function quiver_using_arrows(d::KW) x, y = zeros(0), zeros(0) for i = 1:max(length(xorig), length(yorig)) # get the starting position - xi = cycle(xorig, i) - yi = cycle(yorig, i) + xi = _cycle(xorig, i) + yi = _cycle(yorig, i) # get the velocity - vi = cycle(velocity, i) + vi = _cycle(velocity, i) vx, vy = if istuple(vi) first(vi), last(vi) elseif isscalar(vi) @@ -621,12 +887,12 @@ function quiver_using_hack(d::KW) for i = 1:max(length(xorig), length(yorig)) # get the starting position - xi = cycle(xorig, i) - yi = cycle(yorig, i) + xi = _cycle(xorig, i) + yi = _cycle(yorig, i) p = P2(xi, yi) # get the velocity - vi = cycle(velocity, i) + vi = _cycle(velocity, i) vx, vy = if istuple(vi) first(vi), last(vi) elseif isscalar(vi) @@ -657,9 +923,9 @@ end # function apply_series_recipe(d::KW, ::Type{Val{:quiver}}) @recipe function f(::Type{Val{:quiver}}, x, y, z) if :arrow in supported_attrs() - quiver_using_arrows(d) + quiver_using_arrows(plotattributes) else - quiver_using_hack(d) + quiver_using_hack(plotattributes) end () end @@ -670,7 +936,8 @@ end # TODO: move OHLC to PlotRecipes finance.jl -type OHLC{T<:Real} +"Represent Open High Low Close data (used in finance)" +mutable struct OHLC{T<:Real} open::T high::T low::T @@ -693,7 +960,7 @@ end # get the joined vector function get_xy(v::AVec{OHLC}, x = 1:length(v)) - xdiff = 0.3mean(abs(diff(x))) + xdiff = 0.3ignorenan_mean(abs.(diff(x))) x_out, y_out = zeros(0), zeros(0) for (i,ohlc) in enumerate(v) ox,oy = get_xy(ohlc, x[i], xdiff) @@ -709,10 +976,10 @@ end # to squash ambiguity warnings... @recipe f(x::AVec{Function}, v::AVec{OHLC}) = error() -@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(x::AVec{Function}, v::AVec{Tuple{R1,R2,R3,R4}}) = error() +@recipe f(x::AVec{Function}, v::AVec{Tuple{R1,R2,R3,R4}}) where {R1<:Number,R2<:Number,R3<:Number,R4<:Number} = error() # this must be OHLC? -@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(x::AVec, ohlc::AVec{Tuple{R1,R2,R3,R4}}) = x, OHLC[OHLC(t...) for t in ohlc] +@recipe f(x::AVec, ohlc::AVec{Tuple{R1,R2,R3,R4}}) where {R1<:Number,R2<:Number,R3<:Number,R4<:Number} = x, OHLC[OHLC(t...) for t in ohlc] @recipe function f(x::AVec, v::AVec{OHLC}) seriestype := :path @@ -734,7 +1001,7 @@ end # "Sparsity plot... heatmap of non-zero values of a matrix" # function spy{T<:Real}(z::AMat{T}; kw...) -# mat = map(zi->float(zi!=0), z)' +# mat = reshape(map(zi->float(zi!=0), z),1,:) # 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), @@ -750,6 +1017,10 @@ end @assert length(g.args) == 1 && typeof(g.args[1]) <: AbstractMatrix seriestype := :spy mat = g.args[1] + if length(unique(mat[mat .!= 0])) < 2 + legend --> nothing + seriescolor --> cgrad([invisible(), fg_color(plotattributes)]) + end n,m = size(mat) Plots.SliceIt, 1:m, 1:n, Surface(mat) end @@ -757,48 +1028,101 @@ 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 + newz = fill(NaN, size(z)...) + + for i in eachindex(zs) + newz[rs[i],cs[i]] = zs[i] end - if d[:markersize] == default(:markersize) - markersize := 1 - end - markerstrokewidth := 0 - marker_z := zs - label := "" - x := cs - y := rs - z := nothing - seriestype := :scatter + + seriestype := :heatmap + grid --> false + framestyle --> :box + + x := x + y := y + z := Surface(newz) () end # ------------------------------------------------- -"Adds a+bx... straight line over the current plot" -function abline!(plt::Plot, a, b; kw...) - plot!(plt, [extrema(plt)...], x -> b + a*x; kw...) -end +"Adds a+bx... straight line over the current plot, without changing the axis limits" +abline!(plt::Plot, a, b; kw...) = plot!(plt, [0, 1], [b, b+a]; seriestype = :straightline, kw...) abline!(args...; kw...) = abline!(current(), args...; kw...) # ------------------------------------------------- -# Dates +# Dates & Times -@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))) +dateformatter(dt) = string(Date(Dates.UTD(dt))) +datetimeformatter(dt) = string(DateTime(Dates.UTM(dt))) +timeformatter(t) = string(Dates.Time(Dates.Nanosecond(t))) + +@recipe f(::Type{Date}, dt::Date) = (dt -> Dates.value(dt), dateformatter) +@recipe f(::Type{DateTime}, dt::DateTime) = (dt -> Dates.value(dt), datetimeformatter) +@recipe f(::Type{Dates.Time}, t::Dates.Time) = (t -> Dates.value(t), timeformatter) # ------------------------------------------------- # 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]) +@recipe function f(A::Array{Complex{T}}) where T<:Number + xguide --> "Re(x)" + yguide --> "Im(x)" + real.(A), imag.(A) +end + +# Splits a complex matrix to its real and complex parts +# Reals defaults solid, imaginary defaults dashed +# Label defaults are changed to match the real-imaginary reference / indexing +@recipe function f(x::AbstractArray{T},y::Array{Complex{T2}}) where {T<:Real,T2} + ylabel --> "Re(y)" + zlabel --> "Im(y)" + x,real.(y),imag.(y) +end + + +# -------------------------------------------------- +# Color Gradients + +@userplot ShowLibrary +@recipe function f(cl::ShowLibrary) + if !(length(cl.args) == 1 && isa(cl.args[1], Symbol)) + error("showlibrary takes the name of a color library as a Symbol") + end + + library = PlotUtils.color_libraries[cl.args[1]] + z = sqrt.((1:15)*reshape(1:20,1,:)) + + seriestype := :heatmap + ticks := nothing + legend := false + + layout --> length(library.lib) + + i = 0 + for grad in sort(collect(keys(library.lib))) + @series begin + seriescolor := cgrad(grad, cl.args[1]) + title := string(grad) + subplot := i += 1 + z + end + end +end + +@userplot ShowGradient +@recipe function f(grad::ShowGradient) + if !(length(grad.args) == 1 && isa(grad.args[1], Symbol)) + error("showgradient takes the name of a color gradient as a Symbol") + end + z = sqrt.((1:15)*reshape(1:20,1,:)) + seriestype := :heatmap + ticks := nothing + legend := false + seriescolor := grad.args[1] + title := string(grad.args[1]) + z end diff --git a/src/series.jl b/src/series.jl index f7233c1a..9af76774 100644 --- a/src/series.jl +++ b/src/series.jl @@ -6,9 +6,12 @@ # This should cut down on boilerplate code and allow more focused dispatch on type # note: returns meta information... mainly for use with automatic labeling from DataFrames for now -typealias FuncOrFuncs{F} Union{F, Vector{F}, Matrix{F}} +const FuncOrFuncs{F} = Union{F, Vector{F}, Matrix{F}} -all3D(d::KW) = trueOrAllTrue(st -> st in (:contour, :contourf, :heatmap, :surface, :wireframe, :contour3d, :image), get(d, :seriestype, :none)) +all3D(d::KW) = trueOrAllTrue(st -> st in (:contour, :contourf, :heatmap, :surface, :wireframe, :contour3d, :image, :plots_heatmap), get(d, :seriestype, :none)) + +# unknown +convertToAnyVector(x, d::KW) = error("No user recipe defined for $(typeof(x))") # missing convertToAnyVector(v::Void, d::KW) = Any[nothing], nothing @@ -17,10 +20,10 @@ convertToAnyVector(v::Void, d::KW) = Any[nothing], nothing convertToAnyVector(n::Integer, d::KW) = Any[zeros(0) for i in 1:n], nothing # numeric vector -convertToAnyVector{T<:Number}(v::AVec{T}, d::KW) = Any[v], nothing +convertToAnyVector(v::AVec{T}, d::KW) where {T<:Number} = Any[v], nothing # string vector -convertToAnyVector{T<:AbstractString}(v::AVec{T}, d::KW) = Any[v], nothing +convertToAnyVector(v::AVec{T}, d::KW) where {T<:AbstractString} = Any[v], nothing function convertToAnyVector(v::AMat, d::KW) if all3D(d) @@ -43,7 +46,7 @@ convertToAnyVector(v::Volume, d::KW) = Any[v], nothing # convertToAnyVector(v::AVec{OHLC}, d::KW) = Any[v], nothing # # dates -# convertToAnyVector{D<:Union{Date,DateTime}}(dts::AVec{D}, d::KW) = Any[dts], nothing +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) @@ -96,8 +99,8 @@ nobigs(v) = v end # not allowed -compute_xyz{F<:Function}(x::Void, y::FuncOrFuncs{F}, z) = error("If you want to plot the function `$y`, you need to define the x values!") -compute_xyz{F<:Function}(x::Void, y::Void, z::FuncOrFuncs{F}) = error("If you want to plot the function `$z`, you need to define x and y values!") +compute_xyz(x::Void, y::FuncOrFuncs{F}, z) where {F<:Function} = error("If you want to plot the function `$y`, you need to define the x values!") +compute_xyz(x::Void, y::Void, z::FuncOrFuncs{F}) where {F<:Function} = error("If you want to plot the function `$z`, you need to define x and y values!") compute_xyz(x::Void, y::Void, z::Void) = error("x/y/z are all nothing!") # -------------------------------------------------------------------- @@ -106,7 +109,7 @@ compute_xyz(x::Void, y::Void, z::Void) = error("x/y/z are all nothing!") # we are going to build recipes to do the processing and splitting of the args # ensure we dispatch to the slicer -immutable SliceIt end +struct SliceIt end # the catch-all recipes @recipe function f(::Type{SliceIt}, x, y, z) @@ -125,18 +128,26 @@ immutable SliceIt end z = z.data end - xs, _ = convertToAnyVector(x, d) - ys, _ = convertToAnyVector(y, d) - zs, _ = convertToAnyVector(z, d) + xs, _ = convertToAnyVector(x, plotattributes) + ys, _ = convertToAnyVector(y, plotattributes) + zs, _ = convertToAnyVector(z, plotattributes) - fr = pop!(d, :fillrange, nothing) + fr = pop!(plotattributes, :fillrange, nothing) fillranges, _ = if typeof(fr) <: Number ([fr],nothing) else - convertToAnyVector(fr, d) + convertToAnyVector(fr, plotattributes) end mf = length(fillranges) + rib = pop!(plotattributes, :ribbon, nothing) + ribbons, _ = if typeof(rib) <: Number + ([fr],nothing) + else + convertToAnyVector(rib, plotattributes) + end + mr = length(ribbons) + # @show zs mx = length(xs) @@ -145,7 +156,7 @@ immutable SliceIt end if mx > 0 && my > 0 && mz > 0 for i in 1:max(mx, my, mz) # add a new series - di = copy(d) + di = copy(plotattributes) xi, yi, zi = xs[mod1(i,mx)], ys[mod1(i,my)], zs[mod1(i,mz)] di[:x], di[:y], di[:z] = compute_xyz(xi, yi, zi) @@ -153,6 +164,10 @@ immutable SliceIt end fr = fillranges[mod1(i,mf)] di[:fillrange] = isa(fr, Function) ? map(fr, di[:x]) : fr + # handle ribbons + rib = ribbons[mod1(i,mr)] + di[:ribbon] = isa(rib, Function) ? map(rib, di[:x]) : rib + push!(series_list, RecipeData(di, ())) end end @@ -160,10 +175,10 @@ immutable SliceIt end end # this is the default "type recipe"... just pass the object through -@recipe f{T<:Any}(::Type{T}, v::T) = v +@recipe f(::Type{T}, v::T) where {T<:Any} = v # this should catch unhandled "series recipes" and error with a nice message -@recipe f{V<:Val}(::Type{V}, x, y, z) = error("The backend must not support the series type $V, and there isn't a series recipe defined.") +@recipe f(::Type{V}, x, y, z) where {V<:Val} = error("The backend must not support the series type $V, and there isn't a series recipe defined.") _apply_type_recipe(d, v) = RecipesBase.apply_recipe(d, typeof(v), v)[1].args[1] @@ -171,6 +186,7 @@ _apply_type_recipe(d, v) = RecipesBase.apply_recipe(d, typeof(v), v)[1].args[1] # This sort of recipe should return a pair of functions... one to convert to number, # and one to format tick values. function _apply_type_recipe(d, v::AbstractArray) + isempty(v) && return Float64[] args = RecipesBase.apply_recipe(d, typeof(v[1]), v[1])[1].args if length(args) == 2 && typeof(args[1]) <: Function && typeof(args[2]) <: Function numfunc, formatter = args @@ -197,16 +213,16 @@ end # end # don't do anything for ints or floats -_apply_type_recipe{T<:Union{Integer,AbstractFloat}}(d, v::AbstractArray{T}) = v +_apply_type_recipe(d, v::AbstractArray{T}) where {T<:Union{Integer,AbstractFloat}} = v # handle "type recipes" by converting inputs, and then either re-calling or slicing @recipe function f(x, y, z) did_replace = false - newx = _apply_type_recipe(d, x) + newx = _apply_type_recipe(plotattributes, x) x === newx || (did_replace = true) - newy = _apply_type_recipe(d, y) + newy = _apply_type_recipe(plotattributes, y) y === newy || (did_replace = true) - newz = _apply_type_recipe(d, z) + newz = _apply_type_recipe(plotattributes, z) z === newz || (did_replace = true) if did_replace newx, newy, newz @@ -216,9 +232,9 @@ _apply_type_recipe{T<:Union{Integer,AbstractFloat}}(d, v::AbstractArray{T}) = v end @recipe function f(x, y) did_replace = false - newx = _apply_type_recipe(d, x) + newx = _apply_type_recipe(plotattributes, x) x === newx || (did_replace = true) - newy = _apply_type_recipe(d, y) + newy = _apply_type_recipe(plotattributes, y) y === newy || (did_replace = true) if did_replace newx, newy @@ -227,7 +243,7 @@ end end end @recipe function f(y) - newy = _apply_type_recipe(d, y) + newy = _apply_type_recipe(plotattributes, y) if y !== newy newy else @@ -240,7 +256,7 @@ end @recipe function f(v1, v2, v3, v4, vrest...) did_replace = false newargs = map(v -> begin - newv = _apply_type_recipe(d, v) + newv = _apply_type_recipe(plotattributes, v) if newv !== v did_replace = true end @@ -267,13 +283,13 @@ function wrap_surfaces(d::KW) end end -@recipe f(n::Integer) = is3d(get(d,:seriestype,:path)) ? (SliceIt, n, n, n) : (SliceIt, n, n, nothing) +@recipe f(n::Integer) = is3d(get(plotattributes,:seriestype,:path)) ? (SliceIt, n, n, n) : (SliceIt, n, n, nothing) # return a surface if this is a 3d plot, otherwise let it be sliced up -@recipe function f{T<:Union{Integer,AbstractFloat}}(mat::AMat{T}) - if all3D(d) +@recipe function f(mat::AMat{T}) where T<:Union{Integer,AbstractFloat} + if all3D(plotattributes) n,m = size(mat) - wrap_surfaces(d) + wrap_surfaces(plotattributes) SliceIt, 1:m, 1:n, Surface(mat) else SliceIt, nothing, mat, nothing @@ -281,11 +297,11 @@ end end # if a matrix is wrapped by Formatted, do similar logic, but wrap data with Surface -@recipe function f{T<:AbstractMatrix}(fmt::Formatted{T}) - if all3D(d) +@recipe function f(fmt::Formatted{T}) where T<:AbstractMatrix + if all3D(plotattributes) mat = fmt.data n,m = size(mat) - wrap_surfaces(d) + wrap_surfaces(plotattributes) SliceIt, 1:m, 1:n, Formatted(Surface(mat), fmt.formatter) else SliceIt, nothing, fmt, nothing @@ -293,7 +309,7 @@ end end # assume this is a Volume, so construct one -@recipe function f{T<:Number}(vol::AbstractArray{T,3}, args...) +@recipe function f(vol::AbstractArray{T,3}, args...) where T<:Number seriestype := :volume SliceIt, nothing, Volume(vol, args...), nothing end @@ -301,14 +317,16 @@ end # # images - grays -@recipe function f{T<:Gray}(mat::AMat{T}) +@recipe function f(mat::AMat{T}) where T<:Gray + n, m = size(mat) if is_seriestype_supported(:image) seriestype := :image - n, m = size(mat) + yflip --> true SliceIt, 1:m, 1:n, Surface(mat) else seriestype := :heatmap yflip --> true + cbar --> false fillcolor --> ColorGradient([:black, :white]) SliceIt, 1:m, 1:n, Surface(convert(Matrix{Float64}, mat)) end @@ -316,15 +334,18 @@ end # # images - colors -@recipe function f{T<:Colorant}(mat::AMat{T}) +@recipe function f(mat::AMat{T}) where T<:Colorant + n, m = size(mat) + if is_seriestype_supported(:image) seriestype := :image - n, m = size(mat) + yflip --> true SliceIt, 1:m, 1:n, Surface(mat) else seriestype := :heatmap yflip --> true - z, d[:fillcolor] = replace_image_with_heatmap(mat) + cbar --> false + z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat) SliceIt, 1:m, 1:n, Surface(z) end end @@ -353,16 +374,36 @@ end # function without range... use the current range of the x-axis -@recipe function f{F<:Function}(f::FuncOrFuncs{F}) - plt = d[:plot_object] +@recipe function f(f::FuncOrFuncs{F}) where F<:Function + plt = plotattributes[:plot_object] xmin, xmax = try axis_limits(plt[1][:xaxis]) catch - -5, 5 + xm = tryrange(f, [-5,-1,0,0.01]) + xm, tryrange(f, filter(x->x>xm, [5,1,0.99, 0, -0.01])) end + f, xmin, xmax end +# try some intervals over which the function may be defined +function tryrange(F::AbstractArray, vec) + rets = [tryrange(f, vec) for f in F] # get the preferred for each + maxind = maximum(indexin(rets, vec)) # get the last attempt that succeeded (most likely to fit all) + rets .= [tryrange(f, vec[maxind:maxind]) for f in F] # ensure that all functions compute there + rets[1] +end + +function tryrange(F, vec) + for v in vec + try + tmp = F(v) + return v + catch + end + end + error("$F is not a Function, or is not defined at any of the values $vec") +end # # # -------------------------------------------------------------------- # # 2 arguments @@ -372,7 +413,7 @@ end # # if functions come first, just swap the order (not to be confused with parametric functions... # # as there would be more than one function passed in) -@recipe function f{F<:Function}(f::FuncOrFuncs{F}, x) +@recipe function f(f::FuncOrFuncs{F}, x) where F<:Function F2 = typeof(x) @assert !(F2 <: Function || (F2 <: AbstractArray && F2.parameters[1] <: Function)) # otherwise we'd hit infinite recursion here x, f @@ -403,7 +444,7 @@ end # seriestype := :path3d # end # end - wrap_surfaces(d) + wrap_surfaces(plotattributes) SliceIt, x, y, z end @@ -413,7 +454,7 @@ end @recipe function f(x::AVec, y::AVec, zf::Function) # x = X <: Number ? sort(x) : x # y = Y <: Number ? sort(y) : y - wrap_surfaces(d) + wrap_surfaces(plotattributes) SliceIt, x, y, Surface(zf, x, y) # TODO: replace with SurfaceFunction when supported end @@ -421,13 +462,45 @@ end # # surface-like... matrix grid @recipe function f(x::AVec, y::AVec, z::AMat) - if !like_surface(get(d, :seriestype, :none)) - d[:seriestype] = :contour + if !like_surface(get(plotattributes, :seriestype, :none)) + plotattributes[:seriestype] = :contour end - wrap_surfaces(d) + wrap_surfaces(plotattributes) SliceIt, x, y, Surface(z) end +# # images - grays + +@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where T<:Gray + if is_seriestype_supported(:image) + seriestype := :image + yflip --> true + SliceIt, x, y, Surface(mat) + else + seriestype := :heatmap + yflip --> true + cbar --> false + fillcolor --> ColorGradient([:black, :white]) + SliceIt, x, y, Surface(convert(Matrix{Float64}, mat)) + end +end + +# # images - colors + +@recipe function f(x::AVec, y::AVec, mat::AMat{T}) where T<:Colorant + if is_seriestype_supported(:image) + seriestype := :image + yflip --> true + SliceIt, x, y, Surface(mat) + else + seriestype := :heatmap + yflip --> true + cbar --> false + z, plotattributes[:fillcolor] = replace_image_with_heatmap(mat) + SliceIt, x, y, Surface(z) + end +end + # # # # -------------------------------------------------------------------- @@ -440,19 +513,19 @@ end xs = adapted_grid(f, (xmin, xmax)) xs, f end -@recipe function f{F<:Function}(fs::AbstractArray{F}, xmin::Number, xmax::Number) +@recipe function f(fs::AbstractArray{F}, xmin::Number, xmax::Number) where F<:Function xs = Any[adapted_grid(f, (xmin, xmax)) for f in fs] xs, fs end -@recipe f{F<:Function,G<:Function}(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, u::AVec) = mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u) -@recipe f{F<:Function,G<:Function}(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, umin::Number, umax::Number, n = 200) = fx, fy, linspace(umin, umax, n) +@recipe f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, u::AVec) where {F<:Function,G<:Function} = mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u) +@recipe f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, umin::Number, umax::Number, n = 200) where {F<:Function,G<:Function} = fx, fy, linspace(umin, umax, n) # # # special handling... 3D parametric function(s) -@recipe function f{F<:Function,G<:Function,H<:Function}(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, fz::FuncOrFuncs{H}, u::AVec) +@recipe function f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, fz::FuncOrFuncs{H}, u::AVec) where {F<:Function,G<:Function,H<:Function} mapFuncOrFuncs(fx, u), mapFuncOrFuncs(fy, u), mapFuncOrFuncs(fz, u) end -@recipe function f{F<:Function,G<:Function,H<:Function}(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, fz::FuncOrFuncs{H}, umin::Number, umax::Number, numPoints = 200) +@recipe function f(fx::FuncOrFuncs{F}, fy::FuncOrFuncs{G}, fz::FuncOrFuncs{H}, umin::Number, umax::Number, numPoints = 200) where {F<:Function,G<:Function,H<:Function} fx, fy, fz, linspace(umin, umax, numPoints) end @@ -467,28 +540,28 @@ end # # # (x,y) tuples -@recipe f{R1<:Number,R2<:Number}(xy::AVec{Tuple{R1,R2}}) = unzip(xy) -@recipe f{R1<:Number,R2<:Number}(xy::Tuple{R1,R2}) = [xy[1]], [xy[2]] +@recipe f(xy::AVec{Tuple{R1,R2}}) where {R1<:Number,R2<:Number} = unzip(xy) +@recipe f(xy::Tuple{R1,R2}) where {R1<:Number,R2<:Number} = [xy[1]], [xy[2]] # # # (x,y,z) tuples -@recipe f{R1<:Number,R2<:Number,R3<:Number}(xyz::AVec{Tuple{R1,R2,R3}}) = unzip(xyz) -@recipe f{R1<:Number,R2<:Number,R3<:Number}(xyz::Tuple{R1,R2,R3}) = [xyz[1]], [xyz[2]], [xyz[3]] +@recipe f(xyz::AVec{Tuple{R1,R2,R3}}) where {R1<:Number,R2<:Number,R3<:Number} = unzip(xyz) +@recipe f(xyz::Tuple{R1,R2,R3}) where {R1<:Number,R2<:Number,R3<:Number} = [xyz[1]], [xyz[2]], [xyz[3]] # these might be points+velocity, or OHLC or something else -@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(xyuv::AVec{Tuple{R1,R2,R3,R4}}) = get(d,:seriestype,:path)==:ohlc ? OHLC[OHLC(t...) for t in xyuv] : unzip(xyuv) -@recipe f{R1<:Number,R2<:Number,R3<:Number,R4<:Number}(xyuv::Tuple{R1,R2,R3,R4}) = [xyuv[1]], [xyuv[2]], [xyuv[3]], [xyuv[4]] +@recipe f(xyuv::AVec{Tuple{R1,R2,R3,R4}}) where {R1<:Number,R2<:Number,R3<:Number,R4<:Number} = get(plotattributes,:seriestype,:path)==:ohlc ? OHLC[OHLC(t...) for t in xyuv] : unzip(xyuv) +@recipe f(xyuv::Tuple{R1,R2,R3,R4}) where {R1<:Number,R2<:Number,R3<:Number,R4<:Number} = [xyuv[1]], [xyuv[2]], [xyuv[3]], [xyuv[4]] # # # 2D FixedSizeArrays -@recipe f{T<:Number}(xy::AVec{FixedSizeArrays.Vec{2,T}}) = unzip(xy) -@recipe f{T<:Number}(xy::FixedSizeArrays.Vec{2,T}) = [xy[1]], [xy[2]] +@recipe f(xy::AVec{FixedSizeArrays.Vec{2,T}}) where {T<:Number} = unzip(xy) +@recipe f(xy::FixedSizeArrays.Vec{2,T}) where {T<:Number} = [xy[1]], [xy[2]] # # # 3D FixedSizeArrays -@recipe f{T<:Number}(xyz::AVec{FixedSizeArrays.Vec{3,T}}) = unzip(xyz) -@recipe f{T<:Number}(xyz::FixedSizeArrays.Vec{3,T}) = [xyz[1]], [xyz[2]], [xyz[3]] +@recipe f(xyz::AVec{FixedSizeArrays.Vec{3,T}}) where {T<:Number} = unzip(xyz) +@recipe f(xyz::FixedSizeArrays.Vec{3,T}) where {T<:Number} = [xyz[1]], [xyz[2]], [xyz[3]] # # # -------------------------------------------------------------------- @@ -508,13 +581,69 @@ end # nothing # end +splittable_kw(key, val, lengthGroup) = false +splittable_kw(key, val::AbstractArray, lengthGroup) = (key != :group) && size(val,1) == lengthGroup +splittable_kw(key, val::Tuple, lengthGroup) = all(splittable_kw.(key, val, lengthGroup)) +splittable_kw(key, val::SeriesAnnotations, lengthGroup) = splittable_kw(key, val.strs, lengthGroup) + +split_kw(key, val::AbstractArray, indices) = val[indices, fill(Colon(), ndims(val)-1)...] +split_kw(key, val::Tuple, indices) = Tuple(split_kw(key, v, indices) for v in val) +function split_kw(key, val::SeriesAnnotations, indices) + split_strs = split_kw(key, val.strs, indices) + return SeriesAnnotations(split_strs, val.font, val.baseshape, val.scalefactor) +end + +function groupedvec2mat(x_ind, x, y::AbstractArray, groupby, def_val = y[1]) + y_mat = Array{promote_type(eltype(y), typeof(def_val))}(length(keys(x_ind)), length(groupby.groupLabels)) + fill!(y_mat, def_val) + for i in 1:length(groupby.groupLabels) + xi = x[groupby.groupIds[i]] + yi = y[groupby.groupIds[i]] + y_mat[getindex.(x_ind, xi), i] = yi + end + return y_mat +end + +groupedvec2mat(x_ind, x, y::Tuple, groupby) = Tuple(groupedvec2mat(x_ind, x, v, groupby) for v in y) + +group_as_matrix(t) = false + # split the group into 1 series per group, and set the label and idxfilter for each @recipe function f(groupby::GroupBy, args...) - for (i,glab) in enumerate(groupby.groupLabels) - @series begin - label --> string(glab) - idxfilter --> groupby.groupIds[i] - args + lengthGroup = maximum(union(groupby.groupIds...)) + if !(group_as_matrix(args[1])) + for (i,glab) in enumerate(groupby.groupLabels) + @series begin + label --> string(glab) + idxfilter --> groupby.groupIds[i] + for (key,val) in plotattributes + if splittable_kw(key, val, lengthGroup) + :($key) := split_kw(key, val, groupby.groupIds[i]) + end + end + args + end end + else + g = args[1] + if length(g.args) == 1 + x = zeros(Int, lengthGroup) + for indexes in groupby.groupIds + x[indexes] = 1:length(indexes) + end + last_args = g.args + else + x = g.args[1] + last_args = g.args[2:end] + end + x_u = unique(x) + x_ind = Dict(zip(x_u, 1:length(x_u))) + for (key,val) in plotattributes + if splittable_kw(key, val, lengthGroup) + :($key) := groupedvec2mat(x_ind, x, val, groupby) + end + end + label --> reshape(groupby.groupLabels, 1, :) + typeof(g)((x_u, (groupedvec2mat(x_ind, x, arg, groupby, NaN) for arg in last_args)...)) end end diff --git a/src/subplots.jl b/src/subplots.jl index 3400874c..a47bc8ee 100644 --- a/src/subplots.jl +++ b/src/subplots.jl @@ -1,6 +1,6 @@ -function Subplot{T<:AbstractBackend}(::T; parent = RootLayout()) +function Subplot(::T; parent = RootLayout()) where T<:AbstractBackend Subplot{T}( parent, Series[], @@ -13,6 +13,11 @@ function Subplot{T<:AbstractBackend}(::T; parent = RootLayout()) ) end +""" + plotarea(subplot) + +Return the bounding box of a subplot +""" plotarea(sp::Subplot) = sp.plotarea plotarea!(sp::Subplot, bbox::BoundingBox) = (sp.plotarea = bbox) @@ -32,14 +37,14 @@ get_subplot(plt::Plot, k) = plt.spmap[k] get_subplot(series::Series) = series.d[:subplot] get_subplot_index(plt::Plot, idx::Integer) = Int(idx) -get_subplot_index(plt::Plot, sp::Subplot) = findfirst(_ -> _ === sp, plt.subplots) +get_subplot_index(plt::Plot, sp::Subplot) = findfirst(x -> x === sp, plt.subplots) series_list(sp::Subplot) = sp.series_list # filter(series -> series.d[:subplot] === sp, sp.plt.series_list) function should_add_to_legend(series::Series) series.d[:primary] && series.d[:label] != "" && !(series.d[:seriestype] in ( - :hexbin,:histogram2d,:hline,:vline, + :hexbin,:bins2d,:histogram2d,:hline,:vline, :contour,:contourf,:contour3d,:surface,:wireframe, :heatmap, :pie, :image )) diff --git a/src/themes.jl b/src/themes.jl index 3429585a..c2b2aee1 100644 --- a/src/themes.jl +++ b/src/themes.jl @@ -1,40 +1,168 @@ +""" + theme(s::Symbol) +Specify the colour theme for plots. +""" 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 + defaults = _get_defaults(s) + _theme(s, defaults; kw...) +end + +function _get_defaults(s::Symbol) + thm = PlotThemes._themes[s] + if :defaults in fieldnames(thm) + return thm.defaults + else # old PlotTheme type + defaults = KW( + :bg => thm.bg_secondary, + :bginside => thm.bg_primary, + :fg => thm.lines, + :fgtext => thm.text, + :fgguide => thm.text, + :fglegend => thm.text, + :palette => thm.palette, ) - return + if thm.gradient != nothing + push!(defaults, :gradient => thm.gradient) + end + return defaults + end +end + +function _theme(s::Symbol, defaults::KW; kw...) + # Reset to defaults to overwrite active theme + reset_defaults() + + # Set the theme's gradient as default + if haskey(defaults, :gradient) + PlotUtils.clibrary(:misc) + PlotUtils.default_cgrad(default = :sequential, sequential = PlotThemes.gradient_name(s)) + else + PlotUtils.clibrary(:Plots) + PlotUtils.default_cgrad(default = :sequential, sequential = :inferno) end - # update the default gradient and other defaults - thm = PlotThemes._themes[s] - if thm.gradient != nothing - PlotUtils._default_gradient[] = PlotThemes.gradient_name(s) + # maybe overwrite the theme's gradient + kw = KW(kw) + if haskey(kw, :gradient) + kwgrad = pop!(kw, :gradient) + for clib in clibraries() + if kwgrad in cgradients(clib) + PlotUtils.clibrary(clib) + PlotUtils.default_cgrad(default = :sequential, sequential = kwgrad) + break + end + end 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... - ) + + # Set the theme's defaults + default(; defaults..., kw...) + return end @deprecate set_theme(s) theme(s) + +@userplot ShowTheme + +_color_functions = KW( + :protanopic => protanopic, + :deuteranopic => deuteranopic, + :tritanopic => tritanopic, +) +_get_showtheme_args(thm::Symbol) = thm, identity +_get_showtheme_args(thm::Symbol, func::Symbol) = thm, get(_color_functions, func, identity) + +@recipe function showtheme(st::ShowTheme) + thm, cfunc = _get_showtheme_args(st.args...) + defaults = _get_defaults(thm) + + # get the gradient + gradient_colors = get(defaults, :gradient, cgrad(:inferno).colors) + gradient = cgrad(cfunc.(RGB.(gradient_colors))) + + # get the palette + palette = get(defaults, :palette, get_color_palette(:auto, plot_color(:white), 17)) + palette = cfunc.(RGB.(palette)) + + # apply the theme + for k in keys(defaults) + k in (:gradient, :palette) && continue + def = defaults[k] + arg = get(_keyAliases, k, k) + plotattributes[arg] = if typeof(def) <: Colorant + cfunc(RGB(def)) + elseif eltype(def) <: Colorant + cfunc.(RGB.(def)) + elseif contains(string(arg), "color") + cfunc.(RGB.(plot_color.(def))) + else + def + end + end + + srand(1) + + label := "" + colorbar := false + layout := (2, 3) + + for j in 1:4 + @series begin + subplot := 1 + palette := palette + seriestype := :path + cumsum(randn(50)) + end + + @series begin + subplot := 2 + seriestype := :scatter + palette := palette + marker := (:circle, :diamond, :star5, :square)[j] + randn(10), randn(10) + end + end + + @series begin + subplot := 3 + seriestype := :histogram + palette := palette + randn(1000) .+ (0:2:4)' + end + + f(r) = sin(r) / r + _norm(x, y) = norm([x, y]) + x = y = linspace(-3π, 3π, 30) + z = f.(_norm.(x, y')) + wi = 2:3:30 + + @series begin + subplot := 4 + seriestype := :heatmap + seriescolor := gradient + ticks := -5:5:5 + x, y, z + end + + @series begin + subplot := 5 + seriestype := :surface + seriescolor := gradient + x, y, z + end + + n = 100 + ts = linspace(0, 10π, n) + x = ts .* cos.(ts) + y = (0.1ts) .* sin.(ts) + z = 1:n + + @series begin + subplot := 6 + seriescolor := gradient + linewidth := 3 + line_z := z + x, y, z + end + +end diff --git a/src/types.jl b/src/types.jl index 0f1e4909..bd53715b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -2,28 +2,24 @@ # TODO: I declare lots of types here because of the lacking ability to do forward declarations in current Julia # I should move these to the relevant files when something like "extern" is implemented -typealias AVec AbstractVector -typealias AMat AbstractMatrix -typealias KW Dict{Symbol,Any} +const AVec = AbstractVector +const AMat = AbstractMatrix +const KW = Dict{Symbol,Any} -immutable PlotsDisplay <: Display end - -abstract AbstractBackend -abstract AbstractPlot{T<:AbstractBackend} -abstract AbstractLayout +struct PlotsDisplay <: Display end # ----------------------------------------------------------- -immutable InputWrapper{T} +struct InputWrapper{T} obj::T end -wrap{T}(obj::T) = InputWrapper{T}(obj) +wrap(obj::T) where {T} = InputWrapper{T}(obj) Base.isempty(wrapper::InputWrapper) = false # ----------------------------------------------------------- -type Series +mutable struct Series d::KW end @@ -33,7 +29,7 @@ attr!(series::Series, v, k::Symbol) = (series.d[k] = v) # ----------------------------------------------------------- # a single subplot -type Subplot{T<:AbstractBackend} <: AbstractLayout +mutable struct Subplot{T<:AbstractBackend} <: AbstractLayout parent::AbstractLayout series_list::Vector{Series} # arguments for each series minpad::Tuple # leftpad, toppad, rightpad, bottompad @@ -49,12 +45,12 @@ Base.show(io::IO, sp::Subplot) = print(io, "Subplot{$(sp[:subplot_index])}") # ----------------------------------------------------------- # simple wrapper around a KW so we can hold all attributes pertaining to the axis in one place -type Axis +mutable struct Axis sps::Vector{Subplot} d::KW end -type Extrema +mutable struct Extrema emin::Float64 emax::Float64 end @@ -62,12 +58,12 @@ Extrema() = Extrema(Inf, -Inf) # ----------------------------------------------------------- -typealias SubplotMap Dict{Any, Subplot} +const SubplotMap = Dict{Any, Subplot} # ----------------------------------------------------------- -type Plot{T<:AbstractBackend} <: AbstractPlot{T} +mutable struct Plot{T<:AbstractBackend} <: AbstractPlot{T} backend::T # the backend type n::Int # number of series attr::KW # arguments for the whole plot diff --git a/src/utils.jl b/src/utils.jl index da1d1cb6..2334d96a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -3,7 +3,7 @@ calcMidpoints(edges::AbstractVector) = Float64[0.5 * (edges[i] + edges[i+1]) for "Make histogram-like bins of data" function binData(data, nbins) - lo, hi = extrema(data) + lo, hi = ignorenan_extrema(data) edges = collect(linspace(lo, hi, nbins+1)) midpoints = calcMidpoints(edges) buckets = Int[max(2, min(searchsortedfirst(edges, x), length(edges)))-1 for x in data] @@ -109,12 +109,12 @@ function regressionXY(x, y) β, α = convert(Matrix{Float64}, [x ones(length(x))]) \ convert(Vector{Float64}, y) # make a line segment - regx = [minimum(x), maximum(x)] + regx = [ignorenan_minimum(x), ignorenan_maximum(x)] regy = β * regx + α regx, regy end -function replace_image_with_heatmap{T<:Colorant}(z::Array{T}) +function replace_image_with_heatmap(z::Array{T}) where T<:Colorant @show T, size(z) n, m = size(z) # idx = 0 @@ -137,15 +137,15 @@ function imageHack(d::KW) end # --------------------------------------------------------------- - -type Segments{T} +"Build line segments for plotting" +mutable struct Segments{T} pts::Vector{T} end # Segments() = Segments{Float64}(zeros(0)) Segments() = Segments(Float64) -Segments{T}(::Type{T}) = Segments(T[]) +Segments(::Type{T}) where {T} = Segments(T[]) Segments(p::Int) = Segments(NTuple{2,Float64}[]) @@ -157,7 +157,7 @@ to_nan(::Type{NTuple{2,Float64}}) = (NaN, NaN) coords(segs::Segments{Float64}) = segs.pts coords(segs::Segments{NTuple{2,Float64}}) = Float64[p[1] for p in segs.pts], Float64[p[2] for p in segs.pts] -function Base.push!{T}(segments::Segments{T}, vs...) +function Base.push!(segments::Segments{T}, vs...) where T if !isempty(segments.pts) push!(segments.pts, to_nan(T)) end @@ -167,7 +167,7 @@ function Base.push!{T}(segments::Segments{T}, vs...) segments end -function Base.push!{T}(segments::Segments{T}, vs::AVec) +function Base.push!(segments::Segments{T}, vs::AVec) where T if !isempty(segments.pts) push!(segments.pts, to_nan(T)) end @@ -181,24 +181,43 @@ end # ----------------------------------------------------- # helper to manage NaN-separated segments -type SegmentsIterator +mutable struct SegmentsIterator args::Tuple n::Int end + function iter_segments(args...) tup = Plots.wraptuple(args) n = maximum(map(length, tup)) SegmentsIterator(tup, n) end +function iter_segments(series::Series) + x, y, z = series[:x], series[:y], series[:z] + if has_attribute_segments(series) + if series[:seriestype] in (:scatter, :scatter3d) + return [[i] for i in 1:length(y)] + else + return [i:(i + 1) for i in 1:(length(y) - 1)] + end + else + segs = UnitRange{Int}[] + args = is3d(series) ? (x, y, z) : (x, y) + for seg in iter_segments(args...) + push!(segs, seg) + end + return segs + end +end + # helpers to figure out if there are NaN values in a list of array types -anynan(i::Int, args::Tuple) = any(a -> !isfinite(cycle(a,i)), args) +anynan(i::Int, args::Tuple) = any(a -> try isnan(_cycle(a,i)) catch MethodError false end, args) anynan(istart::Int, iend::Int, args::Tuple) = any(i -> anynan(i, args), istart:iend) allnan(istart::Int, iend::Int, args::Tuple) = all(i -> anynan(i, args), istart:iend) function Base.start(itr::SegmentsIterator) nextidx = 1 - if anynan(1, itr.args) + if !any(isempty,itr.args) && anynan(1, itr.args) _, nextidx = next(itr, 1) end nextidx @@ -231,8 +250,8 @@ 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 +float_extended_type(x::AbstractArray{T}) where {T} = Union{T,Float64} +float_extended_type(x::AbstractArray{T}) where {T<:Real} = Float64 # ------------------------------------------------------------------------------------ @@ -243,56 +262,56 @@ notimpl() = error("This has not been implemented yet") isnothing(x::Void) = true isnothing(x) = false -cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj -cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj +_cycle(wrapper::InputWrapper, idx::Int) = wrapper.obj +_cycle(wrapper::InputWrapper, idx::AVec{Int}) = wrapper.obj -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 +_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 -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(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) +_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] +makevec(v::T) where {T} = T[v] "duplicate a single value, or pass the 2-tuple through" maketuple(x::Real) = (x,x) -maketuple{T,S}(x::Tuple{T,S}) = x +maketuple(x::Tuple{T,S}) where {T,S} = x mapFuncOrFuncs(f::Function, u::AVec) = map(f, u) -mapFuncOrFuncs{F<:Function}(fs::AVec{F}, u::AVec) = [map(f, u) for f in fs] +mapFuncOrFuncs(fs::AVec{F}, u::AVec) where {F<:Function} = [map(f, u) for f in fs] -unzip{X,Y}(xy::AVec{Tuple{X,Y}}) = [t[1] for t in xy], [t[2] for t in xy] -unzip{X,Y,Z}(xyz::AVec{Tuple{X,Y,Z}}) = [t[1] for t in xyz], [t[2] for t in xyz], [t[3] for t in xyz] -unzip{X,Y,U,V}(xyuv::AVec{Tuple{X,Y,U,V}}) = [t[1] for t in xyuv], [t[2] for t in xyuv], [t[3] for t in xyuv], [t[4] for t in xyuv] +unzip(xy::AVec{Tuple{X,Y}}) where {X,Y} = [t[1] for t in xy], [t[2] for t in xy] +unzip(xyz::AVec{Tuple{X,Y,Z}}) where {X,Y,Z} = [t[1] for t in xyz], [t[2] for t in xyz], [t[3] for t in xyz] +unzip(xyuv::AVec{Tuple{X,Y,U,V}}) where {X,Y,U,V} = [t[1] for t in xyuv], [t[2] for t in xyuv], [t[3] for t in xyuv], [t[4] for t in xyuv] -unzip{T}(xy::AVec{FixedSizeArrays.Vec{2,T}}) = T[t[1] for t in xy], T[t[2] for t in xy] -unzip{T}(xy::FixedSizeArrays.Vec{2,T}) = T[xy[1]], T[xy[2]] +unzip(xy::AVec{FixedSizeArrays.Vec{2,T}}) where {T} = T[t[1] for t in xy], T[t[2] for t in xy] +unzip(xy::FixedSizeArrays.Vec{2,T}) where {T} = T[xy[1]], T[xy[2]] -unzip{T}(xyz::AVec{FixedSizeArrays.Vec{3,T}}) = T[t[1] for t in xyz], T[t[2] for t in xyz], T[t[3] for t in xyz] -unzip{T}(xyz::FixedSizeArrays.Vec{3,T}) = T[xyz[1]], T[xyz[2]], T[xyz[3]] +unzip(xyz::AVec{FixedSizeArrays.Vec{3,T}}) where {T} = T[t[1] for t in xyz], T[t[2] for t in xyz], T[t[3] for t in xyz] +unzip(xyz::FixedSizeArrays.Vec{3,T}) where {T} = T[xyz[1]], T[xyz[2]], T[xyz[3]] -unzip{T}(xyuv::AVec{FixedSizeArrays.Vec{4,T}}) = T[t[1] for t in xyuv], T[t[2] for t in xyuv], T[t[3] for t in xyuv], T[t[4] for t in xyuv] -unzip{T}(xyuv::FixedSizeArrays.Vec{4,T}) = T[xyuv[1]], T[xyuv[2]], T[xyuv[3]], T[xyuv[4]] +unzip(xyuv::AVec{FixedSizeArrays.Vec{4,T}}) where {T} = T[t[1] for t in xyuv], T[t[2] for t in xyuv], T[t[3] for t in xyuv], T[t[4] for t in xyuv] +unzip(xyuv::FixedSizeArrays.Vec{4,T}) where {T} = T[xyuv[1]], T[xyuv[2]], T[xyuv[3]], T[xyuv[4]] # given 2-element lims and a vector of data x, widen lims to account for the extrema of x function _expand_limits(lims, x) try - e1, e2 = extrema(x) - lims[1] = min(lims[1], e1) - lims[2] = max(lims[2], e2) + e1, e2 = ignorenan_extrema(x) + lims[1] = NaNMath.min(lims[1], e1) + lims[2] = NaNMath.max(lims[2], e2) # catch err # warn(err) end nothing end -expand_data(v, n::Integer) = [cycle(v, i) for i=1:n] +expand_data(v, n::Integer) = [_cycle(v, i) for i=1:n] # if the type exists in a list, replace the first occurence. otherwise add it to the end function addOrReplace(v::AbstractVector, t::DataType, args...; kw...) @@ -324,7 +343,7 @@ function replaceAliases!(d::KW, aliases::Dict{Symbol,Symbol}) end end -createSegments(z) = collect(repmat(z',2,1))[2:end] +createSegments(z) = collect(repmat(reshape(z,1,:),2,1))[2:end] Base.first(c::Colorant) = c Base.first(x::Symbol) = x @@ -332,32 +351,55 @@ Base.first(x::Symbol) = x sortedkeys(d::Dict) = sort(collect(keys(d))) -"create an (n+1) list of the outsides of heatmap rectangles" -function heatmap_edges(v::AVec) - vmin, vmax = extrema(v) - extra = 0.5 * (vmax-vmin) / (length(v)-1) - vcat(vmin-extra, 0.5 * (v[1:end-1] + v[2:end]), vmax+extra) + +const _scale_base = Dict{Symbol, Real}( + :log10 => 10, + :log2 => 2, + :ln => e, +) + +function _heatmap_edges(v::AVec) + vmin, vmax = ignorenan_extrema(v) + extra_min = (v[2] - v[1]) / 2 + extra_max = (v[end] - v[end - 1]) / 2 + vcat(vmin-extra_min, 0.5 * (v[1:end-1] + v[2:end]), vmax+extra_max) end +"create an (n+1) list of the outsides of heatmap rectangles" +function heatmap_edges(v::AVec, scale::Symbol = :identity) + f, invf = scalefunc(scale), invscalefunc(scale) + map(invf, _heatmap_edges(map(f,v))) +end function calc_r_extrema(x, y) - xmin, xmax = extrema(x) - ymin, ymax = extrema(y) - r = 0.5 * min(xmax - xmin, ymax - ymin) - extrema(r) + xmin, xmax = ignorenan_extrema(x) + ymin, ymax = ignorenan_extrema(y) + r = 0.5 * NaNMath.min(xmax - xmin, ymax - ymin) + ignorenan_extrema(r) end function convert_to_polar(x, y, r_extrema = calc_r_extrema(x, y)) rmin, rmax = r_extrema - phi, r = x, y - r = 0.5 * (r - rmin) / (rmax - rmin) - n = max(length(phi), length(r)) - x = zeros(n) - y = zeros(n) + theta, r = filter_radial_data(x, y, r_extrema) + r = (r - rmin) / (rmax - rmin) + x = r.*cos.(theta) + y = r.*sin.(theta) + x, y +end + +# Filters radial data for points within the axis limits +function filter_radial_data(theta, r, r_extrema::Tuple{Real, Real}) + n = max(length(theta), length(r)) + rmin, rmax = r_extrema + x, y = zeros(n), zeros(n) for i in 1:n - x[i] = cycle(r,i) * cos(cycle(phi,i)) - y[i] = cycle(r,i) * sin(cycle(phi,i)) + x[i] = _cycle(theta, i) + y[i] = _cycle(r, i) end + points = map((a, b) -> (a, b), x, y) + filter!(a -> a[2] >= rmin && a[2] <= rmax, points) + x = map(a -> a[1], points) + y = map(a -> a[2], points) x, y end @@ -374,7 +416,7 @@ isatom() = isdefined(Main, :Atom) && Main.Atom.isconnected() function is_installed(pkgstr::AbstractString) try - Pkg.installed(pkgstr) === nothing ? false: true + Pkg.installed(pkgstr) === nothing ? false : true catch false end @@ -396,20 +438,20 @@ isvertical(d::KW) = get(d, :orientation, :vertical) in (:vertical, :v, :vert) isvertical(series::Series) = isvertical(series.d) -ticksType{T<:Real}(ticks::AVec{T}) = :ticks -ticksType{T<:AbstractString}(ticks::AVec{T}) = :labels -ticksType{T<:AVec,S<:AVec}(ticks::Tuple{T,S}) = :ticks_and_labels +ticksType(ticks::AVec{T}) where {T<:Real} = :ticks +ticksType(ticks::AVec{T}) where {T<:AbstractString} = :labels +ticksType(ticks::Tuple{T,S}) where {T<:AVec,S<:AVec} = :ticks_and_labels ticksType(ticks) = :invalid -limsType{T<:Real,S<:Real}(lims::Tuple{T,S}) = :limits +limsType(lims::Tuple{T,S}) where {T<:Real,S<:Real} = :limits limsType(lims::Symbol) = lims == :auto ? :auto : :invalid limsType(lims) = :invalid # axis_Symbol(letter, postfix) = Symbol(letter * postfix) # axis_symbols(letter, postfix...) = map(s -> axis_Symbol(letter, s), postfix) -Base.convert{T<:Real}(::Type{Vector{T}}, rng::Range{T}) = T[x for x in rng] -Base.convert{T<:Real,S<:Real}(::Type{Vector{T}}, rng::Range{S}) = T[x for x in rng] +Base.convert(::Type{Vector{T}}, rng::Range{T}) where {T<:Real} = T[x for x in rng] +Base.convert(::Type{Vector{T}}, rng::Range{S}) where {T<:Real,S<:Real} = T[x for x in rng] Base.merge(a::AbstractVector, b::AbstractVector) = sort(unique(vcat(a,b))) @@ -469,7 +511,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, Base.cycle(rib))) + for (i, (yi, ri)) in enumerate(zip(y, Base.Iterators.cycle(rib))) frs[i] = yi + ri end frs @@ -480,16 +522,44 @@ function make_fillrange_from_ribbon(kw::KW) y, rib = kw[:y], kw[:ribbon] rib = wraptuple(rib) rib1, rib2 = -first(rib), last(rib) - kw[:ribbon] = nothing + # kw[:ribbon] = nothing kw[:fillrange] = make_fillrange_side(y, rib1), make_fillrange_side(y, rib2) + (get(kw, :fillalpha, nothing) == nothing) && (kw[:fillalpha] = 0.5) +end + +#turn tuple of fillranges to one path +function concatenate_fillrange(x,y::Tuple) + rib1, rib2 = first(y), last(y) + yline = vcat(rib1,(rib2)[end:-1:1]) + xline = vcat(x,x[end:-1:1]) + return xline, yline end function get_sp_lims(sp::Subplot, letter::Symbol) axis_limits(sp[Symbol(letter, :axis)]) end + +""" + xlims([plt]) + +Returns the x axis limits of the current plot or subplot +""" xlims(sp::Subplot) = get_sp_lims(sp, :x) + +""" + ylims([plt]) + +Returns the y axis limits of the current plot or subplot +""" ylims(sp::Subplot) = get_sp_lims(sp, :y) + +""" + zlims([plt]) + +Returns the z axis limits of the current plot or subplot +""" zlims(sp::Subplot) = get_sp_lims(sp, :z) + xlims(plt::Plot, sp_idx::Int = 1) = xlims(plt[sp_idx]) ylims(plt::Plot, sp_idx::Int = 1) = ylims(plt[sp_idx]) zlims(plt::Plot, sp_idx::Int = 1) = zlims(plt[sp_idx]) @@ -497,6 +567,136 @@ xlims(sp_idx::Int = 1) = xlims(current(), sp_idx) ylims(sp_idx::Int = 1) = ylims(current(), sp_idx) zlims(sp_idx::Int = 1) = zlims(current(), sp_idx) + +function get_clims(sp::Subplot) + zmin, zmax = Inf, -Inf + z_colored_series = (:contour, :contour3d, :heatmap, :histogram2d, :surface) + for series in series_list(sp) + for vals in (series[:seriestype] in z_colored_series ? series[:z] : nothing, series[:line_z], series[:marker_z], series[:fill_z]) + if (typeof(vals) <: AbstractSurface) && (eltype(vals.surf) <: Real) + zmin, zmax = _update_clims(zmin, zmax, ignorenan_extrema(vals.surf)...) + elseif (vals != nothing) && (eltype(vals) <: Real) + zmin, zmax = _update_clims(zmin, zmax, ignorenan_extrema(vals)...) + end + end + end + clims = sp[:clims] + if is_2tuple(clims) + isfinite(clims[1]) && (zmin = clims[1]) + isfinite(clims[2]) && (zmax = clims[2]) + end + return zmin < zmax ? (zmin, zmax) : (-0.1, 0.1) +end + +_update_clims(zmin, zmax, emin, emax) = min(zmin, emin), max(zmax, emax) + +function hascolorbar(series::Series) + st = series[:seriestype] + hascbar = st == :heatmap + if st == :contour + hascbar = (isscalar(series[:levels]) ? (series[:levels] > 1) : (length(series[:levels]) > 1)) && (length(unique(Array(series[:z]))) > 1) + end + if series[:marker_z] != nothing || series[:line_z] != nothing || series[:fill_z] != nothing + hascbar = true + end + # no colorbar if we are creating a surface LightSource + if xor(st == :surface, series[:fill_z] != nothing) + hascbar = true + end + return hascbar +end + +function hascolorbar(sp::Subplot) + cbar = sp[:colorbar] + hascbar = false + if cbar != :none + for series in series_list(sp) + if hascolorbar(series) + hascbar = true + end + end + end + hascbar +end + +function get_linecolor(series, i::Int = 1) + lc = series[:linecolor] + lz = series[:line_z] + if lz == nothing + isa(lc, ColorGradient) ? lc : plot_color(_cycle(lc, i)) + else + cmin, cmax = get_clims(series[:subplot]) + grad = isa(lc, ColorGradient) ? lc : cgrad() + grad[clamp((_cycle(lz, i) - cmin) / (cmax - cmin), 0, 1)] + end +end + +function get_linealpha(series, i::Int = 1) + _cycle(series[:linealpha], i) +end + +function get_linewidth(series, i::Int = 1) + _cycle(series[:linewidth], i) +end + +function get_linestyle(series, i::Int = 1) + _cycle(series[:linestyle], i) +end + +function get_fillcolor(series, i::Int = 1) + fc = series[:fillcolor] + fz = series[:fill_z] + if fz == nothing + isa(fc, ColorGradient) ? fc : plot_color(_cycle(fc, i)) + else + cmin, cmax = get_clims(series[:subplot]) + grad = isa(fc, ColorGradient) ? fc : cgrad() + grad[clamp((_cycle(fz, i) - cmin) / (cmax - cmin), 0, 1)] + end +end + +function get_fillalpha(series, i::Int = 1) + _cycle(series[:fillalpha], i) +end + +function get_markercolor(series, i::Int = 1) + mc = series[:markercolor] + mz = series[:marker_z] + if mz == nothing + isa(mc, ColorGradient) ? mc : plot_color(_cycle(mc, i)) + else + cmin, cmax = get_clims(series[:subplot]) + grad = isa(mc, ColorGradient) ? mc : cgrad() + grad[clamp((_cycle(mz, i) - cmin) / (cmax - cmin), 0, 1)] + end +end + +function get_markeralpha(series, i::Int = 1) + _cycle(series[:markeralpha], i) +end + +function get_markerstrokecolor(series, i::Int = 1) + msc = series[:markerstrokecolor] + isa(msc, ColorGradient) ? msc : _cycle(msc, i) +end + +function get_markerstrokealpha(series, i::Int = 1) + _cycle(series[:markerstrokealpha], i) +end + +function has_attribute_segments(series::Series) + # we want to check if a series needs to be split into segments just because + # of its attributes + for letter in (:x, :y, :z) + # If we have NaNs in the data they define the segments and + # SegmentsIterator is used + series[letter] != nothing && NaN in collect(series[letter]) && return false + end + series[:seriestype] == :shape && return false + # ... else we check relevant attributes if they have multiple inputs + return any((typeof(series[attr]) <: AbstractVector && length(series[attr]) > 1) for attr in [:seriescolor, :seriesalpha, :linecolor, :linealpha, :linewidth, :fillcolor, :fillalpha, :markercolor, :markeralpha, :markerstrokecolor, :markerstrokealpha]) || any(typeof(series[attr]) <: AbstractArray{<:Real} for attr in (:line_z, :fill_z, :marker_z)) +end + # --------------------------------------------------------------- makekw(; kw...) = KW(kw) @@ -523,7 +723,7 @@ allFunctions(arg) = trueOrAllTrue(a -> isa(a, Function), arg) """ Allows temporary setting of backend and defaults for Plots. Settings apply only for the `do` block. Example: ``` -with(:gadfly, size=(400,400), type=:histogram) do +with(:gr, size=(400,400), type=:histogram) do plot(rand(10)) plot(rand(10)) end @@ -608,7 +808,7 @@ end # --------------------------------------------------------------- # --------------------------------------------------------------- -type DebugMode +mutable struct DebugMode on::Bool end const _debugMode = DebugMode(false) @@ -645,11 +845,11 @@ end # used in updating an existing series extendSeriesByOne(v::UnitRange{Int}, n::Int = 1) = isempty(v) ? (1:n) : (minimum(v):maximum(v)+n) -extendSeriesByOne(v::AVec, n::Integer = 1) = isempty(v) ? (1:n) : vcat(v, (1:n) + maximum(v)) -extendSeriesData{T}(v::Range{T}, z::Real) = extendSeriesData(float(collect(v)), z) -extendSeriesData{T}(v::Range{T}, z::AVec) = extendSeriesData(float(collect(v)), z) -extendSeriesData{T}(v::AVec{T}, z::Real) = (push!(v, convert(T, z)); v) -extendSeriesData{T}(v::AVec{T}, z::AVec) = (append!(v, convert(Vector{T}, z)); v) +extendSeriesByOne(v::AVec, n::Integer = 1) = isempty(v) ? (1:n) : vcat(v, (1:n) + ignorenan_maximum(v)) +extendSeriesData(v::Range{T}, z::Real) where {T} = extendSeriesData(float(collect(v)), z) +extendSeriesData(v::Range{T}, z::AVec) where {T} = extendSeriesData(float(collect(v)), z) +extendSeriesData(v::AVec{T}, z::Real) where {T} = (push!(v, convert(T, z)); v) +extendSeriesData(v::AVec{T}, z::AVec) where {T} = (append!(v, convert(Vector{T}, z)); v) # ------------------------------------------------------- @@ -667,14 +867,14 @@ function getxyz(plt::Plot, i::Integer) tovec(d[:x]), tovec(d[:y]), tovec(d[:z]) end -function setxy!{X,Y}(plt::Plot, xy::Tuple{X,Y}, i::Integer) +function setxy!(plt::Plot, xy::Tuple{X,Y}, i::Integer) where {X,Y} series = plt.series_list[i] series.d[:x], series.d[:y] = xy sp = series.d[:subplot] reset_extrema!(sp) _series_updated(plt, series) end -function setxyz!{X,Y,Z}(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) +function setxyz!(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) where {X,Y,Z} series = plt.series_list[i] series.d[:x], series.d[:y], series.d[:z] = xyz sp = series.d[:subplot] @@ -682,7 +882,7 @@ function setxyz!{X,Y,Z}(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) _series_updated(plt, series) end -function setxyz!{X,Y,Z<:AbstractMatrix}(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) +function setxyz!(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) where {X,Y,Z<:AbstractMatrix} setxyz!(plt, (xyz[1], xyz[2], Surface(xyz[3])), i) end @@ -691,8 +891,8 @@ end # indexing notation # Base.getindex(plt::Plot, i::Integer) = getxy(plt, i) -Base.setindex!{X,Y}(plt::Plot, xy::Tuple{X,Y}, i::Integer) = (setxy!(plt, xy, i); plt) -Base.setindex!{X,Y,Z}(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) = (setxyz!(plt, xyz, i); plt) +Base.setindex!(plt::Plot, xy::Tuple{X,Y}, i::Integer) where {X,Y} = (setxy!(plt, xy, i); plt) +Base.setindex!(plt::Plot, xyz::Tuple{X,Y,Z}, i::Integer) where {X,Y,Z} = (setxyz!(plt, xyz, i); plt) # ------------------------------------------------------- @@ -804,10 +1004,10 @@ function Base.append!(plt::Plot, i::Integer, x::AVec, y::AVec, z::AVec) end # tuples -Base.push!{X,Y}(plt::Plot, xy::Tuple{X,Y}) = push!(plt, 1, xy...) -Base.push!{X,Y,Z}(plt::Plot, xyz::Tuple{X,Y,Z}) = push!(plt, 1, xyz...) -Base.push!{X,Y}(plt::Plot, i::Integer, xy::Tuple{X,Y}) = push!(plt, i, xy...) -Base.push!{X,Y,Z}(plt::Plot, i::Integer, xyz::Tuple{X,Y,Z}) = push!(plt, i, xyz...) +Base.push!(plt::Plot, xy::Tuple{X,Y}) where {X,Y} = push!(plt, 1, xy...) +Base.push!(plt::Plot, xyz::Tuple{X,Y,Z}) where {X,Y,Z} = push!(plt, 1, xyz...) +Base.push!(plt::Plot, i::Integer, xy::Tuple{X,Y}) where {X,Y} = push!(plt, i, xy...) +Base.push!(plt::Plot, i::Integer, xyz::Tuple{X,Y,Z}) where {X,Y,Z} = push!(plt, i, xyz...) # ------------------------------------------------------- # push/append for all series @@ -871,9 +1071,152 @@ mm2px(mm::Real) = float(px / MM_PER_PX) "Smallest x in plot" -xmin(plt::Plot) = minimum([minimum(series.d[:x]) for series in plt.series_list]) +xmin(plt::Plot) = ignorenan_minimum([ignorenan_minimum(series.d[:x]) for series in plt.series_list]) "Largest x in plot" -xmax(plt::Plot) = maximum([maximum(series.d[:x]) for series in plt.series_list]) +xmax(plt::Plot) = ignorenan_maximum([ignorenan_maximum(series.d[:x]) for series in plt.series_list]) "Extrema of x-values in plot" -Base.extrema(plt::Plot) = (xmin(plt), xmax(plt)) +ignorenan_extrema(plt::Plot) = (xmin(plt), xmax(plt)) + + +# --------------------------------------------------------------- +# get fonts from objects: + +titlefont(sp::Subplot) = font( + sp[:titlefontfamily], + sp[:titlefontsize], + sp[:titlefontvalign], + sp[:titlefonthalign], + sp[:titlefontrotation], + sp[:titlefontcolor], +) + +legendfont(sp::Subplot) = font( + sp[:legendfontfamily], + sp[:legendfontsize], + sp[:legendfontvalign], + sp[:legendfonthalign], + sp[:legendfontrotation], + sp[:legendfontcolor], +) + +tickfont(ax::Axis) = font( + ax[:tickfontfamily], + ax[:tickfontsize], + ax[:tickfontvalign], + ax[:tickfonthalign], + ax[:tickfontrotation], + ax[:tickfontcolor], +) + +guidefont(ax::Axis) = font( + ax[:guidefontfamily], + ax[:guidefontsize], + ax[:guidefontvalign], + ax[:guidefonthalign], + ax[:guidefontrotation], + ax[:guidefontcolor], +) + +# --------------------------------------------------------------- +# converts unicode scientific notation unsupported by pgfplots and gr +# into a format that works + +function convert_sci_unicode(label::AbstractString) + unicode_dict = Dict( + '⁰' => "0", + '¹' => "1", + '²' => "2", + '³' => "3", + '⁴' => "4", + '⁵' => "5", + '⁶' => "6", + '⁷' => "7", + '⁸' => "8", + '⁹' => "9", + '⁻' => "-", + "×10" => "×10^{", + ) + for key in keys(unicode_dict) + label = replace(label, key, unicode_dict[key]) + end + if contains(label, "10^{") + label = string(label, "}") + end + label +end + +function straightline_data(series) + sp = series[:subplot] + xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) + x, y = series[:x], series[:y] + n = length(x) + if n == 2 + return straightline_data(xl, yl, x, y) + else + k, r = divrem(n, 3) + if r == 0 + xdata, ydata = fill(NaN, n), fill(NaN, n) + for i in 1:k + inds = (3 * i - 2):(3 * i - 1) + xdata[inds], ydata[inds] = straightline_data(xl, yl, x[inds], y[inds]) + end + return xdata, ydata + else + error("Misformed data. `straightline_data` either accepts vectors of length 2 or 3k. The provided series has length $n") + end + end +end + +function straightline_data(xl, yl, x, y) + x_vals, y_vals = if y[1] == y[2] + if x[1] == x[2] + error("Two identical points cannot be used to describe a straight line.") + else + [xl[1], xl[2]], [y[1], y[2]] + end + elseif x[1] == x[2] + [x[1], x[2]], [yl[1], yl[2]] + else + # get a and b from the line y = a * x + b through the points given by + # the coordinates x and x + b = y[1] - (y[1] - y[2]) * x[1] / (x[1] - x[2]) + a = (y[1] - y[2]) / (x[1] - x[2]) + # get the data values + xdata = [clamp(x[1] + (x[1] - x[2]) * (ylim - y[1]) / (y[1] - y[2]), xl...) for ylim in yl] + + xdata, a .* xdata .+ b + end + # expand the data outside the axis limits, by a certain factor too improve + # plotly(js) and interactive behaviour + factor = 100 + x_vals = x_vals .+ (x_vals[2] - x_vals[1]) .* factor .* [-1, 1] + y_vals = y_vals .+ (y_vals[2] - y_vals[1]) .* factor .* [-1, 1] + return x_vals, y_vals +end + +function shape_data(series) + sp = series[:subplot] + xl, yl = isvertical(series) ? (xlims(sp), ylims(sp)) : (ylims(sp), xlims(sp)) + x, y = series[:x], series[:y] + factor = 100 + for i in eachindex(x) + if x[i] == -Inf + x[i] = xl[1] - factor * (xl[2] - xl[1]) + elseif x[i] == Inf + x[i] = xl[2] + factor * (xl[2] - xl[1]) + end + end + for i in eachindex(y) + if y[i] == -Inf + y[i] = yl[1] - factor * (yl[2] - yl[1]) + elseif y[i] == Inf + y[i] = yl[2] + factor * (yl[2] - yl[1]) + end + end + return x, y +end + +function construct_categorical_data(x::AbstractArray, axis::Axis) + map(xi -> axis[:discrete_values][searchsortedfirst(axis[:continuous_values], xi)], x) +end diff --git a/test/REQUIRE b/test/REQUIRE index 57bc7828..09681156 100644 --- a/test/REQUIRE +++ b/test/REQUIRE @@ -1,10 +1,8 @@ StatPlots -FactCheck Images ImageMagick @osx QuartzImageIO -GR +GR 0.31.0 RDatasets VisualRegressionTests UnicodePlots -Glob diff --git a/test/imgcomp.jl b/test/imgcomp.jl index 71f94924..7ad523fd 100644 --- a/test/imgcomp.jl +++ b/test/imgcomp.jl @@ -15,8 +15,7 @@ end using Plots using StatPlots -using FactCheck -using Glob +using Base.Test default(size=(500,300)) @@ -24,7 +23,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.6" +const _current_plots_version = v"0.17.4" function image_comparison_tests(pkg::Symbol, idx::Int; debug = false, popup = isinteractive(), sigma = [1,1], eps = 1e-2) @@ -43,11 +42,8 @@ function image_comparison_tests(pkg::Symbol, idx::Int; debug = false, popup = is fn = "ref$idx.png" # firgure out version info - G = glob(joinpath(relpath(refdir), "*")) - # @show refdir fn G - slash = (@static is_windows() ? "\\" : "/") - versions = map(fn -> VersionNumber(split(fn, slash)[end]), G) - versions = reverse(sort(versions)) + vns = filter(x->x[1] != '.', readdir(refdir)) + versions = sort(VersionNumber.(vns), rev = true) versions = filter(v -> v <= _current_plots_version, versions) # @show refdir fn versions @@ -99,7 +95,7 @@ function image_comparison_facts(pkg::Symbol; for i in 1:length(Plots._examples) i in skip && continue if only == nothing || i in only - @fact image_comparison_tests(pkg, i, debug=debug, sigma=sigma, eps=eps) |> success --> true + @test image_comparison_tests(pkg, i, debug=debug, sigma=sigma, eps=eps) |> success == true end end end diff --git a/test/install_wkhtmltoimage.sh b/test/install_wkhtmltoimage.sh index b265d183..dbdf4c90 100755 --- a/test/install_wkhtmltoimage.sh +++ b/test/install_wkhtmltoimage.sh @@ -5,9 +5,11 @@ set -ex sudo apt-get -qq update # sudo apt-get install -y wkhtmltopdf -sudo apt-get install -y xfonts-75dpi -wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2/wkhtmltox-0.12.2_linux-trusty-amd64.deb -sudo dpkg -i wkhtmltox-0.12.2_linux-trusty-amd64.deb +sudo apt-get install -y xfonts-75dpi xfonts-base +wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.2.1/wkhtmltox-0.12.2.1_linux-precise-amd64.deb +sudo dpkg -i wkhtmltox-0.12.2.1_linux-precise-amd64.deb +# wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2/wkhtmltox-0.12.2_linux-trusty-amd64.deb +# sudo dpkg -i wkhtmltox-0.12.2_linux-trusty-amd64.deb wkhtmltoimage http://www.google.com test.png ls diff --git a/test/runtests.jl b/test/runtests.jl index ca7f5e3e..62653f4e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,89 +7,136 @@ srand(1234) default(show=false, reuse=true) img_eps = isinteractive() ? 1e-2 : 10e-2 -# facts("Gadfly") do -# @fact gadfly() --> Plots.GadflyBackend() -# @fact backend() --> Plots.GadflyBackend() +@testset "GR" begin + ENV["PLOTS_TEST"] = "true" + ENV["GKSwstype"] = "100" + @test gr() == Plots.GRBackend() + @test backend() == Plots.GRBackend() + + image_comparison_facts(:gr, eps=img_eps) +end + + +#@testset "PyPlot" begin +# @test pyplot() == Plots.PyPlotBackend() +# @test backend() == Plots.PyPlotBackend() # -# @fact typeof(plot(1:10)) --> Plots.Plot{Plots.GadflyBackend} -# @fact plot(Int[1,2,3], rand(3)) --> not(nothing) -# @fact plot(sort(rand(10)), rand(Int, 10, 3)) --> not(nothing) -# @fact plot!(rand(10,3), rand(10,3)) --> not(nothing) +# image_comparison_facts(:pyplot, eps=img_eps) +#end + +@testset "UnicodePlots" begin + @test unicodeplots() == Plots.UnicodePlotsBackend() + @test backend() == Plots.UnicodePlotsBackend() + + # lets just make sure it runs without error + @test isa(plot(rand(10)), Plots.Plot) == true +end + +# The plotlyjs testimages return a connection error on travis: +# connect: connection refused (ECONNREFUSED) + +# @testset "PlotlyJS" begin +# @test plotlyjs() == Plots.PlotlyJSBackend() +# @test backend() == Plots.PlotlyJSBackend() # -# image_comparison_facts(:gadfly, skip=[4,6,23,24,27], eps=img_eps) +# if is_linux() && isinteractive() +# image_comparison_facts(:plotlyjs, +# skip=[ +# 2, # animation (skipped for speed) +# 27, # (polar plots) takes very long / not working +# 31, # animation (skipped for speed) +# ], +# eps=img_eps) +# end # end -facts("PyPlot") do - @fact pyplot() --> Plots.PyPlotBackend() - @fact backend() --> Plots.PyPlotBackend() - image_comparison_facts(:pyplot, skip=[25,30], eps=img_eps) -end +# InspectDR returns that error on travis: +# ERROR: LoadError: InitError: Cannot open display: +# in Gtk.GLib.GError(::Gtk.##229#230) at /home/travis/.julia/v0.5/Gtk/src/GLib/gerror.jl:17 -facts("GR") do - @fact gr() --> Plots.GRBackend() - @fact backend() --> Plots.GRBackend() - - if is_linux() && isinteractive() - image_comparison_facts(:gr, skip=[2,25,30], eps=img_eps) - end -end - -facts("Plotly") do - @fact plotly() --> Plots.PlotlyBackend() - @fact backend() --> Plots.PlotlyBackend() - - # # until png generation is reliable on OSX, just test on linux - # @static is_linux() && image_comparison_facts(:plotly, only=[1,3,4,7,8,9,10,11,12,14,15,20,22,23,27], eps=img_eps) -end +# @testset "InspectDR" begin +# @test inspectdr() == Plots.InspectDRBackend() +# @test backend() == Plots.InspectDRBackend() +# +# image_comparison_facts(:inspectdr, +# skip=[ +# 2, # animation +# 6, # heatmap not defined +# 10, # heatmap not defined +# 22, # contour not defined +# 23, # pie not defined +# 27, # polar plot not working +# 28, # heatmap not defined +# 31, # animation +# ], +# eps=img_eps) +# end -# facts("Immerse") do -# @fact immerse() --> Plots.ImmerseBackend() -# @fact backend() --> Plots.ImmerseBackend() +# @testset "Plotly" begin +# @test plotly() == Plots.PlotlyBackend() +# @test backend() == Plots.PlotlyBackend() +# +# # # until png generation is reliable on OSX, just test on linux +# # @static is_linux() && image_comparison_facts(:plotly, only=[1,3,4,7,8,9,10,11,12,14,15,20,22,23,27], eps=img_eps) +# end + + +# @testset "Immerse" begin +# @test immerse() == Plots.ImmerseBackend() +# @test backend() == Plots.ImmerseBackend() # # # as long as we can plot anything without error, it should be the same as Gadfly # image_comparison_facts(:immerse, only=[1], eps=img_eps) # end -# facts("PlotlyJS") do -# @fact plotlyjs() --> Plots.PlotlyJSBackend() -# @fact backend() --> Plots.PlotlyJSBackend() +# @testset "PlotlyJS" begin +# @test plotlyjs() == Plots.PlotlyJSBackend() +# @test backend() == Plots.PlotlyJSBackend() # # # as long as we can plot anything without error, it should be the same as Plotly # image_comparison_facts(:plotlyjs, only=[1], eps=img_eps) # end -facts("UnicodePlots") do - @fact unicodeplots() --> Plots.UnicodePlotsBackend() - @fact backend() --> Plots.UnicodePlotsBackend() - - # lets just make sure it runs without error - @fact isa(plot(rand(10)), Plots.Plot) --> true -end +# @testset "Gadfly" begin +# @test gadfly() == Plots.GadflyBackend() +# @test backend() == Plots.GadflyBackend() +# +# @test typeof(plot(1:10)) == Plots.Plot{Plots.GadflyBackend} +# @test plot(Int[1,2,3], rand(3)) == not(nothing) +# @test plot(sort(rand(10)), rand(Int, 10, 3)) == not(nothing) +# @test plot!(rand(10,3), rand(10,3)) == not(nothing) +# +# image_comparison_facts(:gadfly, skip=[4,6,23,24,27], eps=img_eps) +# end -facts("Axes") do + +@testset "Axes" begin p = plot() axis = p.subplots[1][:xaxis] - @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) - @fact axis[:discrete_map] --> Dict{Any,Any}(:yo => 2, "HI" => 1) + @test typeof(axis) == Plots.Axis + @test Plots.discrete_value!(axis, "HI") == (0.5, 1) + @test Plots.discrete_value!(axis, :yo) == (1.5, 2) + @test Plots.ignorenan_extrema(axis) == (0.5,1.5) + @test axis[:discrete_map] == Dict{Any,Any}(:yo => 2, "HI" => 1) Plots.discrete_value!(axis, ["x$i" for i=1:5]) Plots.discrete_value!(axis, ["x$i" for i=0:2]) - @fact extrema(axis) --> (0.5, 7.5) + @test Plots.ignorenan_extrema(axis) == (0.5, 7.5) end +@testset "NoFail" begin + histogram([1, 0, 0, 0, 0, 0]) +end # tests for preprocessing recipes -# facts("recipes") do +# @testset "recipes" begin # user recipe @@ -126,6 +173,4 @@ end # end - -FactCheck.exitstatus() end # module diff --git a/test/travis_commands.jl b/test/travis_commands.jl index d0ff84bd..a283d780 100644 --- a/test/travis_commands.jl +++ b/test/travis_commands.jl @@ -1,5 +1,5 @@ -# Pkg.clone("ImageMagick") -# Pkg.build("ImageMagick") +Pkg.add("ImageMagick") +Pkg.build("ImageMagick") # Pkg.clone("GR") # Pkg.build("GR") @@ -9,13 +9,14 @@ Pkg.clone("https://github.com/JuliaPlots/PlotReferenceImages.jl.git") # Pkg.clone("https://github.com/JuliaStats/KernelDensity.jl.git") Pkg.clone("StatPlots") -Pkg.checkout("PlotUtils") +# Pkg.checkout("PlotUtils") -# Pkg.clone("https://github.com/JunoLab/Blink.jl.git") +# Pkg.clone("Blink") # Pkg.build("Blink") # import Blink # Blink.AtomShell.install() -# Pkg.clone("https://github.com/spencerlyon2/PlotlyJS.jl.git") +# Pkg.add("Rsvg") +# Pkg.add("PlotlyJS") # Pkg.checkout("RecipesBase") # Pkg.clone("VisualRegressionTests") @@ -25,4 +26,6 @@ ENV["PYTHON"] = "" Pkg.add("PyPlot") Pkg.build("PyPlot") +# Pkg.add("InspectDR") + Pkg.test("Plots"; coverage=false)