From 3200c3d86fb4883f877d9545d2c32518eea0f556 Mon Sep 17 00:00:00 2001
From: Sebastien Jourdain <sebastien.jourdain@kitware.com>
Date: Wed, 4 Dec 2024 10:39:48 -0700
Subject: [PATCH] feat(plugin): enable plugin usage with mpld3

---
 examples/issues/2.py      | 194 ++++++++++++++++++++++++++++++++++++++
 vue-components/src/use.js |   4 +
 2 files changed, 198 insertions(+)
 create mode 100644 examples/issues/2.py

diff --git a/examples/issues/2.py b/examples/issues/2.py
new file mode 100644
index 0000000..c4d1ada
--- /dev/null
+++ b/examples/issues/2.py
@@ -0,0 +1,194 @@
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import matplotlib.path as mpath
+import matplotlib.patches as mpatches
+
+from trame.app import get_server
+from trame.ui.vuetify import SinglePageLayout
+from trame.widgets import vuetify, client, matplotlib
+
+from mpld3 import plugins, utils
+
+# -----------------------------------------------------------------------------
+# Trame setup
+# -----------------------------------------------------------------------------
+
+server = get_server(client_type="vue2")
+state, ctrl = server.state, server.controller
+
+# Add d3 for JS code
+server.enable_module(
+    dict(scripts=["https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.js"])
+)
+
+# -----------------------------------------------------------------------------
+# Chart examples from:
+#   - http://jakevdp.github.io/blog/2013/12/19/a-d3-viewer-for-matplotlib/
+# -----------------------------------------------------------------------------
+
+
+class LinkedDragPlugin(plugins.PluginBase):
+    JAVASCRIPT = r"""
+    DragPlugin.prototype = Object.create(mpld3.Plugin.prototype);
+    DragPlugin.prototype.constructor = DragPlugin;
+    DragPlugin.prototype.requiredProps = ["idpts", "idline", "idpatch"];
+    DragPlugin.prototype.defaultProps = {}
+    function DragPlugin(fig, props){
+        mpld3.Plugin.call(this, fig, props);
+    };
+
+    DragPlugin.prototype.draw = function(){
+        var patchobj = mpld3.get_element(this.props.idpatch, this.fig);
+        var ptsobj = mpld3.get_element(this.props.idpts, this.fig);
+        var lineobj = mpld3.get_element(this.props.idline, this.fig);
+
+        var drag = d3.drag()
+            .subject(function(d) { return {x:ptsobj.ax.x(d[0]),
+                                          y:ptsobj.ax.y(d[1])}; })
+            .on("start", dragstarted)
+            .on("drag", dragged)
+            .on("end", dragended);
+
+        lineobj.path.attr("d", lineobj.datafunc(ptsobj.offsets));
+        patchobj.path.attr("d", patchobj.datafunc(ptsobj.offsets,
+                                                  patchobj.pathcodes));
+        lineobj.data = ptsobj.offsets;
+        patchobj.data = ptsobj.offsets;
+
+        ptsobj.elements()
+           .data(ptsobj.offsets)
+           .style("cursor", "default")
+           .call(drag);
+
+        function dragstarted(d) {
+          d3.event.sourceEvent.stopPropagation();
+          d3.select(this).classed("dragging", true);
+        }
+
+        function dragged(d, i) {
+          d[0] = ptsobj.ax.x.invert(d3.event.x);
+          d[1] = ptsobj.ax.y.invert(d3.event.y);
+          d3.select(this)
+            .attr("transform", "translate(" + [d3.event.x,d3.event.y] + ")");
+          lineobj.path.attr("d", lineobj.datafunc(ptsobj.offsets));
+          patchobj.path.attr("d", patchobj.datafunc(ptsobj.offsets,
+                                                    patchobj.pathcodes));
+        }
+
+        function dragended(d, i) {
+          d3.select(this).classed("dragging", false);
+        }
+    }
+
+    mpld3.register_plugin("drag", DragPlugin);
+    """
+
+    def __init__(self, points, line, patch):
+        if isinstance(points, mpl.lines.Line2D):
+            suffix = "pts"
+        else:
+            suffix = None
+
+        self.dict_ = {
+            "type": "drag",
+            "idpts": utils.get_id(points, suffix),
+            "idline": utils.get_id(line),
+            "idpatch": utils.get_id(patch),
+        }
+
+
+def figure_size():
+    if state.figure_size is None:
+        return {}
+
+    dpi = state.figure_size.get("dpi")
+    rect = state.figure_size.get("size")
+    w_inch = rect.get("width") / dpi / 2
+    h_inch = rect.get("height") / dpi / 2
+
+    return {
+        "figsize": (w_inch, h_inch),
+        "dpi": dpi,
+    }
+
+
+def FirstDemo():
+    fig, ax = plt.subplots(**figure_size())
+
+    Path = mpath.Path
+    path_data = [
+        (Path.MOVETO, (1.58, -2.57)),
+        (Path.CURVE4, (0.35, -1.1)),
+        (Path.CURVE4, (-1.75, 2.0)),
+        (Path.CURVE4, (0.375, 2.0)),
+        (Path.LINETO, (0.85, 1.15)),
+        (Path.CURVE4, (2.2, 3.2)),
+        (Path.CURVE4, (3, 0.05)),
+        (Path.CURVE4, (2.0, -0.5)),
+        (Path.CLOSEPOLY, (1.58, -2.57)),
+    ]
+    codes, verts = zip(*path_data)
+    path = mpath.Path(verts, codes)
+    patch = mpatches.PathPatch(path, facecolor="r", alpha=0.5)
+    ax.add_patch(patch)
+
+    # plot control points and connecting lines
+    x, y = zip(*path.vertices[:-1])
+    points = ax.plot(x, y, "go", ms=10)
+    line = ax.plot(x, y, "-k")
+
+    ax.grid(True, color="gray", alpha=0.5)
+    ax.axis("equal")
+    ax.set_title("Drag Points to Change Path", fontsize=18)
+
+    plugins.connect(fig, LinkedDragPlugin(points[0], line[0], patch))
+
+    return fig
+
+
+# -----------------------------------------------------------------------------
+# Callbacks
+# -----------------------------------------------------------------------------
+
+
+@state.change("active_figure", "figure_size")
+def update_chart(active_figure, **kwargs):
+    ctrl.update_figure(globals()[active_figure]())
+
+
+# -----------------------------------------------------------------------------
+# UI
+# -----------------------------------------------------------------------------
+
+state.trame__title = "Matplotly"
+
+with SinglePageLayout(server) as layout:
+    layout.title.set_text("trame ❤️ matplotlib")
+    client.Script(LinkedDragPlugin.JAVASCRIPT)
+
+    with layout.toolbar:
+        vuetify.VSpacer()
+        vuetify.VSelect(
+            v_model=("active_figure", "FirstDemo"),
+            items=(
+                "figures",
+                [
+                    {"text": "First Demo", "value": "FirstDemo"},
+                ],
+            ),
+            hide_details=True,
+            dense=True,
+        )
+
+    with layout.content:
+        with vuetify.VContainer(fluid=True, classes="fill-height pa-0 ma-0"):
+            with client.SizeObserver("figure_size"):
+                html_figure = matplotlib.Figure(style="position: absolute")
+                ctrl.update_figure = html_figure.update
+
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+    server.start()
diff --git a/vue-components/src/use.js b/vue-components/src/use.js
index f90bb65..456dedb 100644
--- a/vue-components/src/use.js
+++ b/vue-components/src/use.js
@@ -1,7 +1,11 @@
+import mpld3 from 'mpld3';
 import components from './components';
 
 export function install(Vue) {
   Object.keys(components).forEach((name) => {
     Vue.component(name, components[name]);
   });
+
+  // Expose mpld3 globaly
+  window.mpld3 = mpld3;
 }