Skip to content

Commit ed2bc3c

Browse files
committed
rewrite kinda a lot
1 parent 4cdaf06 commit ed2bc3c

File tree

6 files changed

+192
-71
lines changed

6 files changed

+192
-71
lines changed

Artifacts.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[plotly_artifacts]
2-
git-tree-sha1 = "d95fb1315e47ef8ed0e43ddb04ab5d6328473637"
2+
git-tree-sha1 = "aa67a2d0c294419c5568a236d0fdf8de18af2bd6"
33

44
[[plotly_artifacts.download]]
5-
sha256 = "4f8fcc209707eb3c5c05de1b427d681e5d9220d467e7481c0011f51c14b493af"
6-
url = "https://gist.github.com/joshday/2e3eab14b37ae13c0ed5b9bc9cd1b29c/raw/d95fb1315e47ef8ed0e43ddb04ab5d6328473637.tar.gz"
5+
sha256 = "9e8f6feca5d99705364e99f931e7b050965b23aed7599c3011a1408c2be75bda"
6+
url = "https://gist.github.com/joshday/9c5faed42fa19016ce0e5c5677c9ac4e/raw/aa67a2d0c294419c5568a236d0fdf8de18af2bd6.tar.gz"

Project.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
name = "PlotlyLight"
22
uuid = "ca7969ec-10b3-423e-8d99-40f33abb42bf"
33
authors = ["joshday <[email protected]>"]
4-
version = "0.10.0"
4+
version = "0.11.0"
55

66
[deps]
77
Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
88
Cobweb = "ec354790-cf28-43e8-bb59-b484409b7bad"
99
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
1010
EasyConfig = "acab07b0-f158-46d4-8913-50acef6d41fe"
1111
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
12-
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
1312
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
14-
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
1513

1614
[compat]
1715
Aqua = "0.8"
@@ -20,7 +18,6 @@ Cobweb = "0.6, 0.7"
2018
Downloads = "1.6"
2119
EasyConfig = "0.1"
2220
JSON3 = "1.14"
23-
StructTypes = "1.10"
2421
julia = "1.7"
2522

2623
[extras]

src/PlotlyLight.jl

Lines changed: 105 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,29 @@ module PlotlyLight
33
using Artifacts: @artifact_str
44
using Downloads: download
55
using Random: randstring
6-
using REPL: REPL
76

87
using JSON3: JSON3
98
using EasyConfig: Config
10-
using StructTypes: StructTypes
119
using Cobweb: Cobweb, h, IFrame, Node
1210

1311
#-----------------------------------------------------------------------------# exports
14-
export Plot, Config, preset, plot
12+
export Config, preset, Plot, plot
13+
14+
#-----------------------------------------------------------------------------# __init__
15+
function __init__()
16+
# Hack since extensions with REPL are wonky
17+
for M in Base.loaded_modules_order
18+
if Symbol(M) == :REPL
19+
@eval Base.display(::$M.REPLDisplay, o::Plot) = Cobweb.preview(html_page(o))
20+
end
21+
end
22+
end
23+
24+
include("json.jl")
1525

16-
#-----------------------------------------------------------------------------# PlotlyArtifacts
1726
artifact(x...) = joinpath(artifact"plotly_artifacts", x...)
1827

28+
#-----------------------------------------------------------------------------# plotly::PlotlyArtifacts
1929
Base.@kwdef struct PlotlyArtifacts
2030
version::VersionNumber = VersionNumber(read(artifact("version.txt"), String))
2131
url::String = "https://cdn.plot.ly/plotly-$version.min.js"
@@ -28,22 +38,36 @@ plotly::PlotlyArtifacts = PlotlyArtifacts()
2838

