R/marks.R

Defines functions check_mark mark_ mark_function mark_svg mark_frame mark_image mark_tickY mark_tickX mark_textY mark_textX mark_text mark_ruleY mark_ruleX mark_rectY mark_rectX mark_rect mark_link mark_lineY mark_lineX mark_line mark_dotY mark_dotX mark_dot mark_cellY mark_cellX mark_cell mark_barY mark_barX mark_areaY mark_areaX mark_area

Documented in mark_area mark_areaX mark_areaY mark_barX mark_barY mark_cell mark_cellX mark_cellY mark_dot mark_dotX mark_dotY mark_frame mark_function mark_image mark_line mark_lineX mark_lineY mark_link mark_rect mark_rectX mark_rectY mark_ruleX mark_ruleY mark_svg mark_text mark_textX mark_textY mark_tickX mark_tickY

#' Add a mark to an obsplot
#'
#' @param g an obsplot object
#' @param ... named options passed to define the mark
#'
#' ... arguments can accept at most two unnamed arguments : a data object and a call
#' to a transform function.
#' @export
mark_area <- function(g, ...) {
    channels <- c("x1", "y1", "x2", "y2", "z")
    req_channels <- c("x1", "y1")
    mark_("area", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_areaX <- function(g, ...) {
    channels <- c("x", "y", "z")
    mark_("areaX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_areaY <- function(g, ...) {
    channels <- c("x", "y", "z")
    mark_("areaY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_barX <- function(g, ...) {
    channels <- c("x1", "x2", "x", "y")
    mark_("barX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_barY <- function(g, ...) {
    channels <- c("y1", "y2", "x", "y")
    mark_("barY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_cell <- function(g, ...) {
    channels <- c("x", "y")
    mark_("cell", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_cellX <- function(g, ...) {
    channels <- c("x", "y")
    mark_("cellX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_cellY <- function(g, ...) {
    channels <- c("x", "y")
    mark_("cellY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_dot <- function(g, ...) {
    channels <- c("x", "y", "r")
    mark_("dot", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_dotX <- function(g, ...) {
    channels <- c("x", "y", "r")
    mark_("dotX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_dotY <- function(g, ...) {
    channels <- c("x", "y", "r")
    mark_("dotY", g, channels, req_channels = NULL, ...)
}


#' @rdname mark_area
#' @export
mark_line <- function(g, ...) {
    channels <- c("x", "y", "z")
    req_channels <- c("x", "y")
    mark_("line", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_lineX <- function(g, ...) {
    channels <- c("x", "y", "z")
    mark_("lineX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_lineY <- function(g, ...) {
    channels <- c("x", "y", "z")
    mark_("lineY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_link <- function(g, ...) {
    channels <- c("x1", "y1", "x2", "y2")
    req_channels <- c("x1", "y1", "x2", "y2")
    mark_("link", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_rect <- function(g, ...) {
    channels <- c("x1", "y1", "x2", "y2")
    mark_("rect", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_rectX <- function(g, ...) {
    channels <- c("x1", "y1", "x2", "y2", "x", "y")
    mark_("rectX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_rectY <- function(g, ...) {
    channels <- c("x1", "y1", "x2", "y2", "y", "x")
    mark_("rectY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_ruleX <- function(g, ...) {
    channels <- c("x", "y1", "y2", "y")
    mark_("ruleX", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_ruleY <- function(g, ...) {
    channels <- c("y", "x1", "x2", "x")
    mark_("ruleY", g, channels, req_channels = NULL, ...)
}

#' @rdname mark_area
#' @export
mark_text <- function(g, ...) {
    channels <- c("x", "y", "text", "fontSize", "rotate")
    req_channels <- "text"
    mark_("text", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_textX <- function(g, ...) {
    channels <- c("x", "y", "text", "fontSize", "rotate")
    req_channels <- "text"
    mark_("textX", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_textY <- function(g, ...) {
    channels <- c("x", "y", "text", "fontSize", "rotate")
    req_channels <- "text"
    mark_("textY", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_tickX <- function(g, ...) {
    channels <- c("x", "y")
    req_channels <- "x"
    mark_("tickX", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_tickY <- function(g, ...) {
    channels <- c("x", "y")
    req_channels <- "y"
    mark_("tickY", g, channels, req_channels, ...)
}

#' @rdname mark_area
#' @export
mark_image <- function(g, ...) {
    channels <- c("x", "y", "width", "height")
    req_channels <- "y"
    mark_("image", g, channels, req_channels, ...)
}


#' Add a decoration mark to an obsplot
#'
#' @param g an obsplot object
#' @param fill fill CSS color
#' @param ... options passed to define the mark
#' @export
mark_frame <- function(g, fill = "none", ...) {
    mark_("frame", g, mark_channels = NULL, req_channels = NULL, fill = fill, ...)
}

#' Add an SVG element to an obsplot
#'
#' @param g an obsplot object
#' @param svg SVG to add, as a character string
#' @param ... options passed to define the mark
#' @export

mark_svg <- function(g, svg, ...) {
    f_code <- paste0("() => svg`", svg, "`")
    f <- JS(f_code) # nolint
    mark_function(g, f)
}

#' Add a render function mark to an obsplot
#'
#' @param g an obsplot object
#' @param f render function, defined with `htmlwidgets::JS()`
#' @param ... options passed to define the mark
#' @export

mark_function <- function(g, f, ...) {
    mark_("function", g, mark_channels = NULL, req_channels = NULL, fun = f, ...)
}


# Add a mark to a chart object
mark_ <- function(mark_type, g, mark_channels, req_channels, ...) {

    # Keep opts as quosures
    opts <- rlang::enquos(...)
    unnamed_opts <- list()

    # Extract unnamed opts, eval the quosures and remove them from opts
    if (is.null(names(opts))) {
        unnamed_opts <- opts |> purrr::map(rlang::eval_tidy)
        opts <- list()
    } else {
        unnamed_opts <- opts[names(opts) == ""] |> purrr::map(rlang::eval_tidy)
        opts <- opts[names(opts) != ""]
    }

    # Get data
    data <- rlang::eval_tidy(opts$data) %||%
        purrr::detect(unnamed_opts, \(v) !is_transform(v)) # nolint
    opts$data <- NULL
    check_data <- data %||%  g$x$data$data # nolint

    # Get transform
    transform <- rlang::eval_tidy(opts$transform) %||%
        purrr::detect(unnamed_opts, is_transform) # nolint
    opts$transform <- NULL

    # If an opt is a symbol which is a data column name, convert it to a string
    # Otherwise, eval the quosure
    opts <- quosures2opts_data(check_data, opts)

    # Get vector channels and remove them from opts
    vector_channels <- get_vector_channels(opts, mark_channels) # nolint
    opts <- opts[!(names(opts) %in% names(vector_channels))]

    # Get column channels and remove them from opts
    column_channels <- get_column_channels(data, opts, mark_channels) # nolint
    opts <- opts[!(names(opts) %in% names(column_channels))]


    # Check channels values
    check_mark(
        data = check_data,
        mark_channels = mark_channels,
        req_channels = req_channels,
        vector_channels = vector_channels,
        column_channels = column_channels,
        mark_unnamed_opts = unnamed_opts,
        mark_opts = opts,
        mark_has_data = !is.null(data),
        mark_has_transform = !is.null(transform)
    )

    # Automatically add indexed data argument if needed
    if (length(vector_channels) >= 1 && is.null(data) && is.null(column_channels)) {
        lengths <- purrr::map_int(vector_channels, length)
        data <- seq_len(max(lengths, na.rm = TRUE))
    }

    # rep() and add metadata to vector channels
    vector_channels <- purrr::map(
        vector_channels,
        \(v) {
            # rep() if needed
            if (length(v) == 1) {
                rep_length <- nrow(data) %||% length(data) # nolint
                v <- rep(v, rep_length)
            }
            # Add metadata
            add_metadata(v) # nolint
        }
    )

    # Add metadata to data object
    data <- add_metadata(data) # nolint

    # Add mark to chart
    mark <- list(
        type = mark_type,
        data = data,
        vector_channels = vector_channels,
        column_channels = column_channels,
        transform = transform,
        opts = opts
    )

    g$x$marks <- append(g$x$marks, list(mark))

    g
}


# Check mark arguments and channels
check_mark <- function(
    data, mark_channels, req_channels,
    vector_channels, column_channels,
    mark_unnamed_opts, mark_opts,
    mark_has_data, mark_has_transform
) {

    # No more than two unnamed opts
    if (length(mark_unnamed_opts) > 2) {
        stop("a mark cannot accept more than two unnamed arguments")
    }

    # Check required channels if there is no transform
    if (!mark_has_transform) {
        missing_channels <- setdiff(
            req_channels,
            c(names(vector_channels), names(column_channels), names(mark_opts))
        )
        if (length(missing_channels) >= 1) {
            stop("missing channels ", paste(missing_channels, collapse = ", "))
        }
    }

    # Check color channels
    color_chan_names <- intersect(
        get_character_channels(mark_opts, mark_channels),
        c("fill", "stroke")
    )
    for (name in color_chan_names) {
        chan <- mark_opts[[name]]
        if (!(is_css_color(chan) || chan %in% names(data))) {
            stop(name, " must be a CSS color or a column of data")
        }
    }

    # Check column channels
    for (chan in column_channels) {
        if (!(chan %in% names(data))) stop(chan, " is not a column of data")
    }

    # Check vector channels
    if (length(vector_channels) >= 1) {
        lengths <- purrr::map_int(vector_channels, length)
        lengths <- unique(lengths[lengths > 1])
        if (length(lengths) > 1) stop(" all vector channels must be of the same length or of length 1")
    }
}
juba/obsplot documentation built on April 27, 2023, 5:28 a.m.