Plots.jl/src/backends/gaston.jl
Nicholas Bauer 70b635dbe1
Cache axis args in a dictionary (#3775)
Co-authored-by: t-bltg <tf.bltg@gmail.com>
Co-authored-by: Simon Christ <SimonChrist@gmx.de>
2021-08-31 21:36:58 +02:00

608 lines
20 KiB
Julia

# https://github.com/mbaz/Gaston.
# --------------------------------------------
# These functions are called by Plots
# --------------------------------------------
# Create the window/figure for this backend.
function _create_backend_figure(plt::Plot{GastonBackend})
state_handle = Gaston.nexthandle() # for now all the figures will be kept
plt.o = Gaston.newfigure(state_handle)
end
function _before_layout_calcs(plt::Plot{GastonBackend})
# Initialize all the subplots first
plt.o.subplots = Gaston.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
if (n = n1 + n2) != 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
for sp in plt.subplots
sp === nothing && continue
for ann in sp[:annotations]
x, y, val = locate_annotation(sp, ann...)
sp.o.axesconf *= "\nset label '$(val.str)' at $x,$y $(gaston_font(val.font))"
end
end
nothing
end
function _update_min_padding!(sp::Subplot{GastonBackend})
sp.minpad = 0mm, 0mm, 0mm, 0mm
nothing
end
function _update_plot_object(plt::Plot{GastonBackend})
# respect the layout ratio
dat = gaston_multiplot_pos_size(plt.layout, (0, 0, 1, 1))
gaston_multiplot_pos_size!(dat)
nothing
end
for (mime, term) in (
"application/eps" => "epscairo",
"image/eps" => "epslatex",
"application/pdf" => "pdfcairo",
"application/postscript" => "postscript",
"image/png" => "png",
"image/svg+xml" => "svg",
"text/latex" => "tikz",
"application/x-tex" => "epslatex",
"text/plain" => "dumb",
)
@eval function _show(io::IO, ::MIME{Symbol($mime)}, plt::Plot{GastonBackend})
term = String($term)
tmpfile = "$(Gaston.tempname()).$term"
Gaston.save(
term = term,
output = tmpfile,
handle = plt.o.handle,
saveopts = gaston_saveopts(plt),
)
while !isfile(tmpfile)
end # avoid race condition with read in next line
write(io, read(tmpfile))
rm(tmpfile, force = true)
nothing
end
end
_display(plt::Plot{GastonBackend}) = display(plt.o)
# --------------------------------------------
# These functions are gaston specific
# --------------------------------------------
function gaston_saveopts(plt::Plot{GastonBackend})
saveopts = String["size $(join(plt.attr[:size], ","))"]
push!(
saveopts,
gaston_font(
plottitlefont(plt),
rot = false,
align = false,
color = false,
scale = 1,
),
)
push!(saveopts, "background $(gaston_color(plt.attr[:background_color]))")
# push!(saveopts, "title '$(plt.attr[:window_title])'")
# Scale all plot elements to match Plots.jl DPI standard
scaling = plt.attr[:dpi] / Plots.DPI
push!(saveopts, "fontscale $scaling lw $scaling dl $scaling ps $scaling")
return join(saveopts, " ")
end
function gaston_get_subplots(n, plt_subplots, layout)
nr, nc = size(layout)
sps = Array{Any}(nothing, nr, nc)
for r in 1:nr, c in 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 in 1:nc, r in 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::Union{Nothing,Subplot{GastonBackend}},
)
if sp === nothing
push!(plt.o.subplots, sp)
else
dims =
RecipesPipeline.is3d(sp) ||
sp.attr[:projection] == "3d" ||
needs_any_3d_axes(sp) ? 3 : 2
any_label = false
for series in series_list(sp)
if dims == 2 && series[:seriestype] (:heatmap, :contour)
dims = 3 # we need heatmap/contour to use splot, not plot
end
any_label |= should_add_to_legend(series)
end
sp.o = Gaston.Plot(
dims = dims,
curves = [],
axesconf = gaston_parse_axes_args(plt, sp, dims, any_label),
)
push!(plt.o.subplots, sp.o)
end
nothing
end
function gaston_multiplot_pos_size(layout, parent_xy_wh)
nr, nc = size(layout)
dat = Array{Any}(nothing, nr, nc)
for r in 1:nr, c in 1:nc
l = layout[r, c]
# width and height (pct) are multiplicative (parent)
w = layout.widths[c].value * parent_xy_wh[3]
h = layout.heights[r].value * parent_xy_wh[4]
if isa(l, EmptyLayout)
dat[r, c] = (c - 1) * w, (r - 1) * h, w, h, nothing
else
# previous position (origin)
prev_r = r > 1 ? dat[r - 1, c] : nothing
prev_c = c > 1 ? dat[r, c - 1] : nothing
prev_r isa Array && (prev_r = prev_r[end, end])
prev_c isa Array && (prev_c = prev_c[end, end])
x = prev_c !== nothing ? prev_c[1] + prev_c[3] : parent_xy_wh[1]
y = prev_r !== nothing ? prev_r[2] + prev_r[4] : parent_xy_wh[2]
if l isa GridLayout
sub = gaston_multiplot_pos_size(l, (x, y, w, h))
dat[r, c] = size(sub) == (1, 1) ? only(sub) : sub
else
dat[r, c] = x, y, w, h, l
end
end
end
return dat
end
function gaston_multiplot_pos_size!(dat)
nr, nc = size(dat)
for r in 1:nr, c in 1:nc
xy_wh_sp = dat[r, c]
if xy_wh_sp isa Array
gaston_multiplot_pos_size!(xy_wh_sp)
elseif xy_wh_sp isa Tuple
x, y, w, h, sp = xy_wh_sp
sp === nothing && continue
sp.o === nothing && continue
# gnuplot screen coordinates: bottom left at 0,0 and top right at 1,1
sp.o.axesconf = "set origin $x, $(1 - y - h)\nset size $w, $h\n" * sp.o.axesconf
end
end
nothing
end
function gaston_add_series(plt::Plot{GastonBackend}, series::Series)
sp = series[:subplot]
gsp = sp.o
x, y, z = series[:x], series[:y], series[:z]
st = series[:seriestype]
curves = []
if gsp.dims == 2 && z === nothing
for (n, seg) in enumerate(series_segments(series, st; check = true))
i, rng = seg.attr_index, seg.range
fr = _cycle(series[:fillrange], 1:length(x[rng]))
for sc in gaston_seriesconf!(sp, series, i, n == 1)
push!(curves, Gaston.Curve(x[rng], y[rng], nothing, fr, sc))
end
end
else
if z isa Surface
z = z.surf
if st == :image
z = reverse(Float32.(Gray.(z)), dims = 1) # flip y axis
nr, nc = size(z)
if (ly = length(y)) == 2 && ly != nr
y = collect(range(y[1], y[2], length = nr))
end
if (lx = length(x)) == 2 && lx != nc
x = collect(range(x[1], x[2], length = nc))
end
elseif st == :heatmap
length(x) == size(z, 2) + 1 && (x = @view x[1:(end - 1)])
length(y) == size(z, 1) + 1 && (y = @view y[1:(end - 1)])
end
end
if st == :mesh3d
x, y, z = mesh3d_triangles(x, y, z, series[:connections])
end
for sc in gaston_seriesconf!(sp, series, 1, true)
push!(curves, Gaston.Curve(x, y, z, nothing, sc))
end
end
for c in curves
append = length(gsp.curves) > 0
push!(gsp.curves, c)
Gaston.write_data(c, gsp.dims, gsp.datafile, append = append)
end
nothing
end
function gaston_seriesconf!(
sp::Subplot{GastonBackend},
series::Series,
i::Int,
add_to_legend::Bool,
)
#=
gnuplot abbreviations (see gnuplot/src/set.c)
---------------------------------------------
dl: dashlength
dt: dashtype
fc: fillcolor
fs: fillstyle
lc: linecolor
lp: linespoints
ls: linestyle
lt: linetype
lw: linewidth
pi: pointinterval
pn: pointnumber
ps: pointscale
pt: pointtype
tc: textcolor
w: with
=#
gsp = sp.o
st = series[:seriestype]
extra = []
add_to_legend &= should_add_to_legend(series)
curveconf = String[add_to_legend ? "title '$(series[:label])'" : "notitle"]
clims = get_clims(sp, series)
if st (:scatter, :scatter3d)
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
pt, ps, mc = gaston_mk_ms_mc(series, clims, i)
push!(curveconf, "w points pt $pt ps $ps lc $mc")
elseif st (:path, :straightline, :path3d)
fr = series[:fillrange]
fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i))
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
if fr !== nothing # filled curves, but not filled curves with markers
push!(
curveconf,
"w filledcurves fc $fc fs solid border lc $lc lw $lw dt $dt,'' w lines lc $lc lw $lw dt $dt",
)
elseif series[:markershape] == :none # simplepath
push!(curveconf, "w lines lc $lc dt $dt lw $lw")
else
pt, ps, mc = gaston_mk_ms_mc(series, clims, i)
push!(curveconf, "w lp lc $mc dt $dt lw $lw pt $pt ps $ps")
end
elseif st == :shape
fc = gaston_color(get_fillcolor(series, i), get_fillalpha(series, i))
lc, _ = gaston_lc_ls_lw(series, clims, i)
push!(curveconf, "w filledcurves fc $fc fs solid border lc $lc")
elseif st (:steppre, :stepmid, :steppost)
step = if st == :steppre
"fsteps"
elseif st == :stepmid
"histeps"
elseif st == :steppost
"steps"
end
push!(curveconf, "w $step")
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
push!(extra, "w points lc $lc dt $dt lw $lw notitle")
elseif st == :image
palette = gaston_palette(series[:seriescolor])
gsp.axesconf *= "\nset palette model RGB defined $palette"
push!(curveconf, "w image pixels")
elseif st (:contour, :contour3d)
push!(curveconf, "w lines")
st == :contour && (gsp.axesconf *= "\nset view map\nunset surface") # 2D
levels = join(map(string, collect(contour_levels(series, clims))), ", ")
gsp.axesconf *= "\nset contour base\nset cntrparam levels discrete $levels"
elseif st (:surface, :heatmap)
push!(curveconf, "w pm3d")
palette = gaston_palette(series[:seriescolor])
gsp.axesconf *= "\nset palette model RGB defined $palette"
st == :heatmap && (gsp.axesconf *= "\nset view map")
elseif st (:wireframe, :mesh3d)
lc, dt, lw = gaston_lc_ls_lw(series, clims, i)
push!(curveconf, "w lines lc $lc dt $dt lw $lw")
elseif st == :quiver
push!(curveconf, "w vectors filled")
else
@warn "Gaston: $st is not implemented yet"
end
return [join(curveconf, " "), extra...]
end
function gaston_parse_axes_args(
plt::Plot{GastonBackend},
sp::Subplot{GastonBackend},
dims::Int,
any_label::Bool,
)
# axesconf = String["set margins 2, 2, 2, 2"] # left, right, bottom, top
axesconf = String[]
polar = ispolar(sp) && dims == 2 # cannot splot in polar coordinates
for letter in (:x, :y, :z)
(letter == :z && dims == 2) && continue
axis = sp.attr[get_attr_symbol(letter, :axis)]
# label names
push!(
axesconf,
"set $(letter)label '$(axis[:guide])' $(gaston_font(guidefont(axis)))",
)
mirror = axis[:mirror] ? "mirror" : "nomirror"
if axis[:scale] == :identity
logscale, base = "nologscale", ""
elseif axis[:scale] == :log10
logscale, base = "logscale", "10"
elseif axis[:scale] == :log2
logscale, base = "logscale", "2"
elseif axis[:scale] == :ln
logscale, base = "logscale", "e"
end
push!(axesconf, "set $logscale $letter $base")
# handle ticks
if polar
push!(axesconf, "set size square\nunset $(letter)tics")
else
push!(
axesconf,
"set $(letter)tics $(mirror) $(axis[:tick_direction]) $(gaston_font(tickfont(axis)))",
)
# major tick locations
if axis[:ticks] != :native
if axis[:flip]
hi, lo = axis_limits(sp, letter)
else
lo, hi = axis_limits(sp, letter)
end
push!(axesconf, "set $(letter)range [$lo:$hi]")
ticks = get_ticks(sp, axis)
gaston_set_ticks!(axesconf, ticks, letter, "", "")
if axis[:minorticks] != :native
minor_ticks = get_minor_ticks(sp, axis, ticks)
gaston_set_ticks!(axesconf, minor_ticks, letter, "m", "add")
end
end
end
if axis[:grid]
push!(axesconf, "set grid " * (polar ? "polar" : "$(letter)tics"))
axis[:minorgrid] &&
push!(axesconf, "set grid " * (polar ? "polar" : "m$(letter)tics"))
end
ratio = get_aspect_ratio(sp)
if ratio != :none
ratio == :equal && (ratio = -1)
push!(axesconf, "set size ratio $ratio")
end
end
gaston_set_legend!(axesconf, sp, any_label)
if hascolorbar(sp)
push!(axesconf, "set cbtics $(gaston_font(colorbartitlefont(sp)))")
end
if sp[:title] !== nothing
push!(axesconf, "set title '$(sp[:title])' $(gaston_font(titlefont(sp)))")
end
if polar
push!(axesconf, "unset border\nset polar\nset border polar")
tmin, tmax = axis_limits(sp, :x, false, false)
rmin, rmax = axis_limits(sp, :y, false, false)
rticks = get_ticks(sp, :y)
if (ttype = ticksType(rticks)) == :ticks
gaston_ticks = string.(rticks)
elseif ttype == :ticks_and_labels
gaston_ticks = String["'$l' $t" for (t, l) in zip(rticks...)]
end
push!(
axesconf,
"set rtics ( " *
join(gaston_ticks, ", ") *
" ) $(gaston_font(tickfont(sp.attr[:yaxis])))",
)
push!(axesconf, "set trange [$(min(0, tmin)):$(max(2π, tmax))]")
push!(axesconf, "set rrange [$rmin:$rmax]")
push!(
axesconf,
"set ttics 0,30 format \"%g\".GPVAL_DEGREE_SIGN $(gaston_font(tickfont(sp.attr[:xaxis])))",
)
push!(axesconf, "set mttics 3")
end
return join(axesconf, "\n")
end
function gaston_set_ticks!(axesconf, ticks, letter, maj_min, add)
ticks == :auto && return
if ticks (:none, nothing, false)
push!(axesconf, "unset $(maj_min)$(letter)tics")
return
end
gaston_ticks = String[]
if (ttype = ticksType(ticks)) == :ticks
tick_locs = @view ticks[:]
for i in eachindex(tick_locs)
tick = if maj_min == "m"
"'' $(tick_locs[i]) 1" # see gnuplot manual 'Mxtics'
else
"$(tick_locs[i])"
end
push!(gaston_ticks, tick)
end
elseif ttype == :ticks_and_labels
tick_locs = @view ticks[1][:]
tick_labels = @view ticks[2][:]
for i in eachindex(tick_locs)
lab = gaston_enclose_tick_string(tick_labels[i])
push!(gaston_ticks, "'$lab' $(tick_locs[i])")
end
else
gaston_ticks = nothing
@error "Gaston: invalid input for $(maj_min)$(letter)ticks: $ticks"
end
if gaston_ticks !== nothing
push!(axesconf, "set $(letter)tics $add (" * join(gaston_ticks, ", ") * ")")
end
nothing
end
function gaston_set_legend!(axesconf, sp, any_label)
leg = sp[:legend]
if sp[:legend] (:none, :inline) && any_label
leg == :best && (leg = :topright)
push!(
axesconf,
"set key " * (occursin("outer", string(leg)) ? "outside" : "inside"),
)
for position in ("top", "bottom", "left", "right")
occursin(position, string(leg)) && push!(axesconf, "set key $position")
end
push!(axesconf, "set key $(gaston_font(legendfont(sp), rot=false, align=false))")
if sp[:legendtitle] !== nothing
# NOTE: cannot use legendtitlefont(sp) as it will override legendfont
push!(axesconf, "set key title '$(sp[:legendtitle])'")
end
push!(axesconf, "set key box lw 1 opaque")
push!(axesconf, "set border back")
else
push!(axesconf, "set key off")
end
nothing
end
# --------------------------------------------
# Helpers
# --------------------------------------------
gaston_halign(k) = (left = :left, hcenter = :center, right = :right)[k]
gaston_valign(k) = (top = :top, vcenter = :center, bottom = :bottom)[k]
gaston_alpha(alpha) = alpha === nothing ? 0 : alpha
gaston_lc_ls_lw(series::Series, clims, i::Int) = (
gaston_color(get_linecolor(series, clims, i), get_linealpha(series, i)),
gaston_linestyle(get_linestyle(series, i)),
get_linewidth(series, i),
)
gaston_mk_ms_mc(series::Series, clims, i::Int) = (
gaston_marker(_cycle(series[:markershape], i), get_markeralpha(series, i)),
_cycle(series[:markersize], i) * 1.3 / 5,
gaston_color(get_markercolor(series, clims, i), get_markeralpha(series, i)),
)
function gaston_font(f; rot = true, align = true, color = true, scale = 1)
font = String["font '$(f.family),$(round(Int, scale * f.pointsize))'"]
align && push!(font, "$(gaston_halign(f.halign))")
rot && push!(font, "rotate by $(f.rotation)")
color && push!(font, "textcolor $(gaston_color(f.color))")
return join(font, " ")
end
function gaston_palette(gradient)
palette = String[]
n = -1
for rgba in gradient # FIXME: naive conversion, inefficient ?
push!(palette, "$(n += 1) $(rgba.r) $(rgba.g) $(rgba.b)")
end
return '(' * join(palette, ", ") * ')'
end
function gaston_marker(marker, alpha)
# NOTE: :rtriangle, :ltriangle, :hexagon, :heptagon, :octagon seems unsupported by gnuplot
filled = gaston_alpha(alpha) == 0
marker == :none && return -1
marker == :pixel && return 0
marker (:+, :cross) && return 1
marker (:x, :xcross) && return 2
marker == :star5 && return 3
marker == :rect && return filled ? 5 : 4
marker == :circle && return filled ? 7 : 6
marker == :utriangle && return filled ? 9 : 8
marker == :dtriangle && return filled ? 11 : 10
marker == :diamond && return filled ? 13 : 12
marker == :pentagon && return filled ? 15 : 14
@warn "Gaston: unsupported marker $marker"
return 1
end
function gaston_color(col, alpha = 0)
col = single_color(col) # in case of gradients
col = alphacolor(col, gaston_alpha(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)
findfirst("^", tick_string) === nothing && return tick_string
base, power = split(tick_string, "^")
return "$base^{$power}"
end