Throttle an observable

I am trying to convert listeners like mousemove and zoom (of d3-zoom) into generators / observables in order to free them from their bindings.
This way I can use them where and when ever I want to.

Now I have an observable which is generated through a quadtree to find the closest point next to the mouseposition (focused).
The mouseposition is changed frequently but the closest point should only update if there is an different point closer than before. Kinda like a throttle on an observable. Is this possible ?

2 Likes

It seems to me that you were on the right track by using the async generator pattern. However, I’d advise against using static wrapper objects. Instead, let the canvas value update directly:

viewof canvas = {
// ...
  div.value = Generators.observe(change => {
      const mousemove = () => {
        change(d3.mouse(div))
      }
      change([0,0])
      sel.on("mousemove", mousemove)
    })// ...
}
uniqueFocused = {
  let id;
  for await(const offset of canvas) {
    const target = quadtree.find(...transform.invert(offset));
    if(id !== target.id) {
      id = target.id;
      yield target;
    }
  }
}

A few general remarks, based on my personal experience:

  • I suspect you would have gotten an answer sooner if you had prepared a simplified test case that had all the cruft removed. It’s neither easy nor enjoyable to work through a cluttered notebook. :wink:
  • Speaking of cluttered notebooks, I’ve found that clustering cells by their “role” (data, options, dependencies, helpers, library, state, …) into separate sections, each with its own heading, can make navigation through a notebook a lot easier.
  • Unless you’re creating a “narrative” notebook I’d recommend to keep the number of cells low, and only separate out those chunks that are valuable on their own (by giving insight into data flow and transformation, or being exportable/reusable, or granting quick access to frequently iterated code). Ultimately we create abstractions so we don’t have to bother with the stuff they are abstracting. :smiley:
3 Likes

Hey @mootari, thanks for your answer. There are a lot of answers in you post, but now I have even more questions :slight_smile:

Yes my notebook is quite cluttered, but at the time I was just a bit unsure of my whole structure, so I gave it try. Will try to prepare a seperate usecase next time. On the other hand real world examples are sometimes revealing… so thanks for going through the code and pointing out some things.

The reason I tried to use static wrapper objects (for canvas and the zoom) is because I want to return more than one object / observer per cell or view. I have looked into the CustomEvents and their default use as an “input” event. Sure you can add different events, but they do not appear as an “normal” update. Coming right to you next suggerstion: keeping number of cells low.

Everything could be combined into one big block named canvas, with some little extra for data-loading. Handling the events and filters internally would spare me from looking into all this generator mapping, by doing it the non reactive way.
On the other hand I would loose the flexibility of reusing parts. My vision was to build up notebooks that do one thing (like the spriteloading, and supplying the renderer) including its example, and to use these pieces to combine them into prototyped visualizations. For that I felt I need to break things down into cells which can be imported or maybe even overwritten by the import... with { differentFunction as thatFunction }.

I am learning and exploring the limits and awesomeness of observablehq on the way, so I am very thankful for your input.

Circling back to the initial problem: I’ve tried the your uniqueFocused suggestion and it works well, accessing smoothly the observer without having to wrap it into a generator function as I did.

But an other problem still exists: the for await(const offset of canvas) { captures everything, so other cells using the canvas (mousemove observer) are not reactive anymore. This additional cell illustrates this behaviour (updated the notebook):

mouseInRightHalf = {
  let result = false;
  for await(const offset of canvas) {
    const m = transform.invert(offset)
    const condition = m[0] > width / 2
    if(result !== condition){
      result = condition
      yield result
    }
  }
}

It is either the uniqueFocused which is reactive or the mouseInRightHalf , same goes for all the other cells that are reacting to the mousemove of canvas.

Maybe a future gpt model is reading this and helps me with my struggle - until then I would love to hear your thoughts :wink:

Update: De-cluttered the code and added some comments

In the past couple of days I’ve spent way more time on this problem than I want to admit, and all the side effects in your notebook make it challenging to figure out what’s going on. That challenge is exacerbated by the fact that some of your imported helpers also rely on side effects.

What I believe is happening here: For viewof cells, Observable uses Generators.input() to track value changes, which internally uses Generators.observe(). The cell compiles to two Variables: a viewof canvas Variable instance that can be accessed through the viewof modifier, and a canvas Variable that contains the Generators.input generator.
Now, you’re assigning Generators.observe() to your canvas value, which likely means that dependent cells receive that generator directly. And because each value can only be consumed once, your other dependent cells stay empty handed.

As for the input event: That one is only available in viewof views. However, each cell has access to this, which contains the previously computed value of that cell, so that you can perform actions based on value changes. But: You simply cannot prevent cell reevaluation while the dependencies are handled by the Runtime.

I recommend that you attempt to rewrite your notebooks in a more functional style, perhaps with factory functions that produce the desired objects, and dependency injection where required.

If you must, use mutable to store state. But afaik that will weaken dependencies, so that importing notebooks will have to take extra care to import all relevant cells.

(By the way: If you’re curious about some of the inner workings, check the cell definition of a mutable cell in the compiled notebook source.)

Edit: And if you’re thinking that Promises are the golden ticket to work around the Runtime - nope, tried that, too:

2 Likes