From 564181eb36f8991a0b2c29990f27a9dcdc5cf82a Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Tue, 6 Aug 2024 09:55:32 +0200 Subject: [PATCH 1/7] format and add `path` kwarg to _preprocess --- src/preprocess.jl | 101 +++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/src/preprocess.jl b/src/preprocess.jl index 1f9801a..1be70a1 100644 --- a/src/preprocess.jl +++ b/src/preprocess.jl @@ -1,73 +1,80 @@ const SKIP_FLOAT32 = Ref(false) -skip_float32(f) = let - SKIP_FLOAT32[] = true - out = f() - SKIP_FLOAT32[] = false - out -end +skip_float32(f) = + let + SKIP_FLOAT32[] = true + out = f() + SKIP_FLOAT32[] = false + out + end #= This function is basically `_json_lower` from PlotlyBase, but we do it directly on the PlutoPlot to avoid the modifying the behavior of `_json_lower` for `Plot` objects (which is required to modify how matrices are passed to `publish_to_js`) -=# +=# # Main _preprocess for the PlutoPlot object function _preprocess(pp::PlutoPlot) - p = pp.Plot + p = pp.Plot out = Dict( - :data => _preprocess(p.data), - :layout => _preprocess(p.layout), - :frames => _preprocess(p.frames), - :config => _preprocess(p.config) + :data => _preprocess(p.data; path=(:data,)), + :layout => _preprocess(p.layout; path=(:layout,)), + :frames => _preprocess(p.frames; path=(:frames,)), + :config => _preprocess(p.config; path=(:config,)) ) - templates = PlotlyBase.templates + templates = PlotlyBase.templates layout_template = p.layout.template template = if layout_template isa String layout_template === "none" ? Template() : templates[layout_template] elseif layout_template === templates[templates.default] - # If we enter here we did not specify any template in the layout, so se use our default + # If we enter here we did not specify any template in the layout, so se use our default DEFAULT_TEMPLATE[] else layout_template - end - out[:layout][:template] = _preprocess(template) + end + out[:layout][:template] = _preprocess(template; path=(:layout, :template)) out end # Defaults to JSON.lower for generic non-overloaded types -_preprocess(x) = PlotlyBase.JSON.lower(x) # Default -_preprocess(x::TimeType) = sprint(print, x) # For handling datetimes +_preprocess(x; path=(:nothing,)) = PlotlyBase.JSON.lower(x) # Default +_preprocess(x::TimeType; path=(:nothing,)) = sprint(print, x) # For handling datetimes -_preprocess(s::AbstractString) = String(s) +function _preprocess(s::AbstractString; path=(:nothing,)) + s = String(s) + return last(path) === :title ? + _preprocess(attr(; text=s); path) : # We make the title in the non-legacy format + s +end -_preprocess(x::Real) = SKIP_FLOAT32[] ? x : Float32(x) +_preprocess(x::Real; path = (:nothing,)) = SKIP_FLOAT32[] ? x : Float32(x) -_preprocess(x::Union{Bool,String,Nothing,Missing}) = x -_preprocess(x::Symbol) = string(x) -_preprocess(x::Union{Tuple,AbstractArray}) = _preprocess.(x) -_preprocess(A::AbstractArray{<:Union{Number, AbstractVector{<:Number}}, N}) where N = if N == 1 - collect(_preprocess.(A)) -else - [_preprocess(collect(s)) for s ∈ eachslice(A; dims = ndims(A))] -end -function _preprocess(d::Dict) - Dict{Any,Any}(k => _preprocess(v) for (k, v) in pairs(d)) +_preprocess(x::Union{Bool,Nothing,Missing}; path = (:nothing,)) = x +_preprocess(x::Symbol; path = (:nothing,)) = string(x) +_preprocess(x::Union{Tuple,AbstractArray}; path = (:nothing,)) = _preprocess.(x) +_preprocess(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}; path = (:nothing,)) where {N} = + if N == 1 + collect(_preprocess.(A); path) + else + [_preprocess(collect(s); path) for s ∈ eachslice(A; dims=ndims(A))] + end +function _preprocess(d::Dict; path = (:nothing,)) + Dict{Any,Any}(k => _preprocess(v; path = (path..., Symbol(k))) for (k, v) in pairs(d)) end -_preprocess(a::PlotlyBase.HasFields) = Dict{Any,Any}(k => _preprocess(v) for (k, v) in pairs(a.fields)) -_preprocess(c::PlotlyBase.Cycler) = c.vals -function _preprocess(c::PlotlyBase.ColorScheme)::Vector{Tuple{Float64,String}} +_preprocess(a::PlotlyBase.HasFields; path = (:nothing,)) = Dict{Any,Any}(k => _preprocess(v; path = (path..., Symbol(k))) for (k, v) in pairs(a.fields)) +_preprocess(c::PlotlyBase.Cycler; path = (:nothin,)) = c.vals +function _preprocess(c::PlotlyBase.ColorScheme; path = (:nothing,))::Vector{Tuple{Float64,String}} N = length(c.colors) map(ic -> ((ic[1] - 1) / (N - 1), _preprocess(ic[2])), enumerate(c.colors)) end -_preprocess(t::PlotlyBase.Template) = Dict( - :data => _preprocess(t.data), - :layout => _preprocess(t.layout) +_preprocess(t::PlotlyBase.Template; path = (:template,)) = Dict( + :data => _preprocess(t.data; path = (path..., :data)), + :layout => _preprocess(t.layout; path = (path..., :layout)) ) -function _preprocess(pc::PlotlyBase.PlotConfig) +function _preprocess(pc::PlotlyBase.PlotConfig; path = (:config,)) out = Dict{Symbol,Any}() for fn in fieldnames(PlotlyBase.PlotConfig) field = getfield(pc, fn) @@ -79,21 +86,21 @@ function _preprocess(pc::PlotlyBase.PlotConfig) end # Files that will be later moved to an extension. At the moment it's pointless because PlotlyBase uses those internally anyway. -_preprocess(s::LaTeXString) = s.s +_preprocess(s::LaTeXString; path = (:nothing,)) = s.s # Colors, they can be put inside an extension -_preprocess(c::Color) = @views begin +_preprocess(c::Color; path = (:nothing,)) = @views begin s = hex(c, :rrggbb) - r = parse(Int, s[1:2]; base = 16) - g = parse(Int, s[3:4]; base = 16) - b = parse(Int, s[5:6]; base = 16) + r = parse(Int, s[1:2]; base=16) + g = parse(Int, s[3:4]; base=16) + b = parse(Int, s[5:6]; base=16) return "rgb($r,$g,$b)" end -_preprocess(c::TransparentColor) = @views begin +_preprocess(c::TransparentColor; path = (:nothing,)) = @views begin s = hex(c, :rrggbbaa) - r = parse(Int, s[1:2]; base = 16) - g = parse(Int, s[3:4]; base = 16) - b = parse(Int, s[5:6]; base = 16) - a = parse(Int, s[7:8]; base = 16) + r = parse(Int, s[1:2]; base=16) + g = parse(Int, s[3:4]; base=16) + b = parse(Int, s[5:6]; base=16) + a = parse(Int, s[7:8]; base=16) return "rgba($r,$g,$b,$(a/255))" end \ No newline at end of file From 9079417c45506e38c3c117052d1312e454a2ba1e Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Tue, 6 Aug 2024 20:10:57 +0200 Subject: [PATCH 2/7] use varargs --- src/preprocess.jl | 83 +++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/src/preprocess.jl b/src/preprocess.jl index 7bbdc7d..5e80c05 100644 --- a/src/preprocess.jl +++ b/src/preprocess.jl @@ -9,14 +9,22 @@ on the PlutoPlot to avoid the modifying the behavior of `_json_lower` for `Plot` objects (which is required to modify how matrices are passed to `publish_to_js`) =# +struct AttrName{S} + name::Symbol + AttrName(s::Symbol) = new{s}(s) +end +AttrName(s) = AttrName(Symbol(s)) +maybewrap(@nospecialize(n::AttrName)) = (n,) +maybewrap(@nospecialize(x)) = x + # Main _preprocess for the PlutoPlot object function _preprocess(pp::PlutoPlot) p = pp.Plot out = Dict( - :data => _preprocess(p.data; path=(:data,)), - :layout => _preprocess(p.layout; path=(:layout,)), - :frames => _preprocess(p.frames; path=(:frames,)), - :config => _preprocess(p.config; path=(:config,)) + :data => _preprocess(p.data, AttrName(:data)), + :layout => _preprocess(p.layout, AttrName(:layout)), + :frames => _preprocess(p.frames, AttrName(:frames)), + :config => _preprocess(p.config, AttrName(:config)) ) templates = PlotlyBase.templates @@ -29,70 +37,73 @@ function _preprocess(pp::PlutoPlot) else layout_template end - out[:layout][:template] = _preprocess(template; path=(:layout, :template)) + out[:layout][:template] = _preprocess(template, AttrName(:template), AttrName(:layout)) out end # Defaults to JSON.lower for generic non-overloaded types -_preprocess(x; path=(:nothing,)) = PlotlyBase.JSON.lower(x) # Default -_preprocess(x::TimeType; path=(:nothing,)) = sprint(print, x) # For handling datetimes - -function _preprocess(s::AbstractString; path=(:nothing,)) - s = String(s) - return last(path) === :title ? - _preprocess(attr(; text=s); path) : # We make the title in the non-legacy format - s -end +_preprocess(x, @nospecialize(args::Vararg{AttrName})) = PlotlyBase.JSON.lower(x) # Default +_preprocess(x::TimeType, @nospecialize(args::Vararg{AttrName})) = sprint(print, x) # For handling datetimes + +_preprocess(s::AbstractString, @nospecialize(args::Vararg{AttrName})) = + String(s) +_preprocess(s::AbstractString, ::AttrName{:title}, @nospecialize(args::Vararg{AttrName})) = + Dict(:text => String(s)) + -_preprocess(x::Real; path = (:nothing,)) = SKIP_FLOAT32[] ? x : Float32(x) +_preprocess(x::Real, @nospecialize(args::Vararg{AttrName})) = SKIP_FLOAT32[] ? x : Float32(x) -_preprocess(x::Union{Bool,Nothing,Missing}; path = (:nothing,)) = x -_preprocess(x::Symbol; path = (:nothing,)) = string(x) -_preprocess(x::Union{Tuple,AbstractArray}; path = (:nothing,)) = _preprocess.(x) -_preprocess(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}; path = (:nothing,)) where {N} = +_preprocess(x::Union{Bool,Nothing,Missing}, @nospecialize(args::Vararg{AttrName})) = x +_preprocess(x::Symbol, @nospecialize(args::Vararg{AttrName})) = string(x) +_preprocess(x::Union{Tuple,AbstractArray}, @nospecialize(args::Vararg{AttrName})) = [_preprocess(el, args...) for el in x] +_preprocess(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}, @nospecialize(args::Vararg{AttrName})) where {N} = if N == 1 - collect(_preprocess.(A); path) + [_preprocess(el, args...) for el in A] else - [_preprocess(collect(s); path) for s ∈ eachslice(A; dims=ndims(A))] + [_preprocess(collect(s, args...)) for s ∈ eachslice(A; dims=ndims(A))] end -function _preprocess(d::Dict; path = (:nothing,)) - Dict{Any,Any}(k => _preprocess(v; path = (path..., Symbol(k))) for (k, v) in pairs(d)) -end -_preprocess(a::PlotlyBase.HasFields; path = (:nothing,)) = Dict{Any,Any}(k => _preprocess(v; path = (path..., Symbol(k))) for (k, v) in pairs(a.fields)) -_preprocess(c::PlotlyBase.Cycler; path = (:nothin,)) = c.vals -function _preprocess(c::PlotlyBase.ColorScheme; path = (:nothing,))::Vector{Tuple{Float64,String}} + +_preprocess(d::Dict, @nospecialize(args::Vararg{AttrName})) = + Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(d)) + +_preprocess(a::PlotlyBase.HasFields, @nospecialize(args::AttrName)) = + Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(a.fields)) + +_preprocess(c::PlotlyBase.Cycler, @nospecialize(args::Vararg{AttrName})) = c.vals + +function _preprocess(c::PlotlyBase.ColorScheme, @nospecialize(args::Vararg{AttrName}))::Vector{Tuple{Float64,String}} N = length(c.colors) - map(ic -> ((ic[1] - 1) / (N - 1), _preprocess(ic[2])), enumerate(c.colors)) + map(ic -> ((ic[1] - 1) / (N - 1), _preprocess(ic[2], args...)), enumerate(c.colors)) end -_preprocess(t::PlotlyBase.Template; path = (:template,)) = Dict( - :data => _preprocess(t.data; path = (path..., :data)), - :layout => _preprocess(t.layout; path = (path..., :layout)) +_preprocess(t::PlotlyBase.Template, @nospecialize(args::Vararg{AttrName})) = Dict( + :data => _preprocess(t.data, AttrName(:data), args...), + :layout => _preprocess(t.layout, AttrName(:layout), args...) ) -function _preprocess(pc::PlotlyBase.PlotConfig; path = (:config,)) +function _preprocess(pc::PlotlyBase.PlotConfig, @nospecialize(args::Vararg{Symbol})) out = Dict{Symbol,Any}() for fn in fieldnames(PlotlyBase.PlotConfig) field = getfield(pc, fn) if !isnothing(field) - out[fn] = field + out[fn] = _preprocess(field, AttrName(fn), args...) end end out end # Files that will be later moved to an extension. At the moment it's pointless because PlotlyBase uses those internally anyway. -_preprocess(s::LaTeXString; path = (:nothing,)) = s.s +_preprocess(s::LaTeXString, @nospecialize(args::Vararg{AttrName})) = s.s # Colors, they can be put inside an extension -_preprocess(c::Color; path = (:nothing,)) = @views begin +_preprocess(c::Color, @nospecialize(args::Vararg{AttrName})) = @views begin s = hex(c, :rrggbb) r = parse(Int, s[1:2]; base=16) g = parse(Int, s[3:4]; base=16) b = parse(Int, s[5:6]; base=16) return "rgb($r,$g,$b)" end -_preprocess(c::TransparentColor; path = (:nothing,)) = @views begin +_preprocess(c::TransparentColor, @nospecialize(args::Vararg{AttrName})) = @views begin s = hex(c, :rrggbbaa) r = parse(Int, s[1:2]; base=16) g = parse(Int, s[3:4]; base=16) From 5dc4263f68f809d36f71fc8088b9299f661e4c04 Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Wed, 7 Aug 2024 09:57:06 +0200 Subject: [PATCH 3/7] add comment --- src/preprocess.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preprocess.jl b/src/preprocess.jl index 5e80c05..bfd9a47 100644 --- a/src/preprocess.jl +++ b/src/preprocess.jl @@ -65,7 +65,7 @@ _preprocess(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}, @nospe _preprocess(d::Dict, @nospecialize(args::Vararg{AttrName})) = Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(d)) - +# We have a separate one because it seems to reduce allocations _preprocess(a::PlotlyBase.HasFields, @nospecialize(args::AttrName)) = Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(a.fields)) From bf5a75c769d5defc15eaea7ae8ccfb55446ebc48 Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Wed, 7 Aug 2024 12:03:03 +0200 Subject: [PATCH 4/7] _preprocess rewrite --- ext/UnitfulExt.jl | 4 +- notebooks/basic_tests.jl | 5 +- src/preprocess.jl | 154 +++++++++++++++++++++++++-------------- src/show.jl | 2 +- test/basic_coverage.jl | 20 ++--- test/extensions.jl | 6 +- 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/ext/UnitfulExt.jl b/ext/UnitfulExt.jl index 53cdee8..015887e 100644 --- a/ext/UnitfulExt.jl +++ b/ext/UnitfulExt.jl @@ -1,8 +1,8 @@ module UnitfulExt -using PlutoPlotly: _preprocess, PlutoPlotly +using PlutoPlotly: _process_with_names, PlutoPlotly, AttrName using Unitful: Quantity, ustrip -PlutoPlotly._preprocess(q::Quantity) = _preprocess(ustrip(q)) +PlutoPlotly._process_with_names(q::Quantity, fl::Val, @nospecialize(args::Vararg{AttrName})) = _process_with_names(ustrip(q), fl, args...) end \ No newline at end of file diff --git a/notebooks/basic_tests.jl b/notebooks/basic_tests.jl index 150449c..23a1b9a 100644 --- a/notebooks/basic_tests.jl +++ b/notebooks/basic_tests.jl @@ -16,9 +16,11 @@ macro bind(def, element) end end +# ╔═╡ db523d3c-df04-4722-937b-33ccf374800f +using PlutoDevMacros + # ╔═╡ 72c073fd-5f1b-4af0-901b-aaa901f0f273 begin - using PlutoDevMacros using PlutoUI using PlutoExtras using Dates @@ -846,6 +848,7 @@ version = "17.4.0+2" # ╔═╡ Cell order: # ╠═7bd46437-8af0-4a15-87e9-1508869e1600 +# ╠═db523d3c-df04-4722-937b-33ccf374800f # ╠═72c073fd-5f1b-4af0-901b-aaa901f0f273 # ╠═70dc8fa0-cc32-4ebe-af0d-62b5bb3a82ed # ╟─acba5003-a456-4c1a-a53f-71a3bec30251 diff --git a/src/preprocess.jl b/src/preprocess.jl index bfd9a47..c4fe333 100644 --- a/src/preprocess.jl +++ b/src/preprocess.jl @@ -1,30 +1,58 @@ -const SKIP_FLOAT32 = ScopedValue(false) -skip_float32(f) = let - with(f, SKIP_FLOAT32 => true) -end +const FORCE_FLOAT32 = ScopedValue(true) -#= -This function is basically `_json_lower` from PlotlyBase, but we do it directly -on the PlutoPlot to avoid the modifying the behavior of `_json_lower` for `Plot` -objects (which is required to modify how matrices are passed to `publish_to_js`) -=# +# Get a val to specify whether +floatval() = Val{FORCE_FLOAT32[]}() +# Helper struct just to have the name as symbol parameter for dispatch struct AttrName{S} name::Symbol AttrName(s::Symbol) = new{s}(s) end AttrName(s) = AttrName(Symbol(s)) -maybewrap(@nospecialize(n::AttrName)) = (n,) -maybewrap(@nospecialize(x)) = x +# This is used to handle args... which in case only one element is passed is not iterable +Base.iterate(n::AttrName, state = 1) = state > 1 ? nothing : (n, state + 1) + +#= +This function is basically `_json_lower` from PlotlyBase, but we do it directly +on the PlutoPlot to avoid the modifying the behavior of `_json_lower` for `Plot` +objects (which is required to modify how matrices are passed to `publish_to_js`). + +We now have a complex dispatch to be able to do custom processing for specific +attributes. + +The standard signature for a _process_with_names method is: + _process_with_names(x, fl::Val, @nospecialize(args::Vararg{AttrName})) + +where +- the first argument should be the actual input to process and should be +typed accordingly for dispatch. +- The second argument is either `Val{true}` or `Val{false}` and represents the +flag to force number to be converted in Float32. # We added this to +significantly improve performance as the runtime check for converting or not was +creating type instability. +- All the remaining arguments are of type `AttrName` and represent the path of +attributes names leading to this specific input. For example, if we are +processing the input that is inside the xaxis_range in the layout, the function +call will have this form: + _process_with_names(x, fl, AttrName(:xaxis), AttrName(:range), AttrName(:layout)) + +This again is to allow dispatch to work on the path so that one can customize behavior of _process_with_names with great control. +At the moment this is only used for modifying the behavior when `title` is +passed as a String, changing it to the more recent plotly synthax (see +https://github.com/JuliaPluto/PlutoPlotly.jl/issues/51) + +The various `@nospecialize` below are to avoid exploding compilation given our exponential number of dispatch options, so we only specialize where we need. +=# -# Main _preprocess for the PlutoPlot object -function _preprocess(pp::PlutoPlot) +# Main _process_with_names for the PlutoPlot object +function _process_with_names(pp::PlutoPlot) p = pp.Plot + fl = floatval() out = Dict( - :data => _preprocess(p.data, AttrName(:data)), - :layout => _preprocess(p.layout, AttrName(:layout)), - :frames => _preprocess(p.frames, AttrName(:frames)), - :config => _preprocess(p.config, AttrName(:config)) + :data => _process_with_names(p.data, fl, AttrName(:data)), + :layout => _process_with_names(p.layout, fl, AttrName(:layout)), + :frames => _process_with_names(p.frames, fl, AttrName(:frames)), + :config => _process_with_names(p.config, fl, AttrName(:config)) ) templates = PlotlyBase.templates @@ -37,73 +65,89 @@ function _preprocess(pp::PlutoPlot) else layout_template end - out[:layout][:template] = _preprocess(template, AttrName(:template), AttrName(:layout)) + out[:layout][:template] = _process_with_names(template, fl, AttrName(:template), AttrName(:layout)) out end -# Defaults to JSON.lower for generic non-overloaded types -_preprocess(x, @nospecialize(args::Vararg{AttrName})) = PlotlyBase.JSON.lower(x) # Default -_preprocess(x::TimeType, @nospecialize(args::Vararg{AttrName})) = sprint(print, x) # For handling datetimes - -_preprocess(s::AbstractString, @nospecialize(args::Vararg{AttrName})) = - String(s) -_preprocess(s::AbstractString, ::AttrName{:title}, @nospecialize(args::Vararg{AttrName})) = - Dict(:text => String(s)) - - -_preprocess(x::Real, @nospecialize(args::Vararg{AttrName})) = SKIP_FLOAT32[] ? x : Float32(x) - -_preprocess(x::Union{Bool,Nothing,Missing}, @nospecialize(args::Vararg{AttrName})) = x -_preprocess(x::Symbol, @nospecialize(args::Vararg{AttrName})) = string(x) -_preprocess(x::Union{Tuple,AbstractArray}, @nospecialize(args::Vararg{AttrName})) = [_preprocess(el, args...) for el in x] -_preprocess(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}, @nospecialize(args::Vararg{AttrName})) where {N} = +# Generic fallbacks +_process_with_names(x, ::Val, @nospecialize(args::Vararg{AttrName})) = _preprocess(x) +_process_with_names(x) = _process_with_names(x, floatval()) + +# Handle strings +_process_with_names(s::AbstractString, ::Val, @nospecialize(args::Vararg{AttrName})) = + _preprocess(s) +_process_with_names(s::AbstractString, ::Val, ::AttrName{:title}, @nospecialize(args::Vararg{AttrName})) = + Dict(:text => _preprocess(s)) + +# Handle Reals +_process_with_names(x::Real, ::Val{false}, @nospecialize(args::Vararg{AttrName})) = x +_process_with_names(x::Real, ::Val{true}, @nospecialize(args::Vararg{AttrName})) = x isa Bool ? x : Float32(x) +# Tuple, Arrays +_process_with_names(x::Union{Tuple,AbstractArray}, fl::Val, @nospecialize(args::Vararg{AttrName})) = [_process_with_names(el, fl, args...) for el in x] +# Multidimensional array of numbers must be nested 1D arrays +_process_with_names(A::AbstractArray{<:Union{Number,AbstractVector{<:Number}},N}, fl::Val, @nospecialize(args::Vararg{AttrName})) where {N} = if N == 1 - [_preprocess(el, args...) for el in A] + [_process_with_names(el, fl, args...) for el in A] else - [_preprocess(collect(s, args...)) for s ∈ eachslice(A; dims=ndims(A))] + [_process_with_names(collect(s), fl, args...) for s ∈ eachslice(A; dims=ndims(A))] end -_preprocess(d::Dict, @nospecialize(args::Vararg{AttrName})) = - Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(d)) +# Dict ans HasFields +_process_with_names(d::Dict, fl::Val, @nospecialize(args::Vararg{AttrName})) = + Dict{Any,Any}(k => _process_with_names(v, fl, args...) for (k, v) in pairs(d)) +_process_with_names(d::Dict{Symbol}, fl::Val, @nospecialize(args::Vararg{AttrName})) = + Dict{Symbol,Any}(k => _process_with_names(v, fl, AttrName(k), args...) for (k, v) in pairs(d)) # We have a separate one because it seems to reduce allocations -_preprocess(a::PlotlyBase.HasFields, @nospecialize(args::AttrName)) = - Dict{Any,Any}(k => _preprocess(v, AttrName(k), maybewrap(args)...) for (k, v) in pairs(a.fields)) - -_preprocess(c::PlotlyBase.Cycler, @nospecialize(args::Vararg{AttrName})) = c.vals +_process_with_names(a::PlotlyBase.HasFields, fl::Val, @nospecialize(args::AttrName)) = + _process_with_names(a.fields, fl, args...) -function _preprocess(c::PlotlyBase.ColorScheme, @nospecialize(args::Vararg{AttrName}))::Vector{Tuple{Float64,String}} - N = length(c.colors) - map(ic -> ((ic[1] - 1) / (N - 1), _preprocess(ic[2], args...)), enumerate(c.colors)) -end - -_preprocess(t::PlotlyBase.Template, @nospecialize(args::Vararg{AttrName})) = Dict( - :data => _preprocess(t.data, AttrName(:data), args...), - :layout => _preprocess(t.layout, AttrName(:layout), args...) +# Templates +_process_with_names(t::PlotlyBase.Template, fl::Val, @nospecialize(args::Vararg{AttrName})) = Dict( + :data => _process_with_names(t.data, fl, AttrName(:data), args...), + :layout => _process_with_names(t.layout, fl, AttrName(:layout), args...) ) -function _preprocess(pc::PlotlyBase.PlotConfig, @nospecialize(args::Vararg{Symbol})) +# Config +function _process_with_names(pc::PlotlyBase.PlotConfig, fl::Val, @nospecialize(args::Vararg{Symbol})) out = Dict{Symbol,Any}() for fn in fieldnames(PlotlyBase.PlotConfig) field = getfield(pc, fn) if !isnothing(field) - out[fn] = _preprocess(field, AttrName(fn), args...) + out[fn] = _process_with_names(field, fl, AttrName(fn), args...) end end out end +## The functions below are the internal processing only taking the value, so not depending on names path or float32 flag +# Defaults to JSON.lower for generic non-overloaded types +_preprocess(x) = PlotlyBase.JSON.lower(x) # Default +_preprocess(x::TimeType) = sprint(print, x) # For handling datetimes + +_preprocess(s::Union{AbstractString, Symbol}) = String(s) + +_preprocess(x::Union{Nothing,Missing}) = x +_preprocess(x::Symbol) = string(x) + +_preprocess(c::PlotlyBase.Cycler) = c.vals + +function _preprocess(c::PlotlyBase.ColorScheme)::Vector{Tuple{Float64,String}} + N = length(c.colors) + map(ic -> ((ic[1] - 1) / (N - 1), _preprocess(ic[2])), enumerate(c.colors)) +end + # Files that will be later moved to an extension. At the moment it's pointless because PlotlyBase uses those internally anyway. -_preprocess(s::LaTeXString, @nospecialize(args::Vararg{AttrName})) = s.s +_preprocess(s::LaTeXString) = s.s # Colors, they can be put inside an extension -_preprocess(c::Color, @nospecialize(args::Vararg{AttrName})) = @views begin +_preprocess(c::Color) = @views begin s = hex(c, :rrggbb) r = parse(Int, s[1:2]; base=16) g = parse(Int, s[3:4]; base=16) b = parse(Int, s[5:6]; base=16) return "rgb($r,$g,$b)" end -_preprocess(c::TransparentColor, @nospecialize(args::Vararg{AttrName})) = @views begin +_preprocess(c::TransparentColor) = @views begin s = hex(c, :rrggbbaa) r = parse(Int, s[1:2]; base=16) g = parse(Int, s[3:4]; base=16) diff --git a/src/show.jl b/src/show.jl index f3de169..87d7556 100644 --- a/src/show.jl +++ b/src/show.jl @@ -11,7 +11,7 @@ function _show(pp::PlutoPlot; script_id = "pluto-plotly-div", ver = get_plotly_v } // Publish the plot object to JS - let plot_obj = _.update($(maybe_publish_to_js(_preprocess(pp))), "layout", removeTypedArray) + let plot_obj = _.update($(maybe_publish_to_js(_process_with_names(pp))), "layout", removeTypedArray) // Get the plotly listeners const plotly_listeners = $(pp.plotly_listeners) // Get the JS listeners diff --git a/test/basic_coverage.jl b/test/basic_coverage.jl index f416705..ef99ca4 100644 --- a/test/basic_coverage.jl +++ b/test/basic_coverage.jl @@ -1,14 +1,16 @@ using Test using PlutoPlotly -using PlutoPlotly: _preprocess, SKIP_FLOAT32, skip_float32, ARTIFACT_VERSION, PLOTLY_VERSION +using PlutoPlotly: _preprocess, FORCE_FLOAT32, ARTIFACT_VERSION, PLOTLY_VERSION, _process_with_names using PlutoPlotly.PlotlyBase: ColorScheme, Colors, Cycler, templates using ScopedValues -@test SKIP_FLOAT32[] == false -@test skip_float32() do - SKIP_FLOAT32[] -end == true -@test SKIP_FLOAT32[] == false +p = plot(rand(Int, 4)); +_p = p |> _process_with_names +@test first(_p[:data])[:y] isa Vector{Float32} +with(FORCE_FLOAT32 => false) do + _p = p |> _process_with_names + @test first(_p[:data])[:y] isa Vector{Int} +end @test force_pluto_mathjax_local() === false try @@ -19,12 +21,12 @@ finally end @test ColorScheme([Colors.RGB(0.0, 0.0, 0.0), Colors.RGB(1.0, 1.0, 1.0)], -"custom", "twotone, black and white") |> _preprocess == [(0.0, "rgb(0,0,0)"), (1.0, "rgb(255,255,255)")] +"custom", "twotone, black and white") |> _process_with_names == [(0.0, "rgb(0,0,0)"), (1.0, "rgb(255,255,255)")] @test _preprocess(SubString("asda",1:3)) === "asd" @test _preprocess(:lol) === "lol" -@test _preprocess(true) === true +@test _process_with_names(true) === true @test _preprocess(Cycler((1,2))) == [1,2] -@test _preprocess(1) === 1.0f0 +@test _process_with_names(1) === 1.0f0 # By default process converts to Float32 @test _preprocess(L"3+2") === raw"$3+2$" # Check that plotly is the default diff --git a/test/extensions.jl b/test/extensions.jl index b52d723..efdb7ce 100644 --- a/test/extensions.jl +++ b/test/extensions.jl @@ -27,11 +27,11 @@ if Sys.islinux() end ## Unitful Extension ## -using PlutoPlotly: _preprocess +using PlutoPlotly: _process_with_names using Unitful: °, ustrip uv_r = range(0°, 100°; step = 1°) -@test _preprocess(uv_r) == collect(0:100) +@test _process_with_names(uv_r) == collect(0:100) uv_a = rand(3,5) .* ° uv_a_strip = ustrip.(uv_a) -@test _preprocess(uv_a) == _preprocess(uv_a_strip) +@test _process_with_names(uv_a) == _process_with_names(uv_a_strip) From 094a7f7c2cc85830c7432d9015126bb14689a3b4 Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Wed, 7 Aug 2024 12:09:15 +0200 Subject: [PATCH 5/7] update Project to breaking def --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f1c35c1..3aad753 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PlutoPlotly" uuid = "8e989ff0-3d88-8e9f-f020-2b208a939ff0" authors = ["Alberto Mengali "] -version = "0.5.0" +version = "0.6.0-DEV" [deps] AbstractPlutoDingetjes = "6e696c72-6542-2067-7265-42206c756150" From 97ff84ac6f7fe88f5216e091c40216e0b2a99e2c Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Wed, 7 Aug 2024 12:26:30 +0200 Subject: [PATCH 6/7] remove unused method --- src/preprocess.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/preprocess.jl b/src/preprocess.jl index c4fe333..433463e 100644 --- a/src/preprocess.jl +++ b/src/preprocess.jl @@ -8,7 +8,6 @@ struct AttrName{S} name::Symbol AttrName(s::Symbol) = new{s}(s) end -AttrName(s) = AttrName(Symbol(s)) # This is used to handle args... which in case only one element is passed is not iterable Base.iterate(n::AttrName, state = 1) = state > 1 ? nothing : (n, state + 1) @@ -38,7 +37,7 @@ call will have this form: This again is to allow dispatch to work on the path so that one can customize behavior of _process_with_names with great control. At the moment this is only used for modifying the behavior when `title` is -passed as a String, changing it to the more recent plotly synthax (see +passed as a String, changing it to the more recent plotly syntax (see https://github.com/JuliaPluto/PlutoPlotly.jl/issues/51) The various `@nospecialize` below are to avoid exploding compilation given our exponential number of dispatch options, so we only specialize where we need. @@ -108,7 +107,7 @@ _process_with_names(t::PlotlyBase.Template, fl::Val, @nospecialize(args::Vararg{ ) # Config -function _process_with_names(pc::PlotlyBase.PlotConfig, fl::Val, @nospecialize(args::Vararg{Symbol})) +function _process_with_names(pc::PlotlyBase.PlotConfig, fl::Val, @nospecialize(args::Vararg{AttrName})) out = Dict{Symbol,Any}() for fn in fieldnames(PlotlyBase.PlotConfig) field = getfield(pc, fn) From f5be86bacf69f56f40f3e4f89fa6c1b665a256a5 Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Wed, 7 Aug 2024 12:28:21 +0200 Subject: [PATCH 7/7] add small test --- test/basic_coverage.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/basic_coverage.jl b/test/basic_coverage.jl index ef99ca4..f83192e 100644 --- a/test/basic_coverage.jl +++ b/test/basic_coverage.jl @@ -28,6 +28,7 @@ end @test _preprocess(Cycler((1,2))) == [1,2] @test _process_with_names(1) === 1.0f0 # By default process converts to Float32 @test _preprocess(L"3+2") === raw"$3+2$" +@test all(x -> _preprocess(x) === x, [nothing, missing]) # Check that plotly is the default @test default_plotly_template() == templates[templates.default]