Repository: ourownstory/neural_prophet

Issue:
Improve plotly interactive rendering performance 
Credits @LeonieFreisinger 

## :microscope: Background

For **large datasets**, the **plotly** plotting_backend entails **large computation time** (in comparison to matplotlib) for plotting a figure. Additionally, when using an interactive renderer the plotted interactive figure in jupyter notebooks causes a **screen freeze** or **lagged interaction**.
**Goal:** Ideally, we will enable interactive rendering without lagged or freezing interaction.

## :crystal_ball: Key changes
- introduce [plotly-resampler](https://github.com/predict-idlab/plotly-resampler) to **reduze the granuarity** of figures. Plotly-resampler downsamples the data respective to a view.
- Downsampling is ensured for using the `'auto' `mode in all plotly-related `.py `files. --> `register_plotly_resampler(mode='auto')`
- Important note: Input data needs to have a `dtype `--> `list `is not accepted as input data. --> Change `lists `to `np.arrays`


## :clipboard: Review Checklist
- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, added docstrings and data types to function definitions.
- [ ] I have added pytests to check whether my feature / fix works.

Please make sure to follow our best practices in the [Contributing guidelines](https://github.com/ourownstory/neural_prophet/blob/main/CONTRIBUTING.md).



Discussion:

Resolving patch:
diff --git a/neuralprophet/plot_forecast_plotly.py b/neuralprophet/plot_forecast_plotly.py
--- a/neuralprophet/plot_forecast_plotly.py
+++ b/neuralprophet/plot_forecast_plotly.py
@@ -12,6 +12,7 @@
     import plotly.express as px
     import plotly.graph_objs as go
     from plotly.subplots import make_subplots
+    from plotly_resampler import register_plotly_resampler
 except ImportError:
     log.error("Importing plotly failed. Interactive plots will not work.")
 
@@ -39,6 +40,7 @@
     "title": dict(font=dict(size=12)),
     "hovermode": "x unified",
 }
+register_plotly_resampler(mode="auto")
 
 
 def plot(fcst, quantiles, xlabel="ds", ylabel="y", highlight_forecast=None, line_per_origin=False, figsize=(700, 210)):
@@ -209,7 +211,6 @@ def plot(fcst, quantiles, xlabel="ds", ylabel="y", highlight_forecast=None, line
         **layout_args,
     )
     fig = go.Figure(data=data, layout=layout)
-
     return fig
 
 
@@ -301,7 +302,7 @@ def plot_components(m, fcst, plot_configuration, df_name="__df__", one_period_pe
         yaxis.update(trace_object["yaxis"])
         yaxis.update(**yaxis_args)
         for trace in trace_object["traces"]:
-            fig.add_trace(trace, j + 1, 1)
+            fig.add_trace(trace, row=j + 1, col=1)  # adapt var name to plotly-resampler
         fig.update_layout(legend={"y": 0.1, "traceorder": "reversed"})
 
     # Reset multiplicative axes labels after tight_layout adjustment
diff --git a/neuralprophet/plot_model_parameters_plotly.py b/neuralprophet/plot_model_parameters_plotly.py
--- a/neuralprophet/plot_model_parameters_plotly.py
+++ b/neuralprophet/plot_model_parameters_plotly.py
@@ -11,6 +11,7 @@
 try:
     import plotly.graph_objs as go
     from plotly.subplots import make_subplots
+    from plotly_resampler import register_plotly_resampler
 except ImportError:
     log.error("Importing plotly failed. Interactive plots will not work.")
 
@@ -34,6 +35,7 @@
     "title": dict(font=dict(size=12)),
     "hovermode": "x unified",
 }
+register_plotly_resampler(mode="auto")
 
 
 def get_dynamic_axis_range(df_range, type, pad=0.05, inverse=False):
@@ -41,8 +43,8 @@ def get_dynamic_axis_range(df_range, type, pad=0.05, inverse=False):
 
     Parameters
     ----------
-        df_range: list
-            List of axis values to pad
+        df_range: np.array
+            Array of axis values to pad
         type : str
             Type of values in the list to pad
         pad : float
@@ -99,9 +101,9 @@ def plot_trend_change(m, quantile, plot_name="Trend Change", df_name="__df__"):
     start = data_params["ds"].shift
     scale = data_params["ds"].scale
     time_span_seconds = scale.total_seconds()
-    cp_t = []
+    cp_t = np.array([])
     for cp in m.model.config_trend.changepoints:
-        cp_t.append(start + datetime.timedelta(seconds=cp * time_span_seconds))
+        cp_t = np.append(cp_t, start + datetime.timedelta(seconds=cp * time_span_seconds))
     # Global/Local Mode
     if m.model.config_trend.trend_global_local == "local":
         quantile_index = m.model.quantiles.index(quantile)
@@ -110,7 +112,7 @@ def plot_trend_change(m, quantile, plot_name="Trend Change", df_name="__df__"):
         quantile_index = m.model.quantiles.index(quantile)
         weights = m.model.get_trend_deltas.detach()[quantile_index, 0, :].numpy()
     # add end-point to force scale to match trend plot
-    cp_t.append(start + scale)
+    cp_t = np.append(cp_t, start + scale)
     weights = np.append(weights, [0.0])
 
     traces = []
@@ -120,7 +122,7 @@ def plot_trend_change(m, quantile, plot_name="Trend Change", df_name="__df__"):
             x=cp_t,
             y=weights,
             marker_color=color,
-        )
+        ),
     )
 
     padded_range = get_dynamic_axis_range(cp_t, type="dt")
