observablehq / observablehq/plot

updating in place?

Open
#1,022 4 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

question
Dominant language
HTML
Stars
5.4k
Forks
244
PR merge metrics
No merged PRs in 30d

Description

If we want to update the marks in place in the DOM rather than throw them away and create a new one on each tick (in the context of the time animations, and later interactions), we can pass the previous node as part of the context (as in the patch below), and use join rather than append.

(And then, why not go further and always pass a g element, on any call to render — that is, independently of whether it's a previously rendered node or a new one just created with create("svg:g", context)? The mark would still be free to ignore it and return something else, of course.)

diff --git a/src/marks/dot.js b/src/marks/dot.js
index e4140b55..3442267f 100644
--- a/src/marks/dot.js
+++ b/src/marks/dot.js
@@ -1,4 +1,4 @@
-import {path, symbolCircle} from "d3";
+import {path, select, symbolCircle} from "d3";
 import {create} from "../context.js";
 import {positive} from "../defined.js";
 import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../options.js";
@@ -55,13 +55,12 @@ export class Dot extends Mark {
     const {x: X, y: Y, r: R, rotate: A, symbol: S} = channels;
     const [cx, cy] = applyFrameAnchor(this, dimensions);
     const circle = this.symbol === symbolCircle;
-    return create("svg:g", context)
+    return (context.node ? select(context.node) : create("svg:g", context)
         .call(applyIndirectStyles, this, scales, dimensions)
-        .call(applyTransform, this, scales)
-        .call(g => g.selectAll()
+        .call(applyTransform, this, scales))
+        .call(g => g.selectAll(circle ? "circle" : "path")
           .data(index, i => i)
-          .enter()
-          .append(circle ? "circle" : "path")
+          .join(circle ? "circle" : "path")
             .call(applyDirectStyles, this)
             .call(circle
               ? selection => {
diff --git a/src/plot.js b/src/plot.js
index 51864c40..6a9ec847 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -376,7 +376,7 @@ export function plot(options = {}) {
           }
           const ifacet = [...facet.filter(i => T[i] < time1), ...(currentTime < time1) ? Ii : [], ...facet.filter(i => T[i] >= time1)];
           const index = mark.timeFilter(ifacet, T, currentTime);
-          timeNode = mark.render(index, scales, interp, dimensions, context);
+          timeNode = mark.render(index, scales, interp, dimensions, {...context, node: timeMark.node});
         } else {
           // here typically t = 0;
           const index = mark.timeFilter(facet, T, currentTime);

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the proposed changes in src/marks/dot.js and src/plot.js, focusing on passing context.node into mark.render and using selection.join. Compare the two suggested behaviors—reusing the previous node or always providing a g element—and clarify which render contract should be adopted. Done means the chosen behavior is implemented consistently for time animations and later interactions.

Written by the indexing model from the issue text.

Assessment

Tech stack
d3js, javascript
Domain
data-visualization
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.