Merge pull request #523 from r2evans/fix/522_specline

fix #522, spec_line glitches and improvements
diff --git a/DESCRIPTION b/DESCRIPTION
index a338828..55d9a67 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -16,7 +16,8 @@
     person('Brian', 'Salzer', role = 'ctb'),
     person('George', 'Gui', role = 'ctb'),
     person('Yeliang', 'Fan', role = 'ctb'),
-    person('Duncan', 'Murdoch', role = 'ctb')
+    person('Duncan', 'Murdoch', role = 'ctb'),
+    person('Bill', 'Evans',  role = 'ctb')
     )
 Description: Build complex HTML or 'LaTeX' tables using 'kable()' from 'knitr' 
     and the piping syntax from 'magrittr'. Function 'kable()' is a light weight 
diff --git a/NAMESPACE b/NAMESPACE
index b8ebdd2..005da73 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -46,7 +46,7 @@
 export(spec_font_size)
 export(spec_hist)
 export(spec_image)
-export(spec_line)
+export(spec_plot)
 export(spec_popover)
 export(spec_tooltip)
 export(text_spec)
diff --git a/R/mini_plots.R b/R/mini_plots.R
index 1c15262..a1463f9 100644
--- a/R/mini_plots.R
+++ b/R/mini_plots.R
@@ -110,7 +110,7 @@
 #' plotted in the same range? Default is True.
 #' @param lim,xlim,ylim Manually specify plotting range in the form of
 #' `c(0, 10)`. `lim` is used in `spec_hist` and `spec_boxplot`; `xlim`
-#' and `ylim` are used in `spec_line`.
+#' and `ylim` are used in `spec_plot`.
 #' @param xaxt On/Off for xaxis text
 #' @param yaxt On/Off for yaxis text
 #' @param ann On/Off for annotations (titles and axis titles)
@@ -239,12 +239,22 @@
 #' @param ann On/Off for annotations (titles and axis titles)
 #' @param col Color for the fill of the histogram bar/boxplot box.
 #' @param border Color for the border.
-#' @param frame.plot On/Off for surrounding box (`spec_line` only). Default
+#' @param frame.plot On/Off for surrounding box (`spec_plot` only). Default
 #' is False.
-#' @param lwd Line width for `spec_line`; within `spec_line`, the `minmax`
+#' @param lwd Line width for `spec_plot`; within `spec_plot`, the `minmax`
 #' argument defaults to use this value for `cex` for points. Default is 2.
+#' @param pch,cex Shape and size for points (if type is other than "l").
+#' @param type Passed to `plot`, often one of "l", "p", or "b", see
+#' [graphics::plot.default()] for more details. Ignored when 'polymin' is
+#' not 'NA'.
+#' @param polymin Special argument that converts a "line" to a polygon,
+#' where the flat portion is this value, and the other side of the polygon
+#' is the 'y' value ('x' if no 'y' provided). If 'NA' (the default), then
+#' this is ignored; otherwise if this is numeric then a polygon is
+#' created (and 'type' is ignored). Note that if 'polymin' is in the middle
+#' of the 'y' values, it will generate up/down polygons around this value.
 #' @param minmax,min,max Arguments passed to `points` to highlight minimum
-#' and maximum values in `spec_line`. If `min` or `max` are `NULL`, they
+#' and maximum values in `spec_plot`. If `min` or `max` are `NULL`, they
 #' default to the value of `minmax`. Set to an empty `list()` to disable.
 #' @param dir Directory of where the images will be saved.
 #' @param file File name. If not provided, a random name will be used
@@ -253,67 +263,113 @@
 #' @param ... extra parameters passing to `plot`
 #'
 #' @export
