diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js
index c92addcff7..787e591dfe 100644
--- a/src/interactions/pointer.js
+++ b/src/interactions/pointer.js
@@ -126,7 +126,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op
// Dispatch the value. When simultaneously exiting this facet and
// entering a new one, prioritize the entering facet.
- if (!(i == null && facetState?.size > 1)) context.dispatchValue(i == null ? null : data[i]);
+ if (!(i == null && facetState?.size > 1)) context.dispatchValue(i == null ? null : data[i], s);
return r;
}
diff --git a/src/plot.js b/src/plot.js
index 541218f306..23878f1973 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -2,18 +2,33 @@ import {creator, select} from "d3";
import {createChannel, inferChannelScale} from "./channel.js";
import {createContext} from "./context.js";
import {createDimensions} from "./dimensions.js";
-import {createFacets, recreateFacets, facetExclude, facetGroups, facetTranslator, facetFilter} from "./facet.js";
+import {createFacets, facetExclude, facetFilter, facetGroups, facetTranslator, recreateFacets} from "./facet.js";
import {pointer, pointerX, pointerY} from "./interactions/pointer.js";
import {createLegends, exposeLegends} from "./legends.js";
import {Mark} from "./mark.js";
import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./marks/axis.js";
import {frame} from "./marks/frame.js";
import {tip} from "./marks/tip.js";
-import {isColor, isIterable, isNone, isScaleOptions} from "./options.js";
-import {arrayify, map, yes, maybeIntervalTransform, subarray} from "./options.js";
+import {
+ arrayify,
+ isColor,
+ isIterable,
+ isNone,
+ isScaleOptions,
+ map,
+ maybeIntervalTransform,
+ subarray,
+ yes
+} from "./options.js";
import {createProjection, getGeometryChannels, hasProjection} from "./projection.js";
-import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js";
-import {innerDimensions, outerDimensions} from "./scales.js";
+import {
+ autoScaleRange,
+ createScaleFunctions,
+ createScales,
+ exposeScales,
+ innerDimensions,
+ outerDimensions
+} from "./scales.js";
import {isPosition, registry as scaleRegistry} from "./scales/index.js";
import {applyInlineStyles, maybeClassName} from "./style.js";
import {initializer} from "./transforms/basic.js";
@@ -172,10 +187,11 @@ export function plot(options = {}) {
};
// Allows e.g. the pointer transform to support viewof.
- context.dispatchValue = (value) => {
- if (figure.value === value) return;
+ context.dispatchValue = (value, sticky) => {
+ if (figure.value === value && context.stickyState === sticky) return;
figure.value = value;
- figure.dispatchEvent(new Event("input", {bubbles: true}));
+ context.stickyState = sticky;
+ figure.dispatchEvent(new StickyEvent("input", {bubbles: true, sticky}));
};
// Reinitialize; for deriving channels dependent on other channels.
@@ -743,3 +759,10 @@ function outerRange(scale) {
if (x2 < x1) [x1, x2] = [x2, x1];
return [x1, x2 + scale.bandwidth()];
}
+
+class StickyEvent extends Event {
+ constructor(type, options) {
+ super(type, options);
+ this.sticky = options.sticky;
+ }
+}
diff --git a/test/output/pointerSticky.html b/test/output/pointerSticky.html
new file mode 100644
index 0000000000..d320531398
--- /dev/null
+++ b/test/output/pointerSticky.html
@@ -0,0 +1,403 @@
+
\ No newline at end of file
diff --git a/test/plots/pointer.ts b/test/plots/pointer.ts
index 1e9df45067..5710f40d98 100644
--- a/test/plots/pointer.ts
+++ b/test/plots/pointer.ts
@@ -45,3 +45,25 @@ export async function pointerNonFaceted() {
]
});
}
+
+export async function pointerSticky() {
+ 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();
+ let prevSticky = false;
+ const textarea = html`