R/Pipeline.R

Defines functions pipeline_text_summary escape_pipes_and_brackets pipeline_nomnoml_code apply_annotations validate_annotation pipeline_network sort_topologically propagate_outofdateness warn_pipeline_is_empty save_pipeline show_pipeline reset_pipeline get_pipeline set_pipeline is_pipeline

Documented in get_pipeline is_pipeline reset_pipeline save_pipeline set_pipeline show_pipeline

# R6 Object --------------------------------------------------------------------

#' Pipeline visualisations
#'
#' @description A Pipeline object is automatically constructed as calls to
#'   `make_*()` are made. It stores the relationships between targets,
#'   dependencies, and sources.
#'
#' @param recipe A language object which, when evaluated, makes the `targets`
#' @param source The path to an R script which makes the `targets`
#' @param targets A character vector of paths to files
#' @param dependencies A character vector of paths to files which the
#'   `targets` depend on
#' @param packages A character vector of names of packages which `targets`
#'   depend on
#' @param envir The environment in which to execute the `source` or `recipe`.
#' @param force A logical determining whether or not execution of the `source`
#'   or `recipe` will be forced (i.e. happen whether or not the targets are
#'   out-of-date)
#'
#' @keywords internal
#' @family pipeline
#' @export Pipeline
#' @aliases Pipeline
#' @importFrom R6 R6Class
Pipeline <- R6::R6Class(classname = "Pipeline",
  private = list(
    edges = data.frame(
      from = factor(),
      to = factor(),
      arrows = character(0),
      .segment_id = integer(0),
      .source = logical(0),
      .recipe = logical(0),
      .pkg = logical(0),
      .outdated = logical(0),
      stringsAsFactors = FALSE
    ),
    nodes = data.frame(
      id = factor(),
      label = character(0),
      note = character(0),
      .source = logical(0),
      .recipe = logical(0),
      .pkg = logical(0),
      stringsAsFactors = FALSE
    ),

    #' @description Add an edge to `edges`
    #' @param new_edge An data.frame constructed with `new_edge()`
    add_edge = function(new_edge) {
      edges <- private$edges

      # Relevel
      all_ids <- unlist(list(edges$from, edges$to, new_edge$from, new_edge$to))
      lvls <- levels(all_ids)
      edges$from <- factor(edges$from, lvls)
      edges$to <- factor(edges$to, lvls)
      new_edge$from <- factor(new_edge$from, lvls)
      new_edge$to <- factor(new_edge$to, lvls)

      # Combine
      private$edges <- rbind(edges, new_edge)

      invisible(self)
    },

    #' @description Add any nodes in `private$edges` that are missing from
    #'   `private$nodes` into `private$nodes`
    refresh_nodes = function() {
      edges <- private$edges
      old_nodes <- private$nodes

      nodes <- lapply(self$segments, function(x) x$nodes)
      if (length(nodes) == 0) {
        return(invisible(self))
      } else if (length(nodes) == 1) {
        nodes <- nodes[[1]]
      } else {
        nodes <- unique(do.call(rbind, nodes))
      }

      # Restore old notes/labels
      nodes$label <- ""
      nodes$note  <- ""
      if (nrow(old_nodes) > 0) {
        labels <- old_nodes$label
        names(labels) <- as.character(old_nodes$id)
        nodes <- apply_annotations(nodes, labels, "label")

        notes <- old_nodes$note
        names(notes) <- as.character(old_nodes$id)
        nodes <- apply_annotations(nodes, notes, "note")
      }

      private$nodes <- nodes
      invisible(self)
    },

    #' @description Reconstruct Pipeline edges from Segment edges. Called
    #'   primarily to update outofdateness
    refresh_edges = function() {
      edges <- lapply(self$segments, function(x) x$edges)
      if (length(edges)==0) {
        return(invisible(self))
      } else if (length(edges) == 1) {
        edges <- edges[[1]]
      } else {
        edges <- do.call(rbind, edges)
      }
      edges <- propagate_outofdateness(edges)

      private$edges <- edges
      invisible(self)
    }
  ),
  public = list(
    #' @field segments A list of `Segment` objects
    segments = NULL,

    #' @description Add a pipeline segment corresponding to a `make_with_source()`
    #'   call
    #' @return The `SegmentSource` added to the `Pipeline`
    add_source_segment = function(source, targets, dependencies, packages, envir, force) {
      id <- as.integer(length(self$segments) + 1)
      new_segment <- SegmentSource$new(
        id, source, targets, dependencies, packages, envir, force,
        FALSE, NULL, NULL
      )

      self$segments <- c(self$segments, new_segment)
      private$add_edge(new_segment$edges)
      private$refresh_nodes()

      new_segment
    },

    #' @description Add a pipeline segment corresponding to a `make_with_recipe()`
    #'   call
    #' @return The `SegmentRecipe` added to the `Pipeline`
    add_recipe_segment = function(recipe, targets, dependencies, packages, envir, force) {
      id <- length(self$segments) + 1
      new_segment <- SegmentRecipe$new(
        id, recipe, targets, dependencies, packages, envir, force,
        FALSE, NULL, NULL
      )

      self$segments <- c(self$segments, new_segment)
      private$add_edge(new_segment$edges)
      private$refresh_nodes()

      new_segment
    },

    #' @description Build all targets
    #' @param quiet A logical determining whether or not messages are signaled
    #' @return `self`
    build = function(quiet = getOption("makepipe.quiet")) {
      if (warn_pipeline_is_empty(self$segments, "Nothing to build")) {
        return(invisible(self))
      }

      edges <- sort_topologically(private$edges)

      executables <- edges[edges$.source, ]
      execution_order <- vapply(
        split(executables, executables$.segment_id),
        function(x) max(x$level),
        numeric(1)
      )
      execution_order <- sort(execution_order)

      for (segment_id in names(execution_order)) {
        segment_id <- as.integer(segment_id)
        self$segments[[segment_id]]$execute()
      }

      invisible(self)
    },

    #' @description Clean all targets
    #' @return `self`
    clean = function() {
      if (warn_pipeline_is_empty(self$segments, "Nothing to clean")) {
        return(invisible(self))
      }

      for (segment in self$segments) {
        file.remove(segment$targets)
      }

      invisible(self)
    },

    #' @description Apply annotations to Pipeline
    #' @param labels A named character vector mapping nodes in the `Pipeline` onto
    #'   labels to display beside them.
    #' @param notes A named character vector mapping nodes in the `Pipeline` onto
    #'   notes to display on beside the labels (nomnoml) or as tooltips (visNetwork).
    annotate = function(labels = NULL, notes = NULL) {
      if (!is.null(notes)) {
        validate_annotation(notes, "notes", private$nodes)
        new_nodes <- apply_annotations(private$nodes, notes, "note")
        private$nodes <- new_nodes
      }

      if (!is.null(labels)) {
        validate_annotation(labels, "labels", private$nodes)
        new_nodes <- apply_annotations(private$nodes, labels, "label")
        private$nodes <- new_nodes
      }

      # Copy label/note info to Segments
      segment_nodes <- private$nodes$id[private$nodes$.source]
      segment_nodes <- private$edges[private$edges$from %in% segment_nodes, c("from", ".segment_id")]
      annotations <- private$nodes[private$nodes$.source, c("id", "label", "note")]
      annotations <- merge(segment_nodes, annotations, by.x = "from", by.y = "id")
      for (i in seq_along(annotations$.segment_id)) {
        id <- annotations$.segment_id[i]
        self$segments[[id]]$annotate(annotations$label[i], annotations$note[i])
      }

      invisible(self)
    },

    #' @description Refresh Pipeline to check outofdateness
    refresh = function() {
      private$refresh_edges()
      private$refresh_nodes()
      invisible(self)
    },

    #' @description Display the pipeline with nomnoml
    #'
    #' @return `self`
    #' @param direction The direction the flowchart should go in
    #' @param arrow_size The arrowhead size
    #' @param edge_style The arrow edge style
    #' @param bend_size The degree of rounding in the arrows (requires
    #'   `edge_style=rounded`)
    #' @param font The name of a font to use
    #' @param font_size The font size
    #' @param line_width The line width for arrows and box outlines
    #' @param padding The amount of padding *within* boxes
    #' @param spacing The amount of spacing *between* boxes,
    #' @param leading The amount of spacing between lines of text
    #' @param stroke The color of arrows, text, and box outlines
    #' @param fill_arrows Whether arrow heads are full triangles (`TRUE`) or
    #'   angled (`FALSE`)
    #' @param gutter The amount space to leave around the flowchart
    #' @param edge_margin The amount of space to leave between boxes and arrows

    nomnoml = function(direction = c("down", "right"),
                       arrow_size = 1,
                       edge_style = c("hard", "rounded"),
                       bend_size = 0.3,
                       font = "Courier",
                       font_size = 12,
                       line_width = 3,
                       padding = 16,
                       spacing = 40,
                       leading = 1.25,
                       stroke = "#33322E",
                       fill_arrows = FALSE,
                       gutter = 5,
                       edge_margin = 0) {
      if (warn_pipeline_is_empty(self$segments, "Nothing to display")) {
        return(invisible(self))
      }

      self$refresh()
      out <- pipeline_nomnoml_code(
        nodes = private$nodes,
        edges = private$edges,
        direction = direction,
        # ranker = ranker,
        arrow_size = arrow_size,
        edge_style = edge_style,
        bend_size = bend_size,
        font = font,
        font_size = font_size,
        line_width = line_width,
        padding = padding,
        spacing = spacing,
        leading = leading,
        stroke = stroke,
        # fill = fill,
        # title = title,
        # zoom = zoom,
        fill_arrows = fill_arrows,
        # acyclicer = acyclicer,
        gutter = gutter,
        edge_margin = edge_margin
      )

      print(nomnoml::nomnoml(out))
      invisible(self)
    },

    #' @description Display the pipeline with nomnoml
    #' @param ...  Arguments (other than `nodes` and `edges`) to pass to
    #'   `visNetwork::visNetwork()`
    #' @return `self`
    visnetwork = function(...) {
      stop_required("visNetwork")
      if (warn_pipeline_is_empty(self$segments, "Nothing to display")) {
        return(invisible(self))
      }

      self$refresh()
      out <- pipeline_network(nodes = private$nodes, edges = private$edges, ...)
      print(out)
      invisible(self)
    },

    #' @description Display a text summary of the pipeline
    #' @return `self`
    text_summary = function() {
      out <- pipeline_text_summary(private$nodes, private$edges, self$segments)
      cat(paste(out, collapse = "\n"))
    },

    #' @description Display
    #' @param ...  Arguments (other than `nodes` and `edges`) to pass to
    #'   `visNetwork::visNetwork()`
    #' @return `self`
    print = function(...) {
      self$nomnoml()
      invisible(self)
    },

    #' @description Save pipeline visNetwork
    #' @param file File to save HTML into
    #' @param selfcontained Whether to save the HTML as a single self-contained
    #'   file (with external resources base64 encoded) or a file with external
    #'   resources placed in an adjacent directory.
    #' @param background Text string giving the html background color of the
    #'   widget. Defaults to white.
    #' @param ...  Arguments (other than `nodes` and `edges`) to pass to
    #'   `visNetwork::visNetwork()`
    #' @return `self`
    save_visnetwork = function(file, selfcontained = TRUE, background = "white", ...) {
      stop_required("visNetwork")
      if (warn_pipeline_is_empty(self$segments, "Nothing to save")) {
        return(invisible(self))
      }

      self$refresh()
      out <- pipeline_network(nodes = private$nodes, edges = private$edges, ...)
      visNetwork::visSave(out, file, selfcontained, background)
      invisible(self)
    },

    #' @description Save pipeline nomnoml
    #' @param file File to save the png into
    #' @param width Image width
    #' @param height Image height
    #' @param ...  Arguments to pass to `self$nomnoml()`
    #' @return `self`
    save_nomnoml = function(file, width = NULL, height = NULL, ...) {
      stop_required("webshot")
      if (warn_pipeline_is_empty(self$segments, "Nothing to save")) {
        return(invisible(self))
      }

      stopifnot("`file` must be a .png path" = grepl(x=file, ".png$"))
      wd <- getwd()
      on.exit(setwd(wd))

      self$refresh()
      code <- pipeline_nomnoml_code(nodes = private$nodes, edges = private$edges, ...)

      x <- list(code = code, svg = FALSE)
      widget <- htmlwidgets::createWidget(
        name = "nomnoml",
        x, width = width, height = height,
        package = "nomnoml"
      )

      html <- tempfile("flow_", fileext = ".html")
      htmlwidgets::saveWidget(widget, html)

      out <- tempfile("out_", fileext = ".png")

      setwd(tempdir())
      webshot::webshot(basename(html), out, selector = "canvas")
      setwd(wd)

      file.copy(out, file)
      unlink(out)
      unlink(html)

      invisible(self)
    },

    #' @description Save a text summary of the pipeline
    #' @param file File to save text summary into
    #' @return `self`
    save_text_summary = function(file) {
      is_txt <- grepl(x=file, ".(txt|md)$", ignore.case = TRUE)
      stopifnot("`file` must be a .txt or .md path" = is_txt)

      out <- pipeline_text_summary(private$nodes, private$edges, self$segments)
      writeLines(out, con = file)

      invisible(self)
    }
  )
)

