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

Composition update proposal #411

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
205 changes: 175 additions & 30 deletions src/vlspec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,33 @@ 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
new_x["type"] = "nominal"
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
)
Expand All @@ -55,12 +55,12 @@ 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)
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)
Expand Down Expand Up @@ -110,22 +110,141 @@ 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"] = []
end
push!(new_spec["layer"], deepcopy(Vega.getparams(b)))
#####
##### Layering and composition
#####

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

const ConcatSpecs = Union{Concat, HConcat, VConcat}
const FacetOrRepeat = Union{Facet, Repeat}
const LayerOrConcat = Union{Layer, ConcatSpecs}
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)
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

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::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::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 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")

"""
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`.

# 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)))")
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)
a_spec = deepcopy(Vega.getparams(a.spec))
b_spec = deepcopy(Vega.getparams(b.spec))
if haskey(a_spec, "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::VLSpecTyped, b::VLSpecTyped) = _compose(a.spec, b.spec)

return VLSpec(new_spec)
function _compose(a::VLSpec, b::VLSpec)
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)
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...)
Expand All @@ -138,10 +257,36 @@ function Base.hcat(A::VLSpec...)
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 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(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")
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
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)
end