2939
#-----------------------------------------------------------------------------# Settings
3040
Base.@kwdef mutable struct Settings
31-
src::Node = h.script(src="https://cdn.plot.ly/plotly-$(plotly.version).min.js", charset="utf-8")
32-
div::Node = h.div(; style="height:100vh;width:100vw;")
41+
src::Node = h.script(src=plotly.url, charset="utf-8")
42+
div::Node = h.div(; class="plotlylight-plot-div")
3343
layout::Config = Config()
34-
config::Config = Config(responsive=true)
44+
config::Config = Config(responsive=true, displaylogo=false)
3545
reuse_preview::Bool = true
36-
style::Dict{String,String} = Dict("display" => "block", "border" => "none", "min-height" => "350px", "min-width" => "350px", "width" => "100%", "height" => "100%")
37-
inject_head::Union{Nothing, Node} = nothing
46+
page_css::Cobweb.Node = h.style("html, body { padding: 0px; margin: 0px; }")
47+
iframe_style = "display:block; border:none; min-height:350px; min-width:350px; width:100%; height:100%"
48+
src_inject::Vector = []
3849
end
3950
settings::Settings = Settings()
4051

41-
#-----------------------------------------------------------------------------# utils/other
42-
# Hack to change behavior of `JSON3.write` for `AbstractMatrix`
43-
_fix(x::Config) = Config(k => _fix(v) for (k,v) in pairs(x))
44-
_fix(x) = x
45-
_fix(x::AbstractMatrix) = eachrow(x)
52+
function Settings(s::Settings; kw...)
53+
s2 = deepcopy(s)
54+
for (k, v) in kw
55+
setfield!(s2, k, v)
56+
end
57+
return s2
58+
end
4659

60+
function with_settings(f; kw...)
61+
old = settings
62+
try
63+
global settings = Settings(settings; kw...)
64+
f(settings)
65+
finally
66+
global settings = old
67+
end
68+
end
69+
70+
#-----------------------------------------------------------------------------# utils/other
4771
attributes(t::Symbol) = plotly.schema.traces[t].attributes
4872
check_attribute(trace, attr::Symbol) = haskey(attributes(Symbol(trace)), attr) || @warn("`$trace` does not have attribute `$attr`.")
4973
check_attributes(trace; kw...) = foreach(k -> check_attribute(Symbol(trace), k), keys(kw))
@@ -53,63 +77,91 @@ mutable struct Plot
5377
data::Vector{Config}
5478
layout::Config
5579
config::Config
56-
Plot(data::Vector{Config}, layout::Config = Config(), config::Config = Config()) = new(data, Config(layout), Config(config))
80+
Plot(data::AbstractVector, layout = Config(), config = Config()) = new(Config.(data), Config(layout), Config(config))
81+
Plot(data, layout = Config(), config = Config()) = new([Config(data)], Config(layout), Config(config))
5782
end
5883

59-
Plot(data::Config, layout::Config = Config(), config::Config = Config()) = Plot([data], layout, config)
60-
Plot(; layout=Config(), config=Config(), kw...) = Plot(Config(kw), Config(layout), Config(config))
84+
Base.:(==)(a::Plot, b::Plot) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Plot))
85+
86+
save(p::Plot, file::AbstractString) = open(io -> print(io, html_page(p)), file, "w")
87+
save(file::AbstractString, p::Plot) = save(p, file)
88+
6189
(p::Plot)(; kw...) = p(Config(kw))
6290
(p::Plot)(data::Config) = (push!(p.data, data); return p)
63-
(p::Plot)(p2::Plot) = (append!(p.data, p2.data); merge!(p.layout, p2.layout); merge!(p.config, p2.config); p)
91+
(p::Plot)(p2::Plot) = merge!(p, p2)
6492

65-
StructTypes.StructType(::Plot) = StructTypes.Struct()
66-
Base.:(==)(a::Plot, b::Plot) = all(getfield(a,f) == getfield(b,f) for f in fieldnames(Plot))
93+
function Plot(; kw...)
94+
Base.depwarn("`Plot(; kw...)` is deprecated. Use `plot(; kw...)` instead.", :Plot, force=true)
95+
plot(; kw...)
96+
end
6797

