From a2bd00b22d4d368a7c8d927f5a9892d3660e27f8 Mon Sep 17 00:00:00 2001 From: brucala Date: Sat, 6 Nov 2021 11:11:26 +0100 Subject: [PATCH 1/7] addition for layering, VLSpecTyped, promote_data_to_toplevel --- src/vlspec.jl | 104 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 29 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 5ac76b26..d7bf7fe7 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -17,13 +17,13 @@ end function augment_encoding_type(x::AbstractDict, data::Vega.DataValuesNode) if !haskey(x, "type") && !haskey(x, "aggregate") && haskey(x, "field") && haskey(data.columns, Symbol(x["field"])) new_x = copy(x) - + jl_type = eltype(data.columns[Symbol(x["field"])]) - + if jl_type <: DataValues.DataValue jl_type = eltype(jl_type) end - + if jl_type <: Number new_x["type"] = "quantitative" elseif jl_type <: AbstractString @@ -31,19 +31,19 @@ function augment_encoding_type(x::AbstractDict, data::Vega.DataValuesNode) elseif jl_type <: Dates.AbstractTime new_x["type"] = "temporal" end - + return new_x else return x -end + end end function add_encoding_types(specdict, parentdata=nothing) - if (haskey(specdict, "data") && haskey(specdict["data"], "values") && specdict["data"]["values"] isa Vega.DataValuesNode) || parentdata !== nothing + if (haskey(specdict, "data") && haskey(specdict["data"], "values") && specdict["data"]["values"] isa Vega.DataValuesNode) || parentdata !== nothing || any(i -> haskey(specdict, i), ("spec", "layer", "concat", "vconcat", "hconcat")) data = (haskey(specdict, "data") && haskey(specdict["data"], "values") && specdict["data"]["values"] isa Vega.DataValuesNode) ? specdict["data"]["values"] : parentdata newspec = OrderedDict{String,Any}( - (k == "encoding" && v isa AbstractDict) ? k => OrderedDict{String,Any}(kk => augment_encoding_type(vv, data) for (kk, vv) in v) : + (k == "encoding" && v isa AbstractDict && !isnothing(data)) ? k => OrderedDict{String,Any}(kk => augment_encoding_type(vv, data) for (kk, vv) in v) : k == "spec" ? k => add_encoding_types(v, data) : k in ("layer", "concat", "vconcat", "hconcat") ? k => [add_encoding_types(i, data) for i in v] : k => v for (k, v) in specdict ) @@ -59,8 +59,8 @@ function our_json_print(io, spec::VLSpec) end function (p::VLSpec)(data) - TableTraits.isiterabletable(data) || throw(ArgumentError("'data' is not a table.")) - + TableTraits.isiterabletable(data) || throw(ArgumentError("'data' is not a table.")) + it = IteratorInterfaceExtensions.getiterator(data) datavaluesnode = Vega.DataValuesNode(it) @@ -110,22 +110,65 @@ Create a copy of `spec` without data. See also [`Vega.deletedata!`](@ref). """ Vega.deletedata(spec::VLSpec) = Vega.deletedata!(copy(spec)) -function Base.:+(a::VLSpec, b::VLSpec) - new_spec = deepcopy(Vega.getparams(a)) - if haskey(new_spec, "facet") || haskey(new_spec, "repeat") - new_spec["spec"] = deepcopy(Vega.getparams(b)) - elseif haskey(Vega.getparams(b), "vconcat") - new_spec["vconcat"] = deepcopy(Vega.getparams(b)["vconcat"]) - elseif haskey(Vega.getparams(b), "hconcat") - new_spec["hconcat"] = deepcopy(Vega.getparams(b)["hconcat"]) - else - if !haskey(new_spec, "layer") - new_spec["layer"] = [] +abstract type VLSpecTyped end +struct SingleView <: VLSpecTyped spec::VLSpec end +abstract type ComposedView <: VLSpecTyped end +struct Layer <: ComposedView spec::VLSpec end +abstract type MultiView <: ComposedView end +struct Facet <: MultiView spec::VLSpec end +struct Repeat <: MultiView spec::VLSpec end +struct Concat <: MultiView spec::VLSpec end +struct HConcat <: MultiView spec::VLSpec end +struct VConcat <: MultiView spec::VLSpec end + +function VLSpecTyped(spec::VLSpec) + specdict = Vega.getparams(spec) + haskey(specdict, "layer") && return Layer(spec) + haskey(specdict, "facet") && return Facet(spec) + haskey(specdict, "repeat") && return Repeat(spec) + haskey(specdict, "concat") && return Concat(spec) + haskey(specdict, "hconcat") && return HConcat(spec) + haskey(specdict, "vconcat") && return VConcat(spec) + return SingleView(spec) +end + +_check_layerable(spec::VLSpec) = _check_layerable(VLSpecTyped(spec)) +_check_layerable(mv::MultiView) = error("Multi-view spec $(typeof(mv)) can not be layered") +_check_layerable(::VLSpecTyped) = true + +""" +Remove potentially duplicated data in composed subspecs and promote it to the top level spec +""" +function promote_data_to_toplevel(spec::VLSpec, key) + specdict = deepcopy(Vega.getparams(spec)) + (key in ("layer", "concat", "vconcat", "hconcat") && haskey(specdict, key)) || return specdict + parentdata = get(specdict, "data", nothing) + for subspec in specdict[key] + haskey(subspec, "data") || continue + data = subspec["data"] + if isnothing(parentdata) + parentdata = data + specdict["data"] = parentdata end - push!(new_spec["layer"], deepcopy(Vega.getparams(b))) + parentdata == data && delete!(subspec, "data") end + return VLSpec(specdict) +end + +function Base.:+(a::VLSpec, b::VLSpec) + _check_layerable(a) + _check_layerable(b) + + a_spec = deepcopy(Vega.getparams(a)) + b_spec = deepcopy(Vega.getparams(b)) + new_spec = OrderedDict{String,Any}() + + new_spec["layer"] = [a_spec, b_spec] - return VLSpec(new_spec) + # TODO: + # - if the specs are layered themself, append layers instead + + return promote_data_to_toplevel(VLSpec(new_spec), "layer") end function Base.hcat(A::VLSpec...) @@ -134,14 +177,17 @@ function Base.hcat(A::VLSpec...) for i in A push!(Vega.getparams(spec)["hconcat"], deepcopy(Vega.getparams(i))) end - return spec + return promote_data_to_toplevel(spec, "hconcat") end function Base.vcat(A::VLSpec...) - spec = VLSpec(OrderedDict{String,Any}()) - Vega.getparams(spec)["vconcat"] = [] - for i in A - push!(Vega.getparams(spec)["vconcat"], deepcopy(Vega.getparams(i))) - end - return spec + spec = VLSpec(OrderedDict{String,Any}()) + Vega.getparams(spec)["vconcat"] = [] + for i in A + push!(Vega.getparams(spec)["vconcat"], deepcopy(Vega.getparams(i))) + end + return promote_data_to_toplevel(spec, "vconcat") end + +#function Base.hvcat(ncols, A::VLSpec...) +#end From c3f09369eda47c9a3bc93beb200981506097824e Mon Sep 17 00:00:00 2001 From: brucala Date: Sat, 6 Nov 2021 11:54:01 +0100 Subject: [PATCH 2/7] layering rules for layered specs --- src/vlspec.jl | 69 +++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index d7bf7fe7..0908a8b4 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -110,6 +110,25 @@ Create a copy of `spec` without data. See also [`Vega.deletedata!`](@ref). """ Vega.deletedata(spec::VLSpec) = Vega.deletedata!(copy(spec)) +""" +Remove potentially duplicated data in composed subspecs and promote it to the top level spec +""" +function promote_data_to_toplevel(spec::VLSpec, key) + specdict = deepcopy(Vega.getparams(spec)) + (key in ("layer", "concat", "vconcat", "hconcat") && haskey(specdict, key)) || return specdict + parentdata = get(specdict, "data", nothing) + for subspec in specdict[key] + haskey(subspec, "data") || continue + data = subspec["data"] + if isnothing(parentdata) + parentdata = data + specdict["data"] = parentdata + end + parentdata == data && delete!(subspec, "data") + end + return VLSpec(specdict) +end + abstract type VLSpecTyped end struct SingleView <: VLSpecTyped spec::VLSpec end abstract type ComposedView <: VLSpecTyped end @@ -132,44 +151,28 @@ function VLSpecTyped(spec::VLSpec) return SingleView(spec) end -_check_layerable(spec::VLSpec) = _check_layerable(VLSpecTyped(spec)) -_check_layerable(mv::MultiView) = error("Multi-view spec $(typeof(mv)) can not be layered") -_check_layerable(::VLSpecTyped) = true +Layer(spec::SingleView) = Layer(VLSpec(OrderedDict{String,Any}("layer" => [deepcopy(Vega.getparams(spec.spec))]))) -""" -Remove potentially duplicated data in composed subspecs and promote it to the top level spec -""" -function promote_data_to_toplevel(spec::VLSpec, key) - specdict = deepcopy(Vega.getparams(spec)) - (key in ("layer", "concat", "vconcat", "hconcat") && haskey(specdict, key)) || return specdict - parentdata = get(specdict, "data", nothing) - for subspec in specdict[key] - haskey(subspec, "data") || continue - data = subspec["data"] - if isnothing(parentdata) - parentdata = data - specdict["data"] = parentdata - end - parentdata == data && delete!(subspec, "data") - end - return VLSpec(specdict) -end - -function Base.:+(a::VLSpec, b::VLSpec) - _check_layerable(a) - _check_layerable(b) - - a_spec = deepcopy(Vega.getparams(a)) - b_spec = deepcopy(Vega.getparams(b)) +Base.:+(a::VLSpec, b::VLSpec) = Base.:+(VLSpecTyped(a), VLSpecTyped(b)) +Base.:+(a::VLSpecTyped, b::MultiView) = b + a +Base.:+(a::MultiView, b::VLSpecTyped) = error("Multi-view spec $(typeof(a)) can not be layered") +Base.:+(a::MultiView, b::MultiView) = error("Multi-view spec $(typeof(a)) can not be layered") +Base.:+(a::VLSpecTyped, b::VLSpecTyped) = error("Layering not implemented for $(typeof(a)) + $(typeof(b))") +function Base.:+(a::SingleView, b::SingleView) + a_spec = deepcopy(Vega.getparams(a.spec)) + b_spec = deepcopy(Vega.getparams(b.spec)) new_spec = OrderedDict{String,Any}() - new_spec["layer"] = [a_spec, b_spec] - - # TODO: - # - if the specs are layered themself, append layers instead - return promote_data_to_toplevel(VLSpec(new_spec), "layer") end +function Base.:+(a::Layer, b::Layer) + a_spec = deepcopy(Vega.getparams(a.spec)) + b_spec = deepcopy(Vega.getparams(b.spec)) + append!(a_spec["layer"], b_spec["layer"]) + return promote_data_to_toplevel(VLSpec(a_spec), "layer") +end +Base.:+(a::SingleView, b::Layer) = Layer(a) + b +Base.:+(a::Layer, b::SingleView) = a + Layer(b) function Base.hcat(A::VLSpec...) spec = VLSpec(OrderedDict{String,Any}()) From 4a5147681a1ec55f232a44b38b0653d61868d644 Mon Sep 17 00:00:00 2001 From: brucala Date: Sat, 6 Nov 2021 11:59:16 +0100 Subject: [PATCH 3/7] simplify layering rules --- src/vlspec.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 0908a8b4..9ba5b81d 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -158,19 +158,13 @@ Base.:+(a::VLSpecTyped, b::MultiView) = b + a Base.:+(a::MultiView, b::VLSpecTyped) = error("Multi-view spec $(typeof(a)) can not be layered") Base.:+(a::MultiView, b::MultiView) = error("Multi-view spec $(typeof(a)) can not be layered") Base.:+(a::VLSpecTyped, b::VLSpecTyped) = error("Layering not implemented for $(typeof(a)) + $(typeof(b))") -function Base.:+(a::SingleView, b::SingleView) - a_spec = deepcopy(Vega.getparams(a.spec)) - b_spec = deepcopy(Vega.getparams(b.spec)) - new_spec = OrderedDict{String,Any}() - new_spec["layer"] = [a_spec, b_spec] - return promote_data_to_toplevel(VLSpec(new_spec), "layer") -end function Base.:+(a::Layer, b::Layer) a_spec = deepcopy(Vega.getparams(a.spec)) b_spec = deepcopy(Vega.getparams(b.spec)) append!(a_spec["layer"], b_spec["layer"]) return promote_data_to_toplevel(VLSpec(a_spec), "layer") end +Base.:+(a::SingleView, b::SingleView) = Layer(a) + Layer(b) Base.:+(a::SingleView, b::Layer) = Layer(a) + b Base.:+(a::Layer, b::SingleView) = a + Layer(b) From e3010d123d58c41e5a62046ba1f8015d03a5a968 Mon Sep 17 00:00:00 2001 From: brucala Date: Sat, 6 Nov 2021 14:46:57 +0100 Subject: [PATCH 4/7] add multiplication operator for composition --- src/vlspec.jl | 62 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 9ba5b81d..99d3a2e1 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -140,6 +140,8 @@ struct Concat <: MultiView spec::VLSpec end struct HConcat <: MultiView spec::VLSpec end struct VConcat <: MultiView spec::VLSpec end +const ConcatSpecs = Union{Concat, HConcat, VConcat} + function VLSpecTyped(spec::VLSpec) specdict = Vega.getparams(spec) haskey(specdict, "layer") && return Layer(spec) @@ -153,9 +155,19 @@ end Layer(spec::SingleView) = Layer(VLSpec(OrderedDict{String,Any}("layer" => [deepcopy(Vega.getparams(spec.spec))]))) +""" + spec1::VLSpec + spec2::VLSpec + +The addition of two `VLSpec` produces a new `VLSpec` with both specs layered. The order matters +as `spec1` will appear below `spec2`. +If the specs contain common data, it will be promoted to the top level specification. +Layering layered specification will append the layers instead of creating a nested layer, so +for example [l1, l2] + [l3, l4] will become [l1, l2, l3, l4] instead of [[l1, l2], [l3, l4]]. +Multi-view specs (facet, repeat, concat) cannot be layered. +""" Base.:+(a::VLSpec, b::VLSpec) = Base.:+(VLSpecTyped(a), VLSpecTyped(b)) -Base.:+(a::VLSpecTyped, b::MultiView) = b + a Base.:+(a::MultiView, b::VLSpecTyped) = error("Multi-view spec $(typeof(a)) can not be layered") +Base.:+(a::VLSpecTyped, b::MultiView) = error("Multi-view spec $(typeof(b)) can not be layered") Base.:+(a::MultiView, b::MultiView) = error("Multi-view spec $(typeof(a)) can not be layered") Base.:+(a::VLSpecTyped, b::VLSpecTyped) = error("Layering not implemented for $(typeof(a)) + $(typeof(b))") function Base.:+(a::Layer, b::Layer) @@ -168,6 +180,51 @@ Base.:+(a::SingleView, b::SingleView) = Layer(a) + Layer(b) Base.:+(a::SingleView, b::Layer) = Layer(a) + b Base.:+(a::Layer, b::SingleView) = a + Layer(b) +""" + spec1::VLSpec * spec2::VLSpec + +Multiplicating two `VLSpec` creates a new `VLSpec` as a composition of the two specifications. +Properties defined in `spec2` have precedence over `spec1`, meaning that if a given property +is specified in both then the result specification will use the property from `spec2`. + +# Examples +some examples +""" +Base.:*(a::VLSpec, b::VLSpec) = Base.:*(VLSpecTyped(a), VLSpecTyped(b)) +Base.:*(a::MultiView, b::MultiView) = error("Two multi-view specs ($(typeof(a)), $(typeof(b))) can not be composed") +Base.:*(a::VLSpecTyped, b::MultiView) = b * Vega.deletedata(a) +function Base.:*(a::MultiView, b::VLSpecTyped) + a_spec = deepcopy(Vega.getparams(a.spec)) + b_spec = deepcopy(Vega.getparams(b.spec)) + if haskey(b_spec, "data") + a_spec["data"] = b_spec["data"] + delete!(b_spec, "data") + end + if haskey(a_spec, "spec") + b_spec = _compose!(a_spec["spec"], b_spec) + end + a_spec["spec"] = b_spec + return VLSpec(a_spec) +end +Base.:*(a::ConcatSpecs, b::VLSpecTyped) = _compose(a, b) # FIXME: do something smarter +Base.:*(a::VLSpecTyped, b::VLSpecTyped) = _compose(a, b) + +function _compose(a::VLSpec, b::VLSpec) + a_spec = deepcopy(Vega.getparams(a.spec)) + b_spec = deepcopy(Vega.getparams(b.spec)) + return VLSpec(_compose!(a_spec, b_spec)) +end +function _compose!(a::OrderedDict, b::OrderedDict) + for (k, v) in b + if haskey(a, k) && a[k] isa OrderedDict && b[k] isa OrderedDict + _compose!(a[k], b[k]) + else + a[k] = b[k] + end + end + return a +end + function Base.hcat(A::VLSpec...) spec = VLSpec(OrderedDict{String,Any}()) Vega.getparams(spec)["hconcat"] = [] @@ -185,6 +242,3 @@ function Base.vcat(A::VLSpec...) end return promote_data_to_toplevel(spec, "vconcat") end - -#function Base.hvcat(ncols, A::VLSpec...) -#end From 9b88f1c2c4adc1677e18b596333882a534f3fdfd Mon Sep 17 00:00:00 2001 From: brucala Date: Sun, 7 Nov 2021 16:14:50 +0100 Subject: [PATCH 5/7] improved layering, improved composing, smarter data promotion --- src/vlspec.jl | 94 +++++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 99d3a2e1..9770c94b 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -55,7 +55,7 @@ function add_encoding_types(specdict, parentdata=nothing) end function our_json_print(io, spec::VLSpec) - JSON.print(io, add_encoding_types(Vega.getparams(spec))) + JSON.print(io, add_encoding_types(Vega.getparams(promote_data_to_toplevel(spec)))) end function (p::VLSpec)(data) @@ -110,26 +110,8 @@ Create a copy of `spec` without data. See also [`Vega.deletedata!`](@ref). """ Vega.deletedata(spec::VLSpec) = Vega.deletedata!(copy(spec)) -""" -Remove potentially duplicated data in composed subspecs and promote it to the top level spec -""" -function promote_data_to_toplevel(spec::VLSpec, key) - specdict = deepcopy(Vega.getparams(spec)) - (key in ("layer", "concat", "vconcat", "hconcat") && haskey(specdict, key)) || return specdict - parentdata = get(specdict, "data", nothing) - for subspec in specdict[key] - haskey(subspec, "data") || continue - data = subspec["data"] - if isnothing(parentdata) - parentdata = data - specdict["data"] = parentdata - end - parentdata == data && delete!(subspec, "data") - end - return VLSpec(specdict) -end - abstract type VLSpecTyped end +struct PropertySpec <: VLSpecTyped spec::VLSpec end struct SingleView <: VLSpecTyped spec::VLSpec end abstract type ComposedView <: VLSpecTyped end struct Layer <: ComposedView spec::VLSpec end @@ -141,6 +123,11 @@ struct HConcat <: MultiView spec::VLSpec end struct VConcat <: MultiView spec::VLSpec end const ConcatSpecs = Union{Concat, HConcat, VConcat} +const FacetOrRepeat = Union{Facet, Repeat} +const SingleOrLayeredView = Union{SingleView, Layer} + +_key(::FacetOrRepeat) = "spec" +_key(::T) where T<:ComposedView = split(lowercase(string(T)), ".")[end] function VLSpecTyped(spec::VLSpec) specdict = Vega.getparams(spec) @@ -150,7 +137,8 @@ function VLSpecTyped(spec::VLSpec) haskey(specdict, "concat") && return Concat(spec) haskey(specdict, "hconcat") && return HConcat(spec) haskey(specdict, "vconcat") && return VConcat(spec) - return SingleView(spec) + haskey(specdict, "mark") && return SingleView(spec) + return PropertySpec(spec) end Layer(spec::SingleView) = Layer(VLSpec(OrderedDict{String,Any}("layer" => [deepcopy(Vega.getparams(spec.spec))]))) @@ -166,20 +154,26 @@ for example [l1, l2] + [l3, l4] will become [l1, l2, l3, l4] instead of [[l1, l2 Multi-view specs (facet, repeat, concat) cannot be layered. """ Base.:+(a::VLSpec, b::VLSpec) = Base.:+(VLSpecTyped(a), VLSpecTyped(b)) -Base.:+(a::MultiView, b::VLSpecTyped) = error("Multi-view spec $(typeof(a)) can not be layered") -Base.:+(a::VLSpecTyped, b::MultiView) = error("Multi-view spec $(typeof(b)) can not be layered") -Base.:+(a::MultiView, b::MultiView) = error("Multi-view spec $(typeof(a)) can not be layered") +Base.:+(a::MultiView, b::VLSpecTyped) = multiview_layering_error(a) +Base.:+(a::VLSpecTyped, b::MultiView) = multiview_layering_error(b) +Base.:+(a::MultiView, b::MultiView) = multiview_layering_error(a) +Base.:+(a::PropertySpec, b::VLSpecTyped) = nonview_layering_error() +Base.:+(a::VLSpecTyped, b::PropertySpec) = nonview_layering_error() +Base.:+(a::PropertySpec, b::PropertySpec) = nonview_layering_error() Base.:+(a::VLSpecTyped, b::VLSpecTyped) = error("Layering not implemented for $(typeof(a)) + $(typeof(b))") function Base.:+(a::Layer, b::Layer) a_spec = deepcopy(Vega.getparams(a.spec)) b_spec = deepcopy(Vega.getparams(b.spec)) append!(a_spec["layer"], b_spec["layer"]) - return promote_data_to_toplevel(VLSpec(a_spec), "layer") + return VLSpec(a_spec) end Base.:+(a::SingleView, b::SingleView) = Layer(a) + Layer(b) Base.:+(a::SingleView, b::Layer) = Layer(a) + b Base.:+(a::Layer, b::SingleView) = a + Layer(b) +multiview_layering_error(a) = error("Multi-view spec $(typeof(a)) can not be layered") +nonview_layering_error() = error("Not possible to layer specs. Make sure they are single views with the \"mark\" property or layered views") + """ spec1::VLSpec * spec2::VLSpec @@ -191,27 +185,26 @@ is specified in both then the result specification will use the property from `s some examples """ Base.:*(a::VLSpec, b::VLSpec) = Base.:*(VLSpecTyped(a), VLSpecTyped(b)) -Base.:*(a::MultiView, b::MultiView) = error("Two multi-view specs ($(typeof(a)), $(typeof(b))) can not be composed") -Base.:*(a::VLSpecTyped, b::MultiView) = b * Vega.deletedata(a) -function Base.:*(a::MultiView, b::VLSpecTyped) - a_spec = deepcopy(Vega.getparams(a.spec)) +Base.:*(a::MultiView, b::MultiView) = error("Two multi-view specs (facet, repeat, concat) can not be composed. Tried to compose ($(typeof(a)), $(typeof(b)))") +Base.:*(a::Layer, b::SingleOrLayeredView) = error("A layered view can not be composed with a single of layered view ($(typeof(a)), $(typeof(b)))") +Base.:*(a::SingleOrLayeredView, b::Layer) = error("A layered view can not be composed with a single of layered view ($(typeof(a)), $(typeof(b)))") +Base.:*(a::SingleOrLayeredView, b::FacetOrRepeat) = *(b, a; b_precedence=false) +function Base.:*(a::FacetOrRepeat, b::SingleOrLayeredView, b_precedence=true) + a_spec = deepcopy(Vega.getparams(a.spec)) b_spec = deepcopy(Vega.getparams(b.spec)) - if haskey(b_spec, "data") - a_spec["data"] = b_spec["data"] - delete!(b_spec, "data") - end if haskey(a_spec, "spec") - b_spec = _compose!(a_spec["spec"], b_spec) + b_spec = b_precedence ? + Vega.getparams(VLSpec(a_spec["spec"]) * VLSpec(b_spec)) : + Vega.getparams(VLSpec(b_spec) * VLSpec(a_spec["spec"])) end a_spec["spec"] = b_spec return VLSpec(a_spec) end -Base.:*(a::ConcatSpecs, b::VLSpecTyped) = _compose(a, b) # FIXME: do something smarter -Base.:*(a::VLSpecTyped, b::VLSpecTyped) = _compose(a, b) +Base.:*(a::VLSpecTyped, b::VLSpecTyped) = _compose(a.spec, b.spec) function _compose(a::VLSpec, b::VLSpec) - a_spec = deepcopy(Vega.getparams(a.spec)) - b_spec = deepcopy(Vega.getparams(b.spec)) + a_spec = deepcopy(Vega.getparams(a)) + b_spec = deepcopy(Vega.getparams(b)) return VLSpec(_compose!(a_spec, b_spec)) end function _compose!(a::OrderedDict, b::OrderedDict) @@ -231,7 +224,7 @@ function Base.hcat(A::VLSpec...) for i in A push!(Vega.getparams(spec)["hconcat"], deepcopy(Vega.getparams(i))) end - return promote_data_to_toplevel(spec, "hconcat") + return spec end function Base.vcat(A::VLSpec...) @@ -240,5 +233,26 @@ function Base.vcat(A::VLSpec...) for i in A push!(Vega.getparams(spec)["vconcat"], deepcopy(Vega.getparams(i))) end - return promote_data_to_toplevel(spec, "vconcat") + return spec +end + +""" +Remove potentially duplicated data in composed subspecs and promote it to the top level spec +""" +promote_data_to_toplevel(spec::VLSpec) = promote_data_to_toplevel!(VLSpecTyped(VLSpec(Vega.getparams(spec)))) +function promote_data_to_toplevel!(spec::VLSpecTyped, toplevel_data=nothing) + specdict = Vega.getparams(spec.spec) + haskey(specdict, "data") && toplevel_data == specdict["data"] && delete!(specdict, "data") + return VLSpec(specdict) +end +function promote_data_to_toplevel!(spec::ComposedView, toplevel_data=nothing) + specdict = Vega.getparams(spec.spec) + parentdata = get(specdict, "data", nothing) + !isnothing(parentdata) && toplevel_data == parentdata && delete!(specdict, "data") + key = _key(spec) + subspec_iter = key == "spec" ? [specdict[key]] : specdict[key] + for subspec in subspec_iter + promote_data_to_toplevel!(VLSpecTyped(VLSpec(subspec)), parentdata) + end + return VLSpec(specdict) end From f105a2591d822685bed5162981435e8556288ac9 Mon Sep 17 00:00:00 2001 From: brucala Date: Tue, 9 Nov 2021 08:51:49 +0100 Subject: [PATCH 6/7] add composing rule for single*layer spec, fix data promotion --- src/vlspec.jl | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 9770c94b..4315e4c2 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -110,8 +110,11 @@ Create a copy of `spec` without data. See also [`Vega.deletedata!`](@ref). """ Vega.deletedata(spec::VLSpec) = Vega.deletedata!(copy(spec)) +##### +##### Layering and composition +##### + abstract type VLSpecTyped end -struct PropertySpec <: VLSpecTyped spec::VLSpec end struct SingleView <: VLSpecTyped spec::VLSpec end abstract type ComposedView <: VLSpecTyped end struct Layer <: ComposedView spec::VLSpec end @@ -124,6 +127,7 @@ struct VConcat <: MultiView spec::VLSpec end const ConcatSpecs = Union{Concat, HConcat, VConcat} const FacetOrRepeat = Union{Facet, Repeat} +const LayerOrConcat = Union{Layer, ConcatSpecs} const SingleOrLayeredView = Union{SingleView, Layer} _key(::FacetOrRepeat) = "spec" @@ -137,8 +141,7 @@ function VLSpecTyped(spec::VLSpec) haskey(specdict, "concat") && return Concat(spec) haskey(specdict, "hconcat") && return HConcat(spec) haskey(specdict, "vconcat") && return VConcat(spec) - haskey(specdict, "mark") && return SingleView(spec) - return PropertySpec(spec) + return SingleView(spec) end Layer(spec::SingleView) = Layer(VLSpec(OrderedDict{String,Any}("layer" => [deepcopy(Vega.getparams(spec.spec))]))) @@ -157,9 +160,6 @@ Base.:+(a::VLSpec, b::VLSpec) = Base.:+(VLSpecTyped(a), VLSpecTyped(b)) Base.:+(a::MultiView, b::VLSpecTyped) = multiview_layering_error(a) Base.:+(a::VLSpecTyped, b::MultiView) = multiview_layering_error(b) Base.:+(a::MultiView, b::MultiView) = multiview_layering_error(a) -Base.:+(a::PropertySpec, b::VLSpecTyped) = nonview_layering_error() -Base.:+(a::VLSpecTyped, b::PropertySpec) = nonview_layering_error() -Base.:+(a::PropertySpec, b::PropertySpec) = nonview_layering_error() Base.:+(a::VLSpecTyped, b::VLSpecTyped) = error("Layering not implemented for $(typeof(a)) + $(typeof(b))") function Base.:+(a::Layer, b::Layer) a_spec = deepcopy(Vega.getparams(a.spec)) @@ -172,7 +172,6 @@ Base.:+(a::SingleView, b::Layer) = Layer(a) + b Base.:+(a::Layer, b::SingleView) = a + Layer(b) multiview_layering_error(a) = error("Multi-view spec $(typeof(a)) can not be layered") -nonview_layering_error() = error("Not possible to layer specs. Make sure they are single views with the \"mark\" property or layered views") """ spec1::VLSpec * spec2::VLSpec @@ -186,10 +185,23 @@ some examples """ Base.:*(a::VLSpec, b::VLSpec) = Base.:*(VLSpecTyped(a), VLSpecTyped(b)) Base.:*(a::MultiView, b::MultiView) = error("Two multi-view specs (facet, repeat, concat) can not be composed. Tried to compose ($(typeof(a)), $(typeof(b)))") -Base.:*(a::Layer, b::SingleOrLayeredView) = error("A layered view can not be composed with a single of layered view ($(typeof(a)), $(typeof(b)))") -Base.:*(a::SingleOrLayeredView, b::Layer) = error("A layered view can not be composed with a single of layered view ($(typeof(a)), $(typeof(b)))") +Base.:*(a::Layer, b::Layer) = error("Two layered specs can not be composed") +Base.:*(a::SingleView, b::LayerOrConcat, b_precedence=true) = *(b, a; b_precedence=false) +function Base.:*(a::LayerOrConcat, b::SingleView; b_precedence=true) + a_spec = deepcopy(Vega.getparams(a.spec)) + b_spec = deepcopy(Vega.getparams(b.spec)) + new_layer = OrderedDict{String, Any}[] + for subspec in a_spec[_key(a)] + new_subspec = b_precedence ? + Vega.getparams(VLSpec(subspec) * VLSpec(b_spec)) : + Vega.getparams(VLSpec(b_spec) * VLSpec(subspec)) + push!(new_layer, new_subspec) + end + a_spec[_key(a)] = new_layer + return VLSpec(a_spec) +end Base.:*(a::SingleOrLayeredView, b::FacetOrRepeat) = *(b, a; b_precedence=false) -function Base.:*(a::FacetOrRepeat, b::SingleOrLayeredView, b_precedence=true) +function Base.:*(a::FacetOrRepeat, b::SingleOrLayeredView; b_precedence=true) a_spec = deepcopy(Vega.getparams(a.spec)) b_spec = deepcopy(Vega.getparams(b.spec)) if haskey(a_spec, "spec") @@ -239,7 +251,7 @@ end """ Remove potentially duplicated data in composed subspecs and promote it to the top level spec """ -promote_data_to_toplevel(spec::VLSpec) = promote_data_to_toplevel!(VLSpecTyped(VLSpec(Vega.getparams(spec)))) +promote_data_to_toplevel(spec::VLSpec) = promote_data_to_toplevel!(VLSpecTyped(VLSpec(deepcopy(Vega.getparams(spec))))) function promote_data_to_toplevel!(spec::VLSpecTyped, toplevel_data=nothing) specdict = Vega.getparams(spec.spec) haskey(specdict, "data") && toplevel_data == specdict["data"] && delete!(specdict, "data") @@ -252,6 +264,11 @@ function promote_data_to_toplevel!(spec::ComposedView, toplevel_data=nothing) key = _key(spec) subspec_iter = key == "spec" ? [specdict[key]] : specdict[key] for subspec in subspec_iter + data = get(subspec, "data", nothing) + if !isnothing(data) && !haskey(specdict, "data") && data != toplevel_data + specdict["data"] = parentdata = data + delete!(subspec, "data") + end promote_data_to_toplevel!(VLSpecTyped(VLSpec(subspec)), parentdata) end return VLSpec(specdict) From e189e7609f6816ed3984e24258270623f2158166 Mon Sep 17 00:00:00 2001 From: brucala Date: Tue, 9 Nov 2021 09:07:32 +0100 Subject: [PATCH 7/7] add composing example --- src/vlspec.jl | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/vlspec.jl b/src/vlspec.jl index 4315e4c2..2e5838bf 100644 --- a/src/vlspec.jl +++ b/src/vlspec.jl @@ -177,11 +177,28 @@ multiview_layering_error(a) = error("Multi-view spec $(typeof(a)) can not be lay spec1::VLSpec * spec2::VLSpec Multiplicating two `VLSpec` creates a new `VLSpec` as a composition of the two specifications. +For instance, `vlplot(:bar) * vlplot(x=:x)` will be equivalent to `vlplot(:bar, x=:x)`. Properties defined in `spec2` have precedence over `spec1`, meaning that if a given property is specified in both then the result specification will use the property from `spec2`. -# Examples -some examples +# Example + +using VegaLite, VegaDatasets + +data = dataset("weather.csv") |> vlplot(); +rep = @vlplot(repeat={column=[:temp_max,:precipitation,:wind]}); +common = @vlplot( + :line, + y={field={repeat=:column},aggregate=:mean,type=:quantitative}, + x="month(date):o", + color=:location +); +details = @vlplot( + detail="year(date)", + opacity={value=0.2} +); + +data * rep * common * (details + vlplot()) """ Base.:*(a::VLSpec, b::VLSpec) = Base.:*(VLSpecTyped(a), VLSpecTyped(b)) Base.:*(a::MultiView, b::MultiView) = error("Two multi-view specs (facet, repeat, concat) can not be composed. Tried to compose ($(typeof(a)), $(typeof(b)))")