Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance savefig() #494

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 34 additions & 48 deletions docs/src/manipulating_plots.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,70 +138,56 @@ More examples are being worked on at this time (2021-07-14), but for now you can

## Saving figures


Figures can be saved in a variety of formats using the `savefig` function.
Figures can be saved in a variety of formats using the [`savefig`](@ref) function.

!!! note
Note that the docs below are shown for the `PlotlyBase.Plot` type, but are also defined for `PlotlyJS.SyncPlot`. Thus, you can use these methods after calling either `plot` or `Plot`.

This function has a few methods:
Note that the docs below are shown for the `PlotlyBase.Plot` type, but are also defined for `PlotlyJS.SyncPlot`.
Thus, you can use these methods after calling either `plot` or `Plot`.

**1**
The `savefig` function can be called in a few ways:

```@docs
savefig(::Union{PlotlyBase.Plot}, ::String)
```

When using this method the format of the file is inferred based on the extension
of the second argument. The examples below show the possible export formats:

```julia
savefig(p::Union{Plot,SyncPlot}, "output_filename.pdf")
savefig(p::Union{Plot,SyncPlot}, "output_filename.html")
savefig(p::Union{Plot,SyncPlot}, "output_filename.json")
savefig(p::Union{Plot,SyncPlot}, "output_filename.png")
savefig(p::Union{Plot,SyncPlot}, "output_filename.svg")
savefig(p::Union{Plot,SyncPlot}, "output_filename.jpeg")
savefig(p::Union{Plot,SyncPlot}, "output_filename.webp")
```
**Save into a file**

**2**
`savefig(p, filename)` saves the plot `p` into `filename`.
Unless explicitly specified, the format of the file is inferred from the `filename` extension.
The examples demonstrate the supported formats:

```julia
savefig(
io::IO,
p::Plot;
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::String="png"
)
savefig(p, "output_filename.pdf")
savefig(p, "output_filename.html")
savefig(p, "output_filename.json")
savefig(p, "output_filename.png")
savefig(p, "output_filename.svg")
savefig(p, "output_filename.jpeg")
savefig(p, "output_filename.webp")
```

This method allows you to save a plot directly to an open IO stream.
**Save into a stream**

See the [`savefig(::IO, ::PlotlyBase.Plot)`](@ref) API docs for more information.
`savefig(io, p)` saves the plot `p` into the open `io` stream.
The figure format could be specified with the `format` keyword, the default format is *PNG*.

**3**
**Display on the screen**

*PlotlyJS.jl* overloads the `Base.show` method to hook into Julia's rich display system:
```julia
Base.show(::IO, ::MIME, ::Union{PlotlyBase.Plot})
Base.show(io::IO, ::MIME, p::Union{PlotlyBase.Plot})
```
Internally, this `Base.show` implementation calls `savefig(io, p)`,
and the `MIME` argument allows to specify the output format.

The following MIME formats are supported:
* `::MIME"application/pdf`
* `::MIME"image/png`
* `::MIME"image/svg+xml`
* `::MIME"image/eps`
* `::MIME"image/jpeg`
* `::MIME"application/json"`
* `::MIME"application/json; charset=UTF-8"`

This method hooks into Julia's rich display system.

Possible arguments for the second argument are shown in the examples below:

