observablehq/plot

Event and pointer interaction in Vue.js context

dawadam opened this issue · 3 comments

Hi,
I'm trying to use Plot on my Vue.js app, using this official code.

It works very well as of now.

Now I want to add pointer interaction, but nothing appears.
I think the problem comes from event implementation because in PlotRender.js, the comment regarding the event part is:

  addEventListener() {
    // ignored; interaction needs real DOM
  }
  removeEventListener() {
    // ignored; interaction needs real DOM
  }
  dispatchEvent() {
    // ignored; interaction needs real DOM
  }

Is it realy impossible to use events in a Vue.js context?

You need to use client-side rendering as described in the Getting started guide. If you use server-side rendering, the generated plot is serialized to HTML and loses all its interaction.

import * as Plot from "@observablehq/plot";
import {h, withDirectives} from "vue";

export default {
  props: ["options"],
  render() {
    const {options} = this;
    return withDirectives(h("div"), [
      [
        {
          mounted(el) {
            el.append(Plot.plot(options));
          }
        }
      ]
    ]);
  }
};

Also, the PlotRender component you linked to isn’t “official” in the sense that it’s not supported — it’s just what we use internally to build the Plot documentation. You’re free to use it but it isn’t guaranteed to work or be documented.

The way that we handle interactive plots in our documentation is the defer directive. For example:

:::plot defer hidden https://observablehq.com/@observablehq/plot-pointer-transform
```js
Plot.plot({
marks: [
Plot.dot(penguins, (pointered ? Plot.pointer : (o) => o)({x: "culmen_length_mm", y: "culmen_depth_mm", fill: "red", r: 8})),
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"})
]
})
```
:::

Note the :::plot defer instead of the normal :::plot as you see with non-interactive plots.

The defer directive triggers client-side rendering. During server-side rendering an empty <div> is rendered, and then using an IntersectionObserver on the client, the plot is rendered on the client just before it becomes visible.

if (this.defer) {
const mounted = (el) => {
disconnect(); // remove old listeners
function observed() {
unmounted(el); // remove old plot (and listeners)
el.append(Plot[method](options));
}
const rect = el.getBoundingClientRect();
if (rect.bottom > 0 && rect.top < window.innerHeight) {
observed();
} else {
this._observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) observed();
},
{rootMargin: "100px"}
);
this._observer.observe(el);
if (typeof requestIdleCallback === "function") {
this._idling = requestIdleCallback(observed);
}
}
};
const unmounted = (el) => {
while (el.lastChild) el.lastChild.remove();
disconnect();
};
const disconnect = () => {
if (this._observer !== undefined) {
this._observer.disconnect();
this._observer = undefined;
}
if (this._idling !== undefined) {
cancelIdleCallback(this._idling);
this._idling = undefined;
}
};
const {height = 400} = this.options;
return withDirectives(
h(
"span",
method === "plot"
? [
h("div", {
style: {
maxWidth: "100%",
width: `688px`,
aspectRatio: `688 / ${height}`
}
})
]
: []
),
[
[
{
mounted,
updated: mounted,
unmounted
}
]
]
);
}

This is useful not just for interactive plots, but for plots that would otherwise generate many megabytes of serialized SVG, such as maps. I’m sure there’s a way to do traditional full server-side rendering with client-side hydration of event listeners in Vue, but we didn’t want to do the typical thing of rendering the charts twice.

Ok thanks, I didn't realize I was using the server version, especially since I don't need it.
So I'm going to work on a client version.
Is there a fully ready version, sample client-side render file, to save me time?

edit:
ok it works with the client version, thank you very much ! :)