# Accessors --------------------------------------------------------------------

#' Access and interface with Pipeline.
#'
#' `get_pipeline()`, `set_pipeline()` and `reset_pipeline()` access and modify
#' the current *active* pipeline, while all other helper functions do not affect
#' the active pipeline
#'
#'
#' @param pipeline A pipeline. See [Pipeline] for more details.
#' @name pipeline-accessors
#' @family pipeline
#' @examples
#' \dontrun{
#' # Build up a pipeline from scratch and save it out
#' reset_pipeline()
#' # A series of `make_with_*()` blocks go here...
#' saveRDS(get_pipeline(), "data/my_pipeline.Rds")
#'
#' # ... Later on we can read in and set the pipeline
#' p <- readRDS("data/my_pipeline.Rds")
#' set_pipeline(p)
#' }
NULL
makepipe_env <- new.env(parent = emptyenv())

#' @rdname pipeline-accessors
#' @export
is_pipeline <- function(pipeline) {
  inherits(pipeline, c("Pipeline", "R6"))
}

#' @rdname pipeline-accessors
#' @export
set_pipeline <- function(pipeline) {
  if (!is_pipeline(pipeline)) {
    stop("`pipeline` must be a Pipeline object", call. = FALSE)
  }

  old <- makepipe_env$pipeline
  makepipe_env$pipeline <- pipeline
  invisible(old)
}