```julia
savefig(io::IO, ::MIME"application/pdf", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"image/png", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"image/svg+xml", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"image/eps", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"image/jpeg", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"application/json", p::Union{Plot,SyncPlot})
savefig(io::IO, ::MIME"application/json; charset=UTF-8", p::Union{Plot,SyncPlot})
```@docs
savefig
```

!!! note
You can also save the json for a figure by calling
`savejson(p::Union{Plot,SyncPlot}, filename::String)`.
12 changes: 5 additions & 7 deletions src/display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ function Base.show(io::IO, mm::MIME"text/html", p::SyncPlot)
end
Base.show(io::IO, mm::MIME"application/prs.juno.plotpane+html", p::SyncPlot) = show(io, mm, p.scope)

# using @eval instead of Union{} to avoid ambiguity with other methods
for mime in [MIME"text/plain", MIME"application/vnd.plotly.v1+json", MIME"application/prs.juno.plotpane+html"]
@eval Base.show(io::IO, mm::$mime, p::SyncPlot, args...; kwargs...) = show(io, mm, p.plot, args...; kwargs...)
end

function SyncPlot(
p::Plot;
kwargs...
Expand Down Expand Up @@ -343,11 +348,4 @@ for f in (:extendtraces!, :prependtraces!)
end
end


for mime in ["text/plain", "application/vnd.plotly.v1+json", "application/prs.juno.plotpane+html"]
function Base.show(io::IO, m::MIME{Symbol(mime)}, p::SyncPlot, args...)
show(io, m, p.plot, args...)
end
end

PlotlyBase.savejson(sp::SyncPlot, fn::String) = PlotlyBase.savejson(sp.plot, fn)
150 changes: 72 additions & 78 deletions src/kaleido.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
using PlotlyKaleido: kill, is_running, start, restart, ALL_FORMATS, TEXT_FORMATS

savefig(p::SyncPlot; kwargs...) = savefig(p.plot; kwargs...)
"""
savefig(
[io::IO], p::Plot, [fn:AbstractString];
width::Union{Nothing,Integer}=700,
height::Union{Nothing,Integer}=500,
scale::Union{Nothing,Real}=nothing,
format::String="png",
plotlyjs::Union{AbstractString, Nothing}=nothing,
plotly_version::Union{AbstractString, Nothing}=nothing
)

Save a plot `p` to the IO stream `io` or to the file named `fn`.

If both `io` and `fn` are not specified, returns the image as a vector of bytes.

## Keyword Arguments

- `format`: the image format, must be one of $(join(string.("*", ALL_FORMATS, "*"), ", ")), or *html*.
- `scale`: the image scale.
- `width` and `height`: the image dimensions, in pixels.
- `plotly_version`, `plotly_js`: the version of *Plotly* JavaScript library to use for rendering.
These arguments are mutually exclusive.
Defaults to using the Plotly library bundled with *PlotlyJS.jl*.
"""
function savefig(
p::Plot;
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
width::Union{Nothing,Integer}=700,
height::Union{Nothing,Integer}=500,
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
format::String="png",
plotlyjs::Union{AbstractString, Nothing}=nothing,
plotly_version::Union{AbstractString, Nothing}=nothing
)
in(format, ALL_FORMATS) ||
throw(ArgumentError("Unknown format: $format. Expected one of: $(join(ALL_FORMATS, ", "))"))

# 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
)
isnothing(width) || (payload[:width] = width)
isnothing(height) || (payload[:height] = height)
isnothing(scale) || (payload[:scale] = scale)

_ensure_kaleido_running()
_ensure_kaleido_running(; plotlyjs, plotly_version)
P = PlotlyKaleido.P
# convert payload to vector of bytes
bytes = transcode(UInt8, JSON.json(payload))
Expand Down Expand Up @@ -53,75 +75,55 @@ function savefig(
end
end

savefig(p::SyncPlot; kwargs...) = savefig(p.plot; kwargs...)

@inline _get_Plot(p::Plot) = p
@inline _get_Plot(p::SyncPlot) = p.plot

"""
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::Union{SyncPlot,Plot};
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::String="png")
function savefig(io::IO, p::Union{SyncPlot,Plot};
format::AbstractString="png",
kwargs...)
if format == "html"
return show(io, MIME("text/html"), _get_Plot(p), include_mathjax="cdn", include_plotlyjs="cdn", full_html=true)
end

bytes = savefig(p, width=width, height=height, scale=scale, format=format)
bytes = savefig(p; format, kwargs...)
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::Union{SyncPlot,Plot}, fn::AbstractString;
format::Union{Nothing,String}=nothing,
width::Union{Nothing,Int}=nothing,
height::Union{Nothing,Int}=nothing,
scale::Union{Nothing,Real}=nothing,
format::Union{Nothing,AbstractString}=nothing,
kwargs...
)
ext = split(fn, ".")[end]
if format === nothing
format = String(ext)
end
format = something(format, String(ext))

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

_ensure_kaleido_running(; kwargs...) = !is_running() && restart(; plotlyjs=_js_path, kwargs...)
# If kaleido is not running, starts it using the specified plotly library.
# The plotly library is specified either as the `plotlyjs` path to the javascript library,
# or as a `plotly_version` (in the latter case the library is taken from `https://cdn.plot.ly/`).
# If none are specified, the plotly library from the `Artifacts.toml` is used.
function _ensure_kaleido_running(;
plotlyjs::Union{AbstractString, Nothing} = nothing,
plotly_version::Union{AbstractString, Nothing} = nothing,
kwargs...
)
if !is_running()
!isnothing(plotly_version) && !isnothing(plotlyjs) &&
throw(ArgumentError("Cannot specify both `plotly_version` and `plotlyjs`"))
if !isnothing(plotly_version)
restart(; plotly_version, kwargs...)
else
restart(; plotlyjs=something(plotlyjs, _js_path), kwargs...)
end
end
end

const _KALEIDO_MIMES = Dict(
"application/pdf" => "pdf",
Expand All @@ -135,21 +137,13 @@ const _KALEIDO_MIMES = Dict(
)

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
@eval Base.show(
io::IO, ::MIME{Symbol($mime)}, plt::Plot;
kwargs...
) = savefig(io, plt; format=$fmt, kwargs...)

@eval Base.show(
io::IO, ::MIME{Symbol($mime)}, plt::SyncPlot;
kwargs...
) = savefig(io, plt.plot; format=$fmt, kwargs...)
end
Loading