Chart <- function(x=NULL, y=NULL, data=d, filter=NULL,

        by=NULL,
        facet=NULL, n_row=NULL, n_col=NULL, aspect="fill",

        form=c("bar", "radar", "bubble", "dot", "pie", "icicle", "treemap"),

        hole=0.65,  # pie chart
        radius=0.50, power=0.5,  # bubble chart
        segments_x=NULL, segments_y=NULL,
        pt_size=1, origin_x=NULL, origin_y=NULL, # dot chart

        stat=c("mean", "sum", "sd", "deviation", "min", "median", "max"),
        stat_x=c("count", "proportion"),

        horiz=FALSE, sort=c("0", "-", "+"),
        beside=FALSE, stack100=FALSE,
        gap=NULL, scale_y=NULL, one_plot=NULL,

        theme=getOption("theme"),
        fill=NULL,
        color=getOption("bar_color_discrete"),
        transparency=getOption("trans_bar_fill"),
        fill_split=NULL, fill_scaled=FALSE, fill_chroma=75,

        labels=c("%", "input", "prop", "off"),
        labels_position=c("in","out"),
        labels_color="white",
        labels_size=0.90,
        labels_decimals=NULL,
        labels_cut=NULL,

        xlab=NULL, ylab=NULL, main=NULL, sub=NULL,
        lab_adjust=c(0,0), margin_adjust=c(0,0,0,0),
        pad_y_min=0, pad_y_max=0,

        rotate_x=getOption("rotate_x"), rotate_y=getOption("rotate_y"),
        break_x=NULL, offset=getOption("offset"),
        axis_fmt=c("K", ",", ".", ""), axis_x_pre="", axis_y_pre="",
        font_size=1, label_max=100,

        legend_title=NULL, legend_position="right_margin",
        legend_labels=NULL, legend_horiz=FALSE,
        legend_size=NULL, legend_abbrev=10, legend_adjust=0,

        add=NULL, x1=NULL, y1=NULL, x2=NULL, y2=NULL,

        quiet=getOption("quiet"), do_plot=TRUE,
        use_plotly=getOption("lessR.use_plotly"),
        pdf_file=NULL, width=6.5, height=6,
        digits_d=NULL,

        eval_df=NULL, fun_call=NULL, ...) {


  if (is.null(fun_call)) fun_call <- match.call()

  if (font_size != 1) {
    .fs_main   <- getOption("main_size", 1.1)
    .fs_lab    <- getOption("lab_size",  1.0)
    .fs_axis   <- getOption("axis_size", 0.76)
    .fs_axis_x <- getOption("axis_x_size")
    .fs_axis_y <- getOption("axis_y_size")
    options(main_size = .fs_main * font_size,
            lab_size  = .fs_lab  * font_size,
            axis_size = .fs_axis * font_size)
    if (!is.null(.fs_axis_x))
      options(axis_x_size = .fs_axis_x * font_size)
    if (!is.null(.fs_axis_y))
      options(axis_y_size = .fs_axis_y * font_size)
    on.exit({
      options(main_size=.fs_main, lab_size =.fs_lab, axis_size=.fs_axis)
      if (!is.null(.fs_axis_x)) options(axis_x_size = .fs_axis_x)
      if (!is.null(.fs_axis_y)) options(axis_y_size = .fs_axis_y)
    }, add = TRUE)
  }

  # collected plotly style options, passed down to plotly internals so they
  # don't reach into getOption() themselves
  style.opts <- .plotly_style()

  mc <- match.call()
  .mc.nms <- names(mc)
  y.is.named <- "y" %in% .mc.nms

  # Note: if fill contains getColors() call, fill already evaluated
  fill.name <- deparse(substitute(fill))

  if (length(form) == 1 && form == "sunburst")
    form <- "pie"
  else
    form <- match.arg(form)

  stat.miss <- missing(stat)
  if (stat.miss) stat <- NULL
  if (!is.null(stat[1])) stat <- match.arg(stat)  # if condition for shiny
  stat_x <- match.arg(stat_x)

  options(xname = NULL)
  options(yname = NULL)
  options(byname = NULL)

  if ("facet2" %in% .mc.nms) {
    cat("\n"); stop(call.=FALSE, "\n------\n",
      "parameter  facet2  not applicable to Chart\n\n")
  }

  facet.miss <- missing(facet)

  if (!is.null(pdf_file) && form != "bar") {
    cat("\n"); stop(call.=FALSE, "\n------\n",
      "Saving PDF files only works for bar charts.\n\n")
  }

  if (form != "dot") {
    if (!quiet && use_plotly && !(form == "bar" && !facet.miss)) {
      txt <- "[Interactive chart from the Plotly R package (Sievert, 2020)]"
      cat(txt, "\n\n")
    }
  }

  labels_size.miss <- missing(labels_size)
  if (!facet.miss && labels_size.miss) labels_size <- .85


# old stuff ---------------------------------------------------------------

  # Check for removed parameters. Uses .mc.nms (names(match.call())) defined
  # at the top of the function — avoids evaluating expressions prematurely.
  if ("rows" %in% .mc.nms) {
    stop(call. = FALSE, "\n------\n",
         "Parameter  rows  has been removed.\n",
         "Use  filter  instead, e.g., filter = (Age > 30)\n\n")
  }
  if ("n_cat" %in% .mc.nms) {
    stop(call. = FALSE, "\n------\n",
         "Parameter  n_cat  has been removed.\n",
         "Convert categorical integer variables to factors instead:\n",
         "  e.g., d$Item1 <- factor(d$Item1)\n\n")
  }
  if ("value_labels" %in% .mc.nms) {
    stop(call. = FALSE, "\n------\n",
         "Parameter  value_labels  has been removed.\n",
         "Convert categorical integer variables to factors instead:\n",
         "  e.g., d$Item1 <- factor(d$Item1, levels=1:3,\n",
         "         labels=c(\"Disagree\", \"Neutral\", \"Agree\"))\n\n")
  }

  if (fill.name == "(count)") {
    message(">>> Now set to TRUE the more general parameter:  fill_scale.\n\n")
    fill <- NULL
  }

  dots <- list(...)
  .style.trap(names(dots))
  n.values <- 0
  if (length(dots) > 0) {
    for (i in seq_along(dots)) {
      if (names(dots)[i] == "type") {
        cat("\n"); stop(call.=FALSE, "\n","------\n",
            "Parameter  type  renamed  form\n\n")
      }
      if (grepl("values", names(dots)[i], fixed=TRUE)) {
        n.values <- n.values + 1
        if (n.values == 1)
          message(">>> Parameters  values, values_color, etc. now ",
                  "renamed to:  labels, labels_color, etc.\n",
                  "    Old parameter names will stop working in the future.\n")
        if (names(dots)[i] == "values") labels <- dots[[i]]
        if (names(dots)[i] == "values_color") labels_color <- dots[[i]]
        if (names(dots)[i] == "values_size") labels_size <- dots[[i]]
        if (names(dots)[i] == "values_digits") labels_decimals <- dots[[i]]
        if (names(dots)[i] == "values_position") labels_position <- dots[[i]]
        if (names(dots)[i] == "values_cut") labels_cut <- dots[[i]]
      }
      if (names(dots)[i] == "addtop") pad_y_max <- dots[[i]]
      if (names(dots)[i] == "add_top") pad_y_max <- dots[[i]]
      if (names(dots)[i] == "stat_yx") {
        stat <- dots[[i]]
        stat.miss <- FALSE
      }
      if (grepl(".", names(dots)[i], fixed=TRUE)) {
        pnm <- gsub(".", "_", names(dots)[i], fixed=TRUE)  # dot to _
        assign(pnm, dots[[i]])
      }
    }
  }

  if (!is.null(stat) && stat[1] == "proportion") {
    cat("\n"); stop(call.=FALSE, "\n------\n",
      "now use parameter  stat_x  for \"proportion\" \n",
      "  \"proportion\" only applies when there is no y numeric variable\n\n")
  }

  # validate stat set from legacy stat_yx, which bypasses match.arg above
  if (!is.null(stat))
    stat <- match.arg(stat, c("mean", "sum", "sd", "deviation",
                              "min", "median", "max"))


  Trellis <- !facet.miss && form == "bar"


# set variables -----------------------------------------------------------

  trans <- transparency

  if (fill_scaled  &&  is.null(fill_split)) fill_split <- 0

  if (nzchar(axis_fmt[1])) axis_fmt <- match.arg(axis_fmt)

  fill.miss <- missing(fill)
  color.miss <- missing(color)
  transparency.miss <- missing(transparency)
  horiz.miss <- missing(horiz)
  y.miss <- missing(y)
  by.miss <- missing(by)
  main.miss <- missing(main)
  theme.miss <- missing(theme)
  hier.type <- form %in% c("treemap", "icicle") ||
               (form == "pie" && !by.miss)
  for (i in seq_along(color)) if (color[i] == "off") color[i] <- "transparent"

  sort.miss <- missing(sort)
  sort <- match.arg(sort)
  labels.miss <- missing(labels)
  labels <- match.arg(labels)
  labels_position <- match.arg(labels_position)


# set some parameter values -----------------------------------------------

  # if theme is set locally
  if (!theme.miss) {
    old_theme <- getOption("theme")
    if (theme != old_theme) {
      style(theme)
      on.exit(style(old_theme), add=TRUE)
      style.opts <- .plotly_style()  # re-capture under new theme
      if (font_size != 1) {  # style() reset font sizes; re-scale
        options(main_size = getOption("main_size", 1.1) * font_size,
                lab_size  = getOption("lab_size",  1.0) * font_size,
                axis_size = getOption("axis_size", 0.76) * font_size)
        .new_x <- getOption("axis_x_size")
        .new_y <- getOption("axis_y_size")
        if (!is.null(.new_x)) options(axis_x_size = .new_x * font_size)
        if (!is.null(.new_y)) options(axis_y_size = .new_y * font_size)
      }
    }
    sty <- style(theme, reset=FALSE)
    if (fill.miss && !hier.type && form != "radar") {
      if (theme == "colors")
        fill <- rgb(150,170,195, maxColorValue=255)
      else
        fill <- sty$bar$bar_fill_discrete
    }
    if (color.miss) color <- sty$bar$color
    if (transparency.miss) trans <- sty$bar$trans_fill
    if (is.null(trans)) trans <- 0.1  # kludge, should not be NULL
  }

  proportion <- stat_x[1] == "proportion"

  if (horiz) sort <- switch(sort, "+" = "-", "-" = "+", "0" = "0")

  if (missing(break_x))
    break_x <- !horiz && rotate_x == 0

  xlab.adj <- lab_adjust[1];   ylab.adj <- lab_adjust[2]
  tm.adj <- margin_adjust[1];  rm.adj <- margin_adjust[2]
  bm.adj <- margin_adjust[3];  lm.adj <- margin_adjust[4]

  lm.adj <- lm.adj + .1  # pull these margins back a bit for bc
  bm.adj <- bm.adj + .1

  if (missing(labels_color)) {
    labels_color <- "white"
    if (labels_position == "out") labels_color <- getOption("axis.text.color")
  }

  # ensure valid parameter values
  # exception: fill=c(lo, hi) with fill_split is user-defined divergent scale —
  # pass fill.miss=TRUE to the validator so it doesn't block this combination
  fill.miss.valid <- fill.miss ||
                    (!fill.miss && !is.null(fill_split) && length(fill) == 2L)
  .bcParamValid(y.miss, by.miss, facet.miss, Trellis, sort,
                fill_split, fill_scaled, fill_chroma, theme,
                fill.miss.valid, labels_position, stat.miss)



# data frame stuff --------------------------------------------------------

  data.d <- missing(data)

  # let deprecated mydata work as default
  dfs <- .getdfs()
  mydata.ok <- FALSE
  if (!is.null(dfs)) {
    if ("mydata" %in% dfs  &&  !("d" %in% dfs)) {
      d <- mydata
      rm(mydata)
      df.name <- "mydata"
      mydata.ok <- TRUE
      options(dname = df.name)
    }
  }

  # get name of data table
  # df.name is NULL if from shiny interact(), not from user-written shiny code
  if (!mydata.ok) {
    df.name <- deparse(substitute(data))
    options(dname = df.name)
  }

  shiny <- FALSE
  if (!is.null(sys.call(-1)))  # is NULL when called directly from R console
    if (sys.call(-1) == "renderPlot()") {  # from shiny, user or interact()
      shiny <- TRUE
      data <- eval(substitute(data), envir=parent.frame())
    }

  # if a tibble, convert to data frame
  if (!shiny) {
    if (exists(df.name, envir=.GlobalEnv)) {
      if (any(grepl("tbl", class(data), fixed=TRUE)))
        data <- data.frame(data)
    }
  }
  else  # no check for existence of df.name
    if (any(grepl("tbl", class(data), fixed=TRUE)))
      data <- data.frame(data)

  x.name <- deparse(substitute(x), width.cutoff = 120L)
  # deparse of a vector (e.g. from Shiny with data=NULL) produces multiple strings
  # use xlab as the display name instead, falling back to "x"
  if (length(x.name) > 1L)
    x.name <- if (!is.null(xlab) && nzchar(xlab)) xlab else "x"
  options(xname = x.name)

  # classify x: a computational expression evaluates directly, with a
  # data-column reference resolved from the data frame, then the
  # caller's environment; tryCatch avoids forcing the data=d promise
  x.expr    <- substitute(x)
  x.is.expr <- !missing(x) && .is_expr_arg(x.expr)
  x.uses.df <- FALSE
  if (x.is.expr)
    x.uses.df <- df.name != "NULL" && length(all.vars(x.expr)) > 0L &&
                 any(all.vars(x.expr) %in%
                     tryCatch(names(data), error=function(e) character(0)))

  # validate x.name is not an expression; global iff user set data=NULL
  # skip validation when called from Shiny with pre-extracted vectors (data=NULL)
  if (!x.is.expr) {
    if (!is.null(x.name) && !(shiny && df.name == "NULL"))
      .in.global(x.name, quiet)
    x.in.global <- if (shiny && df.name == "NULL") TRUE
                   else df.or.global(x.name, df.name, role="x",
                                     fun.name="Chart")
  }
  else
    x.in.global <- !x.uses.df
  if (!x.in.global) {
    # force evaluation (not lazy) if data not specified, relies on default d
    if (data.d) {
      if (!mydata.ok) .nodf(df.name)  # check to see if df exists
      # the 1.201 comes from Shiny, need to reset
      # l.cex and l.axc are set in interact() before shiny run
      if (getOption("lab_size") == 1.201) {
       if (getOption("l.cex") != 1.201) {
          style(lab_size=getOption("l.cex"))
          style(axis_size=getOption("l.axc"))
        }
        else
          style()
      }
    }
  }

  # respect a caller-supplied eval_df, here and again for y below
  eval_df.user <- eval_df
  eval_df <- if (is.null(eval_df.user)) !x.in.global else eval_df.user


# subset filter -----------------------------------------------------------

  if (!missing(filter))
    data <- .apply_filter(data, deparse(substitute(filter)),
                          parent.frame(), x.in.global, quiet)


# establish if a data frame, otherwise identify variable(s)----------------
  # x can be missing entirely, with a data frame passed instead
  # if x a vector, then x.name not in data, but also not in global

  x.call <- NULL

  if (x.name == "NULL") x.name <- ""    # 271, if x NULL, then x.name is "NULL"
  if (x.name %in% c("row_names", "row.names")) {
    # retain order of row names, otherwise will be alphabetical
    x.call <- factor(row.names(data), levels=row.names(data))
    if (is.null(xlab)) xlab <- ""  # unless specified, drop the axis label
  }

  else if (!missing(x)) {

    # x is a computational expression, evaluate directly
    if (x.is.expr) {
      val <- .expr_eval(x.expr, x.name, if (x.uses.df) data else NULL,
                        parent.frame(), role="x", fun.name="Chart")
      if (is.data.frame(val))
        data <- val   # mirrors the global data frame branch below
      else
        x.call <- val
    }

    else if (!x.in.global) {
      if (eval_df) {
        if (!mydata.ok) if (!shiny) .nodf(df.name)  # check to see if df exists
        .xcheck(x.name, df.name, names(data))  # x-vars in df?
      }

      data.vars <- as.list(seq_along(data))
      names(data.vars) <- names(data)
      ind <- eval(substitute(x), envir=data.vars)  # col num of each var
      if (length(ind) > 1) data <- data[, ind]  # x a vars list, no by vars
      if (length(ind) == 1) x.call <- eval(substitute(data$x))  # x is 1 var
    }

    else {  # x is in the global environment (vector, matrix or data frame)
      if (is.data.frame(x))  # x a data frame
        data <- x
      else {  # x a vector or matrix in global
        if (exists(x.name, envir=.GlobalEnv)) if (is.matrix(x)) {
          x.name <- xlab
          xlab <- NULL
          by.name <- legend_title
          options(xname = x.name)
          options(byname = by.name)
        }
        x.call <- x
        if (is.function(x.call)) x.call <- eval(substitute(data$x))
      }
    }

    # if read from console with text parameter can insert extra space at front
    if (is.factor(x.call)) {
      if (nchar(levels(x.call)[1]) > 5)
        levels(x.call) <- trimws(levels(x.call), which="left")
    }
    else if (is.character(x.call)) {
      if (nchar(x.call[1]) > 5) x.call <- trimws(x.call, which="left")
    }
  }  # !missing x


  # -------------------------------------------------
  # -------------------------------------------------
  # x is a single var, not a data frame or a var list
  if (!is.null(x.call)) {


# --- resolve `by`, which can be a vector ---------------------------------

    # shared helper: retrieve column from data frame or global environment
    caller.env <- parent.frame()
    get.col <- function(nm) {
      if (df.name != "NULL") {
        .xcheck(nm, df.name, names(data))
        return(data[[nm]])
      } else {
        return(get(nm, envir = caller.env))
      }
    }

    by.v.raw <- NULL  # pre-coercion by values, for categorical validation

    if (!by.miss) {
      by.expr <- substitute(by)
      by.vars <- all.vars(by.expr)
      by.name <- NULL  # default; set below, but by may resolve to NULL

      if (.is_expr_arg(by.expr)) {  # by is a computational expression
        by.name <- paste(deparse(by.expr, width.cutoff=120L),
                         collapse="")
        uses.df <- df.name != "NULL" &&
                   any(by.vars %in%
                       tryCatch(names(data),
                                error=function(e) character(0)))
        by.call <- .expr_eval(by.expr, by.name,
                              if (uses.df) data else NULL,
                              parent.frame(), role="by",
                              fun.name="Chart")
        by.v.raw <- by.call
        if (!is.factor(by.call)) by.call <- factor(by.call)
        if (length(by.call) != length(x.call)) {
          cat("\n"); stop(call.=FALSE, "\n------\n",
            "x has ", length(x.call), " values, but the  by  ",
            "expression\n",
            "  ", by.name, "  yields ", length(by.call),
            " values\n\n")
        }
      }
      else if (length(by.vars) == 0L) {
        # already-evaluated object (rare); coerce to factor
        by.call <- try(eval(by.expr, envir = parent.frame()), silent = TRUE)
        if (inherits(by.call, "try-error")) by.call <- NULL
        if (!is.null(by.call))
          by.v.raw <- if (is.data.frame(by.call)) by.call[[1]] else by.call
        if (!is.null(by.call) && !is.data.frame(by.call)) {
          if (!is.factor(by.call)) by.call <- factor(by.call)
          by.name <- deparse(by.expr)
        } else if (is.data.frame(by.call)) {
          for (nm in names(by.call))
            if (!is.factor(by.call[[nm]]))
                by.call[[nm]] <- factor(by.call[[nm]])
          by.name <- paste(names(by.call), collapse = " - ")
        }
      }
      else if (length(by.vars) == 1L) {
        by.call <- get.col(by.vars[1])
        by.v.raw <- by.call
        if (!is.factor(by.call)) by.call <- factor(by.call)
        by.name <- by.vars[1]
      }
      else {
        # multiple by variables only apply to the hierarchical charts:
        #   pie/sunburst, treemap, and icicle
        if (!hier.type) {
          cat("\n"); stop(call.=FALSE, "\n------\n",
            "Only one  by  variable is permitted for a  ", form, "  chart,\n",
            "  as the resulting visual clutter compromises interpretability.\n",
            "  Multiple  by  variables apply only to the hierarchical charts --\n",
            "  pie/sunburst, treemap, and icicle -- where they define the\n",
            "  levels of the hierarchy.\n\n")
        }
        # vector of by's -> data.frame of factors
        by.df <- lapply(by.vars, get.col)
        by.v.raw <- by.df[[1]]
        by.df <- lapply(by.df, function(v)
                        if (is.factor(v)) droplevels(v) else factor(v))
        by.call <- as.data.frame(by.df, check.names = FALSE)
        names(by.call) <- by.vars
        by.name <- paste(by.vars, collapse = " - ")
      }
      options(byname = by.name)

      if (labels_position == "out"  &&  !beside) {
        stop(call.=FALSE, "\n------\n",
          "labels_position=\"out\" not meaningful for a  by  variable\n",
          "  without beside=TRUE\n\n")
      }
    }  # end not by.miss 

    else {
      by.call <- NULL
      by.name <- NULL
    }

    if (form == "dot" && !is.null(by.call)) {
      cat("\n"); stop(call.=FALSE, "\n------\n",
        "The  by  variable is not meaningful for dot charts. Do a bar chart.\n\n")
    }


# --- resolve y ------------------------------------------------------

  y.name <- deparse(substitute(y), width.cutoff = 120L)  # can be NULL
  if (length(y.name) > 1L) y.name <- paste(y.name, collapse="")

  # classify y as a computational expression, as for x
  y.expr    <- substitute(y)
  y.is.expr <- !missing(y) && .is_expr_arg(y.expr)
  y.uses.df <- FALSE
  if (y.is.expr)
    y.uses.df <- df.name != "NULL" && length(all.vars(y.expr)) > 0L &&
                 any(all.vars(y.expr) %in%
                     tryCatch(names(data), error=function(e) character(0)))

  # process row.names if specified, applicable to form="dot"
  if (y.name %in% c("row_names", "row.names")) {
    # retain order of row names, otherwise will be alphabetical
    y.call <- data.frame(factor(row.names(data), levels=row.names(data)))
    if (is.null(ylab)) ylab <- ""  # unless specified, drop the axis label
  }

  else {
    # validate y.name is not an expression; global iff user set data=NULL
    if (!y.is.expr) {
      if (!is.null(y.name) && !(shiny && df.name == "NULL"))
        .in.global(y.name, quiet)
      y.in.global <- if (shiny && df.name == "NULL") TRUE
                     else df.or.global(y.name, df.name, role="y",
                                       fun.name="Chart")
    }
    else
      y.in.global <- !y.uses.df
    eval_df <- if (is.null(eval_df.user)) !y.in.global else eval_df.user

    # if not missing, then must be aggregated data
    if (!missing(y)) {  # assign y.call from data or from global

      if (y.is.expr) {  # y is a computational expression
        y.call <- .expr_eval(y.expr, y.name,
                             if (y.uses.df) data else NULL,
                             parent.frame(), role="y",
                             fun.name="Chart")
      }
      else {
      # see if var exists in data frame, if y not in global Env or fun call
      if (eval_df) .xcheck(y.name, df.name, names(data))
      if (!y.in.global) {
        data.vars <- as.list(seq_along(data))
        names(data.vars) <- names(data)
        y.ind <- eval(substitute(y), envir = data.vars)
        if (length(y.ind) > 1L) {
          y.call <- data[, y.ind, drop = FALSE]   # multi-column: data frame
        } else {
          y.call <- eval(substitute(data$y))       # single column: vector
        }
      } else {  # vars that are function names get assigned to global
        y.call <- y
        if (is.function(y.call)) y.call <- eval(substitute(data$y))
      }
      }  # end y is a variable name, not an expression
      if (is.null(digits_d)) digits_d <- .max.dd(
        if (is.data.frame(y.call)) unlist(y.call) else y.call)
    }  # end !missing(y)

    else {
      y.lab <- "Count"
      y.call <- NULL
      if (is.null(digits_d)) digits_d <- 0  # y will be counts
    }
  }  # end !row_name

  # normalize: if y was missing or y=NULL, set y.name to actual NULL
  # (deparse produces the string "NULL" which fails is.null() checks downstream)
  if (missing(y)) y.name <- NULL
  options(yname = y.name)


# --- resolve facet --------------------------------------------------

  facet.call <- NULL
  facet.name <- NULL

  if (!missing(facet)) {
    facet.expr <- substitute(facet)
    facet.vars <- all.vars(facet.expr)

    if (.is_expr_arg(facet.expr)) {  # a computational expression
      facet.name <- paste(deparse(facet.expr, width.cutoff=120L),
                          collapse="")
      uses.df <- df.name != "NULL" &&
                 any(facet.vars %in%
                     tryCatch(names(data),
                              error=function(e) character(0)))
      v <- .expr_eval(facet.expr, facet.name,
                      if (uses.df) data else NULL,
                      parent.frame(), role="facet", fun.name="Chart")
      facet.call <- droplevels(factor(v))
      if (!is.null(x.call)  &&
          length(facet.call) != length(x.call)) {
        cat("\n"); stop(call.=FALSE, "\n------\n",
          "x has ", length(x.call), " values, but the  facet  ",
          "expression\n",
          "  ", facet.name, "  yields ", length(facet.call),
          " values\n\n")
      }
    }
    else if (length(facet.vars) == 1L) {
      # single facet variable — resolve to a factor vector
      v <- get.col(facet.vars[1L])
      facet.call <- if (is.factor(v)) droplevels(v) else factor(v)
      facet.name <- facet.vars[1L]
    } else {
      # multiple facet variables — resolve to a data frame of factors
      fac.list <- lapply(facet.vars, function(nm) {
        v <- get.col(nm)
        if (!is.factor(v)) v <- factor(v)
        droplevels(v)
      })
      facet.call <- as.data.frame(fac.list, stringsAsFactors = FALSE)
      names(facet.call) <- facet.vars
      facet.name <- paste(facet.vars, collapse = ", ")
    }

    # use the legacy option name expected by .bar.lattice()
    options(facet1name = facet.name)
  }

  # save pre-aggregation data for .hier.plotly() hover tooltips —
  # done once here after all four variables are resolved, before any aggregation
  if (form %in% c("pie", "icicle", "treemap")) {
    x.raw     <- x.call
    by.raw    <- by.call
    y.raw     <- y.call
    facet.raw <- facet.call
    # Multiple by= variables define hierarchy levels (kept as a data frame),
    # but multiple facet= variables define the panel grid, not extra levels.
    # Flatten to one interaction factor here -- same " / " convention used
    # for the radar/dot facet paths -- so .hier_aggregate() builds one panel
    # per combination instead of mangling the data frame.
    if (is.data.frame(facet.raw))
      facet.raw <- factor(apply(facet.raw, 1, paste, collapse = " / "))
  }


# ------------------------------------------------------------
# -----------  x, y, by, and facet variables established -----
# ------------------------------------------------------------

# suggestions -------------------------------------------------------------

    txsug <- ""
    if (getOption("suggest") && !quiet  && form != "dot") {
      # function call, with last ) removed for suggestions
      fncl <- .fun_call.deparse(fun_call)  # class call to class character
      fncl <- gsub(")$", "", fncl)  # get function call less closing )
      fncl <- gsub(" = ", "=", fncl)
      txsug <- ">>> Suggestions  or  enter: style(suggest=FALSE)"

      if (!is.null(by.call)) {
        by.sug <- if (grepl(" - ", by.name, fixed=TRUE))
                    paste0("c(", gsub(" - ", ", ", by.name, fixed=TRUE), ")")
                  else by.name
        fc <- paste("Chart(", x.name, ", by=", by.sug, sep="")
      } else
        fc <- paste("Chart(", x.name, sep="")

      if (!grepl("radar", fncl)) {
        txt <- ", form=\"radar\""  # many options"
        cmt <- "  # Plotly radar chart"
        txsug <- paste(txsug, "\n", fc, txt, ")", cmt, "\n", sep="")
      }

      if (!grepl("treemap", fncl)) {
        txt <- ", form=\"treemap\""  # many options"
        cmt <- "  # Plotly treemap chart"
        txsug <- paste(txsug, fc, txt, ")", cmt, "\n", sep="")
      }

      if (!grepl("pie", fncl)) {
        txt <- ", form=\"pie\""  # many options"
        cmt <- "  # Plotly pie/sunburst chart"
        txsug <- paste(txsug, fc, txt, ")", cmt, "\n", sep="")
      }

      if (!grepl("icicle", fncl)) {
        txt <- ", form=\"icicle\""  # many options"
        cmt <- "  # Plotly icicle chart"
        txsug <- paste(txsug, fc, txt, ")", cmt, "\n", sep="")
      }

      if (!grepl("bubble", fncl)) {
        txt <- ", form=\"bubble\""  # many options"
        cmt <- "  # Plotly bubble chart"
        txsug <- paste(txsug, fc, txt, ")", cmt, "\n", sep="")
      }

      txsug <- paste(txsug, "\n")

      class(txsug) <- "out"
      print(txsug)
    }  # end suggest


  # is a numerical, continuous variable in the second or by position?
  n.by <- if (is.null(by.call)) 0
          else if (is.data.frame(by.call)) nrow(unique(by.call))
               else length(unique(by.call))


  # numeric (pre-coercion) variable in the by position; by.call is a factor
  if (!is.null(by.call) && is.numeric(by.v.raw) && !y.is.named) {
    if (!stat.miss && y.miss) {
      cat("\n"); stop(call.=FALSE, "\n","------\n",
        "Parameter  stat  requires a numerical y-variable to transform.\n\n",
        "Perhaps you have a numerical variable that you wish to transform.\n",
        "If so, then you now need to provide the label:  y = ", by.name, "\n\n",
        "Or, if the variable is the 2nd categorical variable in the analysis\n",
        "  then label as:  by = ", by.name, "\n")
    }
  }


# not pre-aggregated data -------------------------------------------------

    n.x <- length(unique(x.call))
    lx <- if (is.data.frame(x.call)) nrow(x.call) else length(x.call)
    if (is.null(by.call))
      is.agg <- n.x >= lx
    else {
      lby.u <- if (is.data.frame(by.call)) nrow(unique(by.call))
               else length(unique(by.call))
      lby <- if (is.data.frame(by.call)) nrow(by.call) else length(by.call)
      is.agg <- n.x * lby.u >= lby
    }

  # set labels default for aggregated data 
    if (labels.miss && (!stat.miss || is.agg))
        labels <- if (beside) "%" else "input"


## ---- Missing data removal: x, y, by (possibly multi-column), facet ----

    # Save the full x.call and y.call before filtering — dot charts display
    # all category labels on the axis, with no dot where y is missing.
    x.call.all <- x.call
    y.call.all <- y.call

    # flatten multi-variable facet to a single interaction vector before
    # casewise missing removal — .drop_casewise_missing expects a vector
    if (is.data.frame(facet.call)) {
      facet.call <- factor(apply(facet.call, 1, paste, collapse = " / "))
    }

    # For multi-column y, collapse to a single logical NA-indicator vector
    # so .drop_casewise_missing() never sees a data frame — avoids tibble
    # recycling errors that occur when dplyr/tibble is loaded in session.
    y.for.cc <- if (is.data.frame(y.call) && ncol(y.call) > 1L)
      rowSums(is.na(y.call)) > 0  # TRUE where any column has NA
    else
      y.call

    cc <- .drop_casewise_missing(  # build data frame with relevant vars
      x.call    = x.call,
      y.call    = y.for.cc,
      by.call   = by.call,
      facet.call= facet.call
    )

    if (!is.null(cc) && !all(cc)) {

      # x.call: vector OR matrix/data.frame
      if (!is.null(x.call)) {
        if (is.matrix(x.call) || is.data.frame(x.call)) {
          x.call <- x.call[cc, , drop = FALSE]
        } else {
          x.call <- x.call[cc]
        }
      }

      # y.call: vector OR data.frame (multi-column y)
      if (!is.null(y.call)) {
        if (is.matrix(y.call) || is.data.frame(y.call)) {
          y.call <- y.call[cc, , drop = FALSE]
        } else {
          y.call <- y.call[cc]
        }
      }

      # by.call: vector OR matrix/data.frame
      if (!is.null(by.call)) {
        if (is.matrix(by.call) || is.data.frame(by.call)) {
          by.call <- by.call[cc, , drop = FALSE]
        } else {
          by.call <- by.call[cc]
        }
      }

      # facet.call: vector OR matrix/data.frame
      if (!is.null(facet.call)) {
        if (is.matrix(facet.call) || is.data.frame(facet.call)) {
          facet.call <- facet.call[cc, , drop = FALSE]
        } else {
          facet.call <- facet.call[cc]
        }
      }
    }

    # recompute n.x after casewise NA removal — used downstream by radar polygon
    # check, fill color count, and dot chart fill range
    n.x <- length(unique(x.call))


# check conditions when a y variable is present ---------------------------

    if (!is.null(y.call)) {  # a y variable present

      # y is the numerical variable that the chart aggregates, and it
      # is the second parameter, so a second CATEGORICAL variable
      # entered positionally lands here. Name  by  rather than
      # report a categorical variable as numerical.
      y.num <- if (is.data.frame(y.call))
                 all(vapply(y.call, is.numeric, logical(1)))
               else
                 is.numeric(y.call)
      if (!y.num) {
        cat("\n"); stop(call.=FALSE, "\n------\n",
          "Parameter  y  is the numerical variable that the chart\n",
          "aggregates, but  y = ", y.name, "  is categorical.\n\n",
          "To stratify by a second categorical variable, specify  by:\n",
          "    by = ", y.name, "\n\n")
      }

      if (is.agg) {  # a summary table
        if (!stat.miss) { # stat specified but data already aggregated
          cat("\n"); stop(call.=FALSE, "\n------\n",
            "The data are a summary table, so do not specify a value of\n",
            "  stat  as the data aggregation has already been done\n\n")
        }
        if (sum(is.na(x.call)) > 0 ||
              sum(is.na(by.call)) > 0 ||
              sum(is.na(y.call)) > 0)   {
            cat("\n"); stop(call.=FALSE, "\n------\n",
              "When reading a summary table, missing data not allowed.\n\n")
        }
      }  # is a summary table
      else {
        # require stat for a numeric y on raw data, as for every other form;
        # a dot plot with a multi-column (paired) y is exempt, as paired
        # values are displayed directly and never aggregated
        paired.dot <- form == "dot" &&
                      is.data.frame(y.call) && ncol(y.call) > 1L
        if (!is.agg && stat.miss && !paired.dot) {
          cat("\n"); stop(call.=FALSE, "\n------\n",
            "The data are not a summary (pivot) table, and you have a ",
            "numerical variable,\n",
            "    y = ", y.name, "\n",
            "so need to specify a value of  stat  to define the aggregation of\n",
            y.name, ", such as stat=\"mean\".\n\n")
        }
      }  # end is not a summary table
    }  # a y variable


# evaluate specified fill (NULL, numeric constant, or a variable) ---------

  if (!is.null(fill)) {
    if (length(fill.name) == 1) {
      if (exists(df.name, envir=.GlobalEnv))
        in.df <- fill.name %in% names(data)
      else
        in.df <- FALSE
    }
      else in.df <- FALSE
    # only works for y given, not tabulated
    if (in.df) {  # fill is a variable
    # need to aggregate cat var x and set fill.val to those limited values
    # currently, fill.val consists of all data values of variable fill
      fill.val <- data[[fill.name]]  # fill is a variable in data
      fill <- .getColC(fill.val, fill_name=fill.name)
      if (sort != "0") {
        srt.dwn <- sort == "-"
        fill <- fill[order(fill.val, decreasing=srt.dwn)]
      }
    }

    # evaluate getColors at the time of the function call
    # re-evaluate here by setting fill with the specified value of n
    if (substr(fill.name[1], 1, 9) == "getColors") {
      n.clr <- if (n.by > 0) n.by else max(n.x, 1)
      fill <- .resolve_getColors_n(fill.name, n.clr)
    }
  }  # end fill is present

  fill.palette <- if (is.null(fill)) .get_fill(theme)
                  else gsub("\"", "", fill.name)  # strip deparse quotes
  seq.pals <- c("grays", "reds", "rusts", "browns", "olives",
                "greens", "emeralds", "turquoises", "aquas", "blues",
                "purples", "violets", "magentas", "terrain")
  is.seq.fill <- tolower(fill.palette) %in% seq.pals ||
                 theme %in% c("gray", "white")
  if ((is.null(fill) || !theme.miss) && !hier.type) {
    if (n.by == 0) {
      if (theme.miss) fill <- NULL  # bc_main sets theme, no luminance gradient 
    } else if (is.seq.fill && n.by > 0) {
      # sequential with by=: n.by colors spanning full range, lighter at bottom
      fill <- rev(.color_range(.get_fill(theme), n.by))
    } else if (form == "bar" && beside && n.by > 0) {
      # beside bar with by=: one color per by-level, not per x-level
      fill <- .color_range(.get_fill(theme), n.by)
    } else {
      fill <- .color_range(.get_fill(theme), max(n.x, n.by))
    }
  }


# Trellis plots for bar plots ---------------------------------------------

  if (Trellis) {

    # facet2 not currently available
    if (do_plot)
      .bar.lattice(x.call, facet.call, facet2=NULL, n_row, n_col, aspect,
                   proportion, fill, color, trans, size.pt=NULL,
                   xlab, ylab, main, rotate_x, offset,
                   axis_fmt, axis_x_pre, axis_y_pre,
                   width, height, pdf_file,
                   segments_x=NULL, breaks=NULL,
                   bin_start=NULL, bin_width=NULL, bin_end=NULL,
                   T.type="bar", quiet)

    # do_plot=FALSE: nothing else applies, do not fall through to the
    # non-faceted bar chart analysis
    return(invisible(NULL))
  }


# set up pdf_file if needed -----------------------------------------------

  if (!is.null(pdf_file)) {
    if (!grepl(".pdf", pdf_file, fixed=TRUE))
      pdf_file <- paste(pdf_file, ".pdf", sep="")
    pdf(file=pdf_file, width=width, height=height, onefile=FALSE)
  }
  else {
    # only open a graphics device for bar charts, other forms use plotly widgets
    if (form == "bar" && df.name != "NULL")
        .opendev(pdf_file, width, height)
  }


# stat aggregation: y is present with raw data and stat not NULL ----------

  if (!is.agg) {
    if (!is.null(stat) && !is.null(y.call)) {  # y with stat

      ## ----- console stats BEFORE reducing data -------------------------
      if (!quiet) {
        txout <- ""
        ## only show the one-way numeric summary when there is
        ## truly only one variable (no by, no facet)
        if (is.null(by.call) && is.null(facet.call) &&
            !is.data.frame(y.call) && !is.matrix(y.call)) {
          options(yname = x.name)  # reverse x and y names, .ss.numeric
          options(xname = y.name)

          stats <- .ss.numeric(y.call, by = x.call, digits_d = digits_d,
                               brief = TRUE, y.name = x.name)
          txout <- stats$tx

          options(xname = x.name)  # reverse back
          options(yname = y.name)
        }

        class(txout) <- "out"
        output <- list(out_txt = txout)
        class(output) <- "out_all"
        print(output)
      }  # end !quiet


      ## ----- aggregate --------------------------------------------------
      is.agg <- TRUE

      # multi-column y (e.g. y=c(Pre,Post)): aggregate each column separately
      if (is.data.frame(y.call) && ncol(y.call) > 1L) {
        if (!is.null(by.call))
          stop(call.=FALSE, "\n------\n",
            "Multiple y variables combined with by= is not supported.\n",
            "Use a single y variable with by=, or omit by=.\n\n")
        if (!is.null(facet.call))
          stop(call.=FALSE, "\n------\n",
            "Multiple y variables combined with facet= is not supported.\n",
            "Use a single y variable with facet=, or omit facet=.\n\n")
        # wide result, one aggregated column per y variable, so done
        # here rather than in .agg(), which returns a single structure
        x.grp <- if (is.data.frame(x.call)) x.call[, 1L] else x.call
        if (stat == "deviation") {
          agg.df <- as.data.frame(
            lapply(y.call, function(col) {
              grp.means <- tapply(col, x.grp, mean, na.rm=TRUE)
              grp.means - mean(grp.means, na.rm=TRUE)
            })
          )
        }
        else {
          agg.df <- as.data.frame(
            lapply(y.call, function(col) tapply(col, x.grp, .stat_fun(stat)))
          )
        }
        x.call <- factor(rownames(agg.df), levels=rownames(agg.df))
        y.call <- agg.df
        if (is.null(ylab)) ylab <- paste(.stat_lbl(stat), "of",
                                         paste(names(agg.df), collapse=" & "))
      } else {
        # .agg() returns by-var as "by.var", facet-var as "facet.var"
        # capture original by/facet names
        #  before aggregation replaces by.call/facet.call
        by.names.original <- if (is.null(by.call)) character(0L)
                             else if (is.data.frame(by.call)) names(by.call)
                             else by.name
        facet.names.original <- if (is.null(facet.call)) character(0L)
                                else if (is.data.frame(facet.call)) 
                                         names(facet.call)
                                else facet.name
        stat.out <- .agg(x.call, y.call, by.call, facet.call, stat, y.name)
        out <- stat.out$out  # aggregated data

        if (is.null(ylab)) ylab <- stat.out$ylab

      # table already printed above
      if (is.null(by.call) && is.null(facet.call)) {  # out has only x, y
        x.call <- out$x
        y.call <- out$y
      }

      else {  # by.call or facet.call present, out has grouping columns
        # build: y ~ group_cols + x

        ## all grouping columns (by + facet) are everything except x,y
        grp.cols <- setdiff(names(out), c("x", "y"))

        if (!quiet) cat("Summary Table for", ylab, "\n\n")

        if (!length(grp.cols)) {
          x.tbl.print <- xtabs(y ~ x, data = out)
        } else {
          fmla  <- reformulate(c(grp.cols, "x"), response = "y")
          x.tbl.print <- xtabs(fmla, data = out)
        }

        x.lbl <- .getlabels(main=main, lab_size=getOption("lab_size"))$xl
        if (!quiet) {
          .print_chart_summary(
            x.tbl    = x.tbl.print,
            x_name   = x.name,
            x.lbl    = x.lbl,
            by_name  = by.name,  # label string, not the columns
            y_name   = y.name,
            stat     = stat,
            digits_d = digits_d
          )
        }

        ## ----- Update x.call / by.call / facet.call / y.call ----------------
        ## After aggregation, downstream plotting should see the *reduced* data.

        x.call <- out[["x"]]
        y.call <- out[["y"]]

        by.cols    <- intersect(grp.cols, c(by.names.original, "by.var"))
        facet.cols <- intersect(grp.cols, c(facet.names.original, "facet.var"))

        # restore original names if .agg() canonicalized them
        if ("by.var" %in% names(out) && length(by.names.original) == 1L)
          names(out)[names(out) == "by.var"] <- by.names.original
        if ("facet.var" %in% names(out) && length(facet.names.original) == 1L)
          names(out)[names(out) == "facet.var"] <- facet.names.original

        # re-resolve cols after rename so downstream sees original names
        grp.cols   <- setdiff(names(out), c("x", "y"))
        by.cols    <- intersect(grp.cols, by.names.original)
        facet.cols <- intersect(grp.cols, facet.names.original)

        ## by.call: NULL, vector, or data.frame depending on how many by vars
        if (!length(by.cols))
          by.call <- NULL
        else if (length(by.cols) == 1L)
          by.call <- out[[by.cols[1L]]]
        else
          by.call <- out[by.cols]

        ## facet.call: NULL, vector, or data.frame depending on how many facets
        if (!length(facet.cols))
          facet.call <- NULL
        else if (length(facet.cols) == 1L)
          facet.call <- out[[facet.cols[1L]]]
        else
          facet.call <- out[facet.cols]
      }
      }  # end single-column y stat aggregation
  }  # y is present for original data and stats not NULL


# else do table stats of counts -------------------------------------------

  else if (form != "bar") {  # do table of counts, done in bc.main for bar
    gl <- .getlabels(main=main, lab_size=getOption("lab_size"))
    x.name <- gl$xn; x.lbl <- gl$xl

    if (!quiet) {
      # build the 1-D / 2-D table strictly for console output & hover
      x.tbl.print <- .build_xtab(
        x        = x.call,
        y        = y.call,
        by       = if (!is.null(by.call)) by.call else facet.call,
        facet    = NULL,
        stat     = stat,
        is.agg   = is.agg,
        digits_d = digits_d
      )

      if (length(dim(x.tbl.print)) < 3) { # by not a vector
        .print_chart_summary(  # print 1-D or 2-D, in zzz_plotly
          x.tbl   = x.tbl.print,
          x_name  = x.name,
          x.lbl   = x.lbl,
          by_name = if (!is.null(by.call)) by.name
                    else if (!is.null(facet.call)) facet.name
                    else NULL,
          y_name  = "Count",
          stat    = stat,
          digits_d = digits_d
        )
      }
    }  # end !quiet
  }  # end need to aggregate
  }  # end if (!is.agg)


# begin processing designated chart form ----------------------------------


  if (form == "bar") {
    bc <- .bc.main(x.call, y.call, by.call, stack100,
          fill, color, trans,
          fill_split, fill_scaled, fill_chroma, fill.palette, theme,
          horiz, gap, proportion, scale_y,
          xlab, ylab, main,
          label_max, beside,
          rotate_x, offset, 
          axis_fmt, axis_x_pre, axis_y_pre,
          break_x, sort,
          labels, labels_color, labels_size, labels_decimals,
          labels_position, labels_cut,
          xlab.adj, ylab.adj, bm.adj, lm.adj, tm.adj, rm.adj,
          pad_y_min, pad_y_max,
          legend_title, legend_position, legend_labels,
          legend_horiz, legend_size, legend_abbrev, legend_adjust,
          add, x1, x2, y1, y2, 80, do_plot, use_plotly, quiet,
          shiny, digits_d, style_opts=style.opts, ...)

    if (!is.null(pdf_file)) {
      dev.off()
      if (!quiet) .showfile(pdf_file, "BarChart")
    }

    return(invisible(bc))
  }


# Hierarchical chart -------------------------------------------------------

  else if (form %in% c("treemap", "icicle") ||
          (form == "pie" && !is.null(by.call))) {
    if (form == "pie") form <- "sunburst"

    # y.name should probably be distinguished from the title, which
    #   it is here, e.g., y.name is "Mean of Sales"
    ttl <- .build_chart_title(main, main.miss, x.name, by.name,
                              facet.name, y.name, stat)

    # aggregate once; .hier.plotly() just renders
    hier.agg <- .hier_aggregate(
      x = x.raw, by = by.raw, y = y.raw, facet = facet.raw,
      stat = stat,
      x_name = x.name, by_name = by.name,
      facet_name = facet.name, y_name = y.name,
      digits_d = digits_d
    )

    # resolve hex colors (uses raw data, not aggregated)
    hier.cols <- .hier_color_resolve(
      x = x.raw, y = y.raw, facet = facet.raw,
      fill = fill, fill.palette = fill.palette,
      theme = theme, stat = stat
    )

    plt <- .hier.plotly(
      nd             = hier.agg$nd,
      nd_byfac       = hier.agg$nd_byfac,
      x_name         = x.name, by_name = by.name, facet_name = facet.name,
      form           = form,
      stat_label     = hier.agg$stat.label,
      non_additive   = hier.agg$non.additive,
      n_levels       = hier.agg$n_levels,
      has_y          = hier.agg$has_y,
      fill_vec       = hier.cols$fill_vec,
      fill_vec_byfac = hier.cols$fill_vec_byfac,
      border         = NULL,
      digits_d       = digits_d,
      facet_opts     = .plotly_facet_opts(),
      main           = ttl,
      labels         = labels, labels_position = labels_position,
      labels_color   = labels_color, labels_size = labels_size,
      style_opts     = style.opts
    )

    if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
      print(plt)

    return(invisible(plt))
  }


  else if (form == "pie") {  # plain pie, no by, maybe facet

    # facet= on a plain pie is treated as nesting levels for a sunburst
    facet.ttl <- NULL   # the pie grid re-uses  by  to group the panels,
                        # but the variable is still a facet, so the title
                        # reads "across", not "by"
    if (is.null(by.call) && !facet.miss) {  # by already shown to not exist
      facet.ttl  <- facet.name
      by.name    <- facet.name
      options(byname = by.name)
      by.call    <- facet.call    # use the facet as the by for aggregation
      facet.name <- NULL
      facet.call <- NULL
    }

    x.tbl <- .build_plotly_tbl(x.call, y.call, by.call)

    if (is.null(by.call) && fill.miss) {
      pie.vals <- as.numeric(x.tbl)
      n.slices <- length(pie.vals)
      if (theme != "colors" && n.slices > 1L &&
          diff(range(pie.vals)) > 0) {
        fill <- setNames(
          .scale.clr(pie.vals, fill=NULL,
            fill_split=0, fill_chroma=75, theme=theme),
          names(x.tbl))
      } else if (theme == "colors") {
        fill <- setNames(
          .color_range("hues", n.slices), names(x.tbl))
      }
    }

    ttl <- .build_chart_title(
             main, main.miss, x.name,
             if (is.null(facet.ttl)) by.name else NULL,
             if (is.null(facet.ttl)) facet.name else facet.ttl,
             y.name, stat)

    plt <- piechart.plotly(
      x = x.tbl, x_name = x.name, y_name = y.name, by_name = by.name,
      main = ttl,
      fill = fill, border = color, opacity = 1 - trans,
      theme = theme,
      hole = hole, ncols = NULL,
      labels = labels, labels_position = labels_position,
      labels_color = labels_color, labels_size = labels_size,
      labels_decimals = labels_decimals,
      digits_d = digits_d,
      style_opts = style.opts
    )

    plt <- .finalize_plotly_widget(
      plt,
      kind         = "pie",
      x_name       = x.name,
      by_name      = if (!is.null(by.call)) by.name else NULL,
      add_title    = FALSE,
      nudge_viewer = (.allow.interactive() &&
                      !isTRUE(getOption("knitr.in.progress")))
    )

    if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
      print(plt)

    return(invisible(plt))
  }


# Bubble chart -----------------------------------------------------------

  else if (form == "bubble") {

    if (color.miss) color <- "black"
    # gray theme with no user fill: uniform gray30 so size alone encodes magnitude
    fill.bub <- if (fill.miss && identical(theme, "gray")) "gray30" else fill

    # faceting is only available for the 1-D bubble (categorical x, no by)
    if (!is.null(facet.call) && !is.null(by.call)) {
      cat("\n"); stop(call.=FALSE, "\n------\n",
        "The  facet  option for bubble plots applies only to a single\n",
        "  categorical variable (no  by  variable).\n\n")
    }

    # build per-facet 1-D tables — one panel per facet level, all carrying
    # the full set of x categories so the panels stay aligned
    facet.tbls <- NULL
    if (!is.null(facet.call)) {
      fac.f   <- factor(facet.call)
      x.fac   <- if (is.factor(x.call)) droplevels(x.call) else factor(x.call)
      facet.tbls <- lapply(levels(fac.f), function(lv) {
        idx <- which(as.character(fac.f) == lv)
        .build_plotly_tbl(factor(x.call[idx], levels = levels(x.fac)),
                          if (is.null(y.call)) NULL else y.call[idx], NULL)
      })
      names(facet.tbls) <- levels(fac.f)
    }

    x.tbl <- .build_plotly_tbl(x.call, y.call, by.call)

    # build title unless user explicitly supplied main=
    ttl <- .build_chart_title(main, main.miss, x.name, by.name,
                              facet.name, y.name, stat)


    plt <- bubble.plotly(
      x = x.tbl, x_name = x.name, y_name = y.name, by_name = by.name,
      x_lab = if (!is.null(xlab)) xlab else x.name,
      y_lab = if (!is.null(ylab)) ylab
              else if (is.null(by.call)) "" else by.name,
      main = ttl,
      fill = fill.bub, border = color, opacity = 1 - trans,
      power = power, radius = radius,
      digits_d = digits_d,
      labels = labels, labels_position = labels_position,
      labels_size = labels_size, labels_color = labels_color,
      labels_decimals = labels_decimals,
      facet_tbls = facet.tbls, facet_name = facet.name,
      facet_opts = .plotly_facet_opts(),
      style_opts = style.opts
    )

    plt <- .finalize_plotly_widget(
      plt,
      kind         = "bubble",
      x_name       = x.name,
      by_name      = if (!is.null(by.call)) by.name else NULL,
      add_title    = FALSE,
      nudge_viewer = (.allow.interactive() &&
                      !isTRUE(getOption("knitr.in.progress")))
    )

    if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
      print(plt)

    return(invisible(plt))
  }


# Radar chart -------------------------------------------------------------

  else if (form == "radar") {

      if (n.x < 3) {  # Axes must define a polygon
        stop("\n\n",
          "radar(): The first categorical variable (x) must have at least ",
          "3 levels to form a polygon. ",
          "Found ", n.x, " levels for ", x.name, "."
        )
      }
      if (n.by == 1) {  # Must have at least two groups to compare
        stop("\n\n",
          "radar(): The second categorical variable (by) must have at least ",
          "2 levels to define multiple polygons. ",
          "Found ", n.by,  " levels for ", by.name, "."
        )
      }

      # stop when any cell is empty:
      # not working now because NA's cause by.call and x.call different lengths
      if (!is.null(by.call)) {

        # ensure x is a vector
        xv <- if (is.data.frame(x.call)) x.call[[1L]] else x.call
        ok <- complete.cases(by.call, xv)

        tbl <- table(by.call[ok], xv[ok], useNA = "no")

        if (any(tbl == 0L)) {
          zeros <- which(tbl == 0L, arr.ind = TRUE)
          print(tbl)

          stop("\n\n",
               "radar(): One or more cells are missing, with 0 entries.\n",
               "Radar polygons assume each group has a value at every axis.\n",
               "Here, ", nrow(zeros), "  cells were found with count=0.\n\n",
               "Use a larger sample, reduce the number of levels,\n",
               " or choose a different chart.\n")
        }
      }

      if (transparency.miss && !is.null(by.call))
        trans <- 0.4

      ttl <- .build_chart_title(main, main.miss, x.name, by.name,
                                facet.name, y.name, stat)

      # aggregate once here; radar.plotly just renders
      radar.agg <- .radar_aggregate(
        x = x.call, by = by.call, facet = facet.call, y = y.call,
        stat = stat, y_name = y.name
      )
      # resolve fill once to exactly n.by colors
      fill.radar <- .color_range(fill %||% .get_fill(theme),
                                 max(radar.agg$n.by, 1L))

      plt <- .radar.plotly(
        agg          = radar.agg,
        x_name       = x.name,
        by_name      = by.name,
        facet_name   = facet.name,
        main         = ttl,
        fill         = fill.radar,
        border       = color,
        opacity      = 1 - trans,
        digits_d     = digits_d,
        # facet layout controls (defaults shared with .hier.plotly)
        facet_opts  = .plotly_facet_opts(),
        style_opts  = style.opts
      )

    if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
      print(plt)

    return(invisible(plt))
    }


# Chart(x=, y=, ...,  form="dot") -> dot.plotly() directly ---------------

  else if (form == "dot") {

      # ---- shared dot.plotly() parameters from Chart()'s resolved state ----
      dot.border  <- getOption("pt_color")
      dot.shape   <- 21
      dot.size    <- pt_size  # default pt_size=1 → px = 10 pixels
      dot.opacity <- 1 - getOption("trans_pt_fill")
      # default fill from theme palette, same hues as the bar chart;
      # sequential-palette themes get the bar's single discrete color,
      # not a light-to-dark range (skip if a local theme= already set fill)
      if (fill.miss && is.null(fill)) {
        pal <- .get_fill(theme)
        fill <- if (identical(pal, "hues"))
                  .color_range(pal, max(n.x, n.by))
                else
                  style(theme, reset = FALSE)$bar$bar_fill_discrete
      }

      if (is.null(segments_x)) segments_x <- TRUE
      if (is.null(segments_y)) segments_y <- TRUE


      # ---- paired dot plot: x=Person (cat), y=c(Before, After) (numeric df) -
      # Kept as a separate dot.plotly() call: paired layout uses a data frame
      # for vals (signaling paired-line geometry in dot.plotly), with unique
      # row-mean sorting and label arrangement that don't fit the single-series
      # path. No facet support.
      if (is.data.frame(y.call) && ncol(y.call) > 1L) {
        if (!is.null(facet.call))
          stop(call.=FALSE, "\n------\n",
            "Multiple y variables (paired dot chart) combined\n",
            " with facet= is not supported.\n\n")

        cats.vec <- as.character(x.call)

        # apply sort= — order rows by row mean across all series
        if (sort != "0") {
          row.means <- rowMeans(as.matrix(y.call), na.rm = TRUE)
          ord       <- order(row.means, decreasing = (sort == "-"))
          cats.vec  <- cats.vec[ord]
          y.call    <- y.call[ord, , drop = FALSE]
        }

        x.lab.dot <- if (!is.null(xlab)) xlab else ""
        og <- .dot_origin_grid(
          na.omit(unlist(y.call)),
          origin_in = origin_x
        )
        origin_x  <- og$origin
        # paired-dot needs a numeric origin_x downstream (rep() with NULL fails)
        if (is.null(origin_x)) origin_x <- 0
        gridT.dot <- og$gridT

        plt <- dot.plotly(
          x          = cats.vec,
          y          = y.call,        # data frame → paired path in dot.plotly()
          fill       = fill[seq_len(min(ncol(y.call), length(fill)))],
          border     = dot.border,
          shape      = dot.shape,
          pt.size    = dot.size,
          x_lab      = x.lab.dot,
          y_lab      = x.name,
          digits_d   = digits_d,
          pt_opacity = dot.opacity,
          gridT      = gridT.dot,
          origin_x   = origin_x,
          height     = NULL,
          main       = .build_chart_title(main, main.miss, x.name, by.name,
                                          facet.name, y.name, stat),
          segments_x = segments_x,
          segments_y = segments_y,
          style_opts = style.opts
        )

        if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
          print(plt)
        return(invisible(plt))
      }

      # ---- single-series dot plot (faceted or not) ------------------------
      # One unified preparation path, one dot.plotly() call. Both branches
      # aggregate here (via .agg) as needed; dot.plotly() just renders.

      x.is.cat   <- is.factor(x.call) || is.character(x.call)
      has.dupes  <- x.is.cat && anyDuplicated(x.call) > 0L
      needs.agg  <- !is.agg && (has.dupes || (!is.null(stat) && !is.null(y.call)))
      is.faceted <- !is.null(facet.call)

      # ---- prepare cats.vec, vals.vec, all.vals (for axis range) ----------
      if (is.faceted) {
        # aggregate (if needed) and sort per facet; dot.plotly() just renders
        fac.chr    <- as.character(facet.call)
        fac.levels <- base::sort(unique(fac.chr[!is.na(fac.chr)]))

        # the main stat block has already reduced the data when both y and
        # stat are present (is.agg), so aggregate here only the raw counts
        # of x when there is no y
        stat.use <- if (!is.agg && !is.null(stat)) stat
                    else if (is.null(y.call)) "sum"  # sum of 1's: count
                    else NULL

        if (!is.null(stat.use)) {
          y.use <- if (!is.null(y.call)) as.numeric(y.call)
                   else rep(1L, length(fac.chr))
          agg.out <- .agg(as.character(x.call), y.use, facet=fac.chr,
                          stat=stat.use)$out
        }

        cats.lst <- vals.lst <- fac.lst <-
          vector("list", length(fac.levels))
        for (k in seq_along(fac.levels)) {
          lv <- fac.levels[k]

          if (!is.null(stat.use)) {  # aggregated rows for this facet panel
            sub <- agg.out[agg.out$facet.var == lv, , drop=FALSE]
            sub <- sub[order(as.character(sub$x)), , drop=FALSE]
            c.i <- as.character(sub$x)
            v.i <- as.numeric(sub$y)
          }
          else {  # data already reduced upstream, or raw passthrough
            keep <- fac.chr == lv & !is.na(fac.chr)
            c.i  <- as.character(x.call[keep])
            v.i  <- as.numeric(y.call[keep])
          }

          if (sort != "0" && length(c.i) == length(v.i)) {
            ord <- order(v.i, decreasing = (sort == "-"),
                         na.last = TRUE)
            c.i <- c.i[ord]; v.i <- v.i[ord]
          }

          cats.lst[[k]] <- c.i
          vals.lst[[k]] <- v.i
          fac.lst[[k]]  <- rep(lv, length(c.i))
        }

        cats.vec       <- unlist(cats.lst, use.names = FALSE)
        vals.vec       <- unlist(vals.lst, use.names = FALSE)
        facet.arg      <- unlist(fac.lst,  use.names = FALSE)
        facet.name.arg <- facet.name
        all.vals       <- vals.vec
        x.lab.dot      <- if (!is.null(xlab)) xlab
                          else if (!is.null(y.call)) y.name
                          else paste("Count of", x.name)
      } else {
        # non-faceted: aggregate here
        facet.arg      <- NULL
        facet.name.arg <- NULL

        if (needs.agg) {
          if (is.null(y.call)) {
            agg <- table(x.call)
            agg <- if (sort != "0")
                     base::sort(agg, decreasing = (sort == "-"))
                   else
                     agg[base::sort(names(agg))]  # alphabetical
            cats.vec  <- names(agg)
            vals.vec  <- as.integer(agg)
            x.lab.dot <- if (!is.null(xlab)) xlab
                         else paste("Count of", x.name)
          } else {
            stat.use  <- if (!is.null(stat)) stat else "mean"
            stat.out  <- .agg(x.call, y.call, NULL, NULL, stat.use, y.name)
            cats.vec  <- as.character(stat.out$out$x)
            vals.vec  <- as.numeric(stat.out$out$y)
            x.lab.dot <- if (!is.null(xlab)) xlab else stat.out$ylab
          }
        } else {
          # already aggregated / unique
          if (is.null(y.call)) {
            cat("\n"); stop(call.=FALSE, "\n------\n",
              "Need to specify a numerical variable for y, y=NAME\n\n")
          }
          if (x.is.cat) {
            # When stat aggregation was done upstream, x.call/y.call already
            # hold the reduced aggregated data. x.call.all/y.call.all only
            # apply for pre-aggregated input data where some categories may
            # have NA values.
            if (!is.null(stat)) {
              cats.vec <- as.character(x.call)
              vals.vec <- as.numeric(y.call)
            } else {
              # pre-aggregated: use .all so all category labels appear on the
              # axis; dot.plotly() skips points where vals is NA.
              cats.vec <- as.character(x.call.all)
              vals.vec <- as.numeric(y.call.all)
            }
            x.lab.dot <- if (!is.null(ylab)) ylab else y.name
          } else {
            # x numeric, y categorical → horizontal
            cats.vec  <- as.character(y.call)
            vals.vec  <- as.numeric(x.call)
            x.lab.dot <- if (!is.null(xlab)) xlab else x.name
          }
        }

        # apply sort= to aggregated category/value vectors
        if (sort != "0" && length(cats.vec) == length(vals.vec)) {
          ord <- order(vals.vec, decreasing = (sort == "-"), na.last = TRUE)
          cats.vec <- cats.vec[ord]
          vals.vec <- vals.vec[ord]
        }
        all.vals <- vals.vec
      }

      # ---- resolve orientation and axis labels ----------------------------
      # Default: categories on x-axis (vertical), matching bar chart convention.
      # horiz=TRUE flips to horizontal (categories on y-axis).
      # Exception: x numeric + y categorical always stays vertical.
      if (!x.is.cat && !is.null(y.call)) {
        orientation <- "v"
        x.lab.arg   <- if (!is.null(xlab)) xlab else x.name
        y.lab.arg   <- x.lab.dot
      } else if (horiz) {
        orientation <- "h"
        x.lab.arg   <- if (!is.null(xlab)) xlab else x.lab.dot
        y.lab.arg   <- if (!is.null(ylab)) ylab else x.name
      } else {
        orientation <- "v"
        x.lab.arg   <- if (!is.null(xlab)) xlab else x.name
        y.lab.arg   <- if (!is.null(ylab)) ylab else x.lab.dot
      }

      # ---- compute origin.val and gridT.dot via shared helper -------------
      og <- .dot_origin_grid(
        all.vals,
        origin_in = if (orientation == "h") origin_x else origin_y,
        # force count semantics when y is missing (Chart-only knowledge)
        is_counts = if (is.null(y.call)) TRUE else NULL
      )
      origin.val <- og$origin
      gridT.dot  <- og$gridT

      # ---- single dot.plotly() call ---------------------------------------
      plt <- dot.plotly(
        x           = cats.vec,
        y           = vals.vec,
        orientation = orientation,
        fill        = fill,
        border      = dot.border,
        shape       = dot.shape,
        pt.size     = dot.size,
        x_lab       = x.lab.arg,
        y_lab       = y.lab.arg,
        digits_d    = digits_d,
        pt_opacity  = dot.opacity,
        gridT       = gridT.dot,
        origin_x    = origin.val,
        height      = NULL,
        facet       = facet.arg,
        facet_name  = facet.name.arg,
        main        = .build_chart_title(main, main.miss, x.name, by.name,
                                         facet.name, y.name, stat),
        segments_x  = segments_x,
        segments_y  = segments_y,
        style_opts  = style.opts
      )

      if (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
        print(plt)
      return(invisible(plt))
    }


  }  # end x is a single var
  # ------------------------
  # ------------------------
  # x is a data frame or var list of multiple variables
  else {

    if (!by.miss || !facet.miss) {
      cat("\n"); stop(call.=FALSE, "\n------\n",
        "by and facet variables not available for multiple x variables\n\n")
    }

    if (!(form %in% c("bar", "bubble"))) {
      cat("\n"); stop(call.=FALSE, "\n------\n",
        "Multiple x variables only available for form=\"bar\"\n",
        "  or form=\"bubble\"\n\n")
    }

    if (form == "bubble") {  # vector bubble, BPFM

      # get labels just for subset data matrix
      attr.labels <- attr(data, which="variable.labels")
      nm <- names(x.call)
      mylabs <- character(length=length(nm))
      mylabs <- ifelse(nm %in% names(attr.labels),
                       attr.labels[nm], "not available")
      if (all(mylabs == "not available")) mylabs <- NULL

      if ("l" %in% ls(name=.GlobalEnv)) mylabs <- get("l", pos=.GlobalEnv)

      if (is.null(xlab)) xlab <- ""  # suppress x-axis label if not specified

    if (is.null(fill)) fill <- getOption("bar_fill_cont")
    if (transparency.miss) trans <- 0.4

    .dpmat.main(
      x=data[, , drop = FALSE], l=mylabs,
      sort_type=sort,

      fill=fill, color=getOption("pt_color"), col.bg=getOption("panel_fill"),
      trans=trans, shape_pts="bubble", col.box=getOption("panel_color"),
      col.low=NULL, col.hi=NULL,

      xy_ticks=TRUE,
      xlab=xlab, ylab=ylab, main=main, sub=sub, cex=1,

      radius=radius, power=power,
      size_cut=1,
      txt_color=getOption("bubble_text_color"),

      bm.adj=0, lm.adj=0, tm.adj=0, rm.adj=0,

      rotate_x=rotate_x, rotate_y=rotate_y, offset=offset, quiet=quiet,

      do_plot=do_plot, fun_call=fun_call,
      ...)
    }  # end vector bubble

    else if (form == "bar") {  # vector bar

    if (is.null(one_plot)) {  # see if one_plot was specified
      one_plot <- TRUE

      mx.ln <- 0  # get variable with the most unique responses
      for (i in seq_len(ncol(data))) {
        ln <- length(na.omit(unique(data[,i])))
        if (ln > mx.ln) {
          ind <- i
          mx.ln <- ln
        }
      }
      uq <- na.omit(unique(data[,ind]))  # first largest set of responses
      uq.ln <- length(uq)  # length of largest set

      # all elements of smaller response set should be in full response set
      for (i in seq_len(ncol(data))) {
        if (length(setdiff(na.omit(unique(data[,i])), uq)) > 0) {
          one_plot <- FALSE
          break;
        }
      }
    }  # end determine one_plot

    # one_plot all x's stacked on a single plot, like a BPFM for bars
    if (one_plot) {
      y.call <- NULL
      by.call <- NULL
      if (is.null(legend_title)) legend_title <- "Responses"
      options(byname = "Responses")
      if (is.null(xlab)) xlab <- ""
      if (is.null(ylab)) ylab <- ""
      if (labels_size.miss) labels_size <- 0.8 - (0.008 * ncol(data))
      if (sort.miss) sort <- "+"
      if (horiz.miss) horiz <- TRUE

      if (color.miss) color <- "transparent"

      # if fill not specified, define divergent palette and get colors
      if (fill.miss) {
        fill <- .get_fill(theme, diverge=TRUE)  # get divergent color names
        fill <-.color_range(fill, uq.ln)  # translate color names to colors
      }  # end fill.miss

      # evaluate getColors at the time of the function call
      # re-evaluate here by setting fill with the specified value of n
      else if (substr(fill.name[1], 1, 9) == "getColors")
        fill <- .resolve_getColors_n(fill.name, uq.ln)

      else  # not fill=getColors(...) but color names were specified
        fill <-.color_range(fill, uq.ln)  # translate color names to colors

      if (!is.null(pdf_file)) {
        if (!grepl(".pdf", pdf_file, fixed=TRUE))
          pdf_file <- paste(pdf_file, ".pdf", sep="")
        pdf(file=pdf_file, width=width, height=height, onefile=FALSE)
      }
      else {
        if (df.name != "NULL")  # not dev.new for shiny
            .opendev(pdf_file, width, height)
      }

      bc <- .bc.main(data, y.call, by.call, stack100,
            fill, color, trans,
            fill_split, fill_scaled, fill_chroma, fill.palette, theme,
            horiz, gap, proportion, scale_y,
            xlab, ylab, main,
            label_max, beside,
            rotate_x, offset,
            axis_fmt, axis_x_pre, axis_y_pre,
            break_x, sort,
            labels, labels_color, labels_size, labels_decimals,
            labels_position, labels_cut,
            xlab.adj, ylab.adj, bm.adj, lm.adj, tm.adj, rm.adj,
            pad_y_min, pad_y_max,
            legend_title, legend_position, legend_labels,
            legend_horiz, legend_size, legend_abbrev, legend_adjust,
            add, x1, x2, y1, y2, 80, do_plot, use_plotly=FALSE,
            quiet, shiny, digits_d, style_opts=style.opts, ...)

      if (!is.null(pdf_file)) {
        dev.off()
        if (!quiet) .showfile(pdf_file, "BarChart")
      }
    }  # end one_plot


    # --------------------------------
    # analyze each x column separately

    else {
      bc.data.frame(data, stack100,
        fill, color, trans, fill_split,
        fill_scaled, fill_chroma, theme,
        horiz, gap, proportion, scale_y,
        xlab, ylab, main,
        label_max, beside,
        rotate_x, offset, 
        axis_fmt, axis_x_pre, axis_y_pre,
        break_x, sort,
        labels, labels_color, labels_size, labels_decimals,
        labels_position, labels_cut,
        xlab.adj, ylab.adj, bm.adj, lm.adj, tm.adj, rm.adj,
        pad_y_min, pad_y_max,
        legend_title, legend_position, legend_labels,
        legend_horiz, legend_size, legend_abbrev, legend_adjust,
        80, do_plot, use_plotly=FALSE, quiet,
        digits_d, shiny, pdf_file, width, height, ...)
    }
  }
  } # end

}
