Description
I just got this idea which might allow us to create a universal model of the store model @clewis7 wrote. Not fully thought out but a starting point.
- Every publisher can be a subscriber, and every subscriber can be a publisher.
- The
StoreModel
receives or emits values of a certain type, call it the "store-data type". - Publishers register not only a callback and the event type, but also a function that maps the callback output to the store-data type.
- Subscribers register a callback function which takes the subscriber and the store-data type.
Examples:
Sync multiple selectors:
def selection_changed(ev) -> float:
# returns data type used by subscriber function set_selection
return ev.selection
def set_selection(selector, new_value: float):
# new_value is returned from selection_changed and used here
selector.selection = new_value
for selector in [sel1, sel2, sel3]:
# the first arg must be an object with an `add_event_handler` method
store_model.add_publisher(selector, selection_changed, "selection")
store_model.subscribe(selector, set_selection)
How this works internally:
# `StoreModel.add_publisher(obj, func_map_to_store, event_types)`
obj.add_event_handler(partial(StoreModel.notify_subscribers, obj, func_map_to_store), *event_types)
# `StoreModel.add_subscriber(obj, func_map_from_store)`
self._subscribers.add((obj, func_map_from_store))
# called by graphic/renderer when event happens
# `StoreModel.notify_subscribers(event_source, func_map_to_store, ev)`
new_value = func_map_to_store(ev)
# block events for all graphics
graphics = (sub[0] for sub in self.subscribers)
# call all subscribers
with pause_events(*graphics):
for subscriber in self.subscribers:
# unpack tuple
obj, func = subscriber
# skip if event source is this subscriber object
if obj is event_source:
continue
# call subscriber func
func(obj, new_value)
This idea can also be extended to have complex bidirectional events across event types since the functions allow to map to a central store-data-type and then inverse map to the subscriber function.
Decorator syntax would be nice here 😄
@store_model.add_publisher([sel1, sel2, sel3], "selection")
def selection_changed(ev) -> float:
# returns data type used by subscriber function set_selection
return ev.selection
@store_model.subscribe([sel1, sel2, sel3])
def set_selection(selector, new_value: float):
# new_value is returned from selection_changed and used here
selector.selection = new_value
What I like about this idea is that the inverse mapping from the store model data can be anything and they are easy to define:
# change image data using the store model
@store_model.subscribe([ig1, ig2, ig3])
def set_data(image_graphic, new_value: float): # this store model only sends floats to subscribers
vid = videos[image_graphic.name]
image_graphic.data = vid[int(new_value)]
# update color at an index in a scatter or line graphic
@store_model.subscribe([sg1, sg2])
def scatter_index(scatter_graphic, new_value: float):
reset_colors(scatter_graphic)
scatter_graphic.colors[new_value] = "w"
Maybe we can even have StoreModel.value
which is settable, allowing things from other libraries or user objects to interface with the StoreModel
, such as ipywidgets
or qt widgets.
StoreModel.value
would just return the most recently set value to the store, which is of the data type that this store uses. When the value
is set it just calls all the subscriber functions and gives them the new value.
We could have a StoreModel.add_event_handler()
to allow registering arbitrary callbacks to external objects (such as an ipywidget object, or any external user object). Making these bidirectional through our StoreModel
would be out of scope, it would have to be done on their end.
Should make use of our Graphic.deleted
event to remove any subscriber/publisher when the graphic is deleted.