6898
Base.getproperty(p::Plot, x::Symbol) = x in fieldnames(Plot) ? getfield(p, x) : (; kw...) -> p(plot(; type=x, kw...))
6999
Base.propertynames(p::Plot) = vcat(fieldnames(Plot)..., keys(plotly.schema.traces)...)
70100

71-
save(p::Plot, file::AbstractString) = open(io -> print(io, html_page(p)), file, "w")
72-
save(file::AbstractString, p::Plot) = save(p, file)
101+
Base.merge!(a::Plot, b::Plot) = (append!(a.data, b.data); merge!(a.layout, b.layout); merge!(a.config, b.config); a)
73102

74103
#-----------------------------------------------------------------------------# plot
75-
plot(; kw...) = plot(get(kw, :type, :scatter); kw...)
76-
plot(trace; kw...) = (check_attributes(trace; kw...); Plot(; type=trace, kw...))
77-
Base.propertynames(::typeof(plot)) = sort!(collect(keys(plotly.schema.traces)))
78-
Base.getproperty(::typeof(plot), x::Symbol) = (; kw...) -> plot(x; kw...)
79-
80-
#-----------------------------------------------------------------------------# display/show
81-
function html_div(o::Plot; id=randstring(10))
82-
data = JSON3.write(_fix.(o.data); allow_inf=true)
83-
layout = JSON3.write(merge(settings.layout, o.layout); allow_inf=true)
84-
config = JSON3.write(merge(settings.config, o.config); allow_inf=true)
85-
h.div(class="plotlylight-parent-div",
86-
settings.src,
87-
settings.div(; id, class="plotlylight-plot-div"),
88-
h.script("Plotly.newPlot(\"$id\", $data, $layout, $config)")
89-
)
104+
function plot(; layout = Config(), config=Config(), type=:scatter, kw...)
105+
check_attributes(type; kw...)
106+
data = isempty(kw) ? Config[] : [Config(; type, kw...)]
107+
Plot(data, layout, config)
108+
end
109+
Base.propertynames(::typeof(plot)) = keys(plotly.schema.traces)
110+
Base.getproperty(::typeof(plot), type::Symbol) = (; kw...) -> plot(; type=type, kw...)
111+
112+
113+
#-----------------------------------------------------------------------------# NewPlotScript
114+
# PlotlyX representation of: <script>Plotly.newPlot("$id", $data, $layout, $config)</script>
115+
struct NewPlotScript
116+
plot::Plot
117+
settings::Settings
118+
id::String
119+
end
120+
function Base.show(io::IO, ::MIME"text/html", o::NewPlotScript)
121+
layout = merge(o.settings.layout, o.plot.layout)
122+
config = merge(o.settings.config, o.plot.config)
123+
print(io, "<script>Plotly.newPlot(\"", o.id, "\",")
124+
json(io, o.plot.data); print(io, ',')
125+
json(io, layout); print(io, ',')
126+
json(io, config)
127+
print(io, ")</script>")
128+
end
129+
130+
#-----------------------------------------------------------------------------# display
131+
rand_id() = "plotlyx-" * join(rand('a':'z', 10))
132+
133+
function html_div(o::Plot, id=rand_id())
134+
h.div(class="plotlylight-parent", settings.src, settings.src_inject..., settings.div(; id), NewPlotScript(o, settings, id))
90135
end
91-
function html_page(o::Plot)
136+
137+
function html_page(o::Plot, id=rand_id())
92138
h.html(
93139
h.head(
94140
h.meta(charset="utf-8"),
95141
h.meta(name="viewport", content="width=device-width, initial-scale=1"),
96-
h.meta(name="description", content="PlotlyLight.jl"),
142+
h.meta(name="description", content="PlotlyLight.jl Plot"),
97143
h.title("PlotlyLight.jl"),
98-
h.style("html, body { padding: 0px; margin: 0px; } /* remove scrollbar in iframe */"),
99-
isnothing(settings.inject_head) ? "" : settings.inject_head
144+
settings.page_css,
145+
settings.src_inject...,
146+
settings.src
100147
),
101-
h.body(html_div(o))
148+
h.body(h.div(class="plotlylight-parent", settings.div(; id), NewPlotScript(o, settings, id)))
102149
)
103150
end
104-
function html_iframe(o::Plot; style=settings.style)
105-
IFrame(html_page(o); style=join(["$k:$v" for (k,v) in style], ';'))
106-
end
107-
Base.show(io::IO, ::MIME"text/html", o::Plot) = show(io, MIME"text/html"(), html_iframe(o))
108-
Base.show(io::IO, ::MIME"juliavscode/html", o::Plot) = show(io, MIME"text/html"(), o)
109151

