Repository: ourownstory/neural_prophet

Issue:
Add plotly-static plotting backend
**Is your feature request related to a problem? Please describe.**

- The plots in the tutorial Jupyter notebooks cannot be saved and restored due the interactive nature of plotly plots.

**Describe the solution you'd like**

- Add a `plotly-static` option for our plotting backend which renders static (non-interactive) plotly plots.

**Alternatives considered**

- It would be possible to set the plotting backend to `plotly` (without the resampler), capture the returned figure and override the renderer, e.g. `fig.show(renderer="svg")`. But this is rather unpractical if users want to opt for the interactive version when they work through the tutorials and have to adjust this each and every time.



Discussion:
I can work on this

Resolving patch:
diff --git a/neuralprophet/forecaster.py b/neuralprophet/forecaster.py
--- a/neuralprophet/forecaster.py
+++ b/neuralprophet/forecaster.py
@@ -1797,14 +1797,15 @@ def set_plotting_backend(self, plotting_backend: str):
                     plotly-resampler package to accelerate visualizing large data by resampling it. Only supported for
                     jupyterlab notebooks and vscode notebooks.
                 * ``plotly``: Use the plotly backend for plotting
+                * ``plotly-static``: Use the plotly backend to generate static svg
                 * ``matplotlib``: use matplotlib for plotting
         """
-        if plotting_backend in ["plotly", "matplotlib", "plotly-resampler"]:
+        if plotting_backend in ["plotly", "matplotlib", "plotly-resampler", "plotly-static"]:
             self.plotting_backend = plotting_backend
             log_warning_deprecation_plotly(self.plotting_backend)
         else:
             raise ValueError(
-                "The parameter `plotting_backend` must be either 'plotly', 'plotly-resampler' or 'matplotlib'."
+                "The parameter `plotting_backend` must be either 'plotly', 'plotly-resampler', 'plotly-resampler' or 'matplotlib'."
             )
 
     def highlight_nth_step_ahead_of_each_forecast(self, step_number: Optional[int] = None):
@@ -1860,6 +1861,7 @@ def plot(
                     environments (colab, pycharm interpreter) plotly-resampler might not properly vizualise the figures.
                     In this case, consider switching to 'plotly-auto'.
                 * ``plotly``: Use the plotly backend for plotting
+                * ``plotly-static``: Use the plotly backend to generate static svg
                 * ``matplotlib``: use matplotlib for plotting
                 * (default) None: Plotting backend ist set automatically. Use plotly with resampling for jupyterlab
                     notebooks and vscode notebooks. Automatically switch to plotly without resampling for all other
@@ -1929,6 +1931,7 @@ def plot(
                 figsize=tuple(x * 70 for x in figsize),
                 highlight_forecast=forecast_in_focus,
                 resampler_active=plotting_backend == "plotly-resampler",
+                plotly_static=plotting_backend == "plotly-static",
             )
         else:
             return plot(
@@ -2047,10 +2050,12 @@ def plot_latest_forecast(
                     environments (colab, pycharm interpreter) plotly-resampler might not properly vizualise the figures.
                     In this case, consider switching to 'plotly-auto'.
                 * ``plotly``: Use the plotly backend for plotting
+                * ``plotly-static``: Use the plotly backend to generate static svg
                 * ``matplotlib``: use matplotlib for plotting
                 ** (default) None: Plotting backend ist set automatically. Use plotly with resampling for jupyterlab
                     notebooks and vscode notebooks. Automatically switch to plotly without resampling for all other
                     environments.
+                * (default) None
         Returns
         -------
             matplotlib.axes.Axes
@@ -2097,6 +2102,7 @@ def plot_latest_forecast(
                 highlight_forecast=self.highlight_forecast_step_n,
                 line_per_origin=True,
                 resampler_active=plotting_backend == "plotly-resampler",
+                plotly_static=plotting_backend == "plotly-static",
             )
         else:
             return plot(
@@ -2169,6 +2175,7 @@ def plot_components(
                     environments (colab, pycharm interpreter) plotly-resampler might not properly vizualise the figures.
                     In this case, consider switching to 'plotly-auto'.
                 * ``plotly``: Use the plotly backend for plotting
+                * ``plotly-static``: Use the plotly backend to generate static svg
                 * ``matplotlib``: use matplotlib for plotting
                 * (default) None: Plotting backend ist set automatically. Use plotly with resampling for jupyterlab
                     notebooks and vscode notebooks. Automatically switch to plotly without resampling for all other
@@ -2260,6 +2267,7 @@ def plot_components(
                 df_name=df_name,
                 one_period_per_season=one_period_per_season,
                 resampler_active=plotting_backend == "plotly-resampler",
+                plotly_static=plotting_backend == "plotly-static",
             )
         else:
             return plot_components(
@@ -2323,6 +2331,7 @@ def plot_parameters(
                     environments (colab, pycharm interpreter) plotly-resampler might not properly vizualise the figures.
                     In this case, consider switching to 'plotly-auto'.
                 * ``plotly``: Use the plotly backend for plotting
+                * ``plotly-static``: Use the plotly backend to generate static svg
                 * ``matplotlib``: use matplotlib for plotting
                 * (default) None: Plotting backend ist set automatically. Use plotly with resampling for jupyterlab
                     notebooks and vscode notebooks. Automatically switch to plotly without resampling for all other
@@ -2331,7 +2340,6 @@ def plot_parameters(
                 Note
                 ----
                 For multiple time series and local modeling of at least one component, the df_name parameter is required.
-
             quantile : float
                 The quantile for which the model parameters are to be plotted
 
@@ -2405,17 +2413,33 @@ def plot_parameters(
 
         log_warning_deprecation_plotly(plotting_backend)
         if plotting_backend.startswith("plotly"):
-            return plot_parameters_plotly(
-                m=self,
-                quantile=quantile,
-                weekly_start=weekly_start,
-                yearly_start=yearly_start,
-                figsize=tuple(x * 70 for x in figsize) if figsize else (700, 210),
-                df_name=valid_plot_configuration["df_name"],
-                plot_configuration=valid_plot_configuration,
-                forecast_in_focus=forecast_in_focus,
-                resampler_active=plotting_backend == "plotly-resampler",
-            )
+            if plotting_backend == "plotly-static":
+                fig = plot_parameters_plotly(
+                    m=self,
+                    quantile=quantile,
+                    weekly_start=weekly_start,
+                    yearly_start=yearly_start,
+                    figsize=tuple(x * 70 for x in figsize) if figsize else (700, 210),
+                    df_name=valid_plot_configuration["df_name"],
+                    plot_configuration=valid_plot_configuration,
+                    forecast_in_focus=forecast_in_focus,
+                    resampler_active=plotting_backend == "plotly-resampler",
+                    plotly_static=plotting_backend == "plotly-static",
+                )
+                fig.show("svg")
+            else:
+                return plot_parameters_plotly(
+                    m=self,
+                    quantile=quantile,
+                    weekly_start=weekly_start,
+                    yearly_start=yearly_start,
+                    figsize=tuple(x * 70 for x in figsize) if figsize else (700, 210),
+                    df_name=valid_plot_configuration["df_name"],
+                    plot_configuration=valid_plot_configuration,
+                    forecast_in_focus=forecast_in_focus,
+                    resampler_active=plotting_backend == "plotly-resampler",
+                    plotly_static=plotting_backend == "plotly-static",
+                )
         else:
             return plot_parameters(
                 m=self,
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
@@ -51,6 +51,7 @@ def plot(
     line_per_origin=False,
     figsize=(700, 210),
     resampler_active=False,
+    plotly_static=False,
 ):
     """
     Plot the NeuralProphet forecast
