code: def plot_forecast_vs_actual(
      df,
      time_col=cst.TIME_COL,
      actual_col=cst.ACTUAL_COL,
      predicted_col=cst.PREDICTED_COL,
      predicted_lower_col=cst.PREDICTED_LOWER_COL,
      predicted_upper_col=cst.PREDICTED_UPPER_COL,
      xlabel=cst.TIME_COL,
      ylabel=cst.VALUE_COL,
      train_end_date=None,
      title=None,
      showlegend=True,
      actual_mode="lines+markers",
      actual_points_color="rgba(250, 43, 20, 0.7)",  # red
      actual_points_size=2.0,
      actual_color_opacity=1.0,
      forecast_curve_color="rgba(0, 90, 181, 0.7)",  # blue
      forecast_curve_dash="solid",
      ci_band_color="rgba(0, 90, 181, 0.15)",  # light blue
      ci_boundary_curve_color="rgba(0, 90, 181, 0.5)",  # light blue
      ci_boundary_curve_width=0.0,  # no line
      vertical_line_color="rgba(100, 100, 100, 0.9)",  # black color with opacity of 0.9
      vertical_line_width=1.0,
  ):
      """Plots forecast with prediction intervals, against actuals
      Adapted from plotly user guide:
      https://plot.ly/python/v3/continuous-error-bars/#basic-continuous-error-bars
  
      Parameters
      ----------
      df : `pandas.DataFrame`
          Timestamp, predicted, and actual values
      time_col : `str`, default `~greykite.common.constants.TIME_COL`
          Column in df with timestamp (x-axis)
      actual_col : `str`, default `~greykite.common.constants.ACTUAL_COL`
          Column in df with actual values
      predicted_col : `str`, default `~greykite.common.constants.PREDICTED_COL`
          Column in df with predicted values
      predicted_lower_col : `str` or None, default `~greykite.common.constants.PREDICTED_LOWER_COL`
          Column in df with predicted lower bound
      predicted_upper_col : `str` or None, default `~greykite.common.constants.PREDICTED_UPPER_COL`
          Column in df with predicted upper bound
      xlabel : `str`, default `~greykite.common.constants.TIME_COL`
          x-axis label.
      ylabel : `str`, default `~greykite.common.constants.VALUE_COL`
          y-axis label.
      train_end_date : `datetime.datetime` or None, default None
          Train end date.
          Must be a value in ``df[time_col]``.
      title : `str` or None, default None
          Plot title.
      showlegend : `bool`, default True
          Whether to show a plot legend.
      actual_mode : `str`, default "lines+markers"
          How to show the actuals.
          Options: ``markers``, ``lines``, ``lines+markers``
      actual_points_color : `str`, default "rgba(99, 114, 218, 1.0)"
          Color of actual line/marker.
      actual_points_size : `float`, default 2.0
          Size of actual markers.
          Only used if "markers" is in ``actual_mode``.
      actual_color_opacity : `float` or None, default 1.0
          Opacity of actual values points.
      forecast_curve_color : `str`, default "rgba(0, 145, 202, 1.0)"
          Color of forecasted values.
      forecast_curve_dash : `str`, default "solid"
          'dash' property of forecast ``scatter.line``.
          One of: ``['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot']``
          or a string containing a dash length list in pixels or percentages
          (e.g. ``'5px 10px 2px 2px'``, ``'5, 10, 2, 2'``, ``'10% 20% 40%'``)
      ci_band_color : `str`, default "rgba(0, 145, 202, 0.15)"
          Fill color of the prediction bands.
      ci_boundary_curve_color : `str`, default "rgba(0, 145, 202, 0.15)"
          Color of the prediction upper/lower lines.
      ci_boundary_curve_width : `float`, default 0.0
          Width of the prediction upper/lower lines.
          default 0.0 (hidden)
      vertical_line_color : `str`, default "rgba(100, 100, 100, 0.9)"
          Color of the vertical line indicating train end date.
          Default is black with opacity of 0.9.
      vertical_line_width : `float`, default 1.0
          width of the vertical line indicating train end date
  
      Returns
      -------
      fig : `plotly.graph_objects.Figure`
          Plotly figure of forecast against actuals, with prediction
          intervals if available.
  
          Can show, convert to HTML, update::
  
              # show figure
              fig.show()
  
              # get HTML string, write to file
              fig.to_html(include_plotlyjs=False, full_html=True)
              fig.write_html("figure.html", include_plotlyjs=False, full_html=True)
  
              # customize layout (https://plot.ly/python/v3/user-guide/)
              update_layout = dict(
                  yaxis=dict(title="new ylabel"),
                  title_text="new title",
                  title_x=0.5,
                  title_font_size=30)
              fig.update_layout(update_layout)
      """
      if title is None:
          title = "Forecast vs Actual"
      if train_end_date is not None and not all(
          pd.Series(train_end_date).isin(df[time_col])
      ):
          raise Exception(
              f"train_end_date {train_end_date} is not found in df['{time_col}']"
          )
  
      fill_dict = {
          "mode": "lines",
          "fillcolor": ci_band_color,
          "fill": "tonexty",
      }
      data = []
      if predicted_lower_col is not None:
          lower_bound = go.Scatter(
              name="Lower Bound",
              x=df[time_col],
              y=df[predicted_lower_col],
              mode="lines",
              line=dict(
                  width=ci_boundary_curve_width,
                  color=ci_boundary_curve_color,
              ),
              legendgroup="interval",  # show/hide with the upper bound
          )
          data.append(lower_bound)
  
      # plotly fills between current and previous element in `data`.
      # Only fill if lower bound exists.
      forecast_fill_dict = (
          fill_dict if predicted_lower_col is not None else {}
      )
      if predicted_upper_col is not None:
          upper_bound = go.Scatter(
              name="Upper Bound",
              x=df[time_col],
              y=df[predicted_upper_col],
              line=dict(
                  width=ci_boundary_curve_width,
                  color=ci_boundary_curve_color,
              ),
              legendgroup="interval",  # show/hide with the lower bound
              **forecast_fill_dict,
          )
          data.append(upper_bound)
  
      # If `predicted_lower_col` and `predicted_upper_col`, then the full range
      # has been filled in. If only one of them, then fill in between that line
      # and forecast.
      actual_params = {}
      if "lines" in actual_mode:
          actual_params.update(line=dict(color=actual_points_color))
      if "markers" in actual_mode:
          actual_params.update(
              marker=dict(
                  color=actual_points_color, size=actual_points_size
              )
          )
      actual = go.Scatter(
          name="Actual",
          x=df[time_col],
          y=df[actual_col],
          mode=actual_mode,
          opacity=actual_color_opacity,
          **actual_params,
      )
      data.append(actual)
  
      forecast_fill_dict = (
          fill_dict
          if (predicted_lower_col is None)
          != (predicted_upper_col is None)
          else {}
      )
      forecast = go.Scatter(
          name="Forecast",
          x=df[time_col],
          y=df[predicted_col],
          line=dict(
              color=forecast_curve_color, dash=forecast_curve_dash
          ),
          **forecast_fill_dict,
      )
      data.append(forecast)
  
      layout = go.Layout(
          xaxis=dict(title=xlabel),
          yaxis=dict(title=ylabel),
          title=title,
          title_x=0.5,
          showlegend=showlegend,
          # legend order from top to bottom: Actual, Forecast, Upper Bound, Lower Bound
          legend={"traceorder": "reversed"},
      )
      fig = go.Figure(data=data, layout=layout)
      fig.update()
  
      # adds a vertical line to separate training and testing phases
      if train_end_date is not None:
          new_layout = dict(
              # add vertical line
              shapes=[
                  dict(
                      type="line",
                      xref="x",
                      yref="paper",  # y-reference is assigned to the plot paper [0,1]
                      x0=train_end_date,
                      y0=0,
                      x1=train_end_date,
                      y1=1,
                      line=dict(
                          color=vertical_line_color,
                          width=vertical_line_width,
                      ),
                  )
              ],
              # add text annotation
              annotations=[
                  dict(
                      xref="x",
                      x=train_end_date,
                      yref="paper",
                      y=0.97,
                      text="Train End Date",
                      showarrow=True,
                      arrowhead=0,
                      ax=-60,
                      ay=0,
                  )
              ],
          )
          fig.update_layout(new_layout)
      return fig
  
imports: import warnings
  import numpy
  import pandas
  import plotly.graph_objects
  from plotly.colors import DEFAULT_PLOTLY_COLORS
  from plotly.subplots import make_subplots
  from greykite.common import constants
  from greykite.common.features.timeseries_features import build_time_features_df
  from greykite.common.logging import LoggingLevelEnum
  from greykite.common.logging import log_message
  from greykite.common.python_utils import update_dictionary
  from greykite.common.viz.colors_utils import get_color_palette
  from greykite.common.viz.colors_utils import get_distinct_colors
label_desc: Use image node_id 4 for calling a global function with example usage: plot_forecast_vs_actual(df, time_col, actual_col, predicted_col, predicted_lower_col, predicted_upper_col, xlabel, ylabel, train_end_date, title, showlegend, actual_mode, actual_points_color, actual_points_size, actual_color_opacity, forecast_curve_color, forecast_curve_dash, ci_band_color, ci_boundary_curve_color, ci_boundary_curve_width, vertical_line_color, vertical_line_width) and returns: fig