-spec_line <- function(x, y = NULL, width = 200, height = 50, res = 300,
+spec_plot <- function(x, y = NULL, width = 200, height = 50, res = 300,
                       same_lim = TRUE, xlim = NULL, ylim = NULL,
                       xaxt = 'n', yaxt = 'n', ann = FALSE,
                       col = "lightgray", border = NULL,
                       frame.plot = FALSE, lwd = 2,
-                      minmax = list(pch = ".", cex = lwd, col = "red"),
+                      pch = ".", cex = 2, type = "l", polymin = NA,
+                      minmax = list(pch = ".", cex = cex, col = "red"),
                       min = minmax, max = minmax,
                       dir = if (is_latex()) rmd_files_dir() else tempdir(),
-                      file = NULL,
-                      file_type = if (is_latex()) "png" else "svg", ...) {
+                      file = NULL, file_type = if (is_latex()) "png" else "svg", ...) {
   if (is.list(x)) {
+    lenx <- length(x)
+
     if (same_lim) {
       if (is.null(xlim)) {
-        xlim <- base::range(unlist(x))
+        xlim <- base::range(unlist(x), na.rm = TRUE)
       }
       if (is.null(ylim) && !is.null(y)) {
-        ylim <- base::range(unlist(y))
+        ylim <- base::range(c(unlist(y), polymin), na.rm = TRUE)
       }
     }
+
     if (is.null(y)) {
-      y <- replicate(length(x), NULL, simplify = FALSE)
-    } else if (!is.list(y) || length(x) != length(y)) {
+      y <- list(y)
+    } else if (length(y) != lenx) {
       stop("'x' and 'y' are not the same length")
     }
-    return(Map(function(x_, y_) {
-      spec_line(x = x_, y = y_,
-                width = width, height = height,
-                same_lim = same_lim, xlim = xlim, ylim = ylim,
-                xaxt = xaxt, yaxt = yaxt, ann = ann, col = col, border = border,
-                frame.plot = frame.plot, lwd = lwd,
-                minmax = minmax, min = min, max = max,
-                dir = dir, file = file, file_type = file_type, ...)
-    }, x, y))
+
+    # any of the arguments can be per-plot controlling, but an arg
+    # that is normally not length-1 may be recycled (incorrectly) by
+    # Map, so we have to listify them if not already lists;
+    # enforce a restriction of recycling only length 1 or lenx
+
+    # first, start with the literals (x,y)
+    # (same_lim is not a factor anymore)
+    dots <- list(x = x, y = y)
+
+    # second, we know these args are likely to be vectors (length > 1)
+    # or lists, so we have to handle them carefully and double-list if
+    # present
+    notlen1vec <- list(xlim = xlim, ylim = ylim)
+    dots <- c(dots, Map(
+      function(L, nm) {
+        if (is.null(L)) return(list(NULL))
+        if (!is.list(L)) return(list(L))
+        if (length(L) == lenx) return(L)
+        stop("length of '", nm, "' must be 1 or the same length as 'x'")
+      }, notlen1vec, names(notlen1vec)))
+
+    # these are special ... they are lists which may need to be
+    # nested, and we can't pass NULL since that may default to the
+    # actual values instead of the intended
+    notlen1lst <- list(minmax = minmax, min = min, max = max)
+    dots <- c(dots, Map(
+      function(L, nm) {
+        if (is.null(L)) return(list(NULL))
+        if (!length(L)) return(list(list()))
+        if (!is.list(L[[1]])) return (list(L))
+        if (length(L) == lenx) return(L)
+        stop("length of '", nm, "' must be 1 or the same length as 'x'")
+      }, notlen1lst, names(notlen1lst)))
+
+    # last, all remaining arguments that we don't already know about
+    # are length-1, so can be easily listified; using 'formals()'
+    # allows us to not hard-code all params
+    len1args <- mget(setdiff(names(formals()),
+                             c(names(dots), "same_lim", "x", "y", "...")))
+    dots <- c(dots, Map(
+      function(V, nm) {
+        if (is.null(V)) return(list(NULL))
+        if (!length(V) %in% c(1L, lenx)) {
+          stop("length of '", nm, "' must be 1 or the same length as 'x'")
+        }
+        V
+      }, len1args, names(len1args)))
+
+    return(do.call(Map, c(list(f = spec_plot), dots)))
+
   }
 
   if (is.null(x)) return(NULL)
 
   if (is.null(y) || !length(y)) {
     y <- x
-    x <- seq(0, 1, length.out = length(y))
-    tmp <- ylim
-    ylim <- xlim
-    xlim <- tmp
+    x <- seq_along(y)
+    if (!is.null(xlim) && is.null(ylim)) {
+      ylim <- range(c(xlim, polymin), na.rm = TRUE)
+      xlim <- range(x)
+    }
   }
 
   if (is.null(xlim)) {
-    xlim <- base::range(x)
+    xlim <- base::range(x, na.rm = TRUE)
   }
 
   if (is.null(ylim) && !is.null(y)) {
-    ylim <- base::range(y)
+    ylim <- base::range(c(y, polymin), na.rm = TRUE)
   }
 
   if (is.null(min)) min <- minmax
   if (is.null(max)) max <- minmax
 
   expand <- c(
-    if (!is.null(min) && length(min)) 0.96 else 1,
-    if (!is.null(max) && length(max)) 1.04 else 1)
-  xlim <- xlim * expand
-  ylim <- ylim * expand
+    if (!is.null(min) && length(min)) -0.04 else 0,
+    if (!is.null(max) && length(max)) +0.04 else 0)
+  xlim <- xlim + diff(xlim) * expand
+  ylim <- ylim + diff(ylim) * expand
 
   file_type <- match.arg(file_type, c("svg", "png"))
 
@@ -336,17 +392,36 @@
   curdev <- grDevices::dev.cur()
   on.exit(grDevices::dev.off(curdev), add = TRUE)
 
-  graphics::par(mar = c(0, 0, 0.2, 0), lwd = lwd)
-  graphics::plot(x, y, type = "l", xlim = xlim, ylim = ylim, border = border,
+  graphics::par(mar = c(0, 0, 0, 0), lwd = lwd)
+
+  dots <- list(...)
+  if (!is.na(polymin) && "angle" %in% names(dots)) {
+    angle <- dots$angle
+    dots$angle <- NULL
+  } else angle <- 45
+
+  do.call(graphics::plot,
+          c(list(x, y, type = if (is.na(polymin)) type else "n",
+                 xlim = xlim, ylim = ylim,
                  xaxt = xaxt, yaxt = yaxt, ann = ann, col = col,
-                 frame.plot = frame.plot, ...)
+                 frame.plot = frame.plot, cex = cex, pch = pch),
+            dots))
+
+  if (!is.na(polymin)) {
+    lty <- if ("lty" %in% names(dots)) dots$lty else graphics::par("lty")
+    polygon(c(x[1], x, x[length(x)]), c(polymin, y, polymin),
+            border = NA, col = col, angle = angle, lty = lty,
+            xpd = if ("xpd" %in% names(dots)) dots$xpd else NA)
+  }
 
   if (!is.null(min) && length(min)) {
+    if (!"xpd" %in% names(min)) min$xpd <- NA
     ind <- which.min(y)
     do.call(graphics::points, c(list(x[ind], y[ind]), min))
   }
 
   if (!is.null(max) && length(max)) {
+    if (!"xpd" %in% names(max)) max$xpd <- NA
     ind <- which.max(y)
     do.call(graphics::points, c(list(x[ind], y[ind]), max))
   }
diff --git a/man/spec_boxplot.Rd b/man/spec_boxplot.Rd
index 1591f5d..ebde455 100644
--- a/man/spec_boxplot.Rd
+++ b/man/spec_boxplot.Rd
@@ -46,7 +46,7 @@
 
 \item{lim, xlim, ylim}{Manually specify plotting range in the form of
 \code{c(0, 10)}. \code{lim} is used in \code{spec_hist} and \code{spec_boxplot}; \code{xlim}
-and \code{ylim} are used in \code{spec_line}.}
+and \code{ylim} are used in \code{spec_plot}.}
 
 \item{xaxt}{On/Off for xaxis text}
 