110-
Base.display(::REPL.REPLDisplay, o::Plot) = Cobweb.preview(h.html(h.body(o, style="margin: 0px;")), reuse=settings.reuse_preview)
152+
function html_iframe(o::Plot, id=rand_id(), kw...)
153+
with_settings() do s
154+
s.div.style = "height:100vh; width:100vw"
155+
Cobweb.IFrame(html_page(o, id); style=s.iframe_style, kw...)
156+
end
157+
end
111158

112-
mathjax_script = h.script(type="text/javascript", async=true, src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js")
159+
function Base.show(io::IO, ::MIME"text/html", o::Plot)
160+
get(io, :jupyter, false) ?
161+
show(io, MIME("text/html"), html_iframe(o)) :
162+
show(io, MIME("text/html"), html_div(o))
163+
end
164+
Base.show(io::IO, ::MIME"juliavscode/html", o) = show(io, MIME("text/html"), o)
113165

114166
#-----------------------------------------------------------------------------# preset
115167
# `preset_template_<X>` overwrites `settings.layout.template`
@@ -137,6 +189,10 @@ preset = (
137189
cdn! = () -> (settings.src = h.script(src=plotly.url, charset="utf-8"); nothing),
138190
local! = () -> (settings.src = h.script(src=plotly.path, charset="utf-8"); nothing),
139191
standalone! = () -> (settings.src = h.script(read(plotly.path, String), charset="utf-8"); nothing)
192+
),
193+
display = (
194+
fullscreen! = () -> (settings.div.style = "height:100vh; width:100vw"),
195+
mathjax! = () -> (push!(settings.src_inject, h.script(src="https://cdn.jsdelivr.net/npm/[email protected]/es5/tex-svg.js"))),
140196
)
141197
)
142198

src/json.jl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
#-----------------------------------------------------------------------------# json
3+
function json_join(io::IO, itr, sep, left, right)
4+
print(io, left)
5+
for (i, item) in enumerate(itr)
6+
i == 1 || print(io, sep)
7+
json(io, item)
8+
end
9+
print(io, right)
10+
end
11+
12+
json(io::IO, x) = json_join(io, x, ',', '[', ']') # ***FALLBACK METHOD***
13+
14+
struct JSON{T}
15+
x::T
16+
end
17+
json(io::IO, x::JSON) = print(io, x.x)
18+
19+
20+
json(x) = sprint(json, x)
21+
json(io::IO, args...) = foreach(x -> json(io, x), args)
22+
23+
# Strings
24+
json(io::IO, x::Union{AbstractChar, AbstractString, Symbol}) = print(io, '"', x, '"')
25+
26+
# Numbers
27+
json(io::IO, x::Real) = isfinite(x) ? print(io, x) : print(io, "null")
28+
json(io::IO, x::Rational) = json(io, float(x))
29+
30+
# Nulls
31+
json(io::IO, ::Union{Missing, Nothing}) = print(io, "null")
32+
33+
# Bools
34+
json(io::IO, x::Bool) = print(io, x ? "true" : "false")
35+
36+
# Arrays
37+
json(io::IO, x::AbstractVector) = json_join(io, x, ',', '[', ']')
38+
json(io::IO, x::AbstractArray) = json(io, eachslice(x; dims=1))
39+
40+
# Objects
41+
json(io::IO, x::Pair) = json(io, x.first, JSON(':'), x.second)
42+
json(io::IO, x::Union{NamedTuple, AbstractDict}) = json_join(io, pairs(x), ',', '{', '}')

