├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS.md ├── R ├── RcppExports.R ├── circleGraphLayout.R ├── circleProgressiveLayout.R ├── circleRemoveOverlaps.R ├── circleRepelLayout.R ├── circleVertices.R ├── data.R └── packcircles.R ├── README.md ├── cran-comments.md ├── data-raw ├── input2.txt └── progressive_example_data.R ├── data └── bacteria.rda ├── man ├── bacteria.Rd ├── circleGraphLayout.Rd ├── circleLayout.Rd ├── circleLayoutVertices.Rd ├── circlePlotData.Rd ├── circleProgressiveLayout.Rd ├── circleRemoveOverlaps.Rd ├── circleRepelLayout.Rd ├── circleVertices.Rd └── packcircles-package.Rd ├── packcircles.Rproj ├── src ├── RcppExports.cpp ├── init.c ├── packcircles.cpp ├── pads_circle_pack.cpp ├── pmenzel_circle_pack.cpp └── select_non_overlapping.cpp └── vignettes ├── graph_example.png ├── graph_packing.Rmd ├── intro.Rmd └── progressive_packing.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^cran\-comments\.md$ 5 | ^data-raw$ 6 | ^CRAN-RELEASE$ 7 | ^CRAN-SUBMISSION$ 8 | ^\.github$ 9 | ^doc$ 10 | ^Meta$ 11 | ^revdep$ 12 | -------------------------------------------------------------------------------- /.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.yaml 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | R-CMD-check: 15 | runs-on: ${{ matrix.config.os }} 16 | 17 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | config: 23 | - {os: macos-latest, r: 'release'} 24 | - {os: windows-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 26 | - {os: ubuntu-latest, r: 'release'} 27 | - {os: ubuntu-latest, r: 'oldrel-1'} 28 | 29 | env: 30 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 31 | R_KEEP_PKG_SOURCE: yes 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - uses: r-lib/actions/setup-pandoc@v2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | http-user-agent: ${{ matrix.config.http-user-agent }} 42 | use-public-rspm: true 43 | 44 | - uses: r-lib/actions/setup-r-dependencies@v2 45 | with: 46 | extra-packages: any::rcmdcheck 47 | needs: check 48 | 49 | - uses: r-lib/actions/check-r-package@v2 50 | with: 51 | upload-snapshots: true 52 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | src/*.o 5 | src/*.so 6 | src/*.dll 7 | inst/doc 8 | # vim backup files 9 | *~ 10 | revdep 11 | CRAN-SUBMISSION 12 | /doc/ 13 | /Meta/ 14 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: packcircles 2 | Type: Package 3 | Version: 0.3.7 4 | Title: Circle Packing 5 | Description: Algorithms to find arrangements of non-overlapping circles. 6 | Date: 2024-11-21 7 | Authors@R: c( 8 | person("Michael", "Bedward", role = c("aut", "cre"), email = "michael.bedward@gmail.com"), 9 | person("David", "Eppstein", role = "aut", email = "david.eppstein@gmail.com", 10 | comment = "Author of Python code for graph-based circle packing ported to C++ for this package"), 11 | person("Peter", "Menzel", role = "aut", email = "pmenzel@gmail.com", 12 | comment = "Author of C code for progressive circle packing ported to C++ for this package") 13 | ) 14 | URL: https://github.com/mbedward/packcircles 15 | BugReports: https://github.com/mbedward/packcircles/issues 16 | Depends: R (>= 3.2) 17 | Imports: Rcpp (>= 1.0.0), 18 | checkmate 19 | Suggests: ggiraph (>= 0.8.4), 20 | ggplot2, 21 | knitr, 22 | rmarkdown, 23 | lpSolve 24 | VignetteBuilder: knitr 25 | LinkingTo: Rcpp (>= 1.0.0) 26 | LazyData: true 27 | License: MIT + file LICENSE 28 | RoxygenNote: 7.3.2 29 | Encoding: UTF-8 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2015-2017 2 | COPYRIGHT HOLDER: Michael Bedward -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(circleGraphLayout) 4 | export(circleLayout) 5 | export(circleLayoutVertices) 6 | export(circlePlotData) 7 | export(circleProgressiveLayout) 8 | export(circleRemoveOverlaps) 9 | export(circleRepelLayout) 10 | export(circleVertices) 11 | importFrom(Rcpp,sourceCpp) 12 | importFrom(stats,rnorm) 13 | useDynLib(packcircles, .registration = TRUE) 14 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # packcircles 0.3.7 (2024-11-21) 2 | 3 | * Fix: Updated progressive packing vignette example to use ggiraph::girafe() 4 | function and fix CRAN warning (thanks to David Gohel). 5 | 6 | * Use the checkmate package to check arguments to functions. 7 | 8 | # packcircles 0.3.6 (2023-09-05) 9 | 10 | * Fix: Minor change to conform to CRAN package doc conventions. 11 | 12 | # packcircles 0.3.5 (2022-11-23) 13 | 14 | * Fix: Updated ggiraph dependency to version 0.8.4 to fix error when building 15 | one of the vignettes. 16 | 17 | # packcircles 0.3.4 (2020-12-10) 18 | 19 | * Fix: Minor updates to comply with R extensions convention for using packages 20 | listed under 'Suggests'. 21 | 22 | # packcircles 0.3.3 (2018-09-18) 23 | 24 | * Fix: Minor updates to comply with the planned introduction of 25 | STRICT_R_HEADERS in Rcpp. 26 | 27 | # packcircles 0.3.2 (2018-04-28) 28 | 29 | * Minor change in `pads_circle_pack.cpp` (graph-based circle packing) 30 | to address a CRAN warning. 31 | 32 | # packcircles 0.3.1 (2018-01-09) 33 | 34 | * Fixed a problem where function circleProgressiveLayout sometimes returned 35 | a layout with overlapping circles. Thanks to Jeffrey Lewis for reporting 36 | and Peter Menzel for code changes. 37 | 38 | # packcircles 0.3.0 (2017-11-24) 39 | 40 | * Feature: Added `circleRemoveOverlaps` which takes a set of circles and 41 | identifies a subset of non-overlapping circles. The function can also be set 42 | to allow some degree of overlap or require space between circles. The 43 | function uses either a fast heuristic algorithm (several choices) or linear 44 | programming (requires package lpSolve). 45 | 46 | * Fix: Replaced use of `sprintf` with `snprintf` in `pads_circle_pack.cpp` 47 | (graph-based circle packing) to address CRAN warning. 48 | 49 | # packcircles 0.2.0 (2017-04-05) 50 | 51 | * Feature: Added `circleProgressiveLayout` which deterministically places each 52 | circle in turn such that it is externally tangent to two previously placed 53 | circles while avoiding overlaps. 54 | 55 | * Feature: Replaced `circleLayout` with a new function `circleRepelLayout`. 56 | The original function is retained for backwards compatibility but is 57 | deprecated and will be removed in a future release. 58 | __Important note:__ the new function accepts circles sizes as either areas or 59 | radii. The default is area, unlike `circleLayout` which assumed sizes were 60 | radii. The type of size value can be specified using the `sizetype` argument. 61 | 62 | * Feature: Replaced `circlePlotData` with a new function `circleLayoutVertices`. 63 | The original function is retained for backwards compatibility but is 64 | deprecated and will be removed in a future release. The new function has a 65 | `sizetype` argument to specify whether the input circle sizes are areas or 66 | radii. By default, radius is assumed as this matches the output of the layout 67 | functions but I realize it's a bit confusing. 68 | 69 | * Internal: Removed `gridExtra` from suggested packages. 70 | 71 | * Internal: Added `ggiraph` (used for vignette). 72 | 73 | * Docs: Re-wrote the introductory vignette and added a new vignette for 74 | `circleProgressiveLayout`. 75 | 76 | * Docs: Edited function docs and examples. 77 | 78 | * Internal: Added file for native routine registration as now required by CRAN. 79 | -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | iterate_layout <- function(xyr, weights, xmin, xmax, ymin, ymax, maxiter, wrap) { 5 | .Call(`_packcircles_iterate_layout`, xyr, weights, xmin, xmax, ymin, ymax, maxiter, wrap) 6 | } 7 | 8 | doCirclePack <- function(internalList, externalDF) { 9 | .Call(`_packcircles_doCirclePack`, internalList, externalDF) 10 | } 11 | 12 | do_progressive_layout <- function(radii) { 13 | .Call(`_packcircles_do_progressive_layout`, radii) 14 | } 15 | 16 | select_non_overlapping <- function(xyr, tolerance, ordering) { 17 | .Call(`_packcircles_select_non_overlapping`, xyr, tolerance, ordering) 18 | } 19 | 20 | -------------------------------------------------------------------------------- /R/circleGraphLayout.R: -------------------------------------------------------------------------------- 1 | #' Find an arrangement of circles satisfying a graph of adjacencies 2 | #' 3 | #' Attempts to derive an arrangement of circles satisfying prior conditions for 4 | #' size and adjacency. Unlike the \code{\link{circleRepelLayout}} function, this 5 | #' is a deterministic algorithm. Circles are classified as either internal or 6 | #' external. Viewing the pattern of adjacencies as a triangulated mesh, external 7 | #' circles are those on the boundary. In the version of the algorithm 8 | #' implemented here, the radii of external circles are provided as inputs, while 9 | #' the radii of internal circles are derived as part of the output arrangement. 10 | #' 11 | #' The \code{internal} argument specifies circle adjacencies (ie. tangencies). 12 | #' The format is an concise representation of graph edges, and consists of a 13 | #' list of vectors: one per internal circle. In each vector the first element is 14 | #' the ID value of the internal circle and the remaining values are IDs of 15 | #' neighbouring circles, which may be either internal or external. 16 | #' 17 | #' The \code{external} argument is a data.frame which specifies the radii of 18 | #' external circles. Internal circle radii should not be specified as they are 19 | #' derived as part of the fitting algorithm. The function will issue an error if 20 | #' any internal circle IDs are present in the \code{external} data. 21 | #' 22 | #' @return A data.frame with columns for circle ID, centre X and Y ordinate, and 23 | #' radius. 24 | #' 25 | #' @note Please treat this function as experimental. 26 | #' 27 | #' @references C.R. Collins & K. Stephenson (2003) An algorithm for circle 28 | #' packing. Computational Geometry Theory and Applications 25:233-256. 29 | #' 30 | #' @param internal A list of vectors of circle ID values where, in each vector, 31 | #' the first element is the ID of an internal circle and the remaining 32 | #' elements are the IDs of that circle's neighbours arranged as a cycle. The 33 | #' cycle may be clockwise or anti-clockwise but the same ordering must be used 34 | #' for all vectors. 35 | #' 36 | #' @param external A data.frame or matrix of external circle radii, with circle 37 | #' IDs in the first column and radii in the second column. 38 | #' 39 | #' @return The output arrangement as a data.frame with columns for circle ID, 40 | #' centre X and Y ordinates, and radius. For external circles the radius will 41 | #' equal input values. 42 | #' 43 | #' @examples 44 | #' ## Simple example with two internal circles surrounded by 45 | #' ## four external circles. Internal circle IDs are 1 and 2. 46 | #' internal <- list( c(1, 3, 4, 5), c(2, 3, 4, 6) ) 47 | #' 48 | #' ## Uniform radius for external circles 49 | #' external <- data.frame(id=3:6, radius=1.0) 50 | #' 51 | #' ## Generate the circle packing 52 | #' packing <- circleGraphLayout(internal, external) 53 | #' 54 | #' @export 55 | #' 56 | circleGraphLayout <- function(internal, external) { 57 | checkmate::assert_list(internal, types = "numeric", any.missing = FALSE, min.len = 1) 58 | 59 | if (is.matrix(external)) external <- as.data.frame(external) 60 | checkmate::assert_data_frame(external, types = "numeric", any.missing = FALSE, ncols = 2) 61 | 62 | doCirclePack(internal, external) 63 | } 64 | -------------------------------------------------------------------------------- /R/circleProgressiveLayout.R: -------------------------------------------------------------------------------- 1 | #' Progressive layout algorithm 2 | #' 3 | #' Arranges a set of circles, which are denoted by their sizes, by consecutively 4 | #' placing each circle externally tangent to two previously placed circles while 5 | #' avoiding overlaps. 6 | #' 7 | #' Based on an algorithm described in the paper: 8 | #' \emph{Visualization of large hierarchical data by circle packing} 9 | #' by Weixin Wang, Hui Wang, Guozhong Dai, and Hongan Wang. Published 10 | #' in \emph{Proceedings of the SIGCHI Conference on Human Factors in Computing Systems}, 11 | #' 2006, pp. 517-520 \doi{10.1145/1124772.1124851} 12 | #' 13 | #' The implementation here was adapted from a version written in C by Peter Menzel: 14 | #' \url{https://github.com/pmenzel/packCircles}. 15 | #' 16 | #' @param x Either a vector of circle sizes, or a matrix or data frame 17 | #' with one column for circle sizes. 18 | #' 19 | #' @param sizecol The index or name of the column in \code{x} for circle sizes. 20 | #' Ignored if \code{x} is a vector. 21 | #' 22 | #' @param sizetype The type of size values: either \code{"area"} (default) 23 | #' or \code{"radius"}. May be abbreviated. 24 | #' 25 | #' @return A data frame with columns: x, y, radius. If any of the input size values 26 | #' were non-positive or missing, the corresponding rows of the output data frame 27 | #' will be filled with \code{NA}s. 28 | #' 29 | #' @examples 30 | #' areas <- sample(c(4, 16, 64), 100, rep = TRUE, prob = c(60, 30, 10)) 31 | #' packing <- circleProgressiveLayout(areas) 32 | #' 33 | #' \dontrun{ 34 | #' 35 | #' # Graph the result with ggplot 36 | #' library(ggplot2) 37 | #' 38 | #' dat.gg <- circleLayoutVertices(packing) 39 | #' 40 | #' ggplot(data = dat.gg, aes(x, y, group = id)) + 41 | #' geom_polygon(colour = "black", fill = "grey90") + 42 | #' coord_equal() + 43 | #' theme_void() 44 | #' 45 | #' } 46 | #' 47 | #' @export 48 | #' 49 | circleProgressiveLayout <- function(x, sizecol = 1, sizetype = c("area", "radius")) { 50 | sizetype = match.arg(sizetype) 51 | 52 | if (is.matrix(x)) { 53 | sizes <- as.numeric(x[, sizecol]) 54 | } 55 | else if (is.data.frame(x)) { 56 | sizes <- as.numeric(x[[sizecol]]) 57 | } 58 | else { 59 | sizes <- as.numeric(x) 60 | } 61 | 62 | if (any(sizes <= 0, na.rm = TRUE)) { 63 | sizes[ sizes <= 0 ] <- NA 64 | } 65 | missing <- is.na(sizes) | is.nan(sizes) 66 | 67 | if (all(missing)) stop("all sizes are missing and/or non-positive") 68 | 69 | if (any(missing)) warning("missing and/or non-positive sizes will be ignored") 70 | 71 | radii <- sizes[ !missing ] 72 | if (sizetype == "area") radii <- sqrt(radii / pi) 73 | 74 | res <- do_progressive_layout(radii) 75 | 76 | if (any(missing)) { 77 | out <- matrix(NA_real_, nrow = length(sizes), ncol = 3) 78 | colnames(out) <- colnames(res) 79 | out[!missing, ] <- as.matrix(res) 80 | as.data.frame(out) 81 | } 82 | else { 83 | res 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /R/circleRemoveOverlaps.R: -------------------------------------------------------------------------------- 1 | #' Filters a set of circles to remove all overlaps 2 | #' 3 | #' Given an initial set of circles, this function identifies a subset of 4 | #' non-overlapping circles using a simple heuristic algorithm. Circle positions 5 | #' remain fixed. 6 | #' 7 | #' The \code{method} argument specifies whether to use the heuristic algorithm or 8 | #' linear programming. The following options select the heuristic algorithm and 9 | #' specify how to choose an overlapping circle to remove at each iteration: 10 | #' \describe{ 11 | #' \item{maxov}{Choose one of the circles with the greatest number of overlaps.} 12 | #' \item{minov}{Choose one of the circles with the least number of overlaps.} 13 | #' \item{largest}{Choose one of the largest circles.} 14 | #' \item{smallest}{Choose one of the smallest circles.} 15 | #' \item{random}{Choose a circle at random.} 16 | #' } 17 | #' At each iteration the number of overlaps is checked for each candidate 18 | #' circle and any non-overlapping circles added to the selected subset. Then a 19 | #' single overlapping circle is chosen, based on the method being used, from 20 | #' among the remainder and marked as rejected. Iterations continue until all 21 | #' circles have been either selected or rejected. The 'maxov' option (default) 22 | #' generally seems to perform best at maximizing the number of circles retained. 23 | #' The other options are provided for comparison and experiment. Beware that 24 | #' some can perform surprisingly poorly, especially 'minov'. 25 | #' 26 | #' Two further options select linear programming: 27 | #' \describe{ 28 | #' \item{lparea}{Maximise the total area of circles in the subset.} 29 | #' \item{lpnum}{Maximise the total number of circles in the subset.} 30 | #' } 31 | #' 32 | #' The `lpSolve` package must be installed to use the linear programming options. 33 | #' These options will find an optimal subset, but for anything other than a small 34 | #' number of initial circles the running time can be prohibitive. 35 | #' 36 | #' 37 | #' @param x A matrix or data frame containing circle x-y centre coordinates 38 | #' and sizes (area or radius). 39 | #' 40 | #' @param xysizecols The integer indices or names of the columns in \code{x} for 41 | #' the centre x-y coordinates and sizes of circles. Default is \code{c(1,2,3)}. 42 | #' 43 | #' @param sizetype The type of size values: either \code{"area"} (default) or 44 | #' \code{"radius"}. May be abbreviated. 45 | #' 46 | #' @param tolerance Controls the amount of overlap allowed. Set to 1 for 47 | #' simple exclusion of overlaps. Values lower than 1 allow more overlap. 48 | #' Values > 1 have the effect of expanding the influence of circles so that 49 | #' more space is required between them. The input value must be > 0. 50 | #' 51 | #' @param method Specifies whether to use linear programming (default) or one of 52 | #' the variants of the heuristic algorithm. Alternatives are: 53 | #' \code{"maxov"}, \code{"minov"}, \code{"largest"}, \code{"smallest"}, 54 | #' \code{"random"}, \code{"lparea"}, \code{"lpnum"}. See Details for further 55 | #' explanation. 56 | #' 57 | #' @return A data frame with centre coordinates and radii of selected circles. 58 | #' 59 | #' @note \emph{This function is experimental} and will almost certainly change before 60 | #' the next package release. In particular, it will probably return something 61 | #' other than a data frame. 62 | #' 63 | #' @export 64 | #' 65 | circleRemoveOverlaps <- function(x, 66 | xysizecols = 1:3, 67 | sizetype = c("area", "radius"), 68 | tolerance = 1.0, 69 | method = c("maxov", "minov", 70 | "largest", "smallest", "random", 71 | "lparea", "lpnum")) { 72 | 73 | sizetype = match.arg(sizetype) 74 | method = match.arg(method) 75 | 76 | # If one of the linear programming options has been specified 77 | # check that package lpSolve is installed. 78 | # 79 | using.lp <- method %in% c("lparea", "lpnum") 80 | if (using.lp) { 81 | if (!requireNamespace("lpSolve", quietly = TRUE)) { 82 | stop("Package lpSolve must be installed to use method='", method, "'", 83 | call. = FALSE) 84 | } 85 | } 86 | 87 | # If using heuristic algorithm, check tolerance argument 88 | if (!using.lp) { 89 | if (tolerance <= 0) stop("tolerance must be positive (default is 1.0)") 90 | } 91 | 92 | if (is.matrix(x)) x <- as.data.frame(x) 93 | checkmate::assert_data_frame(x, min.cols = 3) 94 | 95 | if (is.numeric(xysizecols)) { 96 | checkmate::assert_integer(xysizecols, lower = 1, upper = ncol(x), any.missing = FALSE, len = 3) 97 | } else if (is.character(xysizecols)) { 98 | checkmate::assert_character(xysizecols, any.missing = FALSE, len = 3) 99 | checkmate::assert_subset(xysizecols, colnames(x)) 100 | } 101 | 102 | # Check the input data again to validate types 103 | checkmate::assert_data_frame(x[, xysizecols], types = "numeric") 104 | 105 | xcol <- xysizecols[1] 106 | ycol <- xysizecols[2] 107 | sizecol <- xysizecols[3] 108 | 109 | # get circle sizes and centre coordinates 110 | sizes <- as.numeric(x[[sizecol]]) 111 | xcentres <- as.numeric(x[[xcol]]) 112 | ycentres <- as.numeric(x[[ycol]]) 113 | 114 | if (any(sizes <= 0, na.rm = TRUE)) { 115 | sizes[ sizes <= 0 ] <- NA 116 | } 117 | missing <- is.na(sizes) | is.nan(sizes) 118 | 119 | if (all(missing)) stop("all sizes are missing and/or non-positive") 120 | 121 | if (any(missing)) warning("missing and/or non-positive sizes will be ignored") 122 | 123 | 124 | # convert sizes from area to radii if required 125 | if (sizetype == "area") sizes <- sqrt(sizes / pi) 126 | 127 | xyr <- matrix( c(xcentres, ycentres, sizes), ncol = 3) 128 | colnames(xyr) <- c("x", "y", "radius") 129 | 130 | # Drop any missing data before passing to Rcpp 131 | xyr <- xyr[!missing, , drop=FALSE] 132 | 133 | if (using.lp) { 134 | # Linear programming 135 | selected <- .lp_non_overlapping(xyr, method) 136 | } else { 137 | # Heuristic 138 | selected <- select_non_overlapping(xyr, tolerance, method); 139 | } 140 | 141 | 142 | # Return data frame of selected circles 143 | as.data.frame(xyr[selected, , drop = FALSE]) 144 | } 145 | 146 | 147 | .lp_non_overlapping <- function(xyr, method = c("lparea", "lpnum")) { 148 | method = match.arg(method) 149 | 150 | if (method == "lparea") { 151 | # maximise radius squared 152 | f.obj <- xyr[, "radius"]^2 153 | } 154 | else { 155 | # for lpnum 156 | f.obj <- rep(1, nrow(xyr)) 157 | } 158 | 159 | terms <- .lp_make_terms(xyr) 160 | 161 | res <- lpSolve::lp("max", f.obj, 162 | const.dir = terms$f.dir, 163 | const.rhs = terms$f.rhs, 164 | dense.const = terms$f.con, 165 | scale = 0, 166 | all.bin = TRUE) 167 | 168 | # return selections as logical vector 169 | res$solution > 0 170 | } 171 | 172 | 173 | .lp_make_terms <- function(xyr) { 174 | N <- nrow(xyr) 175 | d <- as.matrix( stats::dist(xyr[, c("x", "y")]) ) 176 | 177 | r <- xyr[, "radius"] 178 | rsums <- outer(r, r, "+") 179 | 180 | overlaps <- 1 * (d < rsums) 181 | numovs <- rowSums(overlaps) 182 | 183 | overlaps[ lower.tri(overlaps) ] <- 0 184 | rs <- rowSums(overlaps) 185 | cons <- lapply(1:N, function(i) { 186 | if (rs[i] > 1) { 187 | ids <- which(overlaps[i, ] == 1) 188 | n <- length(ids) - 1 189 | x <- matrix(1, nrow=2*n, ncol=2) 190 | oddrows <- 1:nrow(x) %% 2 == 1 191 | x[oddrows, 1] <- i 192 | x[!oddrows, 1] <- ids[-1] 193 | x 194 | } 195 | }) 196 | 197 | cons <- do.call(rbind, cons) 198 | cons <- cbind(rep(1:(nrow(cons)/2), each=2), cons) 199 | colnames(cons) <- c("constr.id", "circle", "value") 200 | 201 | f.dir <- rep("<=", nrow(cons)/2) 202 | 203 | # non-overlapping circles 204 | ids <- which(numovs == 1) 205 | n <- length(ids) 206 | 207 | if (n > 0) { 208 | id0 <- max(cons[, "constr.id"]) 209 | 210 | cons <- rbind( 211 | cons, 212 | 213 | cbind(constr.id = (id0+1):(id0 + n), 214 | circle = ids, 215 | value = 1) ) 216 | 217 | f.dir <- c(f.dir, rep("==", n)) 218 | } 219 | 220 | list(f.con = cons, f.dir = f.dir, f.rhs = rep(1, cons[nrow(cons), "constr.id"])) 221 | } 222 | -------------------------------------------------------------------------------- /R/circleRepelLayout.R: -------------------------------------------------------------------------------- 1 | #' Arranges circles by iterative pair-wise repulsion within a bounding rectangle 2 | #' 3 | #' This function takes a set of circles, defined by a data frame of initial 4 | #' centre positions and radii, and uses iterative pair-wise repulsion to try to 5 | #' find a non-overlapping arrangement where all circle centres lie inside a 6 | #' bounding rectangle. If no such arrangement can be found within the specified 7 | #' maximum number of iterations, the last attempt is returned. 8 | #' 9 | #' The algorithm is adapted from a demonstration app written in the Processing 10 | #' language by Sean McCullough (no longer available online). Each circle in the 11 | #' input data is compared to those following it. If two circles overlap, they 12 | #' are moved apart such that the distance moved by each is proportional to the 13 | #' radius of the other, loosely simulating inertia. So when a small circle is 14 | #' overlapped by a larger circle, the small circle moves furthest. This process 15 | #' is repeated until no more movement takes place (acceptable layout) or the 16 | #' maximum number of iterations is reached (layout failure). 17 | #' 18 | #' To avoid edge effects, the bounding rectangle can be treated as a toroid by 19 | #' setting the \code{wrap} argument to \code{TRUE}. With this option, a circle 20 | #' moving outside the bounds re-enters at the opposite side. 21 | #' 22 | #' 23 | #' @param x Either a vector of circle sizes (areas or radii) or a matrix or 24 | #' data frame with a column of sizes and, optionally, columns for initial 25 | #' x-y coordinates of circle centres. 26 | #' 27 | #' @param xlim The bounds in the X direction; either a vector for [xmin, xmax) 28 | #' or a single value interpreted as [0, xmax). Alternatively, omitting this 29 | #' argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 30 | #' vector will result in unbounded movement in the X direction. 31 | #' 32 | #' @param ylim The bounds in the Y direction; either a vector for [ymin, ymax) 33 | #' or a single value interpreted as [0, ymax). Alternatively, omitting this 34 | #' argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 35 | #' vector will result in unbounded movement in the Y direction. 36 | #' 37 | #' @param xysizecols The integer indices or names of the columns in \code{x} 38 | #' for the centre x-y coordinates and sizes of circles. This argument is 39 | #' ignored if \code{x} is a vector. If \code{x} is a matrix or data frame 40 | #' but does not contain initial x-y coordinates, this must be indicated as 41 | #' \code{xysizecols = c(NA, NA, 1)}. 42 | #' 43 | #' @param sizetype The type of size values: either \code{"area"} or \code{"radius"}. 44 | #' May be abbreviated. 45 | #' 46 | #' @param maxiter The maximum number of iterations. 47 | #' 48 | #' @param wrap Whether to treat the bounding rectangle as a toroid (default 49 | #' \code{TRUE}). When this is in effect, a circle leaving the bounds on one 50 | #' side re-enters on the opposite side. 51 | #' 52 | #' @param weights An optional vector of numeric weights (0 to 1 inclusive) to 53 | #' apply to the distance each circle moves during pair-repulsion. A weight of 54 | #' 0 prevents any movement. A weight of 1 gives the default movement distance. 55 | #' A single value can be supplied for uniform weights. A vector with length 56 | #' less than the number of circles will be silently extended by repeating the 57 | #' final value. Any values outside the range [0, 1] will be clamped to 0 or 1. 58 | #' 59 | #' @return A list with components: \describe{ \item{layout}{A 3-column matrix or 60 | #' data frame (centre x, centre y, radius).} \item{niter}{Number of iterations 61 | #' performed.} } 62 | #' 63 | #' @export 64 | #' 65 | circleRepelLayout <- function(x, xlim, ylim, 66 | xysizecols = c(1, 2, 3), 67 | sizetype = c("area", "radius"), 68 | maxiter=1000, wrap=TRUE, weights=1.0) { 69 | 70 | sizetype = match.arg(sizetype) 71 | 72 | xcol <- xysizecols[1] 73 | ycol <- xysizecols[2] 74 | sizecol <- xysizecols[3] 75 | 76 | if (missing(xlim)) xlim <- NULL 77 | xlim <- .checkBounds(xlim) 78 | 79 | if (missing(ylim)) ylim <- NULL 80 | ylim <- .checkBounds(ylim) 81 | 82 | if (is.matrix(x)) x <- as.data.frame(x) 83 | 84 | checkmate::assert_int(maxiter, lower = 1) 85 | checkmate::assert_flag(wrap) 86 | 87 | # get circle sizes and centre coordinates 88 | if (is.data.frame(x)) { 89 | .check_col_index(sizecol, x) 90 | sizes <- as.numeric(x[[sizecol]]) 91 | 92 | if (is.na(xcol)) { 93 | xcentres <- .initial_ordinates(length(sizes), xlim) 94 | } else { 95 | .check_col_index(xcol, x) 96 | xcentres <- as.numeric(x[[xcol]]) 97 | } 98 | 99 | if (is.na(ycol)) { 100 | ycentres <- .initial_ordinates(length(sizes), ylim) 101 | } else { 102 | .check_col_index(ycol, x) 103 | ycentres <- as.numeric(x[[ycol]]) 104 | } 105 | } 106 | else { 107 | # x is a vector of circle sizes 108 | sizes <- as.numeric(x) 109 | xcentres <- .initial_ordinates(length(sizes), xlim) 110 | ycentres <- .initial_ordinates(length(sizes), ylim) 111 | } 112 | 113 | if (any(sizes <= 0, na.rm = TRUE)) { 114 | sizes[ sizes <= 0 ] <- NA 115 | } 116 | missing <- is.na(sizes) | is.nan(sizes) 117 | 118 | if (all(missing)) stop("all sizes are missing and/or non-positive") 119 | 120 | if (any(missing)) warning("missing and/or non-positive sizes will be ignored") 121 | 122 | # convert sizes from area to radii if required 123 | if (sizetype == "area") sizes <- sqrt(sizes / pi) 124 | 125 | # Matrix to pass to Rcpp 126 | xyr <- matrix( c(xcentres, ycentres, sizes), ncol = 3) 127 | colnames(xyr) <- c("x", "y", "radius") 128 | 129 | 130 | if (is.null(weights) || length(weights) == 0) 131 | weights <- rep(1.0, nrow(xyr)) 132 | else { 133 | if (!is.numeric(weights)) 134 | stop("weights must be a numeric vector with values between 0 and 1") 135 | 136 | if (length(weights) < nrow(xyr)) { 137 | i <- length(weights) 138 | weights <- c(weights, rep(weights[i], nrow(xyr) - i)) 139 | } else if (length(weights) > nrow(xyr)) { 140 | weights <- weights[1:nrow(xyr)] 141 | } 142 | 143 | # clamp values to be in the range [0, 1] 144 | weights[ weights < 0 ] <- 0 145 | weights[ weights > 1 ] <- 1 146 | } 147 | 148 | 149 | # Drop any missing data before passing to Rcpp 150 | xyr <- xyr[!missing, , drop=FALSE] 151 | weights <- weights[!missing] 152 | 153 | 154 | # Run Rcpp function which modifies xyr in place 155 | niter = iterate_layout(xyr, weights, xlim[1], xlim[2], ylim[1], ylim[2], maxiter, wrap) 156 | 157 | 158 | # Restore missing data if required 159 | if (any(missing)) { 160 | res <- xyr 161 | xyr <- matrix(NA_real_, nrow = length(sizes), ncol = 3) 162 | colnames(xyr) <- colnames(res) 163 | xyr[!missing, ] <- res 164 | } 165 | 166 | list(layout = as.data.frame(xyr), niter = niter) 167 | } 168 | 169 | 170 | .checkBounds <- function(bounds) { 171 | if (is.null(bounds[1]) || is.na(bounds[1]) || length(bounds) == 0) { 172 | bounds <- c(-Inf, Inf) 173 | } 174 | else { 175 | len <- length(bounds) 176 | arg.name <- deparse(substitute(bounds)) 177 | 178 | if (len == 1) { 179 | bounds <- c(0, bounds) 180 | } else if (len == 2) { 181 | bounds <- c( min(bounds), max(bounds) ) 182 | } else { 183 | stop(arg.name, " has length ", len, "; expected 1 or 2") 184 | } 185 | 186 | if (bounds[1] == bounds[2]) { 187 | stop(arg.name, " min and max should not be equal (", bounds, ")") 188 | } 189 | } 190 | 191 | bounds 192 | } 193 | 194 | 195 | #' @importFrom stats rnorm 196 | #' 197 | .initial_ordinates <- function(n, limits) { 198 | if (anyNA(limits) || length(limits) < 2) 199 | stop("Invalid limits: ", limits) 200 | 201 | infs <- is.infinite(limits) 202 | if (all(infs)) { 203 | limits <- c(0, 1) 204 | } 205 | else if (infs[1]) { 206 | limits[1] <- limits[2] - 1 207 | } 208 | else if (infs[2]) { 209 | limits[2] <- limits[1] + 1 210 | } 211 | 212 | o <- mean(limits) 213 | span <- limits[2] - limits[1] 214 | 215 | o + rnorm(n, 0, span/10) 216 | } 217 | 218 | 219 | #' Arranges circles by iterative pair-wise repulsion within a bounding rectangle 220 | #' 221 | #' This function is deprecated and will be removed in a future release. 222 | #' Please use \code{\link{circleRepelLayout}} instead. 223 | #' 224 | #' @note This function assumes that circle sizes are expressed as radii 225 | #' whereas the default for \code{circleRepelLayout} is area. 226 | #' 227 | #' 228 | #' @param xyr A 3-column matrix or data frame (centre X, centre Y, radius). 229 | #' 230 | #' @param xlim The bounds in the X direction; either a vector for [xmin, xmax) 231 | #' or a single value interpreted as [0, xmax). Alternatively, omitting this 232 | #' argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 233 | #' vector will result in unbounded movement in the X direction. 234 | #' 235 | #' @param ylim The bounds in the Y direction; either a vector for [ymin, ymax) 236 | #' or a single value interpreted as [0, ymax). Alternatively, omitting this 237 | #' argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 238 | #' vector will result in unbounded movement in the Y direction. 239 | #' 240 | #' @param maxiter The maximum number of iterations. 241 | #' 242 | #' @param wrap Whether to treat the bounding rectangle as a toroid (default 243 | #' \code{TRUE}). When this is in effect, a circle leaving the bounds on one 244 | #' side re-enters on the opposite side. 245 | #' 246 | #' @param weights An optional vector of numeric weights (0 to 1 inclusive) to 247 | #' apply to the distance each circle moves during pair-repulsion. A weight of 248 | #' 0 prevents any movement. A weight of 1 gives the default movement distance. 249 | #' A single value can be supplied for uniform weights. A vector with length 250 | #' less than the number of circles will be silently extended by repeating the 251 | #' final value. Any values outside the range [0, 1] will be clamped to 0 or 1. 252 | #' 253 | #' @return A list with components: 254 | #' \describe{ 255 | #' \item{layout}{A 3-column matrix or data.frame (centre x, centre y, radius).} 256 | #' \item{niter}{Number of iterations performed.} 257 | #' } 258 | #' 259 | #' @seealso \code{\link{circleRepelLayout}} 260 | #' 261 | #' @export 262 | #' 263 | circleLayout <- function(xyr, xlim, ylim, 264 | maxiter=1000, wrap=TRUE, weights=1.0) { 265 | 266 | circleRepelLayout(xyr, xlim, ylim, 267 | xysizecols = 1:3, 268 | sizetype = "radius", 269 | maxiter, wrap, weights) 270 | } 271 | 272 | 273 | # Check that a scalar value is a valid integer index or name for a 274 | # data frame column 275 | # 276 | .check_col_index <- function(x, df) { 277 | if (is.numeric(x)) { 278 | checkmate::assert_int(x, lower = 1, upper = ncol(df)) 279 | } else if (is.character(x)) { 280 | checkmate::assert_subset(x, colnames(df)) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /R/circleVertices.R: -------------------------------------------------------------------------------- 1 | #' Generate a set of circle vertices suitable for plotting 2 | #' 3 | #' Given a matrix or data frame for a circle layout, with columns for centre X 4 | #' and Y coordinates and circle sizes, this function generates a data set of 5 | #' vertices which can then be used with ggplot or base graphics functions. If 6 | #' any of the size values in the input data are zero, negative or missing 7 | #' (\code{NA} or \code{NULL}), the corresponding circles will not be generated. 8 | #' This can be useful when displaying alternative subsets of circles. 9 | #' 10 | #' @note \strong{Input sizes are assumed to be radii}. This is slightly 11 | #' confusing because the layout functions \code{circleRepelLayout} and 12 | #' \code{circleProgressiveLayout} treat their input sizes as areas by default. 13 | #' To be safe, you can always set the \code{sizetype} argument explicitly for 14 | #' both this function and layout functions. 15 | #' 16 | #' @param layout A matrix or data frame of circle data (x, y, size). May also 17 | #' contain other columns including an optional identifier column. 18 | #' 19 | #' @param npoints The number of vertices to generate for each circle. 20 | #' 21 | #' @param xysizecols The integer indices or names of columns for the centre X, 22 | #' centre Y and size values. Default is `c(1,2,3)`. 23 | #' 24 | #' @param sizetype The type of size values: either \code{"radius"} (default) or 25 | #' \code{"area"}. May be abbreviated. 26 | #' 27 | #' @param idcol Optional index (integer) or name (character) of an input data 28 | #' column to use as circle identifier values in the \code{id} column of the 29 | #' output data frame. Identifier values may be numeric or character but must 30 | #' be unique. If not provided, the output circle identifiers will be the row 31 | #' numbers of the input circle data. 32 | #' 33 | #' @return A data frame with columns: id, x, y; where id is the unique integer 34 | #' identifier for each circle. If no size values in the input \code{layout} 35 | #' data are positive, a data frame with zero rows will be returned. 36 | #' 37 | #' @seealso \code{\link{circleVertices}} 38 | #' 39 | #' @examples 40 | #' xmax <- 100 41 | #' ymax <- 100 42 | #' rmin <- 10 43 | #' rmax <- 20 44 | #' N <- 20 45 | #' 46 | #' ## Random centre coordinates and radii 47 | #' layout <- data.frame(id = 1:N, 48 | #' x = runif(N, 0, xmax), 49 | #' y = runif(N, 0, ymax), 50 | #' radius = runif(N, rmin, rmax)) 51 | #' 52 | #' ## Get data for circle vertices 53 | #' verts <- circleLayoutVertices(layout, idcol=1, xysizecols=2:4, 54 | #' sizetype = "radius") 55 | #' 56 | #' \dontrun{ 57 | #' library(ggplot2) 58 | #' 59 | #' ## Draw circles annotated with their IDs 60 | #' ggplot() + 61 | #' geom_polygon(data = verts, aes(x, y, group = id), 62 | #' fill = "grey90", 63 | #' colour = "black") + 64 | #' 65 | #' geom_text(data = layout, aes(x, y, label = id)) + 66 | #' 67 | #' coord_equal() + 68 | #' theme_bw() 69 | #' } 70 | #' 71 | #' @export 72 | #' 73 | circleLayoutVertices <- function(layout, npoints=25, xysizecols=1:3, 74 | sizetype = c("radius", "area"), 75 | idcol=NULL) { 76 | 77 | if (is.matrix(layout)) layout <- as.data.frame(layout) 78 | checkmate::assert_data_frame(layout, min.cols = 3) 79 | 80 | checkmate::assert_int(npoints) 81 | 82 | if (is.numeric(xysizecols)) { 83 | checkmate::assert_integer(xysizecols, lower = 1, upper = ncol(layout), any.missing = FALSE, len = 3) 84 | } else if (is.character(xysizecols)) { 85 | checkmate::assert_character(xysizecols, any.missing = FALSE, len = 3) 86 | checkmate::assert_subset(xysizecols, colnames(layout)) 87 | } 88 | 89 | xcol <- xysizecols[1] 90 | ycol <- xysizecols[2] 91 | sizecol <- xysizecols[3] 92 | 93 | checkmate::assert_numeric(layout[[sizecol]], finite = TRUE, all.missing = FALSE) 94 | 95 | 96 | 97 | # Set any negative or missing sizes to zero 98 | ii <- is.null(layout[[sizecol]]) | is.na(layout[[sizecol]]) | layout[[sizecol]] < 0 99 | layout[[sizecol]][ii] <- 0 100 | 101 | # Check if there are any circles with positive sizes. If not, return 102 | # a zero-row data frame 103 | if (isTRUE(all.equal(layout[[sizecol]], rep(0, nrow(layout))))) { 104 | warning("All circles have zero or missing sizes") 105 | return(data.frame(id = integer(0), x = numeric(0), y = numeric(0))) 106 | } 107 | 108 | sizetype <- match.arg(sizetype) 109 | 110 | # Convert sizes to radii if provided as areas 111 | if (sizetype == "area") layout[[sizecol]] <- sqrt(layout[[sizecol]] / pi) 112 | 113 | # Check circle ID values 114 | if (is.null(idcol)) { 115 | circle_ids <- 1:nrow(layout) 116 | } else if (is.numeric(idcol)) { 117 | checkmate::assert_int(idcol, lower = 1, upper = ncol(layout)) 118 | circle_ids <- layout[[idcol]] 119 | } else { 120 | checkmate::assert_string(idcol, min.chars = 1) 121 | checkmate::assert_subset(idcol, colnames(layout)) 122 | circle_ids <- layout[[idcol]] 123 | } 124 | 125 | # ID values should be non-missing and unique 126 | if (any(is.null(circle_ids) | is.na(circle_ids))) { 127 | stop("One or more of the specified ID values for circles are NULL or missing") 128 | } else if (length(circle_ids) > length(unique(circle_ids))) { 129 | stop("Not all of the specified ID values for circles are unique") 130 | } 131 | 132 | verts <- lapply( 133 | 1:nrow(layout), 134 | function(i) { 135 | df <- as.data.frame( 136 | circleVertices(layout[[i, xcol]], 137 | layout[[i, ycol]], 138 | layout[[i, sizecol]], 139 | npoints) ) 140 | 141 | df$id <- circle_ids[i] 142 | df 143 | }) 144 | 145 | do.call(rbind, verts) 146 | } 147 | 148 | 149 | 150 | #' Generate vertex coordinates for a circle 151 | #' 152 | #' Generates vertex coordinates for a circle given its centre coordinates 153 | #' and radius. 154 | #' 155 | #' @param xc Value for centre X ordinate. 156 | #' @param yc Value for centre Y ordinate. 157 | #' @param radius Value for radius. 158 | #' @param npoints The number of distinct vertices required. 159 | #' 160 | #' @return A 2-column matrix of X and Y values. The final row is a copy 161 | #' of the first row to create a closed polygon, so the matrix has 162 | #' \code{npoints + 1} rows. 163 | #' 164 | #' @seealso \code{\link{circleLayoutVertices}} 165 | #' 166 | #' @export 167 | #' 168 | circleVertices <- function(xc, yc, radius, npoints=25) { 169 | checkmate::assert_number(xc, finite = TRUE) 170 | checkmate::assert_number(yc, finite = TRUE) 171 | checkmate::assert_number(radius, finite = TRUE, lower = 0) 172 | 173 | a <- seq(0, 2*pi, length.out = npoints + 1) 174 | x <- xc + radius * cos(a) 175 | y <- yc + radius * sin(a) 176 | m <- cbind("x" = x, "y" = y) 177 | } 178 | 179 | 180 | #' Generate a set of circle vertices suitable for plotting 181 | #' 182 | #' This function is deprecated and will be removed in a future release. 183 | #' Please use \code{circleLayoutVertices} instead. 184 | #' 185 | #' 186 | #' @param layout A matrix or data frame of circle data (x, y, radius). May contain 187 | #' other columns, including an optional ID column. 188 | #' 189 | #' @param npoints The number of vertices to generate for each circle. 190 | #' 191 | #' @param xyr.cols Indices or names of columns for x, y, radius (in that order). 192 | #' Default is columns 1-3. 193 | #' 194 | #' @param id.col Optional index or name of column for circle IDs in output. 195 | #' If not provided, the output circle IDs will be the row numbers of 196 | #' the input circle data. 197 | #' 198 | #' @return A data frame with columns: id, x, y; where id is the unique 199 | #' integer identifier for each circle. 200 | #' 201 | #' @seealso \code{\link{circleLayoutVertices}} \code{\link{circleVertices}} 202 | #' 203 | #' @export 204 | #' 205 | circlePlotData <- function(layout, npoints = 25, xyr.cols = 1:3, id.col = NULL) { 206 | 207 | circleLayoutVertices(layout = layout, 208 | npoints = npoints, 209 | xysizecols = xyr.cols, 210 | sizetype = "radius", 211 | idcol = id.col) 212 | } 213 | 214 | -------------------------------------------------------------------------------- /R/data.R: -------------------------------------------------------------------------------- 1 | #' Abundance of bacteria 2 | #' 3 | #' Names and abundances of bacterial taxa as measured in a study of biofilms. 4 | #' 5 | #' @format ## `bacteria` 6 | #' A data frame with 167 rows and 3 columns: 7 | #' \describe{ 8 | #' \item{value}{measured abundance} 9 | #' \item{colour}{preferred colour for display} 10 | #' \item{label}{taxon name} 11 | #' } 12 | #' 13 | "bacteria" 14 | 15 | -------------------------------------------------------------------------------- /R/packcircles.R: -------------------------------------------------------------------------------- 1 | #' packcircles: Simple algorithms for circle packing 2 | #' 3 | #' This package provides several algorithms to find non-overlapping 4 | #' arrangements of circles: 5 | #' \describe{ 6 | #' \item{circleRepelLayout}{Arranges circles within a bounding rectangle 7 | #' by pairwise repulsion.} 8 | #' \item{circleProgressiveLayout}{Arranges circles in an unbounded area 9 | #' by progressive placement. This is a very efficient algorithm that can 10 | #' handle large numbers of circles.} 11 | #' \item{circleGraphLayout}{Finds an arrangement of circles conforming to 12 | #' a graph specification.} 13 | #' } 14 | #' 15 | #' @importFrom Rcpp sourceCpp 16 | #' @useDynLib packcircles, .registration = TRUE 17 | #' 18 | "_PACKAGE" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![R-CMD-check](https://github.com/mbedward/packcircles/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/mbedward/packcircles/actions/workflows/R-CMD-check.yaml) 3 | 4 | 5 | # packcircles 6 | R package for circle packing. Algorithms to find arrangements of non-overlapping circles 7 | 8 | This package provides functions to find non-overlapping arrangements of circles. 9 | 10 | The function `circleRepelLayout` attempts to arrange a set of circles of specified 11 | radii within a rectangle such that there is no-overlap between circles. 12 | The algorithm is adapted from an example written in Processing by Sean 13 | McCullough (which no longer seems to be available online). It involves 14 | iterative pair-repulsion, in which overlapping circles move away from each 15 | other. The distance moved by each circle is proportional to the radius of the 16 | other to approximate inertia (very loosely), so that when a small circle is 17 | overlapped by a large circle, the small circle moves furthest. This process 18 | is repeated iteratively until no more movement takes place (acceptable 19 | layout) or a maximum number of iterations is reached (layout failure). To 20 | avoid edge effects, the bounding rectangle is treated as a toroid. Each 21 | circle's centre is constrained to lie within the rectangle but its edges are 22 | allowed to extend outside. 23 | 24 | 25 | The function `circleProgressiveLayout` arranges a set of circles, which are 26 | denoted by their sizes, by consecutively placing each circle externally tangent 27 | to two previously placed circles while avoiding overlaps. It was adapted from a 28 | [version written in C](https://github.com/pmenzel/packCircles) by Peter Menzel. 29 | The underlying algorithm is described in the paper: *Visualization of large 30 | hierarchical data by circle packing* by 31 | [Weixin Wang et al. (2006)](https://doi.org/10.1145/1124772.1124851). 32 | 33 | 34 | The function `circleRemoveOverlaps` takes an initial set of overlapping circles 35 | and attempts to find a non-overlapping subset or, optionally, a subset with some 36 | specified degree of overlap. Circle positions remain fixed. It provides several 37 | fast heuristic algorithms to choose from, as well as two based on linear 38 | programming. For the latter, package lpSolve must be installed. 39 | 40 | 41 | The function `circleGraphLayout` is an initial Rcpp port of an algorithm described by 42 | [Collins and Stephenson (2003)](https://doi.org/10.1016/S0925-7721(02)00099-8) 43 | to find an arrangement of circles which corresponds to a graph of desired circle tangencies. 44 | The implementation is based on a Python version by David Eppstein (see CirclePack.py in 45 | the [PADS](https://www.ics.uci.edu/~eppstein/PADS/) library. 46 | 47 | To install: 48 | 49 | * the latest released version: `install.packages("packcircles")` 50 | * the latest development version (usually the same): `remotes::install_github("mbedward/packcircles")` 51 | 52 | See also: 53 | 54 | The [ggcirclepack](https://github.com/EvaMaeRey/ggcirclepack) package 55 | which makes it easier to add circles created with at least one of the `packcircles` 56 | functions to ggplot2 graphs. 57 | 58 | Share and enjoy! 59 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | Minor updates to fix problem in vignette, as advised by CRAN, and add validity 4 | checks to function arguments 5 | 6 | ## Test environments 7 | 8 | * MacOS 14.7 (release) 9 | * Ubuntu 22.04.5 LTS (release and devel) 10 | * win-builder (release and devel) 11 | 12 | ## R CMD check results 13 | 14 | There were no ERRORs or WARNINGs. 15 | 16 | There were no NOTES (other than maintainer details) 17 | 18 | ## Reverse dependencies 19 | 20 | 4 reverse dependencies were identified: cartogram, ggautomap, SUNGEO, tastypie. 21 | R CMD check succeeded for CRAN and dev versions of these packages. 22 | -------------------------------------------------------------------------------- /data-raw/input2.txt: -------------------------------------------------------------------------------- 1 | value colour label 2 | 4232 rgb(221,243,121) Dehalogenimonas sp. WBC-2 3 | 5097 rgb(243,198,121) Desulfatibacillum alkenivorans AK-01 4 | 2825 rgb(121,243,152) Opitutus terrae PB90-1 5 | 3471 rgb(243,198,121) Geobacter 6 | 7515 rgb(121,243,234) Methanosaeta harundinacea 6Ac 7 | 2476 rgb(196,243,121) Rubinisphaera brasiliensis DSM 5305 8 | 2802 rgb(121,243,127) Truepera radiovictrix DSM 17093 9 | 2482 rgb(243,121,191) Thermovirga lienii DSM 17291 10 | 3774 rgb(188,121,243) Thermofilum pendens Hrk 5 11 | 2632 rgb(121,243,234) Methanoregula boonei 6A8 12 | 79467 rgb(243,121,232) Dictyoglomus thermophilum H-6-12 13 | 2799 rgb(121,243,234) Aciduliprofundum boonei T469 14 | 2362 rgb(121,243,234) Methanothermobacter marburgensis str. Marburg 15 | 3273 rgb(243,198,121) Desulfobacterium autotrophicum HRM2 16 | 3186 rgb(221,243,121) Roseiflexus sp. RS-1 17 | 3300 rgb(121,243,234) Methanocella arvoryzae MRE50 18 | 9114 rgb(243,157,121) Melioribacter roseus P3M-2 19 | 35782 rgb(121,243,193) Pseudothermotoga hypogea DSM 11164 = NBRC 106472 20 | 3686 rgb(121,243,234) Methanothermus fervidus DSM 2088 21 | 3985 rgb(243,121,216) Caldicellulosiruptor kristjanssonii I77R1B 22 | 3632 rgb(221,243,121) Sphaerobacter thermophilus DSM 20745 23 | 3343 rgb(243,198,121) Desulfarculus baarsii DSM 2075 24 | 2931 rgb(121,227,243) Thaumarchaeota archaeon SAT1 25 | 4282 rgb(196,243,121) Pirellula staleyi DSM 6068 26 | 3118 rgb(121,186,243) Deferribacter desulfuricans SSM1 27 | 2956 rgb(243,121,216) Ruminiclostridium thermocellum 28 | 5405 rgb(243,121,216) Ammonifex degensii KC4 29 | 2537 rgb(121,243,218) Candidatus Cloacimonas acidaminovorans str. Evry 30 | 2360 rgb(243,198,121) Desulfurivibrio alkaliphilus AHT 2 31 | 10045 rgb(147,121,243) Caldisericum exile AZM16c01 32 | 150634 rgb(228,121,243) Thermodesulfovibrio yellowstonii DSM 11347 33 | 5488 rgb(121,243,127) Thermus oshimai JL-2 34 | 13033 rgb(121,243,193) Fervidobacterium 35 | 2356 rgb(243,121,216) Syntrophomonas wolfei subsp. wolfei str. Goettingen G311 36 | 8453 rgb(121,243,193) Pseudothermotoga thermarum DSM 5069 37 | 2671 rgb(243,121,216) Caldicellulosiruptor lactoaceticus 6A 38 | 52862 rgb(121,243,193) Fervidobacterium pennivorans DSM 9078 39 | 2481 rgb(243,157,121) Chloroherpeton thalassium ATCC 35110 40 | 2874 rgb(121,243,234) Methanothermobacter 41 | 3242 rgb(243,121,216) Caldicellulosiruptor owensensis OL 42 | 4175 rgb(121,243,234) Geoglobus acetivorans 43 | 4546 rgb(243,121,150) Sulfurihydrogenibium sp. YO3AOP1 44 | 2475 rgb(243,121,216) Symbiobacterium thermophilum IAM 14863 45 | 2503 rgb(188,121,243) Thermogladius cellulolyticus 1633 46 | 39174 rgb(121,243,234) Methanolinea tarda NOBI-1 47 | 7538 rgb(221,243,121) Thermomicrobium roseum DSM 5159 48 | 94082 rgb(121,243,193) Fervidobacterium nodosum Rt17-B1 49 | 2620 rgb(188,121,243) Ignicoccus hospitalis KIN4/I 50 | 2481 rgb(221,243,121) Chloroflexus aggregans DSM 9485 51 | 3632 rgb(243,121,191) Anaerobaculum mobile DSM 13181 52 | 4513 rgb(243,121,216) Caldicellulosiruptor hydrothermalis 108 53 | 8932 rgb(243,198,121) Desulfomonile tiedjei DSM 6799 54 | 3347 rgb(243,121,216) Thermoanaerobacter 55 | 5179 rgb(121,243,127) Thermus aquaticus Y51MC23 56 | 3544 rgb(243,198,121) Desulfococcus oleovorans Hxd3 57 | 3093 rgb(243,121,216) Tepidanaerobacter acetatoxydans Re1 58 | 2607 rgb(243,198,121) Desulfurella acetivorans A63 59 | 11869 rgb(243,121,125) Thermodesulfobacterium geofontis OPF15 60 | 2550 rgb(121,161,243) Fimbriimonas ginsengisoli Gsoil 348 61 | 2956 rgb(163,121,243) Candidatus Koribacter versatilis Ellin345 62 | 2600 rgb(121,243,193) Mesotoga prima MesG1.Ag.4.2 63 | 2614 rgb(121,243,234) Aciduliprofundum sp. MAR08-339 64 | 32105 rgb(121,243,193) Thermotoga caldifontis AZM44c09 65 | 3383 rgb(163,121,243) Chloracidobacterium thermophilum B 66 | 2921 rgb(121,243,234) Methanoregula formicica SMSP 67 | 2379 rgb(122,121,243) Spirochaeta thermophila 68 | 7636 rgb(121,243,234) Ferroglobus placidus DSM 10642 69 | 2547 rgb(121,243,234) Euryarchaeota 70 | 71618 rgb(121,243,193) Thermotoga profunda AZM34c06 71 | 4490 rgb(243,121,216) Desulfotomaculum gibsoniae DSM 7213 72 | 30598 rgb(243,121,232) Dictyoglomus 73 | 4025 rgb(156,243,121) Thermobaculum terrenum ATCC BAA-798 74 | 4641 rgb(188,121,243) Thermofilum 75 | 2703 rgb(221,243,121) Roseiflexus castenholzii DSM 13941 76 | 3463 rgb(243,121,216) Candidatus Desulforudis audaxviator MP104C 77 | 2806 rgb(243,121,216) Coprothermobacter proteolyticus DSM 5265 78 | 7393 rgb(243,121,166) Candidatus Korarchaeum cryptofilum OPF8 79 | 4342 rgb(121,243,234) Geoglobus ahangari 80 | 2975 rgb(188,121,243) Pyrolobus fumarii 1A 81 | 4271 rgb(121,243,234) Thermococcus 82 | 3375 rgb(121,243,193) Marinitoga piezophila KA3 83 | 2923 rgb(121,243,193) Petrotoga mobilis SJ95 84 | 2875 rgb(243,121,216) Caldicellulosiruptor obsidiansis OB47 85 | 4207 rgb(243,157,121) Rhodothermus marinus 86 | 8547 rgb(121,243,193) Thermotoga 87 | 4559 rgb(243,121,216) Clostridium 88 | 2540 rgb(121,243,193) Kosmotoga olearia TBF 19.5.1 89 | 6357 rgb(163,121,243) Candidatus Solibacter usitatus Ellin6076 90 | 3205 rgb(243,121,150) Persephonella marina EX-H1 91 | 3447 rgb(122,121,243) Treponema caldarium DSM 7334 92 | 3869 rgb(121,243,234) Methanocella conradii HZ254 93 | 2803 rgb(243,121,216) [Clostridium] stercorarium subsp. stercorarium DSM 8532 94 | 2404 rgb(228,121,243) Nitrospira moscoviensis 95 | 3740 rgb(243,183,121) Terrabacteria group 96 | 18973 rgb(121,243,234) Archaeoglobus fulgidus 97 | 3564 rgb(121,243,234) Candidatus Methanomassiliicoccus intestinalis Issoire-Mx1 98 | 21200 rgb(121,243,127) Thermus scotoductus SA-01 99 | 8440 rgb(243,157,121) Ignavibacterium album JCM 16511 100 | 5534 rgb(243,121,125) Thermodesulfatator indicus DSM 15286 101 | 5279 rgb(243,121,216) Thermacetogenium phaeum DSM 12270 102 | 6923 rgb(243,121,150) Hydrogenobacter thermophilus TK-6 103 | 3462 rgb(243,121,216) Thermincola potens JR 104 | 4912 rgb(121,243,193) Thermosipho africanus TCF52B 105 | 2578 rgb(188,121,243) Thermosphaera aggregans DSM 11486 106 | 2827 rgb(243,121,216) [Clostridium] clariflavum DSM 19732 107 | 2812 rgb(122,121,243) Spirochaeta smaragdinae DSM 11293 108 | 5556 rgb(243,121,150) Thermocrinis ruber 109 | 6730 rgb(243,198,121) Syntrophobacter fumaroxidans MPOB 110 | 6831 rgb(121,243,234) Archaeoglobus profundus DSM 5631 111 | 2507 rgb(121,243,234) Methanosphaerula palustris E1-9c 112 | 3928 rgb(243,198,121) Desulfobacula toluolica Tol2 113 | 6816 rgb(121,243,234) Archaeoglobus sulfaticallidus PM70-1 114 | 2844 rgb(188,121,243) Caldivirga maquilingensis IC-167 115 | 2646 rgb(243,198,121) Hippea maritima DSM 10411 116 | 2690 rgb(243,121,216) Clostridiales 117 | 4501 rgb(243,198,121) Proteobacteria 118 | 2441 rgb(121,243,234) Archaeoglobaceae 119 | 2866 rgb(243,121,216) Caldanaerobacter subterraneus subsp. tengcongensis MB4 120 | 15015 rgb(221,243,121) Anaerolinea thermophila UNI-1 121 | 7954 rgb(243,198,121) Syntrophus aciditrophicus SB 122 | 46488 rgb(243,121,216) Caldicellulosiruptor 123 | 2451 rgb(196,243,121) Isosphaera pallida ATCC 43644 124 | 4103 rgb(243,121,216) Carboxydothermus hydrogenoformans Z-2901 125 | 5935 rgb(243,121,216) Caldicellulosiruptor saccharolyticus DSM 8903 126 | 3178 rgb(243,121,216) Halothermothrix orenii H 168 127 | 31839 rgb(243,223,121) Bacteria 128 | 3236 rgb(121,243,127) Thermus thermophilus 129 | 2422 rgb(243,198,121) Desulfobulbus propionicus DSM 2032 130 | 7885 rgb(121,243,127) Thermus sp. CCB_US3_UF1 131 | 3858 rgb(221,243,121) Dehalococcoides 132 | 3061 rgb(121,243,234) Thermococcaceae 133 | 9407 rgb(121,243,234) Methanosaeta concilii GP6 134 | 3208 rgb(121,243,193) Thermosipho melanesiensis BI429 135 | 3994 rgb(243,121,216) Thermosediminibacter oceani DSM 16646 136 | 2770 rgb(243,121,216) Thermodesulfobium narugense DSM 14796 137 | 2599 rgb(243,121,216) Caldicellulosiruptor kronotskyensis 2002 138 | 13510 rgb(121,243,127) Thermus 139 | 2346 rgb(243,198,121) Pelobacter carbinolicus DSM 2380 140 | 11868 rgb(243,121,125) Thermodesulfobacterium commune DSM 2178 141 | 7399 rgb(121,243,234) Archaeoglobus veneficus SNP6 142 | 3912 rgb(221,243,121) Chloroflexus 143 | 3053 rgb(243,121,216) Paenibacillus 144 | 5791 rgb(243,121,150) Sulfurihydrogenibium azorense Az-Fu1 145 | 6845 rgb(131,243,121) cellular organisms 146 | 5975 rgb(121,186,243) Calditerrivibrio nitroreducens DSM 19672 147 | 2672 rgb(243,121,216) Thermaerobacter marianensis DSM 12885 148 | 2452 rgb(188,121,243) Pyrodictium delaneyi 149 | 3306 rgb(243,121,150) Aquifex aeolicus VF5 150 | 11074 rgb(121,243,193) Pseudothermotoga 151 | 10064 rgb(221,243,121) Caldilinea aerophila DSM 14535 = NBRC 104270 152 | 4707 rgb(243,121,216) Moorella thermoacetica 153 | 44747 rgb(243,121,232) Dictyoglomus turgidum DSM 6724 154 | 3254 rgb(221,243,121) Dehalogenimonas lykanthroporepellens BL-DC-9 155 | 4071 rgb(196,243,121) Singulisphaera acidiphila DSM 18658 156 | 4768 rgb(243,121,216) Syntrophothermus lipocalidus DSM 12680 157 | 5457 rgb(243,121,216) Mahella australiensis 50-1 BON 158 | 3748 rgb(188,121,243) Thermofilum sp. 1807-2 159 | 6287 rgb(243,198,121) Desulfobacca acetoxidans DSM 11109 160 | 2594 rgb(243,121,216) Caldicellulosiruptor bescii DSM 6725 161 | 4535 rgb(121,243,234) Methanosarcina 162 | 2341 rgb(121,243,234) Methanotorris igneus Kol 5 163 | 2505 rgb(243,121,216) Desulfotomaculum acetoxidans DSM 771 164 | 2759 rgb(121,243,152) Verrucomicrobia bacterium L21-Fru-AB 165 | 4883 rgb(243,121,150) Thermocrinis albus DSM 14484 166 | 4408 rgb(121,243,234) Methanospirillum hungatei JF-1 167 | 2444 rgb(121,243,234) Methanoculleus bourgensis MS2 168 | 13932 rgb(121,243,193) Thermotogaceae 169 | -------------------------------------------------------------------------------- /data-raw/progressive_example_data.R: -------------------------------------------------------------------------------- 1 | # Reads Peter's example file and converts colour specifications of the 2 | # form 'rgb(123, 123, 123)' to hex values. 3 | 4 | path <- file.path("data-raw", "input2.txt") 5 | bacteria <- read.delim(path, stringsAsFactors = FALSE) 6 | 7 | # local version of rgb function 8 | rgb <- function(r,g,b) grDevices::rgb(r,g,b, maxColorValue = 255) 9 | 10 | # evaluate input colour specs using the local function 11 | cols <- sapply( bacteria$colour, function(x) eval(parse(text = x)) ) 12 | 13 | # replace colour column and save to data directory 14 | bacteria$colour <- cols 15 | devtools::use_data(bacteria, overwrite = TRUE) -------------------------------------------------------------------------------- /data/bacteria.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbedward/packcircles/469f143bc3264622fcb3174c6be13201089b24fc/data/bacteria.rda -------------------------------------------------------------------------------- /man/bacteria.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data.R 3 | \docType{data} 4 | \name{bacteria} 5 | \alias{bacteria} 6 | \title{Abundance of bacteria} 7 | \format{ 8 | ## `bacteria` 9 | A data frame with 167 rows and 3 columns: 10 | \describe{ 11 | \item{value}{measured abundance} 12 | \item{colour}{preferred colour for display} 13 | \item{label}{taxon name} 14 | } 15 | } 16 | \usage{ 17 | bacteria 18 | } 19 | \description{ 20 | Names and abundances of bacterial taxa as measured in a study of biofilms. 21 | } 22 | \keyword{datasets} 23 | -------------------------------------------------------------------------------- /man/circleGraphLayout.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleGraphLayout.R 3 | \name{circleGraphLayout} 4 | \alias{circleGraphLayout} 5 | \title{Find an arrangement of circles satisfying a graph of adjacencies} 6 | \usage{ 7 | circleGraphLayout(internal, external) 8 | } 9 | \arguments{ 10 | \item{internal}{A list of vectors of circle ID values where, in each vector, 11 | the first element is the ID of an internal circle and the remaining 12 | elements are the IDs of that circle's neighbours arranged as a cycle. The 13 | cycle may be clockwise or anti-clockwise but the same ordering must be used 14 | for all vectors.} 15 | 16 | \item{external}{A data.frame or matrix of external circle radii, with circle 17 | IDs in the first column and radii in the second column.} 18 | } 19 | \value{ 20 | A data.frame with columns for circle ID, centre X and Y ordinate, and 21 | radius. 22 | 23 | The output arrangement as a data.frame with columns for circle ID, 24 | centre X and Y ordinates, and radius. For external circles the radius will 25 | equal input values. 26 | } 27 | \description{ 28 | Attempts to derive an arrangement of circles satisfying prior conditions for 29 | size and adjacency. Unlike the \code{\link{circleRepelLayout}} function, this 30 | is a deterministic algorithm. Circles are classified as either internal or 31 | external. Viewing the pattern of adjacencies as a triangulated mesh, external 32 | circles are those on the boundary. In the version of the algorithm 33 | implemented here, the radii of external circles are provided as inputs, while 34 | the radii of internal circles are derived as part of the output arrangement. 35 | } 36 | \details{ 37 | The \code{internal} argument specifies circle adjacencies (ie. tangencies). 38 | The format is an concise representation of graph edges, and consists of a 39 | list of vectors: one per internal circle. In each vector the first element is 40 | the ID value of the internal circle and the remaining values are IDs of 41 | neighbouring circles, which may be either internal or external. 42 | 43 | The \code{external} argument is a data.frame which specifies the radii of 44 | external circles. Internal circle radii should not be specified as they are 45 | derived as part of the fitting algorithm. The function will issue an error if 46 | any internal circle IDs are present in the \code{external} data. 47 | } 48 | \note{ 49 | Please treat this function as experimental. 50 | } 51 | \examples{ 52 | ## Simple example with two internal circles surrounded by 53 | ## four external circles. Internal circle IDs are 1 and 2. 54 | internal <- list( c(1, 3, 4, 5), c(2, 3, 4, 6) ) 55 | 56 | ## Uniform radius for external circles 57 | external <- data.frame(id=3:6, radius=1.0) 58 | 59 | ## Generate the circle packing 60 | packing <- circleGraphLayout(internal, external) 61 | 62 | } 63 | \references{ 64 | C.R. Collins & K. Stephenson (2003) An algorithm for circle 65 | packing. Computational Geometry Theory and Applications 25:233-256. 66 | } 67 | -------------------------------------------------------------------------------- /man/circleLayout.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleRepelLayout.R 3 | \name{circleLayout} 4 | \alias{circleLayout} 5 | \title{Arranges circles by iterative pair-wise repulsion within a bounding rectangle} 6 | \usage{ 7 | circleLayout(xyr, xlim, ylim, maxiter = 1000, wrap = TRUE, weights = 1) 8 | } 9 | \arguments{ 10 | \item{xyr}{A 3-column matrix or data frame (centre X, centre Y, radius).} 11 | 12 | \item{xlim}{The bounds in the X direction; either a vector for [xmin, xmax) 13 | or a single value interpreted as [0, xmax). Alternatively, omitting this 14 | argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 15 | vector will result in unbounded movement in the X direction.} 16 | 17 | \item{ylim}{The bounds in the Y direction; either a vector for [ymin, ymax) 18 | or a single value interpreted as [0, ymax). Alternatively, omitting this 19 | argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 20 | vector will result in unbounded movement in the Y direction.} 21 | 22 | \item{maxiter}{The maximum number of iterations.} 23 | 24 | \item{wrap}{Whether to treat the bounding rectangle as a toroid (default 25 | \code{TRUE}). When this is in effect, a circle leaving the bounds on one 26 | side re-enters on the opposite side.} 27 | 28 | \item{weights}{An optional vector of numeric weights (0 to 1 inclusive) to 29 | apply to the distance each circle moves during pair-repulsion. A weight of 30 | 0 prevents any movement. A weight of 1 gives the default movement distance. 31 | A single value can be supplied for uniform weights. A vector with length 32 | less than the number of circles will be silently extended by repeating the 33 | final value. Any values outside the range [0, 1] will be clamped to 0 or 1.} 34 | } 35 | \value{ 36 | A list with components: 37 | \describe{ 38 | \item{layout}{A 3-column matrix or data.frame (centre x, centre y, radius).} 39 | \item{niter}{Number of iterations performed.} 40 | } 41 | } 42 | \description{ 43 | This function is deprecated and will be removed in a future release. 44 | Please use \code{\link{circleRepelLayout}} instead. 45 | } 46 | \note{ 47 | This function assumes that circle sizes are expressed as radii 48 | whereas the default for \code{circleRepelLayout} is area. 49 | } 50 | \seealso{ 51 | \code{\link{circleRepelLayout}} 52 | } 53 | -------------------------------------------------------------------------------- /man/circleLayoutVertices.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleVertices.R 3 | \name{circleLayoutVertices} 4 | \alias{circleLayoutVertices} 5 | \title{Generate a set of circle vertices suitable for plotting} 6 | \usage{ 7 | circleLayoutVertices( 8 | layout, 9 | npoints = 25, 10 | xysizecols = 1:3, 11 | sizetype = c("radius", "area"), 12 | idcol = NULL 13 | ) 14 | } 15 | \arguments{ 16 | \item{layout}{A matrix or data frame of circle data (x, y, size). May also 17 | contain other columns including an optional identifier column.} 18 | 19 | \item{npoints}{The number of vertices to generate for each circle.} 20 | 21 | \item{xysizecols}{The integer indices or names of columns for the centre X, 22 | centre Y and size values. Default is `c(1,2,3)`.} 23 | 24 | \item{sizetype}{The type of size values: either \code{"radius"} (default) or 25 | \code{"area"}. May be abbreviated.} 26 | 27 | \item{idcol}{Optional index (integer) or name (character) of an input data 28 | column to use as circle identifier values in the \code{id} column of the 29 | output data frame. Identifier values may be numeric or character but must 30 | be unique. If not provided, the output circle identifiers will be the row 31 | numbers of the input circle data.} 32 | } 33 | \value{ 34 | A data frame with columns: id, x, y; where id is the unique integer 35 | identifier for each circle. If no size values in the input \code{layout} 36 | data are positive, a data frame with zero rows will be returned. 37 | } 38 | \description{ 39 | Given a matrix or data frame for a circle layout, with columns for centre X 40 | and Y coordinates and circle sizes, this function generates a data set of 41 | vertices which can then be used with ggplot or base graphics functions. If 42 | any of the size values in the input data are zero, negative or missing 43 | (\code{NA} or \code{NULL}), the corresponding circles will not be generated. 44 | This can be useful when displaying alternative subsets of circles. 45 | } 46 | \note{ 47 | \strong{Input sizes are assumed to be radii}. This is slightly 48 | confusing because the layout functions \code{circleRepelLayout} and 49 | \code{circleProgressiveLayout} treat their input sizes as areas by default. 50 | To be safe, you can always set the \code{sizetype} argument explicitly for 51 | both this function and layout functions. 52 | } 53 | \examples{ 54 | xmax <- 100 55 | ymax <- 100 56 | rmin <- 10 57 | rmax <- 20 58 | N <- 20 59 | 60 | ## Random centre coordinates and radii 61 | layout <- data.frame(id = 1:N, 62 | x = runif(N, 0, xmax), 63 | y = runif(N, 0, ymax), 64 | radius = runif(N, rmin, rmax)) 65 | 66 | ## Get data for circle vertices 67 | verts <- circleLayoutVertices(layout, idcol=1, xysizecols=2:4, 68 | sizetype = "radius") 69 | 70 | \dontrun{ 71 | library(ggplot2) 72 | 73 | ## Draw circles annotated with their IDs 74 | ggplot() + 75 | geom_polygon(data = verts, aes(x, y, group = id), 76 | fill = "grey90", 77 | colour = "black") + 78 | 79 | geom_text(data = layout, aes(x, y, label = id)) + 80 | 81 | coord_equal() + 82 | theme_bw() 83 | } 84 | 85 | } 86 | \seealso{ 87 | \code{\link{circleVertices}} 88 | } 89 | -------------------------------------------------------------------------------- /man/circlePlotData.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleVertices.R 3 | \name{circlePlotData} 4 | \alias{circlePlotData} 5 | \title{Generate a set of circle vertices suitable for plotting} 6 | \usage{ 7 | circlePlotData(layout, npoints = 25, xyr.cols = 1:3, id.col = NULL) 8 | } 9 | \arguments{ 10 | \item{layout}{A matrix or data frame of circle data (x, y, radius). May contain 11 | other columns, including an optional ID column.} 12 | 13 | \item{npoints}{The number of vertices to generate for each circle.} 14 | 15 | \item{xyr.cols}{Indices or names of columns for x, y, radius (in that order). 16 | Default is columns 1-3.} 17 | 18 | \item{id.col}{Optional index or name of column for circle IDs in output. 19 | If not provided, the output circle IDs will be the row numbers of 20 | the input circle data.} 21 | } 22 | \value{ 23 | A data frame with columns: id, x, y; where id is the unique 24 | integer identifier for each circle. 25 | } 26 | \description{ 27 | This function is deprecated and will be removed in a future release. 28 | Please use \code{circleLayoutVertices} instead. 29 | } 30 | \seealso{ 31 | \code{\link{circleLayoutVertices}} \code{\link{circleVertices}} 32 | } 33 | -------------------------------------------------------------------------------- /man/circleProgressiveLayout.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleProgressiveLayout.R 3 | \name{circleProgressiveLayout} 4 | \alias{circleProgressiveLayout} 5 | \title{Progressive layout algorithm} 6 | \usage{ 7 | circleProgressiveLayout(x, sizecol = 1, sizetype = c("area", "radius")) 8 | } 9 | \arguments{ 10 | \item{x}{Either a vector of circle sizes, or a matrix or data frame 11 | with one column for circle sizes.} 12 | 13 | \item{sizecol}{The index or name of the column in \code{x} for circle sizes. 14 | Ignored if \code{x} is a vector.} 15 | 16 | \item{sizetype}{The type of size values: either \code{"area"} (default) 17 | or \code{"radius"}. May be abbreviated.} 18 | } 19 | \value{ 20 | A data frame with columns: x, y, radius. If any of the input size values 21 | were non-positive or missing, the corresponding rows of the output data frame 22 | will be filled with \code{NA}s. 23 | } 24 | \description{ 25 | Arranges a set of circles, which are denoted by their sizes, by consecutively 26 | placing each circle externally tangent to two previously placed circles while 27 | avoiding overlaps. 28 | } 29 | \details{ 30 | Based on an algorithm described in the paper: 31 | \emph{Visualization of large hierarchical data by circle packing} 32 | by Weixin Wang, Hui Wang, Guozhong Dai, and Hongan Wang. Published 33 | in \emph{Proceedings of the SIGCHI Conference on Human Factors in Computing Systems}, 34 | 2006, pp. 517-520 \doi{10.1145/1124772.1124851} 35 | 36 | The implementation here was adapted from a version written in C by Peter Menzel: 37 | \url{https://github.com/pmenzel/packCircles}. 38 | } 39 | \examples{ 40 | areas <- sample(c(4, 16, 64), 100, rep = TRUE, prob = c(60, 30, 10)) 41 | packing <- circleProgressiveLayout(areas) 42 | 43 | \dontrun{ 44 | 45 | # Graph the result with ggplot 46 | library(ggplot2) 47 | 48 | dat.gg <- circleLayoutVertices(packing) 49 | 50 | ggplot(data = dat.gg, aes(x, y, group = id)) + 51 | geom_polygon(colour = "black", fill = "grey90") + 52 | coord_equal() + 53 | theme_void() 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /man/circleRemoveOverlaps.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleRemoveOverlaps.R 3 | \name{circleRemoveOverlaps} 4 | \alias{circleRemoveOverlaps} 5 | \title{Filters a set of circles to remove all overlaps} 6 | \usage{ 7 | circleRemoveOverlaps( 8 | x, 9 | xysizecols = 1:3, 10 | sizetype = c("area", "radius"), 11 | tolerance = 1, 12 | method = c("maxov", "minov", "largest", "smallest", "random", "lparea", "lpnum") 13 | ) 14 | } 15 | \arguments{ 16 | \item{x}{A matrix or data frame containing circle x-y centre coordinates 17 | and sizes (area or radius).} 18 | 19 | \item{xysizecols}{The integer indices or names of the columns in \code{x} for 20 | the centre x-y coordinates and sizes of circles. Default is \code{c(1,2,3)}.} 21 | 22 | \item{sizetype}{The type of size values: either \code{"area"} (default) or 23 | \code{"radius"}. May be abbreviated.} 24 | 25 | \item{tolerance}{Controls the amount of overlap allowed. Set to 1 for 26 | simple exclusion of overlaps. Values lower than 1 allow more overlap. 27 | Values > 1 have the effect of expanding the influence of circles so that 28 | more space is required between them. The input value must be > 0.} 29 | 30 | \item{method}{Specifies whether to use linear programming (default) or one of 31 | the variants of the heuristic algorithm. Alternatives are: 32 | \code{"maxov"}, \code{"minov"}, \code{"largest"}, \code{"smallest"}, 33 | \code{"random"}, \code{"lparea"}, \code{"lpnum"}. See Details for further 34 | explanation.} 35 | } 36 | \value{ 37 | A data frame with centre coordinates and radii of selected circles. 38 | } 39 | \description{ 40 | Given an initial set of circles, this function identifies a subset of 41 | non-overlapping circles using a simple heuristic algorithm. Circle positions 42 | remain fixed. 43 | } 44 | \details{ 45 | The \code{method} argument specifies whether to use the heuristic algorithm or 46 | linear programming. The following options select the heuristic algorithm and 47 | specify how to choose an overlapping circle to remove at each iteration: 48 | \describe{ 49 | \item{maxov}{Choose one of the circles with the greatest number of overlaps.} 50 | \item{minov}{Choose one of the circles with the least number of overlaps.} 51 | \item{largest}{Choose one of the largest circles.} 52 | \item{smallest}{Choose one of the smallest circles.} 53 | \item{random}{Choose a circle at random.} 54 | } 55 | At each iteration the number of overlaps is checked for each candidate 56 | circle and any non-overlapping circles added to the selected subset. Then a 57 | single overlapping circle is chosen, based on the method being used, from 58 | among the remainder and marked as rejected. Iterations continue until all 59 | circles have been either selected or rejected. The 'maxov' option (default) 60 | generally seems to perform best at maximizing the number of circles retained. 61 | The other options are provided for comparison and experiment. Beware that 62 | some can perform surprisingly poorly, especially 'minov'. 63 | 64 | Two further options select linear programming: 65 | \describe{ 66 | \item{lparea}{Maximise the total area of circles in the subset.} 67 | \item{lpnum}{Maximise the total number of circles in the subset.} 68 | } 69 | 70 | The `lpSolve` package must be installed to use the linear programming options. 71 | These options will find an optimal subset, but for anything other than a small 72 | number of initial circles the running time can be prohibitive. 73 | } 74 | \note{ 75 | \emph{This function is experimental} and will almost certainly change before 76 | the next package release. In particular, it will probably return something 77 | other than a data frame. 78 | } 79 | -------------------------------------------------------------------------------- /man/circleRepelLayout.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleRepelLayout.R 3 | \name{circleRepelLayout} 4 | \alias{circleRepelLayout} 5 | \title{Arranges circles by iterative pair-wise repulsion within a bounding rectangle} 6 | \usage{ 7 | circleRepelLayout( 8 | x, 9 | xlim, 10 | ylim, 11 | xysizecols = c(1, 2, 3), 12 | sizetype = c("area", "radius"), 13 | maxiter = 1000, 14 | wrap = TRUE, 15 | weights = 1 16 | ) 17 | } 18 | \arguments{ 19 | \item{x}{Either a vector of circle sizes (areas or radii) or a matrix or 20 | data frame with a column of sizes and, optionally, columns for initial 21 | x-y coordinates of circle centres.} 22 | 23 | \item{xlim}{The bounds in the X direction; either a vector for [xmin, xmax) 24 | or a single value interpreted as [0, xmax). Alternatively, omitting this 25 | argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 26 | vector will result in unbounded movement in the X direction.} 27 | 28 | \item{ylim}{The bounds in the Y direction; either a vector for [ymin, ymax) 29 | or a single value interpreted as [0, ymax). Alternatively, omitting this 30 | argument or passing any of \code{NULL}, a vector of \code{NA} or an empty 31 | vector will result in unbounded movement in the Y direction.} 32 | 33 | \item{xysizecols}{The integer indices or names of the columns in \code{x} 34 | for the centre x-y coordinates and sizes of circles. This argument is 35 | ignored if \code{x} is a vector. If \code{x} is a matrix or data frame 36 | but does not contain initial x-y coordinates, this must be indicated as 37 | \code{xysizecols = c(NA, NA, 1)}.} 38 | 39 | \item{sizetype}{The type of size values: either \code{"area"} or \code{"radius"}. 40 | May be abbreviated.} 41 | 42 | \item{maxiter}{The maximum number of iterations.} 43 | 44 | \item{wrap}{Whether to treat the bounding rectangle as a toroid (default 45 | \code{TRUE}). When this is in effect, a circle leaving the bounds on one 46 | side re-enters on the opposite side.} 47 | 48 | \item{weights}{An optional vector of numeric weights (0 to 1 inclusive) to 49 | apply to the distance each circle moves during pair-repulsion. A weight of 50 | 0 prevents any movement. A weight of 1 gives the default movement distance. 51 | A single value can be supplied for uniform weights. A vector with length 52 | less than the number of circles will be silently extended by repeating the 53 | final value. Any values outside the range [0, 1] will be clamped to 0 or 1.} 54 | } 55 | \value{ 56 | A list with components: \describe{ \item{layout}{A 3-column matrix or 57 | data frame (centre x, centre y, radius).} \item{niter}{Number of iterations 58 | performed.} } 59 | } 60 | \description{ 61 | This function takes a set of circles, defined by a data frame of initial 62 | centre positions and radii, and uses iterative pair-wise repulsion to try to 63 | find a non-overlapping arrangement where all circle centres lie inside a 64 | bounding rectangle. If no such arrangement can be found within the specified 65 | maximum number of iterations, the last attempt is returned. 66 | } 67 | \details{ 68 | The algorithm is adapted from a demonstration app written in the Processing 69 | language by Sean McCullough (no longer available online). Each circle in the 70 | input data is compared to those following it. If two circles overlap, they 71 | are moved apart such that the distance moved by each is proportional to the 72 | radius of the other, loosely simulating inertia. So when a small circle is 73 | overlapped by a larger circle, the small circle moves furthest. This process 74 | is repeated until no more movement takes place (acceptable layout) or the 75 | maximum number of iterations is reached (layout failure). 76 | 77 | To avoid edge effects, the bounding rectangle can be treated as a toroid by 78 | setting the \code{wrap} argument to \code{TRUE}. With this option, a circle 79 | moving outside the bounds re-enters at the opposite side. 80 | } 81 | -------------------------------------------------------------------------------- /man/circleVertices.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/circleVertices.R 3 | \name{circleVertices} 4 | \alias{circleVertices} 5 | \title{Generate vertex coordinates for a circle} 6 | \usage{ 7 | circleVertices(xc, yc, radius, npoints = 25) 8 | } 9 | \arguments{ 10 | \item{xc}{Value for centre X ordinate.} 11 | 12 | \item{yc}{Value for centre Y ordinate.} 13 | 14 | \item{radius}{Value for radius.} 15 | 16 | \item{npoints}{The number of distinct vertices required.} 17 | } 18 | \value{ 19 | A 2-column matrix of X and Y values. The final row is a copy 20 | of the first row to create a closed polygon, so the matrix has 21 | \code{npoints + 1} rows. 22 | } 23 | \description{ 24 | Generates vertex coordinates for a circle given its centre coordinates 25 | and radius. 26 | } 27 | \seealso{ 28 | \code{\link{circleLayoutVertices}} 29 | } 30 | -------------------------------------------------------------------------------- /man/packcircles-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/packcircles.R 3 | \docType{package} 4 | \name{packcircles-package} 5 | \alias{packcircles} 6 | \alias{packcircles-package} 7 | \title{packcircles: Simple algorithms for circle packing} 8 | \description{ 9 | This package provides several algorithms to find non-overlapping 10 | arrangements of circles: 11 | \describe{ 12 | \item{circleRepelLayout}{Arranges circles within a bounding rectangle 13 | by pairwise repulsion.} 14 | \item{circleProgressiveLayout}{Arranges circles in an unbounded area 15 | by progressive placement. This is a very efficient algorithm that can 16 | handle large numbers of circles.} 17 | \item{circleGraphLayout}{Finds an arrangement of circles conforming to 18 | a graph specification.} 19 | } 20 | } 21 | \seealso{ 22 | Useful links: 23 | \itemize{ 24 | \item \url{https://github.com/mbedward/packcircles} 25 | \item Report bugs at \url{https://github.com/mbedward/packcircles/issues} 26 | } 27 | 28 | } 29 | \author{ 30 | \strong{Maintainer}: Michael Bedward \email{michael.bedward@gmail.com} 31 | 32 | Authors: 33 | \itemize{ 34 | \item David Eppstein \email{david.eppstein@gmail.com} (Author of Python code for graph-based circle packing ported to C++ for this package) 35 | \item Peter Menzel \email{pmenzel@gmail.com} (Author of C code for progressive circle packing ported to C++ for this package) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /packcircles.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | PackageRoxygenize: rd,namespace 19 | -------------------------------------------------------------------------------- /src/RcppExports.cpp: -------------------------------------------------------------------------------- 1 | // Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | // Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | #include 5 | 6 | using namespace Rcpp; 7 | 8 | #ifdef RCPP_USE_GLOBAL_ROSTREAM 9 | Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); 10 | Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); 11 | #endif 12 | 13 | // iterate_layout 14 | int iterate_layout(NumericMatrix xyr, NumericVector weights, double xmin, double xmax, double ymin, double ymax, int maxiter, bool wrap); 15 | RcppExport SEXP _packcircles_iterate_layout(SEXP xyrSEXP, SEXP weightsSEXP, SEXP xminSEXP, SEXP xmaxSEXP, SEXP yminSEXP, SEXP ymaxSEXP, SEXP maxiterSEXP, SEXP wrapSEXP) { 16 | BEGIN_RCPP 17 | Rcpp::RObject rcpp_result_gen; 18 | Rcpp::RNGScope rcpp_rngScope_gen; 19 | Rcpp::traits::input_parameter< NumericMatrix >::type xyr(xyrSEXP); 20 | Rcpp::traits::input_parameter< NumericVector >::type weights(weightsSEXP); 21 | Rcpp::traits::input_parameter< double >::type xmin(xminSEXP); 22 | Rcpp::traits::input_parameter< double >::type xmax(xmaxSEXP); 23 | Rcpp::traits::input_parameter< double >::type ymin(yminSEXP); 24 | Rcpp::traits::input_parameter< double >::type ymax(ymaxSEXP); 25 | Rcpp::traits::input_parameter< int >::type maxiter(maxiterSEXP); 26 | Rcpp::traits::input_parameter< bool >::type wrap(wrapSEXP); 27 | rcpp_result_gen = Rcpp::wrap(iterate_layout(xyr, weights, xmin, xmax, ymin, ymax, maxiter, wrap)); 28 | return rcpp_result_gen; 29 | END_RCPP 30 | } 31 | // doCirclePack 32 | List doCirclePack(List internalList, DataFrame externalDF); 33 | RcppExport SEXP _packcircles_doCirclePack(SEXP internalListSEXP, SEXP externalDFSEXP) { 34 | BEGIN_RCPP 35 | Rcpp::RObject rcpp_result_gen; 36 | Rcpp::RNGScope rcpp_rngScope_gen; 37 | Rcpp::traits::input_parameter< List >::type internalList(internalListSEXP); 38 | Rcpp::traits::input_parameter< DataFrame >::type externalDF(externalDFSEXP); 39 | rcpp_result_gen = Rcpp::wrap(doCirclePack(internalList, externalDF)); 40 | return rcpp_result_gen; 41 | END_RCPP 42 | } 43 | // do_progressive_layout 44 | DataFrame do_progressive_layout(NumericVector radii); 45 | RcppExport SEXP _packcircles_do_progressive_layout(SEXP radiiSEXP) { 46 | BEGIN_RCPP 47 | Rcpp::RObject rcpp_result_gen; 48 | Rcpp::RNGScope rcpp_rngScope_gen; 49 | Rcpp::traits::input_parameter< NumericVector >::type radii(radiiSEXP); 50 | rcpp_result_gen = Rcpp::wrap(do_progressive_layout(radii)); 51 | return rcpp_result_gen; 52 | END_RCPP 53 | } 54 | // select_non_overlapping 55 | LogicalVector select_non_overlapping(NumericMatrix xyr, const double tolerance, const StringVector& ordering); 56 | RcppExport SEXP _packcircles_select_non_overlapping(SEXP xyrSEXP, SEXP toleranceSEXP, SEXP orderingSEXP) { 57 | BEGIN_RCPP 58 | Rcpp::RObject rcpp_result_gen; 59 | Rcpp::RNGScope rcpp_rngScope_gen; 60 | Rcpp::traits::input_parameter< NumericMatrix >::type xyr(xyrSEXP); 61 | Rcpp::traits::input_parameter< const double >::type tolerance(toleranceSEXP); 62 | Rcpp::traits::input_parameter< const StringVector& >::type ordering(orderingSEXP); 63 | rcpp_result_gen = Rcpp::wrap(select_non_overlapping(xyr, tolerance, ordering)); 64 | return rcpp_result_gen; 65 | END_RCPP 66 | } 67 | -------------------------------------------------------------------------------- /src/init.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include // for NULL 4 | #include 5 | 6 | /* FIXME: 7 | Check these declarations against the C/Fortran source code. 8 | */ 9 | 10 | /* .Call calls */ 11 | extern SEXP _packcircles_do_progressive_layout(SEXP); 12 | extern SEXP _packcircles_doCirclePack(SEXP, SEXP); 13 | extern SEXP _packcircles_iterate_layout(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); 14 | extern SEXP _packcircles_select_non_overlapping(SEXP, SEXP, SEXP); 15 | 16 | static const R_CallMethodDef CallEntries[] = { 17 | {"_packcircles_do_progressive_layout", (DL_FUNC) &_packcircles_do_progressive_layout, 1}, 18 | {"_packcircles_doCirclePack", (DL_FUNC) &_packcircles_doCirclePack, 2}, 19 | {"_packcircles_iterate_layout", (DL_FUNC) &_packcircles_iterate_layout, 8}, 20 | {"_packcircles_select_non_overlapping", (DL_FUNC) &_packcircles_select_non_overlapping, 3}, 21 | {NULL, NULL, 0} 22 | }; 23 | 24 | void R_init_packcircles(DllInfo *dll) 25 | { 26 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 27 | R_useDynamicSymbols(dll, FALSE); 28 | } 29 | -------------------------------------------------------------------------------- /src/packcircles.cpp: -------------------------------------------------------------------------------- 1 | #define STRICT_R_HEADERS 2 | #include 3 | using namespace Rcpp; 4 | 5 | 6 | // Forward declarations 7 | // 8 | bool almostZero(double x); 9 | 10 | bool gtZero(double x); 11 | 12 | double ordinate(double x, double lo, double hi, bool wrap); 13 | 14 | double wrapOrdinate(double x, double lo, double hi); 15 | 16 | int do_repulsion(NumericMatrix xyr, NumericVector weights, int c0, int c1, 17 | double xmin, double xmax, double ymin, double ymax, bool wrap); 18 | 19 | 20 | // Attempts to position circles without overlap. 21 | // 22 | // Given an input matrix of circle positions and sizes, attempts to position them 23 | // without overlap by iterating the pair-repulsion algorithm. 24 | // 25 | // @param xyr 3 column matrix (centre x, centre y, radius) 26 | // @param weights vector of double values between 0 and 1, used as multiplicative 27 | // weights for the distance a circle will move with pair-repulsion. 28 | // @param xmin lower X bound 29 | // @param xmax upper X bound 30 | // @param ymin lower Y bound 31 | // @param ymax upper Y bound 32 | // @param maxiter maximum number of iterations 33 | // @param wrap true to allow coordinate wrapping across opposite bounds 34 | // 35 | // @return the number of iterations performed. 36 | // 37 | // [[Rcpp::export]] 38 | int iterate_layout(NumericMatrix xyr, 39 | NumericVector weights, 40 | double xmin, double xmax, 41 | double ymin, double ymax, 42 | int maxiter, 43 | bool wrap) { 44 | 45 | int rows = xyr.nrow(); 46 | int iter; 47 | 48 | for (iter = 0; iter < maxiter; iter++) { 49 | int moved = 0; 50 | for (int i = 0; i < rows-1; ++i) { 51 | for (int j = i+1; j < rows; ++j) { 52 | if (do_repulsion(xyr, weights, i, j, xmin, xmax, ymin, ymax, wrap)) { 53 | moved = 1; 54 | } 55 | } 56 | } 57 | if (!moved) break; 58 | } 59 | 60 | return iter; 61 | } 62 | 63 | 64 | /* 65 | * Checks if two circles overlap excessively and, if so, moves them 66 | * apart. The distance moved by each circle is proportional to the 67 | * radius of the other to give some semblance of intertia. 68 | * 69 | * xyr - 3 column matrix of circle positions and sizes (x, y, radius) 70 | * c0 - index of first circle 71 | * c1 - index of second circle 72 | * xmin - bounds min X 73 | * xmax - bounds max X 74 | * ymin - bounds min Y 75 | * ymax - bounds max Y 76 | * wrap - allow coordinate wrapping across opposite bounds 77 | */ 78 | int do_repulsion(NumericMatrix xyr, 79 | NumericVector weights, 80 | int c0, int c1, 81 | double xmin, double xmax, 82 | double ymin, double ymax, 83 | bool wrap) { 84 | 85 | // if both weights are zero, return zero to indicate 86 | // no movement 87 | if (almostZero(weights[c0]) && almostZero(weights[c1])) return 0; 88 | 89 | double dx = xyr(c1, 0) - xyr(c0, 0); 90 | double dy = xyr(c1, 1) - xyr(c0, 1); 91 | double d = sqrt(dx*dx + dy*dy); 92 | double r = xyr(c1, 2) + xyr(c0, 2); 93 | double p, w0, w1; 94 | 95 | if (gtZero(r - d)) { 96 | if (almostZero(d)) { 97 | // The two centres are coincident or almost so. 98 | // Arbitrarily move along x-axis 99 | p = 1.0; 100 | dx = r - d; 101 | } else { 102 | p = (r - d) / d; 103 | } 104 | 105 | w0 = weights[c0] * xyr(c1, 2) / r; 106 | w1 = weights[c1] * xyr(c0, 2) / r; 107 | 108 | xyr(c1, 0) = ordinate( xyr(c1, 0) + p*dx*w1, xmin, xmax, wrap ); 109 | xyr(c1, 1) = ordinate( xyr(c1, 1) + p*dy*w1, ymin, ymax, wrap ); 110 | xyr(c0, 0) = ordinate( xyr(c0, 0) - p*dx*w0, xmin, xmax, wrap ); 111 | xyr(c0, 1) = ordinate( xyr(c0, 1) - p*dy*w0, ymin, ymax, wrap ); 112 | 113 | return(1); 114 | } 115 | 116 | return(0); 117 | } 118 | 119 | /* 120 | * Adjust an X or Y ordinate to the given bounds by either wrapping 121 | * (if `wrap` is true) or clamping (if `wrap` is false). 122 | */ 123 | double ordinate(double x, double lo, double hi, bool wrap) { 124 | if (wrap) return wrapOrdinate(x, lo, hi); 125 | else return std::max(lo, std::min(hi, x)); 126 | } 127 | 128 | 129 | /* 130 | * Map an X or Y ordinate to the toroidal interval [lo, hi). 131 | * 132 | * x - X or Y ordinate to be adjusted 133 | * lo - lower coordinate bound 134 | * hi - upper coordinate bound 135 | */ 136 | double wrapOrdinate(double x, double lo, double hi) { 137 | double w = hi - lo; 138 | while (x < lo) x += w; 139 | while (x >= hi) x -= w; 140 | return x; 141 | } 142 | 143 | bool almostZero(double x) { 144 | static double TOL = 0.00001; 145 | 146 | return std::abs(x) < TOL; 147 | } 148 | 149 | bool gtZero(double x) { 150 | return !almostZero(x) && (x > 0.0); 151 | } 152 | -------------------------------------------------------------------------------- /src/pads_circle_pack.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Implements a circle packing algorithm described in: 3 | * 4 | * Charles R. Collins & Kenneth Stephenson (2003) A circle packing algorithm. 5 | * Computation Geometry Theory and Applications 25: 233-256. 6 | * 7 | * The algorithm takes a graph which specifies a desired pattern of circle 8 | * tangencies and searchs for an arrangement of circle positions and sizes 9 | * which satisfy that pattern. 10 | * 11 | * This implementation is based on Python code by David Eppstein: specifically 12 | * the file CirclePack.py which is part of PADS (Python Algorithms and 13 | * Data Structures) library, available at: https://www.ics.uci.edu/~eppstein/PADS/ 14 | * 15 | * Original license header: 16 | * 17 | * PADS is licensed under the MIT Licence (https://opensource.org/licenses/MIT): 18 | * 19 | * Copyright (c) 2002-2015, David Eppstein 20 | * 21 | * Permission is hereby granted, free of charge, to any person obtaining a copy 22 | * of this software and associated documentation files (the "Software"), to deal 23 | * in the Software without restriction, including without limitation the rights 24 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | * copies of the Software, and to permit persons to whom the Software is 26 | * furnished to do so, subject to the following conditions: 27 | * 28 | * The above copyright notice and this permission notice shall be included in 29 | * all copies or substantial portions of the Software. 30 | * 31 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 37 | * THE SOFTWARE. 38 | * 39 | */ 40 | 41 | #define STRICT_R_HEADERS 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | using namespace Rcpp; 48 | 49 | using std::map; 50 | using std::pair; 51 | using std::vector; 52 | using std::complex; 53 | 54 | const double Tolerance = 1.0 + 1.0e-8; 55 | 56 | // Utility functions defined in pack_circles.cpp 57 | bool almostZero(double x); 58 | bool gtZero(double x); 59 | 60 | 61 | // Tests if a map contains a given key. 62 | template 63 | bool contains(std::map m, K k) { 64 | return m.count(k) > 0; 65 | } 66 | 67 | 68 | // Computes the angle at a circle of radius rx given by two circles of 69 | // radius ry and rz respectively which are tangential to circle x and 70 | // each other. 71 | // 72 | double acxyz(double rx, double ry, double rz) { 73 | double denom = 2 * (rx + ry) * (rx + rz); 74 | if (almostZero(denom)) return M_PI; 75 | 76 | double num = pow(rx + ry, 2) + pow(rx + rz, 2) - pow(ry + rz, 2); 77 | double term = num / denom; 78 | 79 | if (term < -1.0 || term > 1.0) return M_PI / 3; 80 | else return acos(term); 81 | } 82 | 83 | 84 | // Computes the angle sum around a given internal circle. 85 | // 86 | double flower(const map& radius, 87 | const int center, 88 | const vector& cycle) { 89 | 90 | const int nc = cycle.size(); 91 | const double rc = radius.at(center); 92 | double sum = 0.0; 93 | 94 | for (int i = 0; i < nc; i++) { 95 | int j = i + 1 == nc ? 0 : i + 1; 96 | sum += acxyz(rc, radius.at(cycle.at(i)), radius.at(cycle.at(j))); 97 | } 98 | 99 | return sum; 100 | } 101 | 102 | // Recursively find centers of all circles surrounding k. 103 | // The placements argument is modified in place. 104 | // 105 | void place(map >& placements, 106 | const map& radii, 107 | const map >& internal, 108 | const int centre) { 109 | 110 | // If the centre circle ID is not in the internal map 111 | // there is nothing to do 112 | if ( !contains(internal, centre) ) return; 113 | 114 | vector cycle = internal.at(centre); 115 | const int nc = cycle.size(); 116 | const double rcentre = radii.at(centre); 117 | 118 | const complex minusI = complex(0.0, -1.0); 119 | 120 | for (int i = -nc; i < nc-1; i++) { // loop indices as per Python version 121 | int ks = i < 0 ? nc + i : i; 122 | int s = cycle.at(ks); 123 | double rs = radii.at(s); 124 | 125 | int kt = ks + 1 < nc ? ks + 1 : 0; 126 | int t = cycle.at(kt); 127 | double rt = radii.at(t); 128 | 129 | if ( contains(placements, s) && !contains(placements, t) ) { 130 | double theta = acxyz(rcentre, rs, rt); 131 | 132 | complex offset = (placements.at(s) - placements.at(centre)) / 133 | complex(rs + rcentre); 134 | 135 | offset = offset * exp( minusI * theta ); 136 | 137 | placements[t] = placements.at(centre) + offset * (rt + rcentre); 138 | 139 | place(placements, radii, internal, t); 140 | } 141 | } 142 | } 143 | 144 | // Finds a circle packing for the given configuration of internal and 145 | // external circles. 146 | // 147 | // The two arguments are maps with disjoint keys (integer circle IDs). 148 | // For the internal map, keys are internal circle IDs and values are vectors 149 | // of neighbouring circle IDs. For the external map, keys are external circle 150 | // IDs and values are radii. 151 | // 152 | // Returns a map with circle ID as key and pair values 153 | // representing circle centre and radius. 154 | // 155 | std::map, double> > CirclePack( 156 | map > internal, 157 | map external) { 158 | 159 | // There should be no zero or negative values in external 160 | for (map::iterator it = external.begin(); it != external.end(); ++it) { 161 | if (!gtZero(it->second)) Rcpp::stop("external radii must be positive"); 162 | } 163 | 164 | // Copy the external map 165 | map radii( external ); 166 | 167 | for (map >::iterator it = internal.begin(); 168 | it != internal.end(); ++it) { 169 | 170 | if (contains(external, it->first)) { 171 | std::string msg("ID found in both internal and external map keys: "); 172 | msg += Rcpp::toString(it->first); 173 | Rcpp::stop(msg); 174 | } 175 | 176 | radii[it->first] = 1.0; 177 | } 178 | 179 | // The main iteration for finding the correct set of radii 180 | double lastChange = Tolerance + 1; 181 | while (lastChange > Tolerance) { 182 | lastChange = 1.0; 183 | for (map >::iterator it = internal.begin(); 184 | it != internal.end(); ++it) { 185 | 186 | int k = it->first; 187 | vector& cycle = it->second; 188 | int cycleLen = cycle.size(); 189 | 190 | double theta = flower(radii, k, cycle); 191 | double hat = radii[k] / (1.0 / sin(theta / (2*cycleLen)) - 1); 192 | double newrad = hat * (1.0 / sin(M_PI / cycleLen) - 1); 193 | 194 | double kc = std::max(newrad / radii[k], radii[k] / newrad); 195 | lastChange = std::max(lastChange, kc); 196 | 197 | radii[k] = newrad; 198 | } 199 | } 200 | 201 | // Recursively place all the circles 202 | map > placements; 203 | 204 | int k1 = internal.begin()->first; // pick one internal circle 205 | placements[k1] = complex(0.0); // place it at the origin 206 | 207 | int k2 = internal[k1].at(0); // pick one of its neighbors 208 | placements[k2] = complex(radii[k1] + radii[k2]); // place it on the real axis 209 | place(placements, radii, internal, k1); // recursively place the rest 210 | place(placements, radii, internal, k2); 211 | 212 | map, double> > out; 213 | for (map::iterator it = radii.begin(); it != radii.end(); ++it) { 214 | int k = it->first; 215 | out[k] = pair, double>(placements[k], radii[k]); 216 | } 217 | 218 | return out; 219 | } 220 | 221 | 222 | // Finds a circle packing for the given configuration of internal and 223 | // external circles. This is the interface function for R. 224 | // 225 | // The two arguments are maps with disjoint keys (integer circle IDs). 226 | // For the internal map, keys are internal circle IDs and values are vectors 227 | // of neighbouring circle IDs. For the external map, keys are external circle 228 | // IDs and values are radii. 229 | // 230 | // Returns a List (attributed as a data.frame for R) with columns for circle ID, 231 | // centre X, centre Y and radius. 232 | // 233 | // [[Rcpp::export]] 234 | List doCirclePack(List internalList, DataFrame externalDF) { 235 | 236 | // internalList is a list of vectors where, in each vector, 237 | // the first element is circle ID and the remaining elements are 238 | // IDs of the neighbouring circles. 239 | map > internal; 240 | for (int i = 0; i < internalList.size(); i++) { 241 | IntegerVector v = internalList(i); 242 | int id = v(0); 243 | 244 | vector nbrs; 245 | for (int j = 1; j < v.size(); j++) nbrs.push_back(v(j)); 246 | 247 | internal[id] = nbrs; 248 | } 249 | 250 | // externalDF is a DataFrame with cols ID (int) and radius (double) 251 | map external; 252 | IntegerVector ids = externalDF[0]; 253 | NumericVector radii = externalDF[1]; 254 | 255 | for (int i = 0; i < ids.size(); i++) { 256 | external[ ids[i] ] = radii[i]; 257 | } 258 | 259 | std::map, double> > packing( CirclePack(internal, external) ); 260 | 261 | int N = packing.size(); 262 | 263 | int k = 0; 264 | IntegerVector out_ids(N); 265 | NumericVector out_xs(N); 266 | NumericVector out_ys(N); 267 | NumericVector out_radii(N); 268 | StringVector out_rownames(N); 269 | 270 | for (map, double> >::iterator it = packing.begin(); 271 | it != packing.end(); ++it) { 272 | 273 | out_ids(k) = it->first; 274 | 275 | std::complex c = it->second.first; 276 | out_xs(k) = c.real(); 277 | out_ys(k) = c.imag(); 278 | 279 | double r = it->second.second; 280 | out_radii(k) = r; 281 | 282 | out_rownames(k) = Rcpp::toString(k+1); 283 | 284 | k++ ; 285 | } 286 | 287 | List out_frame = List::create( 288 | _["id"] = out_ids, 289 | _["x"] = out_xs, 290 | _["y"] = out_ys, 291 | _["radius"] = out_radii ); 292 | 293 | out_frame.attr("class") = "data.frame"; 294 | out_frame.attr("row.names") = out_rownames; 295 | 296 | return out_frame; 297 | } 298 | -------------------------------------------------------------------------------- /src/pmenzel_circle_pack.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Progressive circle packing algorithm. 3 | * 4 | * This is a port of C code written by Peter Menzel (https://github.com/pmenzel/packCircles) 5 | * and ported to R/Rcpp with his permission. 6 | * 7 | * ========== Original header (minus compilation and usage notes) ========== 8 | * 9 | * packCircles 1.0 10 | * 11 | * author: Peter Menzel 12 | * 13 | * ABOUT 14 | * ******** 15 | * packCircles arranges a list of circles, which are denoted by their radii, 16 | * by consecutively placing each circle externally tangent to two previously placed 17 | * circles avoiding overlaps. 18 | * 19 | * The program implements the algorithm described in the paper: 20 | * "Visualization of large hierarchical data by circle packing" 21 | * by Weixin Wang, Hui Wang, Guozhong Dai, and Hongan Wang 22 | * in Proceedings of the SIGCHI Conference on Human Factors in Computing Systems, 2006, pp. 517-520 23 | * https://doi.org/10.1145/1124772.1124851 24 | * 25 | * Source code is partially based on a implementation of this algorithm 26 | * in the ProtoVis javascript library: 27 | * https://mbostock.github.io/protovis/ 28 | * 29 | * 30 | * ========== Original license (BSD 2-clause simplified) ========== 31 | * 32 | * Copyright (c) 2016, Peter Menzel 33 | * All rights reserved. 34 | * 35 | * Redistribution and use in source and binary forms, with or without 36 | * modification, are permitted provided that the following conditions are met: 37 | * 38 | * - Redistributions of source code must retain the above copyright notice, this 39 | * list of conditions and the following disclaimer. 40 | * 41 | * - Redistributions in binary form must reproduce the above copyright notice, 42 | * this list of conditions and the following disclaimer in the documentation 43 | * and/or other materials provided with the distribution. 44 | * 45 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 46 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 47 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 48 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 49 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 50 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 51 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 52 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 53 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 54 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 55 | * 56 | */ 57 | 58 | #define STRICT_R_HEADERS 59 | #include 60 | #include 61 | using namespace Rcpp; 62 | 63 | const double INTERSECTION_TOL = 1.0e-4; 64 | 65 | 66 | class Node { 67 | public: 68 | Node() { init(); } 69 | 70 | Node(double r_) { 71 | init(); 72 | radius = r_; 73 | } 74 | 75 | void init() { 76 | x = 0.0; 77 | y = 0.0; 78 | radius = 0.0; 79 | next = NULL; 80 | prev = NULL; 81 | insertnext = NULL; 82 | } 83 | 84 | // Check for intersection with another node 85 | bool intersects(Node* n) { 86 | double dx = x - n->x; 87 | double dy = y - n->y; 88 | double dr = radius + n->radius; 89 | 90 | return ((dr * dr - dx * dx - dy * dy) > INTERSECTION_TOL); 91 | } 92 | 93 | // Place this node after node `a` 94 | void place_after(Node *a) { 95 | Node *n = a->next; 96 | a->next = this; 97 | this->prev = a; 98 | this->next = n; 99 | if (n) n->prev = this; 100 | } 101 | 102 | // Splice this node before node `a` 103 | void splice(Node *a) { 104 | this->next = a; 105 | a->prev = this; 106 | } 107 | 108 | double radius; 109 | double x; 110 | double y; 111 | 112 | Node* next; 113 | Node* prev; 114 | Node* insertnext; 115 | }; 116 | 117 | 118 | void place_circle(Node* a, Node* b, Node* c) { 119 | double da = b->radius + c->radius; 120 | double db = a->radius + c->radius; 121 | double dx = b->x - a->x; 122 | double dy = b->y - a->y; 123 | double dc = sqrt(dx * dx + dy * dy); 124 | if (dc > 0.0) { 125 | double cos = (db * db + dc * dc - da * da) / (2 * db * dc); 126 | double theta = acos(cos); 127 | double x = cos * db; 128 | double h = sin(theta) * db; 129 | dx /= dc; 130 | dy /= dc; 131 | 132 | c->x = a->x + x * dx + h * dy; 133 | c->y = a->y + x * dy - h * dx; 134 | } 135 | else { 136 | c->x = a->x + db; 137 | c->y = a->y; 138 | } 139 | } 140 | 141 | 142 | void place_circles(Node* firstnode) { 143 | Node * a = firstnode; 144 | Node * b = NULL; 145 | Node * c = NULL; 146 | 147 | // First circle 148 | a->x = -1 * a->radius; 149 | 150 | // Second circle 151 | if(!a->insertnext) return; 152 | b = a->insertnext; 153 | b->x = b->radius; 154 | b->y = 0; 155 | 156 | // Third circle 157 | if(!b->insertnext) return; 158 | c = b->insertnext; 159 | place_circle(a, b, c); 160 | if (!c->insertnext) return; 161 | 162 | // Initial node chain 163 | // -> a <--> c <--> b <- 164 | a->next = c; 165 | a->prev = b; 166 | b->next = a; 167 | b->prev = c; 168 | c->next = b; 169 | c->prev = a; 170 | b = c; 171 | 172 | c = c->insertnext; 173 | bool skip = false; 174 | 175 | while(c) { 176 | // pmenzel's comment: 177 | // Determine the node a in the chain, which is nearest to the center 178 | // The new node c will be placed next to a (unless overlap occurs) 179 | // NB: This search is only done the first time for each new node, i.e. 180 | // not again after splicing. 181 | if(!skip) { 182 | Node* n = a; 183 | Node* nearestnode = n; 184 | double nearestdist = FLT_MAX; 185 | 186 | do { 187 | double dist_n = sqrt(n->x * n->x + n->y * n->y); 188 | if(dist_n < nearestdist) { 189 | nearestdist = dist_n; 190 | nearestnode = n; 191 | } 192 | n = n->next; 193 | } while(n != a); 194 | 195 | a = nearestnode; 196 | b = nearestnode->next; 197 | skip=false; 198 | } 199 | 200 | place_circle(a, b, c); 201 | 202 | // Search for possible closest intersection 203 | bool isect = false; 204 | Node* j = b->next; 205 | Node* k = a->prev; 206 | 207 | double sj = b->radius; 208 | double sk = a->radius; 209 | 210 | do { 211 | if (sj <= sk) { 212 | if ( j->intersects(c) ) { 213 | a->splice(j); 214 | b = j; 215 | skip = true; 216 | isect = true; 217 | break; 218 | } 219 | sj += j->radius; 220 | j = j->next; 221 | } 222 | else { 223 | if( c->intersects(k) ) { 224 | k->splice(b); 225 | a = k; 226 | skip = true; 227 | isect = true; 228 | break; 229 | } 230 | sk += k->radius; 231 | k = k->prev; 232 | } 233 | } while (j != k->next); 234 | 235 | // Update the node chain 236 | if(!isect) { 237 | c->place_after(a); 238 | b = c; 239 | 240 | skip = false; 241 | 242 | c = c->insertnext; 243 | } 244 | } 245 | } 246 | 247 | 248 | // [[Rcpp::export]] 249 | DataFrame do_progressive_layout(NumericVector radii) { 250 | int N = radii.length(); 251 | 252 | Node *node = new Node(radii(0)); 253 | Node *firstnode = node; 254 | Node *lastinsertednode = node; 255 | 256 | for (int i = 1; i < N; i++) { 257 | node = new Node(radii[i]); 258 | lastinsertednode->insertnext = node; 259 | 260 | lastinsertednode = node; 261 | } 262 | 263 | place_circles(firstnode); 264 | 265 | NumericVector xs(N); 266 | NumericVector ys(N); 267 | 268 | // Retrieve circle positions and free memory 269 | node = firstnode; 270 | int i = 0; 271 | while (node) { 272 | xs[i] = node->x; 273 | ys[i] = node->y; 274 | radii[i] = node->radius; 275 | node = node->insertnext; 276 | i++; 277 | } 278 | 279 | node = firstnode; 280 | while (node) { 281 | Node *next = node->insertnext; 282 | delete node; 283 | node = next; 284 | } 285 | 286 | return DataFrame::create( 287 | Named("x") = xs, 288 | Named("y") = ys, 289 | Named("radius") = radii); 290 | } 291 | 292 | -------------------------------------------------------------------------------- /src/select_non_overlapping.cpp: -------------------------------------------------------------------------------- 1 | #define STRICT_R_HEADERS 2 | #include 3 | using namespace Rcpp; 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | // Class to generate random integers using 12 | // R runif in the background. 13 | class RandomInts { 14 | public: 15 | const static int NStore = 1000; 16 | 17 | RandomInts() { 18 | store = runif(NStore); 19 | pos = 0; 20 | } 21 | 22 | int nextInt(int max) { 23 | int r = (int)(store[pos] * (max + 1)); 24 | increment_pos(); 25 | return r; 26 | } 27 | 28 | private: 29 | int pos; 30 | NumericVector store; 31 | 32 | void increment_pos() { 33 | pos += 1; 34 | if (pos >= NStore) { 35 | store = runif(NStore); 36 | pos = 0; 37 | } 38 | } 39 | }; 40 | 41 | RandomInts RANDOM; 42 | 43 | 44 | // State values for circles 45 | const int Selected = 1; 46 | const int Candidate = 0; 47 | const int Rejected = -1; 48 | 49 | 50 | // Options for order of removal 51 | const StringVector OrderingLabels = StringVector::create( 52 | "maxov", "minov", "largest", "smallest", "random" 53 | ); 54 | 55 | enum OrderingCodes { 56 | ORDER_MAXOV, 57 | ORDER_MINOV, 58 | ORDER_LARGEST, 59 | ORDER_SMALLEST, 60 | ORDER_RANDOM 61 | }; 62 | 63 | 64 | class Circle { 65 | public: 66 | Circle(double x_, double y_, double r_) : 67 | x(x_), y(y_), radius(r_), state(Candidate) {} 68 | 69 | bool intersects(const Circle& other, double tolerance) { 70 | double dx = x - other.x; 71 | double dy = y - other.y; 72 | double rsum = radius + other.radius; 73 | 74 | return (dx*dx + dy*dy < rsum * rsum * tolerance); 75 | } 76 | 77 | double x; 78 | double y; 79 | double radius; 80 | int state; 81 | }; 82 | 83 | 84 | class Circles { 85 | public: 86 | Circles(NumericMatrix xyr, double tolerance) { 87 | const int N = xyr.nrow(); 88 | 89 | for (int i = 0; i < N; i++) { 90 | _circles.push_back( Circle(xyr(i, 0), xyr(i, 1), xyr(i, 2)) ); 91 | _neighbours.push_back( vector() ); 92 | } 93 | 94 | // Record overlaps for each circle in the initial configuration 95 | for (int i = 0; i < N-1; i++) { 96 | Circle& ci = _circles.at(i); 97 | 98 | for (int j = i+1; j < N; j++) { 99 | Circle& cj = _circles.at(j); 100 | 101 | if (ci.intersects(cj, tolerance)) { 102 | _neighbours.at(i).push_back(j); 103 | _neighbours.at(j).push_back(i); 104 | } 105 | } 106 | } 107 | } 108 | 109 | 110 | // Finds a subset of non-overlapping circles 111 | LogicalVector select_circles(const int ordering) { 112 | const int N = _circles.size(); 113 | int ndone = 0; 114 | 115 | while (ndone < N) { 116 | IntegerVector nbrCount(N, 0); 117 | for (int i = 0; i < N; i++) { 118 | Circle& ci = _circles.at(i); 119 | 120 | if (ci.state == Candidate) { 121 | nbrCount[i] = count_neighbours(i); 122 | 123 | if (nbrCount[i] == 0) { 124 | ci.state = Selected; 125 | ndone++ ; 126 | } 127 | } 128 | } 129 | 130 | if (ndone < N) { 131 | // Find circle(s) with current min or max number of overlaps, 132 | // depending on the argument bigFirst, and randomly choose one 133 | // for removal 134 | LogicalVector b(nbrCount.length()); 135 | switch (ordering) { 136 | case ORDER_MAXOV: 137 | b = nbrCount == max(nbrCount); break; 138 | 139 | case ORDER_MINOV: 140 | { 141 | IntegerVector x = nbrCount[nbrCount > 0]; 142 | int val = min(x); 143 | b = nbrCount == val; 144 | } 145 | break; 146 | 147 | case ORDER_LARGEST: 148 | b = flag_largest(nbrCount > 0); break; 149 | 150 | case ORDER_SMALLEST: 151 | b = flag_smallest(nbrCount > 0); break; 152 | 153 | case ORDER_RANDOM: 154 | b = nbrCount > 0; break; 155 | } 156 | 157 | IntegerVector ids = which(b); 158 | int removeId = sample_one_of(ids); 159 | 160 | _circles.at(removeId).state = Rejected; 161 | 162 | ndone++ ; 163 | } 164 | } 165 | 166 | LogicalVector sel(N, false); 167 | for (int i = 0; i < N; i++) { 168 | sel[i] = _circles.at(i).state == Selected; 169 | } 170 | 171 | return sel; 172 | } 173 | 174 | 175 | private: 176 | // Count Candidate neighbours of circle id. 177 | int count_neighbours(int id) { 178 | int n = 0; 179 | 180 | const vector& nbrs = _neighbours.at(id); 181 | 182 | if (!nbrs.empty()) { 183 | for (unsigned int k = 0; k < nbrs.size(); k++) { 184 | int nbrId = nbrs.at(k); 185 | if (_circles.at(nbrId).state == Candidate) n++ ; 186 | } 187 | } 188 | 189 | return n; 190 | } 191 | 192 | 193 | // helper function - returns a logical vector indicating which 194 | // circles are included and largest amongst included 195 | LogicalVector flag_largest(const LogicalVector& include) { 196 | NumericVector radii(_circles.size(), 0.0); 197 | for (unsigned int i = 0; i < _circles.size(); i++) { 198 | if (include[i]) { 199 | radii[i] = _circles.at(i).radius; 200 | } 201 | } 202 | 203 | return radii == max(radii); 204 | } 205 | 206 | 207 | // helper function - returns a logical vector indicating which 208 | // circles are included and largest amongst included 209 | LogicalVector flag_smallest(const LogicalVector& include) { 210 | NumericVector radii(_circles.size(), DBL_MAX); 211 | for (unsigned int i = 0; i < _circles.size(); i++) { 212 | if (include[i]) { 213 | radii[i] = _circles.at(i).radius; 214 | } 215 | } 216 | 217 | return radii == min(radii); 218 | } 219 | 220 | 221 | // helper function - gets indices of elements 222 | // in a LogicalVector that are true 223 | IntegerVector which(const LogicalVector& b) { 224 | IntegerVector ii = Range(0, b.length() - 1); 225 | return ii[b]; 226 | } 227 | 228 | 229 | // helper function - select a random element 230 | // from x 231 | int sample_one_of(const IntegerVector& x) { 232 | int n = x.length(); 233 | 234 | if (n < 2) return x[0]; 235 | else { 236 | int i = RANDOM.nextInt(n-1); 237 | return x[i]; 238 | } 239 | } 240 | 241 | 242 | vector _circles; 243 | vector< vector > _neighbours; 244 | }; 245 | 246 | 247 | 248 | // Function called from R. 249 | // 250 | // Takes a set of circles, each defined by centre xy coordinates 251 | // and radius, and iteratively selects those with no overlaps and 252 | // discards a random chosen one from those with the most overlaps. 253 | // 254 | // Returns a logical vector with selected = true. 255 | // 256 | // [[Rcpp::export]] 257 | LogicalVector select_non_overlapping(NumericMatrix xyr, 258 | const double tolerance, 259 | const StringVector& ordering) { 260 | 261 | int match = -1; 262 | try { 263 | for (int i = 0; i < OrderingLabels.length(); i++) { 264 | if ( OrderingLabels[i] == ordering[0] ) { 265 | match = i; 266 | break; 267 | } 268 | } 269 | 270 | if (match >= 0) { 271 | Circles cs(xyr, tolerance); 272 | return cs.select_circles(match); 273 | } 274 | else throw std::invalid_argument("Invalid ordering argument"); 275 | 276 | } catch (std::exception& ex) { 277 | forward_exception_to_r(ex); 278 | } 279 | 280 | return NA_LOGICAL; // not reached 281 | } 282 | 283 | -------------------------------------------------------------------------------- /vignettes/graph_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbedward/packcircles/469f143bc3264622fcb3174c6be13201089b24fc/vignettes/graph_example.png -------------------------------------------------------------------------------- /vignettes/graph_packing.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Graph-based circle packing" 3 | author: "Michael Bedward" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{Graph-based circle-packing} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | Another approach to circle packing is to begin with a specification of circle sizes and tangencies (ie. 13 | which circles touch which other circles) and then search for an arrangement satisfying this. Such an 14 | algorithm was described by Collins and Stephenson in a [2003 paper](https://doi.org/10.1016/S0925-7721(02)00099-8) in the journal Computation Geometry Theory and Applications. A version of their algorithm was implemented in Python by David Eppstein as part of his PADS library (see [CirclePack.py](https://www.ics.uci.edu/~eppstein/PADS/)). A port of this version to R/Rcpp is included in the packcircles package. 15 | 16 | In the figure below, the graph on the left represents the desired pattern of circle tangencies. Circles 5, 7, 8 and 9 are *internal*, while the remaining circles are *external*. The packing on the right shows an arrangement of circles which conforms to the input graph. 17 | 18 | ![Tangency graph and resulting packing](graph_example.png) 19 | 20 | The function `circleGraphLayout` implements a basic version of the algorithm. The following example produces a layout similar to the above figure: 21 | 22 | ```{r, eval=FALSE} 23 | 24 | library(packcircles) 25 | library(ggplot2) 26 | 27 | ## List of tangencies. Vector elements are circle IDs. 28 | ## The first element in each vector is an internal circle 29 | ## and the subsequent elements are its neighbours. 30 | internal <- list( 31 | c(9, 4, 5, 6, 10, 11), 32 | c(5, 4, 8, 6, 9), 33 | c(8, 3, 2, 7, 6, 5, 4), 34 | c(7, 8, 2, 1, 6) 35 | ) 36 | 37 | ## Specification of external circle radii. 38 | external <- data.frame(id = c(1, 2, 3, 4, 6, 10, 11), radius=10.0) 39 | 40 | ## The circleGraphLayout function is used to find an arrangement 41 | ## of circles corresponding to the tangencies specified by `internal` 42 | ## and the external circle sizes specified by `external`. The 43 | ## result is a four-column data.frame: id, x, y, radius. 44 | ## 45 | layout <- circleGraphLayout(internal, external) 46 | 47 | ## Get data for circle vertices. 48 | plotdat <- circleLayoutVertices(layout, xysizecols = 2:4, idcol = 1) 49 | 50 | ## Draw circles annotated with their IDs. 51 | ggplot() + 52 | geom_polygon(data=plotdat, aes(x, y, group=id), 53 | fill=NA, colour="black") + 54 | 55 | geom_text(data=layout, aes(x, y, label=id)) + 56 | 57 | coord_equal() + 58 | 59 | theme_bw() 60 | 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /vignettes/intro.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "An introduction" 3 | author: "Michael Bedward" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{An introduction} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include = FALSE} 13 | 14 | if (!requireNamespace("ggplot2", quietly = TRUE)) { 15 | # Cannot run graph code without ggplot2 so turn off chunk evaluation 16 | warning("Package ggplot2 is required for this vignette") 17 | knitr::opts_chunk$set(eval = FALSE) 18 | } 19 | 20 | ``` 21 | 22 | 23 | ## Circle packing algorithms 24 | 25 | A large number of circle packing algorithms, both deterministic and stochastic, have been developed. This package provides several of the simpler ones from which you can choose. 26 | 27 | * `circleRepelLayout` searches for a non-overlapping layout by iteratively moving 28 | circles through pair-wise repulsion. 29 | Circle positions are constrained to remain within a rectangular area (although this 30 | can be made infinite for unconstrained search). To avoid edge effects, the bounding area 31 | can be treated as a torus so that, for example, a circle pushed over the left-hand 32 | edge will re-enter the bounding area at the right-hand edge. This is a very simple 33 | and fairly inefficient algorithm but often produces good results. 34 | 35 | * `circleProgressiveLayout` consecutively places circles so that each is externally 36 | tangent to two previously placed circles. This algorithm is deterministic, although 37 | different layouts can be produced by changing the order of the input circles. It is 38 | also very efficient and, as such, suitable for working with large data sets. See its 39 | [vignette](progressive_packing.html) for examples. 40 | 41 | * `circleGraphLayout` attempts to find an arrangement that satisfies an input graph 42 | of adjacencies. The implementation in this package is experimental. 43 | See its [vignette](graph_packing.html) for details. 44 | 45 | 46 | ## A first example 47 | 48 | We will start by creating a set of circles of various sizes and then using the 49 | `circleRepelLayout` function to find a non-overlapping arrangement which we can display 50 | with ggplot. 51 | 52 | First we create a set of random circles, positioned within the central portion 53 | of a bounding square, with smaller circles being more common than larger ones. 54 | We will express circle size as area. This is the default for `circleRepelLayout` 55 | but you can also express size as radius and specify `sizetype = "radius" when 56 | calling the function. 57 | 58 | 59 | ```{r} 60 | 61 | set.seed(42) 62 | 63 | ncircles <- 200 64 | limits <- c(-40, 40) 65 | inset <- diff(limits) / 3 66 | maxarea <- 40 67 | 68 | areas <- rbeta(ncircles, 1, 5) * maxarea 69 | 70 | ``` 71 | 72 | Next, we use the `circleRepelLayout` function to try to find a non-overlapping 73 | arragement, allowing the circles to occupy any part of the bounding square. 74 | The returned value is a list with elements for the layout and the number 75 | of iterations performed. 76 | 77 | ```{r} 78 | 79 | library(packcircles) 80 | 81 | res <- circleRepelLayout(areas, xlim = limits, ylim = limits) 82 | 83 | cat(res$niter, "iterations performed") 84 | 85 | ``` 86 | 87 | The layout is returned as a data frame with circle centre coordinates and radii. 88 | 89 | ```{r} 90 | 91 | head( res$layout ) 92 | 93 | ``` 94 | 95 | We convert this to a data set of circle vertices, suitable for display with ggplot. The number of vertices can be controlled with the `npoints` argument but we use the default. 96 | 97 | The resulting data set has an integer `id` field which corresponds to the position or row of the circle in the original data passed to `circleRepelLayout`. 98 | 99 | ```{r} 100 | 101 | dat.gg <- circleLayoutVertices(res$layout, sizetype = "radius") 102 | 103 | head(dat.gg) 104 | 105 | ``` 106 | 107 | And now we can draw the layout. 108 | 109 | ```{r, fig.width=7, fig.height=4} 110 | 111 | library(ggplot2) 112 | 113 | t <- theme_bw() + 114 | theme(panel.grid = element_blank(), 115 | axis.text=element_blank(), 116 | axis.ticks=element_blank(), 117 | axis.title=element_blank()) 118 | 119 | theme_set(t) 120 | 121 | ggplot(data = dat.gg, aes(x, y, group = id)) + 122 | geom_polygon(colour="brown", fill="burlywood", alpha=0.3) + 123 | coord_equal(xlim=limits, ylim=limits) 124 | 125 | ``` 126 | 127 | 128 | ## Specifying initial circle positions 129 | 130 | In the previous example, where we passed a vector of circle sizes to `circleRepelLayout`, the function randomly assigned starting positions to the circles by placing them close to the centre of the bounding area. Alternatively, we can specify the initial positions beforehand. To illustrate this, we will initially position all of the circles near one corner of the bounding area. 131 | 132 | ```{r} 133 | 134 | dat.init <- data.frame( 135 | x = runif(ncircles, limits[1], limits[2]), 136 | y = runif(ncircles, limits[1], limits[2]) / 4.0, 137 | area = areas 138 | ) 139 | 140 | res <- circleRepelLayout(dat.init, xlim = limits, ylim = limits, 141 | xysizecols = 1:3) 142 | 143 | cat(res$niter, "iterations performed") 144 | 145 | ``` 146 | 147 | Next we display the initial and final layouts with ggplot. Notice that in our initial layout we expressed circle sizes as areas so we need to specify that when calling the `circleLayoutVertices` function, otherwise it assumes sizes are radii. 148 | 149 | ```{r, fig.width=7, fig.height=4} 150 | 151 | # Get vertex data for the initial layout where sizes are areas 152 | dat.gg.initial <- circleLayoutVertices(dat.init, sizetype = "area") 153 | 154 | # Get vertex data for the layout returned by the function where 155 | # sizes are radii 156 | dat.gg.final <- circleLayoutVertices(res$layout) 157 | 158 | dat.gg <- rbind( 159 | cbind(dat.gg.initial, set=1), 160 | cbind(dat.gg.final, set=2) 161 | ) 162 | 163 | ggplot(data = dat.gg, aes(x, y, group = id)) + 164 | geom_polygon(colour="brown", fill="burlywood", alpha=0.3) + 165 | 166 | coord_equal(xlim=limits, ylim=limits) + 167 | 168 | facet_wrap(~ set, 169 | labeller = as_labeller(c(`1` = "Initial layout", 170 | `2` = "Final layout"))) 171 | 172 | ``` 173 | 174 | ## Moving and fixed circles 175 | 176 | The `circleRepelLayout` function accepts an optional `weights` argument to give extra control over the movement of circles at each iteration of the layout algorithm. The argument takes a numeric vector with values in the range 0-1 inclusive (any values outside this range will be clamped to 0 or 1). A weight of 0 prevents a circle from moving at all while a weight of 1 allows full movement. 177 | 178 | To illustrate this, we will choose a few circles from the data set used ealier, make them larger and fix their position by setting their weights to 0.0. 179 | 180 | ```{r, fig.width=7, fig.height=4} 181 | 182 | # choose several arbitrary circles and make them the larger 183 | # than the others 184 | largest.ids <- sample(1:ncircles, 10) 185 | dat.init$area[largest.ids] <- 2 * maxarea 186 | 187 | # re-generate the vertex data for the initial circles, adding a column 188 | # to indicate if a circle is fixed (the largest) or free 189 | dat.gg.initial <- circleLayoutVertices(dat.init, sizetype = "area") 190 | 191 | dat.gg.initial$state <- ifelse(dat.gg.initial$id %in% largest.ids, "fixed", "free") 192 | 193 | # now re-run the layout algorithm with a weights vector to fix the position 194 | # of the largest circle 195 | wts <- rep(1.0, nrow(dat.init)) 196 | wts[ largest.ids ] <- 0.0 197 | 198 | res <- circleRepelLayout(dat.init, xlim = limits, ylim = limits, weights=wts) 199 | 200 | dat.gg.final <- circleLayoutVertices(res$layout) 201 | dat.gg.final$state <- ifelse(dat.gg.final$id %in% largest.ids, "fixed", "free") 202 | 203 | dat.gg <- rbind( 204 | cbind(dat.gg.initial, set = 1), 205 | cbind(dat.gg.final, set = 2) 206 | ) 207 | 208 | ggplot(data = dat.gg, aes(x, y, group=id, fill=state)) + 209 | 210 | geom_polygon(colour="brown1", show.legend = FALSE) + 211 | 212 | scale_fill_manual(breaks = c("fixed", "free"), values=c("brown4", NA)) + 213 | 214 | coord_equal(xlim=limits, ylim=limits) + 215 | 216 | facet_wrap(~ set, 217 | labeller = as_labeller(c(`1` = "Initial layout", 218 | `2` = "Final layout"))) 219 | 220 | ``` 221 | 222 | Notice that fixed circles that were overlapping in the initial layout are still overlapping in the final layout. Setting their weights to 0.0 resulted in them being ignored by `circleRepelLayout`. 223 | 224 | -------------------------------------------------------------------------------- /vignettes/progressive_packing.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Progressive circle packing" 3 | author: "Michael Bedward" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{Progressive circle packing} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | \usepackage[utf8]{inputenc} 10 | --- 11 | 12 | ```{r setup, include=FALSE} 13 | 14 | set.seed(42) 15 | 16 | library(packcircles) 17 | 18 | if (!requireNamespace("ggplot2", quietly = TRUE)) { 19 | # Cannot run graph code without ggplot2 so turn off chunk evaluation 20 | warning("Package ggplot2 is required for this vignette") 21 | knitr::opts_chunk$set(eval = FALSE) 22 | } 23 | 24 | ``` 25 | 26 | The function `circleProgressiveLayout` arranges a set of circles 27 | deterministically. The first two circles are placed to the left and right of the 28 | origin respectively. Subsequent circles are placed so that each: 29 | 30 | * is externally tangent to two previously placed circles; 31 | * does not overlap another circle; 32 | * and is as close as possible to the origin. 33 | 34 | The algorithm was described in the paper: *Visualization of large hierarchical 35 | data by circle packing* by [Weixin Wang et al. (2006)](https://doi.org/10.1145/1124772.1124851). 36 | The implementation in this package is based on a [version written in C by Peter 37 | Menzel](https://github.com/pmenzel/packCircles). 38 | 39 | The algorithm is very efficient and this, combined with the implementation in Rcpp, means arrangements for large numbers of circles can be found quickly. 40 | 41 | 42 | ## First example 43 | 44 | We begin by arranging 10 circles of various sizes. First we pass a 45 | vector of circle areas to the `circleProgressiveLayout` function. It returns a 46 | data frame of centre coordinates and radii. 47 | 48 | ```{r} 49 | 50 | areas <- c(20, 10, 40, rep(5, 7)) 51 | 52 | # Generate the layout 53 | packing <- circleProgressiveLayout(areas) 54 | 55 | head( round(packing, 2) ) 56 | 57 | ``` 58 | 59 | Next we derive a data frame of circle vertices for plotting using the 60 | `circleLayoutVertices` function, and then use ggplot to display the layout, labelling 61 | the circles to show their order of placement: 62 | 63 | ```{r fig.width=4, fig.height=4} 64 | 65 | library(ggplot2) 66 | 67 | t <- theme_bw() + 68 | theme(panel.grid = element_blank(), 69 | axis.text=element_blank(), 70 | axis.ticks=element_blank(), 71 | axis.title=element_blank()) 72 | 73 | theme_set(t) 74 | 75 | 76 | dat.gg <- circleLayoutVertices(packing, npoints=50) 77 | 78 | ggplot(data = dat.gg) + 79 | geom_polygon(aes(x, y, group = id), colour = "black", 80 | fill = "grey90", alpha = 0.7, show.legend = FALSE) + 81 | 82 | geom_text(data = packing, aes(x, y), label = 1:nrow(packing)) + 83 | 84 | coord_equal() 85 | 86 | ``` 87 | 88 | By default, `circleProgressiveLayout` takes the input sizes to be circle areas. 89 | If instead you have input radii, add `sizetype = "radius"` to the arguments when calling the 90 | function. 91 | 92 | 93 | ## Layouts are order dependent 94 | 95 | Re-ordering the input sizes will generally produce a different layout unless the sizes are uniform. Here we repeatedly shuffle the area values used above and generate a new layout each time. 96 | 97 | ```{r fig.width=7, fig.height=5} 98 | 99 | ncircles <- length(areas) 100 | nreps <- 6 101 | 102 | packings <- lapply( 103 | 1:nreps, 104 | function(i) { 105 | x <- sample(areas, ncircles) 106 | circleProgressiveLayout(x) 107 | }) 108 | 109 | packings <- do.call(rbind, packings) 110 | 111 | npts <- 50 112 | dat.gg <- circleLayoutVertices(packings, npoints = npts) 113 | 114 | dat.gg$rep <- rep(1:nreps, each = ncircles * (npts+1)) 115 | 116 | 117 | ggplot(data = dat.gg, aes(x, y)) + 118 | geom_polygon(aes(group = id), 119 | colour = "black", fill = "grey90") + 120 | 121 | coord_equal() + 122 | 123 | facet_wrap(~ rep, nrow = 2) 124 | 125 | ``` 126 | 127 | 128 | We can use this ordering effect to create some circle art... 129 | 130 | 131 | ```{r fig.width=7, fig.height=4} 132 | 133 | areas <- 1:1000 134 | 135 | # area: small to big 136 | packing1 <- circleProgressiveLayout(areas) 137 | dat1 <- circleLayoutVertices(packing1) 138 | 139 | # area: big to small 140 | packing2 <- circleProgressiveLayout( rev(areas) ) 141 | dat2 <- circleLayoutVertices(packing2) 142 | 143 | dat <- rbind( 144 | cbind(dat1, set = 1), 145 | cbind(dat2, set = 2) ) 146 | 147 | ggplot(data = dat, aes(x, y)) + 148 | geom_polygon(aes(group = id, fill = -id), 149 | colour = "black", show.legend = FALSE) + 150 | 151 | scale_fill_distiller(palette = "RdGy") + 152 | 153 | coord_equal() + 154 | 155 | facet_wrap(~set, 156 | labeller = as_labeller( 157 | c('1' = "small circles first", 158 | '2' = "big circles first")) 159 | ) 160 | 161 | ``` 162 | 163 | 164 | ## More detailed layout display 165 | 166 | The package includes an example data set of the abundance of different types of bacteria measured in a study of biofilms. Columns are value (abundance), display colour and label (bacterial taxon). 167 | 168 | ```{r} 169 | 170 | data("bacteria") 171 | head(bacteria) 172 | 173 | ``` 174 | 175 | The following example shows how to display the abundance values as circles filled with the specified colours. It relies on the fact that the `id` column in the output of `circleLayoutVertices` maps to the row number of the input data. 176 | 177 | Note: the y-axis is reversed so that the layour is rendered similarly to the example [here](https://github.com/pmenzel/packCircles). 178 | 179 | ```{r fig.width=5, fig.height=5, fig.align='center'} 180 | 181 | packing <- circleProgressiveLayout(bacteria) 182 | 183 | dat.gg <- circleLayoutVertices(packing) 184 | 185 | ggplot(data = dat.gg) + 186 | geom_polygon(aes(x, y, group = id, fill = factor(id)), 187 | colour = "black", 188 | show.legend = FALSE) + 189 | 190 | scale_fill_manual(values = bacteria$colour) + 191 | 192 | scale_y_reverse() + 193 | 194 | coord_equal() 195 | 196 | ``` 197 | 198 | 199 | As a further flourish, we can make the plot interactive so that the name of the bacterial taxon is displayed when the mouse cursor hovers a circle. 200 | 201 | Note: the `ggiraph` package is required for this. 202 | 203 | ```{r fig.width=5, fig.height=5} 204 | 205 | if (requireNamespace("ggiraph")) { 206 | 207 | gg <- ggplot(data = dat.gg) + 208 | ggiraph::geom_polygon_interactive( 209 | aes(x, y, group = id, fill = factor(id), 210 | tooltip = bacteria$label[id], data_id = id), 211 | colour = "black", 212 | show.legend = FALSE) + 213 | 214 | scale_fill_manual(values = bacteria$colour) + 215 | 216 | scale_y_reverse() + 217 | 218 | labs(title = "Hover over circle to display taxon name") + 219 | 220 | coord_equal() 221 | 222 | ggiraph::girafe(ggobj = gg, width_svg = 5, height_svg = 5) 223 | 224 | } 225 | 226 | ``` 227 | 228 | --------------------------------------------------------------------------------