@@ -73,6 +74,8 @@ def plot(
             Width, height in inches.
         resampler_active : bool
             Flag whether to activate the plotly-resampler
+        plotly_static: bool
+            Flag whether to generate a static svg image
 
     Returns
     -------
@@ -227,6 +230,8 @@ def plot(
     )
     fig = go.Figure(data=data, layout=layout)
     unregister_plotly_resampler()
+    if plotly_static:
+        fig = fig.show("svg")
     return fig
 
 
@@ -238,6 +243,7 @@ def plot_components(
     one_period_per_season=False,
     figsize=(700, 210),
     resampler_active=False,
+    plotly_static=False,
 ):
     """
     Plot the NeuralProphet forecast components.
@@ -258,6 +264,8 @@ def plot_components(
             Width, height in inches.
         resampler_active : bool
             Flag whether to activate the plotly-resampler
+        plotly_static: bool
+            Flag whether to generate a static svg image
 
     Returns
     -------
@@ -339,6 +347,8 @@ def plot_components(
     for ax in multiplicative_axes:
         ax = set_y_as_percent(ax)
     unregister_plotly_resampler()
+    if plotly_static:
+        fig = fig.show("svg")
     return fig
 
 
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
@@ -817,6 +817,7 @@ def plot_parameters(
     df_name=None,
     forecast_in_focus=None,
     resampler_active=False,
+    plotly_static=False,
 ):
     """Plot the parameters that the model is composed of, visually.
 
@@ -860,6 +861,8 @@ def plot_parameters(
             None (default): plot self.highlight_forecast_step_n by default
         resampler_active : bool
             Flag whether to activate the plotly-resampler
+        plotly_static: bool
+            Flag whether to generate a static svg image
 
     Returns:
         Plotly figure


Test patch:
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -27,9 +27,10 @@
 
 # plot tests cover both plotting backends
 decorator_input = ["plotting_backend", [("matplotlib"), ("plotly")]]
+decorator_input_extended = ["plotting_backend", [("matplotlib"), ("plotly"), ("plotly-static"), ("plotly-resampler")]]
 
 
-@pytest.mark.parametrize(*decorator_input)
+@pytest.mark.parametrize(*decorator_input_extended)
 def test_plot(plotting_backend):
     log.info(f"testing: Basic plotting with forecast in focus with {plotting_backend}")
     df = pd.read_csv(PEYTON_FILE, nrows=NROWS)