#' @rdname pipeline-accessors
#' @export
get_pipeline <- function() {
  pipe <- makepipe_env$pipeline
  pipe
}

#' @rdname pipeline-accessors
#' @export
reset_pipeline <- function() {
  old <- makepipe_env$pipeline
  makepipe_env$pipeline <- Pipeline$new()
  invisible(old)
}


# Visualisors ------------------------------------------------------------------

#' Visualise the Pipeline.
#'
#' Produce a flowchart visualisation of the pipeline. Out-of-date targets will
#' be coloured red, up-to-date targets will be coloured green, and everything
#' else will be blue.
#'
#' Labels and notes must be supplied as named character vector where the
#' names correspond to the filepaths of nodes (i.e. `targets`, `dependencies`,
#' or `source` scripts)
#'
#' @param file File to save png (nomnoml) or html (visnetwork) into
#' @param pipeline A pipeline. See [Pipeline] for more details.
#' @param as A string determining whether to use `nomnoml` or `visNetwork`
#' @param labels A named character vector mapping nodes in the `pipeline` onto
#'   labels to display beside them.
#' @param notes A named character vector mapping nodes in the `Pipeline` onto
#'   notes to display on beside the labels (nomnoml) or as tooltips (visNetwork).
#' @param ...  Arguments passed onto `Pipeline$nomnoml()` or `Pipeline$visnetwork`
#'
#' @name pipeline-vis
#' @family pipeline
#' @examples
#' \dontrun{
#' # Run pipeline
#' make_with_source(
#'   "recode.R",
#'   "data/0 raw_data.R",
#'   "data/1 data.R"
#' )
#' make_with_source(
#'   "merge.R",
#'   c("data/1 data.R", "data/0 raw_pop.R"),
#'   "data/2 data.R"
#' )
#'
#' # Visualise pipeline with custom notes
#' show_pipeline(notes = c(
#'   "data/0 raw_data.R" = "Raw survey data",
#'   "data/0 raw_pop.R" = "Raw population data",
#'   "data/1 data.R" = "Survey data with recodes applied",
#'   "data/2 data.R" = "Survey data with demographic variables merged in"
#' ))
#' }
NULL
#' @rdname pipeline-vis
#' @export
show_pipeline <- function(pipeline = get_pipeline(),
                          as = c("nomnoml", "visnetwork", "text"),
                          labels = NULL, notes = NULL, ...) {
  as <- match.arg(as)
  pipeline$annotate(labels, notes)
  switch(as,
    nomnoml = pipeline$nomnoml(...),
    visnetwork = pipeline$visnetwork(...),
    text = pipeline$text_summary()
  )
}

