-
-
Notifications
You must be signed in to change notification settings - Fork 315
Plot Object Redesign
jkrumbiegel edited this page Nov 18, 2020
·
4 revisions
- absolutely leightweight
- pretty much just wraps user input
- only contains values, no observables
- must work well as PlotInput(observable) and Observable(PlotInput(value))
- can be used everywhere e.g. in recipes, has no real dependencies
- can be easily serialized as JSON
- has just one Observable, that manages inputs/outputs
- is purely for enabling high level API & recipes
struct PlotInput
# name of targeted plot function
# maybe need to be a type paramter to implement conversion functions
# to lower level objects
name::Symbol
attributes::Dict{Symbol, Any}
on_update::Observable{Pair{Symbol, Any}}
on_update_callbacks::Dict{Symbol, Set{Function}}
connections::Dict{Symbol, Observables.ObserverFunction}
function PlotInput(name::Symbol, attributes::Dict{Symbol, Any})
on_update = Observable{Pair{Symbol, Any}}()
on_update_callbacks = Dict{Symbol, Set{Function}}()
connections = Dict{Symbol, Observables.ObserverFunction}()
on(on_update) do (name, value)
# on update callbacks is for things like `on(plot, :mouseposition)`
# but can also be used to register for attribute field updates
if haskey(on_update_callbacks, name)
callbacks = on_update_callbacks[name]
for callback in callbacks
Base.invokelatest(callback, value)
end
end
if haskey(attributes, name)
# If it is an attribute field, we update it!
attributes[name] = value
end
end
return new(name, attributes, on_update, on_update_callbacks, connections)
end
end
"""
Convenience for constructing PlotObject:
@plot scatter(1:3, color=10) == PlotObject(:scatter, 1:3; color=10)
"""
macro plot(expr)
...
end
function PlotObject(name; kw...)
end
function register_observable!(plot::PlotInput, (name, observable)::Pair{Symbol, Observable})
func = on(observable) do value
plot.on_update[] = name => value
end
# update value on first time
attributes[name] = observable[]
plot.connections[name] = func
return
end
function disconnect_observable!(plot::PlotInput, (name, observable)::Pair{Symbol, Observable})
off(plot.on_update, plot.connections[name])
return
end
function on(f::Function, plot::PlotInput, name::Symbol)
callbacks = get(plot.on_update_callbacks, name, Set{Function}())
push!(callbacks, f)
return
end
function disconnect!(plot::PlotInput)
for (name, func) in plot.connections
off(plot.on_update, func)
end
empty!(plot.on_update_callbacks)
end
"""
flatten_plotobject(plot_observable::Observable{PlotObject})
Converts an `Observable{PlotObject}` to a `PlotObject` that updates whenever `plot_observable` updates.
"""
function flatten_plotobject(plot_observable::Observable{PlotObject})
plot = copy(plot_observable[])
on(plot_observable) do new_plot
for (name, value) in new_plot
# do some lightweight diffing
if plot[name] != value
plot[name] = value
end
end
end
return plot
end
- concretly typed
- fully converted
- directly digestable by backends
- well defined API for backends
- a recipe doesn't do anything but convert some type to a number of internal plot objects
- conversion to backend plot object is done optionally by backend, so a backend can overload plot objects at any level
- objects should be as simple and non magical as possible
- conversion/recipe pipeline should look something like this:
while !is_supported(backend, plotobject)
plotobject = apply_recipe(plotobject)
end
draw_object(backend, plotobject)
Example in code:
struct Image
bounds::Tuple{Interval, Interval}
data::AbstractMatrix{T <: Colorant}
end
struct MeshPlot
mesh::Mesh
end
to_sampler(image::AbstractMatrix{<: Colorant}) = image
function to_sampler(image::PlotObject)
if haskey(image, :colormap)
to_sampler(image.data, image.colormap)
else
to_sampler(image.data)
end
end
function recipe(image::PlotObject, ::Val{:image})
bounds = (to_interval(image.x), to_interval(image.y))
return Image(bounds, to_sampler(image))
end
function recipe(image::Image)
xy_min_max = extrema.(image.bounds)
xy = first.(xy_min_max)
widths = last.(xy_min_max) .- xy
bound_rect = Rect(xy, widths)
uv = texturecoordinates(bound_rect)
positions = coordinates(bound_rect)
color = sampler(image.data, uv)
return Mesh(meta(positions, color=color), faces(bound_rect))
end
How are plot objects updated if they only have one observable? What kind of signal does an update generate and how is it picked up by other components?
Can multiple attributes be updated simultaneously to avoid the problem of e.g. array lengths going out of sync?
Maybe one can do it by sending updates via one observable with a Dict of named attributes?
plot.attributes = Dict(:x => randn(100), :y => randn(100))
update!(plot, :x => randn(200), :y => randn(200))
function update!(plot, symbolpairs...)
# do something depending on which symbols were sent?
# I'm imagining something like `lift` but where only one Dict is lifted and
# then it's checked whether the changed symbols actually apply to this `lift`
end
# for example
symbollift(dict_observable, :x, :y, :z) do x, y, z
# is called whenever some subset of {:x, :y, :z} is changed
end