# https://github.com/mbaz/Gaston. const G = Gaston const GastonSubplot = G.Plot const GASTON_MARKER_SCALING = 1.3 / 5.0 # -------------------------------------------- # These functions are called by Plots # -------------------------------------------- # Create the window/figure for this backend. function _create_backend_figure(plt::Plot{GastonBackend}) xsize, ysize = plt.attr[:size] G.set(termopts="size $xsize,$ysize") state_handle = G.nexthandle() # for now all the figures will be kept plt.o = G.newfigure(state_handle) end function _before_layout_calcs(plt::Plot{GastonBackend}) # Initialize all the subplots first plt.o.subplots = G.SubPlot[] n1 = n2 = 0 if length(plt.inset_subplots) > 0 n1, sps = gaston_get_subplots(0, plt.inset_subplots, plt.layout) gaston_init_subplots(plt, sps) end if length(plt.subplots) > 0 n2, sps = gaston_get_subplots(0, plt.subplots, plt.layout) end n = n1 + n2 if n != length(plt.subplots) @error "Gaston: $n != $(length(plt.subplots))" end plt.o.layout = gaston_init_subplots(plt, sps) # Then add the series (curves in gaston) for series in plt.series_list gaston_add_series(plt, series) end nothing end function _update_min_padding!(sp::Subplot{GastonBackend}) # FIXME: make this more flexible sp.minpad = (20mm, 5mm, 2mm, 10mm) end _update_plot_object(plt::Plot{GastonBackend}) = nothing for (mime, term) in ( "application/eps" => "epscairo", # NEED fixing TODO "image/eps" => "epslatex", # NEED fixing TODO "application/pdf" => "pdfcairo", # NEED fixing TODO "application/postscript" => "postscript", # NEED fixing TODO "image/svg+xml" => "svg", "text/latex" => "tikz", # NEED fixing TODO "application/x-tex" => "epslatex", # NEED fixing TODO "text/plain" => "dumb", # NEED fixing TODO ) @eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend}) tmpfile = G.tempname() * "." * $term G.save( term=$term, output=tmpfile, handle=plt.o.handle, ) while !isfile(tmpfile) end # avoid race condition with read in next line write(io, read(tmpfile)) rm(tmpfile, force=true) nothing end end function _show(io::IO, mime::MIME{Symbol("image/png")}, plt::Plot{GastonBackend}) scaling = plt.attr[:dpi] / Plots.DPI # try to respect the layout ratio _, sps = gaston_get_subplots(0, plt.subplots, plt.layout) nr, nc = size(sps); n = 0 for c ∈ 1:nc, r ∈ 1:nr # NOTE: row major sp = plt.o.subplots[n += 1] w = plt.layout.widths[r] h = plt.layout.widths[c] sp.axesconf = "set size $(w.value),$(h.value)\n" * sp.axesconf end # Scale all plot elements to match Plots.jl DPI standard saveopts = "fontscale $scaling lw $scaling dl $scaling ps $scaling" tmpfile = G.tempname() G.save( term="pngcairo", output=tmpfile, handle=plt.o.handle, saveopts=saveopts ) while !isfile(tmpfile) end # avoid race condition with read in next line write(io, read(tmpfile)) rm(tmpfile, force=true) nothing end _display(plt::Plot{GastonBackend}) = display(plt.o) # -------------------------------------------- # These functions are gaston specific # -------------------------------------------- function gaston_get_subplots(n, plt_subplots, layout) nr, nc = size(layout) sps = Array{Any}(undef, nr, nc) for r ∈ 1:nr, c ∈ 1:nc # NOTE: col major l = layout[r, c] if l isa GridLayout n, sub = gaston_get_subplots(n, plt_subplots, l) sps[r, c] = size(sub) == (1, 1) ? only(sub) : sub else sps[r, c] = get(l.attr, :blank, false) ? nothing : plt_subplots[n += 1] end end return n, sps end function gaston_init_subplots(plt, sps) sz = nr, nc = size(sps) for c ∈ 1:nc, r ∈ 1:nr # NOTE: row major sp = sps[r, c] if sp isa Subplot || sp === nothing gaston_init_subplot(plt, sp) else gaston_init_subplots(plt, sp) sz = max.(sz, size(sp)) end end return sz end function gaston_init_subplot(plt::Plot{GastonBackend}, sp::Subplot{GastonBackend}) if sp === nothing push!(plt.o.subplots, sp) else sp.o = GastonSubplot( dims=RecipesPipeline.is3d(sp) ? 3 : 2, axesconf=gaston_parse_axes_args(plt, sp), # Gnuplot string curves=[] ) push!(plt.o.subplots, sp.o) end nothing end function gaston_add_series(plt::Plot{GastonBackend}, series::Series) # Gaston.Curve = Plots.Series sp = series[:subplot] g_sp = sp.o # Gaston subplot object if series[:seriestype] ∈ (:heatmap, :contour) && g_sp.dims == 2 g_sp.dims = 3 # FIXME: this is ugly, we need heatmap/contour to use splot, not plot end x = series[:x] y = series[:y] z = g_sp.dims == 2 ? nothing : series[:z] if z isa Surface z = z.surf end seriesconf = gaston_seriesconf!(sp, series) # Gnuplot string c = G.Curve(x, y, z, nothing, seriesconf) isfirst = length(g_sp.curves) == 0 ? true : false push!(g_sp.curves, c) G.write_data(c, g_sp.dims, g_sp.datafile, append = isfirst ? false : true) nothing end function gaston_seriesconf!(sp, series::Series) gsp = sp.o curveconf = String[] st = series[:seriestype] clims = get_clims(sp, series) label = "title \"$(series[:label])\"" if st ∈ (:scatter, :scatter3d) pt, ps, lc = gaston_mk_ms_mc(series) push!(curveconf, "with points pt $pt ps $ps lc $lc") elseif st ∈ (:path, :straightline, :path3d) lc, dt, lw = gaston_lc_ls_lw(series) if series[:markershape] == :none # simplepath push!(curveconf, "with lines lc $lc dt $dt lw $lw") else pt, ps = gaston_mk_ms_mc(series) push!(curveconf, "with lp lc $lc dt $dt lw $lw pt $pt ps $ps") end elseif st == :shape fc = gaston_color(series[:fillcolor], series[:fillalpha]) fs = "solid" lc, _ = gaston_lc_ls_lw(series) push!(curveconf, "with filledcurves fc $fc fs $fs border lc $lc") elseif st == :steppre push!(curveconf, "with steps") elseif st == :steppost push!(curveconf, "with fsteps") # Not sure if not the other way elseif st ∈ (:contour, :contour3d) label = "notitle" push!(curveconf, "with lines") if st == :contour gsp.axesconf *= "\nset view map\nunset surface" end gsp.axesconf *= "\nunset key" levels = join(map(string, collect(contour_levels(series, clims))), ", ") gsp.axesconf *= "\nset contour base\nset cntrparam levels discrete $levels" elseif st ∈ (:surface, :heatmap) palette = gaston_palette(series[:seriescolor]) gsp.axesconf *= "\nset palette model RGB defined $palette" if st == :heatmap gsp.axesconf *= "\nset view map" end push!(curveconf, "with pm3d") elseif st == :wireframe lc, dt, lw = gaston_lc_ls_lw(series) push!(curveconf, "with lines lc $lc dt $dt lw $lw") else @warn "Gaston: $st is not implemented yet" end push!(curveconf, label) return join(curveconf, " ") end function gaston_parse_axes_args(plt::Plot{GastonBackend}, sp::Subplot{GastonBackend}) axesconf = String[] # Standard 2d axis if !ispolar(sp) && !RecipesPipeline.is3d(sp) # TODO: configure grid, axis spines, thickness end for letter in (:x, :y, :z) axis_attr = sp.attr[Symbol(letter, :axis)] # label names push!(axesconf, "set $(letter)label \"$(axis_attr[:guide])\"") push!(axesconf, "set $(letter)label font \"$(axis_attr[:guidefontfamily]), $(axis_attr[:guidefontsize])\"") # Handle ticks # ticksyle push!(axesconf, "set $(letter)tics font \"$(axis_attr[:tickfontfamily]), $(axis_attr[:tickfontsize])\"") push!(axesconf, "set $(letter)tics textcolor rgb \"#$(hex(axis_attr[:tickfontcolor], :rrggbb))\"") push!(axesconf, "set $(letter)tics $(axis_attr[:tick_direction])") mirror = axis_attr[:mirror] ? "mirror" : "nomirror" push!(axesconf, "set $(letter)tics $(mirror)") logscale = if axis_attr[:scale] == :identity "nologscale" elseif axis_attr[:scale] == :log10 "logscale" end push!(axesconf, "set $logscale $(letter)") # tick locations if axis_attr[:ticks] != :native # axis limits from, to = axis_limits(sp, letter) push!(axesconf, "set $(letter)range [$from:$to]") ticks = get_ticks(sp, axis_attr) gaston_set_ticks!(axesconf, ticks, letter) end ratio = get_aspect_ratio(sp) if ratio != :none if ratio == :equal ratio = -1 end push!(axesconf, "set size ratio $ratio") end end gaston_set_legend!(axesconf, sp) # Set legend params if sp[:title] != nothing push!(axesconf, "set title '$(sp[:title])'") push!(axesconf, "set title font '$(sp[:titlefontfamily]), $(sp[:titlefontsize])'") end return join(axesconf, "\n") end function gaston_set_ticks!(axesconf, ticks, letter) ticks == :auto && return if ticks ∈ (:none, nothing, false) push!(axesconf, "unset $(letter)tics") return end ttype = ticksType(ticks) if ttype == :ticks tick_locations = @view ticks[:] gaston_tick_string = [] for i in eachindex(tick_locations) lac = tick_locations[i] push!(gaston_tick_string, "$loc") end push!( axesconf, "set $(letter)tics (" * join(gaston_tick_string, ", ") * ")" ) elseif ttype == :ticks_and_labels tick_locations = @view ticks[1][:] tick_labels = @view ticks[2][:] gaston_tick_string = [] for i in eachindex(tick_locations) loc = tick_locations[i] lab = gaston_enclose_tick_string(tick_labels[i]) push!(gaston_tick_string, "'$lab' $loc") end push!( axesconf, "set $(letter)tics (" * join(gaston_tick_string, ", ") * ")" ) else error("Invalid input for $(letter)ticks: $ticks") end nothing end function gaston_set_legend!(axesconf, sp) leg = sp[:legend] if sp[:legend] ∉ (:none, :inline) if leg == :best leg = :topright end if occursin("outer", string(leg)) push!(axesconf, "set key outside") else push!(axesconf, "set key inside") end positions = ["top", "bottom", "left", "right"] for position in positions if occursin(position, string(leg)) push!(axesconf, "set key $position") end end if sp[:legendtitle] != nothing push!(axesconf, "set key title '$(sp[:legendtitle])'") end push!(axesconf, "set key box linewidth 1") push!(axesconf, "set key opaque") push!(axesconf, "set border back") push!(axesconf, "set key font \"$(sp[:legendfontfamily]), $(sp[:legendfontsize])\"") else push!(axesconf, "set key off") end nothing end # -------------------------------------------- # Helpers # -------------------------------------------- gaston_lc_ls_lw(series::Series) = ( gaston_color(series[:linecolor], series[:linealpha]), gaston_linestyle(series[:linestyle]), series[:linewidth], ) gaston_mk_ms_mc(series::Series) = ( gaston_marker(series[:markershape]), series[:markersize] * GASTON_MARKER_SCALING, gaston_color(series[:markercolor], series[:markeralpha]), ) function gaston_palette(gradient) palette = String[] n = -1 for rgba ∈ gradient # FIXME: naive conversion, inefficient ? push!(palette, "$(n += 1) $(rgba.r) $(rgba.g) $(rgba.b)") end return '(' * join(palette, ", ") * ')' end function gaston_marker(marker) marker == :none && return -1 marker == :circle && return 7 marker == :rect && return 5 marker == :diamond && return 28 marker == :utriangle && return 9 marker == :dtriangle && return 11 marker == :+ && return 1 marker == :x && return 2 marker == :star5 && return 3 marker == :pentagon && return 15 marker == :pixel && return 0 @warn "Unsupported marker $marker" return 1 end function gaston_color(color, alpha=0.) col = single_color(color) # in case of gradients col = alphacolor(col, alpha == nothing ? 0. : alpha) # add a default alpha if non existent return "rgb \"#$(hex(col, :aarrggbb))\"" end function gaston_linestyle(style) style == :solid && return "1" style == :dash && return "2" style == :dot && return "3" style == :dashdot && return "4" style == :dashdotdot && return "5" end function gaston_enclose_tick_string(tick_string) if findfirst("^", tick_string) == nothing return tick_string end base, power = split(tick_string, "^") power = string("{", power, "}") return string(base, "^", power) end