-
-
Notifications
You must be signed in to change notification settings - Fork 314
Plot Object Redesign
jkrumbiegel edited this page Nov 17, 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