plt.plotly <- function(
  x, y, by, size, fit,             # per-point data vectors
  by_name, n.by,                               # n.by unused; reserved
  fill, border, shape, pt.size,
  x_lab, y_lab, ax, gridT1, gridT2,
  main = NULL,
  digits_d=2, date.var,                        # date.var unused; reserved
  fit_color=border, fit_lwd=NULL, ln.type=NULL, # ln.type unused; reserved
  connect=FALSE, ts_unit=NULL,                 # for time series
  power=0.5, radius.in=0.12,   # area=size^power (for bubble mode)
  size.name="Size",
  ellipse_region=NULL, ellipse_fill=NULL,
  ellipse_color=NULL, ellipse_lwd=NULL,
  se.poly=NULL, se_fill=NULL,
  pt_opacity=0.95,
  frcst=NULL,                      # forecast bundle
  # --- outlier parameters (coordinates, not indices into g) ----------------
  out_x=NULL, out_y=NULL,          # actual x/y coordinates of outliers
  out_fill=NULL, out_color=NULL, out_shape=NULL,
  out_labels=NULL,                 # pre-extracted ID[out_ind]
  ID_color="black", ID_size=0.6,

  style_opts = .plotly_style()
) {

  title.size <- round(16 * getOption("main_size", 1))

  # rebuild the data frame the body expects from the input vectors
  g <- data.frame(x = x, y = y, f = fit, ind = by, size = size)

  # sanitize opacity
  pt_opacity <- as.numeric(pt_opacity[1])
  if (!is.finite(pt_opacity)) pt_opacity <- 0.95
  pt_opacity <- max(0, min(1, pt_opacity))

  # ---- bubbles (global scale) ----
  # should also apply to discrete bubble plots, e.g., XY(Dept, Gender)
  dpi <- style_opts$plotly_dpi
  pt.rows  <- rep(TRUE, nrow(g))  # placeholder; reserved for future row filtering
  size.vec <- as.numeric(g$size)
  use.bubbles <- length(unique(size.vec[!is.na(size.vec)])) > 1L

  if (use.bubbles) {
    sz.all <- pmax(0, size.vec)^power
    mxru   <- max(sz.all, na.rm = TRUE)
    radius.px <- radius.in * dpi
    bubble.diam <- function(v) {
      v.sz <- pmax(0, as.numeric(v))^power
      if (is.finite(mxru) && mxru > 0)
        2 * (v.sz / mxru) * radius.px else rep(0, length(v.sz))
    }
  } else {
    bubble.diam <- NULL
  }


# groups set up -----------------------------------------------------------

  ind.chr   <- trimws(as.character(g$ind))
  valid     <- !is.na(g$ind)
  idx.list  <- split(seq_len(nrow(g))[valid], ind.chr[valid], drop=TRUE)
  grp.names <- names(idx.list)
  has.groups <- length(grp.names) > 1L

  has.out <- !is.null(out_x) && length(out_x) > 0


# initialization ----------------------------------------------------------
  plt <- plotly::plot_ly()
  border.hex <- .to_hex(border)  # fully opaque borders

  symbol <- switch(as.character(shape),
    "21"="circle","22"="square","23"="diamond",
    "24"="triangle-up","25"="triangle-down","circle"
  )



# no groups ---------------------------------------------------------------

  if (!has.groups) {

    if (!is.null(ts_unit)) {
      xv <- as.Date(g$x, origin="1970-01-01")
      xv <- .format_date_labels(xv, ts_unit)
      xlab.h <- "Date"
    } else {
      xv <- as.numeric(g$x)
      xlab.h <- x_lab
    }
    yv <- as.numeric(g$y)

    y_lab.h    <- trimws(y_lab)
    hover.tmpl <- .hover.fmt(xv, yv, xlab.h, y_lab.h, FALSE, digits_d, ts_unit)

    xv <- as.numeric(g$x)        # keep numeric for plotly axis

    # size/marker setup
    if (use.bubbles) {
      diam.px <- bubble.diam(size.vec)
      hover.tmpl <- sub(
        "<extra></extra>$",
        paste0("<br>", size.name, ": %{customdata:.",
               digits_d, "f}<extra></extra>"),
        hover.tmpl
      )
      customdata <- size.vec
    } else {
      px <- as.numeric(pt.size) * 7.25
      if (!is.finite(px)) px <- 5
      diam.px    <- rep(px, length(xv))
      customdata <- NULL
    }

    # set alpha via rgba colors, and leave marker.opacity at 1
    marker.color <- .maketrans_plotly(fill[1], alpha = pt_opacity)

    # ---- regular points (g already contains only non-outlier rows) --------
    plt <- plotly::add_trace(
      plt,
      type  = "scatter",
      mode  = if (connect) "lines+markers" else "markers",
      x = xv, y = yv,
      marker = list(
        symbol   = symbol,
        size     = diam.px,
        sizemode = "diameter",
        color    = marker.color,
        opacity  = 1,  # do not multiply alpha; already in rgba
        line     = list(color = border.hex[1], width = 1)
      ),
      line          = if (connect) list(color = border.hex[1], width = 1.5)
                      else NULL,
      hovertemplate = hover.tmpl,
      customdata    = customdata,
      showlegend    = FALSE
    )

    # ---- outlier points ---------------------------------------------------
    if (has.out) {
      out.symbol <- switch(as.character(out_shape),
        "21"="circle", "22"="square", "23"="diamond",
        "24"="triangle-up", "25"="triangle-down", "circle-open"
      )
      out.marker.color <- .maketrans_plotly(
        if (!is.null(out_fill)) out_fill else fill[1], alpha = pt_opacity
      )
      out.border.hex <- .to_hex(if (!is.null(out_color)) out_color else border[1])

      # use same pixel size as regular points
      px.out <- as.numeric(pt.size) * 7.25
      if (!is.finite(px.out)) px.out <- 5

      plt <- plotly::add_trace(
        plt,
        type = "scatter", mode = "markers",
        x = as.numeric(out_x), y = as.numeric(out_y),
        marker = list(
          symbol   = out.symbol,
          size     = px.out,
          sizemode = "diameter",
          color    = out.marker.color,
          opacity  = 1,
          line     = list(color = out.border.hex, width = 1.5)
        ),
        hovertemplate = hover.tmpl,
        showlegend    = FALSE
      )

      # ---- outlier labels -------------------------------------------------
      if (!is.null(out_labels) && length(out_labels) > 0) {
        font.sz <- max(9L, round(as.numeric(ID_size) * 14))
        plt <- plotly::add_annotations(
          plt,
          x         = as.numeric(out_x),
          y         = as.numeric(out_y),
          text      = as.character(out_labels),
          showarrow = FALSE,
          yshift    = -12,
          font      = list(color = ID_color, size = font.sz, family = "Arial"),
          xref      = "x",
          yref      = "y"
        )
      }
    }

  }


# groups ------------------------------------------------------------------

  else {  # ---- groups ----

    for (i in seq_along(grp.names)) {
      nm  <- grp.names[i]
      idx <- idx.list[[nm]]
      if (!length(idx)) next

# --- x for hover (formatted as Date if time series) ---
      if (!is.null(ts_unit)) {
        xv.h <- as.Date(g$x[idx], origin = "1970-01-01")
        xv.h <- .format_date_labels(xv.h, ts_unit)
        xlab.h <- "Date"   # generic label for time series hover
      } else {
        xv.h   <- as.numeric(g$x[idx])
        xlab.h <- x_lab
      }

      yv <- as.numeric(g$y[idx])

      y_lab.h <- trimws(y_lab)
      hover.i <- sub(
        "<extra></extra>$",
        paste0("<br>", by_name, ": %{text}<extra></extra>"),
        .hover.fmt(xv.h, yv, xlab.h, y_lab.h, FALSE, digits_d, ts_unit)
      )

# --- x for plotly axis positioning (keep numeric day index) ---
      xv <- as.numeric(g$x[idx])

      if (use.bubbles) {
        diam.px  <- bubble.diam(size.vec[idx])
        hover.i  <- sub(
          "<extra></extra>$",
          paste0("<br>", size.name,
                 ": %{customdata:.", digits_d, "f}<extra></extra>"), hover.i
        )
        custom.i <- size.vec[idx]
      } else {
        # Point sizing for standard scatter / time-series.
        # - If user sets pt.size = 0, we suppress markers entirely (lines only).
        # - Otherwise, scale to plotly pixels.
        px <- as.numeric(pt.size) * 6.5
        if (!is.finite(px)) px <- 5
        if (px < 0) px <- 5
        diam.px  <- rep(px, length(idx))
        custom.i <- NULL
      }

      # per-group fill with desired alpha
      fill.i <- .maketrans_plotly(fill[i], alpha = pt_opacity)

      # ---- Main data trace ----
      # For connected series, keep markers on the plot, remove from the legend
      # by adding a separate legend-only line trace (below).
      mode.data <- if (connect) {
        if (use.bubbles) "lines+markers"
        else if (diam.px[1] <= 0) "lines" else "lines+markers"
      } else {
        "markers"
      }

      plt <- plotly::add_trace(
        plt,
        type = "scatter",
        mode = mode.data,
        name = nm, legendgroup = nm, text = nm,
        x = xv, y = yv,
        marker = if (grepl("markers", mode.data, fixed = TRUE)) list(
          symbol   = symbol,
          size     = diam.px,
          sizemode = "diameter",
          color    = fill.i,
          opacity  = 1,
          line     = list(color = .to_hex(border[i]), width = 1)
        ) else NULL,
        line          = if (connect) list(color = .to_hex(border[i]), width = 1.5) else NULL,
        hovertemplate = hover.i,
        customdata    = custom.i,
        showlegend    = if (connect) FALSE else TRUE
      )

      # ---- Legend-only line sample (no dot) ----
      if (connect) {
        ok2 <- which(is.finite(xv) & is.finite(yv))
        if (length(ok2) >= 2L) {
          idx2 <- ok2[1:2]
          plt <- plotly::add_trace(
            plt,
            type = "scatter",
            mode = "lines",
            name = nm, legendgroup = nm,
            x = xv[idx2], y = yv[idx2],
            line = list(color = .to_hex(border[i]), width = 1.5),
            hoverinfo  = "skip",
            showlegend = TRUE
          )
        }
      }
    }

    # ---- outliers for grouped plots (group-agnostic trace) ----------------
    # out_x/out_y are raw coordinates from the full data, so no index
    # translation needed regardless of how groups partition the rows.
    if (has.out) {
      out.symbol <- switch(as.character(out_shape),
        "21"="circle", "22"="square", "23"="diamond",
        "24"="triangle-up", "25"="triangle-down", "circle-open"
      )
      out.marker.color <- .maketrans_plotly(
        if (!is.null(out_fill)) out_fill else fill[1], alpha = pt_opacity
      )
      out.border.hex <- .to_hex(if (!is.null(out_color)) out_color else border[1])

      px.out <- as.numeric(pt.size) * 6.5
      if (!is.finite(px.out)) px.out <- 5

      plt <- plotly::add_trace(
        plt,
        type = "scatter", mode = "markers",
        x = as.numeric(out_x), y = as.numeric(out_y),
        marker = list(
          symbol   = out.symbol,
          size     = px.out,
          sizemode = "diameter",
          color    = out.marker.color,
          opacity  = 1,
          line     = list(color = out.border.hex, width = 1.5)
        ),
        showlegend = FALSE,
        hoverinfo  = "skip"
      )

      if (!is.null(out_labels) && length(out_labels) > 0) {
        font.sz <- max(9L, round(as.numeric(ID_size) * 14))
        plt <- plotly::add_annotations(
          plt,
          x         = as.numeric(out_x),
          y         = as.numeric(out_y),
          text      = as.character(out_labels),
          showarrow = FALSE,
          yshift    = -12,
          font      = list(color = ID_color, size = font.sz, family = "Arial"),
          xref      = "x",
          yref      = "y"
        )
      }
    }

  }


# forecast (time series only) ---------------------------------------------

  if (!is.null(frcst) && !is.null(ts_unit)) {
    xf  <- frcst$x.fit;  yf  <- frcst$y.fit
    xh  <- frcst$x.hat;  yh  <- frcst$y.hat
    yu  <- frcst$y.upr;  yl_ <- frcst$y.lwr
    clr.f      <- frcst$clr.f
    band.alpha  <- frcst$band.alpha

    # plotly rgba variants
    fit.rgba  <- .maketrans_plotly(clr.f, alpha=0.35)  # muted: fit line & PI lines
    fore.rgba <- .maketrans_plotly(clr.f, alpha=0.90)  # solid: forecast points/line
    band.rgba <- .maketrans_plotly(clr.f, alpha=band.alpha)  # light: PI band fill

    # formatted date strings carried as customdata for hover tooltips
    xf.fmt <- .format_date_labels(as.Date(xf, origin="1970-01-01"), ts_unit)
    xh.fmt <- .format_date_labels(as.Date(xh, origin="1970-01-01"), ts_unit)

    fmty <- .get.tick.fmt(c(yf, yh, yu, yl_), digits_d)
    yfmt <- if (nzchar(fmty)) paste0(":", fmty) else ""

    # 1. model fit line (over the historical data range)
    plt <- plotly::add_trace(plt,
      type="scatter", mode="lines",
      x=xf, y=yf,
      customdata=xf.fmt,
      line=list(color=fit.rgba, width=1.5),
      name="Model fit", legendgroup="frcst_fit",
      hovertemplate=paste0(
        "Date: %{customdata}<br>",
        "Fit: %{y", yfmt, "}<extra></extra>"
      ),
      showlegend=TRUE,
      inherit=FALSE
    )

    # 2. connector: last fit point to first forecast point (no legend entry)
    plt <- plotly::add_trace(plt,
      type="scatter", mode="lines",
      x=c(xf[length(xf)], xh[1]),
      y=c(yf[length(yf)], yh[1]),
      line=list(color=fit.rgba, width=1.5),
      showlegend=FALSE,
      hoverinfo="skip",
      inherit=FALSE
    )

    # 3. PI band: filled polygon (upper boundary + reversed lower boundary)
    plt <- plotly::add_trace(plt,
      type="scatter", mode="none",
      x=c(xh, rev(xh)),
      y=c(yu, rev(yl_)),
      fill="toself",
      fillcolor=band.rgba,
      name="95% PI", legendgroup="frcst_pi",
      showlegend=TRUE,
      hoverinfo="skip",
      inherit=FALSE
    )

    # 4. PI boundary lines (upper and lower, light dashed — no legend clutter)
    plt <- plotly::add_trace(plt,
      type="scatter", mode="lines",
      x=xh, y=yu,
      customdata=xh.fmt,
      line=list(color=fit.rgba, width=1, dash="dot"),
      showlegend=FALSE,
      hovertemplate=paste0(
        "Date: %{customdata}<br>",
        "Upper PI: %{y", yfmt, "}<extra></extra>"
      ),
      inherit=FALSE
    )
    plt <- plotly::add_trace(plt,
      type="scatter", mode="lines",
      x=xh, y=yl_,
      customdata=xh.fmt,
      line=list(color=fit.rgba, width=1, dash="dot"),
      showlegend=FALSE,
      hovertemplate=paste0(
        "Date: %{customdata}<br>",
        "Lower PI: %{y", yfmt, "}<extra></extra>"
      ),
      inherit=FALSE
    )

    # 5. forecast points + line
    px.f <- as.numeric(pt.size) * 7.25
    if (!is.finite(px.f) || px.f <= 0) px.f <- 6
    plt <- plotly::add_trace(plt,
      type="scatter", mode="lines+markers",
      x=xh, y=yh,
      customdata=xh.fmt,
      marker=list(
        symbol="circle",
        size=px.f,
        color=fore.rgba,
        line=list(color=fore.rgba, width=1)
      ),
      line=list(color=fore.rgba, width=2),
      name="Forecast", legendgroup="frcst_fore",
      hovertemplate=paste0(
        "Date: %{customdata}<br>",
        "Forecast: %{y", yfmt, "}<extra></extra>"
      ),
      showlegend=TRUE,
      inherit=FALSE
    )
  }  # end forecast


  # ---- ellipses (support per-group) ----
  if (!is.null(ellipse_region)) {
    to.poly.list <- function(obj) {
      if (is.list(obj) && !is.data.frame(obj)) return(obj)
      list(obj)
    }
    ell.list <- to.poly.list(ellipse_region)
    n.ell <- length(ell.list)
    target.len <- if (has.groups) length(grp.names) else 1L
    if (n.ell != target.len) ell.list <- rep(ell.list, length.out = target.len)

    edge.vec <- if (length(ellipse_color))
                    .to_hex(rep_len(ellipse_color, target.len))
                 else .to_hex(rep_len(border, target.len))
    lw.vec   <- as.numeric(if (length(ellipse_lwd))
                       rep_len(ellipse_lwd, target.len) else rep(1, target.len))

    fill.vec <- if (length(ellipse_fill))
                       .to_hex(rep_len(ellipse_fill, target.len))
                 else rep("transparent", target.len)

    for (k in seq_len(target.len)) {
      el <- as.data.frame(ell.list[[k]])
      if (ncol(el) >= 2 && nrow(el) >= 3) {
        plt <- plotly::add_trace(
          plt,
          type = "scatter",
          mode = "lines",
          x = el[[1]], y = el[[2]],
          line = list(color = edge.vec[k], width = lw.vec[k]),
          fill = "toself",
          fillcolor = fill.vec[k],
          showlegend = FALSE,
          hoverinfo = "skip",
          hovertemplate = NULL,
          hoveron = "points",
          inherit = FALSE
        )
      }
    }
  }

  # ---- SE bands ----
  if (!is.null(se.poly) && length(se.poly)) {
    for (b in seq_along(se.poly)) {
      sb <- as.data.frame(se.poly[[b]])
      plt <- plotly::add_trace(
        plt,
        type = "scatter",
        mode = "none",
        x = sb[[1]], y = sb[[2]],
        fill = "toself",
        fillcolor = .to_hex(se_fill %||% "transparent"),
        hoverinfo = "skip",
        showlegend = FALSE,
        inherit = FALSE
      )
    }
  }

  # ---- fits ----
  fit.rows <- is.finite(g$x) & is.finite(g$f)
  if (any(fit.rows)) {
    fit.valid   <- fit.rows & !is.na(g$ind)
    ind.fit.chr <- trimws(as.character(g$ind[fit.valid]))
    idx.fit     <- split(which(fit.valid), ind.fit.chr, drop = TRUE)
    has.group.fits <- length(idx.fit) > 1L

    if (!has.group.fits) {
      xl <- as.numeric(g$x[fit.rows]); yl <- as.numeric(g$f[fit.rows])
      ok <- is.finite(xl) & is.finite(yl); xl <- xl[ok]; yl <- yl[ok]
      if (length(xl) >= 2L) {
        fmtx <- .get.tick.fmt(xl, digits_d); fmty <- .get.tick.fmt(yl, digits_d)
        hover <- paste0(
          x_lab, ": %{x", if (nzchar(fmtx)) paste0(":", fmtx) else "", "}",
          "<br>Fit: %{y",  if (nzchar(fmty)) paste0(":", fmty) else "", "}",
          "<extra></extra>"
        )
        plt <- plotly::add_trace(
          plt, type = "scatter", mode = "lines",
          name = "Fit",
          x = xl, y = yl,
          line = list(
            color = .to_hex(fit_color[1]),
            width = as.numeric(if (length(fit_lwd)) fit_lwd[1] else 2)
          ),
          hovertemplate = hover,
          showlegend    = TRUE,
          legendgroup   = "fit"
        )
      }
    } else {
      gnames <- names(idx.fit)
      for (i in seq_along(gnames)) {
        nm   <- gnames[i]
        rows <- idx.fit[[nm]]
        xl <- as.numeric(g$x[rows]); yl <- as.numeric(g$f[rows])
        ok <- is.finite(xl) & is.finite(yl); xl <- xl[ok]; yl <- yl[ok]
        if (length(xl) < 2L) next

        fmtx <- .get.tick.fmt(xl, digits_d); fmty <- .get.tick.fmt(yl, digits_d)
        hover <- paste0(
          x_lab, ": %{x", if (nzchar(fmtx)) paste0(":", fmtx) else "", "}",
          "<br>Fit (", nm, "): %{y", if (nzchar(fmty))
                                     paste0(":", fmty) else "", "}",
          "<extra></extra>"
        )

        col.i <- if (length(fill) > 1L) {
          .to_hex(fill[i])  # match group fill, as base R path does
        } else {
          .to_hex(if (length(fit_color) >= i) fit_color[i] else border[i])
        }
        lw.i  <- if (length(fit_lwd)) fit_lwd[min(i, length(fit_lwd))] else 2

        plt <- plotly::add_trace(
          plt, type = "scatter", mode = "lines",
          name = paste0("Fit: ", nm),
          legendgroup = nm,
          x = xl, y = yl,
          line = list(color = col.i, width = lw.i),
          hovertemplate = hover,
          showlegend = TRUE
        )
      }
    }
  }

  # ---- axes, grids, legend, bg ----
  x.axis <- axis_num(x_lab, ax$axT1, ax$axL1)
  y.axis <- axis_num(y_lab, ax$axT2, ax$axL2)
  border.shapes <- plot_border()

  leg.color <- .to_hex(style_opts$lab_color)

  plt <- plotly::layout(
    plt,
    xaxis = x.axis, yaxis = y.axis,
    shapes = c(x_grid(gridT1), y_grid(gridT2), border.shapes),
    legend = list(
      title = list(
        text = if (has.groups) by_name else NULL,
        font = list(size = round(15 * getOption("lab_size", 1)),
                    family = "Arial", color = leg.color)
      ),
      font = list(size = round(16 * getOption("axis_size", 0.9)),
                  family = "Arial", color = leg.color),
      orientation = "v",
      x = 1.05, y = 0.5,
      xanchor = "left", yanchor = "middle",
      bgcolor = .to_hex(style_opts$window_fill),
      bordercolor = "#CCCCCC",
      borderwidth = 1,
      itemsizing = "constant"
    ),
    template = NULL,
    uniformtext = list(minsize = 10, mode = "hide")
  )

  plt$x$layout$plot_bgcolor  <- .to_hex(style_opts$panel_fill)
  plt$x$layout$paper_bgcolor <- .to_hex(style_opts$window_fill)

  # resolve axis/label/grid colors using the same NULL-fallback logic
  # as all other lessR plotting functions
  lab.clr <- style_opts$lab_color

  axis.x.clr      <- style_opts$axis_x_color      %||% style_opts$axis_color
  axis.y.clr      <- style_opts$axis_y_color      %||% style_opts$axis_color
  axis.x.text.clr <- style_opts$axis_x_text_color %||% style_opts$axis_text_color
  axis.y.text.clr <- style_opts$axis_y_text_color %||% style_opts$axis_text_color
  grid.x.clr      <- style_opts$grid_x_color      %||% style_opts$grid_color
  grid.y.clr      <- style_opts$grid_y_color      %||% style_opts$grid_color

  # convert to hex for plotly
  lab.clr         <- .to_hex(lab.clr)
  axis.x.clr      <- .to_hex(axis.x.clr)
  axis.y.clr      <- .to_hex(axis.y.clr)
  axis.x.text.clr <- .to_hex(axis.x.text.clr)
  axis.y.text.clr <- .to_hex(axis.y.text.clr)
  grid.x.clr      <- .to_hex(grid.x.clr)
  grid.y.clr      <- .to_hex(grid.y.clr)

  # x-axis
  if (!is.null(plt$x$layout$xaxis$title))
    plt$x$layout$xaxis$title$font$color <- lab.clr
  if (is.null(plt$x$layout$xaxis$tickfont))
    plt$x$layout$xaxis$tickfont <- list()
  plt$x$layout$xaxis$tickfont$color <- axis.x.text.clr
  plt$x$layout$xaxis$linecolor      <- axis.x.clr
  plt$x$layout$xaxis$tickcolor      <- axis.x.clr
  plt$x$layout$xaxis$gridcolor      <- grid.x.clr

  # y-axis
  if (!is.null(plt$x$layout$yaxis$title))
    plt$x$layout$yaxis$title$font$color <- lab.clr
  if (is.null(plt$x$layout$yaxis$tickfont))
    plt$x$layout$yaxis$tickfont <- list()
  plt$x$layout$yaxis$tickfont$color <- axis.y.text.clr
  plt$x$layout$yaxis$linecolor      <- axis.y.clr
  plt$x$layout$yaxis$tickcolor      <- axis.y.clr
  plt$x$layout$yaxis$gridcolor      <- grid.y.clr

  # patch any custom grid shapes (x_grid/y_grid bake their color in at call time)
  if (!is.null(plt$x$layout$shapes)) {
    plt$x$layout$shapes <- lapply(plt$x$layout$shapes, function(sh) {
      if (!is.null(sh$line)) sh$line$color <- grid.x.clr
      sh
    })
  }

  if (is.null(plt$x$layout$margin)) plt$x$layout$margin <- list()

  if (!nzchar(x_lab)) {
    plt$x$layout$margin$b <- 0L
  } else {
    b.default <- plt$x$layout$margin$b %||% 40
    plt$x$layout$margin$b <- max(b.default, 60)
  }

  if (!is.null(main) && nzchar(main)) {
    plt <- plotly::layout(plt,
      title  = list(text = main, x = 0.5,
                    xanchor = "center", y = 0.99, yanchor = "top",
                    font = list(size = title.size)),
      margin = list(t = round(title.size * 2.2))
    )
  }

  plt <- .finalize_plotly_widget(
    plt,
    kind = "sp",
    x_name = x_lab,
    by_name = by_name,
    add_title = FALSE,
    nudge_viewer = TRUE
  )

  invisible(plt)
}