#' @rdname pipeline-vis
#' @export
save_pipeline <- function(file, pipeline = get_pipeline(),
                          as = c("nomnoml", "visnetwork", "text"),
                          labels = NULL, notes = NULL, ...) {
  as <- match.arg(as)
  pipeline$annotate(labels, notes)
  switch(as,
         nomnoml = pipeline$save_nomnoml(file, ...),
         visnetwork = pipeline$save_visnetwork(file, ...),
         text = pipeline$save_text_summary(file, ...)
  )
}


# Internal ---------------------------------------------------------------------

#' Issue a warning because the pipeline is empty
#'
#' @param segments Pipeline segments
#' @param msg A message to warn with if pipeline is empty
#'
#' @return TRUE (if pipeline is empty) or FALSE
#' @noRd
#' @keywords internal
#'
warn_pipeline_is_empty <- function(segments, msg) {
  empty <- length(segments) == 0
  if (empty) warning("`Pipeline` is empty. ", msg, call. = FALSE)
  empty
}

#' Propagate out-of-dateness
#'
#' Any target that is downstream of an out-of-date target is itself out-of-date.
#' This algorithm ensures the `edges` data.frame reflects this fact by
#' propagating out-of-dateness along network edges.
#'
#' @param edges A data.frame defining the edges
#'
#' @return A data.frame defining edges from all nodes in `from` to all nodes in
#'   `to`.
#' @noRd
propagate_outofdateness <- function(edges) {
  if (is.null(edges)) return(NULL)
  nodes <- unlist(list(edges$from, edges$to))
  nodes_left <- nodes
  edges_left <- edges
  while(length(nodes_left)){
    # Targets with no dependencies
    next_targets <- setdiff(nodes_left, edges_left$to)
    next_targets <- factor(next_targets, levels = levels(nodes))

    # Step outofdateness forward
    for (i in next_targets) {
      dependencies <- edges[edges$to %in% i, "from"]
      if (length(dependencies) == 0) next
      dependency_outdated <- any((edges$to %in% dependencies) & edges$.outdated)
      outdated <- edges[edges$to %in% i, ".outdated"] | dependency_outdated
      edges[edges$to %in% i, ".outdated"] <- outdated
    }

    # Prune the graph.
    nodes_left <- setdiff(nodes_left, next_targets)
    edges_left <- edges_left[!(edges_left$from %in% next_targets),]
  }

  edges[edges$.source, ".outdated"] <- FALSE

  edges
}

