Skip to content

Plot Object Redesign

jkrumbiegel edited this page Nov 17, 2020 · 4 revisions

Plot Objects

Plot Input Object

  • 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

Internal Plot object representation

  • 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
Clone this wiki locally