#' @title Function to create an Abanico Plot.
#'
#' @description A plot is produced which allows comprehensive presentation of data precision
#' and its dispersion around a central value as well as illustration of a
#' kernel density estimate, histogram and/or dot plot of the dose values.
#'
#' @details
#' The Abanico Plot is a combination of the classic Radial Plot
#' [Luminescence::plot_RadialPlot] and a kernel density estimate plot (e.g
#' [Luminescence::plot_KDE]. It allows straightforward visualisation of data precision,
#' error scatter around a user-defined central value and the combined
#' distribution of the values, on the actual scale of the measured data (e.g.
#' seconds, equivalent dose, years). The principle of the plot is shown in
#' Galbraith & Green (1990). The function authors are thankful for the
#' thought-provoking figure in this article.
#'
#' The semi circle (z-axis) of the classic Radial Plot is bent to a straight
#' line here, which actually is the basis for combining this polar (radial)
#' part of the plot with any other Cartesian visualisation method
#' (KDE, histogram, PDF and so on). Note that the plot allows displaying
#' two measures of distribution. One is the 2-sigma
#' bar, which illustrates the spread in value errors, and the other is the
#' polygon, which stretches over both parts of the Abanico Plot (polar and
#' Cartesian) and illustrates the actual spread in the values themselves.
#'
#' Since the 2-sigma-bar is a polygon, it can be (and is) filled with shaded
#' lines. To change density (lines per inch, default is 15) and angle (default
#' is 45 degrees) of the shading lines, specify these parameters. See
#' `?polygon()` for further help.
#'
#' The Abanico Plot supports other than the weighted mean as measure of
#' centrality. When it is obvious that the data
#' is not (log-)normally distributed, the mean (weighted or not) cannot be a
#' valid measure of centrality and hence central dose. Accordingly, the median
#' and the weighted median can be chosen as well to represent a proper measure
#' of centrality (e.g. `centrality = "median.weighted"`). Also
#' user-defined numeric values (e.g. from the central age model) can be used if
#' this appears appropriate.
#'
#' The proportion of the polar part and the cartesian part of the Abanico Plot
#' can be modified for display reasons (`plot.ratio = 0.75`). By default,
#' the polar part spreads over 75 % and leaves 25 % for the part that
#' shows the KDE graph.
#'
#'
#' A statistic summary, i.e. a collection of statistic measures of
#' centrality and dispersion (and further measures) can be added by specifying
#' one or more of the following keywords:
#'
#' - `"n"` (number of samples)
#' - `"mean"` (mean De value)
#' - `"median"` (median of the De values)
#' - `"sd.rel"` (relative standard deviation in percent)
#' - `"sd.abs"` (absolute standard deviation)
#' - `"se.rel"` (relative standard error)
#' - `"se.abs"` (absolute standard error)
#' - `"in.2s"` (percent of samples in 2-sigma range)
#' - `"kurtosis"` (kurtosis)
#' - `"skewness"` (skewness)
#'
#' **Note:** the input data for the statistic summary is sent to the function
#' [Luminescence::calc_Statistics] depending on the log-option for the z-scale. If
#' `"log.z = TRUE"`, the summary is based on the logarithms of the input
#' data. If `"log.z = FALSE"` the linearly scaled data is used.
#'
#' **Note:** [Luminescence::calc_Statistics] calculates these statistic
#' measures in three different ways: `unweighted`, `weighted` and
#' `MCM-based` (i.e., based on Monte Carlo Methods). By default, the
#' MCM-based version is used. If you wish to use another method, indicate this
#' with the appropriate keyword using the argument `summary.method`.
#'
#' The optional parameter `layout` allows more sophisticated ways to modify
#' the entire plot. Each element of the plot can be addressed and its properties
#' can be defined. This includes font type, size and decoration, colours and
#' sizes of all plot items. To infer the definition of a specific layout style
#' cf. [Luminescence::get_Layout] or type, e.g. for the layout type `"journal"`.
#' A layout type can be modified by the user by
#' assigning new values to the list object.
#'
#' It is possible for the z-scale to specify where ticks are to be drawn
#'  by using the parameter `at`, e.g. `at = seq(80, 200, 20)`, cf. function
#'  documentation of `axis`. Specifying tick positions manually overrides a
#' `zlim`-definition.
#'
#' @param data [data.frame] or [Luminescence::RLum.Results-class] object (**required**):
#' for `data.frame` two columns: De (`data[,1]`) and De error (`data[,2]`).
#'  To plot several data sets in one plot the data sets must be provided as
#'  `list`, e.g. `list(data.1, data.2)`.\cr
#' For some [Luminescence::RLum.Results-class] objects, one or more lines (and
#' corresponding labels) are drawn automatically:
#' - [Luminescence::calc_AverageDose] (ADM)
#' - [Luminescence::calc_CentralDose] (CDM)
#' - [Luminescence::calc_MaxDose] (MDM)
#' - [Luminescence::calc_MinDose] (MAM)
#' - [Luminescence::calc_FiniteMixture]
#' Alternative labels can be set via the `line.label` option. This behaviour
#' can be suppressed altogether by setting `line = NA`.
#'
#' @param na.rm [logical] (*with default*):
#' exclude `NA` values from the data set prior to any further operations.
#'
#' @param log.z [logical] (*with default*):
#' Option to display the z-axis in logarithmic scale. Default is `TRUE`.
#'
#' @param z.0 [character] or [numeric] (*with default*):
#' User-defined central value, used for centring of data. One out of `"mean"`,
#' `"mean.weighted"` and `"median"` or a numeric value (not its logarithm).
#' Default is `"mean.weighted"`.
#'
#' @param dispersion [character] (*with default*):
#' measure of dispersion, used for drawing the scatter polygon. One out of
#' - `"qr"` (quartile range, default),
#' - `"pnn"` (symmetric percentile range with `nn` the lower percentile, e.g.
#' `"p05"` indicating the range between 5 and 95 %, or `"p10"` indicating
#' the range between 10 and 90 %), or
#' - `"sd"` (standard deviation) and
#' - `"2sd"` (2 standard deviations),
#'
#' The default is `"qr"`. Note that `"sd"` and `"2sd"` are only meaningful in
#' combination with `"z.0 = 'mean'"` because the unweighted mean is used to
#' centre the polygon.
#'
#' @param plot.ratio [numeric] (*with default*):
#' Relative space, given to the radial versus the cartesian plot part,
#' default is 0.75.
#'
#' @param rotate [logical] (*with default*):
#' Option to turn the plot by 90 degrees.
#'
#' @param mtext [character] (*with default*):
#' additional text below the plot title.
#'
#' @param summary [character] (*with default*):
#' add statistic measures of centrality and dispersion to the plot.
#' Can be one or more of several keywords. See details for available keywords.
#' Results differ depending on the log-option for the z-scale (see details).
#'
#' @param summary.pos [numeric] or [character] (*with default*):
#' optional position coordinates or keyword (e.g. `"topright"`) for the
#' statistical summary. Alternatively, the keyword `"sub"` may be
#' specified to place the summary below the plot header. However, this latter
#' option in only possible if `mtext` is not used.
#'
#' @param summary.method [character] (*with default*):
#' keyword indicating the method used to calculate the statistic summary.
#' One of `"MCM"` (default), `"weighted"` or `"unweighted"`.
#' See [Luminescence::calc_Statistics] for details.
#'
#' @param legend [character] vector (*optional*):
#' legend content to be added to the plot.
#'
#' @param legend.pos [numeric] or [character] (*with default*):
#' optional position coordinates or keyword (e.g. `"topright"`)
#' for the legend to be plotted.
#'
#' @param stats [character]:
#' additional labels of statistically important values in the plot.
#' One or more out of the following:
#' - `"min"`,
#' - `"max"`,
#' - `"median"`.
#'
#' @param rug [logical] (*with default*):
#' Option to add a rug to the KDE part, to indicate the location of individual values.
#'
#' @param kde [logical] (*with default*):
#' Option to add a KDE plot to the dispersion part, default is `TRUE`.
#'
#' @param hist [logical] (*with default*):
#' Option to add a histogram to the dispersion part. Only meaningful when not
#' more than one data set is plotted.
#'
#' @param dots [logical] (*with default*):
#' Option to add a dot plot to the dispersion part. If number of dots exceeds
#' space in the dispersion part, a square indicates this.
#'
#' @param boxplot [logical] (*with default*):
#' Option to add a boxplot to the dispersion part, default is `FALSE`.
#'
#' @param y.axis [logical] (*with default*): Option to hide standard y-axis
#' labels and show 0 only.
#' Useful for data with small scatter. If you want to suppress the y-axis entirely
#' please use `yaxt == 'n'` (the standard [graphics::par] setting) instead.
#'
#' @param error.bars [logical] (*with default*):
#' Option to show De-errors as error bars on De-points. Useful in combination
#' with `y.axis = FALSE, bar.col = "none"`.
#'
#' @param bar [numeric] (*with default*):
#' option to add one or more dispersion bars (i.e., bar showing the 2-sigma range)
#' centred at the defined values. By default a bar is drawn according to `"z.0"`.
#' To omit the bar set `"bar = FALSE"`.
#'
#' @param bar.col [character] or [numeric] (*with default*):
#' colour of the dispersion bar. Default is `"grey60"`.
#'
#' @param polygon.col [character] or [numeric] (*with default*):
#' colour of the polygon showing the data scatter. Sometimes this
#' polygon may be omitted for clarity. To disable it use `FALSE` or
#' `polygon = FALSE`. Default is `"grey80"`.
#'
#' @param line [numeric] or [Luminescence::RLum.Results-class]:
#' numeric values of the additional lines to be added. This can be set to `NA`
#' to suppress the line added automatically by some `RLum.Results` objects.
#'
#' @param line.col [character] or [numeric]:
#' colour of the additional lines.
#'
#' @param line.lty [integer]:
#' line type of additional lines.
#'
#' @param line.label [character]:
#' labels for the additional lines.
#'
#' @param grid.col [character] or [numeric] (*with default*):
#' colour of the grid lines (originating at `[0,0]` and stretching to
#' the z-scale). To disable grid lines use `FALSE`. Default is `"grey"`.
#'
#' @param frame [numeric] (*with default*):
#' option to modify the plot frame type. Can be one out of
#' - `0` (no frame),
#' - `1` (frame originates at 0,0 and runs along min/max isochrons),
#' - `2` (frame embraces the 2-sigma bar),
#' - `3` (frame embraces the entire plot as a rectangle).
#'
#' Default is `1`.
#'
#' @param bw [character] (*with default*):
#' bin-width for KDE, choose a numeric value for manual setting.
#'
#' @param interactive [logical] (*with default*):
#' create an interactive abanico plot (requires the `'plotly'` package)
#'
#' @param ... Further plot arguments to pass (see [graphics::plot.default]).
#' Supported are: `main`, `sub`, `ylab`, `xlab`, `zlab`, `zlim`, `ylim`, `cex`,
#' `lty`, `lwd`, `pch`, `col`, `at`, `breaks`. `xlab` must be
#' a vector of length two, specifying the upper and lower x-axis labels.
#'
#' @return
#' Returns a plot object and, optionally, a list with plot calculus data.
#'
#' @section Function version: 0.1.21
#'
#' @author
#' Michael Dietze, GFZ Potsdam (Germany)\cr
#' Sebastian Kreutzer, F2.1 Geophysical Parametrisation/Regionalisation, LIAG - Institute for Applied Geophysics (Germany)\cr
#' Marco Colombo, Institute of Geography, Heidelberg University (Germany)\cr
#' Inspired by a plot introduced by Galbraith & Green (1990)
#'
#' @seealso [Luminescence::plot_RadialPlot], [Luminescence::plot_KDE],
#' [Luminescence::plot_Histogram], [Luminescence::plot_ViolinPlot]
#'
#' @references
#' Galbraith, R. & Green, P., 1990. Estimating the component ages
#' in a finite mixture. International Journal of Radiation Applications and
#' Instrumentation. Part D. Nuclear Tracks and Radiation Measurements 17 (3),
#' 197-206.
#'
#' Dietze, M., Kreutzer, S., Burow, C., Fuchs, M.C., Fischer, M., Schmidt, C., 2015.
#' The abanico plot: visualising chronometric data with individual standard errors.
#' Quaternary Geochronology. doi:10.1016/j.quageo.2015.09.003
#'
#' @examples
#'
#' ## load example data and recalculate to Gray
#' data(ExampleData.DeValues, envir = environment())
#' ExampleData.DeValues <- ExampleData.DeValues$CA1
#'
#' ## plot the example data straightforward
#' plot_AbanicoPlot(data = ExampleData.DeValues)
#'
#' ## now with linear z-scale
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  log.z = FALSE)
#'
#' ## now with output of the plot parameters
#' plot1 <- plot_AbanicoPlot(data = ExampleData.DeValues)
#' str(plot1)
#' plot1$zlim
#'
#' ## now with adjusted z-scale limits
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  zlim = c(10, 200))
#'
#' ## now with adjusted x-scale limits
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  xlim = c(0, 20))
#'
#' ## now with rug to indicate individual values in KDE part
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  rug = TRUE)
#'
#' ## now with a smaller bandwidth for the KDE plot
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  bw = 0.04)
#'
#' ## now with a histogram instead of the KDE plot
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  hist = TRUE,
#'                  kde = FALSE)
#'
#' ## now with a KDE plot and histogram with manual number of bins
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  hist = TRUE,
#'                  breaks = 20)
#'
#' ## now with a KDE plot and a dot plot
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  dots = TRUE)
#'
#' ## now with user-defined plot ratio
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  plot.ratio = 0.5)