@@ -314,7 +316,7 @@ def plot_scalar_weights(weights, plot_name, focus=None, multiplicative=False):
     traces.append(
         go.Bar(
             name=plot_name,
-            x=names,
+            x=np.array(names),
             y=values,
             marker_color=color,
             width=0.8,
@@ -365,7 +367,7 @@ def plot_lagged_weights(weights, comp_name, focus=None):
     traces = []
 
     n_lags = weights.shape[1]
-    lags_range = list(range(1, 1 + n_lags))[::-1]
+    lags_range = np.array(range(1, 1 + n_lags))[::-1]
     if focus is None:
         weights = np.sum(np.abs(weights), axis=0)
         weights = weights / np.sum(weights)
@@ -584,7 +586,7 @@ def plot_weekly(m, quantile, comp_name="weekly", weekly_start=0, quick=True, mul
     traces.append(
         go.Scatter(
             name=comp_name + " Mean" if mean_std else comp_name,
-            x=list(range(len(days_i))),
+            x=np.array(range(len(days_i))),
             # x=df_w['ds'].dt.to_pydatetime(),
             y=predicted[comp_name],
             mode="lines",
@@ -597,7 +599,7 @@ def plot_weekly(m, quantile, comp_name="weekly", weekly_start=0, quick=True, mul
         traces.append(
             go.Scatter(
                 name="Quant 10%",
-                x=list(range(len(days_i))),
+                x=np.array(range(len(days_i))),
                 y=predicted_q10[comp_name],
                 mode="lines",
                 line=dict(color="rgba(45, 146, 255, 0.2)", width=1),
@@ -608,7 +610,7 @@ def plot_weekly(m, quantile, comp_name="weekly", weekly_start=0, quick=True, mul
         traces.append(
             go.Scatter(
                 name="Quant 90%",
-                x=list(range(len(days_i))),
+                x=np.array(range(len(days_i))),
                 y=predicted_q90[comp_name],
                 fill=filling,
                 mode="lines",
@@ -790,7 +792,7 @@ def plot_custom_season(m, comp_name, quantile, multiplicative=False, df_name="__
             fill="none",
         )
     )
-    padded_range = get_dynamic_axis_range(list(range(len(t_i))), type="numeric")
+    padded_range = get_dynamic_axis_range(t_i, type="numeric")
     xaxis = go.layout.XAxis(
         title=f"One period: {comp_name}",
         range=padded_range,
@@ -946,6 +948,6 @@ def plot_parameters(
         xaxis.update(**xaxis_args)
         yaxis.update(**yaxis_args)
         for trace in trace_object["traces"]:
-            fig.add_trace(trace, i + 1, 1)
+            fig.add_trace(trace, row=i + 1, col=1)  # adapt var name to plotly-resampler
 
     return fig


Test patch:
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -26,7 +26,7 @@
 PLOT = False
 
 # plot tests cover both plotting backends
-decorator_input = ["plotting_backend", [("plotly"), ("matplotlib")]]
+decorator_input = ["plotting_backend", [("matplotlib"), ("plotly")]]
 
 
 @pytest.mark.parametrize(*decorator_input)
@@ -98,13 +98,13 @@ def test_plot_components(plotting_backend):
     # select components manually
     fig3 = m.plot_components(forecast, components=["autoregression"], plotting_backend="matplotlib")
     # select plotting components per period
-    fig4 = m.plot_components(forecast, one_period_per_season=True, plotting_backend="plotly")
+    fig4 = m.plot_components(forecast, one_period_per_season=True, plotting_backend=plotting_backend)
 
     log.info("Plot components with wrong component selection - Raise ValueError")
     with pytest.raises(ValueError):
-        m.plot_components(forecast, components=["quantiles"], plotting_backend="plotly")
+        m.plot_components(forecast, components=["quantiles"], plotting_backend=plotting_backend)
     with pytest.raises(ValueError):
-        m.plot_components(forecast, components=["trend123"], plotting_backend="plotly")
+        m.plot_components(forecast, components=["trend123"], plotting_backend=plotting_backend)
 
     if PLOT:
         fig1.show()
@@ -139,13 +139,13 @@ def test_plot_parameters(plotting_backend):
     fig2 = m.plot_parameters(plotting_backend=plotting_backend)
 
     # select components manually
-    fig3 = m.plot_parameters(components="trend", plotting_backend="plotly")
+    fig3 = m.plot_parameters(components="trend", plotting_backend=plotting_backend)
 
     log.info("Plot parameters with wrong component selection - Raise ValueError")
     with pytest.raises(ValueError):
-        m.plot_parameters(components=["events"], plotting_backend="plotly")
+        m.plot_parameters(components=["events"], plotting_backend=plotting_backend)
     with pytest.raises(ValueError):
-        m.plot_parameters(components=["trend123"], plotting_backend="plotly")
+        m.plot_parameters(components=["trend123"], plotting_backend=plotting_backend)
 
     if PLOT:
         fig1.show()
@@ -374,8 +374,8 @@ def test_plot_seasonality(plotting_backend):
     df = df[df["ds"].isin(bdays)]
     metrics_df = m.fit(df, freq="B")
     forecast = m.predict(df)
-    fig5 = m.plot_components(forecast, plotting_backend="plotly")
-    fig6 = m.plot_parameters(plotting_backend="plotly")
+    fig5 = m.plot_components(forecast, plotting_backend=plotting_backend)
+    fig6 = m.plot_parameters(plotting_backend=plotting_backend)
 
     if PLOT:
         fig1.show()
@@ -524,10 +524,10 @@ def test_plot_uncertainty(plotting_backend):
     forecast = m.predict(future)
     log.info("Plot multi-steps ahead forecast without autoregression - Raise ValueError")
     with pytest.raises(ValueError):
-        m.plot(forecast, plotting_backend="plotly", forecast_in_focus=4)
-        m.plot_components(forecast, plotting_backend="plotly", forecast_in_focus=4)
-        m.plot_components(forecast, plotting_backend="plotly", forecast_in_focus=None)
-        m.plot_parameters(quantile=0.75, plotting_backend="plotly", forecast_in_focus=4)
+        m.plot(forecast, plotting_backend=plotting_backend, forecast_in_focus=4)
+        m.plot_components(forecast, plotting_backend=plotting_backend, forecast_in_focus=4)
+        m.plot_components(forecast, plotting_backend=plotting_backend, forecast_in_focus=None)
+        m.plot_parameters(quantile=0.75, plotting_backend=plotting_backend, forecast_in_focus=4)
 
     if PLOT:
         fig1.show()