Răsfoiți Sursa

Reimplement plotly with data,layout,config support (dict) + performance improvements + auto-resize

Rino Beeli 2 ani în urmă
părinte
comite
2caef2dda1
3 a modificat fișierele cu 123 adăugiri și 26 ștergeri
  1. 0 19
      nicegui/elements/plotly.js
  2. 32 7
      nicegui/elements/plotly.py
  3. 91 0
      nicegui/elements/plotly.vue

+ 0 - 19
nicegui/elements/plotly.js

@@ -1,19 +0,0 @@
-export default {
-  template: `<div></div>`,
-  mounted() {
-    setTimeout(() => {
-      import(window.path_prefix + this.lib).then(() => {
-        Plotly.newPlot(this.$el.id, this.options.data, this.options.layout);
-      });
-    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
-  },
-  methods: {
-    update(options) {
-      Plotly.newPlot(this.$el.id, options.data, options.layout);
-    },
-  },
-  props: {
-    options: Object,
-    lib: String,
-  },
-};

+ 32 - 7
nicegui/elements/plotly.py

@@ -1,29 +1,54 @@
-import json
+from typing import Union
 
 import plotly.graph_objects as go
 
 from ..dependencies import js_dependencies, register_component
 from ..element import Element
 
-register_component('plotly', __file__, 'plotly.js', [], ['lib/plotly.min.js'])
+register_component('plotly', __file__, 'plotly.vue', [], ['lib/plotly.min.js'])
 
 
 class Plotly(Element):
 
-    def __init__(self, figure: go.Figure) -> None:
+    def __init__(self, figure: Union[dict, go.Figure]) -> None:
         """Plotly Element
 
-        Renders a plotly figure onto the page.
+        Renders a Plotly chart. There are two ways to pass a Plotly figure for rendering, see parameter `figure`:
 
-        See `plotly documentation <https://plotly.com/python/>`_ for more information.
+        * Pass a `go.Figure` object, see https://plotly.com/python/
 
-        :param figure: the plotly figure to be displayed
+        * Pass a Python `dict` object with keys `data`, `layout`, `config` (optional), see https://plotly.com/javascript/
+
+        For best performance, use the declarative `dict` approach for creating a Plotly chart.
+
+        :param figure: Plotly figure to be rendered. Can be either a `go.Figure` instance, or
+                       a `dict` object with keys `data`, `layout`, `config` (optional).
         """
         super().__init__('plotly')
+
         self.figure = figure
         self._props['lib'] = [d.import_path for d in js_dependencies.values() if d.path.name == 'plotly.min.js'][0]
         self.update()
 
+    def update_figure(self, figure: Union[dict, go.Figure]):
+        """
+        Overrides figure instance of this Plotly chart and updates chart on client side.
+        """
+        self.figure = figure
+        self.update()
+
     def update(self) -> None:
-        self._props['options'] = json.loads(self.figure.to_json())
+        self._props['options'] = self._get_figure_json()
         self.run_method('update', self._props['options'])
+
+    def _get_figure_json(self) -> dict:
+        if isinstance(self.figure, go.Figure):
+            # convert go.Figure to dict object which is directly JSON serializable
+            # orjson supports numpy array serialization
+            return self.figure.to_plotly_json()
+
+        if isinstance(self.figure, dict):
+            # already a dict object with keys: data, layout, config (optional)
+            return self.figure
+
+        raise ValueError(f"Plotly figure is of unknown type '{self.figure.__class__.__name__}'.")

+ 91 - 0
nicegui/elements/plotly.vue

@@ -0,0 +1,91 @@
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {
+  mounted() {
+    setTimeout(() => {
+      this.ensureLibLoaded().then(() => {
+        // initial rendering of chart
+        Plotly.newPlot(this.$el.id, this.options.data, this.options.layout, this.options.config);
+
+        // register resize observer on parent div to auto-resize Plotly chart
+        let doResize = () => {
+          // only call resize if actually visible, otherwise error in Plotly.js internals
+          if (this.isHidden(this.$el)) return;
+          // console.log("Resize plot");
+          Plotly.Plots.resize(this.$el);
+        };
+
+        // throttle Plotly resize calls for better performance
+        // using HTML5 ResizeObserver on parent div
+        this.resizeObserver = new ResizeObserver((entries) => {
+          if (this.timeoutHandle) {
+            clearTimeout(this.timeoutHandle);
+          }
+          this.timeoutHandle = setTimeout(doResize, this.throttleResizeMs);
+        });
+        this.resizeObserver.observe(this.$el);
+      });
+    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
+  },
+  unmounted() {
+    this.resizeObserver.disconnect();
+    clearTimeout(this.timeoutHandle);
+  },
+
+  methods: {
+    isHidden(gd) {
+      // https://github.com/plotly/plotly.js/blob/e1d94b7afad94152db004b3bd5e6060010fbcc28/src/lib/index.js#L1278
+      var display = window.getComputedStyle(gd).display;
+      return !display || display === "none";
+    },
+
+    ensureLibLoaded() {
+      // ensure Plotly imported (lazy-load)
+      return import(window.path_prefix + this.lib);
+    },
+
+    update(options) {
+      console.log("UPDATE called");
+      // ensure Plotly imported, otherwise first plot will fail in update call
+      // because library not loaded yet
+      this.ensureLibLoaded().then(() => {
+        Plotly.newPlot(this.$el.id, options.data, options.layout, options.config);
+      });
+    },
+  },
+
+  data: function () {
+    return {
+      resizeObserver: undefined,
+      timeoutHandle: undefined,
+      throttleResizeMs: 100, // resize at most every 100 ms
+    };
+  },
+
+  props: {
+    options: Object,
+    lib: String,
+  },
+};
+</script>
+
+<style>
+/*
+  fix styles to correctly render modebar, otherwise large
+  buttons with unwanted line breaks are shown, possibly
+  due to other CSS libraries overriding default styles
+  affecting plotly styling.
+*/
+.js-plotly-plot .plotly .modebar-group {
+  display: flex;
+}
+.js-plotly-plot .plotly .modebar-btn {
+  display: flex;
+}
+.js-plotly-plot .plotly .modebar-btn svg {
+  position: static;
+}
+</style>