#' Sort edges topologically
#'
#' This algorithm sorts edges topologically by starting with nodes without
#' dependencies, assigning them to first level, removing them from the graph and
#' then repeating the process. Once again, we take nodes without any
#' dependencies left in the graph, assign them to second level. We keep going
#' until there are no nodes left in the graph.
#'
#' @param edges A data.frame defining the edges
#'
#' @return A data.frame defining edges from all nodes in `from` to all nodes in
#'   `to`.
#' @noRd
sort_topologically <- function(edges) {
  level <- 1
  edges$level <- NA
  nodes <- unlist(list(edges$from, edges$to))

  nodes_left <- nodes
  edges_left <- edges
  while(length(nodes_left)){
    # Targets with no dependencies
    next_targets <- setdiff(nodes_left, edges_left$to)
    next_targets <- factor(next_targets, levels = levels(nodes))

    # Assign to level
    edges[edges$from %in% next_targets, "level"] <- level

    # Prune the graph.
    nodes_left <- setdiff(nodes_left, next_targets)
    edges_left <- edges_left[!(edges_left$from %in% next_targets),]

    level <- level + 1
  }

  edges
}

## Network ---------------------------------------------------------------------
#' Create a network visualisation of the Pipeline
#'
#' @param nodes A data.frame defining the nodes
#' @param edges A data.frame defining the edges
#' @param ...  Arguments (other than `nodes` and `edges`) to pass to
#'   `visNetwork::visNetwork()`
#' @return A visNetwork
#' @noRd
pipeline_network <- function(nodes, edges, ...) {
  stop_required("visNetwork")

  # Add default notes/labels
  recipe_no_label <- nodes$.recipe & (nodes$label == "")
  nodes$label[recipe_no_label] <- "Recipe"
  no_label <-nodes$label == ""
  nodes$label[no_label] <- basename(as.character(nodes$id[no_label]))
  no_note <- (nodes$note == "")
  nodes$note[no_note] <- as.character(nodes$id[no_note])


  # visNetwork expects tooltips to be stored in `title` column
  nodes$title <- nodes$note

  # Add aesthetics to nodes
  nodes$shape <- ifelse(nodes$.recipe, "circle", "square")
  nodes$shape <- ifelse(nodes$.pkg, "triangle", nodes$shape)

  outdated <- nodes$id %in% edges[edges$.outdated, "to"]
  nodes$group <- ifelse(outdated, "Out-of-date", "Up-to-date")
  nodes$group <- ifelse(nodes$.source, "Source", nodes$group)

  # Visualise
  out <- visNetwork::visNetwork(nodes = nodes, edges = edges, ...)
  out <- visNetwork::visGroups(out, groupname = "Out-of-date", color = "#ffcaef")
  out <- visNetwork::visGroups(out, groupname = "Up-to-date", color = "#caffda")
  out <- visNetwork::visLegend(out)

  visNetwork::visHierarchicalLayout(
    out, sortMethod = "directed", direction = "LR"
  )
}


