diff --git a/src/Gnuplot.jl b/src/Gnuplot.jl index a8a6d39..786aa6d 100644 --- a/src/Gnuplot.jl +++ b/src/Gnuplot.jl @@ -1,109 +1,347 @@ -isdefined(Base, :__precompile__) && __precompile__() +__precompile__(false) module Gnuplot using AbbrvKW -include("GnuplotInternals.jl") -importall .p_ - ###################################################################### -# Get/set options +# Structure definitions ###################################################################### -""" -# Gnuplot.getStartup - -Return the gnuplot command(s) to be executed at the beginning of each -session. -""" -getStartup() = p_.main.startup - -""" -# Gnuplot.getSpawnCmd - -Return the command to spawn a gnuplot process. -""" -getSpawnCmd() = p_.main.gnuplotCmd - -""" -# Gnuplot.getVerbose - -Return the verbosity level. -""" -getVerbose() = p_.main.verboseLev - #--------------------------------------------------------------------- """ -# Gnuplot.setOption - -Set package options. - -## Example: -``` -gp.setOption(cmd="/path/to/gnuplot", verb=2, startup="set term wxt") -``` - -## Keywords: -- `cmd::String`: command to spawn a gnuplot process; -- `startup::String`: gnuplot command to be executed at the beginning - of each session; -- `verbose::Int`: verbosity level (in the range 0 รท 4) - -The package options can beretrieved with: `gp.getStartup`, -`gp.getSpawnCmd` and `gp.getVerbose`. +Structure containing a single plot command and the associated +multiplot index. """ -@AbbrvKW function setOption(;cmd::Union{Void,String}=nothing, - startup::Union{Void,String}=nothing, - verbose::Union{Void,Int}=nothing) - if startup != nothing - p_.main.startup = startup - end +mutable struct Command + cmd::String # command + id::Int # multiplot index +end - if cmd != nothing - p_.main.gnuplotCmd = cmd - p_.checkGnuplotVersion() - end +#--------------------------------------------------------------------- +""" +Structure containing the state of a single gnuplot session. +""" +mutable struct Session + pin::Base.Pipe + pout::Base.Pipe + perr::Base.Pipe + proc::Base.Process + channel::Channel{String} + blockCnt::Int # data blocks counter + lastBlock::String # name of the last data block + cmds::Vector{Command} # gnuplot commands + data::Vector{String} # data blocks + splot::Bool # plot / splot session + plot::Vector{Command} # plot specifications associated to each data block + cid::Int # current multiplot index (0 if no multiplot) - if verbose != nothing - @assert (0 <= verbose <= 4) - p_.main.verboseLev = verbose - end + function Session(cmd) + this = new() + this.pin = Base.Pipe() + this.pout = Base.Pipe() + this.perr = Base.Pipe() + this.channel = Channel{String}(32) + # Start gnuplot process + this.proc = spawn(`$cmd`, (this.pin, this.pout, this.perr)) + + # Close unused sides of the pipes + Base.close_pipe_sync(this.pout.in) + Base.close_pipe_sync(this.perr.in) + Base.close_pipe_sync(this.pin.out) + Base.start_reading(this.pout.out) + Base.start_reading(this.perr.out) + + this.blockCnt = 0 + this.lastBlock = "" + this.cmds = Vector{Command}() + this.data = Vector{String}() + this.splot = false + this.plot = Vector{Command}() + this.cid = 0 + return this + end +end + + +#--------------------------------------------------------------------- +""" +Structure containing the global package state. +""" +mutable struct State + sessions::Dict{Int, Session} + current::Int + + State() = new(Dict{Int, Session}(), 0) +end + +const state = State() + + +#--------------------------------------------------------------------- +""" +Structure containing the global options. +""" +mutable struct Options + colorOut::Symbol # gnuplot STDOUT is printed with this color + colorIn::Symbol # gnuplot STDIN is printed with this color + verbosity::Int # verbosity level (0 - 4), default: 3 + gnuplotCmd::String # command used to start the gnuplot process + startup::String # commands automatically sent to each new gnuplot process + + Options() = new(:cyan, :yellow, 3, "gnuplot", "") +end + +const gpOptions = Options() + + + +###################################################################### +# Utils +###################################################################### + +#--------------------------------------------------------------------- +""" +Logging facility (each line is prefixed with the session ID.) + +Printing occur only if the logging level is >= current verbosity +level. +""" +function log(level::Int, s::String, id=0) + (gpOptions.verbosity < level) && return + if id == 0 + id = state.current + color = gpOptions.colorOut + else + color = gpOptions.colorIn + end + prefix = string("GP(", id, ")") + for v in split(s, "\n") + print_with_color(color, "$prefix $v\n") + end return nothing end -###################################################################### -# Functions to handle multiple gnuplot instances -###################################################################### - #--------------------------------------------------------------------- """ -# Gnuplot.handles - -Return a `Vector{Int}` of available session handles. +sessionCollector """ -function handles() - return deepcopy(p_.main.handles) -end +function sessionCollector() + for (id, cur) in state.sessions + if !Base.process_running(cur.proc) + log(3, "Deleting session $id") + delete!(state.sessions, id) - -""" -# Gnuplot.current - -Return the handle of the current session. -""" -function current() - p_.getProcOrStartIt() - return p_.main.handles[p_.main.curPos] + if (id == state.current) + state.current = 0 + end + end + end end #--------------------------------------------------------------------- """ -# Gnuplot.setCurrent +Read gnuplot outputs, and optionally redirect to a `Channel`. + +This fuction is supposed to be run in a `Task`. +""" +function readTask(sIN, channel, id) + saveOutput = false + while isopen(sIN) + line = convert(String, readline(sIN)) + + if line == "GNUPLOT_JL_SAVE_OUTPUT" + saveOutput = true + log(4, "|begin of captured data =========================", id) + else + if saveOutput + put!(channel, line) + end + + if line == "GNUPLOT_JL_SAVE_OUTPUT_END" + saveOutput = false + log(4, "|end of captured data ===========================", id) + elseif line != "" + if saveOutput + log(3, "| " * line, id) + else + log(2, " " * line, id) + end + end + end + end + + log(1, "pipe closed") + return nothing +end + + +#--------------------------------------------------------------------- +""" +Return the current session, or start a new one if none is running. +""" +function getCurrentOrStartIt() + sessionCollector() + + if haskey(state.sessions, state.current) + return state.sessions[state.current] + end + + return state.sessions[gpNewSession()] +end + + +###################################################################### +# Exported symbols +###################################################################### + +export gpCheckVersion, gpNewSession, gpExit, gpExitAll, gpSetCurrentID, + gpCurrentID, gpGetIDs, gpSend, gpReset, gpCmd, gpData, gpLastBlock, + gpGetVal, gpPlot, gpMulti, gpNext, gpDump, gpOptions, + gpTerminals, gpTerminal, + @gpi, @gp, @gp_str, @gp_cmd + +#--------------------------------------------------------------------- +""" +Check gnuplot is runnable with the command given in `state.gnuplotCmd`. +Also check that gnuplot version is >= 4.7 (required to use data +blocks). +""" +function gpCheckVersion() + cmd = `$(gpOptions.gnuplotCmd) --version` + out, procs = open(`$cmd`, "r") + s = String(read(out)) + if !success(procs) + error("An error occurred while running: " * string(cmd)) + end + + s = split(s, " ") + ver = "" + for token in s + try + ver = VersionNumber("$token") + break + catch + end + end + + if ver < v"4.7" + # Do not raise error in order to pass Travis CI test, since it has v4.6 + warn("gnuplot ver. >= 4.7 is required, but " * string(ver) * " was found.") + end + if ver < v"4.6" + error("gnuplot ver. >= 4.7 is required, but " * string(ver) * " was found.") + end + log(1, "Found gnuplot version: " * string(ver)) + return ver +end + + + +#--------------------------------------------------------------------- +""" +# gpNewSession + +Create a new session (by starting a new gnuplot process), make it the +current one, and return the new session ID. + +E.g., to compare the look and feel of two terminals: +``` +id1 = gpNewSession() +gpSend("set term qt") +gpSend("plot sin(x)") + +id2 = gpNewSession() +gpSend("set term wxt") +gpSend("plot sin(x)") + +gpSetCurrentID(id1) +gpSend("set title 'My title'") +gpSend("replot") + +gpSetCurrentID(id2) +gpSend("set title 'My title'") +gpSend("replot") + +gpExitAll() +``` +""" +function gpNewSession() + gpCheckVersion() + + cur = Session(gpOptions.gnuplotCmd) + + id = 1 + if length(state.sessions) > 0 + id = maximum(keys(state.sessions)) + 1 + end + state.sessions[id] = cur + state.current = id + + # Start reading tasks for STDOUT and STDERR + @async readTask(cur.pout, cur.channel, id) + @async readTask(cur.perr, cur.channel, id) + gpCmd(gpOptions.startup) + + log(1, "New session started with ID $id") + return id +end + + +#--------------------------------------------------------------------- +""" +# gpExit + +Close current session and quit the corresponding gnuplot process. +""" +function gpExit() + sessionCollector() + + if length(state.sessions) == 0 + log(1, "No session to close.") + return 0 + end + + cur = state.sessions[state.current] + close(cur.pin) + close(cur.pout) + close(cur.perr) + wait( cur.proc) + exitCode = cur.proc.exitcode + log(1, string("Process exited with status ", exitCode)) + + sessionCollector() + + # Select next session + if length(state.sessions) > 0 + state.current = maximum(keys(state.sessions)) + end + + return exitCode +end + + +#--------------------------------------------------------------------- +""" +# gpExitAll + +Repeatedly call `gpExit` until all sessions are closed. +""" +function gpExitAll() + while length(state.sessions) > 0 + gpExit() + end + return nothing +end + + +#--------------------------------------------------------------------- +""" +# gpSetCurrentID Change the current session. @@ -111,122 +349,38 @@ Change the current session. - `handle::Int`: the handle of the session to select as current. ## See also: -- `gp.current`: return the current session handle; -- `gp.handles`: return the list of available handles. +- `gpCurrentID`: return the current session handle; +- `gpGetIDs`: return the list of available handles. """ -function setCurrent(handle) - i = find(p_.main.handles .== handle) - @assert length(i) == 1 - i = i[1] - @assert Base.process_running(p_.main.procs[i].proc) - - p_.main.curPos = i +function gpSetCurrentID(id::Int) + sessionCollector() + @assert haskey(state.sessions, id) "No session with ID $id" + state.current = id + nothing end #--------------------------------------------------------------------- """ -# Gnuplot.session +# gpCurrentID -Create a new session (by starting a new gnuplot process), make it the -current one, and return the new handle. - -E.g., to compare the look and feel of two terminals: -``` -id1 = gp.session() -gp.send("set term qt") -gp.send("plot sin(x)") - -id2 = gp.session() -gp.send("set term wxt") -gp.send("plot sin(x)") - -gp.setCurrent(id1) -gp.send("set title 'My title'") -gp.send("replot") - -gp.setCurrent(id2) -gp.send("set title 'My title'") -gp.send("replot") - -gp.exitAll() -``` +Return the handle of the current session. """ -function session() - if length(p_.main.handles) > 0 - newhandle = max(p_.main.handles...) + 1 - else - newhandle = 1 - end - - if p_.main.gnuplotCmd == "" - p_.main.gnuplotCmd = "gnuplot" - p_.checkGnuplotVersion() - end - - push!(p_.main.procs, p_.GnuplotProc(p_.main.gnuplotCmd)) - push!(p_.main.states, p_.GnuplotSession()) - push!(p_.main.handles, newhandle) - p_.main.curPos = length(p_.main.handles) - - # Start reading tasks for STDOUT and STDERR - @async p_.readTask(p_.main.procs[end].pout, p_.main.procs[end].channel, id=newhandle) - @async p_.readTask(p_.main.procs[end].perr, p_.main.procs[end].channel, id=newhandle) - - if p_.main.startup != "" - cmd(p_.main.startup) - end - - p_.log(1, "New session started with handle $newhandle") - return newhandle +function gpCurrentID() + sessionCollector() + return state.current end #--------------------------------------------------------------------- """ -# Gnuplot.exit +# gpGetIDs -Close current session and quit the corresponding gnuplot process. +Return a `Vector{Int}` of available session IDs. """ -function exit() - if p_.main.curPos == 0 - return 0 - end - - p = p_.main.procs[p_.main.curPos] - close(p.pin) - close(p.pout) - close(p.perr) - wait(p.proc) - @assert !Base.process_running(p.proc) - - p_.log(1, string("Process exited with status ", p.proc.exitcode)) - - deleteat!(p_.main.procs , p_.main.curPos) - deleteat!(p_.main.states , p_.main.curPos) - deleteat!(p_.main.handles, p_.main.curPos) - - if length(p_.main.handles) > 0 - setCurrent(max(p_.main.handles...)) - else - p_.main.curPos = 0 - end - - return p.proc.exitcode -end - - -#--------------------------------------------------------------------- -""" -# Gnuplot.exitAll - -Repeatedly call `gp.exit` until all sessions are closed. -""" -function exitAll() - while length(p_.main.handles) > 0 - exit() - end - return nothing +function gpGetIDs() + sessionCollector() + return keys(state.sessions) end @@ -235,16 +389,16 @@ end ###################################################################### """ -# Gnuplot.send +# gpSend Send a string to the current session's gnuplot STDIN. -The commands sent through `gp.send` are not stored in the current -session (use `gp.cmd` to save commands in the current session). +The commands sent through `gpSend` are not stored in the current +session (use `cmd` to save commands in the current session). ## Example: ``` -println("Current terminal: ", gp.send("print GPVAL_TERM", capture=true)) +println("Current terminal: ", gpSend("print GPVAL_TERM", capture=true)) ``` ## Arguments: @@ -255,8 +409,8 @@ println("Current terminal: ", gp.send("print GPVAL_TERM", capture=true)) reply, and return it as a `Vector{String}`. Otherwise return `nothing` immediately. """ -@AbbrvKW function send(cmd::String; capture::Bool=false) - p = p_.getProcOrStartIt() +@AbbrvKW function gpSend(cmd::String; capture::Bool=false, verbosity=2) + p = getCurrentOrStartIt() if capture write(p.pin, "print 'GNUPLOT_JL_SAVE_OUTPUT'\n") @@ -264,7 +418,7 @@ println("Current terminal: ", gp.send("print GPVAL_TERM", capture=true)) for s in split(cmd, "\n") w = write(p.pin, strip(s) * "\n") - p_.log(2, "-> $s", color=p_.main.colorIn) + log(verbosity, "-> $s") w <= 0 && error("Writing on gnuplot STDIN pipe returned $w") end @@ -288,30 +442,34 @@ println("Current terminal: ", gp.send("print GPVAL_TERM", capture=true)) return nothing end -###################################################################### -# Handle session, and send data/commands to Gnuplot -###################################################################### #--------------------------------------------------------------------- """ -# Gnuplot.reset +# gpReset Send a 'reset session' command to gnuplot and delete all commands, data, and plots in the current session. """ -function reset() - send("reset session", capture=true) - p_.main.states[p_.main.curPos] = p_.GnuplotSession() - if p_.main.startup != "" - cmd(p_.main.startup) - end +function gpReset() + cur = getCurrentOrStartIt() + + cur.blockCnt = 0 + cur.lastBlock = "" + cur.cmds = Vector{Command}() + cur.data = Vector{String}() + cur.splot = false + cur.plot = Vector{Command}() + cur.cid = 0 + + gpSend("reset session", capture=true) + gpCmd(gpOptions.startup) return nothing end #--------------------------------------------------------------------- """ -# Gnuplot.cmd +# gpCmd Send a command to gnuplot process and store it in the current session. A few, commonly used, commands may be specified through keywords (see @@ -319,16 +477,16 @@ below). ## Examples: ``` -gp.cmd("set grid") -gp.cmd("set key left", xrange=(1,3)) -gp.cmd(title="My title", xlab="X label", xla="Y label") +gpCmd("set grid") +gpCmd("set key left", xrange=(1,3)) +gpCmd(title="My title", xlab="X label", xla="Y label") ``` ## Arguments: - `cmd::String`: command to be sent. ## Keywords: -- `multiID::Int`: ID of the plot the commands belongs to (only useful +- `cid::Int`: ID of the plot the commands belongs to (only useful for multiplots); - `splot::Bool`: set to `true` for a "splot" gnuplot session, `false` for a "plot" one; @@ -343,44 +501,46 @@ gp.cmd(title="My title", xlab="X label", xla="Y label") - `yrange::NTuple{2, Number}`: Y axis range; - `zrange::NTuple{2, Number}`: Z axis range; """ -@AbbrvKW function cmd(s::String=""; - splot::Union{Void,Bool}=nothing, - multiID::Union{Void,Int}=nothing, - xrange::Union{Void,NTuple{2, Number}}=nothing, - yrange::Union{Void,NTuple{2, Number}}=nothing, - zrange::Union{Void,NTuple{2, Number}}=nothing, - title::Union{Void,String}=nothing, - xlabel::Union{Void,String}=nothing, - ylabel::Union{Void,String}=nothing, - zlabel::Union{Void,String}=nothing, - xlog::Union{Void,Bool}=nothing, - ylog::Union{Void,Bool}=nothing, - zlog::Union{Void,Bool}=nothing) +@AbbrvKW function gpCmd(s::String=""; + splot::Union{Void,Bool}=nothing, + cid::Union{Void,Int}=nothing, + xrange::Union{Void,NTuple{2, Number}}=nothing, + yrange::Union{Void,NTuple{2, Number}}=nothing, + zrange::Union{Void,NTuple{2, Number}}=nothing, + title::Union{Void,String}=nothing, + xlabel::Union{Void,String}=nothing, + ylabel::Union{Void,String}=nothing, + zlabel::Union{Void,String}=nothing, + xlog::Union{Void,Bool}=nothing, + ylog::Union{Void,Bool}=nothing, + zlog::Union{Void,Bool}=nothing) - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] - splot == nothing || (cur.splot = splot) - mID = multiID == nothing ? cur.multiID : multiID + cur = getCurrentOrStartIt() + mID = (cid == nothing ? cur.cid : cid) + + if splot != nothing + cur.splot = splot + end if s != "" - push!(cur.cmds, p_.MultiCmd(s, mID)) + push!(cur.cmds, Command(s, mID)) if mID == 0 - send(s) + gpSend(s) end end - xrange == nothing || cmd(multiID=mID, "set xrange [" * join(xrange, ":") * "]") - yrange == nothing || cmd(multiID=mID, "set yrange [" * join(yrange, ":") * "]") - zrange == nothing || cmd(multiID=mID, "set zrange [" * join(zrange, ":") * "]") + xrange == nothing || gpCmd(cid=mID, "set xrange [" * join(xrange, ":") * "]") + yrange == nothing || gpCmd(cid=mID, "set yrange [" * join(yrange, ":") * "]") + zrange == nothing || gpCmd(cid=mID, "set zrange [" * join(zrange, ":") * "]") - title == nothing || cmd(multiID=mID, "set title '" * title * "'") - xlabel == nothing || cmd(multiID=mID, "set xlabel '" * xlabel * "'") - ylabel == nothing || cmd(multiID=mID, "set ylabel '" * ylabel * "'") - zlabel == nothing || cmd(multiID=mID, "set zlabel '" * zlabel * "'") + title == nothing || gpCmd(cid=mID, "set title '" * title * "'") + xlabel == nothing || gpCmd(cid=mID, "set xlabel '" * xlabel * "'") + ylabel == nothing || gpCmd(cid=mID, "set ylabel '" * ylabel * "'") + zlabel == nothing || gpCmd(cid=mID, "set zlabel '" * zlabel * "'") - xlog == nothing || cmd(multiID=mID, (xlog ? "" : "un") * "set logscale x") - ylog == nothing || cmd(multiID=mID, (ylog ? "" : "un") * "set logscale y") - zlog == nothing || cmd(multiID=mID, (zlog ? "" : "un") * "set logscale z") + xlog == nothing || gpCmd(cid=mID, (xlog ? "" : "un") * "set logscale x") + ylog == nothing || gpCmd(cid=mID, (ylog ? "" : "un") * "set logscale y") + zlog == nothing || gpCmd(cid=mID, (zlog ? "" : "un") * "set logscale z") return nothing end @@ -388,30 +548,25 @@ end #--------------------------------------------------------------------- """ -# Gnuplot.data +# gpData Send data to the gnuplot process, store it in the current session, and -return the name of the data block (to be later used with `gp.plot`). +return the name of the data block (to be later used with `plot`). ## Example: ``` x = collect(1.:10) # Automatically generated data block name -name1 = gp.data(x, x.^2) +name1 = gpData(x, x.^2) -# Specify a prefix for the data block name, a sequential counter will -# be appended to ensure the black names are unique -name2 = gp.data(x, x.^2.2, prefix="MyPrefix") - -# Specify the whole data block name. NOTE: avoid using the same name +# Specify the data block name. NOTE: avoid using the same name # multiple times! -name3 = gp.data(x, x.^1.8, name="MyChosenName") +name2 = gpData(x, x.^1.8, name="MyChosenName") -gp.plot(name1) -gp.plot(name2) -gp.plot(name3) -gp.dump() +gpPlot(name1) +gpPlot(name2) +gpDump() ``` ## Arguments: @@ -424,49 +579,100 @@ gp.dump() - `prefix::String`: prefix for data block name (an automatic counter will be appended); """ -@AbbrvKW function data(data::Vararg{AbstractArray{T,1},N}; - name::Union{Void,String}=nothing, - prefix::Union{Void,String}=nothing) where {T<:Number,N} - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] +function gpData(data::Vararg{AbstractArray{T},M}; name="") where {T<:Number,M} + cur = getCurrentOrStartIt() - if name == nothing - name = p_.mkBlockName(prefix=prefix) + if name == "" + name = string("data", cur.blockCnt) + cur.blockCnt += 1 end name = "\$$name" - for i in 2:length(data) - @assert length(data[1]) == length(data[i]) + # Check dimensions + dimX = (size(data[1]))[1] + dimY = 0 + is2D = false + first1D = 0 + coordX = Vector{Float64}() + coordY = Vector{Float64}() + for i in length(data):-1:1 + d = data[i] + @assert ndims(d) <=2 "Array dimensions must be <= 2" + + if ndims(d) == 2 + dimY == 0 && (dimY = (size(d))[2]) + @assert dimX == (size(d))[1] "Array size are incompatible" + @assert dimY == (size(d))[2] "Array size are incompatible" + @assert first1D == 0 "2D data must be given at the end of argument list" + is2D = true + end + + if ndims(d) == 1 + if !is2D + @assert dimX == (size(d))[1] "Array size are incompatible" + else + @assert i <= 2 "When 2D data are given only the first two arrays must be 1D" + + if i == 1 + @assert dimX == (size(d))[1] "Array size are incompatible" + end + if i == 2 + @assert dimY == (size(d))[1] "Array size are incompatible" + end + end + + first1D = i + end end + if is2D + if ndims(data[1]) == 1 + @assert ndims(data[2]) == 1 "Only one coordinate of a 2D dataset has been given" + coordX = deepcopy(data[1]) + coordY = deepcopy(data[2]) + else + coordX = collect(1.:1.:dimX) + coordY = collect(1.:1.:dimY) + end + end + v = "$name << EOD" push!(cur.data, v) - send(v) + gpSend(v, verbosity=3) - origVerb = p_.main.verboseLev - for i in 1:length(data[1]) - v = "" - for j in 1:length(data) - v *= " " * string(data[j][i]) + if !is2D + for i in 1:dimX + v = "" + for j in 1:length(data) + v *= " " * string(data[j][i]) + end + push!(cur.data, v) + gpSend(v, verbosity=4) end - push!(cur.data, v) - - if i>3 && i<=(length(data[1])-3) && p_.main.verboseLev < 4 - p_.log(2, "...", color=p_.main.colorIn) - p_.main.verboseLev = 0 - else - p_.main.verboseLev = origVerb + else + for i in 1:dimX + for j in 1:dimY + v = string(coordX[i]) * " " * string(coordY[j]) + for d in data + ndims(d) == 1 && (continue) + v *= " " * string(d[i,j]) + end + push!(cur.data, v) + gpSend(v, verbosity=4) + end + push!(cur.data, "") + gpSend("", verbosity=4) end - - send(v) end - p_.main.verboseLev = origVerb v = "EOD" push!(cur.data, v) - send(v) + gpSend(v, verbosity=3) - cur.lastDataName = name + cur.lastBlock = name + if is2D + cur.splot = true + end return name end @@ -474,30 +680,29 @@ end #--------------------------------------------------------------------- """ -# Gnuplot.lastData +# gpLastBlock Return the name of the last data block. """ -function lastData() - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] - return cur.lastDataName +function gpLastBlock() + cur = getCurrentOrStartIt() + return cur.lastBlock end #--------------------------------------------------------------------- """ -# Gnuplot.getVal +# gpGetVal Return the value of one (or more) gnuplot variables. ## Example -- argtuple of strings with gnuplot variable +- argtuple of strings with gnuplot variable """ -function getVal(args...) +function gpGetVal(args...) out = Vector{String}() for arg in args - push!(out, string(send("print $arg", capture=true)...)) + push!(out, string(gpSend("print $arg", capture=true)...)) end if length(out) == 1 @@ -510,7 +715,7 @@ end #--------------------------------------------------------------------- """ -# Gnuplot.plot +# gpPlot Add a new plot/splot comand to the current session @@ -518,16 +723,16 @@ Add a new plot/splot comand to the current session ``` x = collect(1.:10) -gp.data(x, x.^2) -gp.plot(last=true, "w l tit 'Pow 2'") +gpData(x, x.^2) +gpPlot(last=true, "w l tit 'Pow 2'") -src = gp.data(x, x.^2.2) -gp.plot("\$src w l tit 'Pow 2.2'") +src = gpData(x, x.^2.2) +gpPlot("\$src w l tit 'Pow 2.2'") # Re use the same data block -gp.plot("\$src u 1:(\\\$2+10) w l tit 'Pow 2.2, offset=10'") +gpPlot("\$src u 1:(\\\$2+10) w l tit 'Pow 2.2, offset=10'") -gp.dump() # Do the plot +gpDump() # Do the plot ``` ## Arguments: @@ -538,34 +743,33 @@ gp.dump() # Do the plot - `file::String`: if given the plot command will be prefixed with `'\$file'`; -- `lastData::Bool`: if true the plot command will be prefixed with the +- `lastBlock::Bool`: if true the plot command will be prefixed with the last inserted data block name; -- `multiID::Int`: ID of the plot the command belongs to (only useful +- `cid::Int`: ID of the plot the command belongs to (only useful for multiplots); """ -@AbbrvKW function plot(spec::String; - lastData::Bool=false, - file::Union{Void,String}=nothing, - multiID::Union{Void,Int}=nothing) +@AbbrvKW function gpPlot(spec::String; + lastBlock::Bool=false, + file::Union{Void,String}=nothing, + cid::Union{Void,Int}=nothing) - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] - mID = multiID == nothing ? cur.multiID : multiID + cur = getCurrentOrStartIt() + mID = (cid == nothing ? cur.cid : cid) src = "" - if lastData - src = cur.lastDataName + if lastBlock + src = cur.lastBlock elseif file != nothing src = "'" * file * "'" end - push!(cur.plot, p_.MultiCmd("$src $spec", mID)) + push!(cur.plot, Command("$src $spec", mID)) return nothing end #--------------------------------------------------------------------- """ -# Gnuplot.multi +# gpMulti Initialize a multiplot (through the "set multiplot" Gnuplot command). @@ -574,17 +778,16 @@ Initialize a multiplot (through the "set multiplot" Gnuplot command). - `multiCmd::String`: multiplot command (see Gnuplot manual) without the leading "set multiplot" string; -## See also: `gp.next`. +## See also: `gpNext`. """ -function multi(multiCmd::String="") - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] - if cur.multiID != 0 - error("Current multiplot ID is $cur.multiID, while it should be 0") +function gpMulti(multiCmd::String="") + cur = getCurrentOrStartIt() + if cur.cid != 0 + error("Current multiplot ID is $(cur.cid), while it should be 0") end - cur.multiID += 1 - cmd("set multiplot $multiCmd") + cur.cid += 1 + gpCmd("set multiplot $multiCmd") # Ensure all plot commands have ID >= 1 for p in cur.plot @@ -597,21 +800,20 @@ end #--------------------------------------------------------------------- """ -# Gnuplot.next +# gpNext Select next slot for multiplot sessions. """ -function next() - p_.getProcOrStartIt() - cur = p_.main.states[p_.main.curPos] - cur.multiID += 1 +function gpNext() + cur = getCurrentOrStartIt() + cur.cid += 1 return nothing end #--------------------------------------------------------------------- """ -# Gnuplot.dump +# gpDump Send all necessary commands to gnuplot to actually do the plot. Optionally, the commands may be sent to a file. In any case the @@ -634,22 +836,17 @@ commands are returned as `Vector{String}`. - `file::String`: filename to redirect all outputs. Implies `all=true, dry=true`. """ -@AbbrvKW function dump(; all::Bool=false, - dry::Bool=false, - cmd::Bool=false, - data::Bool=false, - file::Union{Void,String}=nothing) - - if p_.main.curPos == 0 - return "" - end - +@AbbrvKW function gpDump(; all::Bool=false, + dry::Bool=false, + cmd::Bool=false, + data::Bool=false, + file::Union{Void,String}=nothing) if file != nothing all = true dry = true end - cur = p_.main.states[p_.main.curPos] + cur = getCurrentOrStartIt() out = Vector{String}() all && (push!(out, "reset session")) @@ -660,7 +857,7 @@ commands are returned as `Vector{String}`. end end - for id in 0:cur.multiID + for id in 0:cur.cid for m in cur.cmds if (m.id == id) && ((id > 0) || all) push!(out, m.cmd) @@ -682,7 +879,7 @@ commands are returned as `Vector{String}`. end end - if cur.multiID > 0 + if cur.cid > 0 push!(out, "unset multiplot") end @@ -693,8 +890,8 @@ commands are returned as `Vector{String}`. end if !dry - for s in out; send(s); end - send("", capture=true) + for s in out; gpSend(s); end + gpSend("", capture=true) end return join(out, "\n") @@ -704,35 +901,29 @@ end ###################################################################### # Misc. functions ###################################################################### -terminals() = send("print GPVAL_TERMINALS", capture=true) -terminal() = send("print GPVAL_TERM", capture=true) +gpTerminals() = gpSend("print GPVAL_TERMINALS", capture=true) +gpTerminal() = gpSend("print GPVAL_TERM", capture=true) -###################################################################### -# Exported symbols -###################################################################### - -export @gpi, @gp, @gp_str, @gp_cmd - #--------------------------------------------------------------------- """ -# Gnuplot.@gpi +# @gpi -Similar to `@gp`, but the call to `Gnuplot.reset()` occur only when -the `:reset` symbol is given, and the `Gnuplot.dump()` call occur only +Similar to `@gp`, but the call to `gpReset()` occur only when +the `:reset` symbol is given, and the `gpDump()` call occur only if no arguments are given. See `@gp` documentation for further information. """ macro gpi(args...) if length(args) == 0 - return :(Gnuplot.dump()) + return :(gpDump()) end exprBlock = Expr(:block) exprData = Expr(:call) - push!(exprData.args, :(Gnuplot.data)) + push!(exprData.args, :(gpData)) pendingPlot = false pendingMulti = false @@ -740,48 +931,48 @@ macro gpi(args...) #println(typeof(arg), " ", arg) if isa(arg, Expr) && (arg.head == :quote) && (arg.args[1] == :reset) - push!(exprBlock.args, :(Gnuplot.reset())) + push!(exprBlock.args, :(gpReset())) elseif isa(arg, Expr) && (arg.head == :quote) && (arg.args[1] == :dump) - push!(exprBlock.args, :(Gnuplot.dump())) + push!(exprBlock.args, :(gpDump())) elseif isa(arg, Expr) && (arg.head == :quote) && (arg.args[1] == :plot) pendingPlot = true elseif isa(arg, Expr) && (arg.head == :quote) && (arg.args[1] == :multi) pendingMulti = true elseif isa(arg, Expr) && (arg.head == :quote) && (arg.args[1] == :next) - push!(exprBlock.args, :(Gnuplot.next())) + push!(exprBlock.args, :(gpNext())) elseif (isa(arg, Expr) && (arg.head == :string)) || isa(arg, String) # Either a plot or cmd string if pendingPlot if length(exprData.args) > 1 push!(exprBlock.args, exprData) exprData = Expr(:call) - push!(exprData.args, :(Gnuplot.data)) + push!(exprData.args, :(gpData)) end - push!(exprBlock.args, :(Gnuplot.plot(last=true, $arg))) + push!(exprBlock.args, :(gpPlot(last=true, $arg))) pendingPlot = false elseif pendingMulti - push!(exprBlock.args, :(Gnuplot.multi($arg))) + push!(exprBlock.args, :(gpMulti($arg))) pendingMulti = false else - push!(exprBlock.args, :(Gnuplot.cmd($arg))) + push!(exprBlock.args, :(gpCmd($arg))) end elseif (isa(arg, Expr) && (arg.head == :(=))) # A cmd keyword sym = arg.args[1] val = arg.args[2] - push!(exprBlock.args, :(Gnuplot.cmd($sym=$val))) + push!(exprBlock.args, :(gpCmd($sym=$val))) else # A data set push!(exprData.args, arg) pendingPlot = true end end - #dump(exprBlock) + #gpDump(exprBlock) if pendingPlot && length(exprData.args) >= 2 push!(exprBlock.args, exprData) - push!(exprBlock.args, :(Gnuplot.plot(last=true, ""))) + push!(exprBlock.args, :(gpPlot(last=true, ""))) end return esc(exprBlock) @@ -790,23 +981,23 @@ end #--------------------------------------------------------------------- """ -# Gnuplot.@gp +# @gp The `@gp` (and its companion `gpi`) allows to exploit almost all -**Gnuplot.jl** package functionalities using an extremely efficient +**jl** package functionalities using an extremely efficient and concise syntax. In the vast majority of cases you can use a -single call to `@gp` instead of many calls to `gp.cmd`, `gp.data`, -`gp.plot`, etc... to produce (even very complex) plots. +single call to `@gp` instead of many calls to `gpCmd`, `gpData`, +`gpPlot`, etc... to produce (even very complex) plots. The syntax is as follows: ``` -@gp( ["a command"], # passed to gp.cmd() as a command string - [Symbol=(Value | Expr)] # passed to gp.cmd() as a keyword - [(one or more Expression | Array) "plot spec"], # passed to gp.data() and - # gp.plot(last=true) respectively - [:plot "plot spec"], # passed to gp.plot() - [:multi "multi spec"], # passed to gp.multi() - [:next] # calls gp.next() +@gp( ["a command"], # passed to gpCmd() as a command string + [Symbol=(Value | Expr)] # passed to gpCmd() as a keyword + [(one or more Expression | Array) "plot spec"], # passed to gpData() and + # gpPlot(last=true) respectively + [:plot "plot spec"], # passed to gpPlot() + [:multi "multi spec"], # passed to gpMulti() + [:next] # calls gpNext() etc... ) ``` @@ -823,18 +1014,18 @@ A simple example will clarify the usage: This call epands as follows: ``` -Gnuplot.reset() -begin - Gnuplot.cmd("set key left") - Gnuplot.cmd(title="My title") - Gnuplot.cmd(xr=(1, 12)) - Gnuplot.data(1:10) - Gnuplot.plot(last=true, "with lines tit 'Data'") +gpReset() +begin + gpCmd("set key left") + gpCmd(title="My title") + gpCmd(xr=(1, 12)) + gpData(1:10) + gpPlot(last=true, "with lines tit 'Data'") end -Gnuplot.dump() +gpDump() ``` -A closely related macro is `@gpi` which do not adds the `Gnuplot.reset()` -and `Gnuplot.dump()` calls. +A closely related macro is `@gpi` which do not adds the `gpReset()` +and `gpDump()` calls. ## Examples: ``` @@ -879,17 +1070,17 @@ macro gp(args...) e = :(@gpi($(esc_args...))) f = Expr(:block) - push!(f.args, esc(:( Gnuplot.reset()))) + push!(f.args, esc(:( gpReset()))) push!(f.args, e) - push!(f.args, esc(:( Gnuplot.dump()))) + push!(f.args, esc(:( gpDump()))) return f end """ -# Gnuplot.@gp_str +# @gp_str -Call `gp.send` with a non-standard string literal. +Call `gpSend` with a non-standard string literal. NOTE: this is supposed to be used interactively on the REPL, not in functions. @@ -918,13 +1109,13 @@ splot '\$grid' matrix with lines notitle ``` """ macro gp_str(s::String) - return Gnuplot.send(s, capture=true) + return gpSend(s, capture=true) end #--------------------------------------------------------------------- """ -# Gnuplot.@gp_cmd +# @gp_cmd Call the gnuplot "load" command passing the filename given as non-standard string literal. @@ -935,13 +1126,13 @@ functions. Example: ``` @gp (1:10).^3 "w l notit lw 4" -gp.dump(file="test.gp") -gp.exitAll() +gpDump(file="test.gp") +gpExitAll() gp`test.gp` ``` """ macro gp_cmd(file::String) - return Gnuplot.send("load '$file'", capture=true) + return gpSend("load '$file'", capture=true) end diff --git a/src/GnuplotInternals.jl b/src/GnuplotInternals.jl deleted file mode 100644 index 18dcab1..0000000 --- a/src/GnuplotInternals.jl +++ /dev/null @@ -1,247 +0,0 @@ -isdefined(Base, :__precompile__) && __precompile__() - -###################################################################### -# MODULE GnuplotInternals (private functions and definitions) -###################################################################### -module p_ - -importall Gnuplot -const P_ = Gnuplot - - -###################################################################### -# Structure definitions -###################################################################### - -""" -Structure containing the `Pipe` and `Process` objects associated to a -Gnuplot process. -""" -mutable struct GnuplotProc - pin::Base.Pipe - pout::Base.Pipe - perr::Base.Pipe - proc::Base.Process - channel::Channel{String} - -""" -Start a new gnuplot process using the command given in the `cmd` argument. -""" - function GnuplotProc(cmd::String) - this = new() - this.pin = Base.Pipe() - this.pout = Base.Pipe() - this.perr = Base.Pipe() - this.channel = Channel{String}(32) - - # Start gnuplot process - this.proc = spawn(`$cmd`, (this.pin, this.pout, this.perr)) - - # Close unused sides of the pipes - Base.close_pipe_sync(this.pout.in) - Base.close_pipe_sync(this.perr.in) - Base.close_pipe_sync(this.pin.out) - Base.start_reading(this.pout.out) - Base.start_reading(this.perr.out) - - return this - end -end - - -#--------------------------------------------------------------------- -""" -Structure containing a single plot command and the associated -multiplot index. -""" -mutable struct MultiCmd - cmd::String # command - id::Int # multiplot index -end - -""" -Structure containing the state of a single gnuplot session. -""" -mutable struct GnuplotSession - blockCnt::Int # data blocks counter - cmds::Vector{MultiCmd} # gnuplot commands - data::Vector{String} # data blocks - plot::Vector{MultiCmd} # plot specifications associated to each data block - splot::Bool # plot / splot session - lastDataName::String # name of the last data block - multiID::Int # current multiplot index (0 if no multiplot) - - GnuplotSession() = new(1, Vector{MultiCmd}(), Vector{String}(), - Vector{MultiCmd}(), false, "", 0) -end - - -#--------------------------------------------------------------------- -""" -Structure containing the global package state. -""" -mutable struct MainState - colorOut::Symbol # gnuplot STDOUT is printed with this color - colorIn::Symbol # gnuplot STDIN is printed with this color - verboseLev::Int # verbosity level (0 - 3), default: 3 - gnuplotCmd::String # command used to start the gnuplot process - startup::String # commands automatically sent to each new gnuplot process - procs::Vector{GnuplotProc} # array of currently active gnuplot process and pipes - states::Vector{GnuplotSession} # array of gnuplot sessions - handles::Vector{Int} # handles of gnuplot sessions - curPos::Int # index in the procs, states and handles array of current session - - MainState() = new(:cyan, :yellow, 0, - "", "", Vector{GnuplotProc}(), Vector{GnuplotSession}(), - Vector{Int}(), 0) -end - - -###################################################################### -# Functions -###################################################################### - -""" -Check gnuplot is runnable with the command given in `main.gnuplotCmd`. -Also check that gnuplot version is >= 4.7 (required to use data -blocks). -""" -function checkGnuplotVersion() - cmd = `$(main.gnuplotCmd) --version` - out, procs = open(`$cmd`, "r") - s = String(read(out)) - if !success(procs) - error("An error occurred while running: " * string(cmd)) - end - - s = split(s, " ") - ver = "" - for token in s - try - ver = VersionNumber("$token") - break - catch - end - end - - if ver < v"4.7" - # Do not raise error in order to pass Travis CI test, since it has v4.6 - println("gnuplot ver. >= 4.7 is required, but " * string(ver) * " was found.") - end - if ver < v"4.6" - error("gnuplot ver. >= 4.7 is required, but " * string(ver) * " was found.") - end - log(1, "Found gnuplot version: " * string(ver)) - return ver -end - - -#--------------------------------------------------------------------- -""" -Logging facility (each line is prefixed with the session handle.) - -Printing occur only if the logging level is >= current verbosity -level. -""" -function log(level::Int, s::String; id=nothing, color=nothing) - if (main.verboseLev < level) - return - end - - color == nothing && (color = main.colorOut) - - prefix = "" - if (id == nothing) && (main.curPos > 0) - id = main.handles[main.curPos] - end - prefix = string("GP(", id, ")") - - a = split(s, "\n") - for v in a - print_with_color(color, "$prefix $v\n") - end - return nothing -end - - -#--------------------------------------------------------------------- -""" -Read gnuplot outputs, and optionally redirect to a `Channel`. - -This fuction is supposed to be run in a `Task`. -""" -function readTask(sIN, channel; kw...) - saveOutput::Bool = false - while isopen(sIN) - line = convert(String, readline(sIN)) - - if line == "GNUPLOT_JL_SAVE_OUTPUT" - saveOutput = true - log(4, "|begin of captured data =========================") - else - if saveOutput - put!(channel, line) - end - - if line == "GNUPLOT_JL_SAVE_OUTPUT_END" - saveOutput = false - log(4, "|end of captured data ===========================") - elseif line != "" - if saveOutput - log(3, "| " * line; kw...) - else - log(2, " " * line; kw...) - end - end - end - end - - log(1, "pipe closed"; kw...) - return nothing -end - - -#--------------------------------------------------------------------- -""" -Return a unique data block name -""" -function mkBlockName(;prefix::Union{Void,String}=nothing) - if prefix == nothing - prefix = string("d", main.handles[main.curPos]) - end - - cur = main.states[main.curPos] - name = string(prefix, "_", cur.blockCnt) - cur.blockCnt += 1 - - return name -end - - -#--------------------------------------------------------------------- -""" -Return the GnuplotProc structure of current session, or start a new -gnuplot process if none is running. -""" -function getProcOrStartIt() - if main.curPos == 0 - log(1, "Starting a new gnuplot process...") - id = P_.session() - end - - p = main.procs[main.curPos] - - if !Base.process_running(p.proc) - error("The current gnuplot process is no longer running.") - end - - return p -end - - -###################################################################### -# Module initialization -###################################################################### -const main = MainState() - -end #module diff --git a/test/runtests.jl b/test/runtests.jl index e3fae8f..c83ad15 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,5 @@ using Base.Test using Gnuplot -const gp = Gnuplot function pressEnter() println("Press enter...") @@ -8,72 +7,72 @@ function pressEnter() end function gp_test(terminal="unknown") - gp.setOption(startup="set term $terminal") + gpOptions.startup = "set term $terminal" - gp.reset() + gpReset() x = collect(1.:100) #----------------------------------------------------------------- - gp.send("plot sin(x)") + gpSend("plot sin(x)") terminal == "unknown" || pressEnter() #----------------------------------------------------------------- - id1 = gp.current() - id2 = gp.session() - id3 = gp.session() + id1 = gpCurrentID() + id2 = gpNewSession() + id3 = gpNewSession() for i in 1:10 - gp.setCurrent(id1) - gp.send("plot sin($i*x)") + gpSetCurrentID(id1) + gpSend("plot sin($i*x)") - gp.setCurrent(id2) - gp.send("plot sin($i*x)") + gpSetCurrentID(id2) + gpSend("plot sin($i*x)") - gp.setCurrent(id3) - gp.send("plot sin($i*x)") + gpSetCurrentID(id3) + gpSend("plot sin($i*x)") sleep(0.3) end terminal == "unknown" || pressEnter() - gp.exitAll() + gpExitAll() #----------------------------------------------------------------- - gp.reset() - name = gp.data([1,2,3,5,8,13]) - gp.plot("$name w points ps 3") - gp.dump() + gpReset() + name = gpData([1,2,3,5,8,13]) + gpPlot("$name w points ps 3") + gpDump() terminal == "unknown" || pressEnter() - gp.plot(last=true, "w l lw 3") - gp.dump() + gpPlot(last=true, "w l lw 3") + gpDump() terminal == "unknown" || pressEnter() #----------------------------------------------------------------- - gp.reset() + gpReset() - gp.cmd("set format y \"%.1f\"") - gp.cmd("set key box opaque") - gp.cmd("set xrange [-2*pi:2*pi]") - gp.multi("layout 2,2 columnsfirst title \"Multiplot title\"") + gpCmd("set format y \"%.1f\"") + gpCmd("set key box opaque") + gpCmd("set xrange [-2*pi:2*pi]") + gpMulti("layout 2,2 columnsfirst title \"Multiplot title\"") - gp.cmd(ylab="Y label") - gp.plot("sin(x) lt 1") + gpCmd(ylab="Y label") + gpPlot("sin(x) lt 1") - gp.next() - gp.cmd(xlab="X label") - gp.plot("cos(x) lt 2") + gpNext() + gpCmd(xlab="X label") + gpPlot("cos(x) lt 2") - gp.next() - gp.cmd("unset ylabel") - gp.cmd("unset ytics") - gp.cmd("unset xlabel") - gp.plot("sin(2*x) lt 3") + gpNext() + gpCmd("unset ylabel") + gpCmd("unset ytics") + gpCmd("unset xlabel") + gpPlot("sin(2*x) lt 3") - gp.next() - gp.cmd(xlab="X label") - gp.plot("cos(2*x) lt 4") + gpNext() + gpCmd(xlab="X label") + gpPlot("cos(2*x) lt 4") - gp.dump() + gpDump() terminal == "unknown" || pressEnter() #----------------------------------------------------------------- @@ -184,7 +183,7 @@ function gp_test(terminal="unknown") :plot, "x8, v, (u<0.5) ? -1 : sinc(x8,v) notitle", :plot, "x9, v, (u<0.5) ? -1 : sinc(x9,v) notitle") - gp.exitAll() + gpExitAll() return true end