diff --git a/man/spec_line.Rd b/man/spec_plot.Rd
similarity index 67%
rename from man/spec_line.Rd
rename to man/spec_plot.Rd
index 8d881d1..1917de1 100644
--- a/man/spec_line.Rd
+++ b/man/spec_plot.Rd
@@ -1,10 +1,10 @@
 % Generated by roxygen2: do not edit by hand
 % Please edit documentation in R/mini_plots.R
-\name{spec_line}
-\alias{spec_line}
+\name{spec_plot}
+\alias{spec_plot}
 \title{Helper functions to generate inline sparklines}
 \usage{
-spec_line(
+spec_plot(
   x,
   y = NULL,
   width = 200,
@@ -20,7 +20,11 @@
   border = NULL,
   frame.plot = FALSE,
   lwd = 2,
-  minmax = list(pch = ".", cex = lwd, col = "red"),
+  pch = ".",
+  cex = 2,
+  type = "l",
+  polymin = NA,
+  minmax = list(pch = ".", cex = cex, col = "red"),
   min = minmax,
   max = minmax,
   dir = if (is_latex()) rmd_files_dir() else tempdir(),
@@ -54,14 +58,27 @@
 
 \item{border}{Color for the border.}
 
-\item{frame.plot}{On/Off for surrounding box (\code{spec_line} only). Default
+\item{frame.plot}{On/Off for surrounding box (\code{spec_plot} only). Default
 is False.}
 
-\item{lwd}{Line width for \code{spec_line}; within \code{spec_line}, the \code{minmax}
+\item{lwd}{Line width for \code{spec_plot}; within \code{spec_plot}, the \code{minmax}
 argument defaults to use this value for \code{cex} for points. Default is 2.}
 
+\item{pch, cex}{Shape and size for points (if type is other than "l").}
+
+\item{type}{Passed to \code{plot}, often one of "l", "p", or "b", see
+\code{\link[graphics:plot.default]{graphics::plot.default()}} for more details. Ignored when 'polymin' is
+not 'NA'.}
+
+\item{polymin}{Special argument that converts a "line" to a polygon,
+where the flat portion is this value, and the other side of the polygon
+is the 'y' value ('x' if no 'y' provided). If 'NA' (the default), then
+this is ignored; otherwise if this is numeric then a polygon is
+created (and 'type' is ignored). Note that if 'polymin' is in the middle
+of the 'y' values, it will generate up/down polygons around this value.}
+
 \item{minmax, min, max}{Arguments passed to \code{points} to highlight minimum
-and maximum values in \code{spec_line}. If \code{min} or \code{max} are \code{NULL}, they
+and maximum values in \code{spec_plot}. If \code{min} or \code{max} are \code{NULL}, they
 default to the value of \code{minmax}. Set to an empty \code{list()} to disable.}
 
 \item{dir}{Directory of where the images will be saved.}
diff --git a/vignettes/awesome_table_in_html.Rmd b/vignettes/awesome_table_in_html.Rmd
index cb7b1e3..a98ae10 100644
--- a/vignettes/awesome_table_in_html.Rmd
+++ b/vignettes/awesome_table_in_html.Rmd
@@ -235,17 +235,24 @@
     c("kableExtra_sm.png", "kableExtra_sm.png"), 50, 50))
 ```
 
-`kableExtra` also provides a few inline plotting tools. Right now, there are `spec_hist`, `spec_boxplot`, and `spec_line`. One key feature is that by default, the limits of every subplots are fixed so you can compare across rows. Note that in html, you can also use package `sparkline` to create some jquery based interactive sparklines. Check out the end of this guide for details.
+`kableExtra` also provides a few inline plotting tools. Right now, there are `spec_hist`, `spec_boxplot`, and `spec_plot`. One key feature is that by default, the limits of every subplots are fixed so you can compare across rows. Note that in html, you can also use package `sparkline` to create some jquery based interactive sparklines. Check out the end of this guide for details.
 
 ```{r}
 mpg_list <- split(mtcars$mpg, mtcars$cyl)
-inline_plot <- data.frame(cyl = c(4, 6, 8), mpg_box = "", mpg_hist = "", mpg_line = "")
-inline_plot %>% 
-  kbl(booktabs = T) %>%
-  kable_paper(full_width = F) %>%
+disp_list <- split(mtcars$disp, mtcars$cyl)
+inline_plot <- data.frame(cyl = c(4, 6, 8), mpg_box = "", mpg_hist = "",
+                          mpg_line1 = "", mpg_line2 = "",
+                          mpg_points1 = "", mpg_points2 = "", mpg_poly = "")
+inline_plot %>%
+  kbl(booktabs = TRUE) %>%
+  kable_paper(full_width = FALSE) %>%
   column_spec(2, image = spec_boxplot(mpg_list)) %>%
   column_spec(3, image = spec_hist(mpg_list)) %>%
-  column_spec(4, image = spec_line(mpg_list, same_lim = FALSE))
+  column_spec(4, image = spec_plot(mpg_list, same_lim = TRUE)) %>%
+  column_spec(5, image = spec_plot(mpg_list, same_lim = FALSE)) %>%
+  column_spec(6, image = spec_plot(mpg_list, type = "p")) %>%
+  column_spec(7, image = spec_plot(mpg_list, disp_list, type = "p")) %>%
+  column_spec(8, image = spec_plot(mpg_list, polymin = 5))
 ```
 
 ## Row spec
diff --git a/vignettes/awesome_table_in_pdf.Rmd b/vignettes/awesome_table_in_pdf.Rmd
index 2fd3b25..0c7791a 100644
--- a/vignettes/awesome_table_in_pdf.Rmd
+++ b/vignettes/awesome_table_in_pdf.Rmd
@@ -263,17 +263,24 @@
     c("kableExtra_sm.png", "kableExtra_sm.png"), 50, 50))
 ```
 
-`kableExtra` also provides a few inline plotting tools. Right now, there are `spec_hist`, `spec_boxplot`, and `spec_line`. One key feature is that by default, the limits of every subplots are fixed so you can compare across rows.
+`kableExtra` also provides a few inline plotting tools. Right now, there are `spec_hist`, `spec_boxplot`, and `spec_plot`. One key feature is that by default, the limits of every subplots are fixed so you can compare across rows.
 
 ```{r}
 mpg_list <- split(mtcars$mpg, mtcars$cyl)
-inline_plot <- data.frame(cyl = c(4, 6, 8), mpg_box = "", mpg_hist = "", mpg_line = "")
-inline_plot %>% 
-  kbl(booktabs = T) %>%
-  kable_paper(full_width = F) %>%
+disp_list <- split(mtcars$disp, mtcars$cyl)
+inline_plot <- data.frame(cyl = c(4, 6, 8), mpg_box = "", mpg_hist = "",
+                          mpg_line1 = "", mpg_line2 = "",
+                          mpg_points1 = "", mpg_points2 = "", mpg_poly = "")
+inline_plot %>%
+  kbl(booktabs = TRUE) %>%
+  kable_paper(full_width = FALSE) %>%
   column_spec(2, image = spec_boxplot(mpg_list)) %>%
   column_spec(3, image = spec_hist(mpg_list)) %>%
-  column_spec(4, image = spec_line(mpg_list, same_lim = FALSE))
+  column_spec(4, image = spec_plot(mpg_list, same_lim = TRUE)) %>%
+  column_spec(5, image = spec_plot(mpg_list, same_lim = FALSE)) %>%
+  column_spec(6, image = spec_plot(mpg_list, type = "p")) %>%
+  column_spec(7, image = spec_plot(mpg_list, disp_list, type = "p")) %>%
+  column_spec(8, image = spec_plot(mpg_list, polymin = 5))
 ```
 
 ## Row spec