diff --git a/Project.toml b/Project.toml index 6b6c0284..b87f0ffd 100644 --- a/Project.toml +++ b/Project.toml @@ -56,6 +56,7 @@ ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" @@ -64,4 +65,4 @@ UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" [targets] -test = ["FileIO", "GeometryTypes", "Gtk", "ImageMagick", "Images", "LaTeXStrings", "LibGit2", "Random", "RDatasets", "StatsPlots", "Test", "UnicodePlots", "VisualRegressionTests"] +test = ["FileIO", "GeometryTypes", "Gtk", "ImageMagick", "Images", "LaTeXStrings", "LibGit2", "PGFPlotsX", "Random", "RDatasets", "StatsPlots", "Test", "UnicodePlots", "VisualRegressionTests"] diff --git a/src/backends.jl b/src/backends.jl index 26f01c91..9bcba334 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -37,7 +37,6 @@ macro init_backend(s) _backendType[Symbol($str)] = $T _backendSymbol[$T] = Symbol($str) _backend_packages[Symbol($str)] = Symbol($package_str) - # include("backends/" * $str * ".jl") end) end @@ -252,6 +251,7 @@ end @init_backend PlotlyJS @init_backend GR @init_backend PGFPlots +@init_backend PGFPlotsX @init_backend InspectDR @init_backend HDF5 @@ -661,3 +661,57 @@ const _inspectdr_marker = Symbol[ ] const _inspectdr_scale = [:identity, :ln, :log2, :log10] +# ------------------------------------------------------------------------------ +# pgfplotsx + +const _pgfplotsx_attr = merge_with_base_supported([ + :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, + :label, + :seriescolor, :seriesalpha, + :linecolor, :linestyle, :linewidth, :linealpha, + :markershape, :markercolor, :markersize, :markeralpha, + :markerstrokewidth, :markerstrokecolor, :markerstrokealpha, + :fillrange, :fillcolor, :fillalpha, + :bins, + :layout, + :title, :window_title, + :guide, :lims, :ticks, :scale, :flip, + :match_dimensions, + :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, :colorbar_title, :colorbar_entry, + :fill_z, :line_z, :marker_z, :levels, + :ribbon, :quiver, + :orientation, + :overwrite_figure, + :polar, + :aspect_ratio, + :normalize, :weights, + :inset_subplots, + :bar_width, + :arrow, + :framestyle, + :tick_direction, + :camera, + :contour_labels, +]) +const _pgfplotsx_seriestype = + [:path, :scatter, :straightline, + :path3d, :scatter3d, :surface, :wireframe, + :heatmap, :contour, :contour3d, + :shape, + :steppre, :stepmid, :steppost, :ysticks, :xsticks] +const _pgfplotsx_style = [:auto, :solid, :dash, :dot, :dashdot, :dashdotdot] +const _pgfplotsx_marker = [:none, :auto, :circle, :rect, :diamond, :utriangle, :dtriangle, :ltriangle, :rtriangle, :cross, :xcross, :star5, :pentagon, :hline, :vline, Shape] +const _pgfplotsx_scale = [:identity, :ln, :log2, :log10] +is_marker_supported(::PGFPlotsXBackend, shape::Shape) = true diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl new file mode 100644 index 00000000..cc8d7013 --- /dev/null +++ b/src/backends/pgfplotsx.jl @@ -0,0 +1,845 @@ +using Contour: Contour + +Base.@kwdef mutable struct PGFPlotsXPlot + is_created::Bool = false + was_shown::Bool = false + the_plot::PGFPlotsX.TikzDocument = PGFPlotsX.TikzDocument() + function PGFPlotsXPlot(is_created, was_shown, the_plot) + pgfx_plot = new(is_created, was_shown, the_plot) + # tikz libraries + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, "\\usetikzlibrary{arrows.meta}") + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, "\\usetikzlibrary{backgrounds}") + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, + """ + \\pgfkeys{/tikz/.cd, + background color/.initial=white, + background color/.get=\\backcol, + background color/.store in=\\backcol, + } + \\tikzset{background rectangle/.style={ + fill=\\backcol, + }, + use background/.style={ + show background rectangle + } + } + """ + ) + # pgfplots libraries + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, "\\usepgfplotslibrary{patchplots}") + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, "\\usepgfplotslibrary{fillbetween}") + pgfx_plot + end +end + +## end user utility functions +function pgfx_axes(pgfx_plot::PGFPlotsXPlot) + gp = pgfx_plot.the_plot.elements[1].elements[1] + return gp isa PGFPlotsX.GroupPlot ? gp.contents : gp +end + +function pgfx_preamble(pgfx_plot::Plot{PGFPlotsXBackend}) + old_flag = pgfx_plot.attr[:tex_output_standalone] + pgfx_plot.attr[:tex_output_standalone] = true + fulltext = String(repr("application/x-tex", pgfx_plot)) + preamble = fulltext[1:first(findfirst("\\begin{document}", fulltext)) - 1] + pgfx_plot.attr[:tex_output_standalone] = old_flag + preamble +end +## + +function surface_to_vecs(x::AVec, y::AVec, s::Union{AMat, Surface}) + a = Array(s) + xn = Vector{eltype(x)}(undef, length(a)) + yn = Vector{eltype(y)}(undef, length(a)) + zn = Vector{eltype(s)}(undef, length(a)) + for (n, (i, j)) in enumerate(Tuple.(CartesianIndices(a))) + xn[n] = x[j] + yn[n] = y[i] + zn[n] = a[i,j] + end + return xn, yn, zn +end + +function Base.push!(pgfx_plot::PGFPlotsXPlot, item) + push!(pgfx_plot.the_plot, item) +end + +function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) + if !pgfx_plot.is_created + the_plot = PGFPlotsX.TikzPicture(PGFPlotsX.Options()) + rows, cols = size(plt.layout.grid) + bgc = plt.attr[:background_color_outside] == :match ? plt.attr[:background_color] : plt.attr[:background_color_outside] + if bgc isa Colors.Colorant + cstr = plot_color(bgc) + a = alpha(cstr) + push!(the_plot.options, + "draw opacity" => a, + "background color" => cstr, + "use background" => nothing, + ) + end + + # the combination of groupplot and polaraxis is broken in pgfplots + if !any( sp -> ispolar(sp), plt.subplots ) + pl_height, pl_width = plt.attr[:size] + push!( the_plot, PGFPlotsX.GroupPlot( + PGFPlotsX.Options( + "group style" => PGFPlotsX.Options( + "group size" => string(cols)*" by "*string(rows), + "horizontal sep" => string(maximum(sp -> sp.minpad[1], plt.subplots)), + "vertical sep" => string(maximum(sp -> sp.minpad[2], plt.subplots)), + ), + "height" => pl_height > 0 ? string(pl_height)*"px" : "{}", + "width" => pl_width > 0 ? string(pl_width)*"px" : "{}", + ) + ) + ) + end + for sp in plt.subplots + bb = bbox(sp) + cstr = plot_color(sp[:background_color_legend]) + a = alpha(cstr) + fg_alpha = alpha(plot_color(sp[:foreground_color_legend])) + title_cstr = plot_color(sp[:titlefontcolor]) + title_a = alpha(title_cstr) + axis_opt = PGFPlotsX.Options( + "height" => string(height(bb)), + "width" => string(width(bb)), + "title" => sp[:title], + "title style" => PGFPlotsX.Options( + "font" => pgfx_font(sp[:titlefontsize], pgfx_thickness_scaling(sp)), + "color" => title_cstr, + "draw opacity" => title_a, + "rotate" => sp[:titlefontrotation] + ), + "legend style" => PGFPlotsX.Options( + pgfx_linestyle(pgfx_thickness_scaling(sp), sp[:foreground_color_legend], fg_alpha, "solid") => nothing, + "fill" => cstr, + "fill opacity" => a, + "text opacity" => alpha(plot_color(sp[:legendfontcolor])), + "font" => pgfx_font(sp[:legendfontsize], pgfx_thickness_scaling(sp)) + ), + "axis background/.style" => PGFPlotsX.Options( + "fill" => sp[:background_color_inside] + ) + ) + # legend position + if sp[:legend] isa Tuple + x, y = sp[:legend] + push!(axis_opt["legend style"], "at={($x, $y)}" ) + else + push!(axis_opt, "legend pos" => get(_pgfplotsx_legend_pos, sp[:legend], "outer north east"), + ) + end + for letter in (:x, :y, :z) + if letter != :z || is3d(sp) + pgfx_axis!(axis_opt, sp, letter) + end + 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.plotattributes[col]) == ColorGradient + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, """\\pgfplotsset{ + colormap={plots$(sp.attr[:subplot_index])}{$(pgfx_colormap(series.plotattributes[col]))}, + }""") + push!(axis_opt, + "colorbar" => nothing, + "colormap name" => "plots$(sp.attr[:subplot_index])", + "colormap access" => "direct", + ) + # goto is needed to break out of col and series for + @goto colorbar_end + end + end + end + @label colorbar_end + + push!(axis_opt, "colorbar style" => PGFPlotsX.Options( + "title" => sp[:colorbar_title], + "point meta max" => get_clims(sp)[2], + "point meta min" => get_clims(sp)[1] + ) + ) + if is3d(sp) + azim, elev = sp[:camera] + push!( axis_opt, "view" => (azim, elev) ) + end + axisf = if sp[:projection] == :polar + # push!(axis_opt, "xmin" => 90) + # push!(axis_opt, "xmax" => 450) + PGFPlotsX.PolarAxis + else + PGFPlotsX.Axis + end + axis = axisf( + axis_opt + ) + for (series_index, series) in enumerate(series_list(sp)) + opt = series.plotattributes + st = series[:seriestype] + series_opt = PGFPlotsX.Options( + "color" => single_color(opt[:linecolor]), + ) + if is3d(series) || st == :heatmap + series_func = PGFPlotsX.Plot3 + else + series_func = PGFPlotsX.Plot + end + if series[:fillrange] !== nothing && !isfilledcontour(series) && series[:ribbon] === nothing + push!(series_opt, "area legend" => nothing) + end + if st == :heatmap + push!(axis.options, + "view" => "{0}{90}", + "shader" => "flat corner", + ) + end + # treat segments + segments = if st in (:wireframe, :heatmap, :contour, :surface, :contour3d) + iter_segments(surface_to_vecs(series[:x], series[:y], series[:z])...) + else + iter_segments(series) + end + for (i, rng) in enumerate(segments) + segment_opt = PGFPlotsX.Options() + segment_opt = merge( segment_opt, pgfx_linestyle(opt, i) ) + if opt[:markershape] != :none + marker = opt[:markershape] + if marker isa Shape + x = marker.x + y = marker.y + scale_factor = 0.025 + mark_size = opt[:markersize] * scale_factor + path = join(["($(x[i] * mark_size), $(y[i] * mark_size))" for i in eachindex(x)], " -- ") + c = get_markercolor(series, i) + a = get_markeralpha(series, i) + PGFPlotsX.push_preamble!(pgfx_plot.the_plot, + """ + \\pgfdeclareplotmark{PlotsShape$(series_index)}{ + \\filldraw + $path; + } + """ + ) + end + segment_opt = merge( segment_opt, pgfx_marker(opt, i) ) + end + if st == :shape || + isfilledcontour(series) || + series[:ribbon] !== nothing + segment_opt = merge( segment_opt, pgfx_fillstyle(opt, i) ) + end + # add fillrange + if series[:fillrange] !== nothing && !isfilledcontour(series) && series[:ribbon] === nothing + pgfx_fillrange_series!( axis, series, series_func, i, _cycle(series[:fillrange], rng), rng) + # add to legend? + if i == 1 && opt[:label] != "" && sp[:legend] != :none && should_add_to_legend(series) + io = IOBuffer() + PGFPlotsX.print_tex(io, pgfx_fillstyle(opt, i)) + style = strip(String(take!(io)),['[',']', ' ']) + push!( segment_opt, "legend image code/.code" => """{ + \\draw[##1,/tikz/.cd, $style] (0cm,-0.1cm) rectangle (0.6cm,0.1cm); + }""" ) + end + end + # series + coordinates = pgfx_series_coordinates!( sp, series, segment_opt, opt, rng ) + segment_plot = series_func( + merge(series_opt, segment_opt), + coordinates, + ) + push!(axis, segment_plot) + # add ribbons? + ribbon = series[:ribbon] + if ribbon !== nothing + pgfx_add_ribbons!( axis, series, segment_plot, series_func, series_index ) + end + # add to legend? + if i == 1 && opt[:label] != "" && sp[:legend] != :none && should_add_to_legend(series) + legend = PGFPlotsX.LegendEntry(PGFPlotsX.Options(), opt[:label], true) + push!( axis, legend ) + end + # add series annotations + anns = series[:series_annotations] + for (xi,yi,str,fnt) in EachAnn(anns, series[:x], series[:y]) + pgfx_add_annotation!(axis, xi, yi, PlotText(str, fnt), pgfx_thickness_scaling(series)) + end + end + # add subplot annotations + anns = sp.attr[:annotations] + for (xi,yi,txt) in anns + pgfx_add_annotation!(axis, xi, yi, txt, pgfx_thickness_scaling(sp)) + end + end + if ispolar(sp) + axes = the_plot + else + axes = the_plot.elements[1] + end + push!( axes, axis ) + if length(plt.o.the_plot.elements) > 0 + plt.o.the_plot.elements[1] = the_plot + else + push!(plt.o, the_plot) + end + end + pgfx_plot.is_created = true + end + return pgfx_plot +end +## seriestype specifics +@inline function pgfx_series_coordinates!(sp, series, segment_opt, opt, rng) + st = series[:seriestype] + # function args + args = if st in (:contour, :contour3d) + opt[:x], opt[:y], Array(opt[:z])' + elseif st in (:heatmap, :surface, :wireframe) + surface_to_vecs(opt[:x], opt[:y], opt[:z]) + elseif is3d(st) + opt[:x], opt[:y], opt[:z] + elseif st == :straightline + straightline_data(series) + elseif st == :shape + shape_data(series) + elseif ispolar(sp) + theta, r = opt[:x], opt[:y] + rad2deg.(theta), r + else + opt[:x], opt[:y] + end + seg_args = if st in (:contour, :contour3d) + args + else + (arg[rng] for arg in args) + end + if opt[:quiver] !== nothing + push!(segment_opt, "quiver" => PGFPlotsX.Options( + "u" => "\\thisrow{u}", + "v" => "\\thisrow{v}", + pgfx_arrow(opt[:arrow]) => nothing + ), + ) + x, y = collect(seg_args) + return PGFPlotsX.Table([:x => x, :y => y, :u => opt[:quiver][1], :v => opt[:quiver][2]]) + else + if isfilledcontour(series) + st = :filledcontour + end + pgfx_series_coordinates!(Val(st), segment_opt, opt, seg_args) + end +end +function pgfx_series_coordinates!(st_val::Union{Val{:path}, Val{:path3d}, Val{:straightline}}, segment_opt, opt, args) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Union{Val{:scatter}, Val{:scatter3d}}, segment_opt, opt, args) + push!( segment_opt, "only marks" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:heatmap}, segment_opt, opt, args) + push!(segment_opt, + "surf" => nothing, + "mesh/rows" => length(opt[:x]), + "mesh/cols" => length(opt[:y]), + ) + return PGFPlotsX.Table(args...) +end + +function pgfx_series_coordinates!(st_val::Val{:steppre}, segment_opt, opt, args) + push!( segment_opt, "const plot mark right" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:stepmid}, segment_opt, opt, args) + push!( segment_opt, "const plot mark mid" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:steppost}, segment_opt, opt, args) + push!( segment_opt, "const plot" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Union{Val{:ysticks},Val{:sticks}}, segment_opt, opt, args) + push!( segment_opt, "ycomb" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:xsticks}, segment_opt, opt, args) + push!( segment_opt, "xcomb" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:surface}, segment_opt, opt, args) + push!( segment_opt, "surf" => nothing, + "mesh/rows" => length(opt[:x]), + "mesh/cols" => length(opt[:y]), + ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:volume}, segment_opt, opt, args) + push!( segment_opt, "patch" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:wireframe}, segment_opt, opt, args) + push!( segment_opt, "mesh" => nothing, + "mesh/rows" => length(opt[:x]) + ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Val{:shape}, segment_opt, opt, args) + push!( segment_opt, "area legend" => nothing ) + return PGFPlotsX.Coordinates(args...) +end +function pgfx_series_coordinates!(st_val::Union{Val{:contour}, Val{:contour3d}}, segment_opt, opt, args) + push!(segment_opt, + "contour prepared" => PGFPlotsX.Options( + "labels" => opt[:contour_labels], + ), + ) + return PGFPlotsX.Table(Contour.contours(args..., opt[:levels])) +end +function pgfx_series_coordinates!(st_val::Val{:filledcontour}, segment_opt, opt, args) + xs, ys, zs = collect(args) + push!(segment_opt, + "contour filled" => PGFPlotsX.Options( + "labels" => opt[:contour_labels], + ), + "point meta" => "explicit", + "shader" => "flat" + ) + if opt[:levels] isa Number + push!(segment_opt["contour filled"], + "number" => opt[:levels], + ) + elseif opt[:levels] isa AVec + push!(segment_opt["contour filled"], + "levels" => opt[:levels], + ) + end + + cs = join([ + join(["($x, $y) [$(zs[j, i])]" for (j, x) in enumerate(xs)], " ") for (i, y) in enumerate(ys)], "\n\n" + ) + """ + coordinates { + $cs + }; + """ +end +## +const _pgfplotsx_linestyles = KW( + :solid => "solid", + :dash => "dashed", + :dot => "dotted", + :dashdot => "dashdotted", + :dashdotdot => "dashdotdotted", +) + +const _pgfplotsx_markers = KW( + :none => "none", + :cross => "+", + :xcross => "x", + :+ => "+", + :x => "x", + :utriangle => "triangle*", + :dtriangle => "triangle*", + :rtriangle => "triangle*", + :ltriangle => "triangle*", + :circle => "*", + :rect => "square*", + :star5 => "star", + :star6 => "asterisk", + :diamond => "diamond*", + :pentagon => "pentagon*", + :hline => "-", + :vline => "|" +) + +const _pgfplotsx_legend_pos = KW( + :bottomleft => "south west", + :bottomright => "south east", + :topright => "north east", + :topleft => "north west", + :outertopright => "outer north east", +) + +const _pgfx_framestyles = [:box, :axes, :origin, :zerolines, :grid, :none] +const _pgfx_framestyle_defaults = Dict(:semi => :box) + +# we use the anchors to define orientations for example to align left +# one needs to use the right edge as anchor +const _pgfx_annotation_halign = KW( + :center => "", + :left => "right", + :right => "left" +) +## -------------------------------------------------------------------------------------- +# Generates a colormap for pgfplots based on a ColorGradient +pgfx_arrow(::Nothing) = "every arrow/.append style={-}" +function pgfx_arrow( arr::Arrow ) + components = String[] + head = String[] + push!(head, "{stealth[length = $(arr.headlength)pt, width = $(arr.headwidth)pt") + if arr.style == :open + push!(head, ", open") + end + push!(head, "]}") + head = join(head, "") + if arr.side == :both || arr.side == :tail + push!( components, head ) + end + push!(components, "-") + if arr.side == :both || arr.side == :head + push!( components, head ) + end + components = join( components, "" ) + return "every arrow/.append style={$(components)}" +end + +function pgfx_colormap(grad::ColorGradient) + join(map(grad.colors) do c + @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)) + end,"\n") +end +function pgfx_colormap(grad::Vector{<:Colorant}) + join(map(grad) do c + @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)) + end,"\n") +end + +function pgfx_framestyle(style::Symbol) + if style in _pgfx_framestyles + return style + else + default_style = get(_pgfx_framestyle_defaults, style, :axes) + @warn("Framestyle :$style is not (yet) supported by the PGFPlots backend. :$default_style was cosen instead.") + default_style + end +end + +pgfx_thickness_scaling(plt::Plot) = plt[:thickness_scaling] +pgfx_thickness_scaling(sp::Subplot) = pgfx_thickness_scaling(sp.plt) +pgfx_thickness_scaling(series) = pgfx_thickness_scaling(series[:subplot]) + +function pgfx_fillstyle(plotattributes, i = 1) + cstr = get_fillcolor(plotattributes, i) + a = get_fillalpha(plotattributes, i) + if a === nothing + a = alpha(single_color(cstr)) + end + PGFPlotsX.Options("fill" => cstr, "fill opacity" => a) +end + +function pgfx_linestyle(linewidth::Real, color, α = 1, linestyle = "solid") + cstr = plot_color(color, α) + a = alpha(cstr) + return PGFPlotsX.Options( + "color" => cstr, + "draw opacity" => a, + "line width" => linewidth, + get(_pgfplotsx_linestyles, linestyle, "solid") => nothing + ) +end + +function pgfx_linestyle(plotattributes, i = 1) + lw = pgfx_thickness_scaling(plotattributes) * get_linewidth(plotattributes, i) + lc = single_color(get_linecolor(plotattributes, i)) + la = get_linealpha(plotattributes, i) + ls = get_linestyle(plotattributes, i) + return pgfx_linestyle(lw, lc, la, ls) +end + +function pgfx_font(fontsize, thickness_scaling = 1, font = "\\selectfont") + fs = fontsize * thickness_scaling + return string("{\\fontsize{", fs, " pt}{", 1.3fs, " pt}", font, "}") +end + +function pgfx_marker(plotattributes, i = 1) + shape = _cycle(plotattributes[:markershape], i) + cstr = plot_color(get_markercolor(plotattributes, i), get_markeralpha(plotattributes, i)) + a = alpha(cstr) + cstr_stroke = plot_color(get_markerstrokecolor(plotattributes, i), get_markerstrokealpha(plotattributes, i)) + a_stroke = alpha(cstr_stroke) + mark_size = pgfx_thickness_scaling(plotattributes) * 0.5 * _cycle(plotattributes[:markersize], i) + return PGFPlotsX.Options( + "mark" => shape isa Shape ? "PlotsShape$i" : get(_pgfplotsx_markers, shape, "*"), + "mark size" => "$mark_size pt", + "mark options" => PGFPlotsX.Options( + "color" => cstr_stroke, + "draw opacity" => a_stroke, + "fill" => cstr, + "fill opacity" => a, + "line width" => pgfx_thickness_scaling(plotattributes) * _cycle(plotattributes[:markerstrokewidth], i), + "rotate" => if shape == :dtriangle + 180 + elseif shape == :rtriangle + 270 + elseif shape == :ltriangle + 90 + else + 0 + end, + get(_pgfplotsx_linestyles, _cycle(plotattributes[:markerstrokestyle], i), "solid") => nothing + ) + ) +end + +function pgfx_add_annotation!(o, x, y, val, thickness_scaling = 1) + # Construct the style string. + cstr = val.font.color + a = alpha(cstr) + push!(o, ["\\node", + PGFPlotsX.Options( + get(_pgfx_annotation_halign, val.font.halign, "") => nothing, + "color" => cstr, + "draw opacity" => convert(Float16, a), + "rotate" => val.font.rotation, + "font" => pgfx_font(val.font.pointsize, thickness_scaling) + ), + " at (axis cs:$x, $y) {$(val.str)};" + ]) +end + +function pgfx_add_ribbons!( axis, series, segment_plot, series_func, series_index ) + ribbon_y = series[:ribbon] + opt = series.plotattributes + if ribbon_y isa AVec + ribbon_n = length(opt[:y]) ÷ length(ribbon) + ribbon_y = repeat(ribbon, outer = ribbon_n) + end + # upper ribbon + ribbon_name_plus = "plots_rib_p$series_index" + ribbon_opt_plus = merge(segment_plot.options, PGFPlotsX.Options( + "name path" => ribbon_name_plus, + "color" => opt[:fillcolor], + "draw opacity" => opt[:fillalpha] + )) + coordinates_plus = PGFPlotsX.Coordinates(opt[:x], opt[:y] .+ ribbon_y) + ribbon_plot_plus = series_func( + ribbon_opt_plus, + coordinates_plus + ) + push!(axis, ribbon_plot_plus) + # lower ribbon + ribbon_name_minus = "plots_rib_m$series_index" + ribbon_opt_minus = merge(segment_plot.options, PGFPlotsX.Options( + "name path" => ribbon_name_minus, + "color" => opt[:fillcolor], + "draw opacity" => opt[:fillalpha] + )) + coordinates_plus = PGFPlotsX.Coordinates(opt[:x], opt[:y] .- ribbon_y) + ribbon_plot_plus = series_func( + ribbon_opt_minus, + coordinates_plus + ) + push!(axis, ribbon_plot_plus) + # fill + push!(axis, series_func( + pgfx_fillstyle(opt, series_index), + "fill between [of=$(ribbon_name_plus) and $(ribbon_name_minus)]" + )) + return axis +end + +function pgfx_fillrange_series!(axis, series, series_func, i, fillrange, rng) + fillrange_opt = PGFPlotsX.Options( + "line width" => "0", + "draw opacity" => "0", + ) + fillrange_opt = merge( fillrange_opt, pgfx_fillstyle(series, i) ) + fillrange_opt = merge( fillrange_opt, pgfx_marker(series, i) ) + push!( fillrange_opt, "forget plot" => nothing ) + opt = series.plotattributes + args = is3d(series) ? (opt[:x][rng], opt[:y][rng], opt[:z][rng]) : (opt[:x][rng], opt[:y][rng]) + push!(axis, PGFPlotsX.PlotInc(fillrange_opt, pgfx_fillrange_args(fillrange, args...))) + return axis +end + +function pgfx_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 PGFPlotsX.Coordinates(x_fill, y_fill) +end + +function pgfx_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 PGFPlotsX.Coordiantes(x_fill, y_fill, z_fill) +end +# -------------------------------------------------------------------------------------- +function pgfx_axis!(opt::PGFPlotsX.Options, sp::Subplot, letter) + axis = sp[Symbol(letter,:axis)] + + # turn off scaled ticks + push!(opt, "scaled $(letter) ticks" => "false", + string(letter,:label) => axis[:guide], + ) + + # set to supported framestyle + framestyle = pgfx_framestyle(sp[:framestyle] == false ? :none : sp[:framestyle]) + + # axis label position + labelpos = "" + if letter == :x && axis[:guide_position] == :top + labelpos = "at={(0.5,1)},above," + elseif letter == :y && axis[:guide_position] == :right + labelpos = "at={(1,0.5)},below," + end + + # Add label font + cstr = plot_color(axis[:guidefontcolor]) + α = alpha(cstr) + push!(opt, string(letter, "label style") => PGFPlotsX.Options( + labelpos => nothing, + "font" => pgfx_font(axis[:guidefontsize], pgfx_thickness_scaling(sp)), + "color" => cstr, + "draw opacity" => α, + "rotate" => axis[:guidefontrotation], + ) + ) + + # flip/reverse? + axis[:flip] && push!(opt, "$letter dir" => "reverse") + + # scale + scale = axis[:scale] + if scale in (:log2, :ln, :log10) + push!(opt, string(letter,:mode) => "log") + scale == :ln || push!(opt, "log basis $letter" => "$(scale == :log2 ? 2 : 10)") + end + + # ticks on or off + if axis[:ticks] in (nothing, false, :none) || framestyle == :none + push!(opt, "$(letter)majorticks" => "false") + end + + # grid on or off + if axis[:grid] && framestyle != :none + push!(opt, "$(letter)majorgrids" => "true") + else + push!(opt, "$(letter)majorgrids" => "false") + end + + # limits + lims = ispolar(sp) && letter == :x ? rad2deg.(axis_limits(sp, :x)) : axis_limits(sp, letter) + push!( opt, + string(letter,:min) => lims[1], + string(letter,:max) => lims[2] + ) + + if !(axis[:ticks] in (nothing, false, :none, :native)) && framestyle != :none + ticks = get_ticks(sp, axis) + #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!(opt, string(letter, "tick") => string("{", join(tick_values,","), "}")) + if axis[:showaxis] && axis[:scale] in (:ln, :log2, :log10) && axis[:ticks] == :auto + # wrap the power part of label with } + tick_labels = Vector{String}(undef, length(ticks[2])) + for (i, label) in enumerate(ticks[2]) + base, power = split(label, "^") + power = string("{", power, "}") + tick_labels[i] = string(base, "^", power) + end + push!(opt, string(letter, "ticklabels") => string("{\$", 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, Ref("×" => "\\times")) + end + push!(opt, string(letter, "ticklabels") => string("{", join(tick_labels,","), "}")) + else + push!(opt, string(letter, "ticklabels") => "{}") + end + push!(opt, string(letter, "tick align") => (axis[:tick_direction] == :out ? "outside" : "inside")) + cstr = plot_color(axis[:tickfontcolor]) + α = alpha(cstr) + push!(opt, string(letter, "ticklabel style") => PGFPlotsX.Options( + "font" => pgfx_font(axis[:tickfontsize], pgfx_thickness_scaling(sp)), + "color" => cstr, + "draw opacity" => α, + "rotate" => axis[:tickfontrotation] + ) + ) + push!(opt, string(letter, " grid style") => pgfx_linestyle(pgfx_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" + if axis[:draw_arrow] + push!(opt, string("axis ", letter, " line") => axispos) + else + # the * after line disables the arrow at the axis + push!(opt, string("axis ", letter, " line*") => axispos) + end + end + + if framestyle == :zerolines + push!(opt, string("extra ", letter, " ticks") => "0") + push!(opt, string("extra ", letter, " tick labels") => "") + push!(opt, string("extra ", letter, " tick style") => PGFPlotsX.Options( + "grid" => "major", + "major grid style" => pgfx_linestyle(pgfx_thickness_scaling(sp), axis[:foreground_color_border], 1.0) + ) + ) + end + + if !axis[:showaxis] + push!(opt, "separate axis lines") + end + if !axis[:showaxis] || framestyle in (:zerolines, :grid, :none) + push!(opt, string(letter, " axis line style") => "{draw opacity = 0}") + else + push!(opt, string(letter, " axis line style") => pgfx_linestyle(pgfx_thickness_scaling(sp), axis[:foreground_color_border], 1.0) + ) + end +end +# -------------------------------------------------------------------------------------- +# display calls this and then _display, its called 3 times for plot(1:5) +# 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{PGFPlotsXBackend}) + # TODO: make padding more intelligent + # order: right, top, left, bottom + sp.minpad = (22mm + sp[:right_margin], + 12mm + sp[:top_margin], + 2mm + sp[:left_margin], + 10mm + sp[:bottom_margin]) +end + +function _create_backend_figure(plt::Plot{PGFPlotsXBackend}) + plt.o = PGFPlotsXPlot() +end + +function _series_added(plt::Plot{PGFPlotsXBackend}, series::Series) + plt.o.is_created = false +end + +function _update_plot_object(plt::Plot{PGFPlotsXBackend}) + plt.o(plt) +end + +for mime in ("application/pdf", "image/png", "image/svg+xml") + @eval function _show(io::IO, mime::MIME{Symbol($mime)}, plt::Plot{PGFPlotsXBackend}) + show(io, mime, plt.o.the_plot) + end +end + +function _show(io::IO, mime::MIME{Symbol("application/x-tex")}, plt::Plot{PGFPlotsXBackend}) + PGFPlotsX.print_tex(io, plt.o.the_plot, include_preamble = plt.attr[:tex_output_standalone]) +end + +function _display(plt::Plot{PGFPlotsXBackend}) + display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot) +end diff --git a/src/init.jl b/src/init.jl index 2e1a3404..ee629f73 100644 --- a/src/init.jl +++ b/src/init.jl @@ -46,6 +46,12 @@ function __init__() @require Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Revise.track(Plots, fn) end + @require PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" begin + fn = joinpath(@__DIR__, "backends", "pgfplotsx.jl") + include(fn) + @require Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Revise.track(Plots, fn) + end + @require PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" begin fn = joinpath(@__DIR__, "backends", "plotlyjs.jl") include(fn) diff --git a/test/runtests.jl b/test/runtests.jl index 55c7e963..16ad485f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,8 @@ using Gtk using LibGit2 using GeometryTypes +include("test_pgfplotsx.jl") + reference_dir(args...) = joinpath(homedir(), ".julia", "dev", "PlotReferenceImages", args...) function reference_file(backend, i, version) diff --git a/test/test_pgfplotsx.jl b/test/test_pgfplotsx.jl new file mode 100644 index 00000000..ca033a52 --- /dev/null +++ b/test/test_pgfplotsx.jl @@ -0,0 +1,136 @@ +using Plots, Test +pgfplotsx() + +function create_plot( args...; kwargs... ) + pgfx_plot = plot(args...; kwargs...) + return pgfx_plot, repr("application/x-tex", pgfx_plot) +end + +function create_plot!( args...; kwargs... ) + pgfx_plot = plot!(args...; kwargs...) + return pgfx_plot, repr("application/x-tex", pgfx_plot) +end + +@testset "PGFPlotsX" begin + pgfx_plot = plot(1:5) + Plots._update_plot_object(pgfx_plot) + @test pgfx_plot.o.the_plot isa PGFPlotsX.TikzDocument + @test pgfx_plot.series_list[1].plotattributes[:quiver] === nothing + + @testset "3D docs example" begin + n = 100 + ts = range(0, stop=8π, length=n) + x = ts .* map(cos, ts) + y = (0.1ts) .* map(sin, ts) + z = 1:n + pl = plot(x, y, z, zcolor=reverse(z), m=(10, 0.8, :blues, Plots.stroke(0)), leg=false, cbar=true, w=5) + pgfx_plot = plot!(pl, zeros(n), zeros(n), 1:n, w=10) + Plots._update_plot_object(pgfx_plot) + if @test_nowarn(haskey(Plots.pgfx_axes(pgfx_plot.o)[1].options.dict, "colorbar") == true) + @test Plots.pgfx_axes(pgfx_plot.o)[1]["colorbar"] === nothing + end + end # testset + @testset "Color docs example" begin + y = rand(100) + plot(0:10:100, rand(11, 4), lab="lines", w=3, palette=:grays, fill=0, α=0.6) + pl = scatter!(y, zcolor=abs.(y .- 0.5), m=(:heat, 0.8, Plots.stroke(1, :green)), ms=10 * abs.(y .- 0.5) .+ 4, lab="grad") + Plots._update_plot_object(pl) + axis = Plots.pgfx_axes(pl.o)[1] + @test count( x->x isa PGFPlotsX.LegendEntry, axis.contents ) == 5 + @test count( x->x isa PGFPlotsX.Plot, axis.contents ) == 108 # each marker is its own plot, fillranges create 2 plot-objects + marker = axis.contents[15] + @test marker isa PGFPlotsX.Plot + @test marker.options["mark"] == "*" + @test marker.options["mark options"]["color"] == RGBA{Float64}( colorant"green", 0.8) + @test marker.options["mark options"]["line width"] == 1 + end # testset + @testset "Plot in pieces" begin + plot(rand(100) / 3, reg=true, fill=(0, :green)) + scatter!(rand(100), markersize=6, c=:orange) + end # testset + @testset "Marker types" begin + markers = filter((m->begin + m in Plots.supported_markers() + end), Plots._shape_keys) + markers = reshape(markers, 1, length(markers)) + n = length(markers) + x = (range(0, stop=10, length=n + 2))[2:end - 1] + y = repeat(reshape(reverse(x), 1, :), n, 1) + scatter(x, y, m=(8, :auto), lab=map(string, markers), bg=:linen, xlim=(0, 10), ylim=(0, 10)) + end # testset + @testset "Layout" begin + plot(Plots.fakedata(100, 10), layout=4, palette=[:grays :blues :heat :lightrainbow], bg_inside=[:orange :pink :darkblue :black]) + end # testset + @testset "Polar plots" begin + Θ = range(0, stop=1.5π, length=100) + r = abs.(0.1 * randn(100) + sin.(3Θ)) + plot(Θ, r, proj=:polar, m=2) + end # testset + @testset "Drawing shapes" begin + verts = [(-1.0, 1.0), (-1.28, 0.6), (-0.2, -1.4), (0.2, -1.4), (1.28, 0.6), (1.0, 1.0), (-1.0, 1.0), (-0.2, -0.6), (0.0, -0.2), (-0.4, 0.6), (1.28, 0.6), (0.2, -1.4), (-0.2, -1.4), (0.6, 0.2), (-0.2, 0.2), (0.0, -0.2), (0.2, 0.2), (-0.2, -0.6)] + x = 0.1:0.2:0.9 + y = 0.7 * rand(5) .+ 0.15 + plot(x, y, line=(3, :dash, :lightblue), marker=(Shape(verts), 30, RGBA(0, 0, 0, 0.2)), bg=:pink, fg=:darkblue, xlim=(0, 1), ylim=(0, 1), leg=false) + end # testset + @testset "Histogram 2D" begin + histogram2d(randn(10000), randn(10000), nbins=20) + end # testset + @testset "Heatmap-like" begin + xs = [string("x", i) for i = 1:10] + ys = [string("y", i) for i = 1:4] + z = float((1:4) * reshape(1:10, 1, :)) + pgfx_plot = heatmap(xs, ys, z, aspect_ratio=1) + Plots._update_plot_object(pgfx_plot) + if @test_nowarn(haskey(Plots.pgfx_axes(pgfx_plot.o)[1].options.dict, "colorbar") == true) + @test Plots.pgfx_axes(pgfx_plot.o)[1]["colorbar"] === nothing + @test Plots.pgfx_axes(pgfx_plot.o)[1]["colormap name"] == "plots1" + end + + pgfx_plot = wireframe(xs, ys, z, aspect_ratio=1) + # TODO: clims are wrong + end # testset + @testset "Contours" begin + x = 1:0.5:20 + y = 1:0.5:10 + f(x, y) = begin + (3x + y ^ 2) * abs(sin(x) + cos(y)) + end + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(y, 1, length(x)) + Z = map(f, X, Y) + p2 = contour(x, y, Z) + p1 = contour(x, y, f, fill=true) + plot(p1, p2) + # TODO: colorbar for filled contours + end # testset + @testset "Varying colors" begin + t = range(0, stop=1, length=100) + θ = (6π) .* t + x = t .* cos.(θ) + y = t .* sin.(θ) + p1 = plot(x, y, line_z=t, linewidth=3, legend=false) + p2 = scatter(x, y, marker_z=((x, y)->begin + x + y + end), color=:bluesreds, legend=false) + plot(p1, p2) + end # testset + @testset "Framestyles" begin + scatter(fill(randn(10), 6), fill(randn(10), 6), framestyle=[:box :semi :origin :zerolines :grid :none], title=[":box" ":semi" ":origin" ":zerolines" ":grid" ":none"], color=permutedims(1:6), layout=6, label="", markerstrokewidth=0, ticks=-2:2) + # TODO: support :semi + end # testset + @testset "Quiver" begin + x = -2pi:0.2:2*pi + y = sin.(x) + + u = ones(length(x)) + v = cos.(x) + plot( x, y, quiver = (u, v), arrow = true ) + # TODO: could adjust limits to fit arrows if too long, but how? + end # testset + @testset "Annotations" begin + y = rand(10) + plot(y, annotations=(3, y[3], Plots.text("this is \\#3", :left)), leg=false) + annotate!([(5, y[5], Plots.text("this is \\#5", 16, :red, :center)), (10, y[10], Plots.text("this is \\#10", :right, 20, "courier"))]) + scatter!(range(2, stop=8, length=6), rand(6), marker=(50, 0.2, :orange), series_annotations=["series", "annotations", "map", "to", "series", Plots.text("data", :green)]) + end # testset +end # testset