1900 lines
68 KiB
Julia
1900 lines
68 KiB
Julia
module Gnuplot
|
|
|
|
using StatsBase, ColorSchemes, ColorTypes, Colors, StructC14N, DataStructures
|
|
using REPL, ReplMaker
|
|
|
|
import Base.reset
|
|
import Base.write
|
|
import Base.display
|
|
|
|
export session_names, dataset_names, palette_names, linetypes, palette,
|
|
terminal, terminals, test_terminal,
|
|
stats, @gp, @gsp, save, gpexec,
|
|
boxxyerror, contourlines, hist, recipe, gpvars
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ TYPE DEFINITIONS │
|
|
# │ User data representation │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
abstract type Dataset end
|
|
|
|
struct DatasetEmpty <: Dataset
|
|
end
|
|
|
|
mutable struct DatasetText <: Dataset
|
|
preview::Vector{String}
|
|
data::String
|
|
DatasetText(::Val{:inner}, preview, data) = new(preview, data)
|
|
end
|
|
|
|
mutable struct DatasetBin <: Dataset
|
|
file::String
|
|
source::String
|
|
DatasetBin(::Val{:inner}, file, source) = new(file, source)
|
|
end
|
|
|
|
# ---------------------------------------------------------------------
|
|
mutable struct PlotElement
|
|
mid::Int
|
|
is3d::Bool
|
|
cmds::Vector{String}
|
|
name::String
|
|
data::Dataset
|
|
plot::Vector{String}
|
|
|
|
function PlotElement(;mid::Int=0, is3d::Bool=false,
|
|
cmds::Union{String, Vector{String}}=Vector{String}(),
|
|
name::String="",
|
|
data::Dataset=DatasetEmpty(),
|
|
plot::Union{String, Vector{String}}=Vector{String}(),
|
|
kwargs...)
|
|
c = isa(cmds, String) ? [cmds] : cmds
|
|
append!(c, parseKeywords(; kwargs...))
|
|
new(mid, is3d, deepcopy(c), name, data,
|
|
isa(plot, String) ? [plot] : deepcopy(plot))
|
|
end
|
|
end
|
|
|
|
|
|
function display(v::PlotElement)
|
|
if isa(v.data, DatasetText)
|
|
data = "DatasetText"
|
|
elseif isa(v.data, DatasetBin)
|
|
data = "DatasetBin: \n" * v.data.source
|
|
else
|
|
data = "DatasetEmpty"
|
|
end
|
|
plot = length(v.plot) > 0 ? join(v.plot, "\n") : []
|
|
@info("PlotElement", mid=v.mid, is3d=v.is3d, cmds=join(v.cmds, "\n"),
|
|
name=v.name, data, plot=plot)
|
|
end
|
|
|
|
function display(v::Vector{PlotElement})
|
|
for p in v
|
|
display(p)
|
|
println()
|
|
end
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ TYPE DEFINITIONS │
|
|
# │ Sessions data structures │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------
|
|
mutable struct SinglePlot
|
|
cmds::Vector{String}
|
|
elems::Vector{String}
|
|
is3d::Bool
|
|
SinglePlot() = new(Vector{String}(), Vector{String}(), false)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
abstract type Session end
|
|
|
|
mutable struct DrySession <: Session
|
|
sid::Symbol # session ID
|
|
datas::OrderedDict{String, Dataset} # data sets
|
|
plots::Vector{SinglePlot} # commands and plot commands (one entry for each plot of the multiplot)
|
|
curmid::Int # current multiplot ID
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
mutable struct GPSession <: Session
|
|
sid::Symbol # session ID
|
|
datas::OrderedDict{String, Dataset} # data sets
|
|
plots::Vector{SinglePlot} # commands and plot commands (one entry for each plot of the multiplot)
|
|
curmid::Int # current multiplot ID
|
|
pin::Base.Pipe;
|
|
pout::Base.Pipe;
|
|
perr::Base.Pipe;
|
|
proc::Base.Process;
|
|
channel::Channel{String};
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
"""
|
|
Options
|
|
|
|
Structure containing the package global options, accessible through `Gnuplot.options`.
|
|
|
|
# Fields
|
|
- `dry::Bool`: whether to use *dry* sessions, i.e. without an underlying Gnuplot process (default: `false`)
|
|
- `cmd::String`: command to start the Gnuplot process (default: `"gnuplot"`)
|
|
- `default::Symbol`: default session name (default: `:default`)
|
|
- `init::Vector{String}`: commands to initialize the session when it is created (e.g., to set default terminal);
|
|
- `reset::Vector{String}`: commands to initialize the session when it is reset (e.g., to set default palette);
|
|
- `verbose::Bool`: verbosity flag (default: `false`)
|
|
- `preferred_format::Symbol`: preferred format to send data to gnuplot. Value must be one of:
|
|
- `bin`: fastest solution for large datasets, but uses temporary files;
|
|
- `text`: may be slow for large datasets, but no temporary file is involved;
|
|
- `auto` (default) automatically choose the best strategy.
|
|
"""
|
|
Base.@kwdef mutable struct Options
|
|
dry::Bool = false
|
|
cmd::String = "gnuplot"
|
|
default::Symbol = :default
|
|
init::Vector{String} = Vector{String}()
|
|
reset::Vector{String} = Vector{String}()
|
|
verbose::Bool = false
|
|
preferred_format::Symbol = :auto
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ GLOBAL VARIABLES │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
const sessions = OrderedDict{Symbol, Session}()
|
|
const options = Options()
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ LOW LEVEL FUNCTIONS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
function parseKeywords(; kwargs...)
|
|
template = (xrange=NTuple{2, Real},
|
|
yrange=NTuple{2, Real},
|
|
zrange=NTuple{2, Real},
|
|
cbrange=NTuple{2, Real},
|
|
key=AbstractString,
|
|
title=AbstractString,
|
|
xlabel=AbstractString,
|
|
ylabel=AbstractString,
|
|
zlabel=AbstractString,
|
|
cblabel=AbstractString,
|
|
xlog=Bool,
|
|
ylog=Bool,
|
|
zlog=Bool,
|
|
cblog=Bool)
|
|
|
|
kw = canonicalize(template; kwargs...)
|
|
out = Vector{String}()
|
|
ismissing(kw.xrange ) || (push!(out, replace("set xrange [" * join(kw.xrange , ":") * "]", "NaN"=>"*")))
|
|
ismissing(kw.yrange ) || (push!(out, replace("set yrange [" * join(kw.yrange , ":") * "]", "NaN"=>"*")))
|
|
ismissing(kw.zrange ) || (push!(out, replace("set zrange [" * join(kw.zrange , ":") * "]", "NaN"=>"*")))
|
|
ismissing(kw.cbrange) || (push!(out, replace("set cbrange [" * join(kw.cbrange, ":") * "]", "NaN"=>"*")))
|
|
ismissing(kw.key ) || (push!(out, "set key " * kw.key * ""))
|
|
ismissing(kw.title ) || (push!(out, "set title \"" * kw.title * "\""))
|
|
ismissing(kw.xlabel ) || (push!(out, "set xlabel \"" * kw.xlabel * "\""))
|
|
ismissing(kw.ylabel ) || (push!(out, "set ylabel \"" * kw.ylabel * "\""))
|
|
ismissing(kw.zlabel ) || (push!(out, "set zlabel \"" * kw.zlabel * "\""))
|
|
ismissing(kw.cblabel) || (push!(out, "set cblabel \"" * kw.cblabel * "\""))
|
|
ismissing(kw.xlog ) || (push!(out, (kw.xlog ? "" : "un") * "set logscale x"))
|
|
ismissing(kw.ylog ) || (push!(out, (kw.ylog ? "" : "un") * "set logscale y"))
|
|
ismissing(kw.zlog ) || (push!(out, (kw.zlog ? "" : "un") * "set logscale z"))
|
|
ismissing(kw.cblog ) || (push!(out, (kw.cblog ? "" : "un") * "set logscale cb"))
|
|
return join(out, ";\n")
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
"""
|
|
arrays2datablock(arrays...)
|
|
|
|
Convert one (or more) arrays into an `Vector{String}`, ready to be ingested as an *inline datablock*.
|
|
|
|
Data are sent from Julia to gnuplot in the form of an array of strings, also called *inline datablock* in the gnuplot manual. This function performs such transformation.
|
|
|
|
If you experience errors when sending data to gnuplot try to filter the arrays through this function.
|
|
"""
|
|
function arrays2datablock(args...)
|
|
tostring(v::AbstractString) = "\"" * string(v) * "\""
|
|
tostring(v::Real) = string(v)
|
|
tostring(::Missing) = "?"
|
|
#tostring(c::ColorTypes.RGB) = string(Int(c.r*255)) * " " * string(Int(c.g*255)) * " " * string(Int(c.b*255))
|
|
@assert length(args) > 0
|
|
|
|
# Collect lengths and number of dims
|
|
lengths = Vector{Int}()
|
|
dims = Vector{Int}()
|
|
firstMultiDim = 0
|
|
for i in 1:length(args)
|
|
d = args[i]
|
|
@assert ndims(d) <= 3 "Array dimensions must be <= 3"
|
|
push!(lengths, length(d))
|
|
push!(dims , ndims(d))
|
|
(firstMultiDim == 0) && (ndims(d) > 1) && (firstMultiDim = i)
|
|
end
|
|
|
|
accum = Vector{String}()
|
|
|
|
# All scalars
|
|
if minimum(dims) == 0
|
|
# @info "Case 0" # debug
|
|
@assert maximum(dims) == 0 "Input data are ambiguous: either use all scalar or arrays of floats"
|
|
v = ""
|
|
for iarg in 1:length(args)
|
|
d = args[iarg]
|
|
v *= " " * tostring(d)
|
|
end
|
|
push!(accum, v)
|
|
return accum
|
|
end
|
|
|
|
@assert all((dims .== 1) .| (dims .== maximum(dims))) "Array size are incompatible"
|
|
|
|
# All 1D
|
|
if firstMultiDim == 0
|
|
# @info "Case 1" # debug
|
|
@assert minimum(lengths) == maximum(lengths) "Array size are incompatible"
|
|
for i in 1:lengths[1]
|
|
v = ""
|
|
for iarg in 1:length(args)
|
|
d = args[iarg]
|
|
v *= " " * tostring(d[i])
|
|
end
|
|
push!(accum, v)
|
|
end
|
|
return accum
|
|
end
|
|
|
|
# Multidimensional, no independent 1D indices
|
|
if firstMultiDim == 1
|
|
# @info "Case 2" # debug
|
|
@assert minimum(lengths) == maximum(lengths) "Array size are incompatible"
|
|
i = 1
|
|
for CIndex in CartesianIndices(size(args[1]'))
|
|
indices = Tuple(CIndex)
|
|
(i > 1) && (indices[end-1] == 1) && (push!(accum, "")) # blank line
|
|
if length(args) == 1
|
|
# Add independent indices (starting from zero, useful when plotting "with image")
|
|
v = join(string.(getindex.(Ref(Tuple(indices)), 1:ndims(args[1])) .- 1), " ")
|
|
else
|
|
# Do not add independent indices since there is no way to distinguish a "z" array from additional arrays
|
|
v = ""
|
|
end
|
|
for iarg in 1:length(args)
|
|
d = args[iarg]'
|
|
v *= " " * tostring(d[i])
|
|
end
|
|
i += 1
|
|
push!(accum, v)
|
|
end
|
|
return accum
|
|
end
|
|
|
|
# Multidimensional (independent indices provided in input)
|
|
if firstMultiDim >= 2
|
|
refLength = lengths[firstMultiDim]
|
|
@assert all(lengths[firstMultiDim:end] .== refLength) "Array size are incompatible"
|
|
|
|
if lengths[1] < refLength
|
|
# @info "Case 3" # debug
|
|
# Cartesian product of Independent variables
|
|
checkLength = prod(lengths[1:firstMultiDim-1])
|
|
@assert prod(lengths[1:firstMultiDim-1]) == refLength "Array size are incompatible"
|
|
|
|
i = 1
|
|
for CIndex in CartesianIndices(size(args[firstMultiDim]))
|
|
indices = Tuple(CIndex)
|
|
(i > 1) && (indices[end-1] == 1) && (push!(accum, "")) # blank line
|
|
v = ""
|
|
for iarg in 1:firstMultiDim-1
|
|
d = args[iarg]
|
|
v *= " " * tostring(d[indices[iarg]])
|
|
end
|
|
for iarg in firstMultiDim:length(args)
|
|
d = args[iarg]
|
|
v *= " " * tostring(d[i])
|
|
end
|
|
i += 1
|
|
push!(accum, v)
|
|
end
|
|
return accum
|
|
else
|
|
# @info "Case 4" # debug
|
|
# All Independent variables have the same length as the main multidimensional data
|
|
@assert all(lengths[1:firstMultiDim-1] .== refLength) "Array size are incompatible"
|
|
|
|
i = 1
|
|
for CIndex in CartesianIndices(size(args[firstMultiDim]))
|
|
indices = Tuple(CIndex)
|
|
(i > 1) && (indices[end-1] == 1) && (push!(accum, "")) # blank line
|
|
v = ""
|
|
for iarg in 1:length(args)
|
|
d = args[iarg]
|
|
v *= " " * tostring(d[i])
|
|
end
|
|
i += 1
|
|
push!(accum, v)
|
|
end
|
|
return accum
|
|
end
|
|
end
|
|
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ SESSION CONSTRUCTORS AND getsession() │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
function DrySession(sid::Symbol)
|
|
(sid in keys(sessions)) && error("Gnuplot session $sid is already active")
|
|
out = DrySession(sid, OrderedDict{String, Dataset}(), [SinglePlot()], 1)
|
|
sessions[sid] = out
|
|
return out
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
pagerTokens() = ["Press return for more:"]
|
|
|
|
function GPSession(sid::Symbol)
|
|
function readTask(sid, stream, channel)
|
|
function gpreadline(stream)
|
|
line = ""
|
|
while true
|
|
c = read(stream, Char)
|
|
(c == '\r') && continue
|
|
(c == '\n') && break
|
|
if c == Char(0x1b) # sixel
|
|
buf = Vector{UInt8}()
|
|
push!(buf, UInt8(c))
|
|
while true
|
|
c = read(stream, Char)
|
|
push!(buf, UInt8(c))
|
|
(c == Char(0x1b)) && break
|
|
end
|
|
c = read(stream, Char)
|
|
push!(buf, UInt8(c))
|
|
write(stdout, buf)
|
|
continue
|
|
end
|
|
line *= c
|
|
for token in pagerTokens() # handle pager interaction
|
|
if (length(line) == length(token)) && (line == token)
|
|
return line
|
|
end
|
|
end
|
|
end
|
|
return line
|
|
end
|
|
|
|
saveOutput = false
|
|
while isopen(stream)
|
|
line = gpreadline(stream)
|
|
if line == "GNUPLOT_CAPTURE_BEGIN"
|
|
saveOutput = true
|
|
elseif line == "GNUPLOT_CAPTURE_END"
|
|
put!(channel, line)
|
|
saveOutput = false
|
|
else
|
|
if line != ""
|
|
if options.verbose || !saveOutput
|
|
printstyled(color=:cyan, "GNUPLOT ($sid) -> $line\n")
|
|
end
|
|
end
|
|
(saveOutput) && (put!(channel, line))
|
|
end
|
|
end
|
|
delete!(sessions, sid)
|
|
return nothing
|
|
end
|
|
|
|
session = DrySession(sid)
|
|
if !options.dry
|
|
try
|
|
gpversion()
|
|
catch
|
|
@warn "Cound not start a gnuplot process with command \"$(options.cmd)\". Enabling dry sessions..."
|
|
options.dry = true
|
|
sessions[sid] = session
|
|
return session
|
|
end
|
|
end
|
|
|
|
pin = Base.Pipe()
|
|
pout = Base.Pipe()
|
|
perr = Base.Pipe()
|
|
proc = run(pipeline(`$(options.cmd)`, stdin=pin, stdout=pout, stderr=perr), wait=false)
|
|
chan = Channel{String}(32)
|
|
|
|
# Close unused sides of the pipes
|
|
Base.close(pout.in)
|
|
Base.close(perr.in)
|
|
Base.close(pin.out)
|
|
Base.start_reading(pout.out)
|
|
Base.start_reading(perr.out)
|
|
|
|
# Start reading tasks
|
|
@async readTask(sid, pout, chan)
|
|
@async readTask(sid, perr, chan)
|
|
|
|
out = GPSession(getfield.(Ref(session), fieldnames(DrySession))...,
|
|
pin, pout, perr, proc, chan)
|
|
sessions[sid] = out
|
|
|
|
for l in options.init
|
|
gpexec(out, l)
|
|
end
|
|
|
|
# Set window title (if not already set)
|
|
term = writeread(out, "print GPVAL_TERM")[1]
|
|
if term in ("aqua", "x11", "qt", "wxt")
|
|
opts = writeread(out, "print GPVAL_TERMOPTIONS")[1]
|
|
if findfirst("title", opts) == nothing
|
|
writeread(out, "set term $term $opts title 'Gnuplot.jl: $(out.sid)'")
|
|
end
|
|
end
|
|
|
|
return out
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function getsession(sid::Symbol=options.default)
|
|
if !(sid in keys(sessions))
|
|
if options.dry
|
|
DrySession(sid)
|
|
else
|
|
GPSession(sid)
|
|
end
|
|
end
|
|
return sessions[sid]
|
|
end
|
|
|
|
|
|
function gp_write_table(args...; kw...)
|
|
tmpfile = Base.Filesystem.tempname()
|
|
sid = Symbol("j", Base.Libc.getpid())
|
|
gp = getsession(sid)
|
|
reset(gp)
|
|
gpexec(sid, "set term unknown")
|
|
driver(sid, "set table '$tmpfile'", args...; kw...)
|
|
gpexec(sid, "unset table")
|
|
quit(sid)
|
|
out = readlines(tmpfile)
|
|
rm(tmpfile)
|
|
return out
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ write() and writeread() │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
|
|
"""
|
|
write(gp, str)
|
|
|
|
Send a string to gnuplot's STDIN.
|
|
|
|
The commands sent through `write` are not stored in the current session (use `add_cmd` to save commands in the current session).
|
|
"""
|
|
write(gp::DrySession, str::AbstractString) = nothing
|
|
function write(gp::GPSession, str::AbstractString)
|
|
if options.verbose
|
|
printstyled(color=:light_yellow, "GNUPLOT ($(gp.sid)) $str\n")
|
|
end
|
|
w = write(gp.pin, strip(str) * "\n")
|
|
w <= 0 && error("Writing on gnuplot STDIN pipe returned $w")
|
|
flush(gp.pin)
|
|
return w
|
|
end
|
|
|
|
|
|
write(gp::DrySession, name::String, d::Dataset) = nothing
|
|
write(gp::GPSession, name::String, d::DatasetBin) = nothing
|
|
function write(gp::GPSession, name::String, d::DatasetText)
|
|
if options.verbose
|
|
printstyled(color=:light_black, "GNUPLOT ($(gp.sid)) ", name, " << EOD\n")
|
|
printstyled(color=:light_black, join("GNUPLOT ($(gp.sid)) " .* d.preview, "\n") * "\n")
|
|
printstyled(color=:light_black, "GNUPLOT ($(gp.sid)) ", "EOD\n")
|
|
end
|
|
out = write(gp.pin, name * " << EOD\n")
|
|
out += write(gp.pin, d.data)
|
|
out += write(gp.pin, "\nEOD\n")
|
|
flush(gp.pin)
|
|
return out
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
writeread(gp::DrySession, str::AbstractString) = [""]
|
|
function writeread(gp::GPSession, str::AbstractString)
|
|
verbose = options.verbose
|
|
|
|
options.verbose = false
|
|
write(gp, "print 'GNUPLOT_CAPTURE_BEGIN'")
|
|
|
|
options.verbose = verbose
|
|
write(gp, str)
|
|
|
|
options.verbose = false
|
|
write(gp, "print 'GNUPLOT_CAPTURE_END'")
|
|
options.verbose = verbose
|
|
|
|
out = Vector{String}()
|
|
while true
|
|
l = take!(gp.channel)
|
|
if l in pagerTokens()
|
|
# Consume all data from the pager
|
|
while true
|
|
write(gp, "")
|
|
sleep(0.5)
|
|
if isready(gp.channel)
|
|
while isready(gp.channel)
|
|
push!(out, take!(gp.channel))
|
|
end
|
|
else
|
|
options.verbose = false
|
|
write(gp, "print 'GNUPLOT_CAPTURE_END'")
|
|
options.verbose = verbose
|
|
break
|
|
end
|
|
end
|
|
else
|
|
l == "GNUPLOT_CAPTURE_END" && break
|
|
push!(out, l)
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ Dataset CONSTRUCTORS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
|
|
#=
|
|
The following is dismissed since `binary matrix` do not allows to use
|
|
keywords such as `rotate`.
|
|
# ---------------------------------------------------------------------
|
|
function write_binary(M::Matrix{T}) where T <: Real
|
|
x = collect(1:size(M)[1])
|
|
y = collect(1:size(M)[2])
|
|
|
|
MS = Float32.(zeros(length(x)+1, length(y)+1))
|
|
MS[1,1] = length(x)
|
|
MS[1,2:end] = y
|
|
MS[2:end,1] = x
|
|
MS[2:end,2:end] = M
|
|
|
|
(path, io) = mktemp()
|
|
write(io, MS)
|
|
close(io)
|
|
return (path, " '$path' binary matrix")
|
|
end
|
|
=#
|
|
|
|
# ---------------------------------------------------------------------
|
|
function DatasetBin(VM::Vararg{AbstractMatrix{T}, N}) where {T <: Real, N}
|
|
for i in 2:N
|
|
@assert size(VM[i]) == size(VM[1])
|
|
end
|
|
s = size(VM[1])
|
|
(path, io) = mktemp()
|
|
|
|
for i in 1:s[1]
|
|
for j in 1:s[2]
|
|
for k in 1:N
|
|
write(io, Float32(VM[k][i,j]))
|
|
end
|
|
end
|
|
end
|
|
source = " '$path' binary array=(" * join(string.(reverse(s)), ", ") * ")"
|
|
# Note: can't add `using` here, otherwise we can't append `flipy`.
|
|
close(io)
|
|
return DatasetBin(Val(:inner), path, source)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function DatasetBin(cols::Vararg{AbstractVector, N}) where N
|
|
source = "binary record=$(length(cols[1])) format='"
|
|
types = Vector{DataType}()
|
|
(length(cols) == 1) && (source *= "%int")
|
|
for i in 1:length(cols)
|
|
@assert length(cols[1]) == length(cols[i])
|
|
if isa(cols[i][1], Int32); push!(types, Int32); source *= "%int"
|
|
elseif isa(cols[i][1], Int); push!(types, Int32); source *= "%int"
|
|
elseif isa(cols[i][1], Float32); push!(types, Float32); source *= "%float"
|
|
elseif isa(cols[i][1], Float64); push!(types, Float32); source *= "%float"
|
|
elseif isa(cols[i][1], Char); push!(types, Char); source *= "%char"
|
|
else
|
|
error("Unsupported data on column $i: $(typeof(cols[i][1]))")
|
|
end
|
|
end
|
|
source *= "'"
|
|
|
|
(path, io) = mktemp()
|
|
source = " '$path' $source"
|
|
for row in 1:length(cols[1])
|
|
(length(cols) == 1) && (write(io, convert(Int32, row)))
|
|
for col in 1:length(cols)
|
|
write(io, convert(types[col], cols[col][row]))
|
|
end
|
|
end
|
|
close(io)
|
|
source *= " using " * join(1:N, ":") * " "
|
|
return DatasetBin(Val(:inner), path, source)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
DatasetText(args...) = DatasetText(arrays2datablock(args...))
|
|
function DatasetText(data::Vector{String})
|
|
preview = (length(data) <= 4 ? deepcopy(data) : [data[1:4]..., "..."])
|
|
d = DatasetText(Val(:inner), preview, join(data, "\n"))
|
|
return d
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ PRIVATE FUNCTIONS TO MANIPULATE SESSIONS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
function reset(gp::Session)
|
|
delete_binaries(gp)
|
|
gp.datas = OrderedDict{String, Dataset}()
|
|
gp.plots = [SinglePlot()]
|
|
gp.curmid = 1
|
|
gpexec(gp, "unset multiplot")
|
|
gpexec(gp, "set output")
|
|
gpexec(gp, "reset session")
|
|
add_cmd.(Ref(gp), options.reset)
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function setmulti(gp::Session, mid::Int)
|
|
@assert mid >= 1 "Multiplot ID must be a >= 1"
|
|
while length(gp.plots) < mid
|
|
push!(gp.plots, SinglePlot())
|
|
end
|
|
gp.curmid = mid
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
newDatasetName(gp::Session) = string("\$data", length(gp.datas)+1)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function useBinaryMethod(args...)
|
|
@assert options.preferred_format in [:auto, :bin, :text] "Unexpected value for `options.preferred_format`: $(options.preferred_format)"
|
|
binary = false
|
|
if options.preferred_format == :bin
|
|
binary = true
|
|
elseif options.preferred_format == :auto
|
|
if (length(args) == 1) && isa(args[1], AbstractMatrix)
|
|
binary = true
|
|
else
|
|
s = sum(length.(args))
|
|
if s > 1e4
|
|
binary = true
|
|
end
|
|
end
|
|
end
|
|
return binary
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function Dataset(accum)
|
|
if useBinaryMethod(accum...)
|
|
try
|
|
return DatasetBin(accum...)
|
|
catch err
|
|
isa(err, MethodError) || rethrow()
|
|
end
|
|
end
|
|
return DatasetText(accum...)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function add_cmd(gp::Session, v::String)
|
|
(v != "") && (push!(gp.plots[gp.curmid].cmds, v))
|
|
(length(gp.plots) == 1) && (gpexec(gp, v)) # execute now to check against errors
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function add_plot(gp::Session, plotspec)
|
|
push!(gp.plots[gp.curmid].elems, plotspec)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function delete_binaries(gp::Session)
|
|
for (name, d) in gp.datas
|
|
if isa(d, DatasetBin) && (d.file != "")
|
|
rm(d.file, force=true)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function quit(gp::DrySession)
|
|
delete_binaries(gp)
|
|
delete!(sessions, gp.sid)
|
|
return 0
|
|
end
|
|
|
|
function quit(gp::GPSession)
|
|
close(gp.pin)
|
|
close(gp.pout)
|
|
close(gp.perr)
|
|
wait( gp.proc)
|
|
exitCode = gp.proc.exitcode
|
|
delete_binaries(gp)
|
|
delete!(sessions, gp.sid)
|
|
return exitCode
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
function stats(gp::Session, name::String)
|
|
@info sid=gp.sid name=name source=gp.datas[name].source
|
|
println(gpexec(gp, "stats " * gp.datas[name].source))
|
|
end
|
|
stats(gp::Session) = for (name, d) in gp.datas
|
|
stats(gp, name)
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ gpexec(), execall(), amd savescript() │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
gpexec(gp::DrySession, command::String) = ""
|
|
function gpexec(gp::GPSession, command::String)
|
|
answer = Vector{String}()
|
|
push!(answer, writeread(gp, command)...)
|
|
|
|
verbose = options.verbose
|
|
options.verbose = false
|
|
errno = writeread(gp, "print GPVAL_ERRNO")
|
|
options.verbose = verbose
|
|
@assert length(errno) == 1
|
|
if errno[1] != "0"
|
|
@error "\n" * join(answer, "\n")
|
|
errmsg = writeread(gp, "print GPVAL_ERRMSG")
|
|
write(gp.pin, "reset error\n")
|
|
error("Gnuplot error: $errmsg")
|
|
end
|
|
|
|
return join(answer, "\n")
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
execall(gp::DrySession; term::AbstractString="", output::AbstractString="") = nothing
|
|
function execall(gp::GPSession; term::AbstractString="", output::AbstractString="")
|
|
gpexec(gp, "reset")
|
|
if term != ""
|
|
former_term = writeread(gp, "print GPVAL_TERM")[1]
|
|
former_opts = writeread(gp, "print GPVAL_TERMOPTIONS")[1]
|
|
gpexec(gp, "set term $term")
|
|
end
|
|
(output != "") && gpexec(gp, "set output '$output'")
|
|
|
|
for i in 1:length(gp.plots)
|
|
d = gp.plots[i]
|
|
for j in 1:length(d.cmds)
|
|
gpexec(gp, d.cmds[j])
|
|
end
|
|
if length(d.elems) > 0
|
|
s = (d.is3d ? "splot " : "plot ") * " \\\n " *
|
|
join(d.elems, ", \\\n ")
|
|
gpexec(gp, s)
|
|
end
|
|
end
|
|
(length(gp.plots) > 1) && gpexec(gp, "unset multiplot")
|
|
(output != "") && gpexec(gp, "set output")
|
|
if term != ""
|
|
gpexec(gp, "set term $former_term $former_opts")
|
|
end
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
function savescript(gp::Session, filename; term::AbstractString="", output::AbstractString="")
|
|
function copy_binary_files(gp, filename)
|
|
function data_dirname(path)
|
|
dir = dirname(path)
|
|
(dir == "") && (dir = ".")
|
|
base = basename(path)
|
|
s = split(base, ".")
|
|
if length(s) > 1
|
|
base = join(s[1:end-1], ".")
|
|
end
|
|
base *= "_data/"
|
|
out = dir * "/" * base
|
|
return out
|
|
end
|
|
|
|
path_from = Vector{String}()
|
|
path_to = Vector{String}()
|
|
datapath = data_dirname(filename)
|
|
for (name, d) in gp.datas
|
|
if isa(d, DatasetBin) && (d.file != "")
|
|
if (length(path_from) == 0)
|
|
#isdir(datapath) && rm(datapath, recursive=true)
|
|
mkpath(datapath)
|
|
end
|
|
to = datapath * basename(d.file)
|
|
cp(d.file, to, force=true)
|
|
push!(path_from, d.file)
|
|
push!(path_to, to)
|
|
end
|
|
end
|
|
return (path_from, path_to)
|
|
end
|
|
function redirect_elements(elems, path_from, path_to)
|
|
(length(path_from) == 0) && (return elems)
|
|
|
|
out = deepcopy(elems)
|
|
for i in 1:length(out)
|
|
for j in 1:length(path_from)
|
|
tmp = replace(out[i], path_from[j] => path_to[j])
|
|
out[i] = tmp
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
stream = open(filename, "w")
|
|
|
|
println(stream, "reset session")
|
|
if term != ""
|
|
println(stream, "set term $term")
|
|
end
|
|
(output != "") && println(stream, "set output '$output'")
|
|
|
|
paths = copy_binary_files(gp, filename)
|
|
for (name, d) in gp.datas
|
|
if isa(d, DatasetText)
|
|
println(stream, name * " << EOD")
|
|
println(stream, d.data)
|
|
println(stream, "EOD")
|
|
end
|
|
end
|
|
|
|
for i in 1:length(gp.plots)
|
|
d = gp.plots[i]
|
|
for j in 1:length(d.cmds)
|
|
println(stream, d.cmds[j])
|
|
end
|
|
if length(d.elems) > 0
|
|
s = (d.is3d ? "splot " : "plot ") * " \\\n " *
|
|
join(redirect_elements(d.elems, paths...), ", \\\n ")
|
|
println(stream, s)
|
|
end
|
|
end
|
|
(length(gp.plots) > 1) && println(stream, "unset multiplot")
|
|
println(stream, "set output")
|
|
close(stream)
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ parseArgument() amd driver() │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# ---------------------------------------------------------------------
|
|
function parseArguments(_args...)
|
|
function parseCmd(s::String)
|
|
(isplot, is3d, cmd) = (false, false, s)
|
|
(length(s) >= 2) && (s[1:2] == "p " ) && ((isplot, is3d, cmd) = (true, false, strip(s[2:end])))
|
|
(length(s) >= 3) && (s[1:3] == "pl " ) && ((isplot, is3d, cmd) = (true, false, strip(s[3:end])))
|
|
(length(s) >= 4) && (s[1:4] == "plo " ) && ((isplot, is3d, cmd) = (true, false, strip(s[4:end])))
|
|
(length(s) >= 5) && (s[1:5] == "plot " ) && ((isplot, is3d, cmd) = (true, false, strip(s[5:end])))
|
|
(length(s) >= 2) && (s[1:2] == "s " ) && ((isplot, is3d, cmd) = (true, true , strip(s[2:end])))
|
|
(length(s) >= 3) && (s[1:3] == "sp " ) && ((isplot, is3d, cmd) = (true, true , strip(s[3:end])))
|
|
(length(s) >= 4) && (s[1:4] == "spl " ) && ((isplot, is3d, cmd) = (true, true , strip(s[4:end])))
|
|
(length(s) >= 5) && (s[1:5] == "splo " ) && ((isplot, is3d, cmd) = (true, true , strip(s[5:end])))
|
|
(length(s) >= 6) && (s[1:6] == "splot ") && ((isplot, is3d, cmd) = (true, true , strip(s[6:end])))
|
|
return (isplot, is3d, string(cmd))
|
|
end
|
|
|
|
# First pass: check for `:-` and session names
|
|
sid = options.default
|
|
doDump = true
|
|
doReset = true
|
|
if length(_args) == 0
|
|
return (sid, doReset, doDump, Vector{PlotElement}())
|
|
end
|
|
for iarg in 1:length(_args)
|
|
arg = _args[iarg]
|
|
|
|
if typeof(arg) == Symbol
|
|
if arg == :-
|
|
if iarg == 1
|
|
doReset = false
|
|
elseif iarg == length(_args)
|
|
doDump = false
|
|
else
|
|
@warn "Symbol `:-` at position $iarg in argument list has no meaning."
|
|
end
|
|
else
|
|
@assert (sid == options.default) "Only one session at a time can be addressed"
|
|
sid = arg
|
|
end
|
|
end
|
|
end
|
|
|
|
# Second pass: check data types, run implicit recipes and splat
|
|
# Vector{PlotElement}
|
|
args = Vector{Any}([_args...])
|
|
pos = 1
|
|
while pos <= length(args)
|
|
arg = args[pos]
|
|
if isa(arg, Symbol) # session ID (already handled)
|
|
deleteat!(args, pos)
|
|
continue
|
|
elseif isa(arg, Int) # ==> multiplot index
|
|
@assert arg > 0 "Multiplot index must be a positive integer"
|
|
elseif isa(arg, AbstractString) # ==> a plotspec or a command
|
|
deleteat!(args, pos)
|
|
insert!(args, pos, string(strip(arg)))
|
|
elseif isa(arg, Tuple) && # ==> a keyword/value pair
|
|
length(arg) == 2 &&
|
|
isa(arg[1], Symbol)
|
|
deleteat!(args, pos)
|
|
insert!(args, pos, parseKeywords(; [arg]...))
|
|
continue
|
|
elseif isa(arg, Pair) # ==> a named dataset
|
|
@assert typeof(arg[1]) == String "Dataset name must be a string"
|
|
@assert arg[1][1] == '$' "Dataset name must start with a dollar sign"
|
|
deleteat!(args, pos)
|
|
for i in length(arg[2]):-1:1
|
|
insert!(args, pos, arg[2][i])
|
|
end
|
|
insert!(args, pos, string(strip(arg[1])) => nothing)
|
|
elseif isa(arg, AbstractArray) && # ==> a dataset column
|
|
((valtype(arg) <: Real) ||
|
|
(valtype(arg) <: AbstractString)) ;
|
|
elseif isa(arg, Real) # ==> a dataset column with only one row
|
|
args[pos] = [arg]
|
|
elseif isa(arg, Dataset) ; # ==> a Dataset object
|
|
elseif hasmethod(recipe, tuple(typeof(arg))) # ==> implicit recipe
|
|
# @info which(recipe, tuple(typeof(arg))) # debug
|
|
deleteat!(args, pos)
|
|
insert!(args, pos, recipe(arg))
|
|
continue
|
|
elseif isa(arg, Vector{PlotElement}) # ==> explicit recipe (vector)
|
|
deleteat!(args, pos)
|
|
for i in length(arg):-1:1
|
|
insert!(args, arg[i])
|
|
end
|
|
elseif isa(arg, PlotElement) ; # ==> explicit recipe (scalar)
|
|
else
|
|
error("Unexpected argument with type " * string(typeof(arg)))
|
|
end
|
|
|
|
pos += 1
|
|
end
|
|
|
|
# Third pass: convert data into Dataset objetcs
|
|
pos = 1
|
|
while pos <= length(args)
|
|
arg = args[pos]
|
|
if isa(arg, AbstractArray) && # ==> beginning of a dataset
|
|
((valtype(arg) <: Real) ||
|
|
(valtype(arg) <: AbstractString))
|
|
|
|
# Collect all data
|
|
accum = Vector{AbstractArray}()
|
|
while isa(arg, AbstractArray) &&
|
|
((valtype(arg) <: Real) ||
|
|
(valtype(arg) <: AbstractString))
|
|
push!(accum, arg)
|
|
deleteat!(args, pos)
|
|
if pos <= length(args)
|
|
arg = args[pos]
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
mm = extrema(length.(accum))
|
|
if mm[1] == 0
|
|
# empty Dataset
|
|
@assert mm[1] == mm[2] "At least one input array is empty, while other(s) are not"
|
|
d = DatasetEmpty()
|
|
else
|
|
d = Dataset(accum)
|
|
end
|
|
insert!(args, pos, d)
|
|
end
|
|
pos += 1
|
|
end
|
|
|
|
# Fourth pass: collect PlotElement objects
|
|
mid = 0
|
|
name = ""
|
|
cmds = Vector{String}()
|
|
elems = Vector{PlotElement}()
|
|
pos = 1
|
|
while pos <= length(args)
|
|
arg = args[pos]
|
|
|
|
if isa(arg, Int) # ==> multiplot index
|
|
if length(cmds) > 0
|
|
push!(elems, PlotElement(mid=mid, cmds=cmds))
|
|
empty!(cmds)
|
|
end
|
|
mid = arg
|
|
name = ""
|
|
empty!(cmds)
|
|
elseif isa(arg, String) # ==> a plotspec or a command
|
|
(isPlot, is3d, s) = parseCmd(arg)
|
|
if isPlot
|
|
push!(elems, PlotElement(mid=mid, is3d=is3d, cmds=cmds, plot=s))
|
|
empty!(cmds)
|
|
else
|
|
push!(cmds, s)
|
|
end
|
|
name = ""
|
|
elseif isa(arg, Pair) # ==> dataset name
|
|
name = arg[1]
|
|
elseif isa(arg, Dataset) # ==> A Dataset
|
|
spec = Vector{String}()
|
|
if name == "" # only unnamed data sets have an associated plot spec
|
|
spec = ""
|
|
if (pos < length(args)) &&
|
|
isa(args[pos+1], String)
|
|
spec = args[pos+1]
|
|
deleteat!(args, pos+1)
|
|
end
|
|
end
|
|
if !isa(arg, DatasetEmpty)
|
|
push!(elems, PlotElement(mid=mid, cmds=cmds, name=name, data=arg, plot=spec))
|
|
end
|
|
name = ""
|
|
empty!(cmds)
|
|
elseif isa(arg, PlotElement)
|
|
if length(cmds) > 0
|
|
push!(elems, PlotElement(mid=mid, cmds=cmds))
|
|
empty!(cmds)
|
|
end
|
|
name = ""
|
|
(mid != 0) && (arg.mid = mid)
|
|
push!(elems, arg)
|
|
else
|
|
error("Unexpected argument with type " * string(typeof(arg)))
|
|
end
|
|
pos += 1
|
|
end
|
|
if length(cmds) > 0
|
|
push!(elems, PlotElement(mid=mid, cmds=cmds))
|
|
empty!(cmds)
|
|
end
|
|
|
|
return (sid, doReset, doDump, elems)
|
|
end
|
|
|
|
|
|
function driver(_args...; is3d=false)
|
|
if length(_args) == 0
|
|
gp = getsession()
|
|
execall(gp)
|
|
return nothing
|
|
end
|
|
|
|
(sid, doReset, doDump, elems) = parseArguments(_args...)
|
|
gp = getsession(sid)
|
|
doReset && reset(gp)
|
|
|
|
# Set curent multiplot ID and sort elements
|
|
for elem in elems
|
|
if elem.mid == 0
|
|
elem.mid = gp.curmid
|
|
end
|
|
end
|
|
elems = elems[sortperm(getfield.(elems, :mid))]
|
|
# display(elems) # debug
|
|
|
|
# Set dataset names and send them to gnuplot process
|
|
for elem in elems
|
|
(elem.name == "") && (elem.name = newDatasetName(gp))
|
|
if !isa(elem.data, DatasetEmpty) &&
|
|
!haskey(gp.datas, elem.name)
|
|
gp.datas[elem.name] = elem.data
|
|
write(gp, elem.name, elem.data)
|
|
end
|
|
end
|
|
|
|
for elem in elems
|
|
(elem.mid > 0) && setmulti(gp, elem.mid)
|
|
gp.plots[gp.curmid].is3d = (is3d | elem.is3d)
|
|
|
|
for cmd in elem.cmds
|
|
add_cmd(gp, cmd)
|
|
end
|
|
|
|
if !isa(elem.data, DatasetEmpty)
|
|
for spec in elem.plot
|
|
if isa(elem.data, DatasetBin)
|
|
add_plot(gp, elem.data.source * " " * spec)
|
|
else
|
|
add_plot(gp, elem.name * " " * spec)
|
|
end
|
|
end
|
|
else
|
|
for spec in elem.plot
|
|
for (name, data) in gp.datas
|
|
if isa(data, DatasetBin)
|
|
spec = replace(spec, name => data.source)
|
|
end
|
|
end
|
|
add_plot(gp, spec)
|
|
end
|
|
end
|
|
end
|
|
|
|
(doDump) && (execall(gp))
|
|
|
|
return nothing
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ NON-EXPORTED FUNCTIONS MEANT TO BE INVOKED BY USERS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
"""
|
|
Gnuplot.version()
|
|
|
|
Return the **Gnuplot.jl** package version.
|
|
"""
|
|
version() = v"1.2.0"
|
|
|
|
# ---------------------------------------------------------------------
|
|
"""
|
|
Gnuplot.gpversion()
|
|
|
|
Return the gnuplot application version.
|
|
|
|
Raise an error if version is < 5.0 (required to use data blocks).
|
|
"""
|
|
function gpversion()
|
|
options.dry && (return v"0.0.0")
|
|
icmd = `$(options.cmd) --version`
|
|
|
|
proc = open(`$icmd`, read=true)
|
|
s = String(read(proc))
|
|
if !success(proc)
|
|
error("An error occurred while running: " * string(icmd))
|
|
end
|
|
|
|
s = split(s, " ")
|
|
ver = ""
|
|
for token in s
|
|
try
|
|
ver = VersionNumber("$token")
|
|
break
|
|
catch
|
|
end
|
|
end
|
|
|
|
if ver < v"5.0"
|
|
error("gnuplot ver. >= 5.0 is required, but " * string(ver) * " was found.")
|
|
end
|
|
return ver
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
gpexec(sid::Symbol, command::String)
|
|
gpexec(command::String)
|
|
|
|
Execute the gnuplot command `command` on the underlying gnuplot process of the `sid` session, and return the results as a `Vector{String}`. If a gnuplot error arises it is propagated as an `ErrorException`.
|
|
|
|
The the `sid` argument is not provided, the default session is considered.
|
|
|
|
## Examples:
|
|
```julia-repl
|
|
gpexec("print GPVAL_TERM")
|
|
gpexec("plot sin(x)")
|
|
```
|
|
"""
|
|
gpexec(sid::Symbol, s::String) = gpexec(getsession(sid), s)
|
|
gpexec(s::String) = gpexec(getsession(), s)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
"""
|
|
Gnuplot.quit(sid::Symbol)
|
|
|
|
Quit the session identified by `sid` and the associated gnuplot process (if any).
|
|
"""
|
|
function quit(sid::Symbol)
|
|
(sid in keys(sessions)) || (return 0)
|
|
return quit(sessions[sid])
|
|
end
|
|
|
|
"""
|
|
Gnuplot.quitall()
|
|
|
|
Quit all the sessions and the associated gnuplot processes.
|
|
"""
|
|
function quitall()
|
|
for sid in keys(sessions)
|
|
quit(sid)
|
|
end
|
|
return nothing
|
|
end
|
|
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ EXPORTED FUNCTIONS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
@gp args...
|
|
|
|
The `@gp` macro, and its companion `@gsp` for 3D plots, allows to send data and commands to the gnuplot using an extremely concise syntax. The macros accepts any number of arguments, with the following meaning:
|
|
|
|
- one, or a group of consecutive, array(s) of either `Real` or `String` build up a dataset. The different arrays are accessible as columns 1, 2, etc. from the `gnuplot` process. The number of required input arrays depends on the chosen plot style (see `gnuplot` documentation);
|
|
|
|
- a string occurring before a dataset is interpreted as a `gnuplot` command (e.g. `set grid`);
|
|
|
|
- a string occurring immediately after a dataset is interpreted as a *plot element* for the dataset, by which you can specify `using` clause, `with` clause, line styles, etc.. All keywords may be abbreviated following gnuplot conventions. Moreover, "plot" and "splot" can be abbreviated to "p" and "s" respectively;
|
|
|
|
- the special symbol `:-` allows to split one long statement into multiple (shorter) ones. If given as first argument it avoids starting a new plot. If it given as last argument it avoids immediately running all commands to create the final plot;
|
|
|
|
- any other symbol is interpreted as a session ID;
|
|
|
|
- an `Int` (>= 1) is interpreted as the plot destination in a multi-plot session (this specification applies to subsequent arguments, not previous ones);
|
|
|
|
- an input in the form `"\\\$name"=>(array1, array2, etc...)` is interpreted as a named dataset. Note that the dataset name must always start with a "`\$`";
|
|
|
|
- an input in the form `keyword=value` is interpreted as a keyword/value pair. The accepted keywords and their corresponding gnuplot commands are as follows:
|
|
- `xrange=[low, high]` => `"set xrange [low:high]`;
|
|
- `yrange=[low, high]` => `"set yrange [low:high]`;
|
|
- `zrange=[low, high]` => `"set zrange [low:high]`;
|
|
- `cbrange=[low, high]`=> `"set cbrange[low:high]`;
|
|
- `key="..."` => `"set key ..."`;
|
|
- `title="..."` => `"set title \"...\""`;
|
|
- `xlabel="..."` => `"set xlabel \"...\""`;
|
|
- `ylabel="..."` => `"set ylabel \"...\""`;
|
|
- `zlabel="..."` => `"set zlabel \"...\""`;
|
|
- `cblabel="..."` => `"set cblabel \"...\""`;
|
|
- `xlog=true` => `set logscale x`;
|
|
- `ylog=true` => `set logscale y`;
|
|
- `zlog=true` => `set logscale z`.
|
|
- `cblog=true` => `set logscale cb`.
|
|
All Keyword names can be abbreviated as long as the resulting name is unambiguous. E.g. you can use `xr=[1,10]` in place of `xrange=[1,10]`.
|
|
|
|
- a `PlotElement` object is expanded in and its fields processed as one of the previous arguments;
|
|
|
|
- any other data type is processed through an implicit recipe. If a suitable recipe do not exists an error is raised.
|
|
"""
|
|
macro gp(args...)
|
|
out = Expr(:call)
|
|
push!(out.args, :(Gnuplot.driver))
|
|
for iarg in 1:length(args)
|
|
arg = args[iarg]
|
|
if (isa(arg, Expr) && (arg.head == :(=)))
|
|
sym = string(arg.args[1])
|
|
val = arg.args[2]
|
|
push!(out.args, :((Symbol($sym),$val)))
|
|
else
|
|
push!(out.args, arg)
|
|
end
|
|
end
|
|
return esc(out)
|
|
end
|
|
|
|
|
|
"""
|
|
@gsp args...
|
|
|
|
This macro accepts the same syntax as [`@gp`](@ref), but produces a 3D plot instead of a 2D one.
|
|
"""
|
|
macro gsp(args...)
|
|
out = Expr(:macrocall, Symbol("@gp"), LineNumberNode(1, nothing))
|
|
push!(out.args, args...)
|
|
push!(out.args, Expr(:kw, :is3d, true))
|
|
return esc(out)
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
save(sid::Symbol; term="", output="")
|
|
save(sid::Symbol, script_filename::String, ;term="", output="")
|
|
save(; term="", output="")
|
|
save(script_filename::String ;term="", output="")
|
|
|
|
Export a (multi-)plot into the external file name provided in the `output=` keyword. The gnuplot terminal to use is provided through the `term=` keyword.
|
|
|
|
If the `script_filename` argument is provided a *gnuplot script* will be written in place of the output image. The latter can then be used in a pure gnuplot session (Julia is no longer needed) to generate exactly the same original plot.
|
|
|
|
If the `sid` argument is provided the operation applies to the corresponding session.
|
|
"""
|
|
save( ; kw...) = execall(getsession() ; kw...)
|
|
save(sid::Symbol; kw...) = execall(getsession(sid); kw...)
|
|
save( file::AbstractString; kw...) = savescript(getsession() , file, kw...)
|
|
save(sid::Symbol, file::AbstractString; kw...) = savescript(getsession(sid), file, kw...)
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ HIGH LEVEL FACILITIES │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# --------------------------------------------------------------------
|
|
function splash(outputfile="")
|
|
quit(:splash)
|
|
gp = getsession(:splash)
|
|
if outputfile == ""
|
|
# Try to set a reasonably modern terminal. Setting the size
|
|
# is necessary for the text to be properly sized. The
|
|
# `noenhanced` option is required to display the "@" character
|
|
# (alternatively use "\\\\@", but it doesn't work on all
|
|
# terminals).
|
|
terms = terminals()
|
|
if "wxt" in terms
|
|
gpexec(gp, "set term wxt noenhanced size 600,300")
|
|
elseif "qt" in terms
|
|
gpexec(gp, "set term qt noenhanced size 600,300")
|
|
elseif "aqua" in terms
|
|
gpexec(gp, "set term aqua noenhanced size 600,300")
|
|
else
|
|
@warn "None of the `wxt`, `qt` and `aqua` terminals are available. Output may look strange..."
|
|
end
|
|
else
|
|
gpexec(gp, "set term unknown")
|
|
end
|
|
@gp :- :splash "set margin 0" "set border 0" "unset tics" :-
|
|
@gp :- :splash xr=[-0.3,1.7] yr=[-0.3,1.1] :-
|
|
@gp :- :splash "set origin 0,0" "set size 1,1" :-
|
|
@gp :- :splash "set label 1 at graph 1,1 right offset character -1,-1 font 'Verdana,20' tc rgb '#4d64ae' ' Ver: " * string(version()) * "' " :-
|
|
@gp :- :splash "set arrow 1 from graph 0.05, 0.15 to graph 0.95, 0.15 size 0.2,20,60 noborder lw 9 lc rgb '#4d64ae'" :-
|
|
@gp :- :splash "set arrow 2 from graph 0.15, 0.05 to graph 0.15, 0.95 size 0.2,20,60 noborder lw 9 lc rgb '#4d64ae'" :-
|
|
@gp :- :splash ["0.35 0.65 @ 13253682", "0.85 0.65 g 3774278", "1.3 0.65 p 9591203"] "w labels notit font 'Mono,160' tc rgb var"
|
|
(outputfile == "") || save(:splash, term="pngcairo transparent noenhanced size 600,300", output=outputfile)
|
|
nothing
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
dataset_names(sid::Symbol)
|
|
dataset_names()
|
|
|
|
Return a vector with all dataset names for the `sid` session. If `sid` is not provided the default session is considered.
|
|
"""
|
|
dataset_names(sid::Symbol) = string.(keys(getsession(sid).datas))
|
|
dataset_names() = dataset_names(options.default)
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
session_names()
|
|
|
|
Return a vector with all currently active sessions.
|
|
"""
|
|
session_names() = Symbol.(keys(sessions))
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
stats(sid::Symbol,name::String)
|
|
stats(name::String)
|
|
stats(sid::Symbol)
|
|
stats()
|
|
|
|
Print a statistical summary for the `name` dataset, belonging to `sid` session. If `name` is not provdied a summary is printed for each dataset in the session. If `sid` is not provided the default session is considered.
|
|
|
|
This function is actually a wrapper for the gnuplot command `stats`.
|
|
"""
|
|
stats(sid::Symbol, name::String) = stats(getsession(sid), name)
|
|
stats(name::String) = stats(options.default, name)
|
|
stats(sid::Symbol) = stats(getsession(sid))
|
|
stats() = for (sid, d) in sessions
|
|
stats(sid)
|
|
end
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
"""
|
|
palette_names()
|
|
|
|
Return a vector with all available color schemes for the [`palette`](@ref) and [`linetypes`](@ref) function.
|
|
"""
|
|
palette_names() = Symbol.(keys(ColorSchemes.colorschemes))
|
|
|
|
|
|
"""
|
|
linetypes(cmap::ColorScheme; lw=1, ps=1, dashed=false, rev=false)
|
|
linetypes(s::Symbol; lw=1, ps=1, dashed=false, rev=false)
|
|
|
|
Convert a `ColorScheme` object into a string containing the gnuplot commands to set up *linetype* colors.
|
|
|
|
If the argument is a `Symbol` it is interpreted as the name of one of the predefined schemes in [ColorSchemes](https://juliagraphics.github.io/ColorSchemes.jl/stable/basics/#Pre-defined-schemes-1).
|
|
|
|
If `rev=true` the line colors are reversed. If a numeric or string value is provided through the `lw` and `ps` keywords thay are used to set the line width and the point size respectively. If `dashed` is true the linetypes with index greater than 1 will be displayed with dashed pattern.
|
|
"""
|
|
linetypes(s::Symbol; kwargs...) = linetypes(colorschemes[s]; kwargs...)
|
|
function linetypes(cmap::ColorScheme; lw=1, ps=1, dashed=false, rev=false)
|
|
out = Vector{String}()
|
|
push!(out, "unset for [i=1:256] linetype i")
|
|
for i in 1:length(cmap.colors)
|
|
if rev
|
|
color = cmap.colors[end - i + 1]
|
|
else
|
|
color = cmap.colors[i]
|
|
end
|
|
dt = (dashed ? "$i" : "solid")
|
|
push!(out, "set linetype $i lc rgb '#" * Colors.hex(color) * "' lw $lw dt $dt pt $i ps $ps")
|
|
end
|
|
return join(out, "\n") * "\nset linetype cycle " * string(length(cmap.colors)) * "\n"
|
|
end
|
|
|
|
|
|
"""
|
|
palette(cmap::ColorScheme; rev=false)
|
|
palette(s::Symbol; rev=false)
|
|
|
|
Convert a `ColorScheme` object into a string containing the gnuplot commands to set up the corresponding palette.
|
|
|
|
If the argument is a `Symbol` it is interpreted as the name of one of the predefined schemes in [ColorSchemes](https://juliagraphics.github.io/ColorSchemes.jl/stable/basics/#Pre-defined-schemes-1). If `rev=true` the palette is reversed.
|
|
"""
|
|
palette(s::Symbol; rev=false) = palette(colorschemes[s], rev=rev)
|
|
function palette(cmap::ColorScheme; rev=false)
|
|
levels = Vector{String}()
|
|
for x in LinRange(0, 1, length(cmap.colors))
|
|
if rev
|
|
color = get(cmap, 1-x)
|
|
else
|
|
color = get(cmap, x)
|
|
end
|
|
push!(levels, "$x '#" * Colors.hex(color) * "'")
|
|
end
|
|
return "set palette defined (" * join(levels, ", ") * ")\nset palette maxcol $(length(cmap.colors))\n"
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
terminals()
|
|
|
|
Return a `Vector{String}` with the names of all the available gnuplot terminals.
|
|
"""
|
|
terminals() = string.(split(strip(gpexec("print GPVAL_TERMINALS")), " "))
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
terminal(sid::Symbol)
|
|
terminal()
|
|
|
|
Return a `String` with the current gnuplot terminal (and its options) of the process associated to session `sid`, or to the default session (if `sid` is not provided).
|
|
"""
|
|
terminal(sid::Symbol=options.default) = gpexec(getsession(sid), "print GPVAL_TERM") * " " * gpexec(getsession(sid), "print GPVAL_TERMOPTIONS")
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
test_terminal(term=nothing; linetypes=nothing, palette=nothing)
|
|
|
|
Run the `test` and `test palette` commands on a gnuplot terminal.
|
|
|
|
If no `term` is given it will use the default terminal. If `lt` and `pal` are given they are used as input to the [`linetypes`](@ref) and [`palette`](@ref) function repsetcively to load the associated color scheme.
|
|
|
|
# Examples
|
|
```julia
|
|
test_terminal()
|
|
test_terminal("wxt", lt=:rust, pal=:viridis)
|
|
```
|
|
"""
|
|
function test_terminal(term=nothing; lt=nothing, pal=nothing)
|
|
quit(:test_term)
|
|
quit(:test_palette)
|
|
if !isnothing(term)
|
|
gpexec(:test_term , "set term $term")
|
|
gpexec(:test_palette , "set term $term")
|
|
end
|
|
s = (isnothing(lt) ? "" : linetypes(lt))
|
|
gpexec(:test_term , "$s; test")
|
|
s = (isnothing(pal) ? "" : palette(pal))
|
|
gpexec(:test_palette , "$s; test palette")
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
Histogram1D
|
|
|
|
A 1D histogram data.
|
|
|
|
# Fields
|
|
- `bins::Vector{Float64}`: bin center values;
|
|
- `counts::Vector{Float64}`: counts in the bins;
|
|
- `binsize::Float64`: size of each bin;
|
|
"""
|
|
mutable struct Histogram1D
|
|
bins::Vector{Float64}
|
|
counts::Vector{Float64}
|
|
binsize::Float64
|
|
end
|
|
|
|
"""
|
|
Histogram2D
|
|
|
|
A 2D histogram data.
|
|
|
|
# Fields
|
|
- `bins1::Vector{Float64}`: bin center values along first dimension;
|
|
- `bins2::Vector{Float64}`: bin center values along second dimension;
|
|
- `counts::Vector{Float64}`: counts in the bins;
|
|
- `binsize1::Float64`: size of each bin along first dimension;
|
|
- `binsize2::Float64`: size of each bin along second dimension;
|
|
"""
|
|
mutable struct Histogram2D
|
|
bins1::Vector{Float64}
|
|
bins2::Vector{Float64}
|
|
counts::Matrix{Float64}
|
|
binsize1::Float64
|
|
binsize2::Float64
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
hist(v::Vector{T}; range=extrema(v), bs=NaN, nbins=0, pad=true) where T <: Real
|
|
|
|
Calculates the histogram of the values in `v` and returns a [`Histogram1D`](@ref) structure.
|
|
|
|
# Arguments
|
|
- `v`: a vector of values to compute the histogra;
|
|
- `range`: values of the left edge of the first bin and of the right edge of the last bin;
|
|
- `bs`: size of histogram bins;
|
|
- `nbins`: number of bins in the histogram;
|
|
- `pad`: if true add one dummy bins with zero counts before the first bin and after the last.
|
|
|
|
If `bs` is given `nbins` is ignored.
|
|
|
|
# Example
|
|
```julia
|
|
v = randn(1000)
|
|
h = hist(v, bs=0.5)
|
|
@gp h # preview
|
|
@gp h.bins h.counts "w histep notit"
|
|
```
|
|
"""
|
|
function hist(v::Vector{T}; range=[NaN,NaN], bs=NaN, nbins=0, pad=true) where T <: Real
|
|
i = findall(isfinite.(v))
|
|
isnan(range[1]) && (range[1] = minimum(v[i]))
|
|
isnan(range[2]) && (range[2] = maximum(v[i]))
|
|
i = findall(isfinite.(v) .& (v.>= range[1]) .& (v.<= range[2]))
|
|
(nbins > 0) && (bs = (range[2] - range[1]) / nbins)
|
|
if isfinite(bs)
|
|
rr = range[1]:bs:range[2]
|
|
if maximum(rr) < range[2]
|
|
rr = range[1]:bs:(range[2]+bs)
|
|
end
|
|
hh = fit(Histogram, v[i], rr, closed=:left)
|
|
if sum(hh.weights) < length(i)
|
|
j = findall(v[i] .== range[2])
|
|
@assert length(j) == (length(i) - sum(hh.weights))
|
|
hh.weights[end] += length(j)
|
|
end
|
|
else
|
|
hh = fit(Histogram, v[i], closed=:left)
|
|
end
|
|
@assert sum(hh.weights) == length(i)
|
|
x = collect(hh.edges[1])
|
|
x = (x[1:end-1] .+ x[2:end]) ./ 2
|
|
h = hh.weights
|
|
binsize = x[2] - x[1]
|
|
if pad
|
|
x = [x[1]-binsize, x..., x[end]+binsize]
|
|
h = [0, h..., 0]
|
|
end
|
|
return Histogram1D(x, h, binsize)
|
|
end
|
|
|
|
|
|
"""
|
|
hist(v1::Vector{T1 <: Real}, v2::Vector{T2 <: Real}; range1=[NaN,NaN], bs1=NaN, nbins1=0, range2=[NaN,NaN], bs2=NaN, nbins2=0)
|
|
|
|
Calculates the 2D histogram of the values in `v1` and `v2` and returns a [`Histogram2D`](@ref) structure.
|
|
|
|
# Arguments
|
|
- `v1`: a vector of values along the first dimension;
|
|
- `v2`: a vector of values along the second dimension;
|
|
- `range1`: values of the left edge of the first bin and of the right edge of the last bin, along the first dimension;
|
|
- `range1`: values of the left edge of the first bin and of the right edge of the last bin, along the second dimension;
|
|
- `bs1`: size of histogram bins along the first dimension;
|
|
- `bs2`: size of histogram bins along the second dimension;
|
|
- `nbins1`: number of bins along the first dimension;
|
|
- `nbins2`: number of bins along the second dimension;
|
|
|
|
If `bs1` (`bs2`) is given `nbins1` (`nbins2`) is ignored.
|
|
|
|
# Example
|
|
```julia
|
|
v1 = randn(1000)
|
|
v2 = randn(1000)
|
|
h = hist(v1, v2, bs1=0.5, bs2=0.5)
|
|
@gp h # preview
|
|
@gp "set size ratio -1" "set auto fix" h.bins1 h.bins2 h.counts "w image notit"
|
|
```
|
|
"""
|
|
function hist(v1::Vector{T1}, v2::Vector{T2};
|
|
range1=[NaN,NaN], bs1=NaN, nbins1=0,
|
|
range2=[NaN,NaN], bs2=NaN, nbins2=0) where {T1 <: Real, T2 <: Real}
|
|
@assert length(v1) == length(v2)
|
|
i = findall(isfinite.(v1) .& isfinite.(v2))
|
|
isnan(range1[1]) && (range1[1] = minimum(v1[i]))
|
|
isnan(range1[2]) && (range1[2] = maximum(v1[i]))
|
|
isnan(range2[1]) && (range2[1] = minimum(v2[i]))
|
|
isnan(range2[2]) && (range2[2] = maximum(v2[i]))
|
|
|
|
i = findall(isfinite.(v1) .& (v1.>= range1[1]) .& (v1.<= range1[2]) .&
|
|
isfinite.(v2) .& (v2.>= range2[1]) .& (v2.<= range2[2]))
|
|
(nbins1 > 0) && (bs1 = (range1[2] - range1[1]) / nbins1)
|
|
(nbins2 > 0) && (bs2 = (range2[2] - range2[1]) / nbins2)
|
|
if isfinite(bs1) && isfinite(bs2)
|
|
hh = fit(Histogram, (v1[i], v2[i]), (range1[1]:bs1:range1[2], range2[1]:bs2:range2[2]), closed=:left)
|
|
else
|
|
hh = fit(Histogram, (v1[i], v2[i]), closed=:left)
|
|
end
|
|
x1 = collect(hh.edges[1])
|
|
x1 = (x1[1:end-1] .+ x1[2:end]) ./ 2
|
|
x2 = collect(hh.edges[2])
|
|
x2 = (x2[1:end-1] .+ x2[2:end]) ./ 2
|
|
|
|
binsize1 = x1[2] - x1[1]
|
|
binsize2 = x2[2] - x2[1]
|
|
return Histogram2D(x1, x2, hh.weights, binsize1, binsize2)
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
boxxyerror(x, y; xmin=NaN, ymin=NaN, xmax=NaN, ymax=NaN, cartesian=false)
|
|
"""
|
|
function boxxyerror(x, y; xmin=NaN, ymin=NaN, xmax=NaN, ymax=NaN, cartesian=false)
|
|
function box(v; vmin=NaN, vmax=NaN)
|
|
vlow = Vector{Float64}(undef, length(v))
|
|
vhigh = Vector{Float64}(undef, length(v))
|
|
for i in 2:length(v)-1
|
|
vlow[i] = (v[i-1] + v[i]) / 2
|
|
vhigh[i] = (v[i+1] + v[i]) / 2
|
|
end
|
|
vlow[1] = v[ 1 ] - (v[ 2 ] - v[ 1 ] ) / 2
|
|
vlow[end] = v[end] - (v[end] - v[end-1]) / 2
|
|
vhigh[1] = v[ 1 ] + (v[ 2 ] - v[ 1 ] ) / 2
|
|
vhigh[end] = v[end] + (v[end] - v[end-1]) / 2
|
|
|
|
isfinite(vmin) && (vlow[ 1 ] = vmin)
|
|
isfinite(vmax) && (vhigh[end] = vmax)
|
|
return (vlow, vhigh)
|
|
end
|
|
@assert issorted(x)
|
|
@assert issorted(y)
|
|
xlow, xhigh = box(x, vmin=xmin, vmax=xmax)
|
|
ylow, yhigh = box(y, vmin=ymin, vmax=ymax)
|
|
if !cartesian
|
|
return (x, y, xlow, xhigh, ylow, yhigh)
|
|
end
|
|
i = repeat(1:length(x), outer=length(y))
|
|
j = repeat(1:length(y), inner=length(x))
|
|
return (x[i], y[j], xlow[i], xhigh[i], ylow[j], yhigh[j])
|
|
end
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
Path2d
|
|
|
|
A path in 2D.
|
|
|
|
# Fields
|
|
- `x::Vector{Float64}`
|
|
- `y::Vector{Float64}`
|
|
"""
|
|
mutable struct Path2d
|
|
x::Vector{Float64}
|
|
y::Vector{Float64}
|
|
Path2d() = new(Vector{Float64}(), Vector{Float64}())
|
|
end
|
|
|
|
|
|
"""
|
|
IsoContourLines
|
|
|
|
Coordinates of all contour lines of a given level.
|
|
|
|
# Fields
|
|
- `paths::Vector{Path2d}`: vector of [`Path2d`](@ref) objects, one for each continuous path;
|
|
- `data::Vector{String}`: vector with string representation of all paths (ready to be sent to gnuplot);
|
|
- `z::Float64`: level of the contour lines.
|
|
"""
|
|
mutable struct IsoContourLines
|
|
paths::Vector{Path2d}
|
|
data::Vector{String}
|
|
z::Float64
|
|
function IsoContourLines(paths::Vector{Path2d}, z)
|
|
@assert length(z) == 1
|
|
data = Vector{String}()
|
|
for i in 1:length(paths)
|
|
append!(data, arrays2datablock(paths[i].x, paths[i].y, z .* fill(1., length(paths[i].x))))
|
|
push!(data, "")
|
|
push!(data, "")
|
|
end
|
|
return new(paths, data, z)
|
|
end
|
|
end
|
|
|
|
|
|
"""
|
|
contourlines(x::Vector{Float64}, y::Vector{Float64}, h::Matrix{Float64}; cntrparam="level auto 10")
|
|
|
|
Compute paths of contour lines for 2D data, and return a vector of [`IsoContourLines`](@ref) object.
|
|
|
|
# Arguments:
|
|
- `x`, `y`: Coordinates;
|
|
- `h`: the levels on which iso contour lines are to be calculated
|
|
- `cntrparam`: settings to compute contour line paths (see gnuplot documentation for `cntrparam`).
|
|
|
|
# Example
|
|
```julia
|
|
x = randn(5000);
|
|
y = randn(5000);
|
|
h = hist(x, y, nbins1=20, nbins2=20);
|
|
clines = contourlines(h.bins1, h.bins2, h.counts, cntrparam="levels discrete 15, 30, 45");
|
|
@gp "set size ratio -1"
|
|
for i in 1:length(clines)
|
|
@gp :- clines[i].data "w l t '\$(clines[i].z)' dt \$i"
|
|
end
|
|
```
|
|
"""
|
|
function contourlines(args...; cntrparam="level auto 10")
|
|
lines = gp_write_table("set contour base", "unset surface",
|
|
"set cntrparam $cntrparam", args..., is3d=true)
|
|
|
|
level = NaN
|
|
path = Path2d()
|
|
paths = Vector{Path2d}()
|
|
levels = Vector{Float64}()
|
|
for l in lines
|
|
l = strip(l)
|
|
if (l == "") ||
|
|
!isnothing(findfirst("# Contour ", l))
|
|
if length(path.x) > 2
|
|
push!(paths, path)
|
|
push!(levels, level)
|
|
end
|
|
path = Path2d()
|
|
|
|
if l != ""
|
|
level = Meta.parse(strip(split(l, ':')[2]))
|
|
end
|
|
continue
|
|
end
|
|
(l[1] == '#') && continue
|
|
|
|
n = Meta.parse.(split(l))
|
|
@assert length(n) == 3
|
|
push!(path.x, n[1])
|
|
push!(path.y, n[2])
|
|
end
|
|
if length(path.x) > 2
|
|
push!(paths, path)
|
|
push!(levels, level)
|
|
end
|
|
@assert length(paths) > 0
|
|
i = sortperm(levels)
|
|
paths = paths[ i]
|
|
levels = levels[i]
|
|
|
|
# Join paths with the same level
|
|
out = Vector{IsoContourLines}()
|
|
for z in unique(levels)
|
|
i = findall(levels .== z)
|
|
push!(out, IsoContourLines(paths[i], z))
|
|
end
|
|
return out
|
|
end
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ GNUPLOT REPL │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# --------------------------------------------------------------------
|
|
"""
|
|
Gnuplot.init_repl(; start_key='>')
|
|
|
|
Install a hook to replace the common Julia REPL with a gnuplot one. The key to start the REPL is the one provided in `start_key` (default: `>`).
|
|
|
|
Note: the gnuplot REPL operates only on the default session.
|
|
"""
|
|
function repl_init(; start_key='>')
|
|
function repl_exec(s)
|
|
for s in writeread(getsession(), s)
|
|
println(s)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function repl_isvalid(s)
|
|
input = strip(String(take!(copy(REPL.LineEdit.buffer(s)))))
|
|
(length(input) == 0) || (input[end] != '\\')
|
|
end
|
|
|
|
initrepl(repl_exec,
|
|
prompt_text="gnuplot> ",
|
|
prompt_color = :blue,
|
|
start_key=start_key,
|
|
mode_name="Gnuplot",
|
|
completion_provider=REPL.LineEdit.EmptyCompletionProvider(),
|
|
valid_input_checker=repl_isvalid)
|
|
end
|
|
|
|
|
|
|
|
# ╭───────────────────────────────────────────────────────────────────╮
|
|
# │ VARIABLE ACCESS │
|
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
# --------------------------------------------------------------------
|
|
gpvars() = gpvars(options.default)
|
|
function gpvars(sid::Symbol)
|
|
gp = getsession(sid)
|
|
vars = string.(strip.(split(gpexec("show var all"), '\n')))
|
|
|
|
out = Dict{Symbol, Union{String, Real}}()
|
|
for v in vars
|
|
if length(v) > 6
|
|
if v[1:6] == "GPVAL_"
|
|
s = string.(strip.(split(v[7:end], '=')))
|
|
key = Symbol(s[1])
|
|
if s[2][1] == '"'
|
|
out[key] = s[2][2:end-1]
|
|
else
|
|
try
|
|
out[key] = Meta.parse(s[2])
|
|
catch
|
|
out[key] = s[2]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
|
|
|
|
|
|
include("recipes.jl")
|
|
|
|
|
|
end #module
|