Skip to content

Commit

Permalink
Merge pull request #396 from JuliaPlots/kaleido
Browse files Browse the repository at this point in the history
migrate kaleido support from PlotlyBase over to PlotlyJS
  • Loading branch information
sglyon authored Jul 24, 2021
2 parents 64f9083 + 1872f24 commit 39e57f1
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 14 deletions.
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ authors = ["Spencer Lyon <[email protected]>"]
version = "0.18.0"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Blink = "ad839575-38b3-5650-b840-f874b8c74a25"
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
JSExpr = "97c1335a-c9c5-57fe-bc5d-ec35cebe8660"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Kaleido_jll = "f7e6163d-2fa5-5f23-b69c-1db539e41963"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
Expand All @@ -20,7 +22,7 @@ WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29"
Blink = "0.12"
JSExpr = "0.5"
JSON = "0.20, 0.21"
PlotlyBase = "0.6, 0.7"
PlotlyBase = "0.6, ^0.7"
Reexport = "0.2, 1"
Requires = "1.0"
WebIO = "0.8"
Expand Down
17 changes: 4 additions & 13 deletions src/PlotlyJS.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module PlotlyJS

using Base64
using Reexport
@reexport using PlotlyBase
using JSON
Expand Down Expand Up @@ -34,6 +35,7 @@ struct PlotlyJSDisplay <: AbstractDisplay end
# include the rest of the core parts of the package
include("display.jl")
include("util.jl")
include("kaleido.jl")

make_subplots(;kwargs...) = plot(Layout(Subplots(;kwargs...)))

Expand All @@ -50,19 +52,6 @@ function docs()
Blink.content!(w, "html", open(f -> read(f, String), schema_path), fade=false, async=false)
end

PlotlyBase.savefig(p::SyncPlot, a...; k...) = savefig(p.plot, a...; k...)
PlotlyBase.savefig(io::IO, p::SyncPlot, a...; k...) = savefig(io, p.plot, a...; k...)

for (mime, fmt) in PlotlyBase._KALEIDO_MIMES
@eval function Base.show(
io::IO, ::MIME{Symbol($mime)}, plt::SyncPlot,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
)
savefig(io, plt.plot, format=$fmt)
end
end

@enum RENDERERS BLINK IJULIA BROWSER DOCS

Expand Down Expand Up @@ -106,6 +95,8 @@ function __init__()
@warn("Warnings were generated during the last build of PlotlyJS: please check the build log at $_build_log")
end

@async _start_kaleido_process()

if !isfile(_js_path)
@info("plotly.js javascript libary not found -- downloading now")
include(joinpath(_pkg_root, "deps", "build.jl"))
Expand Down
219 changes: 219 additions & 0 deletions src/kaleido.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
using Kaleido_jll

mutable struct Pipes
stdin::Pipe
stdout::Pipe
stderr::Pipe
proc::Base.Process
Pipes() = new()
end

const P = Pipes()

const ALL_FORMATS = ["png", "jpeg", "webp", "svg", "pdf", "eps", "json"]
const TEXT_FORMATS = ["svg", "json", "eps"]

function _restart_kaleido_process()
if isdefined(P, :proc) && process_running(P.proc)
kill(P.proc)
end
_start_kaleido_process()
end


function _start_kaleido_process()
global P
try
BIN = let
art = Kaleido_jll.artifact_dir
cmd = if Sys.islinux() || Sys.isapple()
joinpath(art, "kaleido")
else
# Windows
joinpath(art, "kaleido.cmd")
end
no_sandbox = "--no-sandbox"
Sys.isapple() ? `$(cmd) plotly --disable-gpu --single-process` : `$(cmd) plotly --disable-gpu $(no_sandbox)`
end
kstdin = Pipe()
kstdout = Pipe()
kstderr = Pipe()
kproc = run(pipeline(BIN,
stdin=kstdin, stdout=kstdout, stderr=kstderr),
wait=false)
process_running(kproc) || error("There was a problem startink up kaleido.")
close(kstdout.in)
close(kstderr.in)
close(kstdin.out)
Base.start_reading(kstderr.out)
P.stdin = kstdin
P.stdout = kstdout
P.stderr = kstderr
P.proc = kproc

# read startup message and check for errors
res = readline(P.stdout)
if length(res) == 0
error("Could not start Kaleido process")
end