#' ## now with user-defined central value
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  z.0 = 70)
#'
#' ## now with median as central value
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  z.0 = "median")
#'
#' ## now with the 17-83 percentile range as definition of scatter
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  z.0 = "median",
#'                  dispersion = "p17")
#'
#' ## now with user-defined green line for minimum age model
#' CAM <- calc_CentralDose(ExampleData.DeValues,
#'                         plot = FALSE)
#' plot_AbanicoPlot(data = CAM,
#'                  line.col = "darkseagreen")
#'
#' ## now create plot with legend, colour, different points and smaller scale
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  legend = "Sample 1",
#'                  col = "tomato4",
#'                  bar.col = "peachpuff",
#'                  pch = "R",
#'                  cex = 0.8)
#'
#' ## now without 2-sigma bar, polygon, grid lines and central value line
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  bar.col = FALSE,
#'                  polygon.col = FALSE,
#'                  grid.col = FALSE,
#'                  y.axis = FALSE,
#'                  lwd = 0)
#'
#' ## now with direct display of De errors, without 2-sigma bar
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  bar.col = FALSE,
#'                  ylab = "",
#'                  y.axis = FALSE,
#'                  error.bars = TRUE)
#'
#' ## now with user-defined axes labels
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  xlab = c("Data error (%)",
#'                           "Data precision"),
#'                  ylab = "Scatter",
#'                  zlab = "Equivalent dose [Gy]")
#'
#' ## now with minimum, maximum and median value indicated
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  stats = c("min", "max", "median"))
#'
#' ## now with a brief statistical summary as subheader
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  summary = c("n", "in.2s"))
#'
#' ## now with another statistical summary
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  summary = c("mean.weighted", "median"),
#'                  summary.pos = "topleft")
#'
#' ## now a plot with two 2-sigma bars for one data set
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  bar = c(30, 100))
#'
#' ## now the data set is split into sub-groups, one is manipulated
#' data.1 <- ExampleData.DeValues[1:30,]
#' data.2 <- ExampleData.DeValues[31:62,] * 1.3
#'
#' ## now a common dataset is created from the two subgroups
#' data.3 <- list(data.1, data.2)
#'
#' ## now the two data sets are plotted in one plot
#' plot_AbanicoPlot(data = data.3)
#'
#' ## now with some graphical modification
#' plot_AbanicoPlot(data = data.3,
#'                  z.0 = "median",
#'                  col = c("steelblue4", "orange4"),
#'                  bar.col = c("steelblue3", "orange3"),
#'                  polygon.col = c("steelblue1", "orange1"),
#'                  pch = c(2, 6),
#'                  angle = c(30, 50),
#'                  summary = c("n", "in.2s", "median"))
#'
#' ## create Abanico plot with predefined layout definition
#' plot_AbanicoPlot(data = ExampleData.DeValues,
#'                  layout = "journal")
#'
#' ## now with predefined layout definition and further modifications
#' plot_AbanicoPlot(
#'  data = data.3,
#'  z.0 = "median",
#'  layout = "journal",
#'  col = c("steelblue4", "orange4"),
#'  bar.col = adjustcolor(c("steelblue3", "orange3"),
#'                          alpha.f = 0.5),
#'  polygon.col = c("steelblue3", "orange3"))
#'
#' ## for further information on layout definitions see documentation
#' ## of function get_Layout()
#'
#' ## now with manually added plot content
#' ## create empty plot with numeric output
#' AP <- plot_AbanicoPlot(data = ExampleData.DeValues,
#'                        pch = NA)
#'
#' ## identify data in 2 sigma range
#' in_2sigma <- AP$data[[1]]$data.in.2s
#'
#' ## restore function-internal plot parameters
#' par(AP$par)
#'
#' ## add points inside 2-sigma range
#' points(x = AP$data[[1]]$precision[in_2sigma],
#'        y = AP$data[[1]]$std.estimate.plot[in_2sigma],
#'        pch = 16)
#'
#' ## add points outside 2-sigma range
#' points(x = AP$data[[1]]$precision[!in_2sigma],
#'        y = AP$data[[1]]$std.estimate.plot[!in_2sigma],
#'        pch = 1)
#'
#' @export
plot_AbanicoPlot <- function(
  data,
  na.rm = TRUE,
  log.z = TRUE,
  z.0 = c("mean.weighted", "mean.weighted", "median"),
  dispersion = c("qr", "sd", "2sd"),
  plot.ratio = 0.75,
  rotate = FALSE,
  mtext = "",
  summary = c("n", "in.2s"),
  summary.pos = "sub",
  summary.method = c("MCM", "weighted", "unweighted"),
  legend = NULL,
  legend.pos = "topleft",
  stats = NULL,
  rug = FALSE,
  kde = TRUE,
  hist = FALSE,
  dots = FALSE,
  boxplot = FALSE,
  y.axis = TRUE,
  error.bars = FALSE,
  bar = NULL,
  bar.col = NULL,
  polygon.col = NULL,
  line = NULL,
  line.col = NULL,
  line.lty = NULL,
  line.label = NULL,
  grid.col = NULL,
  frame = 1,
  bw = "SJ",
  interactive = FALSE,
  ...
) {
  .set_function_name("plot_AbanicoPlot")
  on.exit(.unset_function_name(), add = TRUE)

  ## Integrity checks -------------------------------------------------------

  ## Homogenise input data format
  if (!inherits(data, "list")) {
    data <- list(data)
  }

  ## whether the user has set the line argument or not
  line.is.null <- is.null(line)
  line.mtext <- NULL

  ## Check input data
  for (i in seq_along(data)) {
    .validate_class(data[[i]], c("data.frame", "RLum.Results"),
                    name = "All elements of 'data'")
    if (inherits(data[[i]], "RLum.Results")) {
      if (line.is.null &&
          .check_originator(data[[i]], c("calc_AverageDose", "calc_CentralDose",
                                         "calc_MaxDose", "calc_MinDose",
                                         "calc_FiniteMixture"))) {
        ## set lines automatically based on originator
        de <- data[[i]]$summary$de
        de.pm.err <- sprintf("%.2f \u00b1 %.2f", de, data[[i]]$summary$de_err)
        lab <- switch(data[[i]]@originator,
                      "calc_AverageDose" = "ADM",
                      "calc_CentralDose" = "CDM",
                      "calc_MaxDose" = "MDM",
                      "calc_MinDose" = "MAM",
                      "calc_FiniteMixture" = de.pm.err)
        line <- c(line, de)
        line.label <- c(line.label, lab)
        if (data[[i]]@originator == "calc_FiniteMixture")
          line.mtext <- c(line.mtext, paste("FMM:", length(de), "components"))
        else
          line.mtext <- c(line.mtext, paste0(lab, ": ", de.pm.err))
      }
      data[[i]] <- get_RLum(data[[i]], "data")
    }

      if (ncol(data[[i]]) < 2) {
        .throw_error("Data set ", i, " has fewer than 2 columns: data ",
                     "without errors cannot be displayed")
      }

      data[[i]] <- data[[i]][, 1:2]
  }
  if (!is.null(line.mtext) && mtext == "" && summary.pos != "sub")
    mtext <- line.mtext

  ## optionally, remove NA-values
  .validate_logical_scalar(na.rm)
  if (na.rm) {
    for(i in seq_along(data)) {
      n.NA <- sum(!stats::complete.cases(data[[i]]))
      if (n.NA > 0) {
        .throw_message("Data set ", i, ": ", n.NA, " NA value",
                       ifelse(n.NA > 1, "s", ""), " excluded", error = FALSE)
        data[[i]] <- na.exclude(data[[i]])
      }
    }
  }

  ##AFTER NA removal, we should check the data set carefully again ...
  ##(1)
  ##check if there is still data left in the entire set
  if(all(sapply(data, nrow) == 0)){
    .throw_message("'data' is empty, nothing plotted")
    return(NULL)
  }
  ##(2)
  ##check for sets with only 1 row or 0 rows at all
  if (any(sapply(data, nrow) < 2)) {
    ##select problematic sets and remove the entries from the list
    NArm.id <- which(sapply(data, nrow) <= 1)
    data[NArm.id] <- NULL
    .throw_warning("Data set ", toString(NArm.id),
                   " empty or consisting of only 1 row, removed")

    ##unfortunately, the data set might become now empty at all
    if(length(data) == 0){
      .throw_message("After removing invalid entries, nothing is plotted")
      return(NULL)
    }
  }

  ## check for zero-error values
  for(i in 1:length(data)) {
    if (any(data[[i]][, 2] == 0)) {
      data[[i]] <- data[[i]][data[[i]][,2] > 0,]
      if (nrow(data[[i]]) < 1){
        .throw_error("Data set contains only values with zero errors")
      }
      .throw_warning("Values with zero errors cannot be displayed and were removed")
    }
  }

  ## check for 0 values in dataset for log
  .validate_logical_scalar(log.z)
  if (log.z) {
    for(i in 1:length(data)) {
      if(any(data[[i]][[1]] == 0)) {
        .throw_warning("Found zero values in x-column of dataset ", i,
                       ", 'log.z' set to FALSE")
        log.z <- FALSE
      }
    }
  }

  ## plot.ratio must be numeric and positive
  .validate_positive_scalar(plot.ratio)
  .validate_logical_scalar(rotate)
  .validate_logical_scalar(rug)
  .validate_logical_scalar(kde)
  .validate_logical_scalar(hist)
  .validate_logical_scalar(dots)
  .validate_logical_scalar(boxplot)
  .validate_logical_scalar(error.bars)

  if (!is.numeric(z.0)) {
    .validate_class(z.0, "character")
    z.0 <- .validate_args(z.0, c("mean", "mean.weighted", "median"),
                          extra = "a numerical value")
  }

  ## the 'pnn' option need some special treatment
  main.choices <- c("qr", "sd", "2sd")
  extra.choice <-"a percentile of the form 'pnn' (eg. 'p05')"
  if (!any(grepl("^p[0-9][0-9]$", dispersion)))
    dispersion <- .validate_args(dispersion, main.choices, extra = extra.choice)
  .validate_length(dispersion, 1)

  valid.pos <- c("left", "center", "right", "topleft", "top", "topright",
                 "bottomleft", "bottom", "bottomright")
  .validate_class(summary, "character")
  if (is.numeric(summary.pos)) {
    .validate_length(summary.pos, 2)
  }
  else {
    summary.pos <- .validate_args(summary.pos, c("sub", valid.pos))
  }
  .validate_class(legend, "character", null.ok = TRUE)
  if (is.numeric(legend.pos)) {
    .validate_length(legend.pos, 2)
  } else {
    legend.pos <- .validate_args(legend.pos, valid.pos)
  }

  summary.method <- .validate_args(summary.method, c("MCM", "weighted", "unweighted"))
  frame <- .validate_args(frame, c(0, 1, 2, 3))

  ## check/set layout definitions
  layout <- get_Layout(layout = list(...)$layout %||% "default")

  if (is.null(bar))
    bar <- rep(TRUE, length(data))

  if (is.null(bar.col)) {
    bar.fill <- rep(rep_len(layout$abanico$colour$bar.fill, length(data)),
                    length(bar))
    bar.line <- rep(rep_len(layout$abanico$colour$bar.line, length(data)),
                    length(bar))
  } else {
    bar.fill <- bar.col
    bar.line <- NA
  }

  if (is.null(polygon.col)) {
    polygon.fill <- rep_len(layout$abanico$colour$poly.fill, length(data))
    polygon.line <- rep_len(layout$abanico$colour$poly.line, length(data))
  } else {
    polygon.fill <- polygon.col
    polygon.line <- NA
  }

  if (is.null(grid.col)) {
    grid.major <- layout$abanico$colour$grid.major
    grid.minor <- layout$abanico$colour$grid.minor
  } else {
    grid.major <- grid.col[1]
    grid.minor <- grid.col[min(length(grid.col), 2)]
  }

  ## create preliminary global data set
  De.global <- unlist(lapply(data, function(x) x[, 1]))

  ## additional arguments
  extraArgs <- list(...)
  breaks <- extraArgs$breaks %||% "Sturges"
  fun <- isTRUE(list(...)$fun)

  ## check/set bw-parameter
  for(i in 1:length(data)) {
    bw.test <- try(density(x = data[[i]][,1],
                           bw = bw),
                   silent = TRUE)
    if (inherits(bw.test, "try-error")) {
      bw <- "nrd0"
      .throw_warning("Option for 'bw' not valid, reset to 'nrd0'")
    }
  }

  ## check for negative values, stop function, but do not stop
  De.add <- 0
  if(min(De.global) < 0) {
    if("zlim" %in% names(extraArgs)) {
      De.add <- abs(extraArgs$zlim[1])
    } else {
      ## estimate delta De to add to all data
      De.add <-  min(10^ceiling(log10(abs(De.global))) * 10)

      ## optionally readjust delta De for extreme values
      if(De.add <= abs(min(De.global))) {
        De.add <- De.add * 10
      }
    }
  }

  ## optionally add correction dose to data set and adjust error
  if (log.z) {
    for(i in 1:length(data))
      data[[i]][,1] <- data[[i]][,1] + De.add

    De.global <- De.global + De.add
  }

  ## calculate and append statistical measures --------------------------------

  ## z-values and se based on log-option
  data <- lapply(data, function(x, De.add) {
    cbind(x,
          z = if (log.z) log(x[, 1]) else x[, 1],
          se = if (log.z) x[, 2] / (x[, 1] + De.add) else x[, 2])
  }, De.add = De.add)

  ## calculate initial data statistics
  stats.init <- lapply(data, function(x) calc_Statistics(data = x[, 3:4]))

  ## calculate central values
  if (z.0 %in% c("mean", "median")) {
    z.central <- lapply(1:length(data), function(x){
      rep(stats.init[[x]]$unweighted[[z.0]],
          length(data[[x]][,3]))})

  } else if(z.0 == "mean.weighted") {
    z.central <- lapply(1:length(data), function(x){
      rep(stats.init[[x]]$weighted$mean,
          length(data[[x]][,3]))})

  } else {
    ## z.0 is numeric
    z.central <- lapply(1:length(data), function(x){
      rep(ifelse(log.z,
                 log(z.0),
                 z.0),
          length(data[[x]][,3]))})
  }

  data <- lapply(1:length(data), function(x) {
    cbind(data[[x]], z.central[[x]])})
  rm(z.central)

  ## calculate precision and standard estimate
  data <- lapply(data, function(x) {
    cbind(x,
          precision = 1 / x[, 4],
          std.estimate = (x[, 3] - x[, 5]) / x[, 4],
          std.estimate.plot = NA)
  })

  ## append optional weights for KDE curve
  use.weights <- "weights" %in% names(extraArgs) && extraArgs$weights
  data <- lapply(data, function(x) {
    cbind(x,
          weights = if (use.weights) (1 / x[, 2]) / sum(1 / x[, 2]^2)
                    else 1 / nrow(x))
  })

  ## generate global data set
  data.global <- cbind(data[[1]], 1)
  colnames(data.global) <- rep("", 10)

  if(length(data) > 1) {
    for(i in 2:length(data)) {
      data.add <- cbind(data[[i]], i)
      colnames(data.add) <- rep("", 10)
      data.global <- rbind(data.global,
                           data.add)
    }
  }

  ## create column names
  colnames(data.global) <- c("De",
                             "error",
                             "z",
                             "se",
                             "z.central",
                             "precision",
                             "std.estimate",
                             "std.estimate.plot",
                             "weights",
                             "data set")

  ## calculate global data statistics
  stats.global <- calc_Statistics(data = data.global[,3:4])

  ## calculate global central value
  if (z.0 %in% c("mean", "median")) {
    z.central.global <- stats.global$unweighted[[z.0]]
  } else  if(z.0 == "mean.weighted") {
    z.central.global <- stats.global$weighted$mean
  } else if(is.numeric(z.0)) {
    z.central.global <- ifelse(log.z,
                               log(z.0),
                               z.0)
  }

  ## create column names
  for(i in 1:length(data)) {
    colnames(data[[i]]) <- c("De",
                             "error",
                             "z",
                             "se",
                             "z.central",
                             "precision",
                             "std.estimate",
                             "std.estimate.plot",
                             "weights")
  }

  ## re-calculate standardised estimate for plotting
  for(i in 1:length(data)) {
    data[[i]][,8] <- (data[[i]][,3] - z.central.global) / data[[i]][,4]
  }

  data.global.plot <- data[[1]][,8]
  if(length(data) > 1) {
    for(i in 2:length(data)) {
      data.global.plot <- c(data.global.plot, data[[i]][,8])
    }
  }
  data.global[,8] <- data.global.plot

  ## print message for too small scatter
  if(max(abs(1 / data.global[6])) < 0.02) {
    .throw_message("Small standardised estimate scatter, toggle off y.axis?",
                   error = FALSE)
  }

  ## read out additional arguments---------------------------------------------
  extraArgs <- list(...)

  main <- extraArgs$main %||% expression(D[e] * " " * "distribution")
  sub <- extraArgs$sub %||% ""

  xlab <- if ("xlab" %in% names(extraArgs)) {
            if (!length(extraArgs$xlab) %in% c(2, 3))
              .throw_error("'xlab' must have length 2")
            c(extraArgs$xlab[1:2], "Density")
          } else {
            c(if (log.z) "Relative standard error [%]" else "Standard error",
              "Precision",
              "Density")
          }
  ylab <- extraArgs$ylab %||% "Standardised estimate"
  zlab <- extraArgs$zlab %||% expression(D[e] * " " * "[Gy]")

  limits.z <- extraArgs$zlim
  if (!is.null(limits.z)) {
    .validate_class(limits.z, "numeric", name = "'zlim'")
    if (log.z && any(limits.z <= 0)) {
      .throw_error("'zlim' should only contain positive values when 'log.z = TRUE'")
    }
  } else {
    z.span <- (mean(data.global[,1]) * 0.5) / (sd(data.global[,1]) * 100)
    z.span <- ifelse(z.span > 1, 0.9, z.span)
    limits.z <- c((0.9 - z.span) * min(data.global[[1]]),
                  (1.1 + z.span) * max(data.global[[1]]))
  }

  limits.x <- extraArgs$xlim
  if (!is.null(limits.x)) {
    .validate_class(limits.x, "numeric", name = "'xlim'")
    if (limits.x[1] != 0) {
      .throw_warning("Lower x-axis limit was ", limits.x[1], ", reset to zero")
      limits.x[1] <- 0
    }
  } else {
    limits.x <- c(0, max(data.global[,6]) * 1.05)
  }

  limits.y <- extraArgs$ylim
  if (!is.null(limits.y)) {
    .validate_class(limits.y, "numeric", name = "'ylim'")
  } else {
    y.span <- (mean(data.global[,1]) * 10) / (sd(data.global[,1]) * 100)
    y.span <- ifelse(y.span > 1, 0.98, y.span)
    limits.y <- c(-(1 + y.span) * max(abs(data.global[,7])),
                  (1 + y.span) * max(abs(data.global[,7])))
  }

  cex <- extraArgs$cex %||% 1
  lty <- extraArgs$lty %||% rep(rep(2, length(data)), length(bar))
  lwd <- extraArgs$lwd %||% rep(rep(1, length(data)), length(bar))
  pch <- extraArgs$pch %||% rep(20, length(data))

  if("col" %in% names(extraArgs)) {
    bar.col <- extraArgs$col
    kde.line <- extraArgs$col
    kde.fill <- NA
    value.dot <- extraArgs$col
    value.bar <- extraArgs$col
    value.rug <- extraArgs$col
    summary.col <- extraArgs$col
    centrality.col <- extraArgs$col
  } else {
    bar.col <- layout$abanico$colour$bar.fill
    if(length(layout$abanico$colour$bar.fill) == 1) {
      bar.col <- 1:length(data)
    }

    kde.line <- layout$abanico$colour$kde.line
    if(length(layout$abanico$colour$kde.line) == 1) {
      kde.line <- 1:length(data)
    }

    kde.fill <- layout$abanico$colour$kde.fill
    if(length(layout$abanico$colour$kde.fill) == 1) {
      kde.fill <- rep(layout$abanico$colour$kde.fill, length(data))
    }

    value.dot <- layout$abanico$colour$value.dot
    if(length(layout$abanico$colour$value.dot) == 1) {
      value.dot <- 1:length(data)
    }

    value.bar <- layout$abanico$colour$value.bar
    if(length(layout$abanico$colour$value.bar) == 1) {
      value.bar <- 1:length(data)
    }

    value.rug <- layout$abanico$colour$value.rug
    if(length(layout$abanico$colour$value.rug) == 1) {
      value.rug <- 1:length(data)
    }

    summary.col <- layout$abanico$colour$summary
    if(length(layout$abanico$colour$summary) == 1) {
      summary.col <- 1:length(data)
    }

    if(length(layout$abanico$colour$centrality) == 1) {
      centrality.col <- rep(x = 1:length(data), times = length(bar))
    } else {
      centrality.col <- rep(x = layout$abanico$colour$centrality,
                            times = length(bar))
    }
  }

  ## update central line colour
  centrality.col <- rep(centrality.col, length(bar))

  ## define auxiliary plot parameters -----------------------------------------
  ## set space between z-axis and baseline of cartesian part
  lostintranslation <- 1.03
  if (!boxplot) {
    plot.ratio <- plot.ratio * 1.05
  }

  ## save the original plot parameters and restore them when exiting
  ## this must be done after all validations have completed, otherwise a
  ## warning may be generated (#1001) if any validation step fails.
  if (sum(par()$mfrow) == 2 && sum(par()$mfcol) == 2) {
    par.default <- .par_defaults()
  } else {
    ## this ensures that mfrow/mfcol are not reset when we want to draw
    ## several plots on one page
    par.default <- par(c("mar", "mai", "xpd", "cex"))
  }
  on.exit(par(par.default), add = TRUE)

  ## wrapper functions to deal with rotation
  plot.rot <- function(xlim, ylim, ...) {
    if (!rotate) plot(xlim = xlim, ylim = ylim, ...) else plot(xlim = ylim, ylim = xlim, ...)
  }
  polygon.rot <- function(x, y, ...) {
    if (!rotate) polygon(x, y, ...) else polygon(y, x, ...)
  }
  points.rot <- function(x, y, ...) {
    if (!rotate) points(x, y, ...) else points(y, x, ...)
  }
  lines.rot <- function(x, y, ...) {
    if (!rotate) lines(x, y, ...) else lines(y, x, ...)
  }
  text.rot <- function(x, y, ...) {
    if (!rotate) text(x, y, ...) else text(y, x, ...)
  }
  text_with_bg.rot <- function(x, y, ...) {
    if (!rotate) .text_with_bg(x, y, ...) else .text_with_bg(y, x, ...)
  }

  ## create empty plot to update plot parameters
  plot.rot(NA,
       xlim = c(limits.x[1], limits.x[2] * (1 / plot.ratio)),
       ylim = limits.y,
       main = "",
       sub = "",
       xlab = "",
       ylab = "",
       xaxs = "i",
       yaxs = "i",
       frame.plot = FALSE,
       axes = FALSE)

  ## calculate conversion factor for plot coordinates
  f <- 0

  ## calculate major and minor z-tick values
  if("at" %in% names(extraArgs)) {
    tick.values.major <- extraArgs$at
    tick.values.minor <- extraArgs$at
  } else {
    tick.values.major <- signif(pretty(limits.z, n = 5), 3)
    tick.values.minor <- signif(pretty(limits.z, n = 25), 3)
  }

  tick.values.major <- tick.values.major[
      between(tick.values.major, limits.z[1], limits.z[2]) &
      between(tick.values.major, min(tick.values.minor), max(tick.values.minor))]
  tick.values.minor <- tick.values.minor[
      between(tick.values.minor, limits.z[1], limits.z[2])]
  label.z.text <- signif(tick.values.major, 3)

  if (log.z) {
    tick.values.major[tick.values.major == 0] <- 1
    tick.values.minor[tick.values.minor == 0] <- 1

    tick.values.major <- log(tick.values.major)
    tick.values.minor <- log(tick.values.minor)
    label.z.text <- signif(exp(tick.values.major) - De.add, 3)
  }

  ## calculate z-axis radius
  r <- max(sqrt((limits.x[2])^2 + (data.global[,7] * f)^2))

  ## calculate node coordinates for semi-circle
  ellipse.values <- c(min(ifelse(log.z,
                                 log(limits.z[1]),
                                 limits.z[1]),
                          tick.values.major,
                          tick.values.minor),
                      max(ifelse(log.z,
                                 log(limits.z[2]),
                                 limits.z[2]),
                          tick.values.major,
                          tick.values.minor))

  ## correct for unpleasant value
  ellipse.values[ellipse.values == -Inf] <- 0

  ellipse.x <- r / sqrt(1 + f^2 * (ellipse.values - z.central.global)^2)
  ellipse.y <- (ellipse.values - z.central.global) * ellipse.x
  ellipse <- cbind(ellipse.x, ellipse.y)
  if (rotate)
    ellipse <- ellipse[, 2:1]

  ## calculate statistical labels
  stats.data <- matrix(nrow = 3, ncol = 3)
  data.stats <- as.numeric(data.global[,1])

  if ("min" %in% stats) {
    stats.data[1, 3] <- data.stats[data.stats == min(data.stats)][1]
    stats.data[1, 1] <- data.global[data.stats == stats.data[1, 3], 6][1]
    stats.data[1, 2] <- data.global[data.stats == stats.data[1, 3], 8][1]
  }

  if ("max" %in% stats) {
    stats.data[2, 3] <- data.stats[data.stats == max(data.stats)][1]
    stats.data[2, 1] <- data.global[data.stats == stats.data[2, 3], 6][1]
    stats.data[2, 2] <- data.global[data.stats == stats.data[2, 3], 8][1]
  }

  if ("median" %in% stats) {
    stats.data[3, 3] <- data.stats[data.stats == quantile(data.stats, 0.5, type = 3)]
    stats.data[3, 1] <- data.global[data.stats == stats.data[3, 3], 6][1]
    stats.data[3, 2] <- data.global[data.stats == stats.data[3, 3], 8][1]
  }

  ## index to pick according to the value of the rotate argument
  rotate.idx <- if (!rotate) 1 else 2
  min.ellipse <- min(ellipse[, rotate.idx])
  max.ellipse <- max(ellipse[, rotate.idx])

  ## re-calculate axes limits if necessary
  if(!("ylim" %in% names(extraArgs))) {
    limits.z.y <- range(ellipse[, 3 - rotate.idx])
    if(limits.z.y[1] < 0.66 * limits.y[1]) {
      limits.y[1] <- 1.8 * limits.z.y[1]
    }
    if(limits.z.y[2] > 0.77 * limits.y[2]) {
      limits.y[2] <- 1.3 * limits.z.y[2]
    }

    if (rotate) {
      limits.y <- c(-max(abs(limits.y)), max(abs(limits.y)))
    }
  }
  if(!("xlim" %in% names(extraArgs))) {
    limits.z.x <- range(ellipse[, rotate.idx])
    limits.x[2] <- max(limits.z.x[2], limits.z.x)
  }

  ## calculate and paste statistical summary
  De.stats <- matrix(nrow = length(data), ncol = 12)
  colnames(De.stats) <- c("n",
                          "mean",
                          "median",
                          "kdemax",
                          "sd.abs",
                          "sd.rel",
                          "se.abs",
                          "se.rel",
                          "q.25",
                          "q.75",
                          "skewness",
                          "kurtosis")

  for(i in 1:length(data)) {
    statistics <- calc_Statistics(data[[i]])[[summary.method]]
    statistics.2 <- calc_Statistics(data[[i]][,3:4])[[summary.method]]

    De.stats[i,1] <- statistics$n
    De.stats[i,2] <- statistics.2$mean
    De.stats[i,3] <- statistics.2$median
    De.stats[i,5] <- statistics$sd.abs
    De.stats[i,6] <- statistics$sd.rel
    De.stats[i,7] <- statistics$se.abs
    De.stats[i,8] <- statistics$se.rel
    De.stats[i,9] <- quantile(data[[i]][,1], 0.25)
    De.stats[i,10] <- quantile(data[[i]][,1], 0.75)
    De.stats[i,11] <- statistics$skewness
    De.stats[i,12] <- statistics$kurtosis

    ## account for log.z-option
    if (log.z) {
      De.stats[i,2:4] <- exp(De.stats[i,2:4]) - De.add
    }

    ## kdemax - here a little doubled as it appears below again
    De.density <- try(density(x = data[[i]][,1],
                              kernel = "gaussian",
                              bw = bw,
                              from = limits.z[1],
                              to = limits.z[2]),
                      silent = TRUE)

    De.stats[i, 4] <- NA
    if (!inherits(De.density, "try-error")) {
      De.stats[i, 4] <- De.density$x[which.max(De.density$y)]
    }
  }

  ## helper to generate an element of the statistical summary
  .summary_line <- function(keyword, summary, val, label = keyword,
                            percent = FALSE, sep = FALSE, digits = 2) {
    ifelse(keyword %in% summary,
           paste0(label, " = ", round(val, digits),
                  if (percent) " %" else NULL, if (sep) " | " else "\n"),
           "")
  }

  is.sub <- summary.pos[1] == "sub"
  stops <- NULL
  label.text <- list()
  for (i in 1:length(data)) {
    if (!is.sub)
      stops <- strrep("\n", (i - 1) * length(summary))

    summary.text <- character(0)
    for (j in 1:length(summary)) {
      summary.text <-
        c(summary.text,
          .summary_line("n", summary[j], De.stats[i, 1], sep = is.sub),
          .summary_line("mean", summary[j], De.stats[i, 2], sep = is.sub),
          .summary_line("median", summary[j], De.stats[i, 3], sep = is.sub),
          .summary_line("kdemax", summary[j], De.stats[i, 4], sep = is.sub),
          .summary_line("sd.abs", summary[j], De.stats[i, 5], sep = is.sub,
                        label = "abs. sd"),
          .summary_line("sd.rel", summary[j], De.stats[i, 6], sep = is.sub,
                        label = "rel. sd"),
          .summary_line("se.abs", summary[j], De.stats[i, 7], sep = is.sub,
                        label = "se"),
          .summary_line("se.rel", summary[j], De.stats[i, 8], sep = is.sub,
                        label = "rel. se", percent = TRUE),
          .summary_line("skewness", summary[j], De.stats[i, 11], sep = is.sub),
          .summary_line("kurtosis", summary[j], De.stats[i, 12], sep = is.sub),
          .summary_line("in.2s", summary[j],
                        sum(data[[i]][,7] > -2 & data[[i]][,7] < 2) /
                        nrow(data[[i]]) * 100, sep = is.sub,
                        label = "in 2 sigma", percent = TRUE, digits = 1))
    }
    label.text[[i]] <- paste0(
        if (is.sub) "" else stops,
        paste(summary.text, collapse = ""),
        stops)
  }

  ## remove outer vertical lines from string1
  if (is.sub) {
    for (i in seq_along(label.text)) {
      label.text[[i]] <- substr(x = label.text[[i]],
                                start = 1,
                                stop = nchar(label.text[[i]]) - 3)
      if (!is.null(line.mtext))
        label.text[[i]] <- paste(label.text[[i]], "|", line.mtext[i])
    }
  }

  if (!rotate) {
    ## convert keywords into summary placement coordinates
    coords <- .get_keyword_coordinates(summary.pos, limits.x, limits.y)

    ## apply some adjustments to the y positioning
    if (summary.pos[1] %in% c("topleft", "top", "topright")) {
        coords$pos[2] <- coords$pos[2] - par()$cxy[2] * 1.0
      } else if (summary.pos[1] %in% c("bottomleft", "bottom", "bottomright")) {
        coords$pos[2] <- coords$pos[2] + par()$cxy[2] * 3.5
    }
    summary.pos <- coords$pos
    summary.adj <- coords$adj

    ## convert keywords into legend placement coordinates
    coords <- .get_keyword_coordinates(legend.pos, limits.x, limits.y)
    legend.pos <- coords$pos
    legend.adj <- coords$adj

  } else {
    ## convert keywords into summary placement coordinates
    ## this time we swap x and y limits as we are rotated, then apply some
    ## adjustments to the x positioning
    coords <- .get_keyword_coordinates(summary.pos, limits.y, limits.x)
    if (summary.pos[1] %in% c("topleft", "left", "bottomleft")) {
      coords$pos[1] <- coords$pos[1] + par()$cxy[1] * 7.5
    }
    summary.pos <- coords$pos
    summary.adj <- coords$adj

    ## convert keywords into legend placement coordinates
    ## this time we swap x and y limits as we are rotated, then apply some
    ## adjustments to the x positioning
    coords <- .get_keyword_coordinates(legend.pos, limits.y, limits.x)
    if (!is.null(legend.pos) &&
        legend.pos[1] %in% c("topleft", "left", "bottomleft")) {
      coords$pos[1] <- coords$pos[1] + par()$cxy[1] * 7.5
    }
    legend.pos <- coords$pos
    legend.adj <- coords$adj
  }

  ## define cartesian plot origins
  if (!rotate) {
    xy.0 <- c(min(ellipse[,1]) * lostintranslation, min(ellipse[,2]))
  } else {
    xy.0 <- c(min(ellipse[,1]), min(ellipse[,2]) * lostintranslation)
  }

  ## calculate coordinates for dispersion polygon overlay
  y.max <- if (!rotate) par()$usr[2] else par()$usr[4]

  polygons.x <- c(limits.x[1], limits.x[2], xy.0[rotate.idx], y.max, y.max,
                  xy.0[rotate.idx], limits.x[2])
  polygons.y <- matrix(nrow = length(data), ncol = 7)
  for(i in 1:length(data)) {
    if(dispersion == "qr") {
      ci.lo_up <- quantile(data[[i]][, 1], c(0.25, 0.75))
    } else if (grepl("p", dispersion)) {
      ci.plot <- as.numeric(strsplit(x = dispersion,
                                     split = "p")[[1]][2])
      ci.plot <- (100 - ci.plot) / 100
      ci.lo_up <- quantile(data[[i]][, 1], c(ci.plot, 1 - ci.plot))
    } else if(dispersion == "sd") {
      if (log.z) {
        ci.lo_up <- exp(mean(log(data[[i]][, 1])) + c(-1, 1) * sd(log(data[[i]][, 1])))
      } else {
        ci.lo_up <- mean(data[[i]][, 1]) + c(-1, 1) * sd(data[[i]][, 1])
      }
    } else if(dispersion == "2sd") {
      if (log.z) {
        ci.lo_up <- exp(mean(log(data[[i]][, 1])) + c(-2, 2) * sd(log(data[[i]][, 1])))
      } else {
        ci.lo_up <- mean(data[[i]][, 1]) + c(-2, 2) * sd(data[[i]][, 1])
      }
    }

    if (log.z) {
      ci.lo_up[which(ci.lo_up < 0)] <- 1
      ci.lo_up <- log(ci.lo_up)
    }
    y.lower <- ci.lo_up[1] - z.central.global
    y.upper <- ci.lo_up[2] - z.central.global

    polygons.y[i, ] <- c(0,
                         y.upper * limits.x[2],
                         y.upper * xy.0[rotate.idx],
                         y.upper * xy.0[rotate.idx],
                         y.lower * xy.0[rotate.idx],
                         y.lower * xy.0[rotate.idx],
                         y.lower * limits.x[2])
  }

  ## append information about data in confidence interval
  for(i in 1:length(data)) {
    data.in.2s <- rep(x = FALSE, times = nrow(data[[i]]))
    data.in.2s[data[[i]][,8] > -2 & data[[i]][,8] < 2] <- TRUE
    data[[i]] <- cbind(data[[i]], data.in.2s)
  }

  ## calculate KDE
  KDE <- list()
  KDE.bw <- numeric(length(data))

  for(i in 1:length(data)) {
    KDE.i <- density(x = data[[i]][,3],
                     kernel = "gaussian",
                     bw = bw,
                     from = ellipse.values[1],
                     to = ellipse.values[2],
                     weights = data[[i]]$weights)
    KDE.bw[i] <- KDE.i$bw
    KDE[[i]] <- rbind(c(min(KDE.i$x), 0),
                      cbind(KDE.i$x, KDE.i$y),
                      c(max(KDE.i$x), 0))
  }

  ## calculate mean KDE bandwidth
  KDE.bw <- mean(KDE.bw, na.rm = TRUE)

  ## calculate KDE width
  KDE.max <- max(vapply(KDE, function(x) max(x[, 2]), numeric(1)))

  ## optionally adjust KDE width for boxplot option
  if (boxplot) {
    KDE.max <- 1.3 * KDE.max
  }

  ## Generate plot ------------------------------------------------------------
  ##
  ## determine number of subheader lines to shift the plot
  if(length(summary) > 0 & summary.pos[1] == "sub") {
    shift.lines <- (length(data) + 1) * layout$abanico$dimension$summary.line/100
  } else {
    shift.lines <- 1
  }

  ## extract original plot parameters
  bg.original <- par()$bg
  par(bg = layout$abanico$colour$background)

  ## setup plot area
  par(mar = if (!rotate) c(4.5, 4.5, shift.lines + 1.5, 7) else c(4, 4, shift.lines + 5, 4),
      xpd = TRUE,
      cex = cex)

    dim <- layout$abanico$dimension
    if (dim$figure.width != "auto" || dim$figure.height != "auto") {
      par(mai = dim$margin / 25.4,
          pin = c(dim$figure.width - dim$margin[2] - dim$margin[4],
                  dim$figure.height - dim$margin[1] - dim$margin[3]) / 25.4)
    }

    ## create empty plot
    par(new = TRUE)
    plot.rot(NA,
         xlim = c(limits.x[1], limits.x[2] * (1 / plot.ratio)),
         ylim = limits.y,
         main = "",
         sub = sub,
         xlab = "",
         ylab = "",
         xaxs = "i",
         yaxs = "i",
         frame.plot = FALSE,
         axes = FALSE)

    ## add y-axis label
    mtext(text = ylab,
          at = 0,
          adj = 0.5,
          side = 3 - rotate.idx,
          line = 3 * layout$abanico$dimension$ylab.line / 100,
          col = layout$abanico$colour$ylab,
          family = layout$abanico$font.type$ylab,
          font = which(c("normal", "bold", "italic", "bold italic") ==
                         layout$abanico$font.deco$ylab)[1],
          cex = cex * layout$abanico$font.size$ylab / 12)

    ## calculate upper x-axis label values
    label.x.upper <- as.character(round(1 / axTicks(side = rotate.idx)[-1] *
                                        if (log.z) 100 else 1, 1))

    ## optionally, plot 2-sigma-bar
    if (!isFALSE(bar[1])) {
      if (is.logical(bar)) {
        bar <- sapply(data, function(x) x[1, 5])
      } else if (log.z) {
        bar <- log(bar)
      }
      bars.xmax <- ifelse("xlim" %in% names(extraArgs),
                          extraArgs$xlim[2] * 0.95,
                          max(data.global$precision))
      bars.ymax <- (bar - z.central.global) * bars.xmax
      bars.x <- c(limits.x[1], limits.x[1], bars.xmax, bars.xmax)
      bars.y <- cbind(-2, 2, bars.ymax + 2, bars.ymax - 2)

      for (i in 1:length(bar)) {
        polygon.rot(x = bars.x,
                    y = bars.y[i, ],
                    col = bar.fill[i],
                    border = bar.line[i])
      }
    }

    ## remove unwanted parts
    polygon.rot(x = par()$usr[2] * c(1, 1, 2, 2),
                y = c(min(ellipse[, 2]), max(ellipse[, 2]),
                      max(ellipse[, 2]), min(ellipse[, 2])) * 2,
            col = bg.original,
            lty = 0)

    ## optionally, plot dispersion polygon
    if (polygon.fill[1] != "none") {
      for (i in 1:length(data)) {
        polygon.rot(x = polygons.x,
                    y = polygons.y[i, ],
                col = polygon.fill[i],
                border = polygon.line[i])
      }
    }

    ## optionally, add minor grid lines
    if (grid.minor != "none") {
      for (i in 1:length(tick.values.minor)) {
        lines.rot(x = c(limits.x[1], min.ellipse),
              y = c(0, tick.values.minor[i] - z.central.global) *
                min.ellipse,
              col = grid.minor,
              lwd = 1)
        lines.rot(x = c(xy.0[rotate.idx], y.max),
              y = c(tick.values.minor[i] - z.central.global,
                    tick.values.minor[i] - z.central.global) * min.ellipse,
              col = grid.minor,
              lwd = 1)
      }
    }

    ## optionally, add major grid lines
    if (grid.major != "none") {
      for (i in 1:length(tick.values.major)) {
        lines.rot(x = c(limits.x[1], min.ellipse),
              y = c(0, tick.values.major[i] - z.central.global) *
                min.ellipse,
              col = grid.major,
              lwd = 1)
        lines.rot(x = c(xy.0[rotate.idx], y.max),
              y = c(tick.values.major[i] - z.central.global,
                    tick.values.major[i] - z.central.global) * min.ellipse,
              col = grid.major,
              lwd = 1)
      }
    }

    ## optionally, plot lines for each bar
    if (lwd[1] > 0 && lty[1] > 0 && !isFALSE(bar[1])) {
      for (i in 1:length(data)) {
        z.line <- if (length(bar) == 1) bar[1] else bar[i]
        x2 <- r / sqrt(1 + f^2 * (z.line - z.central.global)^2)
        y2 <- (z.line - z.central.global) * x2
        lines.rot(x = c(limits.x[1], x2, xy.0[rotate.idx], y.max),
              y = c(0, y2, y2, y2),
              lty = lty[i],
              lwd = lwd[i],
              col = centrality.col[i])
      }
    }

  ## optionally add KDE plot
  if (kde) {
    ## calculate max KDE value for axis label
    KDE.max.plot <- 0
    for (x in data) {
      KDE.plot <- density(x[, 1],
                          kernel = "gaussian",
                          bw = bw,
                          from = limits.z[1],
                          to = limits.z[2])
      KDE.max.plot <- max(KDE.plot$y, KDE.max.plot)
    }
    KDE.scale <- (y.max - xy.0[rotate.idx]) / (KDE.max * 1.05)

    ## plot KDE lines
    for (i in 1:length(data)) {
      polygon.rot(x = xy.0[rotate.idx] + KDE[[i]][, 2] * KDE.scale,
                  y = (KDE[[i]][, 1] - z.central.global) * min.ellipse,
                  col = kde.fill[i],
                  border = kde.line[i],
                  lwd = 1.7)
    }

    ## plot KDE x-axis
    axis(side = rotate.idx,
         at = c(xy.0[rotate.idx], y.max),
         col = layout$abanico$colour$xtck3,
         col.axis = layout$abanico$colour$xtck3,
         labels = NA,
         tcl = -layout$abanico$dimension$xtcl3 / 200,
         cex = cex)

    axis(side = rotate.idx,
         at = c(xy.0[rotate.idx], y.max),
         labels = as.character(round(c(0, KDE.max.plot), 3)),
         line = 2 * layout$abanico$dimension$xtck3.line / 100 - 2,
         lwd = 0,
         col = layout$abanico$colour$xtck3,
         family = layout$abanico$font.type$xtck3,
         font = which(c("normal", "bold", "italic", "bold italic") ==
                      layout$abanico$font.deco$xtck3)[1],
         col.axis = layout$abanico$colour$xtck3,
         cex.axis = layout$abanico$font.size$xtck3 / 12)

    ## KDE x-axis label including bandwidth size
    mtext(text = paste0(xlab[3],
                        " (bw ",
                        round(x = KDE.bw,
                              digits = 3),
                        ")"),
          at = (xy.0[rotate.idx] + y.max) / 2,
          side = rotate.idx,
          line = 2.5 * layout$abanico$dimension$xlab3.line / 100,
          col = layout$abanico$colour$xlab3,
          family = layout$abanico$font.type$xlab3,
          font = which(c("normal", "bold", "italic", "bold italic") ==
                       layout$abanico$font.deco$xlab3)[1],
          cex = cex * layout$abanico$font.size$xlab3 / 12)
  }

  ## optionally add further lines
  if (length(line) > 0) {

    ## check if line parameters are RLum.Results objects
    if (is.list(line)) {
      for (i in seq_along(line)) {
        if (inherits(line[[i]], "RLum.Results")) {
          line[[i]] <- as.numeric(get_RLum(line[[i]], data.object = "summary")$de)
        }
      }
    } else if (inherits(line, "RLum.Results")) {
      line <- as.numeric(get_RLum(line, data.object = "summary")$de)
    }

    ## convert list to vector
    if (is.list(line))
      line <- unlist(line)
    if (log.z)
      line <- log(line)
    if (is.null(line.col))
      line.col <- seq_along(line) + ifelse(line.is.null, 1, 0)

    ## calculate line coordinates and further parameters
    line.x <- c(limits.x[1], min.ellipse, y.max)
    line.y <- (line - z.central.global) * min.ellipse

    for (i in 1:length(line)) {
        lines.rot(x = line.x,
                  y = c(0, line.y[i], line.y[i]),
                  col = line.col[i],
                  lty = line.lty[i] %||% 1
                  )
        text_with_bg.rot(x = line.x[3] - par()$cxy[rotate.idx] * 0.5,
                         y = line.y[i] + par()$cxy[3 - rotate.idx] * 0.5,
                         label = line.label[i] %||% "",
                         pos = 3 - rotate.idx,
                         bg = ifelse(line.is.null, line.col[i], NA),
                         cex = 0.8)
      }
    }

    ## add plot title
    add.shift <- if (!rotate) 0 else 3.5
    title(main = main,
          family = layout$abanico$font.type$main,
          font = which(c("normal", "bold", "italic", "bold italic") ==
                         layout$abanico$font.deco$main)[1],
          col.main = layout$abanico$colour$main,
          cex = layout$abanico$font.size$main / 12,
          line = (shift.lines + add.shift) * layout$abanico$dimension$main / 100)

    ## calculate lower x-axis (precision)
    x.axis.ticks <- axTicks(side = rotate.idx)
    x.axis.ticks <- x.axis.ticks[c(TRUE, x.axis.ticks <= limits.x[2])]
    x.axis.ticks <- x.axis.ticks[x.axis.ticks <= max.ellipse]

    ## x-axis with labels and ticks
    axis(side = rotate.idx,
         at = x.axis.ticks,
         col = layout$abanico$colour$xtck1,
         col.axis = layout$abanico$colour$xtck1,
         labels = NA,
         tcl = -layout$abanico$dimension$xtcl1 / 200,
         cex = cex)
    axis(side = rotate.idx,
         at = x.axis.ticks,
         line = 2 * layout$abanico$dimension$xtck1.line / 100 - 2,
         lwd = 0,
         col = layout$abanico$colour$xtck1,
         family = layout$abanico$font.type$xtck1,
         font = which(c("normal", "bold", "italic", "bold italic") ==
                        layout$abanico$font.deco$xtck1)[1],
         col.axis = layout$abanico$colour$xtck1,
         cex.axis = layout$abanico$font.size$xlab1 / 12)

    ## extend axis line to right side of the plot
    lines.rot(x = c(max(x.axis.ticks), max.ellipse),
          y = c(limits.y[1], limits.y[1]),
          col = layout$abanico$colour$xtck1)

    ## draw closing tick on right hand side
    axis(side = rotate.idx,
         tcl = -layout$abanico$dimension$xtcl1 / 200,
         lwd = 0,
         lwd.ticks = 1,
         at = limits.x[2],
         labels = FALSE,
         col = layout$abanico$colour$xtck1)

    axis(side = rotate.idx,
         tcl = layout$abanico$dimension$xtcl2 / 200,
         lwd = 0,
         lwd.ticks = 1,
         at = limits.x[2],
         labels = FALSE,
         col = layout$abanico$colour$xtck2)

    ## add lower axis label
    mtext(xlab[2],
          at = (limits.x[1] + max.ellipse) / 2,
          side = rotate.idx,
          line = 2.5 * layout$abanico$dimension$xlab1.line / 100,
          col = layout$abanico$colour$xlab1,
          family = layout$abanico$font.type$xlab1,
          font = which(c("normal", "bold", "italic", "bold italic") ==
                         layout$abanico$font.deco$xlab1)[1],
          cex = cex * layout$abanico$font.size$xlab1 / 12)

    ## add upper axis label
    mtext(xlab[1],
          at = (limits.x[1] + max.ellipse) / 2,
          side = rotate.idx,
          line = -3.5 * layout$abanico$dimension$xlab2.line / 100,
          col = layout$abanico$colour$xlab2,
          family = layout$abanico$font.type$xlab2,
          font = which(c("normal", "bold", "italic", "bold italic") ==
                         layout$abanico$font.deco$xlab2)[1],
          cex = cex * layout$abanico$font.size$xlab2 / 12)

    ## plot upper x-axis
    axis(side = rotate.idx,
         at = x.axis.ticks[-1],
         col = layout$abanico$colour$xtck2,
         col.axis = layout$abanico$colour$xtck2,
         labels = NA,
         tcl = layout$abanico$dimension$xtcl2 / 200,
         cex = cex)

    ## remove first tick label (infinity)
    label.x.upper <- label.x.upper[1:(length(x.axis.ticks) - 1)]

  if (length(x.axis.ticks) > 1) {
    axis(side = rotate.idx,
         at = x.axis.ticks[-1],
         labels = label.x.upper,
         line = -1 * layout$abanico$dimension$xtck2.line / 100 - 2,
         lwd = 0,
         col = layout$abanico$colour$xtck2,
         family = layout$abanico$font.type$xtck2,
         font = which(c("normal", "bold", "italic", "bold italic") ==
                        layout$abanico$font.deco$xtck2)[1],
         col.axis = layout$abanico$colour$xtck2,
         cex.axis = layout$abanico$font.size$xlab2 / 12)
  }

  ## plot y-axis
  if (is.null(extraArgs$yaxt) || extraArgs$yaxt != "n") {
    line <- 2 * layout$abanico$dimension$ytck.line / 100 - 2
    family <- layout$abanico$font.type$ytck
    font <- which(c("normal", "bold", "italic", "bold italic") ==
                  layout$abanico$font.deco$ytck)[1]
    col.axis <- layout$abanico$colour$ytck
    cex.axis <- layout$abanico$font.size$ylab / 12
    if (y.axis) {
      char.height <- par()$cxy[2]

      ## this comes into play for panel plots, e.g., par(mfrow = c(4,4))
      if (char.height > 4.5 / cex) {
        axis(side = 3 - rotate.idx,
             at = c(-2, 2),
             tcl = -layout$abanico$dimension$ytcl / 200,
             lwd = 1,
             lwd.ticks = 1,
             labels = NA,
             las = 1,
             col = col.axis)
        axis(side = 3 - rotate.idx,
             at = 0,
             tcl = 0,
             labels = "\u00B1 2",
             line = line, las = 1,
             family = family, font = font,
             col.axis = col.axis, cex.axis = cex.axis)
      } else {
        axis(side = 3 - rotate.idx,
             at = seq(-2, 2, by = 2),
             line = line, las = 1,
             tcl = -layout$abanico$dimension$ytcl / 200,
             family = family, font = font,
             col.axis = col.axis, cex.axis = cex.axis)
      }
    } else {
      axis(side = 3 - rotate.idx,
           at = 0,
           line = line, las = 1,
           tcl = -layout$abanico$dimension$ytcl / 200,
           family = family, font = font,
           col.axis = col.axis, cex.axis = cex.axis)
    }
  }

  ## plot minor z-ticks
  for (i in 1:length(tick.values.minor)) {
    lines.rot(x = y.max * c(1, 1 + 0.007 * layout$abanico$dimension$ztcl / 100),
            y = c(tick.values.minor[i] - z.central.global,
                  tick.values.minor[i] - z.central.global) *
              min(ellipse[, rotate.idx]),
            col = layout$abanico$colour$ztck)
  }

  ## plot major z-ticks
  for (i in 1:length(tick.values.major)) {
    lines.rot(x = y.max * c(1, 1 + 0.015 * layout$abanico$dimension$ztcl / 100),
              y = c(tick.values.major[i] - z.central.global,
                    tick.values.major[i] - z.central.global) *
                min(ellipse[, rotate.idx]),
              col = layout$abanico$colour$ztck)
  }

  ## plot z-axes
  lines(ellipse, col = layout$abanico$colour$border)
  lines.rot(x = rep(y.max, nrow(ellipse)),
            y = ellipse[, 2],
            col = layout$abanico$colour$ztck)

  ## plot z-axis text
  text.rot(x = y.max * (1 + 0.02 * layout$abanico$dimension$ztcl / 100),
           y = (tick.values.major - z.central.global) * min(ellipse[, rotate.idx]),
           labels = label.z.text,
           adj = if (rotate) c(0.5, 0) else c(0, 0.5),
           family = layout$abanico$font.type$ztck,
           font = which(c("normal", "bold", "italic", "bold italic") ==
                        layout$abanico$font.deco$ztck)[1],
           cex = layout$abanico$font.size$ztck / 12)

  ## plot z-label
  mtext(text = zlab,
        at = 0,
        side = 5 - rotate.idx,
        las = ifelse(rotate, 1, 3),
        adj = 0.5,
        line = (ifelse(rotate, 1.5, 4) + cex) * layout$abanico$dimension$zlab.line / 100,
        col = layout$abanico$colour$zlab,
        family = layout$abanico$font.type$zlab,
        font = which(c("normal", "bold", "italic", "bold italic") ==
                     layout$abanico$font.deco$zlab)[1],
        cex = cex * layout$abanico$font.size$zlab / 12)

  ## plot values and optionally error bars
  if (error.bars) {
    for (i in 1:length(data)) {
      arrow.x <- data[[i]][, 6]
      arrow.y1 <- data[[i]][, 1] - data[[i]][, 2]
      arrow.y2 <- data[[i]][, 1] + data[[i]][, 2]
      if (log.z) {
        arrow.y1 <- log(arrow.y1)
        arrow.y2 <- log(arrow.y2)
      }

      arrow.coords <- cbind(
          arrow.x,
          arrow.x,
          (arrow.y1 - z.central.global) * arrow.x,
          (arrow.y2 - z.central.global) * arrow.x)

      graphics::arrows(
               x0 = arrow.coords[, 2 * rotate.idx - 1],
               x1 = arrow.coords[, 2 * rotate.idx],
               y0 = arrow.coords[, 2 * (3 - rotate.idx) - 1],
               y1 = arrow.coords[, 2 * (3 - rotate.idx)],
               length = 0,
               angle = 90,
               code = 3,
               col = value.bar[i])
    }
  }

  for (i in 1:length(data)) {
    points.rot(x = data[[i]][, 6][data[[i]][, 6] <= limits.x[2]],
               y = data[[i]][, 8][data[[i]][, 6] <= limits.x[2]],
             col = value.dot[i],
             pch = pch[i],
             cex = layout$abanico$dimension$pch / 100)
  }

  ## compute data for histogram and dot plot
  if (hist || dots) {
    ## calculate histogram data without plotting
    hist.data <- lapply(data, function(x) {
      hist(x[, 3], plot = FALSE, breaks = breaks)
    })

    ## calculate maximum histogram bar height for normalisation
    hist.max <- max(vapply(hist.data, function(x) max(x$counts, na.rm = TRUE),
                           numeric(1)), na.rm = TRUE)

    ## calculate scaling factor for histogram bar heights
    hist.scale <- (y.max - xy.0[rotate.idx]) / (hist.max * 1.05)

    ## normalise histogram bar height to KDE dimensions
    for (i in 1:length(data)) {
      hist.data[[i]]$density <- hist.data[[i]]$counts * hist.scale
      hist.data[[i]]$breaks <- hist.data[[i]]$breaks - z.central.global
    }
  }

  ## optionally add histogram
  if (hist) {
      axis(side = rotate.idx,
           at = c(xy.0[rotate.idx], y.max),
           labels = as.character(c(0, hist.max)),
           line = -1 * layout$abanico$dimension$xtck3.line / 100 - 2,
           lwd = 0,
           col = layout$abanico$colour$xtck3,
           family = layout$abanico$font.type$xtck3,
           font = which(c("normal", "bold", "italic", "bold italic") ==
                          layout$abanico$font.deco$xtck3)[1],
           col.axis = layout$abanico$colour$xtck3,
           cex.axis = layout$abanico$font.size$xtck3 / 12)

      ## add label
      mtext(text = "n",
            at = (xy.0[rotate.idx] + y.max) / 2,
            side = rotate.idx,
            line = -3.5 * layout$abanico$dimension$xlab2.line / 100,
            col = layout$abanico$colour$xlab2,
            family = layout$abanico$font.type$xlab2,
            font = which(c("normal", "bold", "italic", "bold italic") ==
                           layout$abanico$font.deco$xlab2)[1],
            cex = cex * layout$abanico$font.size$xlab2 / 12)

      ## plot ticks
      axis(side = rotate.idx,
           at = c(xy.0[rotate.idx], y.max),
           col = layout$abanico$colour$xtck2,
           col.axis = layout$abanico$colour$xtck2,
           labels = NA,
           tcl = layout$abanico$dimension$xtcl2 / 200,
           cex = cex)

    ## draw each bar for each data set
    for (i in 1:length(data)) {
      for (j in 1:length(hist.data[[i]]$density)) {
          ## calculate x-coordinates
          hist.x.i <- xy.0[rotate.idx] + c(0, 0, rep(hist.data[[i]]$density[j], 2))

          ## calculate y-coordinates
          hist.y.i <- c(hist.data[[i]]$breaks[j],
                        hist.data[[i]]$breaks[j + 1],
                        hist.data[[i]]$breaks[j + 1],
                        hist.data[[i]]$breaks[j]) * min.ellipse

          ## remove data out of z-axis range
          hist.y.i <- pmax(hist.y.i, min(ellipse[, 3 - rotate.idx]))
          hist.y.i <- pmin(hist.y.i, max(ellipse[, 3 - rotate.idx]))

          ## draw the bars
          polygon.rot(x = hist.x.i,
                      y = hist.y.i,
                      col = kde.fill[i],
                      border = kde.line[i])
        }
    }
  }

  ## optionally add box plot
  if (boxplot) {

    box.x <- c(min.ellipse + KDE.max * 0.85, xy.0[rotate.idx] + KDE.max * 0.95)
    for (i in 1:length(data)) {
      ## calculate boxplot data without plotting
      boxplot.data <- graphics::boxplot(data[[i]][, 3], plot = FALSE)
      stats <- (boxplot.data$stats[, 1] - z.central.global) * min.ellipse

      ## draw median line
      lines.rot(x = box.x,
                y = c(stats[3], stats[3]),
                lwd = 2,
                col = kde.line[i])

      ## draw p25-p75-polygon
      polygon.rot(x = rep(box.x, each = 2),
                  y = c(stats[2], stats[4], stats[4], stats[2]),
                  border = kde.line[i])

      ## draw lower whisker
      lines.rot(x = c(rep(mean(box.x), 2), box.x),
                y = c(stats[2], stats[1], stats[1], stats[1]),
                col = kde.line[i])

      ## draw upper whisker
      lines.rot(x = c(rep(mean(box.x), 2), box.x),
                y = c(stats[4], stats[5], stats[5], stats[5]),
                col = kde.line[i])

      ## draw outlier points
      points.rot(x = rep(mean(box.x), length(boxplot.data$out)),
                 y = (boxplot.data$out - z.central.global) * min.ellipse,
                 cex = 0.8,
                 col = kde.line[i])
    }
  }

  ## optionally add dot plot
  if (dots) {
    ## calculate distance between dots
    dots.distance <- (y.max - (xy.0[rotate.idx] + par()$cxy[rotate.idx] * 0.4)) / hist.max

    for (i in 1:length(data)) {
      for (j in 1:length(hist.data[[i]]$counts)) {
        dots.x.i <- seq(from = xy.0[rotate.idx] + par()$cxy[rotate.idx] * 0.4,
                        by = dots.distance,
                        length.out = hist.data[[i]]$counts[j])

        dots.y.i <- rep((hist.data[[i]]$mids[j] - z.central.global) *
                        min.ellipse, length(dots.x.i))

        ## remove data out of z-axis range
        keep.idx <- between(dots.y.i,
                            min(ellipse[, 3 - rotate.idx]),
                            max(ellipse[, 3 - rotate.idx]))
        dots.x.i <- dots.x.i[keep.idx]
        dots.y.i <- dots.y.i[keep.idx]

        max.val <- y.max - par()$cxy[rotate.idx] * 0.4
        if (max(c(0, dots.x.i), na.rm = TRUE) >= max.val) {
          dots.y.i <- dots.y.i[dots.x.i < max.val]
          dots.x.i <- dots.x.i[dots.x.i < max.val]
        }

        ## plot points
        points.rot(x = dots.x.i,
                   y = dots.y.i,
                   pch = if (!rotate) "|" else "-",
                   cex = 0.7,
                   col = kde.line[i])
      }
    }
  }

  ## optionally add stats, i.e. min, max, median sample text
  if (length(stats) > 0) {
    text.rot(x = stats.data[, 1],
             y = stats.data[, 2],
             pos = 2,
             labels = round(stats.data[, 3], 1),
             family = layout$abanico$font.type$stats,
             font = which(c("normal", "bold", "italic", "bold italic") ==
                          layout$abanico$font.deco$stats)[1],
             cex = layout$abanico$font.size$stats / 12,
             col = layout$abanico$colour$stats)
  }

  ## optionally add rug
  if (rug) {
    rug.x <- c(1 - 0.013 * (layout$abanico$dimension$rugl / 100), 1) * xy.0[rotate.idx]
    rug.y <- ((if (log.z) log(De.global) else De.global) - z.central.global) * min.ellipse
    for (i in 1:length(rug.y)) {
      lines.rot(x = rug.x,
                y = rep(rug.y[i], 2),
                col = value.rug[data.global[i, 10]])
    }
  }

  ## plot KDE base line
  lines.rot(x = c(xy.0[rotate.idx], xy.0[rotate.idx]),
            y = c(min(ellipse[, 3 - rotate.idx]), max(ellipse[, 3 - rotate.idx])),
            col = layout$abanico$colour$border)

  ## draw border around plot
  if (frame == 1) {
    polygon.rot(x = c(limits.x[1], min.ellipse, y.max,
                      y.max, min.ellipse),
                y = c(0, max(ellipse[, 3 - rotate.idx]), max(ellipse[, 3 - rotate.idx]),
                      min(ellipse[, 3 - rotate.idx]), min(ellipse[, 3 - rotate.idx])),
                border = layout$abanico$colour$border,
                lwd = 0.8)
  } else if (frame == 2) {
    polygon.rot(x = c(limits.x[1], min.ellipse, y.max,
                      y.max, min.ellipse, limits.x[1]),
                y = c(2, max(ellipse[, 3 - rotate.idx]), max(ellipse[, 3 - rotate.idx]),
                      min(ellipse[, 3 - rotate.idx]), min(ellipse[, 3 - rotate.idx]), -2),
                border = layout$abanico$colour$border,
                lwd = 0.8)
  } else if (frame == 3) {
    polygon.rot(x = c(limits.x[1], y.max, y.max, limits.x[1]),
                y = c(max(ellipse[, 3 - rotate.idx]), max(ellipse[, 3 - rotate.idx]),
                      min(ellipse[, 3 - rotate.idx]), min(ellipse[, 3 - rotate.idx])),
                border = layout$abanico$colour$border,
                lwd = 0.8)
  }

  ## optionally add legend content
  if (!is.null(legend)) {
    ## store and change font familiy
    par.family <- par()$family
    par(family = layout$abanico$font.type$legend)

    scale.rot <- if (!rotate) c(1, 0.8) else c(0.8, 1)
    if (rotate)
      legend.adj <- rev(legend.adj)
    legend(x = legend.pos[1] * scale.rot[1],
           y = legend.pos[2] * scale.rot[2],
           xjust = legend.adj[1],
           yjust = legend.adj[2],
           legend = legend,
           pch = pch,
           col = value.dot,
           text.col = value.dot,
           text.font = which(c("normal", "bold", "italic", "bold italic") ==
                             layout$abanico$font.deco$legend)[1],
           cex = layout$abanico$font.size$legend / 12,
           bty = "n")

    ## restore font family
    par(family = par.family)
  }

  ## optionally add subheader text
  add.shift <- if (!rotate) 0 else 3.5
  mtext(text = mtext,
        side = 3,
        line = (shift.lines - 2 + add.shift) * layout$abanico$dimension$mtext / 100,
        col = layout$abanico$colour$mtext,
        family = layout$abanico$font.type$mtext,
        font = which(c("normal", "bold", "italic", "bold italic") ==
                     layout$abanico$font.deco$mtext)[1],
        cex = cex * layout$abanico$font.size$mtext / 12)

  ## add summary content
  for (i in 1:length(data)) {
    if (summary.pos[1] != "sub") {
        text(x = summary.pos[1],
             y = summary.pos[2],
             adj = summary.adj,
             labels = label.text[[i]],
             col = summary.col[i],
             family = layout$abanico$font.type$summary,
             font = which(c("normal", "bold", "italic", "bold italic") ==
                            layout$abanico$font.deco$summary)[1],
             cex = layout$abanico$font.size$summary / 12)
    } else if (mtext == "") {
          mtext(side = 3,
                line = (shift.lines - 1 + add.shift - i) *
                  layout$abanico$dimension$summary / 100 ,
                text = label.text[[i]],
                col = summary.col[i],
                family = layout$abanico$font.type$summary,
                font = which(c("normal", "bold", "italic", "bold italic") ==
                               layout$abanico$font.deco$summary)[1],
                cex = cex * layout$abanico$font.size$summary / 12)
    }
  }

  ##sTeve
  if (fun && !interactive) sTeve() # nocov

  ## create numeric output
  plot.output <- list(xlim = limits.x,
                      ylim = limits.y,
                      zlim = limits.z,
                      polar.box = c(limits.x[1],
                                    limits.x[2],
                                    min(ellipse[,2]),
                                    max(ellipse[,2])),
                      cartesian.box = c(xy.0[1],
                                        par()$usr[2],
                                        xy.0[2],
                                        max(ellipse[,2])),
                      plot.ratio = plot.ratio,
                      data = data,
                      data.global = data.global,
                      KDE = KDE,
                      par = par(no.readonly = TRUE))

  ## INTERACTIVE PLOT ----------------------------------------------------------
  if (interactive) {
    .require_suggested_package("plotly", "The interactive abanico plot")

    ## tidy data ----
    data <- plot.output
    kde <- data.frame(x = data$KDE[[1]][ ,2], y = data$KDE[[1]][ ,1])

    # radial scatter plot ----
    point.text <- paste0("Measured value:<br />",
                         data$data.global$De, " &plusmn; ",
                         data$data.global$error, "<br />",
                         "P(",format(data$data.global$precision,  digits = 2, nsmall = 1),", ",
                         format(data$data.global$std.estimate,  digits = 2, nsmall = 1),")")
    IAP <- plotly::plot_ly(data = data$data.global,
                           x = data$data.global$precision,
                           y = data$data.global$std.estimate,
                           type = "scatter", mode = "markers",
                           hoverinfo = "text", text = point.text,
                           name = "Points",
                           yaxis = "y")

    ellipse <- as.data.frame(ellipse)
    IAP <- plotly::add_trace(IAP, data = ellipse,
                             x = ~ellipse.x, y = ~ellipse.y,
                             type = "scatter", mode = "lines",
                             hoverinfo = "none", text = "",
                             name = "z-axis (left)",
                             line = list(color = "black",
                                         width = 1),
                             yaxis = "y")

    ellipse.right <- ellipse
    ellipse.right$ellipse.x <- ellipse.right$ellipse.x * 1/0.75

    IAP <- plotly::add_trace(IAP, data = ellipse.right,
                             x = ~ellipse.x, y = ~ellipse.y,
                             type = "scatter", mode = "lines",
                             hoverinfo = "none", text = "",
                             name = "z-axis (right)",
                             line = list(color = "black",
                                         width = 1),
                             yaxis = "y")

    # z-axis ticks
    major.ticks.x <- c(data$xlim[2] * 1/0.75,
                       (1 + 0.015 * layout$abanico$dimension$ztcl / 100) *
                         data$xlim[2] * 1/0.75)
    minor.ticks.x <- c(data$xlim[2] * 1/0.75,
                       (1 + 0.01 * layout$abanico$dimension$ztcl / 100) *
                         data$xlim[2] * 1/0.75)
    major.ticks.y <- (tick.values.major - z.central.global) *  min(ellipse[ ,1])
    minor.ticks.y <- (tick.values.minor - z.central.global) *  min(ellipse[ ,1])

    # major z-tick lines
    for (i in 1:length(major.ticks.y)) {
      major.tick <- data.frame(x = major.ticks.x, y = rep(major.ticks.y[i], 2))
      IAP <- plotly::add_trace(IAP, data = major.tick,
                               x = ~x, y = ~y, showlegend = FALSE,
                               type = "scatter", mode = "lines",
                               hoverinfo = "none", text = "",
                               line = list(color = "black",
                                           width = 1),
                               yaxis = "y")
    }

    # minor z-tick lines
    for (i in 1:length(minor.ticks.y)) {
      minor.tick <- data.frame(x = minor.ticks.x, y = rep(minor.ticks.y[i], 2))
      IAP <- plotly::add_trace(IAP, data = minor.tick,
                               x = ~x, y = ~y, showlegend = FALSE,
                               type = "scatter", mode = "lines",
                               hoverinfo = "none", text = "",
                               line = list(color = "black",
                                           width = 1),
                               yaxis = "y")
    }

    # z-tick label
    tick.text <- paste(" ", exp(tick.values.major))
    tick.pos <- data.frame(x = major.ticks.x[2],
                           y = major.ticks.y)

    IAP <- plotly::add_trace(IAP, data = tick.pos,
                             x = ~x, y = ~y, showlegend = FALSE,
                             hoverinfo = "none",
                             text = tick.text, textposition = "right",
                             type = "scatter", mode = "text",
                             yaxis = "y")

    # Central Line ----
    central.line <- data.frame(x = c(-100, data$xlim[2]*1/0.75), y = c(0, 0))
    central.line.text <- paste0("Central value: ",
                                format(exp(z.central.global), digits = 2, nsmall = 1))

    IAP <- plotly::add_trace(IAP, data = central.line,
                             x = ~x, y = ~y, name = "Central line",
                             type = "scatter", mode = "lines",
                             hoverinfo = "text", text = central.line.text,
                             yaxis = "y",
                             line = list(color = "black",
                                         width = 0.5,
                                         dash = 2))

    # KDE plot ----
    KDE.x <- xy.0[1] + KDE[[1]][ ,2] * KDE.scale
    KDE.y <- (KDE[[1]][ ,1] - z.central.global) * min(ellipse[,1])
    KDE.curve <- data.frame(x = KDE.x, y = KDE.y)
    KDE.curve <- KDE.curve[KDE.curve$x != xy.0[1], ]
    KDE.text <- paste0("Value:",
                       format(exp(KDE.curve$x), digits = 2, nsmall = 1), "<br />",
                       "Density:",
                       format(KDE.curve$y, digits = 2, nsmall = 1))

    IAP <- plotly::add_trace(IAP, data = KDE.curve,
                             x = ~x, y = ~y, name = "KDE",
                             type = "scatter", mode = "lines",
                             hoverinfo = "text",
                             text = KDE.text,
                             line = list(color = "red"),
                             yaxis = "y")

    # set layout ----
    IAP <- plotly::layout(IAP,
                          hovermode = "closest",
                          dragmode = "pan",
                          xaxis = list(range = c(data$xlim[1], data$xlim[2] * 1/0.65),
                                       zeroline = FALSE,
                                       showgrid = FALSE,
                                       tickmode = "array",
                                       tickvals = x.axis.ticks),
                          yaxis = list(range = data$ylim,
                                       zeroline = FALSE,
                                       showline = FALSE,
                                       showgrid = FALSE,
                                       tickmode = "array",
                                       tickvals = c(-2, 0, 2)),
                          shapes = list(list(type = "rect", # 2 sigma bar
                                             x0 = 0, y0 = -2,
                                             x1 = bars.x[3], y1 = 2,
                                             xref = "x", yref = "y",
                                             fillcolor = "grey",
                                             opacity = 0.2))
    )

    # show and return interactive plot ----
    #print(plotly::subplot(IAP, IAP.kde))
    print(IAP)
    return(IAP)
  }

  ## create and return numeric output
  invisible(plot.output)
}