test/quarto.qmd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
engine: julia
3+
---
4+
5+
```{julia}
6+
using PlotlyLight
7+
8+
plot()
9+
```

test/runtests.jl

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using PlotlyLight
2-
using PlotlyLight: settings
2+
using PlotlyLight: settings, Plot, json
33
using Cobweb
44
using Cobweb: h
55
using JSON3: JSON3
@@ -8,32 +8,50 @@ using Aqua
88

99
html(x) = repr("text/html", x)
1010

11+
#-----------------------------------------------------------------------------# json
12+
@testset "json" begin
13+
@test json(1) == "1"
14+
@test json(1.0) == "1.0"
15+
@test json(1//2) == "0.5"
16+
@test json([1,2,3]) == "[1,2,3]"
17+
@test json([1.0,2.0,3.0]) == "[1.0,2.0,3.0]"
18+
@test json([1 2; 3 4]) == "[[1,2],[3,4]]"
19+
@test json((x=1,y=2)) == "{\"x\":1,\"y\":2}"
20+
@test json(nothing) == "null"
21+
@test json(true) == "true"
22+
@test json(false) == "false"
23+
@test json("test") == "\"test\""
24+
@test json(missing) == "null"
25+
@test json(NaN) == "null"
26+
@test json(Inf) == "null"
27+
@test json(-Inf) == "null"
28+
end
29+
1130
#-----------------------------------------------------------------------------# Plot methods
1231
@testset "Plot methods" begin
13-
p = Plot(Config(x = 1:10))
32+
p = Plot(Config(x = 1:10, type=:scatter))
1433
@test p isa Plot
15-
@test Plot(; x=1:10) == p
34+
@test Plot(; x=1:10, type=:scatter) == p
1635
@test !occursin("Title", html(p))
17-
@test !occursin("displaylogo", html(p))
36+
@test occursin("\"displaylogo\":false", html(p))
1837

1938
p2 = Plot(Config(x = 1:10), Config(title="Title"))
2039
@test occursin("Title", html(p2))
21-
@test !occursin("displaylogo", html(p2))
2240

2341
p3 = Plot(Config(x = 1:10), Config(title="Title"), Config(displaylogo=true))
2442
@test occursin("Title", html(p3))
25-
@test occursin("displaylogo", html(p3))
43+
@test occursin("\"displaylogo\":true", html(p3))
2644

27-
p4 = Plot()
28-
@test isempty(only(p4.data))
29-
p4(Config(x=1:10,y=1:10))
30-
@test length(p4.data) == 2
45+
p4 = Plot();
46+
@test isempty(p4.data)
47+
@test p4(Config(x=1:10,y=1:10)) isa Plot
48+
@test length(p4.data) == 1
3149
p4(;x=1:10, y=1:10)
32-
@test length(p4.data) == 3
33-
@test p4.data[2] == p4.data[3]
50+
@test length(p4.data) == 2
51+
@test p4.data[1] == p4.data[2]
3452

3553
p5 = p(p2(p3(p4)))
36-
@test length(p5.data) == 6
54+
@test length(p5.data) == 5
3755
end
3856

3957
@testset "plot" begin
@@ -44,7 +62,7 @@ end
4462

4563
@testset "settings" begin
4664
@test PlotlyLight.settings.layout == Config()
47-
@test PlotlyLight.settings.config == Config(; responsive=true)
65+
@test PlotlyLight.settings.config == Config(; responsive=true, displaylogo=false)
4866
end
4967

5068
@testset "saving" begin
@@ -61,7 +79,6 @@ end
6179
@testset "other" begin
6280
@test propertynames(Plot()) isa Vector{Symbol}
6381
@test all(x in propertynames(Plot()) for x in propertynames(plot))
64-
@test PlotlyLight._fix([1 2; 3 4]) == [[1, 2], [3, 4]]
6582
@test propertynames(JSON3.read(JSON3.write(Plot()))) == [:data, :layout, :config]
6683
end
6784

0 commit comments

Comments
 (0)