diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js
index f2cf4a7271..72f1b2d7a8 100644
--- a/src/interactions/pointer.js
+++ b/src/interactions/pointer.js
@@ -22,6 +22,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
// response to pointer events.
render: composeRender(function (index, scales, values, dimensions, context, next) {
const svg = context.ownerSVGElement;
+ const {data} = context.getMarkState(this);
// Isolate state per-pointer, per-plot; if the pointer is reused by
// multiple marks, they will share the same state (e.g., sticky modality).
@@ -118,8 +119,9 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
}
g.replaceWith(r);
}
- state.roots[renderIndex] = r;
- return (g = r);
+ state.roots[renderIndex] = g = r;
+ context.dispatchValue(i == null ? null : data[i]);
+ return r;
}
function pointermove(event) {
diff --git a/src/plot.d.ts b/src/plot.d.ts
index df8b680085..fbf407d806 100644
--- a/src/plot.d.ts
+++ b/src/plot.d.ts
@@ -370,6 +370,9 @@ export interface Plot {
* scales.
*/
legend(name: ScaleName, options?: LegendOptions): SVGSVGElement | HTMLElement | undefined;
+
+ /** For interactive plots, the current value. */
+ value?: any;
}
/**
diff --git a/src/plot.js b/src/plot.js
index 60d4f53269..bf9b5020ed 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -153,6 +153,7 @@ export function plot(options = {}) {
const context = createContext(options);
const document = context.document;
const svg = creator("svg").call(document.documentElement);
+ let figure = svg; // replaced with the figure element, if any
context.ownerSVGElement = svg;
context.className = className;
context.projection = createProjection(options, subdimensions);
@@ -169,6 +170,13 @@ export function plot(options = {}) {
return {...state, channels: {...state.channels, ...facetState?.channels}};
};
+ // Allows e.g. the pointer transform to support viewof.
+ context.dispatchValue = (value) => {
+ if (figure.value === value) return;
+ figure.value = value;
+ figure.dispatchEvent(new Event("input", {bubbles: true}));
+ };
+
// Reinitialize; for deriving channels dependent on other channels.
const newByScale = new Set();
for (const [mark, state] of stateByMark) {
@@ -311,7 +319,6 @@ export function plot(options = {}) {
}
// Wrap the plot in a figure with a caption, if desired.
- let figure = svg;
const legends = createLegends(scaleDescriptors, context, options);
if (caption != null || legends.length > 0) {
figure = document.createElement("figure");
diff --git a/test/output/pointerViewof.html b/test/output/pointerViewof.html
new file mode 100644
index 0000000000..decf83aef9
--- /dev/null
+++ b/test/output/pointerViewof.html
@@ -0,0 +1,403 @@
+
+
+
\ No newline at end of file
diff --git a/test/plots/pointer.ts b/test/plots/pointer.ts
index 158b1378d4..d6ea70904a 100644
--- a/test/plots/pointer.ts
+++ b/test/plots/pointer.ts
@@ -1,5 +1,6 @@
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
+import {html} from "htl";
export async function pointerRenderCompose() {
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
@@ -23,3 +24,13 @@ export async function pointerRenderCompose() {
]
});
}
+
+export async function pointerViewof() {
+ const penguins = await d3.csv("data/penguins.csv", d3.autoType);
+ const plot = Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", tip: true}).plot();
+ const textarea = html`