using Contour: Contour using UUIDs using Latexify 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}", ) # pgfplots libraries PGFPlotsX.push_preamble!( pgfx_plot.the_plot, "\\usepgfplotslibrary{patchplots}", ) PGFPlotsX.push_preamble!( pgfx_plot.the_plot, "\\usepgfplotslibrary{fillbetween}", ) # compatibility fixes # add background layer to standard layers PGFPlotsX.push_preamble!( pgfx_plot.the_plot, raw""" \pgfplotsset{% layers/standard/.define layer set={% background,axis background,axis grid,axis ticks,axis lines,axis tick labels,pre main,main,axis descriptions,axis foreground% }{grid style= {/pgfplots/on layer=axis grid},% tick style= {/pgfplots/on layer=axis ticks},% axis line style= {/pgfplots/on layer=axis lines},% label style= {/pgfplots/on layer=axis descriptions},% legend style= {/pgfplots/on layer=axis descriptions},% title style= {/pgfplots/on layer=axis descriptions},% colorbar style= {/pgfplots/on layer=axis descriptions},% ticklabel style= {/pgfplots/on layer=axis tick labels},% axis background@ style={/pgfplots/on layer=axis background},% 3d box foreground style={/pgfplots/on layer=axis foreground},% }, } """, ) pgfx_plot end end ## end user utility functions function pgfx_axes(pgfx_plot::PGFPlotsXPlot) return pgfx_plot.the_plot.elements[1].elements end pgfx_preamble() = pgfx_preamble(current()) 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))) if length(x) == size(s)[1] i, j = j, i end 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_split_extra_opts(extra) return (get(extra, :add, nothing), filter( x-> first(x) != :add, extra)) end function (pgfx_plot::PGFPlotsXPlot)(plt::Plot{PGFPlotsXBackend}) if !pgfx_plot.is_created || pgfx_plot.was_shown pgfx_sanitize_plot!(plt) # extract extra kwargs extra_plot, extra_plot_opt = pgfx_split_extra_opts(plt[:extra_plot_kwargs]) the_plot = PGFPlotsX.TikzPicture(PGFPlotsX.Options(extra_plot_opt...)) if extra_plot !== nothing extra_plot = wraptuple(extra_plot) push!(the_plot, extra_plot...) end 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, "/tikz/background rectangle/.style" => PGFPlotsX.Options( # "draw" => "black", "fill" => cstr, "draw opacity" => a, ), "show background rectangle" => nothing, ) end for sp in plt.subplots bb1 = sp.plotarea bb2 = bbox(sp) sp_width = width(bb2) sp_height = height(bb2) dx, dy = bb2.x0 lpad = leftpad(sp) + sp[:left_margin] rpad = rightpad(sp) + sp[:right_margin] tpad = toppad(sp) + sp[:top_margin] bpad = bottompad(sp) + sp[:bottom_margin] dx += lpad dy += tpad axis_height = sp_height - (tpad + bpad) axis_width = sp_width - (rpad + lpad) 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) title_loc = sp[:titlelocation] bgc_inside = plot_color(sp[:background_color_inside]) bgc_inside_a = alpha(bgc_inside) axis_opt = PGFPlotsX.Options( "point meta max" => get_clims(sp)[2], "point meta min" => get_clims(sp)[1], "legend cell align" => "left", "title" => sp[:title], "title style" => PGFPlotsX.Options( pgfx_get_title_pos(title_loc)..., "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), ), "text" => plot_color(sp[:legendfontcolor]), ), "axis background/.style" => PGFPlotsX.Options( "fill" => bgc_inside, "opacity" => bgc_inside_a, ), # These are for layouting "anchor" => "north west", "xshift" => string(dx), "yshift" => string(-dy), ) sp_width > 0 * mm ? push!(axis_opt, "width" => string(axis_width)) : nothing sp_height > 0 * mm ? push!(axis_opt, "height" => string(axis_height)) : nothing # legend position push!(axis_opt["legend style"], pgfx_get_legend_pos(sp[:legend])...) for letter in (:x, :y, :z) if letter != :z || RecipesPipeline.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) if hascolorbar(series) cg = get_colorgradient(series) cm = pgfx_colormap(get_colorgradient(series)) PGFPlotsX.push_preamble!( pgfx_plot.the_plot, """\\pgfplotsset{ colormap={plots$(sp.attr[:subplot_index])}{$cm}, }""", ) push!(axis_opt, "colormap name" => "plots$(sp.attr[:subplot_index])") if cg isa PlotUtils.CategoricalColorGradient push!( axis_opt, "colormap access" => "piecewise const", "colorbar sampled" => nothing, ) end # goto is needed to break out of col and series for @goto colorbar_end end end @label colorbar_end if hascolorbar(sp) colorbar_style = PGFPlotsX.Options( "title" => sp[:colorbar_title], "xticklabel style" => pgfx_get_ticklabel_style(sp, sp[:xaxis]), "yticklabel style" => pgfx_get_ticklabel_style(sp, sp[:yaxis]), ) if sp[:colorbar] === :top push!(colorbar_style, "at" => string((0.5, 1.05)), "anchor" => "south", "xticklabel pos" => "upper", ) end push!( axis_opt, string("colorbar", pgfx_get_colorbar_pos(sp[:colorbar])) => nothing, "colorbar style" => colorbar_style, ) end if RecipesPipeline.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 extra_sp, extra_sp_opt = pgfx_split_extra_opts(sp[:extra_kwargs]) axis = axisf(merge(axis_opt, PGFPlotsX.Options(extra_sp_opt...))) if extra_sp !== nothing extra_sp = wraptuple(extra_sp) push!(axis, extra_sp...) end if sp[:legendtitle] !== nothing push!(axis, PGFPlotsX.Options("\\addlegendimage{empty legend}" => nothing)) push!( axis, PGFPlotsX.LegendEntry( string("\\hspace{-.6cm}{\\textbf{", sp[:legendtitle], "}}"), false, ), ) end for (series_index, series) in enumerate(series_list(sp)) # give each series a uuid for fillbetween series_id = uuid4() _pgfplotsx_series_ids[Symbol("$series_index")] = series_id opt = series.plotattributes st = series[:seriestype] sf = series[:fillrange] series_opt = PGFPlotsX.Options( "color" => single_color(opt[:linecolor]), "name path" => string(series_id), ) extra_series, extra_series_opt = pgfx_split_extra_opts(series[:extra_kwargs]) series_opt = merge(series_opt, PGFPlotsX.Options(extra_series_opt...)) if RecipesPipeline.is3d(series) || st in (:heatmap, :contour) || (st == :quiver && opt[:z] !== nothing) series_func = PGFPlotsX.Plot3 else series_func = PGFPlotsX.Plot end if sf !== nothing && !isfilledcontour(series) && series[:ribbon] === nothing push!(series_opt, "area legend" => nothing) end pgfx_add_series!(Val(st), axis, series_opt, series, series_func, opt) if extra_series !== nothing extra_series = wraptuple(extra_series) push!(axis.contents[end], extra_series...) 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 # for series # add subplot annotations for ann in sp[:annotations] pgfx_add_annotation!( axis, locate_annotation(sp, ann...)..., pgfx_thickness_scaling(sp), ) end push!(the_plot, 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 # for subplots pgfx_plot.is_created = true pgfx_plot.was_shown = false end # if return pgfx_plot end ## seriestype specifics function pgfx_add_series!(axis, series_opt, series, series_func, opt) args = pgfx_series_arguments(series, opt) series_plot = series_func(series_opt, PGFPlotsX.Table(args...)) push!(axis, series_plot) pgfx_add_legend!(axis, series, opt) end function pgfx_add_series!(::Val{:path}, axis, series_opt, series, series_func, opt) # treat segments segments = collect(series_segments(series, series[:seriestype])) sf = opt[:fillrange] for segment in segments i, rng = segment.attr_index, segment.range segment_opt = PGFPlotsX.Options() segment_opt = merge(segment_opt, pgfx_linestyle(opt, i)) if opt[:markershape] != :none marker = _cycle(opt[:markershape], i) if marker isa Shape x = marker.x y = marker.y scale_factor = 0.00125 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!( series[:plot_object].o.the_plot, """ \\pgfdeclareplotmark{PlotsShape$(series[:series_plotindex])}{ \\filldraw $path; } """, ) end segment_opt = merge(segment_opt, pgfx_marker(opt, i)) end # add fillrange if sf !== nothing && !isfilledcontour(series) if sf isa Number || sf isa AVec pgfx_fillrange_series!( axis, series, series_func, i, _cycle(sf, rng), rng, ) elseif sf isa Tuple && series[:ribbon] !== nothing for sfi in sf pgfx_fillrange_series!( axis, series, series_func, i, _cycle(sfi, rng), rng, ) end end if i == 1 && series[:subplot][:legend] != :none && pgfx_should_add_to_legend(series) pgfx_filllegend!(series_opt, opt) end end # handle arrows arrow = opt[:arrow] if arrow isa Arrow arrow_opt = merge( segment_opt, PGFPlotsX.Options("quiver" => PGFPlotsX.Options( "u" => "\\thisrow{u}", "v" => "\\thisrow{v}", pgfx_arrow(arrow, :head) => nothing, ) ) ) if arrow.side == :head x_arrow = opt[:x][rng][end-1:end] y_arrow = opt[:y][rng][end-1:end] x_path = opt[:x][rng][1:end-1] y_path = opt[:y][rng][1:end-1] elseif arrow.side == :tail x_arrow = opt[:x][rng][2:-1:1] y_arrow = opt[:y][rng][2:-1:1] x_path = opt[:x][rng][2:end] y_path = opt[:y][rng][2:end] elseif arrow.side == :both x_arrow = opt[:x][rng][[2,1,end-1,end]] y_arrow = opt[:y][rng][[2,1,end-1,end]] x_path = opt[:x][rng][2:end-1] y_path = opt[:y][rng][2:end-1] end coordinates = PGFPlotsX.Table([ :x => x_arrow[1:2:end-1], :y => y_arrow[1:2:end-1], :u => [x_arrow[i] - x_arrow[i-1] for i in 2:2:lastindex(x_arrow)], :v => [y_arrow[i] - y_arrow[i-1] for i in 2:2:lastindex(y_arrow)], ]) arrow_plot = series_func(merge(series_opt, arrow_opt), coordinates) push!(axis, arrow_plot) coordinates = PGFPlotsX.Table(x_path, y_path) segment_plot = series_func(merge(series_opt, segment_opt), coordinates) push!(axis, segment_plot) else coordinates = PGFPlotsX.Table(pgfx_series_arguments(series, opt, rng)...) segment_plot = series_func(merge(series_opt, segment_opt), coordinates) push!(axis, segment_plot) end # fill between functions if sf isa Tuple && series[:ribbon] === nothing sf1, sf2 = sf @assert sf1 == series_index "First index of the tuple has to match the current series index." push!( axis, series_func( merge( pgfx_fillstyle(opt, series_index), PGFPlotsX.Options("forget plot" => nothing), ), "fill between [of=$series_id and $(_pgfplotsx_series_ids[Symbol(string(sf2))])]", ), ) end pgfx_add_legend!(axis, series, opt, i) end # for segments # get that last marker if !isnothing(opt[:y]) && !any(isnan, opt[:y]) && opt[:markershape] isa AVec additional_plot = PGFPlotsX.PlotInc(pgfx_marker(opt, length(segments) + 1), PGFPlotsX.Coordinates(tuple((last(opt[:x]), last(opt[:y]))))) push!(axis, additional_plot) end end function pgfx_add_series!(::Val{:scatter}, axis, series_opt, series, series_func, opt) push!(series_opt, "only marks" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:straightline}, axis, series_opt, series, series_func, opt) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:path3d}, axis, series_opt, series, series_func, opt) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:scatter3d}, axis, series_opt, series, series_func, opt) push!(series_opt, "only marks" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:surface}, axis, series_opt, series, series_func, opt) push!( series_opt, "surf" => nothing, "mesh/rows" => length(opt[:x]), "mesh/cols" => length(opt[:y]), "z buffer" => "sort", "opacity" => get_fillalpha(series), ) pgfx_add_series!(axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:wireframe}, axis, series_opt, series, series_func, opt) push!(series_opt, "mesh" => nothing, "mesh/rows" => length(opt[:x]) ) pgfx_add_series!(axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:heatmap}, axis, series_opt, series, series_func, opt) push!(axis.options, "view" => "{0}{90}") push!( series_opt, "matrix plot*" => nothing, "mesh/rows" => length(opt[:x]), "mesh/cols" => length(opt[:y]), "point meta" => "\\thisrow{meta}", ) args = pgfx_series_arguments(series, opt) meta = [any(!isfinite, r) ? NaN : r[3] for r in zip(args...)] for arg in args arg[(!isfinite).(arg)] .= 0 end t = PGFPlotsX.Table(["x" => args[1], "y" => args[2], "z" => args[3], "meta" => meta]) series_plot = series_func(series_opt, t) push!(axis, series_plot) pgfx_add_legend!(axis, series, opt) end function pgfx_add_series!(::Val{:mesh3d}, axis, series_opt, series, series_func, opt) ptable = join([ string(i, " ", j, " ", k, "\\\\") for (i, j, k) in zip(opt[:connections]...) ], "\n ") push!( series_opt, "patch" => nothing, "table/row sep" => "\\\\", "patch table" => ptable ) pgfx_add_series!(axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:contour}, axis, series_opt, series, series_func, opt) push!(axis.options, "view" => "{0}{90}") if isfilledcontour(series) pgfx_add_series!(Val(:filledcontour), axis, series_opt, series, series_func, opt) return nothing end pgfx_add_series!(Val(:contour3d), axis, series_opt, series, series_func, opt) return nothing end function pgfx_add_series!(::Val{:filledcontour}, axis, series_opt, series, series_func, opt) push!( series_opt, "contour filled" => PGFPlotsX.Options(), # labels not supported "patch type" => "bilinear", "shader" => "flat", ) if opt[:levels] isa Number push!(series_opt["contour filled"], "number" => opt[:levels]) elseif opt[:levels] isa AVec push!(series_opt["contour filled"], "levels" => opt[:levels]) end pgfx_add_series!(axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:contour3d}, axis, series_opt, series, series_func, opt) push!( series_opt, "contour prepared" => PGFPlotsX.Options("labels" => opt[:contour_labels]), ) series_opt = merge( series_opt, pgfx_linestyle(opt) ) args = pgfx_series_arguments(series, opt) series_plot = series_func(series_opt, PGFPlotsX.Table(Contour.contours(args..., opt[:levels]))) push!(axis, series_plot) pgfx_add_legend!(axis, series, opt) end function pgfx_add_series!(::Val{:quiver}, axis, series_opt, series, series_func, opt) if opt[:quiver] !== nothing push!( series_opt, "quiver" => PGFPlotsX.Options( "u" => "\\thisrow{u}", "v" => "\\thisrow{v}", pgfx_arrow(opt[:arrow]) => nothing, ), ) x = opt[:x] y = opt[:y] z = opt[:z] if z !== nothing push!(series_opt["quiver"], "w" => "\\thisrow{w}") pgfx_axis!(axis.options, series[:subplot], :z) table = PGFPlotsX.Table([ :x => x, :y => y, :z => z, :u => opt[:quiver][1], :v => opt[:quiver][2], :w => opt[:quiver][3], ]) else table = PGFPlotsX.Table([ :x => x, :y => y, :u => opt[:quiver][1], :v => opt[:quiver][2], ]) end end series_plot = series_func(series_opt, table) push!(axis, series_plot) pgfx_add_legend!(axis, series, opt) end function pgfx_add_series!(::Val{:shape}, axis, series_opt, series, series_func, opt) push!(series_opt, "area legend" => nothing) series_opt = merge(series_opt, pgfx_fillstyle(opt)) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:steppre}, axis, series_opt, series, series_func, opt) push!(series_opt, "const plot mark right" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:stepmid}, axis, series_opt, series, series_func, opt) push!(series_opt, "const plot mark mid" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:steppost}, axis, series_opt, series, series_func, opt) push!(series_opt, "const plot" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:ysticks}, axis, series_opt, series, series_func, opt) push!(series_opt, "ycomb" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_series!(::Val{:xsticks}, axis, series_opt, series, series_func, opt) push!(series_opt, "xcomb" => nothing) pgfx_add_series!(Val(:path), axis, series_opt, series, series_func, opt) end function pgfx_add_legend!(axis, series, opt, i = 1) if series[:subplot][:legend] != :none leg_entry = if opt[:label] isa AVec get(opt[:label], i, "") elseif opt[:label] isa AbstractString if i == 1 get(opt, :label, "") else "" end else throw(ArgumentError("Malformed label. label = $(opt[:label])")) end if leg_entry == "" || !pgfx_should_add_to_legend(series) push!(axis.contents[end].options, "forget plot" => nothing) else leg_opt = PGFPlotsX.Options() legend = PGFPlotsX.LegendEntry(leg_opt, leg_entry, false) push!(axis, legend) end end return nothing end pgfx_series_arguments(series, opt, range) = (arg[range] for arg in pgfx_series_arguments(series, opt)) function pgfx_series_arguments(series, opt) st = series[:seriestype] return if st in (:contour, :contour3d) opt[:x], opt[:y], handle_surface(opt[:z]) elseif st in (:heatmap, :surface, :wireframe) surface_to_vecs(opt[:x], opt[:y], opt[:z]) elseif RecipesPipeline.is3d(st) opt[:x], opt[:y], opt[:z] elseif st == :straightline straightline_data(series) elseif st == :shape shape_data(series) elseif ispolar(series) theta, r = opt[:x], opt[:y] rad2deg.(theta), r else opt[:x], opt[:y] end end ## pgfx_get_linestyle(k) = get( ( solid = "solid", dash = "dashed", dot = "dotted", dashdot = "dashdotted", dashdotdot = "dashdotdotted", ), Symbol(k), "solid", ) pgfx_get_marker(k) = get( ( 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 = "|", ), Symbol(k), "*", ) pgfx_get_xguide_pos(k) = get( ( top = "at={(0.5,1)},above,", right = "at={(ticklabel* cs:1.02)}, anchor=west,", left = "at={(ticklabel* cs:-0.02)}, anchor=east,", ), k, "at={(ticklabel cs:0.5)}, anchor=near ticklabel" ) pgfx_get_yguide_pos(k) = get( ( top = "at={(ticklabel* cs:1.02)}, anchor=south", right = "at={(1,0.5)},below,", bottom = "at={(ticklabel* cs:-0.02)}, anchor=north,", ), k, "at={(ticklabel cs:0.5)}, anchor=near ticklabel" ) pgfx_get_legend_pos(k) = get( ( top = ("at" => string((0.5, 0.98)), "anchor" => "north"), bottom = ("at" => string((0.5, 0.02)), "anchor" => "south"), left = ("at" => string((0.02, 0.5)), "anchor" => "west"), right = ("at" => string((0.98, 0.5)), "anchor" => "east"), bottomleft = ("at" => string((0.02, 0.02)), "anchor" => "south west"), bottomright = ("at" => string((0.98, 0.02)), "anchor" => "south east"), topright = ("at" => string((0.98, 0.98)), "anchor" => "north east"), topleft = ("at" => string((0.02, 0.98)), "anchor" => "north west"), outertop = ("at" => string((0.5, 1.02)), "anchor" => "south"), outerbottom = ("at" => string((0.5, -0.02)), "anchor" => "north"), outerleft = ("at" => string((-0.02, 0.5)), "anchor" => "east"), outerright = ("at" => string((1.02, 0.5)), "anchor" => "west"), outerbottomleft = ("at" => string((-0.02, -0.02)), "anchor" => "north east"), outerbottomright = ("at" => string((1.02, -0.02)), "anchor" => "north west"), outertopright = ("at" => string((1.02, 1)), "anchor" => "north west"), outertopleft = ("at" => string((-0.02, 1)), "anchor" => "north east"), ), Symbol(k), ("at" => string((1.02, 1)), "anchor" => "north west"), ) pgfx_get_legend_pos(t::Tuple) = ("at" => "{$(string(t))}", "anchor" => "north west") pgfx_get_legend_pos(nt::NamedTuple) = ("at" => "{$(string(nt.at))}", "anchor" => string(nt.anchor)) pgfx_get_colorbar_pos(s) = get((left = " left", bottom = " horizontal", top = " horizontal"), s, "") pgfx_get_colorbar_pos(b::Bool) = "" pgfx_get_title_pos(s) = get(( left = ("at" => "{(0,1)}", "anchor" => "south west"), right = ("at" => "{(1,1)}", "anchor" => "south east"), ), s, ("at" => "{(0.5,1)}", "anchor" => "south")) pgfx_get_title_pos(t::Tuple) = ("at" => "{$(string(t))}", "anchor" => "south") pgfx_get_title_pos(nt::NamedTuple) = ("at" => "{$(string(nt.at))}", "anchor" => string(nt.anchor)) function pgfx_get_ticklabel_style(sp, axis) cstr = plot_color(axis[:tickfontcolor]) return PGFPlotsX.Options( "font" => pgfx_font( axis[:tickfontsize], pgfx_thickness_scaling(sp) ), "color" => cstr, "draw opacity" => alpha(cstr), "rotate" => axis[:tickfontrotation], ) end ## -------------------------------------------------------------------------------------- # Generates a colormap for pgfplots based on a ColorGradient pgfx_arrow(::Nothing) = "every arrow/.append style={-}" function pgfx_arrow(arr::Arrow, side = arr.side) 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 side == :both || side == :tail push!(components, head) end push!(components, "-") if side == :both || side == :head push!(components, head) end components = join(components, "") return "every arrow/.append style={$(components)}" end function pgfx_filllegend!(series_opt, opt) io = IOBuffer() PGFPlotsX.print_tex(io, pgfx_fillstyle(opt)) style = strip(String(take!(io)), ['[', ']', ' ']) push!(series_opt, "legend image code/.code" => """{ \\draw[$style] (0cm,-0.1cm) rectangle (0.6cm,0.1cm); }""") end pgfx_colormap(cl::PlotUtils.AbstractColorList) = pgfx_colormap(color_list(cl)) function pgfx_colormap(v::Vector{<:Colorant}) join(map(v) do c @sprintf("rgb=(%.8f,%.8f,%.8f)", red(c), green(c), blue(c)) end, "\n") end function pgfx_colormap(cg::ColorGradient) join(map(1:length(cg)) do i @sprintf("rgb(%.8f)=(%.8f,%.8f,%.8f)", cg.values[i], red(cg.colors[i]), green(cg.colors[i]), blue(cg.colors[i])) end, "\n") end function pgfx_framestyle(style::Symbol) if style in (:box, :axes, :origin, :zerolines, :grid, :none) return style else default_style = get((semi = :box,), style, :axes) @warn( "Framestyle :$style is not (yet) supported by the PGFPlotsX 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, pgfx_get_linestyle(linestyle) => 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 # If a particular fontsize parameter is `nothing`, produce a figure that doesn't specify the # font size, and therefore uses whatever fontsize is utilised by the doc in which the # figure is located. function pgfx_font(fontsize::Nothing, thickness_scaling = 1, font = "\\selectfont") return string("{", font, "}") end function pgfx_should_add_to_legend(series::Series) series.plotattributes[:primary] && !( series.plotattributes[:seriestype] in ( :hexbin, :bins2d, :histogram2d, :hline, :vline, :contour, :contourf, :contour3d, :heatmap, :image, ) ) 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.75 * _cycle(plotattributes[:markersize], i) mark_freq = !any(isnan, plotattributes[:y]) && plotattributes[:markershape] isa AVec ? length(plotattributes[:markershape]) : 1 return PGFPlotsX.Options( "mark" => shape isa Shape ? "PlotsShape$i" : pgfx_get_marker(shape), "mark size" => "$mark_size pt", "mark repeat" => mark_freq, "mark options" => PGFPlotsX.Options( "color" => cstr_stroke, "draw opacity" => a_stroke, "fill" => cstr, "fill opacity" => a, "line width" => pgfx_thickness_scaling(plotattributes) * 0.75 * _cycle(plotattributes[:markerstrokewidth], i), "rotate" => if shape == :dtriangle 180 elseif shape == :rtriangle 270 elseif shape == :ltriangle 90 else 0 end, pgfx_get_linestyle(_cycle(plotattributes[:markerstrokestyle], i)) => 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((center = "", left = "right", right = "left"), 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_y) ribbon_yp = ribbon_ym = repeat(ribbon_y, outer = ribbon_n) elseif ribbon_y isa Tuple ribbon_ym, ribbon_yp = ribbon_y ribbon_nm = length(opt[:y]) ÷ length(ribbon_ym) ribbon_ym = repeat(ribbon_ym, outer = ribbon_nm) ribbon_np = length(opt[:y]) ÷ length(ribbon_yp) ribbon_yp = repeat(ribbon_yp, outer = ribbon_np) else ribbon_yp = ribbon_ym = ribbon_y end # upper ribbon rib_uuid = uuid4() ribbon_name_plus = "plots_rib_p$rib_uuid" ribbon_opt_plus = merge( segment_plot.options, PGFPlotsX.Options( "name path" => ribbon_name_plus, "color" => opt[:fillcolor], "draw opacity" => opt[:fillalpha], "forget plot" => nothing, ), ) coordinates_plus = PGFPlotsX.Coordinates(opt[:x], opt[:y] .+ ribbon_yp) ribbon_plot_plus = series_func(ribbon_opt_plus, coordinates_plus) push!(axis, ribbon_plot_plus) # lower ribbon ribbon_name_minus = "plots_rib_m$rib_uuid" ribbon_opt_minus = merge( segment_plot.options, PGFPlotsX.Options( "name path" => ribbon_name_minus, "color" => opt[:fillcolor], "draw opacity" => opt[:fillalpha], "forget plot" => nothing, ), ) coordinates_plus = PGFPlotsX.Coordinates(opt[:x], opt[:y] .- ribbon_ym) ribbon_plot_plus = series_func(ribbon_opt_minus, coordinates_plus) push!(axis, ribbon_plot_plus) # fill push!( axis, series_func( merge( pgfx_fillstyle(opt, series_index), PGFPlotsX.Options("forget plot" => nothing), ), "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 = RecipesPipeline.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.Coordinates(x_fill, y_fill, z_fill) end function pgfx_sanitize_string(p::PlotText) PlotText(pgfx_sanitize_string(p.str), p.font) end function pgfx_sanitize_string(s::AbstractString) s = replace(s, r"\\?\#" => "\\#") s = replace(s, r"\\?\%" => "\\%") s = replace(s, r"\\?\_" => "\\_") s = replace(s, r"\\?\&" => "\\&") s = replace(s, r"\\?\{" => "\\{") s = replace(s, r"\\?\}" => "\\}") s = map(split(s, "")) do s isascii(s) ? s : latexify(s) end |> join end @require LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" begin using .LaTeXStrings function pgfx_sanitize_string(s::LaTeXString) s = replace(s, r"\\?\#" => "\\#") s = replace(s, r"\\?\%" => "\\%") return LaTeXString(s) end end function pgfx_sanitize_plot!(plt) for (key, value) in plt.attr if value isa Union{AbstractString, AbstractVector{<:AbstractString}} plt.attr[key] = pgfx_sanitize_string.(value) end end for subplot in plt.subplots for (key, value) in subplot.attr if key == :annotations && subplot.attr[:annotations] !== nothing old_ann = subplot.attr[key] for i in eachindex(old_ann) subplot.attr[key][i] = (old_ann[i][1], old_ann[i][2], pgfx_sanitize_string(old_ann[i][3])) end elseif value isa Union{AbstractString, AbstractVector{<:AbstractString}} subplot.attr[key] = pgfx_sanitize_string.(value) end end end for series in plt.series_list for (key, value) in series.plotattributes if key == :series_annotations && series.plotattributes[:series_annotations] !== nothing old_ann = series.plotattributes[key].strs for i in eachindex(old_ann) series.plotattributes[key].strs[i] = pgfx_sanitize_string(old_ann[i]) end elseif value isa Union{AbstractString, AbstractVector{<:AbstractString}} series.plotattributes[key] = pgfx_sanitize_string.(value) end end end ## 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], ) tick_color = plot_color(axis[:foreground_color_axis]) push!(opt, "$(letter) tick style" => PGFPlotsX.Options( "color" => color(tick_color), "opacity" => alpha(tick_color), ), ) tick_label_color = plot_color(axis[:tickfontcolor]) push!(opt, "$(letter) tick label style" => PGFPlotsX.Options( "color" => color(tick_color), "opacity" => alpha(tick_color), "rotate" => axis[:rotation] ), ) # set to supported framestyle framestyle = pgfx_framestyle(sp[:framestyle] == false ? :none : sp[:framestyle]) # axis label position labelpos = "" if letter == :x labelpos = pgfx_get_xguide_pos(axis[:guide_position]) elseif letter == :y labelpos = pgfx_get_yguide_pos(axis[:guide_position]) 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") elseif framestyle in (:grid, :zerolines) push!(opt, "$letter tick style" => PGFPlotsX.Options("draw" => "none")) 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 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"), ) push!( opt, string(letter, "ticklabel style") => pgfx_get_ticklabel_style(sp, axis) ) push!( opt, string(letter, " grid style") => pgfx_linestyle( pgfx_thickness_scaling(sp) * axis[:gridlinewidth], axis[:foreground_color_grid], axis[:gridalpha], axis[:gridstyle], ), ) # minor ticks # NOTE: PGFPlots would provide "minor x ticks num", but this only places minor ticks # between major ticks and not outside first and last tick to the axis limits. # Hence, we hack around with extra ticks. Unfortunately this conflicts with # `:zerolines` framestyle hack. So minor ticks are not working with # `:zerolines`. minor_ticks = get_minor_ticks(sp, axis, ticks) if minor_ticks !== nothing minor_ticks = ispolar(sp) && letter == :x ? [rad2deg.(minor_ticks)[3:end]..., 360, 405] : minor_ticks push!( opt, string("extra ", letter, " ticks") => string("{", join(minor_ticks, ","), "}"), ) push!(opt, string("extra ", letter, " tick labels") => "") push!( opt, string("extra ", letter, " tick style") => PGFPlotsX.Options( "grid" => axis[:minorgrid] ? "major" : "none", string(letter, " grid style") => pgfx_linestyle( pgfx_thickness_scaling(sp) * axis[:minorgridlinewidth], axis[:foreground_color_minor_grid], axis[:minorgridalpha], axis[:minorgridstyle], ), "major tick length" => typeof(axis[:minorticks]) <: Integer && axis[:minorticks] > 1 || axis[:minorticks] ? "0.1cm" : "0" ), ) end end # framestyle if framestyle in (:axes, :origin) axispos = axis[:mirror] ? "right" : 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", string(letter, " 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}) leg = sp[:legend] if leg in (:best, :outertopright, :outerright, :outerbottomright) || (leg isa Tuple && leg[1] >= 1) sp.minpad = (0mm, 0mm, 5mm, 0mm) else sp.minpad = (0mm, 0mm, 0mm, 0mm) end 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/svg+xml") @eval function _show( io::IO, mime::MIME{Symbol($mime)}, plt::Plot{PGFPlotsXBackend}, ) plt.o.was_shown = true show(io, mime, plt.o.the_plot) end end function _show( io::IO, mime::MIME{Symbol("image/png")}, plt::Plot{PGFPlotsXBackend}, ) plt.o.was_shown = true plt_file = tempname() * ".png" PGFPlotsX.pgfsave(plt_file, plt.o.the_plot; dpi=plt[:dpi]) write(io, read(plt_file)) rm(plt_file; force = true) end function _show( io::IO, mime::MIME{Symbol("application/x-tex")}, plt::Plot{PGFPlotsXBackend}, ) plt.o.was_shown = true PGFPlotsX.print_tex( io, plt.o.the_plot, include_preamble = plt.attr[:tex_output_standalone], ) end function _display(plt::Plot{PGFPlotsXBackend}) plt.o.was_shown = true display(PGFPlotsX.PGFPlotsXDisplay(), plt.o.the_plot) end