#' Validate annotations
#'
#' @param x An annotation
#' @param x_name A string, the kind of annotation
#' @param nodes A data.frame defining the nodes
#'
#' @return A data.frame defining edges from all nodes in `from` to all nodes in
#'   `to`.
#' @noRd
validate_annotation <- function(x, x_name, nodes) {
  stopifnot_class(x, "character")
  if (!identical(length(names(x)), length(x))) {
    stop("`", x_name, "` must be named", call. = FALSE)
  }

  if (any(duplicated(names(x)))) {
    stop("names of `", x_name, "` must not be duplicated", call. = FALSE)
  }

  bad_nodes <- setdiff(names(x), as.character(nodes$id))
  if (length(bad_nodes) > 0) {
    stop(
      "`", paste(bad_nodes, collapse = "`, "), "` ",
      "are not nodes in `Pipeline`", call. = FALSE
    )
  }

  invisible(NULL)
}

#' Validate annotations
#'
#' @param nodes A data.frame defining the nodes
#' @param annotations A named character vector of annotations to apply
#' @param at A string, the kind of annotation to apply
#'
#' @return A data.frame defining the nodes with annotations applied
#' @noRd
apply_annotations <- function(nodes, annotations, at) {
  nodes$node_id <- as.character(nodes$id)
  annotations <- data.frame(
    node_id = names(annotations),
    ..annotation = annotations,
    stringsAsFactors = FALSE
  )

  new_nodes <- merge(nodes, annotations, by = "node_id", all.x = TRUE)
  new_nodes[[at]] <- ifelse(
    is.na(new_nodes$..annotation),
    new_nodes[[at]],
    new_nodes$..annotation
  )
  new_nodes <- new_nodes[, setdiff(names(new_nodes), c("node_id", "..annotation"))]
  new_nodes
}


