├── .Rbuildignore ├── .gitattributes ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ ├── lint.yaml │ ├── pkgdown.yaml │ └── test-coverage.yaml ├── .gitignore ├── .lintr ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── add_node_attr.R ├── arrow_direction.R ├── get_edge_labels.R ├── get_edges.R ├── get_layout.R ├── get_nodes.R ├── ggflowchart.R └── globalVariables.R ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── ggflowchart.Rproj ├── man ├── figures │ ├── README-minimal.png │ └── logo.png └── ggflowchart.Rd ├── tests ├── testthat.R └── testthat │ └── test-column-tests.R └── vignettes ├── .gitignore ├── decision-tree-example.Rmd ├── layout-options.Rmd ├── minimal-example.Rmd └── style-nodes.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ggflowchart\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | .lintr 5 | ^_pkgdown\.yml$ 6 | ^docs$ 7 | ^pkgdown$ 8 | ^\.github$ 9 | cran-comments.md 10 | .gitattributes 11 | ^cran-comments\.md$ 12 | ^CRAN-SUBMISSION$ 13 | ^codecov\.yml$ 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=false 2 | *.scss linguist-detectable=false 3 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'release'} 24 | 25 | env: 26 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 27 | R_KEEP_PKG_SOURCE: yes 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: r-lib/actions/setup-pandoc@v2 33 | 34 | - uses: r-lib/actions/setup-r@v2 35 | with: 36 | r-version: ${{ matrix.config.r }} 37 | http-user-agent: ${{ matrix.config.http-user-agent }} 38 | use-public-rspm: true 39 | 40 | - uses: r-lib/actions/setup-r-dependencies@v2 41 | with: 42 | extra-packages: any::rcmdcheck 43 | needs: check 44 | 45 | - uses: r-lib/actions/check-r-package@v2 46 | with: 47 | upload-snapshots: true 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: lint 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: r-lib/actions/setup-r@v2 20 | with: 21 | use-public-rspm: true 22 | 23 | - uses: r-lib/actions/setup-r-dependencies@v2 24 | with: 25 | extra-packages: any::lintr, local::. 26 | needs: lint 27 | 28 | - name: Lint 29 | run: lintr::lint_package() 30 | shell: Rscript {0} 31 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | name: pkgdown 11 | 12 | jobs: 13 | pkgdown: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: r-lib/actions/setup-pandoc@v2 21 | 22 | - uses: r-lib/actions/setup-r@v2 23 | with: 24 | use-public-rspm: true 25 | 26 | - uses: r-lib/actions/setup-r-dependencies@v2 27 | with: 28 | extra-packages: any::pkgdown, local::. 29 | needs: website 30 | 31 | - name: Deploy package 32 | run: | 33 | git config --local user.name "$GITHUB_ACTOR" 34 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 35 | Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 36 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: covr::codecov(quiet = FALSE) 31 | shell: Rscript {0} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | inst/doc 6 | docs 7 | CRAN-SUBMISSION 8 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: linters_with_defaults( 2 | line_length_linter = line_length_linter(160) 3 | ) 4 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggflowchart 2 | Title: Flowcharts with 'ggplot2' 3 | Version: 1.0.0.9007 4 | Authors@R: 5 | person(given = "Nicola", 6 | family = "Rennie", 7 | role = c("aut", "cre", "cph"), 8 | email = "nrennie35@gmail.com") 9 | Description: Flowcharts can be a useful way to visualise complex processes. This package 10 | uses the layered grammar of graphics of 'ggplot2' to create simple flowcharts. 11 | License: MIT + file LICENSE 12 | Encoding: UTF-8 13 | LazyData: true 14 | Depends: 15 | R (>= 3.6) 16 | Imports: 17 | dplyr, 18 | igraph, 19 | ggplot2 (>= 3.4.0), 20 | purrr, 21 | rlang, 22 | tibble, 23 | tidyr 24 | Suggests: 25 | knitr, 26 | rcartocolor, 27 | rmarkdown, 28 | testthat (>= 3.0.0) 29 | Roxygen: list(markdown = TRUE) 30 | RoxygenNote: 7.2.3 31 | VignetteBuilder: knitr 32 | Config/Needs/website: nrennie/nrenniepkgdown 33 | URL: https://nrennie.github.io/ggflowchart/ 34 | BugReports: https://github.com/nrennie/ggflowchart/issues 35 | Config/testthat/edition: 3 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: ggflowchart authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 ggflowchart authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(ggflowchart) 4 | importFrom(dplyr,"%>%") 5 | importFrom(rlang,.data) 6 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | ## ggflowchart 1.0.0.9000+ 2023_12_20 2 | 3 | * Add vignette on how to style nodes 4 | * Add initial unit testing 5 | * Add utils function to determine arrow type 6 | * Add lintr checks to GH Actions 7 | * Add contributor guidelines 8 | * Add option to parse text in nodes. 9 | * Update README 10 | * Update linting 11 | * Add link to issues in Description 12 | * Add `alpha` as argument 13 | * Same level arrows support (issue #5) 14 | * Add option for custom layout (issue #11) 15 | * Allow `x_nudge` and `y_nudge` to vary for each node. (issue #12) 16 | * Add `arrow_linewidth` and `arrow_linewidth` as arguments (issue #14) 17 | * Add `linewidth` as argument (issue #14) 18 | * Add ability to add arrow labels (issue #15) 19 | * Add codecov GitHub action (#24) 20 | * More informative error handling (issue #29) 21 | * Set minimum version of {ggplot2} (issue #31) 22 | 23 | ## ggflowchart 1.0.0 2023_05_10 24 | 25 | * Initial CRAN submission 26 | 27 | ## ggflowchart 0.0.2 2023_05_10 28 | 29 | * Add `horizontal` argument to `ggflowchart()` 30 | * Add `arrow_size` argument to `ggflowchart()` 31 | * Add `text_size` argument to `ggflowchart()` 32 | * Add option to set `fill` based on column in `node_data` 33 | * Add option to set `text_colour` based on column in `node_data` 34 | * Update vignette examples 35 | 36 | ## ggflowchart 0.0.1 2022_08_06 37 | 38 | * Initialise package 39 | * Adds non-exported `get_layout()` to obtain centre of node positions 40 | * Adds non-exported `get_nodes()` to define perimeter of node boxes 41 | * Adds non-exported `add_node_attr()` to include node attributes including labels 42 | * Adds non-exported `get_edges()` to define arrow start and end positions 43 | * Adds `ggflowchart()` to create flowchart 44 | -------------------------------------------------------------------------------- /R/add_node_attr.R: -------------------------------------------------------------------------------- 1 | #' Add node attributes 2 | #' 3 | #' Adds additional information to the node e.g. variable to colour by 4 | #' 5 | #' @param node_layout Data frame of node layout returned by \code{get_layout()}. 6 | #' @param node_data Data frame or tibble of node information. Must have at least one column 7 | #' called "name" for node names to join by. To change labels that appear on nodes, include 8 | #' a column with the name "label". 9 | #' @return A tibble. 10 | #' @noRd 11 | 12 | add_node_attr <- function(node_layout, node_data) { 13 | # define notin 14 | "%notin%" <- function(x, y) { 15 | !("%in%" (x, y)) 16 | } 17 | # if no additional node info provided 18 | if (is.null(node_data)) { 19 | return(node_layout) 20 | } else { 21 | # check if names correct if any missing 22 | if ("name" %notin% colnames(node_data)) { 23 | stop("node_data must have a column called `name` to join to node layout") 24 | } 25 | # add node attributes 26 | node_layout <- dplyr::left_join(node_layout, node_data, by = "name") 27 | return(node_layout) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /R/arrow_direction.R: -------------------------------------------------------------------------------- 1 | #' Single arrow direction 2 | #' 3 | #' Define the direction of a single arrow 4 | #' 5 | #' @param plot_nodes Tibble of node coordinates output from \code{get_nodes()}. 6 | #' @param from Character name of starting node. 7 | #' @param to Character name of end node. 8 | #' @importFrom rlang .data 9 | #' @return A tibble. 10 | #' @noRd 11 | 12 | single_arrow_direction <- function(plot_nodes, 13 | from, 14 | to) { 15 | # calculate slope 16 | from_coords <- unlist(dplyr::filter(plot_nodes, .data$name == from)[, c("x", "y")]) 17 | to_coords <- unlist(dplyr::filter(plot_nodes, .data$name == to)[, c("x", "y")]) 18 | x_diff <- to_coords["x"] - from_coords["x"] 19 | y_diff <- (to_coords["y"] - from_coords["y"]) 20 | # arrow type 21 | if (y_diff == 0 && x_diff > 0) { 22 | return("lr") 23 | } else if (y_diff == 0 && x_diff < 0) { 24 | return("rl") 25 | } else if (y_diff > 0) { 26 | return("up") 27 | } else { 28 | return("down") 29 | } 30 | } 31 | 32 | #' Arrow direction 33 | #' 34 | #' Define the direction of each arrow 35 | #' @param data Data frame or tibble of edges. Must have two columns, first 36 | #' column are "from" node names, second column is "to" node names. 37 | #' Node names must be unique. 38 | #' @param plot_nodes Tibble of node coordinates output from \code{get_nodes()}. 39 | #' @param node_layout Data frame of node layout returned by \code{get_layout()} 40 | #' @importFrom rlang .data 41 | #' @return A tibble. 42 | #' @noRd 43 | 44 | arrow_direction <- function(data, 45 | plot_nodes, 46 | node_layout) { 47 | data %>% 48 | dplyr::mutate( 49 | arrow_direction = purrr::map2( 50 | .x = data$from, 51 | .y = data$to, 52 | .f = ~ single_arrow_direction(node_layout, .x, .y) 53 | ) %>% 54 | unlist() 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /R/get_edge_labels.R: -------------------------------------------------------------------------------- 1 | #' Get edge labels 2 | #' 3 | #' Define the end points of the arrows 4 | #' 5 | #' @param data Data frame or tibble of edges. Must have at least two columns, if not labelled "from" 6 | #' and "to" it is assumed first column are "from" node names, 7 | #' second column is "to" node names. Node names must be unique. 8 | #' @param plot_edges Tibble of edge coordinates output from \code{get_edges()}. 9 | #' @importFrom dplyr %>% 10 | #' @importFrom rlang .data 11 | #' @return A tibble. 12 | #' @noRd 13 | 14 | get_edge_labels <- function( 15 | data, 16 | plot_edges) { 17 | if (is.null(data[["label"]])) { 18 | return(NULL) 19 | } else { 20 | # check number of columns 21 | if (ncol(data) < 2) { 22 | stop("Incorrect number of columns in data: data should contain at least two columns.") 23 | } 24 | # define notin 25 | "%notin%" <- function(x, y) { 26 | !("%in%"(x, y)) 27 | } 28 | # check column names 29 | if ("from" %in% colnames(data) && "to" %notin% colnames(data)) { 30 | stop("data contains only 'from' column but not 'to'") 31 | } 32 | if ("to" %in% colnames(data) && "from" %notin% colnames(data)) { 33 | stop("data contains only 'to' column but not 'from'") 34 | } 35 | if (!all(c("from", "to") %in% colnames(data))) { 36 | colnames(data)[1:2] <- c("from", "to") 37 | message("'from' and 'to' not found in column names, using first two columns instead.") 38 | } 39 | edge_labels <- plot_edges %>% 40 | dplyr::group_by(.data$id) %>% 41 | dplyr::mutate( 42 | text_x = mean(.data$x), 43 | text_y = mean(.data$y) 44 | ) %>% 45 | dplyr::ungroup() %>% 46 | dplyr::select(.data$s_e, .data$name, .data$text_x, .data$text_y) %>% 47 | tidyr::pivot_wider( 48 | names_from = .data$s_e, 49 | values_from = .data$name 50 | ) %>% 51 | dplyr::left_join( 52 | data, 53 | by = c("from", "to") 54 | ) %>% 55 | dplyr::select(.data$text_x, .data$text_y, .data$label) 56 | return(edge_labels) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /R/get_edges.R: -------------------------------------------------------------------------------- 1 | #' Get edges 2 | #' 3 | #' Define the end points of the arrows 4 | #' 5 | #' @param data Data frame or tibble of edges. Must have at least two columns, if not labelled "from" 6 | #' and "to" it is assumed first column are "from" node names, 7 | #' second column is "to" node names. Node names must be unique. 8 | #' @param plot_nodes Tibble of node coordinates output from \code{get_nodes()}. 9 | #' @param node_layout Data frame of node layout returned by \code{get_layout()} 10 | #' @importFrom dplyr %>% 11 | #' @importFrom rlang .data 12 | #' @return A tibble. 13 | #' @noRd 14 | 15 | get_edges <- function(data, plot_nodes, node_layout) { 16 | if (ncol(data) < 2) { 17 | stop("Incorrect number of columns in data: input data should contain at least two columns.") 18 | } 19 | if (!all(c("from", "to") %in% colnames(data))) { 20 | colnames(data)[1:2] <- c("from", "to") 21 | } 22 | # define edge types 23 | edge_type_data <- arrow_direction( 24 | data = data, 25 | plot_nodes = plot_nodes, 26 | node_layout = node_layout 27 | ) 28 | # compute start and end points for each type of edge 29 | # Downwards (default) 30 | plot_edge_down <- edge_type_data %>% 31 | dplyr::filter(.data$arrow_direction == "down") %>% 32 | dplyr::select(c(.data$from, .data$to)) %>% 33 | dplyr::mutate(id = paste("down", dplyr::row_number())) %>% 34 | tidyr::pivot_longer( 35 | cols = c("from", "to"), 36 | names_to = "s_e", 37 | values_to = "name" 38 | ) %>% 39 | dplyr::left_join( 40 | dplyr::select(plot_nodes, -c(.data$x_nudge, .data$y_nudge)), 41 | by = "name" 42 | ) %>% 43 | dplyr::select(-c( 44 | .data$y, 45 | .data$xmin, 46 | .data$xmax 47 | )) %>% 48 | dplyr::mutate(y = ifelse(.data$s_e == "from", 49 | .data$ymin, 50 | .data$ymax 51 | )) %>% 52 | dplyr::select(-c( 53 | .data$ymin, 54 | .data$ymax 55 | )) 56 | # Upwards 57 | plot_edge_up <- edge_type_data %>% 58 | dplyr::filter(.data$arrow_direction == "up") %>% 59 | dplyr::select(c(.data$from, .data$to)) %>% 60 | dplyr::mutate(id = paste("up", dplyr::row_number())) %>% 61 | tidyr::pivot_longer( 62 | cols = c("from", "to"), 63 | names_to = "s_e", 64 | values_to = "name" 65 | ) %>% 66 | dplyr::left_join( 67 | dplyr::select(plot_nodes, -c(.data$x_nudge, .data$y_nudge)), 68 | by = "name" 69 | ) %>% 70 | dplyr::select(-c( 71 | .data$y, 72 | .data$xmin, 73 | .data$xmax 74 | )) %>% 75 | dplyr::mutate(y = ifelse(.data$s_e == "from", 76 | .data$ymax, 77 | .data$ymin 78 | )) %>% 79 | dplyr::select(-c( 80 | .data$ymin, 81 | .data$ymax 82 | )) 83 | # Left to right 84 | plot_edge_lr <- edge_type_data %>% 85 | dplyr::filter(.data$arrow_direction == "lr") %>% 86 | dplyr::select(c(.data$from, .data$to)) %>% 87 | dplyr::mutate(id = paste("lr", dplyr::row_number())) %>% 88 | tidyr::pivot_longer( 89 | cols = c("from", "to"), 90 | names_to = "s_e", 91 | values_to = "name" 92 | ) %>% 93 | dplyr::left_join( 94 | dplyr::select( 95 | plot_nodes, -c(.data$x_nudge, .data$y_nudge) 96 | ), 97 | by = "name" 98 | ) %>% 99 | dplyr::select(-c( 100 | .data$x, 101 | .data$ymin, 102 | .data$ymax 103 | )) %>% 104 | dplyr::mutate(x = ifelse(.data$s_e == "from", 105 | .data$xmax, 106 | .data$xmin 107 | )) %>% 108 | dplyr::select(-c( 109 | .data$xmin, 110 | .data$xmax 111 | )) 112 | # right to left 113 | plot_edge_rl <- edge_type_data %>% 114 | dplyr::filter(.data$arrow_direction == "rl") %>% 115 | dplyr::select(c(.data$from, .data$to)) %>% 116 | dplyr::mutate(id = paste("rl", dplyr::row_number())) %>% 117 | tidyr::pivot_longer( 118 | cols = c("from", "to"), 119 | names_to = "s_e", 120 | values_to = "name" 121 | ) %>% 122 | dplyr::left_join( 123 | dplyr::select(plot_nodes, -c(.data$x_nudge, .data$y_nudge)), 124 | by = "name" 125 | ) %>% 126 | dplyr::select(-c( 127 | .data$x, 128 | .data$ymin, 129 | .data$ymax 130 | )) %>% 131 | dplyr::mutate(x = ifelse(.data$s_e == "from", 132 | .data$xmin, 133 | .data$xmax 134 | )) %>% 135 | dplyr::select(-c( 136 | .data$xmin, 137 | .data$xmax 138 | )) 139 | # join data set together 140 | plot_edges <- rbind( 141 | plot_edge_up, plot_edge_down, plot_edge_rl, plot_edge_lr 142 | ) 143 | return(plot_edges) 144 | } 145 | -------------------------------------------------------------------------------- /R/get_layout.R: -------------------------------------------------------------------------------- 1 | #' Get layout 2 | #' 3 | #' Obtains the coordinates for the nodes of the flowchart from the input edges 4 | #' 5 | #' @param data Data frame or tibble of edges. Must have at least two columns, if not labelled "from" 6 | #' it is assumed first column is `"from"` node names, 7 | #' second column is `"to"` node names. Node names must be unique. 8 | #' @param layout One of `c("tree", "custom")`. If `"tree"` uses the tree layout 9 | #' from {igraph}. If `"custom"`, then `x` and `y` columns must be provided in 10 | #' `node_data` specifying the coordinates of the centre of the boxes. Default 11 | #' `"tree"`. 12 | #' @importFrom dplyr %>% 13 | #' @return A tibble. 14 | #' @noRd 15 | 16 | get_layout <- function(data, layout = "tree", node_data = NULL) { 17 | # check number of columns 18 | if (ncol(data) < 2) { 19 | stop("Incorrect number of columns in data: data should contain at least two columns.") 20 | } 21 | # define notin 22 | "%notin%" <- function(x, y) { 23 | !("%in%"(x, y)) 24 | } 25 | # check column names 26 | if ("from" %in% colnames(data) && "to" %notin% colnames(data)) { 27 | stop("data contains only 'from' column but not 'to'") 28 | } 29 | if ("to" %in% colnames(data) && "from" %notin% colnames(data)) { 30 | stop("data contains only 'to' column but not 'from'") 31 | } 32 | if (!all(c("from", "to") %in% colnames(data))) { 33 | colnames(data)[1:2] <- c("from", "to") 34 | message("'from' and 'to' not found in column names, using first two columns instead.") 35 | } 36 | # generate layout 37 | if (layout == "tree") { 38 | data <- dplyr::select(data, c(.data$from, .data$to)) 39 | g <- igraph::graph_from_data_frame(data, directed = TRUE) 40 | coords <- igraph::layout_as_tree(g) 41 | colnames(coords) <- c("x", "y") 42 | output <- tibble::as_tibble(coords) %>% 43 | dplyr::mutate(name = igraph::vertex_attr(g, "name")) 44 | } else if (layout == "custom") { 45 | # check if names correct if any missing 46 | if ("name" %notin% colnames(node_data)) { 47 | stop("node_data must have a column called `name` to join to node layout") 48 | } 49 | # check if x and y exist 50 | if (any(c("x", "y") %notin% colnames(node_data))) { 51 | stop("node_data must have a columns called `x` and `y` with node coordinates") 52 | } 53 | output <- node_data %>% 54 | dplyr::select(c(.data$x, .data$y, .data$name)) 55 | } 56 | return(output) 57 | } 58 | -------------------------------------------------------------------------------- /R/get_nodes.R: -------------------------------------------------------------------------------- 1 | #' Get nodes 2 | #' 3 | #' Define the corners of the nodes. 4 | #' 5 | #' @param node_layout Data frame of node layout returned by \code{get_layout()} 6 | #' @param x_nudge Distance from centre of edge of node box in x direction. 7 | #' Default 0.35. 8 | #' @param y_nudge Distance from centre of edge of node box in y direction. 9 | #' Default 0.25. 10 | #' @importFrom dplyr %>% 11 | #' @importFrom rlang .data 12 | #' @return A tibble. 13 | #' @noRd 14 | 15 | get_nodes <- function(node_layout, 16 | x_nudge = 0.35, 17 | y_nudge = 0.25) { 18 | # if x_nudge not specified use global values 19 | if (!("x_nudge" %in% colnames(node_layout))) { 20 | node_layout$x_nudge <- x_nudge 21 | } 22 | # if y_nudge not specified use global values 23 | if (!("y_nudge" %in% colnames(node_layout))) { 24 | node_layout$y_nudge <- y_nudge 25 | } 26 | # define corners 27 | plot_nodes <- node_layout %>% 28 | dplyr::mutate(xmin = .data$x - .data$x_nudge, 29 | xmax = .data$x + .data$x_nudge, 30 | ymin = .data$y - .data$y_nudge, 31 | ymax = .data$y + .data$y_nudge) 32 | return(plot_nodes) 33 | } 34 | -------------------------------------------------------------------------------- /R/ggflowchart.R: -------------------------------------------------------------------------------- 1 | #' Generate a flowchart in ggplot2 2 | #' 3 | #' Generates the flowchart 4 | #' 5 | #' @param data Data frame or tibble of edges. Must have two columns, first 6 | #' column are `"from"` node names, second column is `"to"` node names. Node names must be unique. 7 | #' @param node_data Data frame or tibble of node information. If not `NULL`, 8 | #' must have at least one column called "name" for node names to join by. 9 | #' Default `NULL`. 10 | #' @param layout One of `c("tree", "custom")`. If `"tree"` uses the tree layout 11 | #' from {igraph}. If `"custom"`, then `x` and `y` columns must be provided in 12 | #' `node_data` specifying the coordinates of the centre of the boxes. Default 13 | #' `"tree"`. 14 | #' @param fill Fill colour of nodes. Must be a valid colour name or hex 15 | #' code, or the name of a column in node_data (quoted or unquoted). 16 | #' Column names take priority over names of colours. Default `"white"`. 17 | #' @param colour Outline colour of nodes. Must be a valid colour name or hex 18 | #' code. Default `"black"`. 19 | #' @param linewidth Width of node outlines. Default 0.5. 20 | #' @param alpha Transparency of fill colour in nodes. Default 1. 21 | #' @param text_colour Colour of labels in nodes. Must be a valid colour name 22 | #' or hex code, or the name of a column in node_data (quoted or unquoted). 23 | #' Column names take priority over names of colours. Default `"black"`. 24 | #' @param text_size Font size of labels in nodes. Default 3.88. 25 | #' @param family Font family for node labels. Default `"sans"`. 26 | #' @param parse If TRUE, the labels will be parsed into expressions 27 | #' and displayed as described in ?plotmath. Default `FALSE`. 28 | #' @param arrow_colour Colour of arrows. Must be a valid colour name or hex 29 | #' code. Default `"black"`. 30 | #' @param arrow_size Size of arrow head. Default 0.3. 31 | #' @param arrow_linewidth Linewidth of arrow lines. Default 0.5. 32 | #' @param arrow_linetype Linetype of arrow lines. Default `"solid"`. 33 | #' @param arrow_label_fill Fill colour of arrow labels. Default `"white"`. 34 | #' @param x_nudge Distance from centre of edge of node box in x direction. 35 | #' Ignored if `x_nudge` is a column in `node_data`. Default 0.35. 36 | #' @param y_nudge Distance from centre of edge of node box in y direction. 37 | #' Ignored if `y_nudge` is a column in `node_data`. Default 0.25. 38 | #' @param horizontal Boolean specifying if flowchart should go from left to 39 | #' right. Default `FALSE`. 40 | #' @param color Outline colour of nodes - overrides colour. Must be a valid colour name or hex 41 | #' code. Default `NULL`. 42 | #' @param text_color Colour of labels in nodes - overrides text_colour. Must be a valid colour name 43 | #' or hex code. Default `NULL`. 44 | #' @param arrow_color Colour of arrows - overrides arrow_colour. Must be a valid colour name or hex 45 | #' code. Default `NULL`. 46 | #' @importFrom rlang .data 47 | #' @return A ggplot2 object. 48 | #' @export 49 | #' @examples 50 | #' data <- tibble::tibble(from = c("A", "A", "A", "B", "C", "F"), to = c("B", "C", "D", "E", "F", "G")) 51 | #' ggflowchart(data) 52 | ggflowchart <- function(data, 53 | node_data = NULL, 54 | layout = "tree", 55 | fill = "white", 56 | colour = "black", 57 | linewidth = 0.5, 58 | alpha = 1, 59 | text_colour = "black", 60 | text_size = 3.88, 61 | family = "sans", 62 | parse = FALSE, 63 | arrow_colour = "black", 64 | arrow_size = 0.3, 65 | arrow_linewidth = 0.5, 66 | arrow_linetype = "solid", 67 | arrow_label_fill = "white", 68 | x_nudge = 0.35, 69 | y_nudge = 0.25, 70 | horizontal = FALSE, 71 | color = NULL, 72 | text_color = NULL, 73 | arrow_color = NULL) { 74 | # define notin 75 | "%notin%" <- function(x, y) { 76 | !("%in%"(x, y)) 77 | } 78 | # check layout valid 79 | if (layout %notin% c("tree", "custom")) { 80 | stop('Layout must be one of c("tree", "custom").') 81 | } 82 | # check data has non-zero rows 83 | if (nrow(data) < 1) { 84 | stop("data must have at least one row.") 85 | } 86 | # convert arguments 87 | fill <- rlang::ensym(fill) 88 | text_colour <- rlang::ensym(text_colour) 89 | # set colours 90 | if (!is.null(color)) { 91 | colour <- color 92 | } 93 | if (!is.null(rlang::enexpr(text_color))) { 94 | text_colour <- rlang::ensym(text_color) 95 | } 96 | if (!is.null(arrow_color)) { 97 | arrow_colour <- arrow_color 98 | } 99 | # define position of nodes 100 | node_layout <- get_layout( 101 | data = data, 102 | layout = layout, 103 | node_data = node_data 104 | ) 105 | # add edge attributes 106 | if (layout == "custom") { 107 | node_layout <- add_node_attr( 108 | node_layout = node_layout, 109 | node_data = dplyr::select(node_data, -dplyr::any_of(c("x", "y"))) 110 | ) 111 | } else { 112 | node_layout <- add_node_attr( 113 | node_layout = node_layout, 114 | node_data = node_data 115 | ) 116 | } 117 | # define edges of node rectangles 118 | plot_nodes <- get_nodes( 119 | node_layout = node_layout, 120 | x_nudge = x_nudge, 121 | y_nudge = y_nudge 122 | ) 123 | # check if labels exist as a column, 124 | # if not, add it as a duplicate of name 125 | if ("label" %notin% colnames(plot_nodes)) { 126 | plot_nodes <- dplyr::mutate(plot_nodes, label = .data$name) 127 | } 128 | # define arrows 129 | plot_edges <- get_edges( 130 | data = data, 131 | plot_nodes = plot_nodes, 132 | node_layout = node_layout 133 | ) 134 | # define edge labels 135 | edge_labels <- get_edge_labels( 136 | data = data, 137 | plot_edges = plot_edges 138 | ) 139 | # create the flowchart 140 | p <- ggplot2::ggplot() 141 | # add nodes 142 | if (as.character(fill) %in% colnames(node_data)) { 143 | p <- p + 144 | ggplot2::geom_rect( 145 | data = plot_nodes, 146 | mapping = ggplot2::aes( 147 | xmin = .data$xmin, 148 | ymin = .data$ymin, 149 | xmax = .data$xmax, 150 | ymax = .data$ymax, 151 | fill = !!fill 152 | ), 153 | alpha = alpha, 154 | colour = colour, 155 | linewidth = linewidth 156 | ) 157 | } else { 158 | p <- p + 159 | ggplot2::geom_rect( 160 | data = plot_nodes, 161 | mapping = ggplot2::aes( 162 | xmin = .data$xmin, 163 | ymin = .data$ymin, 164 | xmax = .data$xmax, 165 | ymax = .data$ymax 166 | ), 167 | alpha = alpha, 168 | colour = colour, 169 | fill = as.character(fill), 170 | linewidth = linewidth 171 | ) 172 | } 173 | # add text 174 | if (as.character(text_colour) %in% colnames(node_data)) { 175 | p <- p + 176 | ggplot2::geom_text( 177 | data = plot_nodes, 178 | mapping = ggplot2::aes( 179 | x = .data$x, 180 | y = .data$y, 181 | label = .data$label, 182 | colour = !!text_colour 183 | ), 184 | family = family, 185 | size = text_size, 186 | parse = parse 187 | ) 188 | } else { 189 | p <- p + 190 | ggplot2::geom_text( 191 | data = plot_nodes, 192 | mapping = ggplot2::aes( 193 | x = .data$x, 194 | y = .data$y, 195 | label = .data$label 196 | ), 197 | family = family, 198 | size = text_size, 199 | parse = parse, 200 | colour = as.character(text_colour) 201 | ) 202 | } 203 | # add arrows 204 | p <- p + 205 | ggplot2::geom_path( 206 | data = plot_edges, 207 | mapping = ggplot2::aes( 208 | x = .data$x, 209 | y = .data$y, 210 | group = .data$id 211 | ), 212 | arrow = ggplot2::arrow( 213 | length = ggplot2::unit(arrow_size, "cm"), 214 | type = "closed" 215 | ), 216 | linewidth = arrow_linewidth, 217 | linetype = arrow_linetype, 218 | colour = arrow_colour 219 | ) + 220 | ggplot2::theme_void() + 221 | ggplot2::theme( 222 | plot.margin = ggplot2::unit(c(0.5, 0.5, 0.5, 0.5), unit = "cm") 223 | ) 224 | # add arrow edge labels 225 | if (!is.null(edge_labels)) { 226 | p <- p + 227 | ggplot2::geom_label( 228 | data = edge_labels, 229 | mapping = ggplot2::aes( 230 | x = .data$text_x, 231 | y = .data$text_y, 232 | label = .data$label 233 | ), 234 | fill = arrow_label_fill, 235 | family = family, 236 | size = text_size 237 | ) 238 | } 239 | # check if horizontal 240 | if (horizontal == TRUE) { 241 | p <- p + 242 | ggplot2::coord_flip() + 243 | ggplot2::scale_y_reverse() 244 | } 245 | return(p) 246 | } 247 | -------------------------------------------------------------------------------- /R/globalVariables.R: -------------------------------------------------------------------------------- 1 | utils::globalVariables("%>%") 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![R-CMD-check](https://github.com/nrennie/ggflowchart/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/nrennie/ggflowchart/actions/workflows/R-CMD-check.yaml) 3 | [![codecov](https://codecov.io/gh/nrennie/ggflowchart/branch/main/graph/badge.svg)](https://app.codecov.io/gh/nrennie/ggflowchart?branch=main) 4 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/ggflowchart)](https://cran.r-project.org/package=ggflowchart) 5 | 6 | 7 | # {ggflowchart} 8 | 9 | {ggflowchart} is an R package for producing flowcharts using {ggplot2}. 10 | 11 | ## Installation 12 | 13 | Install the package from CRAN using: 14 | 15 | ```r 16 | install.packages("ggflowchart") 17 | ``` 18 | or install the development version from GitHub: 19 | 20 | ```r 21 | remotes::install_github("nrennie/ggflowchart") 22 | ``` 23 | 24 | ## Usage 25 | 26 | The idea of {ggflowchart} is to create simple flowcharts with minimal effort. Currently, all flowcharts are constructed using the `ggflowchart()` function. For the most basic flowchart, it takes as input a data frame containing (at least) two columns for the start and end points of the edges in the flowchart. 27 | 28 | ```r 29 | data <- tibble::tibble(from = c("A", "A", "A", "B", "C", "F"), 30 | to = c("B", "C", "D", "E", "F", "G")) 31 | ``` 32 | The flowchart is then created using `ggflowchart()`. 33 | 34 | ```r 35 | ggflowchart(data) 36 | ``` 37 | 38 | ![](man/figures/README-minimal.png) 39 | 40 | See [vignettes](https://nrennie.github.io/ggflowchart/articles/) for further examples of usage. 41 | 42 | ## Aesthetic mappings 43 | 44 | Some arguments can be mapped to aesthetics and some cannot. Arguments that can currently be mapped to aesthetics: 45 | 46 | * `fill`: must be a valid colour, or the name of a column in `node_data` if mapping to a variable. Fill colours can be changed with e.g. `scale_fill_manual()`. 47 | * `text_colour` (or `text_color`): must be a valid colour, or the name of a column in `node_data` if mapping to a variable. Colours can be changed with e.g. `scale_colour_manual()`. 48 | 49 | Arguments that *may* be mapped to aesthetics in future: 50 | 51 | * `colour` (or `color`) for the outline colour of nodes. 52 | * `arrow_colour` (or `arrow_color`) 53 | * `arrow_linewidth` 54 | * `arrow_linetype` 55 | * `alpha` 56 | 57 | ## Upcoming features 58 | 59 | Upcoming features that are currently listed as issues being worked on include: 60 | 61 | * Ability to have elliptical (or circular) nodes rather than just rectangles. 62 | 63 | If you have a suggestion of an additional feature, or find a bug, please file an issue on the [GitHub repository](https://github.com/nrennie/ggflowchart/issues). 64 | 65 | ## Contributor guidelines 66 | 67 | If you'd like to contribute to {ggflowchart}, I'd welcome your help. If you're making a PR, please follow the guidelines below, to make the collaboration easier: 68 | 69 | - You have updated the NEWS and version number in DESCRIPTION. 70 | - You have checked that R CMD check passes with no ERRORs or WARNINGs. If there is a NOTE - please outline what it is in the PR. 71 | - You have checked that `lintr::lint_package()` passes. 72 | - You have checked the list of packages in Imports is still in alphabetical order to enable better tracking of dependencies as the package grows. 73 | - You have not used the base R `|>` pipe (we're not quite ready to specify R 4.1 or higher as a dependency yet!). 74 | - If this is a feature request PR (not a bug fix) please make sure it relates to an issue that has not been assigned to someone else (and tag the issue in the PR description). 75 | 76 | If these checks fail, and there is no response from the PR author for 1 month, the PR will be automatically closed. 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://nrennie.github.io/ggflowchart/ 2 | template: 3 | package: nrenniepkgdown 4 | bootstrap: 5 5 | articles: 6 | - title: Examples 7 | navbar: Examples 8 | contents: 9 | - minimal-example 10 | - decision-tree-example 11 | - style-nodes 12 | - layout-options 13 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD check results 2 | There were no ERRORs, or WARNINGs. 3 | 4 | Besides the new submission NOTE, there are two NOTES that are only found on Windows (Server 2022, R-devel 64-bit): 5 | 6 | ``` 7 | * checking for non-standard things in the check directory ... NOTE 8 | Found the following files/directories: 9 | ''NULL'' 10 | ``` 11 | 12 | ``` 13 | * checking for detritus in the temp directory ... NOTE 14 | Found the following files/directories: 15 | 'lastMiKTeXException' 16 | ``` 17 | 18 | A further NOTE is found on Fedora Linux (R-devel, clang, gfortran) and Ubuntu Linux (20.04.1 LTS, R-release, GCC). 19 | 20 | ``` 21 | * checking HTML version of manual ... NOTE 22 | Skipping checking HTML validation: no command 'tidy' found 23 | ``` 24 | 25 | ## Downstream dependencies 26 | There are currently no downstream dependencies for this package. 27 | -------------------------------------------------------------------------------- /ggflowchart.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /man/figures/README-minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrennie/ggflowchart/ff34410c09ce928f64688a2bf68bfc2cb4e37364/man/figures/README-minimal.png -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrennie/ggflowchart/ff34410c09ce928f64688a2bf68bfc2cb4e37364/man/figures/logo.png -------------------------------------------------------------------------------- /man/ggflowchart.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggflowchart.R 3 | \name{ggflowchart} 4 | \alias{ggflowchart} 5 | \title{Generate a flowchart in ggplot2} 6 | \usage{ 7 | ggflowchart( 8 | data, 9 | node_data = NULL, 10 | layout = "tree", 11 | fill = "white", 12 | colour = "black", 13 | linewidth = 0.5, 14 | alpha = 1, 15 | text_colour = "black", 16 | text_size = 3.88, 17 | family = "sans", 18 | parse = FALSE, 19 | arrow_colour = "black", 20 | arrow_size = 0.3, 21 | arrow_linewidth = 0.5, 22 | arrow_linetype = "solid", 23 | arrow_label_fill = "white", 24 | x_nudge = 0.35, 25 | y_nudge = 0.25, 26 | horizontal = FALSE, 27 | color = NULL, 28 | text_color = NULL, 29 | arrow_color = NULL 30 | ) 31 | } 32 | \arguments{ 33 | \item{data}{Data frame or tibble of edges. Must have two columns, first 34 | column are \code{"from"} node names, second column is \code{"to"} node names. Node names must be unique.} 35 | 36 | \item{node_data}{Data frame or tibble of node information. If not \code{NULL}, 37 | must have at least one column called "name" for node names to join by. 38 | Default \code{NULL}.} 39 | 40 | \item{layout}{One of \code{c("tree", "custom")}. If \code{"tree"} uses the tree layout 41 | from {igraph}. If \code{"custom"}, then \code{x} and \code{y} columns must be provided in 42 | \code{node_data} specifying the coordinates of the centre of the boxes. Default 43 | \code{"tree"}.} 44 | 45 | \item{fill}{Fill colour of nodes. Must be a valid colour name or hex 46 | code, or the name of a column in node_data (quoted or unquoted). 47 | Column names take priority over names of colours. Default \code{"white"}.} 48 | 49 | \item{colour}{Outline colour of nodes. Must be a valid colour name or hex 50 | code. Default \code{"black"}.} 51 | 52 | \item{linewidth}{Width of node outlines. Default 0.5.} 53 | 54 | \item{alpha}{Transparency of fill colour in nodes. Default 1.} 55 | 56 | \item{text_colour}{Colour of labels in nodes. Must be a valid colour name 57 | or hex code, or the name of a column in node_data (quoted or unquoted). 58 | Column names take priority over names of colours. Default \code{"black"}.} 59 | 60 | \item{text_size}{Font size of labels in nodes. Default 3.88.} 61 | 62 | \item{family}{Font family for node labels. Default \code{"sans"}.} 63 | 64 | \item{parse}{If TRUE, the labels will be parsed into expressions 65 | and displayed as described in ?plotmath. Default \code{FALSE}.} 66 | 67 | \item{arrow_colour}{Colour of arrows. Must be a valid colour name or hex 68 | code. Default \code{"black"}.} 69 | 70 | \item{arrow_size}{Size of arrow head. Default 0.3.} 71 | 72 | \item{arrow_linewidth}{Linewidth of arrow lines. Default 0.5.} 73 | 74 | \item{arrow_linetype}{Linetype of arrow lines. Default \code{"solid"}.} 75 | 76 | \item{arrow_label_fill}{Fill colour of arrow labels. Default \code{"white"}.} 77 | 78 | \item{x_nudge}{Distance from centre of edge of node box in x direction. 79 | Ignored if \code{x_nudge} is a column in \code{node_data}. Default 0.35.} 80 | 81 | \item{y_nudge}{Distance from centre of edge of node box in y direction. 82 | Ignored if \code{y_nudge} is a column in \code{node_data}. Default 0.25.} 83 | 84 | \item{horizontal}{Boolean specifying if flowchart should go from left to 85 | right. Default \code{FALSE}.} 86 | 87 | \item{color}{Outline colour of nodes - overrides colour. Must be a valid colour name or hex 88 | code. Default \code{NULL}.} 89 | 90 | \item{text_color}{Colour of labels in nodes - overrides text_colour. Must be a valid colour name 91 | or hex code. Default \code{NULL}.} 92 | 93 | \item{arrow_color}{Colour of arrows - overrides arrow_colour. Must be a valid colour name or hex 94 | code. Default \code{NULL}.} 95 | } 96 | \value{ 97 | A ggplot2 object. 98 | } 99 | \description{ 100 | Generates the flowchart 101 | } 102 | \examples{ 103 | data <- tibble::tibble(from = c("A", "A", "A", "B", "C", "F"), to = c("B", "C", "D", "E", "F", "G")) 104 | ggflowchart(data) 105 | } 106 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(ggflowchart) 11 | 12 | test_check("ggflowchart") 13 | -------------------------------------------------------------------------------- /tests/testthat/test-column-tests.R: -------------------------------------------------------------------------------- 1 | test_that("Single column fails", { 2 | expect_error(ggflowchart(data = data.frame(from = c(1, 2, 3)))) 3 | }) 4 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/decision-tree-example.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Decision Tree Example" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{decision-tree-example} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, warning=FALSE, message=FALSE} 18 | library(ggflowchart) 19 | library(dplyr) 20 | ``` 21 | 22 | # Define input data 23 | 24 | The main input data is a two column data frame or tibble of edges. It must have two columns, with the first column containing node names for the "from" nodes, and the second containing node names for the "to" nodes. Node names must be unique. 25 | 26 | ```{r data} 27 | goldilocks <- tibble::tibble( 28 | from = c( 29 | "Goldilocks", 30 | "Porridge", "Porridge", "Porridge", 31 | "Just right", 32 | "Chairs", "Chairs", "Chairs", 33 | "Just right2", 34 | "Beds", "Beds", "Beds", 35 | "Just right3" 36 | ), 37 | to = c( 38 | "Porridge", 39 | "Too cold", "Too hot", "Just right", 40 | "Chairs", 41 | "Still too big", "Too big", "Just right2", 42 | "Beds", 43 | "Too soft", "Too hard", "Just right3", 44 | "Bears!" 45 | ) 46 | ) 47 | ``` 48 | 49 | Optionally, also define additional node attributes. 50 | 51 | ```{r node-data} 52 | node_data <- tibble::tibble(name = c( 53 | "Goldilocks", "Porridge", "Just right", "Chairs", 54 | "Just right2", "Beds", "Just right3", "Too cold", 55 | "Too hot", "Still too big", "Too big", "Too soft", 56 | "Too hard", "Bears!" 57 | )) %>% 58 | dplyr::mutate(label = gsub("\\d+$", "", name)) 59 | ``` 60 | 61 | Create the flowchart using the `ggflowchart()` function. 62 | 63 | ```{r flowchart} 64 | ggflowchart(goldilocks, node_data) 65 | ``` 66 | 67 | We can also specify a column in `node_data` we want to colour the nodes based on: 68 | 69 | ```{r flowchart-fill-columm} 70 | node_data <- node_data %>% 71 | dplyr::mutate( 72 | type = c( 73 | "Character", "Question", "Answer", 74 | "Question", "Answer", "Question", 75 | "Answer", "Answer", "Answer", 76 | "Answer", "Answer", "Answer", 77 | "Answer", "Character" 78 | ) 79 | ) 80 | ``` 81 | 82 | and pass this to the `fill` argument: 83 | 84 | ```{r flowchart-fill} 85 | ggflowchart(goldilocks, node_data, fill = type) 86 | ``` 87 | 88 | Note that the column name can be passed either quoted or unquoted e.g., `ggflowchart(goldilocks, node_data, fill = "type")` will also work. If single colour is passed instead (in quotes e.g., `"blue"`) and this is also the name of a column, it will assume you want to use the values in the column. If that isn't the case, please rename the column. 89 | 90 | Optionally, add some styling using the {ggplot2} `theme()` function and title using the `labs()` function. 91 | 92 | ```{r flowchart-style, warning=FALSE, message=FALSE} 93 | # add additional packages 94 | library(ggplot2) 95 | library(rcartocolor) 96 | 97 | # make flowchart 98 | fchart <- ggflowchart(goldilocks, 99 | node_data, 100 | fill = type, 101 | family = "serif", 102 | colour = "#585c45", 103 | text_colour = "#585c45", 104 | arrow_colour = "#585c45", 105 | arrow_size = 0.15, 106 | text_size = 2.5, 107 | alpha = 0.4, 108 | x_nudge = 0.45, 109 | y_nudge = 0.35 110 | ) 111 | 112 | # customise theme 113 | fchart + 114 | scale_x_reverse() + 115 | scale_fill_carto_d(palette = "Antique") + 116 | labs( 117 | title = "The Goldilocks Decision Tree", 118 | caption = "Data: Robert Southey. Goldilocks and the Three Bears. 1837." 119 | ) + 120 | theme( 121 | legend.position = "none", 122 | plot.background = element_rect( 123 | colour = "#f2e4c1", 124 | fill = "#f2e4c1" 125 | ), 126 | plot.title = element_text( 127 | size = 14, 128 | hjust = 0, 129 | face = "bold", 130 | family = "serif", 131 | colour = "#585c45" 132 | ), 133 | plot.caption = element_text( 134 | size = 8, 135 | hjust = 0, 136 | lineheight = 0.5, 137 | face = "bold", 138 | family = "serif", 139 | colour = "#585c45" 140 | ), 141 | plot.margin = margin(5, 5, 5, 5) 142 | ) 143 | ``` 144 | -------------------------------------------------------------------------------- /vignettes/layout-options.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Layout Options" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{layout-options} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, warning=FALSE} 18 | library(ggflowchart) 19 | ``` 20 | 21 | Create a tibble (or data frame) with each row representing an edge. 22 | 23 | ```{r data} 24 | data <- tibble::tribble( 25 | ~from, ~to, 26 | "A", "B", 27 | "B", "C" 28 | ) 29 | ``` 30 | 31 | The default layout option, `"tree"` uses the tree layout from {igraph}. 32 | 33 | ```{r tree} 34 | ggflowchart(data, layout = "tree") 35 | ``` 36 | 37 | If you'd prefer a horizontal, left-to-right, flowchart simply set `horizontal = TRUE`: 38 | 39 | ```{r horizontal} 40 | ggflowchart(data, layout = "tree", horizontal = TRUE) 41 | ``` 42 | 43 | If you want more control over where the nodes are positioned, you can specify `layout = "custom"`. If you choose this option, you **must** have a column named `x` and a column named `y` in `node_data` (alongside `name`) specifying the coordinates of the centre of each node. 44 | 45 | ```{r custom} 46 | node_data <- tibble::tribble( 47 | ~name, ~x, ~y, 48 | "A", 1, 3, 49 | "B", 2, 2, 50 | "C", 3, 1 51 | ) 52 | ggflowchart(data, node_data = node_data, layout = "custom") 53 | ``` 54 | 55 | This means you can also create layouts using other functions or packages. For example, you could create a custom layout using `igraph::layout_nicely()` and pass it into `node_data`: 56 | 57 | ```{r igraph, warning=FALSE, message=FALSE} 58 | library(dplyr) 59 | 60 | # generate edges 61 | data <- tibble::tribble( 62 | ~from, ~to, 63 | "A", "B", 64 | "A", "C", 65 | "A", "D" 66 | ) 67 | 68 | # use a different layout from {igraph} 69 | g <- igraph::graph_from_data_frame( 70 | select(data, c(from, to)), 71 | directed = TRUE 72 | ) 73 | coords <- igraph::layout_as_star(g) 74 | colnames(coords) <- c("x", "y") 75 | node_data <- tibble::as_tibble(coords) %>% 76 | mutate(name = igraph::vertex_attr(g, "name")) 77 | 78 | # create flowchart 79 | ggflowchart(data, node_data = node_data, layout = "custom") 80 | ``` 81 | 82 | Some of these alternative layouts (especially ones from {igraph}), may eventually become additional layout options within {ggflowchart}. Note that the placement of the arrows may work better with some layout options than others. 83 | -------------------------------------------------------------------------------- /vignettes/minimal-example.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Minimal Example" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{minimal-example} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, warning=FALSE} 18 | library(ggflowchart) 19 | ``` 20 | 21 | Create a tibble (or data frame) with each row representing an edge. 22 | 23 | ```{r data} 24 | data <- tibble::tibble( 25 | from = c("A", "A", "A", "B", "C", "F"), 26 | to = c("B", "C", "D", "E", "F", "G") 27 | ) 28 | ``` 29 | 30 | Create the flowchart using the `ggflowchart()` function. 31 | 32 | ```{r flowchart} 33 | ggflowchart(data) 34 | ``` 35 | 36 | Change the default styling. 37 | 38 | ```{r flowchart-style} 39 | ggflowchart(data, 40 | colour = "red", 41 | text_colour = "red", 42 | arrow_colour = "red", 43 | family = "serif", 44 | x_nudge = 0.25 45 | ) 46 | ``` 47 | 48 | Add labels to the arrows. 49 | 50 | ```{r flowchart-arrow-label} 51 | data <- tibble::tibble( 52 | from = c("A", "A", "A", "B", "C", "F"), 53 | to = c("B", "C", "D", "E", "F", "G"), 54 | label = c("Yes", "No", NA, "Yes", NA, NA) 55 | ) 56 | ggflowchart(data) 57 | ``` 58 | -------------------------------------------------------------------------------- /vignettes/style-nodes.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Styling Flowchart Nodes" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{style-nodes} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, warning=FALSE} 18 | library(ggflowchart) 19 | ``` 20 | 21 | Create a tibble (or data frame) with each row representing an edge. 22 | 23 | ```{r data} 24 | data <- tibble::tribble( 25 | ~from, ~to, 26 | "A", "B", 27 | "B", "C" 28 | ) 29 | ``` 30 | 31 | Vary the widths of the nodes by adding a column called `x_nudge` to the `node_data`: 32 | 33 | ```{r node-width} 34 | node_data <- tibble::tribble( 35 | ~name, ~x_nudge, 36 | "A", 0.3, 37 | "B", 0.4, 38 | "C", 0.5 39 | ) 40 | ggflowchart(data, node_data) 41 | ``` 42 | 43 | Similarly, the height of the boxes can vary by adding a column called `y_nudge`. If neither `x_nudge` or `y_nudge` columns are present in `node_data`, then the values specified in the `x_nudge` or `y_nudge` arguments of `ggflowchart()` are used for all boxes. 44 | 45 | Vary the fill colours of the nodes by specifying a column in `node_data` to colour by: 46 | 47 | ```{r node-fill} 48 | node_data <- tibble::tribble( 49 | ~name, ~type, 50 | "A", "Yes", 51 | "B", "Yes", 52 | "C", "No" 53 | ) 54 | ggflowchart(data, node_data, fill = type) 55 | ``` 56 | 57 | You can use existing `scale_fill_*` functions to change the colours: 58 | 59 | ```{r node-fill-change} 60 | ggflowchart(data, node_data, fill = type) + 61 | ggplot2::scale_fill_brewer(palette = "Dark2") 62 | ``` 63 | 64 | Alternatively, if you want them all to be the same colour you can specify a single colour instead: 65 | 66 | ```{r node-fill-one} 67 | ggflowchart(data, node_data, fill = "lightblue") 68 | ``` 69 | 70 | Note that quoting column names also works, e.g. `ggflowchart(data, node_data, fill = "type")` gives the same output as `ggflowchart(data, node_data, fill = type)`. This means that if you have a column in `node_data` called `"lightblue"` and you want to colour all of the nodes light blue instead of using the values in the `"lightblue"` column, you would need to rename the column. 71 | 72 | You can do something very similar to change the colour of the text in the nodes: 73 | 74 | ```{r node-text} 75 | ggflowchart(data, node_data, text_colour = type) + 76 | ggplot2::theme(legend.position = "none") 77 | ``` 78 | 79 | You will soon also be able to change the shape of nodes - keep an eye on the development version for this feature! 80 | --------------------------------------------------------------------------------