js = JSON.parse(res)
if get(js, "code", 0) != 0
error("Could not start Kaleido process")
end
catch e
@warn "Kaleido is not available on this system. Julia will be unable to save images of any plots."
@warn "$e"
end
nothing
end

function savefig(
p::Plot;
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::String="png"
)::Vector{UInt8}
if !(format in ALL_FORMATS)
error("Unknown format $format. Expected one of $ALL_FORMATS")
end

# construct payload
_get(x, def) = x === nothing ? def : x
payload = Dict(
:width => _get(width, 700),
:height => _get(height, 500),
:scale => _get(scale, 1),
:format => format,
:data => p
)

_ensure_kaleido_running()

# convert payload to vector of bytes
bytes = transcode(UInt8, JSON.json(payload))
write(P.stdin, bytes)
write(P.stdin, transcode(UInt8, "\n"))
flush(P.stdin)

# read stdout and parse to json
res = readline(P.stdout)
js = JSON.parse(res)

# check error code
code = get(js, "code", 0)
if code != 0
msg = get(js, "message", nothing)
error("Transform failed with error code $code: $msg")
end

# get raw image
img = String(js["result"])

# base64 decode if needed, otherwise transcode to vector of byte
if format in TEXT_FORMATS
return transcode(UInt8, img)
else
return base64decode(img)
end
end

"""
savefig(
io::IO,
p::Plot;
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::String="png"
)
Save a plot `p` to the io stream `io`. They keyword argument `format`
determines the type of data written to the figure and must be one of
$(join(ALL_FORMATS, ", ")), or html. `scale` sets the
image scale. `width` and `height` set the dimensions, in pixels. Defaults
are taken from `p.layout`, or supplied by plotly
"""
function savefig(io::IO,
p::Plot;
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::String="png")


if format == "html"
return show(io, MIME("text/html"), p, include_mathjax="cdn", include_plotlyjs="cdn", full_html=true)
end

bytes = savefig(p, width=width, height=height, scale=scale, format=format)
write(io, bytes)
end

"""
savefig(
p::Plot, fn::AbstractString;
format::Union{Nothing,String}=nothing,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
)
Save a plot `p` to a file named `fn`. If `format` is given and is one of
$(join(ALL_FORMATS, ", ")), or html; it will be the format of the file. By
default the format is guessed from the extension of `fn`. `scale` sets the
image scale. `width` and `height` set the dimensions, in pixels. Defaults
are taken from `p.layout`, or supplied by plotly
"""
function savefig(
p::Plot, fn::AbstractString;
format::Union{Nothing,String}=nothing,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
)
ext = split(fn, ".")[end]
if format === nothing
format = String(ext)
end

open(fn, "w") do f
savefig(f, p; format=format, scale=scale, width=width, height=height)
end
return fn
end

_kaleido_running() = isdefined(P, :stdin) && isopen(P.stdin) && process_running(P.proc)
_ensure_kaleido_running() = !_kaleido_running() && _restart_kaleido_process()

const _KALEIDO_MIMES = Dict(
"application/pdf" => "pdf",
"image/png" => "png",
"image/svg+xml" => "svg",
"image/eps" => "eps",
"image/jpeg" => "jpeg",
"image/jpeg" => "jpeg",
"application/json" => "json",
"application/json; charset=UTF-8" => "json",
)

for (mime, fmt) in _KALEIDO_MIMES
@eval function Base.show(
io::IO, ::MIME{Symbol($mime)}, plt::Plot,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
)
savefig(io, plt, format=$fmt)
end

@eval function Base.show(
io::IO, ::MIME{Symbol($mime)}, plt::SyncPlot,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
)
savefig(io, plt.plot, format=$fmt)
end
end
17 changes: 17 additions & 0 deletions test/kaleido.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function myplot(fn)
x = 0:0.1:2π
plt = Plot(scatter(x=x, y=sin.(x)))
savefig(plt, fn)
end

@testset "kaleido" begin
for ext in [PlotlyBase.ALL_FORMATS..., "html"]
if ext === "eps"
continue
end
@show fn = tempname() * "." * ext
myplot(fn) == fn
@test isfile(fn)
rm(fn)
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ using Blink
!Blink.AtomShell.isinstalled() && Blink.AtomShell.install()

include("blink.jl")
include("kaleido.jl")

end

0 comments on commit 39e57f1

Please sign in to comment.