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 @@ +
+ + + + + + + + + + + + + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + + + ↑ culmen_depth_mm + + + + + + + + + + 35 + 40 + 45 + 50 + 55 + + + culmen_length_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ 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`