## Nomnoml ---------------------------------------------------------------------
pipeline_nomnoml_code <- function(nodes,
                                  edges,
                                  direction = c("down", "right"),
                                  ranker = c("network-simplex", "tight-tree", "longest-path"),
                                  arrow_size = 1,
                                  edge_style = c("hard", "rounded"),
                                  bend_size = 0.3,
                                  font = "Courier",
                                  font_size = 12,
                                  line_width = 3,
                                  padding = 16,
                                  spacing = 40,
                                  leading = 1.25,
                                  stroke = "#33322E",
                                  fill = "#eee8d5",
                                  title = "filename",
                                  zoom = 1,
                                  fill_arrows = FALSE,
                                  acyclicer = "greedy",
                                  gutter = 5,
                                  edge_margin = 0) {
  direction <- match.arg(direction)
  edge_style     <- match.arg(edge_style)
  ranker    <- match.arg(ranker)

  # FALSE or TRUE must become "false" or "true" for nomnoml
  fill_arrows <- tolower(fill_arrows)

  # Add default notes/labels
  recipe_no_label <- nodes$.recipe & (nodes$label == "")
  nodes$label[recipe_no_label] <- "Recipe"
  no_label <-nodes$label == ""
  nodes$label[no_label] <- basename(as.character(nodes$id[no_label]))
  recipe_no_note <- nodes$.recipe & (nodes$note == "")
  nodes$note[recipe_no_note] <- as.character(nodes$id[recipe_no_note])

  # Enforce uniquness of label since this is our nomnoml id
  nodes$label <- make.unique(nodes$label, sep = " +")

  # Now wrap the ones that aren't code
  note_matches_id <- nodes$note==as.character(nodes$id)
  nodes$note[!note_matches_id] <- strwrap2(nodes$note[!note_matches_id], 40)

  # Escape special characters
  nodes$label <- escape_pipes_and_brackets(nodes$label)
  nodes$note <- escape_pipes_and_brackets(nodes$note)

  # Add aesthetics

  outdated <- nodes$id %in% edges[edges$.outdated, "to"]
  nodes$shape <- ifelse(nodes$.recipe, "recipe", "box")
  nodes$shape <- ifelse(nodes$.pkg, "pkg", nodes$shape)
  nodes$color <- ifelse(outdated, "red", "green")
  nodes$color <- ifelse(nodes$.source, "blue", nodes$color)

  # Build boxes
  nodes$box <- NA
  nodes$box <- sprintf(
    "[<%s%s> %s | %s]",
    nodes$color,
    nodes$shape,
    nodes$label,
    nodes$note
  )
  nodes$box  <- sub(" \\|\\s* ]$", "]", nodes$box) # Cleanup if no note

  # Graph
  nom_edges <- sprintf(
    "%s --> %s",
    nodes$box[match(edges$from, nodes$id)],
    nodes$box[match(edges$to, nodes$id)]
  )
  nom_edges <- paste(nom_edges, collapse = "\n")

  # Styles
  header <- paste0(
    "#.redbox: fill=#ffcaef title=bold align=center\n",
    "#.greenbox: fill=#caffda title=bold align=center\n",
    "#.bluebox: fill=#77b6fe title=bold align=center\n",
    "#.bluerecipe: align=center fill=#77b6fe title=bold\n",
    "#.bluepkg: visual=database fill=#77b6fe title=bold align=center\n",
    "#arrowSize: ", arrow_size, "\n",
    "#bendSize: ", bend_size, "\n",
    "#direction: ", direction, "\n",
    "#gutter: ", gutter, "\n",
    "#edgeMargin: ", edge_margin, "\n",
    "#edges: ", edge_style, "\n",
    "#fill: ", fill, "\n",
    "#fillArrows: ", fill_arrows, "\n",
    "#font: ", font, "\n",
    "#fontSize: ", font_size, "\n",
    "#leading: ", leading, "\n",
    "#lineWidth: ", line_width, "\n",
    "#padding: ", padding, "\n",
    "#spacing: ", spacing, "\n",
    "#stroke: ", stroke, "\n",
    "#title: ", title, "\n",
    "#zoom: ", zoom, "\n",
    "#acyclicer: ", acyclicer, "\n",
    "#ranker: ", ranker, "\n"
  )

  paste0(header,"\n", nom_edges)
}


escape_pipes_and_brackets <- function(x) {
  x <- gsub("]","\\]", x ,fixed = TRUE)
  x <- gsub("[","\\[", x ,fixed = TRUE)
  x <- gsub("|","\\|", x ,fixed = TRUE)
  x
}


# Text summary ------------------------------------------------------------

pipeline_text_summary <- function(nodes, edges, segments) {
  edges <- sort_topologically(edges)

  # Sort segments by topological order
  segment_nodes <- nodes$id[nodes$.source]
  segment_nodes <- edges[edges$from %in% segment_nodes, c("from", ".segment_id", "level")]
  segment_nodes <- segment_nodes[order(segment_nodes$level), ]

  out <- c("# Pipeline", "")
  for (i in segment_nodes$.segment_id) {
    out <- c(out, segments[[i]]$text_summary, "")
  }

  out
}

Try the makepipe package in your browser

Any scripts or data that you put into this service are public.

makepipe documentation built on Dec. 7, 2022, 5:16 p.m.