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 -[](https://travis-ci.org/tbreloff/Plots.jl) +[](https://travis-ci.org/JuliaPlots/Plots.jl) +[](https://ci.appveyor.com/project/mkborregaard/plots-jl) [](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