├── .Rbuildignore ├── .travis.yml ├── CONTRIBUTING.md ├── CRAN.md ├── DESCRIPTION ├── DEVNOTES.md ├── NAMESPACE ├── NEWS.md ├── R ├── geom-car.R ├── geom-null.R ├── geom.car.data.R ├── ggbg-package.R ├── load.R ├── position-waterfall.R ├── proto.R ├── stat-waterfall.R └── sysdata.rda ├── README.Rmd ├── README.md ├── covr.R ├── data └── geom.car.data.rda ├── inst └── doc │ ├── extensions.R │ ├── extensions.Rmd │ └── extensions.html ├── man ├── figures │ ├── README-geom-car-1.png │ ├── README-geoms-1.png │ ├── README-geoms-2.png │ ├── README-stats-1.png │ ├── README-stats-2.png │ ├── README-waterfall-1.png │ └── README-waterfall-2.png ├── geom.car.data.Rd ├── geom_car.Rd ├── ggbg-ggproto.Rd ├── ggbg.Rd └── position_waterfall.Rd ├── tests ├── run.R └── unitizer │ ├── car.R │ ├── car.unitizer │ └── data.rds │ ├── waterfall.R │ └── waterfall.unitizer │ └── data.rds └── vignettes ├── extensions.Rmd └── styles.css /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^DEVNOTES\.md$ 4 | ^CONTRIBUTING\.md$ 5 | ^CRAN\.md$ 6 | ^\.travis\.yml$ 7 | .Rdata 8 | ^covr\.R 9 | ^extra$ 10 | ^scratch\.R 11 | ^README\.Rmd 12 | ^README\.html 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: r 2 | r: 3 | - oldrel 4 | - release 5 | - devel 6 | 7 | sudo: false 8 | 9 | cache: packages 10 | 11 | r_github_packages: tidyverse/ggplot2 12 | 13 | r_packages: 14 | - covr 15 | 16 | branches: 17 | only: 18 | - master 19 | - development 20 | - rc 21 | 22 | after_success: 23 | - Rscript covr.R 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to this package. To make sure you 4 | do not waste your time or mine, please read and follow the guidelines here. 5 | 6 | ## Reporting Issues 7 | 8 | Please create a: 9 | 10 | * minimal: as little code as possible. 11 | * reproducible: include the **minimal** input data. 12 | * example: include the expected output. 13 | 14 | Additionally: 15 | 16 | * Include output of `sessionInfo()` 17 | * Format the example code so that it can be copy-pasted into an R console 18 | 19 | ## Submitting PRs 20 | 21 | ### Before you Start 22 | 23 | Create an issue that highlights the problem, and describe how you hope to solve 24 | it. Do not be offended if your offer for help is refused. Accepting a PR 25 | creates a maintenance burden that I might not be willing to take on. 26 | 27 | I realize the requirements I lay out here are annoying. If they prevent you 28 | from making a contribution I am sorry and sympathize, having been on the 29 | opposite side of such requirements myself. Nonetheless the requirements stand 30 | to ensure your contribution does not end up creating more work than it 31 | saves. 32 | 33 | ### Requirements 34 | 35 | * Check the diff prior to submitting the PR and make sure there are no 36 | unnecessary changes (e.g. meaningless white space changes, etc.). 37 | * All PRs should be made as a new branch off of the "development" branch. 38 | * Every line of code you contribute should be tested as shown by `covr`. 39 | * Unit tests should be done in `unitizer`. 40 | * You should license all contributions you make with a license compatible with 41 | that of the package, and you should ensure you are the copyright holder for 42 | all the contributions. 43 | 44 | ### Style Guide 45 | 46 | Strict: 47 | 48 | * Wrap text at 80 columns 49 | * Indentations are 2 spaces 50 | * Strip trailing whitespace 51 | 52 | Suggested: 53 | 54 | * function_name() 55 | * object.name 56 | * FormalClassName 57 | * formalMethodName() 58 | 59 | ## Thank You! 60 | 61 | For taking the time to read these contribution guidelines. I apologize if they 62 | seem a little hostile, but time, yours and mine, is precious and it would be a 63 | shame to waste any of it. 64 | -------------------------------------------------------------------------------- /CRAN.md: -------------------------------------------------------------------------------- 1 | ## Submission Checklist 2 | 3 | * Review CRAN policy 4 | * Check version 5 | * Run tests with 6 | * winbuilder 7 | * valgrind 8 | * rchk 9 | * Check coverage 10 | 11 | ## Submission Notes: 12 | 13 | 14 | ## R CMD check --as-cran 15 | 16 | Status: OK 17 | 18 | ### Test Environments 19 | 20 | * Travis Ubuntu 14.04.5 LTS 21 | * R devel (2018-03-30 r74497) 22 | * R version 3.4.4 (2017-01-27) 23 | * R version 3.2.5 (2017-01-27) 24 | * Winbuilder 25 | * R version 3.4.4 (2017-01-27) 26 | * R version 3.5.0 alpha (2018-03-27 2018-03-30 r74498) 27 | * r-debug docker container, level 2 valgrind 28 | * R devel (2018-03-16 r74417) 29 | * Locally on Mac OS 10.12.6 30 | * R version 3.4.1 (2017-06-30) 31 | * rhub i386-pc-solaris2.10 (32-bit): 32 | * R version 3.4.1 Patched (2017-07-15 r72924) 33 | 34 | 35 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggbg 2 | Title: Assorted GGPlot Extensions 3 | Description: Collection of ggplot extensions. 4 | Version: 0.1.0 5 | Authors@R: c( 6 | person("Brodie", "Gaslam", email="brodie.gaslam@yahoo.com", 7 | role=c("aut", "cre")) 8 | ) 9 | Depends: 10 | R (>= 3.3.0) 11 | License: GPL (>=2) 12 | LazyData: true 13 | URL: https://github.com/brodieG/ggbg 14 | BugReports: https://github.com/brodieG/ggbg/issues 15 | VignetteBuilder: knitr 16 | Imports: 17 | ggplot2 (>= 2.2.1.9000), 18 | grid, 19 | vetr, 20 | utils 21 | Suggests: 22 | knitr, 23 | rmarkdown, 24 | unitizer 25 | RoxygenNote: 6.0.1.9000 26 | Roxygen: list(markdown = TRUE) 27 | Encoding: UTF-8 28 | Collate: 29 | 'geom.car.data.R' 30 | 'ggbg-package.R' 31 | 'geom-null.R' 32 | 'proto.R' 33 | 'geom-car.R' 34 | 'position-waterfall.R' 35 | 'stat-waterfall.R' 36 | 'load.R' 37 | -------------------------------------------------------------------------------- /DEVNOTES.md: -------------------------------------------------------------------------------- 1 | 2 | # Developer Notes 3 | 4 | ## Relevant docs 5 | 6 | * ggplot2 docs `?Geom`, etc. 7 | * ggplot2 vignette 8 | * [B. Rudis in-progress Book](https://rud.is/books/creating-ggplot2-extensions/) 9 | 10 | ## Waterfall 11 | 12 | ### geom / stat / position 13 | 14 | Some question of whether it should be a geom, or a position, or even a stat. 15 | Unfortunately the ggplot2 semantics don't really fit fully with any of this. 16 | Technically it's not really a geom since we're just making bars/rects here. 17 | We're translating the points though, which maybe is more like a position, except 18 | we're doing it across x values instead of within a value, so it's not a standard 19 | position adjustment. It's also not a stat since we're not changing the number of 20 | rows in the data. 21 | 22 | Given stacking doesn't work, maybe we should do this via position: 23 | waterfall-stack / waterfall-dodge. Even though it doesn't naturally meet with 24 | the ggplot2 semantics, the position adjustments appear to have all the data for 25 | each layer. 26 | 27 | ### stacking 28 | 29 | Need to figure out what the generic stacking mechanism is for various inputs: 30 | 31 | * `y` 32 | * `y` + `height` 33 | * `ymin` + `ymax` 34 | * `ymin` + `ymax` + `height` 35 | * `y` + `ymin` + `ymax` 36 | * `y` + `ymin` + `ymax` + `height` 37 | 38 | Let's take the case with `y` + `height`. What is the desired outcome? We want 39 | the graphical elements to align on top of each other. So as soon as there is 40 | height there is the potential for an element to float off of the x-axis. But 41 | then it becomes difficult to distinguish what it means for the waterfall to go 42 | up or down. If not floating then it is obvious based on the sign of y. If it 43 | is floating then it becomes a lot trickier as the semantics change completely. 44 | 45 | Alternate: If the entire "value" of our element is dictated purely by the `y` 46 | aesthetic this simplifies things a bit. How do we handle conflicts with 47 | pre-existing `ymin` and `ymax` values? We're going to hijack them, so if they 48 | already exist it is a bit of a problem. And should we also force `height` to 49 | conform? Seemingly no, both stack and dodge appear to operate purely by 50 | modifying `xmin/xmax/ymin/ymax`. 51 | 52 | So basically, we require a `y` value. If `ymax/min` values exist we over-write 53 | them with the adjustments, so height values will probably stop working? 54 | 55 | Actually, more important than anything is not hijacking existing xmin/xmax 56 | values, so we just need to apply the stacking purely based on `y` values, and 57 | whatever happens happens. The only sensible stacking is then on geoms that 58 | don't explicitly specify `ymin`/`ymax` values, or if they do, where the `y` 59 | is the same as `ymax` if it is positive, and the same as `ymin` if it is 60 | negative. 61 | 62 | ### Dodging 63 | 64 | This is actually a bit tricky for objects that come in with a width. For 65 | something like a bar where we don't explicitly define the width, it's 66 | straightforward. Same when the width are all the same. It gets a lot harder 67 | when we have different widths across different elements. Probably need to use 68 | the widest element as the target width, and then allocate relative to that. If 69 | we do "preserve" single, then use the same measure for all. 70 | 71 | Some complications given how this is done currently where there is limited 72 | flexibility for different widths, etc. If everything is given widths to begin 73 | with, we'll need to scale them all to fit within some width. Annoying 74 | scenarios: 75 | 76 | * some widths are missing or ridiculous 77 | * widths are not all the same, or all different 78 | * need to communicate what the overall max width is, which means changing how 79 | setup params works right now since it runs before setup data 80 | * reconciling dodging width with actual width 81 | 82 | Width of elements at x value should be width of widest at that value? But it is 83 | possible for specific groups to have different widths at different x values? Do 84 | we just not support this? Or only not support it in preserve mode? Yes, I 85 | think this is what we do. 86 | 87 | Do we support missing `x`? In theory we can do this with `x` + `width`, or 88 | `xmin` + `xmax`. I guess we should. We need something that will dodge/adjust 89 | any of `x`, `xmin`, `xmax` if they exist. 90 | 91 | * Geom width is `xmax` - `xmin`. 92 | * If there is no geom width, then use the dodge width. 93 | * Preserve only works if all the widths are the same. 94 | * If there is no dodge width, use the geom width. 95 | * Do we just ignore the `width` aesthetic if present? Yes, under the assumption 96 | that `xmin` and `xmax` will have been computed? What if we don't have dodge 97 | width? Do we take `width` aesthetic before default dodge? 98 | 99 | 100 | ## Ggplot Doc Issues 101 | 102 | ### ?Geom 103 | 104 | Seems like a paragraph is out of place in ?Geom: 105 | 106 | > Each of the ‘Geom’ objects is a ‘ggproto’ object, descended from the 107 | > top-level ‘Geom’, and each implements various methods and fields. To create a 108 | > new type of Geom object, you typically will want to implement one or more of 109 | > the following: 110 | 111 | And then there is another paragraph before the bullet point list. There is 112 | also conflict between "implement" and "override" in the first bullet point. 113 | 114 | ## Gotcha's 115 | 116 | You need to generate your first set of docs before you use any of the `ggproto` 117 | calls because those are evaluated at package load time, and will break 118 | documentation (and NAMESPACE) generation. 119 | 120 | ## Random Ggplot Analysis Notes 121 | 122 | Need to check where `draw_panel` is called. AFAICT this is through the 123 | `draw_layer` method of `Geom`, which invokes it as: 124 | 125 | ``` 126 | args <- c(list(quote(data), quote(panel_params), quote(coord)), params) 127 | plyr::dlply(data, "PANEL", function(data) { 128 | if (empty(data)) return(zeroGrob()) 129 | 130 | panel_params <- layout$panel_params[[data$PANEL[1]]] 131 | do.call(self$draw_panel, args) 132 | }, .drop = FALSE) 133 | ``` 134 | 135 | Since the arguments are not named, it is quite remarkable this works. Not sure 136 | how it is possible for this to work with the different signatures. 137 | 138 | Actually, by the looks of it `params` in the composition of `args` can include a 139 | named `self`. This from a traceback of `geom_rect`: 140 | 141 | ``` 142 | [17]] 143 | do.call(self$draw_panel, args) 144 | 145 | [[18]] 146 | (function (...) 147 | f(..., self = self))(data, panel_params, coord) 148 | 149 | [[19]] 150 | f(..., self = self) 151 | ``` 152 | 153 | Ah, and it looks that what's happening is that a `ggproto` method is not just 154 | the function you define, but a processed version of it that looks like: 155 | 156 | ``` 157 | 158 | 159 | function (...) 160 | f(..., self = self) 161 | 162 | 163 | function (self, data, panel_params, coord) 164 | { 165 | ``` 166 | 167 | This should mean that `self` is provided if it isn't otherwise, but if you do 168 | provide somehow in a direct call to `draw_panel` it then you must do so with a 169 | `self=self` form as otherwise it gets captured by the dots. 170 | 171 | Actually, for `geom_point` which doesn't have a `self` param, the call doesn't 172 | include `self`: 173 | 174 | ``` 175 | $ : language do.call(self$draw_panel, args) 176 | $ : language (function (...) f(...))(data, panel_params, coord, na.rm = FALSE) 177 | $ : language f(...) 178 | ``` 179 | 180 | Alright, looks like there is automated parameter trimming going on, which is why 181 | `GeomPoint` doesn't need `self`: 182 | 183 | ``` 184 | params <- params[intersect(names(params), self$parameters())] 185 | ``` 186 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(PositionWaterfall) 4 | export(StatWaterfall) 5 | export(geom_car) 6 | export(position_waterfall) 7 | import(ggplot2) 8 | import(vetr) 9 | importFrom(grid,gpar) 10 | importFrom(grid,grobTree) 11 | importFrom(grid,rasterGrob) 12 | importFrom(grid,rectGrob) 13 | importFrom(utils,globalVariables) 14 | importFrom(utils,head) 15 | importFrom(utils,tail) 16 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # ggbg Release Notes 2 | 3 | ## v0.1.0 4 | 5 | Initial release. 6 | 7 | 8 | -------------------------------------------------------------------------------- /R/geom-car.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | # Generate a car 'grob' using a baseline PNG; we don't store the actual PNG with 18 | # the package, instead we have the pre-stored car.raster variable in sysdata.rda 19 | # car.raster <- png::readPNG("~/Downloads/car2.png") 20 | 21 | # The `grid` grob actually responsible for rendering our car 22 | 23 | # nocov start 24 | # covr doesn't seem to correctly handle this 25 | 26 | carGrob <- function(x, y, length, width, gp) { 27 | grobTree( 28 | rectGrob( 29 | x, y, hjust=.5, height=width, width=length, 30 | gp = gp 31 | ), 32 | rasterGrob( 33 | car.raster, x=x, y=y, hjust=.5, height=width, width=length 34 | ) ) } 35 | # nocov end 36 | 37 | #' Renders the `geom_car` Layer 38 | #' 39 | #' `GeomCar` renders cars on a layer. It is quite limited, but a useful example 40 | #' for understanding how to create custom geoms with custom grobs. 41 | #' 42 | #' @rdname ggbg-ggproto 43 | #' @importFrom grid grobTree rasterGrob rectGrob gpar 44 | 45 | GeomCar <- ggproto("GeomCar", Geom, 46 | # Generate grobs from the data, we have to reconvert length/width so 47 | # that the transformations persist 48 | 49 | draw_panel=function(self, data, panel_params, coords) { 50 | with( 51 | coords$transform(data, panel_params), 52 | carGrob( 53 | x, y, length=xmax-xmin, width=ymax-ymin, 54 | gp=gpar( 55 | col = colour, fill = alpha(fill, alpha), 56 | lwd = size * .pt, lty = linetype, lineend = "butt" 57 | ) ) ) }, 58 | # Convert data to coordinates that will get transformed (length/width don't 59 | # normally). 60 | 61 | setup_data=function(self, data, params) { 62 | transform(data, 63 | xmin = x - length / 2, xmax = x + length / 2, 64 | ymin = y - width / 2, ymax = y + width / 2 65 | ) }, 66 | # Required and default aesthetics 67 | 68 | required_aes=c("x", "y", "length", "width"), 69 | default_aes = aes( 70 | colour = NA, fill = "grey35", size = 0.5, linetype = 1, alpha = NA 71 | ), 72 | # Use the car grob in the legend 73 | 74 | draw_key = function(data, params, size) { 75 | with( 76 | data, 77 | carGrob( 78 | 0.5, 0.5, length=.75, width=.5, 79 | gp = gpar( 80 | col = colour, fill = alpha(fill, alpha), 81 | lwd = size * .pt, lty = linetype, lineend = "butt" 82 | ) ) ) } 83 | ) 84 | #' Cars 85 | #' 86 | #' A geom that displays cars. This geom was implemented on a lark as an answer 87 | #' to a [question on SO](https://stackoverflow.com/questions/22159087/is-it-possible-to-draw-diagrams-in-r/22207979#22207979). 88 | #' 89 | #' @eval ggplot2:::rd_aesthetics("geom", "car") 90 | #' @param ... additional arguments passed on to `ggplot::layer`. 91 | #' @inheritParams ggplot2::layer 92 | #' @export 93 | #' @examples 94 | #' library(ggplot2) 95 | #' ggplot( 96 | #' geom.car.data, # ggbg data set 97 | #' aes(x=x, y=y, length=length, width=width, fill=label) 98 | #' ) + 99 | #' geom_hline(yintercept=seq(5, 35, by=10), color="white", size=2, linetype=2) + 100 | #' geom_car() + 101 | #' coord_equal() + 102 | #' theme(panel.background = element_rect(fill="#555555"), 103 | #' panel.grid.major = element_blank(), 104 | #' panel.grid.minor = element_blank()) 105 | 106 | geom_car <- function( 107 | mapping=NULL, data=NULL, ..., inherit.aes=TRUE, show.legend=NA 108 | ) { 109 | layer( 110 | data=data, mapping=mapping, geom=GeomCar, position="identity", 111 | stat="identity", show.legend = show.legend, inherit.aes = inherit.aes, 112 | params=list(...) 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /R/geom-null.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | ## A Simple Geom That does Nothing 18 | ## 19 | ## For testing purposes, this allows us to pass in whacky aesthetics without 20 | ## geom processing freaking out about it (and therefore letting other code run 21 | ## and fail / handle the whacky inputs) 22 | 23 | geom_null <- function(mapping=NULL, data=NULL, position=NULL, stat="identity") { 24 | layer( 25 | mapping=mapping, data=data, position=position, geom=GeomNull, 26 | stat="identity" 27 | ) 28 | } 29 | GeomNull <- ggproto("GeomNull", Geom, 30 | draw_panel = function(data, panel_params, coord) { 31 | zeroGrob() 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /R/geom.car.data.R: -------------------------------------------------------------------------------- 1 | #' Car Coordinates 2 | #' 3 | #' A data frame with car coordinates and sizes for use in examples. 4 | #' 5 | #' @format A data frame with 8 rows and 6 variables 6 | #' \describe{ 7 | #' \item{vehicle}{vehicle id} 8 | #' \item{x}{x coordinate} 9 | #' \item{y}{y coordinate} 10 | #' \item{length}{vehicle length} 11 | #' \item{width}{vehicle width} 12 | #' \item{label}{vehicle description} 13 | #' } 14 | 15 | "geom.car.data" 16 | -------------------------------------------------------------------------------- /R/ggbg-package.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | #' Assorted GGPlot Extensions. 18 | #' 19 | #' @docType package 20 | #' @importFrom utils globalVariables 21 | #' @import ggplot2 22 | #' @import vetr 23 | #' @name ggbg 24 | 25 | NULL 26 | 27 | # For `vetr` 28 | 29 | globalVariables(".") 30 | 31 | -------------------------------------------------------------------------------- /R/load.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | # nocov start 18 | .onLoad <- function(libname, pkgname) { 19 | 20 | .default.opts <- list( 21 | ggbg.signif=11L, 22 | ggbg.vjust.mode="end", 23 | ggbg.vjust=0.5 24 | ) 25 | existing.opts <- options() 26 | options(.default.opts[setdiff(names(.default.opts), names(existing.opts))]) 27 | } 28 | # nocov end 29 | -------------------------------------------------------------------------------- /R/position-waterfall.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | #' Stack Chart Elements on Cumulative Value 18 | #' 19 | #' A waterfall chart is a bar chart where each segment starts where the prior 20 | #' segment left off. This is similar to a stacked bar chart, except that 21 | #' the stacking does not reset across `x` values. It is the visualization of a 22 | #' cumulative sum. Another similar type of chart is the candlestick plot, 23 | #' except those have "whiskers", and typically require you to manually specify 24 | #' the `ymin` and `ymax` values. 25 | #' 26 | #' `position_waterfall` creates waterfall charts when it is applied to 27 | #' `geom_col` or `geom_bar`. You can apply it to any geom, so long as the 28 | #' geom specifies a `y` aesthetic, and either an `x` aesthetic, or both 29 | #' `xmin` and `xmax` aesthetics. It may not make sense to apply 30 | #' `position_waterfall` to arbitrary geoms, particularly those that represent 31 | #' single graphical elements with multiple x/y coordinates such as 32 | #' `geom_polygon`. `ymin`/`ymax` aesthetics will be shifted by the cumulative 33 | #' `y` value. 34 | #' 35 | #' Since stat layers are computed prior to position adjustments, you can also 36 | #' use `position_waterfall` with stats (e.g `stat_bin`, see examples). 37 | #' 38 | #' We also implement a [StatWaterfall][ggbg-ggproto] `ggproto` object that 39 | #' can be accessed within `geom_*` calls by specifying `stat='waterfall'`. 40 | #' Unlike typical stat `ggproto` objects, this one does not have a layer 41 | #' instantiation function (i.e. `stat_waterfall` does not exist). The sole 42 | #' purpose of the stat is to compute the `ycum` aesthetic that can then be used 43 | #' by the `geom` layer (see the labeling examples). 44 | #' 45 | #' @section Stacking: 46 | #' 47 | #' The stacking is always computed on the `y` aesthetic. The order of the 48 | #' stacking is determined by the `x` aesthetic. The actual position of the 49 | #' objects are also affected by `vjust`, and you may need to change the value of 50 | #' `vjust` if you are using `position_waterfall` with geoms other than columns. 51 | #' For example, for dimensionless elements such as `geom_point` and `geom_text`, 52 | #' the default `vjust` of 0.5 leads to alignment at the midpoint between 53 | #' previous and subsequent values in the cumulative sequence. 54 | #' 55 | #' If only `xmin` and `xmax` aesthetics are present the `x` value will be 56 | #' inferred as the midpoint of those two. 57 | #' 58 | #' @section Dodging: 59 | #' 60 | #' Unlike most `position_*` adjustments, `position_waterfall` adjust positions 61 | #' across different `x` values. However, we still need to resolve `x` 62 | #' value overlaps. The default approach is to apply the same type of adjustment 63 | #' across groups within any given `x` value. This stacks and dodges elements. 64 | #' 65 | #' Dodging involves changing the `width` of the geom and also shifting the 66 | #' `geom` horizontally. Geom width adjustments will always be made based on the 67 | #' `xmin`/`xmax`/`width` aesthetics. The shifting itself can be controlled 68 | #' separately with `position_waterfall(width=...)`. That parameter should 69 | #' really be called `dodge.width` to avoid confusion with the geom `width`, but 70 | #' we left it as `width` for consistency with [`position_dodge`]. 71 | #' 72 | #' You you can turn off dodging within `x` values by setting 73 | #' `position_waterfall(dodge=FALSE)` which will result in stacking within each 74 | #' `x` value. 75 | #' 76 | #' @inheritParams ggplot2::position_dodge 77 | #' @inheritParams ggplot2::position_stack 78 | #' @importFrom utils head tail 79 | #' @param dodge TRUE (default) or FALSE, controls how to resolve 80 | #' groups that overlap on the `x` axis. The default is to dodge them 81 | #' to form mini-waterfalls within each `x` value, but you can chose to stack 82 | #' them instead by setting `dodge=FALSE`. Negative and positive values are 83 | #' segregated prior to stacking so they do not overlap. Interpreting 84 | #' waterfall charts with stacked sub-groups is difficult when they contain 85 | #' negative values, so we recommend you use the default setting instead. 86 | #' Observations within a group that have the same `x` value are always 87 | #' stacked, so if you have both positive and negative values for any given `x` 88 | #' value you may want to consider segregating the positives and negatives in 89 | #' different groups. 90 | #' @param vjust like the `vjust` parameter for [`ggplot2::position_stack`], 91 | #' except that by default the direction of justification follows the direction 92 | #' of the bar (see `vjust.mode`), and the default value is `0.5` instead of 93 | #' `1`. This only has an effect on geoms with positions like text, points, or 94 | #' lines. The default setting places elements midway through the height of 95 | #' the corresponding waterfall step. The default value is convenient for 96 | #' labeling `geom_col` waterfalls. Use `1` to position at the "end" of each 97 | #' waterfall step. This is different to the `vjust` for geoms like 98 | #' `geom_text` where `vjust=1` shift the text down, but it is consistent with 99 | #' what [`ggplot2::position_stack`] does. 100 | #' @param vjust.mode character(1L), one of "end" (default), or "top" where "top" 101 | #' results in the same behavior as in [`ggplot2::position_stack`]. "end" 102 | #' means the justification is relative to the "end" of the waterfall bar. So 103 | #' if a waterfall bar is heading down (i.e. negative `y` value), the "end" is 104 | #' at the bottom. If it heading up (i.e. positive `y` value), the "end" is at 105 | #' the top. For positive `y` values "end" and "top" do the same thing. 106 | #' @param signif integer(1L) between 1 and 22, defaults to 11, corresponds to 107 | #' the `digits` parameter for [`signif`] and is used to reduce the precision 108 | #' of numeric `x` aesthetic values so that stacking is not foiled by double 109 | #' precision imprecision. 110 | #' @param y.start numeric(1L), defaults to 0, will be starting point for the 111 | #' cumulative sum of `y` values. This could be useful if you want to combine 112 | #' waterfalls with other layers and need the waterfall to start at a specific 113 | #' value. 114 | #' @export 115 | #' @examples 116 | #' ## These examples are best run via `example(position_waterfall)` 117 | #' library(ggplot2) 118 | #' dat <- data.frame(x=3:1, y=1:3) 119 | #' p1 <- ggplot(dat, aes(x=x, y=y)) + geom_col(position='waterfall') 120 | #' 121 | #' ## Add text or labels; defaults to middle waterfall position 122 | #' ## which can be modified with `vjust` 123 | #' p1 + geom_label(aes(label=x), position='waterfall') 124 | #' 125 | #' ## We can also add the cumulative running to the top of 126 | #' ## the bars with `stat='waterfall'` and position adjustments 127 | #' p1 + geom_label(aes(label=x), position='waterfall') + 128 | #' geom_label( 129 | #' stat="waterfall", # adds `ycum` computed variable 130 | #' aes(label=stat(ycum)), # which we can use for label 131 | #' position=position_waterfall(vjust=1), # text to end of column 132 | #' vjust=0, # tweak so it's on top 133 | #' ) 134 | #' ## A poor person's candlestick chart: 135 | #' dat.r.walk <- data.frame(x=1:20, y=rnorm(20)) 136 | #' ggplot(dat.r.walk, aes(x=x, y=y, fill=y > 0)) + 137 | #' geom_col(position='waterfall') 138 | #' 139 | #' ## We can use arbitrary geoms 140 | #' ggplot(dat, aes(x=x, y=y)) + 141 | #' geom_point() + 142 | #' geom_point(position='waterfall', color='blue') + # default vjust=0.5 143 | #' geom_point(position=position_waterfall(vjust=1), color='red') 144 | #' 145 | #' ## Or stats; here we turn a histogram into an ecdf plot 146 | #' dat.norm <- data.frame(x=rnorm(1000)) 147 | #' ggplot(dat.norm, aes(x=x)) + geom_histogram(position='waterfall') 148 | #' ggplot(dat.norm, aes(x=x)) + stat_bin(position='waterfall') 149 | #' 150 | #' ## Data with groups 151 | #' dat3 <- data.frame( 152 | #' x=c(3, 2, 2, 2, 1, 1), y=c(-3, 1, 4, -6, -1, 10), 153 | #' grp=rep(c("A", "B", "C"), lenght.out=6) 154 | #' ) 155 | #' p2 <- ggplot(dat3, aes(x=x, y=y, fill=grp)) 156 | #' p2 + geom_col(position="waterfall") 157 | #' 158 | #' ## Equal width columns 159 | #' p2 + geom_col(position=position_waterfall(preserve='single')) 160 | #' 161 | #' ## Stacking groups is possible, bug hard to interpret when 162 | #' ## negative values present 163 | #' p2 + geom_col(position=position_waterfall(dodge=FALSE)) 164 | 165 | position_waterfall <- function( 166 | width = NULL, 167 | preserve = c("total", "single"), 168 | reverse = FALSE, 169 | dodge = TRUE, 170 | vjust = getOption('ggbg.vjust'), 171 | vjust.mode = getOption('ggbg.vjust.mode'), 172 | signif = getOption('ggbg.signif'), 173 | y.start = 0 174 | ) { 175 | vetr( 176 | dodge=LGL.1, vjust=NUM.1, vjust.mode=CHR.1 && . %in% c("top", "end"), 177 | signif=INT.1 && . >= 1 && . <= 22, y.start=NUM.1 178 | ) 179 | ggproto(NULL, PositionWaterfall, 180 | width = width, 181 | preserve = match.arg(preserve), 182 | reverse = reverse, 183 | vjust=vjust, 184 | vjust.mode=vjust.mode, 185 | dodge = dodge, 186 | signif = signif, 187 | y.start = y.start 188 | ) 189 | } 190 | 191 | ## Much of this code is lifted from ggplot2/R/position-dodge.R 192 | 193 | ## Check in puts and pre-compute variables that are based on the entire data. 194 | ## 195 | ## One of the key things is work through the potential hierarchy of width data, 196 | ## with a preference for explicit width via xmin/xmax, or failing that via the 197 | ## width aesthetic, or failing that implicit via the x values and resolution. 198 | ## 199 | ## Beware that the 'width' aesthetic is not the same as the 'width' parameter to 200 | ## `position_waterfall`; the former is the width of the graphical element, the 201 | ## latter is the dodging width. 202 | 203 | setup_params_waterfall <- function(self, data) { 204 | signif <- self[['signif']] 205 | x.check <- all(c("xmin", "xmax") %in% names(data)) 206 | self[['has.x.width']] <- 207 | is.numeric(data[["xmin"]]) && is.numeric(data[["xmax"]]) && 208 | !anyNA(data[['xmin']]) && !anyNA(data[['xmax']]) && 209 | all(data[['xmax']] >= data[['xmin']]) 210 | 211 | if(!self[['has.x.width']] && any(x.check)) { 212 | warning( 213 | "Cannot interpret 'xmin' and 'xmax' aesthetics either because ", 214 | "one of them is missing, either contains missing values, either ", 215 | "is not numeric, or some 'xmax' values are less than the corresponding ", 216 | "'xmin' values. Geom width adjustments will not be applied by `", 217 | self[['name']], "`." 218 | ) 219 | } 220 | # Detect the special case where graphical objects have a pre-defined 221 | # width based on xmin/xmax that is the same for all objects. 222 | 223 | width.geom.unique <- self[['width.geom.unique']] 224 | if(is.null(width.geom.unique)) { 225 | if(self[['has.x.width']]) { 226 | width.tmp <- 227 | unique(signif(data[["xmax"]] - data[["xmin"]], digits=signif)) 228 | if(length(width.tmp) == 1 && !is.na(width.tmp) && width.tmp > 0) 229 | width.geom.unique <- width.tmp 230 | } 231 | } 232 | preserve <- self[['preserve']] 233 | if(isTRUE(preserve == 'single') && is.null(width.geom.unique)) { 234 | warning( 235 | "`preserve='single'` for `", self$name, "` only works if all widths ", 236 | "for a layer as implied by 'xmin' and 'xmax' are the same, positive, ", 237 | "and not NA. Proceeding with `preserve='total'`." 238 | ) 239 | preserve <- "total" 240 | } 241 | # Default width based on the x data 242 | 243 | if("x" %in% names(data)) { 244 | self[['width.default']] = 245 | signif(resolution(data[['x']], FALSE) * 0.9, digits=signif) 246 | } 247 | if( 248 | "width" %in% names(data) && is.numeric(data[['width']]) && 249 | all(data[['width']] > 0) 250 | ) { 251 | self[['has.width']] <- TRUE 252 | } else if('width' %in% names(data) && !has.x.width && is.null(width)) { 253 | warning( 254 | "'width' aesthetic contains values that are not strictly positive ", 255 | "numerics, so it cannot be used for dodging." 256 | ) 257 | } 258 | list( 259 | width = self[['width']], 260 | preserve = self[['preserve']], 261 | dodge = self[['dodge']], 262 | reverse = self[['reverse']], 263 | vjust = self[['vjust']], 264 | vjust.mode = self[['vjust.mode']], 265 | width.geom.unique = width.geom.unique, 266 | width.default = self[['width.default']], 267 | has.x.width = self[['has.x.width']], 268 | has.width = self[['has.width']], 269 | groups=sort(unique(data[['group']])), 270 | signif=signif, 271 | y.start=self[['y.start']] 272 | ) 273 | } 274 | ## Compute Panel for Waterfall 275 | ## 276 | ## Chunk data by `x` aesthetic, and also compute the `y` starting point for each 277 | ## chunk. 278 | 279 | compute_panel_waterfall <- function(self, data, params, scales) { 280 | check.x <- c("xmin", "xmax") %in% names(data) 281 | x <- if("x" %in% names(data)) signif(data[['x']], digits=params[['signif']]) 282 | else if (params[['has.x.width']]) 283 | signif( 284 | (data[['xmax']] - data[['xmin']]) / 2 + data[['xmin']], 285 | digits=params[['signif']] 286 | ) 287 | else { 288 | warning( 289 | "Either 'x', or 'xmin' and 'xmax' must be specified; `", 290 | self$name, "` will not be applied." 291 | ) 292 | NULL 293 | } 294 | if(!is.null(x)) { 295 | if(!"y" %in% names(data)) { 296 | warning( 297 | "'y' aesthetic is missing; `", self$name, "` will not be applied." 298 | ) 299 | } else { 300 | # group by x, and then stack / dodge, we also need to track the 301 | # cumulative height of the previous bars 302 | 303 | ord.idx <- order(x, data[['group']] * if(params[['reverse']]) -1 else 1) 304 | data <- data[ord.idx , , drop=FALSE] 305 | x <- x[ord.idx] 306 | 307 | y.cum <- cumsum(c(params[['y.start']], data[["y"]])) 308 | y.cum.last <- tapply(tail(y.cum, -1L), x, tail, 1L) 309 | prev.last <- c(params[['y.start']], head(y.cum.last, -1)) 310 | 311 | # For each `x` value, compute stacking and dodging 312 | 313 | d.s <- split(data, x) 314 | 315 | d.s.proc <- mapply( 316 | pos_waterfall, 317 | df=d.s, 318 | y.start=prev.last, 319 | MoreArgs=list( 320 | width=params[['width']], 321 | width.geom.unique=params[['width.geom.unique']], 322 | width.default=params[['width.default']], 323 | dodge=params[['dodge']], 324 | has.x.width=params[['has.x.width']], 325 | has.width=params[['has.width']], 326 | vjust=params[['vjust']], 327 | vjust.mode=params[['vjust.mode']], 328 | groups=params[['groups']], 329 | preserve=params[['preserve']], 330 | signif=params[['signif']], 331 | reverse=params[['reverse']] 332 | ), 333 | SIMPLIFY=FALSE 334 | ) 335 | # Re-assemble and restore order of data 336 | 337 | data <- do.call(rbind, d.s.proc)[ 338 | order(seq(length.out=length(ord.idx))[ord.idx]), , drop=FALSE 339 | ] 340 | } 341 | } 342 | data 343 | } 344 | 345 | ## Recompute widths based on group size 346 | 347 | calc_width <- function(widths, width.geom.unique, group.map, groups, preserve) { 348 | # possible for there to be multiple values within a group, so pick max, 349 | # and make sure everything sorted by integer value of group map 350 | group.widths <- tapply( 351 | widths, factor(group.map, levels=seq_along(groups)), max 352 | ) 353 | group.widths <- group.widths[order(as.integer(names(group.widths)))] 354 | 355 | if(identical(preserve, 'single')) { 356 | width.scale <- width.geom.unique * length(groups) 357 | group.widths[is.na(group.widths)] <- width.geom.unique 358 | } else { 359 | group.widths[is.na(group.widths)] <- 0 360 | width.scale <- sum(group.widths) 361 | width.geom.unique <- max(group.widths) 362 | } 363 | widths.fin <- widths / width.scale * width.geom.unique 364 | list( 365 | width=widths.fin, group.widths=group.widths, 366 | scale=width.geom.unique / width.scale 367 | ) 368 | } 369 | # Dodging can handle different width as well as overlapping intervals. Stacking 370 | # is done relative to the `x` value (or midpoint of `xmin`/`xmax`) 371 | 372 | pos_waterfall <- function( 373 | df, width, width.geom.unique, width.default, dodge, y.start, 374 | vjust, vjust.mode, has.x.width, has.width, groups, preserve, signif, 375 | reverse 376 | ) { 377 | group.map <- match(df[['group']], sort(groups, decreasing=reverse)) 378 | 379 | df <- if(dodge) { 380 | geom.widths.raw <- if(has.x.width) 381 | df[['xmax']] - df[['xmin']] else if(has.width) df[['width']] 382 | 383 | dodge.widths.raw <- if(!is.null(width)) rep(width, length.out=nrow(df)) 384 | else if (!is.null(geom.widths.raw)) geom.widths.raw 385 | else rep(width.default, length.out=nrow(df)) 386 | 387 | geom.widths <- if(!is.null(geom.widths.raw)) 388 | calc_width( 389 | geom.widths.raw, width.geom.unique, group.map, groups, preserve 390 | ) 391 | dodge.widths <- calc_width( 392 | dodge.widths.raw, width.geom.unique, group.map, groups, preserve 393 | ) 394 | # Adjust the width sizes based on geom.widths 395 | 396 | if(!is.null(geom.widths.raw)) { 397 | if(has.x.width) { 398 | width.ratio <- geom.widths[['width']] / geom.widths.raw 399 | xmid <- df[['xmin']] + geom.widths.raw / 2 400 | df[['xmin']] <- xmid - geom.widths[['width']] / 2 401 | df[['xmax']] <- xmid + geom.widths[['width']] / 2 402 | } 403 | if(has.width) { 404 | df[['width']] <- geom.widths[['width']] 405 | } 406 | } 407 | # Adjust positions based on dodge width. For each group the dodge amount is 408 | # the difference between its middle and the middle of the total width. 409 | 410 | dodge.width.total <- sum(dodge.widths[['group.widths']]) 411 | dodge.width.mid <- dodge.width.total / 2 412 | dodge.width.cum <- cumsum(dodge.widths[['group.widths']]) 413 | dodge.width.lead <- c(0, head(dodge.width.cum, -1)) 414 | dodge.width.offset.group <- 415 | ((dodge.width.cum + dodge.width.lead) / 2) - dodge.width.mid 416 | 417 | dodge.width.offset <- dodge.width.offset.group[group.map] * 418 | dodge.widths[['scale']] 419 | 420 | if(has.x.width) { 421 | df[['xmin']] <- df[['xmin']] + dodge.width.offset 422 | df[['xmax']] <- df[['xmax']] + dodge.width.offset 423 | } 424 | if("x" %in% names(df) && is.numeric(df[['x']])) { 425 | df[['x']] <- df[['x']] + dodge.width.offset 426 | } 427 | stack_waterfall(df, y.start, vjust, vjust.mode) 428 | } else { 429 | # stack mode, need to segregate positives and negatives 430 | 431 | df.pos <- df[df[["y"]] >= 0, , drop=FALSE] 432 | df.neg <- df[df[["y"]] < 0, , drop=FALSE] 433 | rbind( 434 | stack_waterfall(df.pos, y.start, vjust, vjust.mode), 435 | stack_waterfall(df.neg, y.start, vjust, vjust.mode) 436 | ) 437 | } 438 | df 439 | } 440 | stack_waterfall <- function(df, y.start, vjust, vjust.mode) { 441 | y.all <- c(y.start, df[["y"]]) 442 | y.cum <- cumsum(y.all) 443 | y.lead <- head(y.cum, -1L) 444 | y.lag <- tail(y.cum, -1L) 445 | 446 | y.orig <- df[["y"]] 447 | y.min <- pmin(y.lead, y.lag) 448 | y.max <- pmax(y.lead, y.lag) 449 | 450 | df[["y"]] <- y.lag 451 | 452 | # adjust v position 453 | 454 | df[["y"]] <- ifelse( 455 | y.orig < 0 & identical(vjust.mode, "end"), 456 | y.max - vjust * (y.max - y.min), 457 | y.min + vjust * (y.max - y.min) 458 | ) 459 | if("ymin" %in% names(df)) df[["ymin"]] <- df[["ymin"]] + y.lag - y.orig 460 | if("ymax" %in% names(df)) df[["ymax"]] <- df[["ymax"]] + y.lag - y.orig 461 | df 462 | } 463 | 464 | #' Compute Position Adjustments Based on Cumulative Value 465 | #' 466 | #' `PositionWaterfall` is the `ggproto` object used to generate the position 467 | #' adjustments that correspond to [position_waterfall]. 468 | #' 469 | #' @rdname ggbg-ggproto 470 | #' @format NULL 471 | #' @export 472 | 473 | PositionWaterfall <- ggproto( 474 | "PositionWaterfall", Position, 475 | name = "position_waterfall", 476 | width = NULL, # dodge width 477 | width.geom.unique = NULL, # all geoms have same width 478 | width.default = 1, 479 | reverse = FALSE, 480 | preserve = "total", 481 | dodge = TRUE, 482 | vjust = 0.5, 483 | vjust.mode="end", 484 | has.x.width=FALSE, # xmin and xmax are present and reasonable 485 | has.width=FALSE, # width aesthetic present and reasonable 486 | groups=integer(), # all possible groups, 487 | # significance for x values to compute, width, overlap, etc. 488 | signif=11, 489 | y.start=0, 490 | 491 | setup_params = setup_params_waterfall, 492 | 493 | # We don't want to modify the data at this point because we don't want to add 494 | # aesthetics to the data frame, so we'll do everything at the compute panel 495 | # step. This is out of an abundance of caution to avoid messing with 496 | # downstream rendering of geoms. 497 | 498 | setup_data = function(self, data, params) { 499 | data 500 | }, 501 | compute_panel = compute_panel_waterfall 502 | ) 503 | 504 | -------------------------------------------------------------------------------- /R/proto.R: -------------------------------------------------------------------------------- 1 | #' ggbg ggproto Objects 2 | #' 3 | #' These are all the `ggproto` objects implemented by `ggbg`. 4 | #' 5 | #' @seealso [`ggplot2::ggproto`] for details about `ggproto`. 6 | #' @name ggbg-ggproto 7 | #' @rdname ggbg-ggproto 8 | 9 | NULL 10 | -------------------------------------------------------------------------------- /R/stat-waterfall.R: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2018 Brodie Gaslam 2 | ## 3 | ## This file is part of "ggbg - Assorted Ggplot Extensions" 4 | ## 5 | ## This program is free software: you can redistribute it and/or modify 6 | ## it under the terms of the GNU General Public License as published by 7 | ## the Free Software Foundation, either version 2 of the License, or 8 | ## (at your option) any later version. 9 | ## 10 | ## This program is distributed in the hope that it will be useful, 11 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ## GNU General Public License for more details. 14 | ## 15 | ## Go to for a copy of the license. 16 | 17 | #' Computes the `ycum` Aesthetic 18 | #' 19 | #' `StatWaterfall` is intended for use only via the `stat` parameter to other 20 | #' layers (e.g. `geom_...(stat='waterfall')`). It will add a `ycum` aesthetic 21 | #' to the data which can then be used by the layer. Unlike other `Stat` 22 | #' objects, it does not exist as a stand-alone layer (i.e. `stat_waterfall` is 23 | #' not defined). Its main purpose is to generate the text for cumulative 24 | #' labels. See [`position_waterfall`] for usage examples. 25 | #' 26 | #' @export 27 | #' @rdname ggbg-ggproto 28 | #' @format NULL 29 | #' @seealso [`position_waterfall`] for usage examples. 30 | 31 | StatWaterfall <- ggproto("StatWaterfall", Stat, 32 | required_aes = c("x", "y"), 33 | reverse = FALSE, 34 | 35 | compute_panel = function(data, scales, reverse=FALSE) { 36 | if("ycum" %in% names(data)) { 37 | warning( 38 | "`ycum` aesthetic aesthetic already exists, `stat_waterfall` ", 39 | "will not recompute it." 40 | ) 41 | } else { 42 | dat.ord <- 43 | order(data[["x"]], data[["group"]] * if(reverse) -1 else 1) 44 | 45 | data.o <- data[dat.ord, , drop=FALSE] 46 | data.o[["ycum"]] <- cumsum(data.o[["y"]]) 47 | data <- data.o[rank(dat.ord, ties.method='first'), , drop=FALSE] 48 | } 49 | data 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /R/sysdata.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/R/sysdata.rda -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```{r, echo = FALSE} 4 | knitr::opts_chunk$set( 5 | collapse = TRUE, 6 | comment = "##", 7 | fig.path = "man/figures/README-", 8 | error = TRUE 9 | ) 10 | library(ggbg) 11 | library(ggplot2) 12 | ``` 13 | ## ggbg - Assorted Ggplot Extensions 14 | 15 | [![](https://travis-ci.org/brodieG/ggbg.svg?branch=master)](https://travis-ci.org/brodieG/ggbg) 16 | [![](https://codecov.io/github/brodieG/ggbg/coverage.svg?branch=master)](https://codecov.io/github/brodieG/ggbg?branch=master) 17 | [![](http://www.r-pkg.org/badges/version/ggbg)](https://cran.r-project.org/package=ggbg) 18 | 19 | Assorted experimental `ggplot2` extensions. This package is partly a learning 20 | exercise for myself, so not all contents will be useful to others. 21 | 22 | ## position_waterfall 23 | 24 | A position adjustment that both stacks and dodges bars to create waterfall 25 | charts: 26 | 27 | ```{r eval=FALSE} 28 | set.seed(1) 29 | p <- ggplot(data.frame(x=1:20, y=rnorm(20)), aes(x=x, y=y, fill=y > 0)) 30 | p + geom_col() 31 | p + geom_col(position='waterfall') 32 | ``` 33 | ```{r waterfall, echo=FALSE, fig.width=5, fig.height=4, out.width='49%'} 34 | set.seed(1) 35 | p <- ggplot(data.frame(x=1:20, y=rnorm(20)), aes(x=x, y=y, fill=y > 0)) + 36 | guides(fill=FALSE) 37 | p + geom_col() + ggtitle('geom_col()') 38 | p + geom_col(position='waterfall') + ggtitle('geom_col(position="waterfall")') 39 | ``` 40 | 41 | It is primarily intended for `geom_col`, but can be used with arbitrary geoms: 42 | 43 | ```{r eval=FALSE} 44 | p + geom_point() 45 | p + geom_point(position=position_waterfall(vjust=1)) 46 | ``` 47 | ```{r geoms, echo=FALSE, fig.width=5, fig.height=4, out.width='49%'} 48 | p + geom_point() + ggtitle('geom_point()') 49 | p + geom_point(position=position_waterfall(vjust=1)) + 50 | ggtitle('geom_point(position=position_waterfall(vjust=1))') 51 | ``` 52 | 53 | If you use arbitrary geoms you may need to adjust position with `vjust` as we do 54 | here as otherwise the graphical elements are centered between the end of the 55 | previous cumulative value and the beginning of the next. Please see 56 | `?position_watefall` for details. 57 | 58 | It is can also be used with stats: 59 | 60 | ```{r eval=FALSE} 61 | dat.norm <- data.frame(x=rnorm(1000)) 62 | ggplot(dat.norm, aes(x=x)) + stat_bin() 63 | ggplot(dat.norm, aes(x=x)) + stat_bin(position='waterfall') 64 | ``` 65 | ```{r stats, echo=FALSE, fig.width=5, fig.height=4, out.width='49%'} 66 | dat.norm <- data.frame(x=rnorm(1000)) 67 | ggplot(dat.norm, aes(x=x)) + stat_bin(bins=30) + ggtitle('stat_bin()') 68 | ggplot(dat.norm, aes(x=x)) + stat_bin(bins=30, position='waterfall') + 69 | ggtitle('stat_bin(position="waterfall")') 70 | ``` 71 | 72 | For more examples try `example(position_waterfall, package='ggbg')`. 73 | 74 | ## geom_car 75 | 76 | Plot cars! This geom was implemented on a lark as an answer to an [SO 77 | Question](https://stackoverflow.com/questions/22159087/is-it-possible-to-draw-diagrams-in-r/22207979#22207979). 78 | 79 | ```{r geom-car, fig.width=7, fig.height=2.5, out.width='99%'} 80 | ggplot( 81 | geom.car.data, # ggbg data set 82 | aes(x=x, y=y, length=length, width=width, fill=label) 83 | ) + 84 | geom_hline(yintercept=seq(5, 35, by=10), color="white", size=2, linetype=2) + 85 | geom_car() + 86 | coord_equal() + 87 | theme(panel.background = element_rect(fill="#555555"), 88 | panel.grid.major = element_blank(), 89 | panel.grid.minor = element_blank()) 90 | ``` 91 | 92 | ## Installation 93 | 94 | This package is currently github-only. You can get it with 95 | `devtools::install_github('brodieg/ggbg')` or: 96 | 97 | ```{r eval=FALSE} 98 | f.dl <- tempfile() 99 | f.uz <- tempfile() 100 | github.url <- 'https://github.com/brodieG/ggbg/archive/master.zip' 101 | download.file(github.url, f.dl) 102 | unzip(f.dl, exdir=f.uz) 103 | install.packages(file.path(f.uz, 'ggbg-master'), repos=NULL, type='source') 104 | unlink(c(f.dl, f.uz)) 105 | ``` 106 | 107 | ## Related Packages 108 | 109 | * [ggplot2](https://github.com/tidyverse/ggplot2) 110 | 111 | ## Acknowledgments 112 | 113 | * R Core for developing and maintaining such a wonderful language. 114 | * CRAN maintainers, for patiently shepherding packages onto CRAN and maintaining 115 | the repository, and Uwe Ligges in particular for maintaining 116 | [Winbuilder](http://win-builder.r-project.org/). 117 | * [Hadley Wickham](https://github.com/hadley/) for `ggplot2`, and in particular 118 | for making it so easily extensible. 119 | * [Jim Hester](https://github.com/jimhester) because 120 | [covr](https://cran.r-project.org/package=covr) rocks. 121 | * [Dirk Eddelbuettel](https://github.com/eddelbuettel) and [Carl 122 | Boettiger](https://github.com/cboettig) for the 123 | [rocker](https://github.com/rocker-org/rocker) project, and [Gábor 124 | Csárdi](https://github.com/gaborcsardi) and the 125 | [R-consortium](https://www.r-consortium.org/) for 126 | [Rhub](https://github.com/r-hub), without which testing bugs on R-devel and 127 | other platforms would be a nightmare. 128 | * [Hadley Wickham](https://github.com/hadley/) for 129 | [devtools](https://cran.r-project.org/package=devtools) and with [Peter 130 | Danenberg](https://github.com/klutometis) for 131 | [roxygen2](https://cran.r-project.org/package=roxygen2). 132 | * [Yihui Xie](https://github.com/yihui) for 133 | [knitr](https://cran.r-project.org/package=knitr) and [J.J. 134 | Allaire](https://github.com/jjallaire) etal for 135 | [rmarkdown](https://cran.r-project.org/package=rmarkdown), and by extension 136 | John MacFarlane for [pandoc](http://pandoc.org/). 137 | * All open source developers out there that make their work freely available 138 | for others to use. 139 | * [Github](https://github.com/), [Travis-CI](https://travis-ci.org/), 140 | [Codecov](https://codecov.io/), [Vagrant](https://www.vagrantup.com/), 141 | [Docker](https://www.docker.com/), [Ubuntu](https://www.ubuntu.com/), 142 | [Brew](https://brew.sh/) for providing infrastructure that greatly simplifies 143 | open source development. 144 | * [Free Software Foundation](http://fsf.org/) for developing the GPL license and 145 | promotion of the free software movement. 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## ggbg - Assorted Ggplot Extensions 5 | 6 | [![](https://travis-ci.org/brodieG/ggbg.svg?branch=master)](https://travis-ci.org/brodieG/ggbg) 7 | [![](https://codecov.io/github/brodieG/ggbg/coverage.svg?branch=master)](https://codecov.io/github/brodieG/ggbg?branch=master) 8 | [![](http://www.r-pkg.org/badges/version/ggbg)](https://cran.r-project.org/package=ggbg) 9 | 10 | Assorted experimental `ggplot2` extensions. This package is partly a learning 11 | exercise for myself, so not all contents will be useful to others. 12 | 13 | ## position_waterfall 14 | 15 | A position adjustment that both stacks and dodges bars to create waterfall 16 | charts: 17 | 18 | 19 | ```r 20 | set.seed(1) 21 | p <- ggplot(data.frame(x=1:20, y=rnorm(20)), aes(x=x, y=y, fill=y > 0)) 22 | p + geom_col() 23 | p + geom_col(position='waterfall') 24 | ``` 25 | plot of chunk waterfallplot of chunk waterfall 26 | 27 | It is primarily intended for `geom_col`, but can be used with arbitrary geoms: 28 | 29 | 30 | ```r 31 | p + geom_point() 32 | p + geom_point(position=position_waterfall(vjust=1)) 33 | ``` 34 | plot of chunk geomsplot of chunk geoms 35 | 36 | If you use arbitrary geoms you may need to adjust position with `vjust` as we do 37 | here as otherwise the graphical elements are centered between the end of the 38 | previous cumulative value and the beginning of the next. Please see 39 | `?position_watefall` for details. 40 | 41 | It is can also be used with stats: 42 | 43 | 44 | ```r 45 | dat.norm <- data.frame(x=rnorm(1000)) 46 | ggplot(dat.norm, aes(x=x)) + stat_bin() 47 | ggplot(dat.norm, aes(x=x)) + stat_bin(position='waterfall') 48 | ``` 49 | plot of chunk statsplot of chunk stats 50 | 51 | For more examples try `example(position_waterfall, package='ggbg')`. 52 | 53 | ## geom_car 54 | 55 | Plot cars! This geom was implemented on a lark as an answer to an [SO 56 | Question](https://stackoverflow.com/questions/22159087/is-it-possible-to-draw-diagrams-in-r/22207979#22207979). 57 | 58 | 59 | ```r 60 | ggplot( 61 | geom.car.data, # ggbg data set 62 | aes(x=x, y=y, length=length, width=width, fill=label) 63 | ) + 64 | geom_hline(yintercept=seq(5, 35, by=10), color="white", size=2, linetype=2) + 65 | geom_car() + 66 | coord_equal() + 67 | theme(panel.background = element_rect(fill="#555555"), 68 | panel.grid.major = element_blank(), 69 | panel.grid.minor = element_blank()) 70 | ``` 71 | 72 | plot of chunk geom-car 73 | 74 | ## Installation 75 | 76 | This package is currently github-only. You can get it with 77 | `devtools::install_github('brodieg/ggbg')` or: 78 | 79 | 80 | ```r 81 | f.dl <- tempfile() 82 | f.uz <- tempfile() 83 | github.url <- 'https://github.com/brodieG/ggbg/archive/master.zip' 84 | download.file(github.url, f.dl) 85 | unzip(f.dl, exdir=f.uz) 86 | install.packages(file.path(f.uz, 'ggbg-master'), repos=NULL, type='source') 87 | unlink(c(f.dl, f.uz)) 88 | ``` 89 | 90 | ## Related Packages 91 | 92 | * [ggplot2](https://github.com/tidyverse/ggplot2) 93 | 94 | ## Acknowledgments 95 | 96 | * R Core for developing and maintaining such a wonderful language. 97 | * CRAN maintainers, for patiently shepherding packages onto CRAN and maintaining 98 | the repository, and Uwe Ligges in particular for maintaining 99 | [Winbuilder](http://win-builder.r-project.org/). 100 | * [Hadley Wickham](https://github.com/hadley/) for `ggplot2`, and in particular 101 | for making it so easily extensible. 102 | * [Jim Hester](https://github.com/jimhester) because 103 | [covr](https://cran.r-project.org/package=covr) rocks. 104 | * [Dirk Eddelbuettel](https://github.com/eddelbuettel) and [Carl 105 | Boettiger](https://github.com/cboettig) for the 106 | [rocker](https://github.com/rocker-org/rocker) project, and [Gábor 107 | Csárdi](https://github.com/gaborcsardi) and the 108 | [R-consortium](https://www.r-consortium.org/) for 109 | [Rhub](https://github.com/r-hub), without which testing bugs on R-devel and 110 | other platforms would be a nightmare. 111 | * [Hadley Wickham](https://github.com/hadley/) for 112 | [devtools](https://cran.r-project.org/package=devtools) and with [Peter 113 | Danenberg](https://github.com/klutometis) for 114 | [roxygen2](https://cran.r-project.org/package=roxygen2). 115 | * [Yihui Xie](https://github.com/yihui) for 116 | [knitr](https://cran.r-project.org/package=knitr) and [J.J. 117 | Allaire](https://github.com/jjallaire) etal for 118 | [rmarkdown](https://cran.r-project.org/package=rmarkdown), and by extension 119 | John MacFarlane for [pandoc](http://pandoc.org/). 120 | * All open source developers out there that make their work freely available 121 | for others to use. 122 | * [Github](https://github.com/), [Travis-CI](https://travis-ci.org/), 123 | [Codecov](https://codecov.io/), [Vagrant](https://www.vagrantup.com/), 124 | [Docker](https://www.docker.com/), [Ubuntu](https://www.ubuntu.com/), 125 | [Brew](https://brew.sh/) for providing infrastructure that greatly simplifies 126 | open source development. 127 | * [Free Software Foundation](http://fsf.org/) for developing the GPL license and 128 | promotion of the free software movement. 129 | -------------------------------------------------------------------------------- /covr.R: -------------------------------------------------------------------------------- 1 | covr::codecov() 2 | -------------------------------------------------------------------------------- /data/geom.car.data.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/data/geom.car.data.rda -------------------------------------------------------------------------------- /inst/doc/extensions.R: -------------------------------------------------------------------------------- 1 | ## ----global_options, echo=FALSE------------------------------------------ 2 | knitr::opts_chunk$set(error=TRUE, comment=NA) 3 | library(ggbg) 4 | 5 | -------------------------------------------------------------------------------- /inst/doc/extensions.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Writing Ggplot2 Extensions" 3 | author: "Brodie Gaslam" 4 | output: 5 | rmarkdown::html_vignette: 6 | toc: true 7 | css: styles.css 8 | 9 | vignette: > 10 | %\VignetteIndexEntry{extensions} 11 | %\VignetteEngine{knitr::rmarkdown} 12 | %\usepackage[utf8]{inputenc} 13 | --- 14 | 15 | ```{r global_options, echo=FALSE} 16 | knitr::opts_chunk$set(error=TRUE, comment=NA) 17 | library(ggbg) 18 | ``` 19 | 20 | This document is under development. 21 | 22 | ## Introduction 23 | 24 | I wrote this document while figuring out how to write my own custom geoms for 25 | `ggplot`. The existing documentation available through `?Geom` and 26 | `browseVignettes('ggplot2')` was a great starting point for me, but I found 27 | myself wanting more context and details than it provides. 28 | 29 | This is unofficial documentation based on my interpretation of the sources as of 30 | ~4/18/2018. It may become out-of-date as `ggplot2` evolves. 31 | 32 | This document assumes the reader is familiar with how to use `ggplot2` for 33 | plotting purposes. 34 | 35 | 36 | ## Ggplot Basics - an Extension Developer Perspective 37 | 38 | ### It's The Printing, Stupid 39 | 40 | In order to create and display a `ggplot` plot we go through two major steps: 41 | 42 | * Specification (`ggplot() + ...`): user specifies what the plot 43 | should look like with `geom_*`, `stat_*`, `scale_*`, etc.. 44 | * Generation and rendering, typically triggered by printing a `ggplot` 45 | object. 46 | 47 | As a `ggplot` user you are mostly concerned with the specification of the plot. 48 | As a `ggplot` extension developer, you are also concerned with the plot 49 | generation step. This is were `ggplot` does most of the work. You can look at 50 | `ggplot2:::print.ggplot` to see the major steps involved, which can be 51 | summarized as: 52 | 53 | 1. Build (`ggplot_build`): analyzes and transforms data into a format suited for 54 | translation into graphical objects: 55 | 1. Assigns facet panels. 56 | 2. Computes aesthetic values from data and `aes(...)` specifications. 57 | 3. Assigns groups. 58 | 4. Rescales data if non linear scales are specified. 59 | 5. Computes and maps statistics. 60 | 6. Transforms special aesthetics (e.g. `width` to `xmin`-`xmax`) 61 | (Layer$compute_geom_1 -> Geom$setup_data, check_required_aesthetic) 62 | 7. Adjusts positions (e.g. dodging/stacking with `$compute_positions`). 63 | (Layer$compute_positions -> 64 | Position$setup_params/setup_data/compute_layer) 65 | 8. Recompute scales. 66 | 9. Adds default aesthetics as needed. 67 | 2. Graphical Object Construction (`ggplot_gtable`): 68 | 1. Applies coordinate transformations (if necessary) 69 | 2. Translate the data into shapes, colors, positions, etc. 70 | 3. Rendering (`grid.draw`): display the resulting plot in a viewport. 71 | 72 | Need to discuss what is implemented via the `ggproto` objects. 73 | 74 | Each of the sub-steps in the build step is applied to every layer before moving 75 | to the next step with the `by_layer` function. Additionally, each proto driven 76 | calculation follows this hierarchy: 77 | 78 | `(draw|compute)_layer` 79 | `(draw|compute)_panel` 80 | `(draw|compute)_group` 81 | 82 | The `*_layer` functions typically split the data by panel and forward each 83 | chunk to the corresponding `*_panel` function. The base `ggproto` `*_panel` 84 | methods will split the data by group and forward it to the corresponding 85 | `*_group` function. This allows you to override either the `*_panel` or the 86 | `*_group` function depending on what you are trying to do. 87 | 88 | ## Understanding the `data` Object 89 | 90 | * Internal aesthetics 91 | * Special variables (`group`, `panel`) 92 | * `group` are "integer" values from 1 to n where n is the number of groups, or 93 | -1 if there aren't any groups. 94 | * Computed variables (`..level..`) 95 | 96 | ## Position Adjustments 97 | 98 | Step 1.7 is carried out by modifying the coordinates in the data objects. 99 | Both `position_dodge` and `position_stack` group data by `xmin` coordinate value 100 | (either user provided, or derived from `x` and `width` or some such), and then 101 | resolve any `xmin` overlaps by allocating the available width for each distinct 102 | `group` (or should it be element?) within. 103 | 104 | ## Alternate Docs 105 | 106 | ### On `gg_proto` 107 | 108 | Things to know: 109 | 110 | * Objects that contain member functions and data 111 | * The members can be accessed like list objects `object$member` 112 | * When member functions are invoked they are always automatically provided with 113 | a `self` object if `self` is part of the signature (you can probably still 114 | access `self` even if it isn't in the sig, need to test). 115 | 116 | ### Ggplot Basics 117 | 118 | ### Mapping 119 | 120 | 121 | ### Rendering 122 | 123 | 124 | 125 | Extending `ggplot` requires influencing how steps 1. and 2. are carried out. 126 | This is done by creating layer functions (e.g. `geom_*` or `stat_*`) functions that return layers containing custom `Geom*` or `Stat*` objects. 127 | 128 | 129 | 130 | ``` 131 | ## For reference, `sys.calls()` from a debugged `setup_data`: 132 | $ : language function (x, ...) UseMethod("print")(x) 133 | $ : language print.ggplot(x) 134 | $ : language ggplot_build(x) 135 | $ : language by_layer(function(l, d) l$compute_geom_1(d)) 136 | $ : language f(l = layers[[i]], d = data[[i]]) 137 | $ : language l$compute_geom_1(d) 138 | $ : language f(..., self = self) 139 | $ : language self$geom$setup_data(data, c(self$geom_params, self$aes_params)) 140 | $ : language f(...) 141 | ``` 142 | 143 | ### Geom Basics 144 | 145 | To implement a geom you need: 146 | 147 | * A `geom_*` standard function 148 | * A `Geom*` object, possibly re-used from an existing geom 149 | 150 | ### The `Geom*` Object 151 | 152 | `Geom*` objects such as `GeomPoint` or `GeomRect` are responsible for 153 | translating your data into graphical objects. In `ggplot` these graphical 154 | objects are called "grobs", short for Grid Graphical Objects, because they are 155 | encoded in a format defined by the `grid` R package. In order to create your 156 | own geoms you will need to learn how to use `grid`, or alternatively to re-use 157 | existing `Geom*` objects to generate grobs suited for your purposes. For 158 | example in this package we re-use `GeomRect` for `GeomWaterfall`. 159 | 160 | `Geom*` objects are implemented using `ggproto`, a `ggplot2` specific Object 161 | Oriented framework. `ggproto` is derived from the `proto` R OOP package. 162 | 163 | 164 | 165 | ## Extending Ggplot 2 vignette 166 | 167 | ### What `ggplot2` Does 168 | 169 | ### Creating Geoms 170 | 171 | The `setup_data` and `draw_panel` functions we referenced above are part of the 172 | `Geom*` `ggproto` objects. 173 | 174 | `setup_data` is used to convert parameters / aesthetics that are not in a format 175 | amenable to plotting, to one that is. One prime example is converting `width` 176 | and `height` to `xmin/xmax` and `ymin/ymax` values. 177 | 178 | 179 | In "Creating a new Geom", `draw_panel` is described as having 3 parameters, 180 | instead of the 4 + (in particular starting with `self`) in other docs and in the 181 | source. Additionally, the `panel_scales` param appears to actually be 182 | `panel_params`, at least in the sample fun we used (but `?Geom` also references 183 | `panel_scales`). 184 | 185 | For `draw_panel` sigs, we have 186 | 187 | * `GeomPoint`: `function(data, panel_params, coord, na.rm = FALSE)` 188 | * `GeomRect`: `function(self, data, panel_params, coord)` 189 | * `Geom`: `function(self, data, panel_params, coord, ...)` 190 | 191 | From what I can infer from docs and source your function must accept at least 192 | three arguments. `draw_layer` from `Geom` will call `draw_panel` with three 193 | unnamed parameters that in theory should match up to `data`, `panel_params`, 194 | `coord`. 195 | 196 | * `data`: A data.frame with all the aesthetic values as specified via `aes`. 197 | The column names corresponding to the aesthetic names, not the original data 198 | frame column names. Additionally contains `PANEL`, `group` (set to -1 if 199 | there are no groups), and any default aesthetics specified in 200 | `Geom*$default_aes`. 201 | * `panel_params`: named list, the intersection of parameters provided to the 202 | `geom_*` function with the formals of the `draw_panel` method, although you 203 | can customize `Geom` objects to return a specific eligible parameter list. 204 | * `coord`: coord transformation functions? 205 | 206 | Additionally, named parameters that are the intersection of the parameters 207 | provided to the `geom_*` function by the user and the parameters of `draw_panel` 208 | are supplied. 209 | 210 | Finally, since `draw_panel` is a `ggproto` method `self` will be provided if it 211 | is part of the `draw_panel` signature. 212 | 213 | ## Deprecated Stuff 214 | 215 | ### `setup_data` vs `reparametrise` 216 | 217 | It seems like both those functions can be used for the same purpose. For 218 | example, in `GeomRect`, `setup_data` is explicitly used to convert width to 219 | `xmax/xmin`. 220 | 221 | Actually, looks like `reparameterise` doesn't exist anymore? 222 | 223 | draw_legend / draw_key 224 | 225 | ## Grid Stuff 226 | 227 | You cannot use `gList` in the key/legend. However, `grobTree` works. 228 | 229 | -------------------------------------------------------------------------------- /inst/doc/extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Writing Ggplot2 Extensions 17 | 18 | 19 | 20 | 21 | 22 | 23 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 |

Writing Ggplot2 Extensions

244 |

Brodie Gaslam

245 | 246 | 247 | 273 | 274 |

This document is under development.

275 |
276 |

Introduction

277 |

I wrote this document while figuring out how to write my own custom geoms for ggplot. The existing documentation available through ?Geom and browseVignettes('ggplot2') was a great starting point for me, but I found myself wanting more context and details than it provides.

278 |

This is unofficial documentation based on my interpretation of the sources as of ~4/18/2018. It may become out-of-date as ggplot2 evolves.

279 |

This document assumes the reader is familiar with how to use ggplot2 for plotting purposes.

280 |
281 |
282 |

Ggplot Basics - an Extension Developer Perspective

283 |
284 |

It’s The Printing, Stupid

285 |

In order to create and display a ggplot plot we go through two major steps:

286 |
    287 |
  • Specification (ggplot() + ...): user specifies what the plot should look like with geom_*, stat_*, scale_*, etc..
  • 288 |
  • Generation and rendering, typically triggered by printing a ggplot object.
  • 289 |
290 |

As a ggplot user you are mostly concerned with the specification of the plot. As a ggplot extension developer, you are also concerned with the plot generation step. This is were ggplot does most of the work. You can look at ggplot2:::print.ggplot to see the major steps involved, which can be summarized as:

291 |
    292 |
  1. Build (ggplot_build): analyzes and transforms data into a format suited for translation into graphical objects: 293 |
      294 |
    1. Assigns facet panels.
    2. 295 |
    3. Computes aesthetic values from data and aes(...) specifications.
    4. 296 |
    5. Assigns groups.
    6. 297 |
    7. Rescales data if non linear scales are specified.
    8. 298 |
    9. Computes and maps statistics.
    10. 299 |
    11. Transforms special aesthetics (e.g. width to xmin-xmax) (Layer\(compute_geom_1 -> Geom\)setup_data, check_required_aesthetic)
    12. 300 |
    13. Adjusts positions (e.g. dodging/stacking with $compute_positions). (Layer\(compute_positions -> Position\)setup_params/setup_data/compute_layer)
    14. 301 |
    15. Recompute scales.
    16. 302 |
    17. Adds default aesthetics as needed.
    18. 303 |
  2. 304 |
  3. Graphical Object Construction (ggplot_gtable): 305 |
      306 |
    1. Applies coordinate transformations (if necessary)
    2. 307 |
    3. Translate the data into shapes, colors, positions, etc.
    4. 308 |
  4. 309 |
  5. Rendering (grid.draw): display the resulting plot in a viewport.
  6. 310 |
311 |

Need to discuss what is implemented via the ggproto objects.

312 |

Each of the sub-steps in the build step is applied to every layer before moving to the next step with the by_layer function. Additionally, each proto driven calculation follows this hierarchy:

313 |

(draw|compute)_layer (draw|compute)_panel (draw|compute)_group

314 |

The *_layer functions typically split the data by panel and forward each chunk to the corresponding *_panel function. The base ggproto *_panel methods will split the data by group and forward it to the corresponding *_group function. This allows you to override either the *_panel or the *_group function depending on what you are trying to do.

315 |
316 |
317 |
318 |

Understanding the data Object

319 |
    320 |
  • Internal aesthetics
  • 321 |
  • Special variables (group, panel) 322 |
      323 |
    • group are “integer” values from 1 to n where n is the number of groups, or -1 if there aren’t any groups.
    • 324 |
  • 325 |
  • Computed variables (..level..)
  • 326 |
327 |
328 |
329 |

Position Adjustments

330 |

Step 1.7 is carried out by modifying the coordinates in the data objects. Both position_dodge and position_stack group data by xmin coordinate value (either user provided, or derived from x and width or some such), and then resolve any xmin overlaps by allocating the available width for each distinct group (or should it be element?) within.

331 |
332 |
333 |

Alternate Docs

334 |
335 |

On gg_proto

336 |

Things to know:

337 |
    338 |
  • Objects that contain member functions and data
  • 339 |
  • The members can be accessed like list objects object$member
  • 340 |
  • When member functions are invoked they are always automatically provided with a self object if self is part of the signature (you can probably still access self even if it isn’t in the sig, need to test).
  • 341 |
342 |
343 |
344 |

Ggplot Basics

345 |
346 |
347 |

Mapping

348 |
349 |
350 |

Rendering

351 |

Extending ggplot requires influencing how steps 1. and 2. are carried out. This is done by creating layer functions (e.g. geom_* or stat_*) functions that return layers containing custom Geom* or Stat* objects.

352 |
 ## For reference, `sys.calls()` from a debugged `setup_data`:
353 |  $ : language function (x, ...)  UseMethod("print")(x)
354 |  $ : language print.ggplot(x)
355 |  $ : language ggplot_build(x)
356 |  $ : language by_layer(function(l, d) l$compute_geom_1(d))
357 |  $ : language f(l = layers[[i]], d = data[[i]])
358 |  $ : language l$compute_geom_1(d)
359 |  $ : language f(..., self = self)
360 |  $ : language self$geom$setup_data(data, c(self$geom_params, self$aes_params))
361 |  $ : language f(...)
362 |
363 |
364 |

Geom Basics

365 |

To implement a geom you need:

366 |
    367 |
  • A geom_* standard function
  • 368 |
  • A Geom* object, possibly re-used from an existing geom
  • 369 |
370 |
371 |
372 |

The Geom* Object

373 |

Geom* objects such as GeomPoint or GeomRect are responsible for translating your data into graphical objects. In ggplot these graphical objects are called “grobs”, short for Grid Graphical Objects, because they are encoded in a format defined by the grid R package. In order to create your own geoms you will need to learn how to use grid, or alternatively to re-use existing Geom* objects to generate grobs suited for your purposes. For example in this package we re-use GeomRect for GeomWaterfall.

374 |

Geom* objects are implemented using ggproto, a ggplot2 specific Object Oriented framework. ggproto is derived from the proto R OOP package.

375 |
376 |
377 |
378 |

Extending Ggplot 2 vignette

379 |
380 |

What ggplot2 Does

381 |
382 |
383 |

Creating Geoms

384 |

The setup_data and draw_panel functions we referenced above are part of the Geom* ggproto objects.

385 |

setup_data is used to convert parameters / aesthetics that are not in a format amenable to plotting, to one that is. One prime example is converting width and height to xmin/xmax and ymin/ymax values.

386 |

In “Creating a new Geom”, draw_panel is described as having 3 parameters, instead of the 4 + (in particular starting with self) in other docs and in the source. Additionally, the panel_scales param appears to actually be panel_params, at least in the sample fun we used (but ?Geom also references panel_scales).

387 |

For draw_panel sigs, we have

388 |
    389 |
  • GeomPoint: function(data, panel_params, coord, na.rm = FALSE)
  • 390 |
  • GeomRect: function(self, data, panel_params, coord)
  • 391 |
  • Geom: function(self, data, panel_params, coord, ...)
  • 392 |
393 |

From what I can infer from docs and source your function must accept at least three arguments. draw_layer from Geom will call draw_panel with three unnamed parameters that in theory should match up to data, panel_params, coord.

394 |
    395 |
  • data: A data.frame with all the aesthetic values as specified via aes. The column names corresponding to the aesthetic names, not the original data frame column names. Additionally contains PANEL, group (set to -1 if there are no groups), and any default aesthetics specified in Geom*$default_aes.
  • 396 |
  • panel_params: named list, the intersection of parameters provided to the geom_* function with the formals of the draw_panel method, although you can customize Geom objects to return a specific eligible parameter list.
  • 397 |
  • coord: coord transformation functions?
  • 398 |
399 |

Additionally, named parameters that are the intersection of the parameters provided to the geom_* function by the user and the parameters of draw_panel are supplied.

400 |

Finally, since draw_panel is a ggproto method self will be provided if it is part of the draw_panel signature.

401 |
402 |
403 |
404 |

Deprecated Stuff

405 |
406 |

setup_data vs reparametrise

407 |

It seems like both those functions can be used for the same purpose. For example, in GeomRect, setup_data is explicitly used to convert width to xmax/xmin.

408 |

Actually, looks like reparameterise doesn’t exist anymore?

409 |

draw_legend / draw_key

410 |
411 |
412 |
413 |

Grid Stuff

414 |

You cannot use gList in the key/legend. However, grobTree works.

415 |
416 | 417 | 418 | 419 | 420 | 428 | 429 | 430 | 431 | -------------------------------------------------------------------------------- /man/figures/README-geom-car-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-geom-car-1.png -------------------------------------------------------------------------------- /man/figures/README-geoms-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-geoms-1.png -------------------------------------------------------------------------------- /man/figures/README-geoms-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-geoms-2.png -------------------------------------------------------------------------------- /man/figures/README-stats-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-stats-1.png -------------------------------------------------------------------------------- /man/figures/README-stats-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-stats-2.png -------------------------------------------------------------------------------- /man/figures/README-waterfall-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-waterfall-1.png -------------------------------------------------------------------------------- /man/figures/README-waterfall-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/man/figures/README-waterfall-2.png -------------------------------------------------------------------------------- /man/geom.car.data.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/geom.car.data.R 3 | \docType{data} 4 | \name{geom.car.data} 5 | \alias{geom.car.data} 6 | \title{Car Coordinates} 7 | \format{A data frame with 8 rows and 6 variables 8 | \describe{ 9 | \item{vehicle}{vehicle id} 10 | \item{x}{x coordinate} 11 | \item{y}{y coordinate} 12 | \item{length}{vehicle length} 13 | \item{width}{vehicle width} 14 | \item{label}{vehicle description} 15 | }} 16 | \usage{ 17 | geom.car.data 18 | } 19 | \description{ 20 | A data frame with car coordinates and sizes for use in examples. 21 | } 22 | \keyword{datasets} 23 | -------------------------------------------------------------------------------- /man/geom_car.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/geom-car.R 3 | \name{geom_car} 4 | \alias{geom_car} 5 | \title{Cars} 6 | \usage{ 7 | geom_car(mapping = NULL, data = NULL, ..., inherit.aes = TRUE, 8 | show.legend = NA) 9 | } 10 | \arguments{ 11 | \item{mapping}{Set of aesthetic mappings created by \code{\link[=aes]{aes()}} or 12 | \code{\link[=aes_]{aes_()}}. If specified and \code{inherit.aes = TRUE} (the 13 | default), it is combined with the default mapping at the top level of the 14 | plot. You must supply \code{mapping} if there is no plot mapping.} 15 | 16 | \item{data}{The data to be displayed in this layer. There are three 17 | options: 18 | 19 | If \code{NULL}, the default, the data is inherited from the plot 20 | data as specified in the call to \code{\link[=ggplot]{ggplot()}}. 21 | 22 | A \code{data.frame}, or other object, will override the plot 23 | data. All objects will be fortified to produce a data frame. See 24 | \code{\link[=fortify]{fortify()}} for which variables will be created. 25 | 26 | A \code{function} will be called with a single argument, 27 | the plot data. The return value must be a \code{data.frame.}, and 28 | will be used as the layer data.} 29 | 30 | \item{...}{additional arguments passed on to \code{ggplot::layer}.} 31 | 32 | \item{inherit.aes}{If \code{FALSE}, overrides the default aesthetics, 33 | rather than combining with them. This is most useful for helper functions 34 | that define both data and aesthetics and shouldn't inherit behaviour from 35 | the default plot specification, e.g. \code{\link[=borders]{borders()}}.} 36 | 37 | \item{show.legend}{logical. Should this layer be included in the legends? 38 | \code{NA}, the default, includes if any aesthetics are mapped. 39 | \code{FALSE} never includes, and \code{TRUE} always includes. 40 | It can also be a named logical vector to finely select the aesthetics to 41 | display.} 42 | } 43 | \description{ 44 | A geom that displays cars. This geom was implemented on a lark as an answer 45 | to a \href{https://stackoverflow.com/questions/22159087/is-it-possible-to-draw-diagrams-in-r/22207979#22207979}{question on SO}. 46 | } 47 | \section{Aesthetics}{ 48 | 49 | \code{geom_car} understands the following aesthetics (required aesthetics are in bold): 50 | \itemize{ 51 | \item \strong{\code{x}} 52 | \item \strong{\code{y}} 53 | \item \strong{\code{length}} 54 | \item \strong{\code{width}} 55 | \item \code{alpha} 56 | \item \code{colour} 57 | \item \code{fill} 58 | \item \code{group} 59 | \item \code{linetype} 60 | \item \code{size} 61 | } 62 | Learn more about setting these aesthetics in \code{vignette("ggplot2-specs")} 63 | } 64 | 65 | \examples{ 66 | library(ggplot2) 67 | ggplot( 68 | geom.car.data, # ggbg data set 69 | aes(x=x, y=y, length=length, width=width, fill=label) 70 | ) + 71 | geom_hline(yintercept=seq(5, 35, by=10), color="white", size=2, linetype=2) + 72 | geom_car() + 73 | coord_equal() + 74 | theme(panel.background = element_rect(fill="#555555"), 75 | panel.grid.major = element_blank(), 76 | panel.grid.minor = element_blank()) 77 | } 78 | -------------------------------------------------------------------------------- /man/ggbg-ggproto.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/proto.R, R/geom-car.R, R/position-waterfall.R, 3 | % R/stat-waterfall.R 4 | \docType{data} 5 | \name{ggbg-ggproto} 6 | \alias{ggbg-ggproto} 7 | \alias{GeomCar} 8 | \alias{PositionWaterfall} 9 | \alias{StatWaterfall} 10 | \title{ggbg ggproto Objects} 11 | \format{An object of class \code{GeomCar} (inherits from \code{Geom}, \code{ggproto}, \code{gg}) of length 6.} 12 | \usage{ 13 | GeomCar 14 | 15 | PositionWaterfall 16 | 17 | StatWaterfall 18 | } 19 | \description{ 20 | These are all the \code{ggproto} objects implemented by \code{ggbg}. 21 | 22 | \code{GeomCar} renders cars on a layer. It is quite limited, but a useful example 23 | for understanding how to create custom geoms with custom grobs. 24 | 25 | \code{PositionWaterfall} is the \code{ggproto} object used to generate the position 26 | adjustments that correspond to \link{position_waterfall}. 27 | 28 | \code{StatWaterfall} is intended for use only via the \code{stat} parameter to other 29 | layers (e.g. \code{geom_...(stat='waterfall')}). It will add a \code{ycum} aesthetic 30 | to the data which can then be used by the layer. Unlike other \code{Stat} 31 | objects, it does not exist as a stand-alone layer (i.e. \code{stat_waterfall} is 32 | not defined). Its main purpose is to generate the text for cumulative 33 | labels. See \code{\link{position_waterfall}} for usage examples. 34 | } 35 | \seealso{ 36 | \code{\link[ggplot2:ggproto]{ggplot2::ggproto}} for details about \code{ggproto}. 37 | 38 | \code{\link{position_waterfall}} for usage examples. 39 | } 40 | \keyword{datasets} 41 | -------------------------------------------------------------------------------- /man/ggbg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggbg-package.R 3 | \docType{package} 4 | \name{ggbg} 5 | \alias{ggbg} 6 | \alias{ggbg-package} 7 | \title{Assorted GGPlot Extensions.} 8 | \description{ 9 | Assorted GGPlot Extensions. 10 | } 11 | -------------------------------------------------------------------------------- /man/position_waterfall.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/position-waterfall.R 3 | \name{position_waterfall} 4 | \alias{position_waterfall} 5 | \title{Stack Chart Elements on Cumulative Value} 6 | \usage{ 7 | position_waterfall(width = NULL, preserve = c("total", "single"), 8 | reverse = FALSE, dodge = TRUE, vjust = getOption("ggbg.vjust"), 9 | vjust.mode = getOption("ggbg.vjust.mode"), 10 | signif = getOption("ggbg.signif"), y.start = 0) 11 | } 12 | \arguments{ 13 | \item{width}{Dodging width, when different to the width of the individual 14 | elements. This is useful when you want to align narrow geoms with wider 15 | geoms. See the examples.} 16 | 17 | \item{preserve}{Should dodging preserve the total width of all elements 18 | at a position, or the width of a single element?} 19 | 20 | \item{reverse}{If \code{TRUE}, will reverse the default stacking order. 21 | This is useful if you're rotating both the plot and legend.} 22 | 23 | \item{dodge}{TRUE (default) or FALSE, controls how to resolve 24 | groups that overlap on the \code{x} axis. The default is to dodge them 25 | to form mini-waterfalls within each \code{x} value, but you can chose to stack 26 | them instead by setting \code{dodge=FALSE}. Negative and positive values are 27 | segregated prior to stacking so they do not overlap. Interpreting 28 | waterfall charts with stacked sub-groups is difficult when they contain 29 | negative values, so we recommend you use the default setting instead. 30 | Observations within a group that have the same \code{x} value are always 31 | stacked, so if you have both positive and negative values for any given \code{x} 32 | value you may want to consider segregating the positives and negatives in 33 | different groups.} 34 | 35 | \item{vjust}{like the \code{vjust} parameter for \code{\link[ggplot2:position_stack]{ggplot2::position_stack}}, 36 | except that by default the direction of justification follows the direction 37 | of the bar (see \code{vjust.mode}), and the default value is \code{0.5} instead of 38 | \code{1}. This only has an effect on geoms with positions like text, points, or 39 | lines. The default setting places elements midway through the height of 40 | the corresponding waterfall step. The default value is convenient for 41 | labeling \code{geom_col} waterfalls. Use \code{1} to position at the "end" of each 42 | waterfall step. This is different to the \code{vjust} for geoms like 43 | \code{geom_text} where \code{vjust=1} shift the text down, but it is consistent with 44 | what \code{\link[ggplot2:position_stack]{ggplot2::position_stack}} does.} 45 | 46 | \item{vjust.mode}{character(1L), one of "end" (default), or "top" where "top" 47 | results in the same behavior as in \code{\link[ggplot2:position_stack]{ggplot2::position_stack}}. "end" 48 | means the justification is relative to the "end" of the waterfall bar. So 49 | if a waterfall bar is heading down (i.e. negative \code{y} value), the "end" is 50 | at the bottom. If it heading up (i.e. positive \code{y} value), the "end" is at 51 | the top. For positive \code{y} values "end" and "top" do the same thing.} 52 | 53 | \item{signif}{integer(1L) between 1 and 22, defaults to 11, corresponds to 54 | the \code{digits} parameter for \code{\link{signif}} and is used to reduce the precision 55 | of numeric \code{x} aesthetic values so that stacking is not foiled by double 56 | precision imprecision.} 57 | 58 | \item{y.start}{numeric(1L), defaults to 0, will be starting point for the 59 | cumulative sum of \code{y} values. This could be useful if you want to combine 60 | waterfalls with other layers and need the waterfall to start at a specific 61 | value.} 62 | } 63 | \description{ 64 | A waterfall chart is a bar chart where each segment starts where the prior 65 | segment left off. This is similar to a stacked bar chart, except that 66 | the stacking does not reset across \code{x} values. It is the visualization of a 67 | cumulative sum. Another similar type of chart is the candlestick plot, 68 | except those have "whiskers", and typically require you to manually specify 69 | the \code{ymin} and \code{ymax} values. 70 | } 71 | \details{ 72 | \code{position_waterfall} creates waterfall charts when it is applied to 73 | \code{geom_col} or \code{geom_bar}. You can apply it to any geom, so long as the 74 | geom specifies a \code{y} aesthetic, and either an \code{x} aesthetic, or both 75 | \code{xmin} and \code{xmax} aesthetics. It may not make sense to apply 76 | \code{position_waterfall} to arbitrary geoms, particularly those that represent 77 | single graphical elements with multiple x/y coordinates such as 78 | \code{geom_polygon}. \code{ymin}/\code{ymax} aesthetics will be shifted by the cumulative 79 | \code{y} value. 80 | 81 | Since stat layers are computed prior to position adjustments, you can also 82 | use \code{position_waterfall} with stats (e.g \code{stat_bin}, see examples). 83 | 84 | We also implement a \link[=ggbg-ggproto]{StatWaterfall} \code{ggproto} object that 85 | can be accessed within \code{geom_*} calls by specifying \code{stat='waterfall'}. 86 | Unlike typical stat \code{ggproto} objects, this one does not have a layer 87 | instantiation function (i.e. \code{stat_waterfall} does not exist). The sole 88 | purpose of the stat is to compute the \code{ycum} aesthetic that can then be used 89 | by the \code{geom} layer (see the labeling examples). 90 | } 91 | \section{Stacking}{ 92 | 93 | 94 | The stacking is always computed on the \code{y} aesthetic. The order of the 95 | stacking is determined by the \code{x} aesthetic. The actual position of the 96 | objects are also affected by \code{vjust}, and you may need to change the value of 97 | \code{vjust} if you are using \code{position_waterfall} with geoms other than columns. 98 | For example, for dimensionless elements such as \code{geom_point} and \code{geom_text}, 99 | the default \code{vjust} of 0.5 leads to alignment at the midpoint between 100 | previous and subsequent values in the cumulative sequence. 101 | 102 | If only \code{xmin} and \code{xmax} aesthetics are present the \code{x} value will be 103 | inferred as the midpoint of those two. 104 | } 105 | 106 | \section{Dodging}{ 107 | 108 | 109 | Unlike most \code{position_*} adjustments, \code{position_waterfall} adjust positions 110 | across different \code{x} values. However, we still need to resolve \code{x} 111 | value overlaps. The default approach is to apply the same type of adjustment 112 | across groups within any given \code{x} value. This stacks and dodges elements. 113 | 114 | Dodging involves changing the \code{width} of the geom and also shifting the 115 | \code{geom} horizontally. Geom width adjustments will always be made based on the 116 | \code{xmin}/\code{xmax}/\code{width} aesthetics. The shifting itself can be controlled 117 | separately with \code{position_waterfall(width=...)}. That parameter should 118 | really be called \code{dodge.width} to avoid confusion with the geom \code{width}, but 119 | we left it as \code{width} for consistency with \code{\link{position_dodge}}. 120 | 121 | You you can turn off dodging within \code{x} values by setting 122 | \code{position_waterfall(dodge=FALSE)} which will result in stacking within each 123 | \code{x} value. 124 | } 125 | 126 | \examples{ 127 | ## These examples are best run via `example(position_waterfall)` 128 | library(ggplot2) 129 | dat <- data.frame(x=3:1, y=1:3) 130 | p1 <- ggplot(dat, aes(x=x, y=y)) + geom_col(position='waterfall') 131 | 132 | ## Add text or labels; defaults to middle waterfall position 133 | ## which can be modified with `vjust` 134 | p1 + geom_label(aes(label=x), position='waterfall') 135 | 136 | ## We can also add the cumulative running to the top of 137 | ## the bars with `stat='waterfall'` and position adjustments 138 | p1 + geom_label(aes(label=x), position='waterfall') + 139 | geom_label( 140 | stat="waterfall", # adds `ycum` computed variable 141 | aes(label=stat(ycum)), # which we can use for label 142 | position=position_waterfall(vjust=1), # text to end of column 143 | vjust=0, # tweak so it's on top 144 | ) 145 | ## A poor person's candlestick chart: 146 | dat.r.walk <- data.frame(x=1:20, y=rnorm(20)) 147 | ggplot(dat.r.walk, aes(x=x, y=y, fill=y > 0)) + 148 | geom_col(position='waterfall') 149 | 150 | ## We can use arbitrary geoms 151 | ggplot(dat, aes(x=x, y=y)) + 152 | geom_point() + 153 | geom_point(position='waterfall', color='blue') + # default vjust=0.5 154 | geom_point(position=position_waterfall(vjust=1), color='red') 155 | 156 | ## Or stats; here we turn a histogram into an ecdf plot 157 | dat.norm <- data.frame(x=rnorm(1000)) 158 | ggplot(dat.norm, aes(x=x)) + geom_histogram(position='waterfall') 159 | ggplot(dat.norm, aes(x=x)) + stat_bin(position='waterfall') 160 | 161 | ## Data with groups 162 | dat3 <- data.frame( 163 | x=c(3, 2, 2, 2, 1, 1), y=c(-3, 1, 4, -6, -1, 10), 164 | grp=rep(c("A", "B", "C"), lenght.out=6) 165 | ) 166 | p2 <- ggplot(dat3, aes(x=x, y=y, fill=grp)) 167 | p2 + geom_col(position="waterfall") 168 | 169 | ## Equal width columns 170 | p2 + geom_col(position=position_waterfall(preserve='single')) 171 | 172 | ## Stacking groups is possible, bug hard to interpret when 173 | ## negative values present 174 | p2 + geom_col(position=position_waterfall(dodge=FALSE)) 175 | } 176 | -------------------------------------------------------------------------------- /tests/run.R: -------------------------------------------------------------------------------- 1 | # to avoid variability on terminals with different capabilities 2 | # plus generally random options being set 3 | 4 | if(getRversion() < "3.2.0") { 5 | warning("Cannot run tests with R version less than 3.2.0.") 6 | } else if(suppressWarnings(require('unitizer'))) { 7 | old.opt <- options( 8 | # warnPartialMatchArgs = TRUE, 9 | # warnPartialMatchAttr = TRUE, 10 | # warnPartialMatchDollar = TRUE 11 | ) 12 | on.exit(old.opt) 13 | unitize_dir('unitizer', state='recommended') 14 | } else { 15 | warning("Cannot run tests without package `unitizer`") 16 | } 17 | -------------------------------------------------------------------------------- /tests/unitizer/car.R: -------------------------------------------------------------------------------- 1 | library(ggbg) 2 | library(ggplot2) 3 | 4 | unitizer_sect("Car", { 5 | p <- ggplot( 6 | geom.car.data, aes(x=x, y=y, length=length, width=width, fill=label) 7 | ) + geom_car() 8 | unclass(ggplot_build(p)$plot) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/unitizer/car.unitizer/data.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/tests/unitizer/car.unitizer/data.rds -------------------------------------------------------------------------------- /tests/unitizer/waterfall.R: -------------------------------------------------------------------------------- 1 | 2 | library(ggplot2) 3 | library(ggbg) 4 | 5 | unitizer_sect("basic A", { 6 | dat <- data.frame(x=3:1, y=1:3) 7 | 8 | gb.0 <- ggplot(dat, aes(x=x, y=y)) 9 | p0 <- gb.0 + geom_col(position=position_waterfall()) 10 | ggplot_build(p0)[["data"]] 11 | 12 | p1 <- gb.0 + geom_col(position="waterfall") 13 | ggplot_build(p1)[["data"]] 14 | 15 | p2 <- ggplot(dat, aes(x=x)) + geom_bar(position="waterfall") 16 | ggplot_build(p2)[["data"]] 17 | 18 | # add overlapping values 19 | 20 | dat2 <- data.frame( 21 | x=c(3, 2, 2, 2, 1, 1), y=1:6, grp=rep(c("A", "B", "C"), lenght.out=6) 22 | ) 23 | gb.1 <- ggplot(dat2, aes(x=x, y=y, fill=grp)) 24 | p3 <- gb.1 + geom_col(position="waterfall") 25 | ggplot_build(p3)[["data"]] 26 | 27 | p4 <- gb.1 + geom_col(position=position_waterfall(dodge=FALSE)) 28 | ggplot_build(p4)[["data"]] 29 | 30 | # negative values 31 | 32 | dat3 <- data.frame( 33 | x=c(3, 2, 2, 2, 1, 1), 34 | y=c(-3, 1, 4, -6, -1, 10), 35 | grp=rep(c("A", "B", "C"), lenght.out=6) 36 | ) 37 | gb.2 <- ggplot(dat3, aes(x=x, y=y, fill=grp)) 38 | 39 | p5 <- gb.2 + geom_col(position=position_waterfall(dodge=FALSE)) 40 | ggplot_build(p5)[["data"]] 41 | 42 | p6 <- gb.2 + geom_col(position="waterfall") 43 | ggplot_build(p6)[["data"]] 44 | 45 | p6a <- gb.2 + geom_col(position=position_waterfall(reverse=TRUE)) 46 | ggplot_build(p6a)[["data"]] 47 | 48 | # preserve 49 | 50 | pw.pres.dodge <- position_waterfall(preserve="single") 51 | p7 <- gb.2 + geom_col(position=pw.pres.dodge) 52 | ggplot_build(p7)[["data"]] 53 | 54 | pw.pres.dodge.rev <- position_waterfall( 55 | preserve="single", reverse=TRUE 56 | ) 57 | p7a <- gb.2 + geom_col(position=pw.pres.dodge.rev) 58 | ggplot_build(p7a)[["data"]] 59 | 60 | # duplicate group values 61 | 62 | p10 <- ggplot(rbind(dat, dat), aes(x=x, y=y)) + 63 | geom_col(position="waterfall", color="white") 64 | 65 | ggplot_build(p10)[["data"]] 66 | 67 | # non-numeric x values 68 | 69 | dat3a <- transform(dat3, xchr=LETTERS[x]) 70 | p22 <- ggplot(dat3a, aes(x=xchr, y=y, fill=grp)) + 71 | geom_col(position='waterfall') 72 | ggplot_build(p22)[["data"]] 73 | }) 74 | unitizer_sect("other geoms B ", { 75 | dat <- data.frame(x=3:1, y=1:3) 76 | gb.0 <- ggplot(dat, aes(x=x, y=y)) 77 | p8 <- gb.0 + geom_point(position=position_waterfall()) 78 | 79 | ggplot_build(p8)[["data"]] 80 | 81 | p8a <- gb.0 + geom_tile(position='waterfall') 82 | ggplot_build(p8a)[["data"]] 83 | 84 | p8b <- gb.0 + geom_tile(width=2, height=2, position='waterfall') 85 | ggplot_build(p8b)[["data"]] 86 | 87 | # geoms with multiple x/y values per group in one entity 88 | 89 | dat8 <- data.frame( 90 | x=c(1:3, 1:3), y=c(1,2,1,2,1,2), grp=rep(c("A", "B"), each=3) 91 | ) 92 | p8c <- ggplot(dat8, aes(x=x, y=y, fill=grp)) + 93 | geom_polygon(position='waterfall') 94 | ggplot_build(p8c)[['data']] 95 | }) 96 | unitizer_sect("vjust and labels and facets C", { 97 | dat5 <- rbind( 98 | cbind(dat3, facet="X"), 99 | cbind(transform(dat3, x=rev(x)), facet="Y") 100 | ) 101 | p9 <- 102 | ggplot(dat5, aes(x=x, y=y, fill=grp)) + 103 | geom_col(position='waterfall') + 104 | geom_label( 105 | aes(label=y, group=grp), 106 | position='waterfall', color="gray", fill="white" 107 | ) + 108 | geom_text( 109 | aes(label=stat(ycum), vjust=ifelse(y < 0, 1.5, -0.5)), 110 | position=position_waterfall(vjust=1), 111 | stat="waterfall", 112 | size=6 113 | ) + 114 | facet_wrap(~facet) 115 | 116 | ggplot_build(p9)[["data"]] 117 | }) 118 | unitizer_sect("y offset D", { 119 | pD1 <- gb.1 + geom_col(position=position_waterfall(y.start=5)) 120 | ggplot_build(pD1)[['data']] 121 | pD2 <- gb.1 + geom_col(position=position_waterfall(y.start=-5)) 122 | ggplot_build(pD2)[['data']] 123 | pD3 <- ggplot(dat2, aes(x=x, y=y)) + 124 | geom_col(position=position_waterfall(y.start=-5)) + facet_wrap(~grp) 125 | ggplot_build(pD3)[['data']] 126 | }) 127 | unitizer_sect("corner cases E", { 128 | # empty data 129 | p11 <- ggplot(data.frame(x=numeric(), y=numeric())) + 130 | geom_col(position='waterfall') 131 | ggplot_build(p11)[["data"]] 132 | 133 | p12 <- ggplot(data.frame()) + geom_col(position='waterfall') 134 | ggplot_build(p12)[["data"]] 135 | 136 | # variations on missing data 137 | 138 | dat6 <- data.frame(x=3:1, y=1:3) 139 | dat6 <- transform(dat6, xmin=x-.6, xmax=x+.6, ymin=0, ymax=y) 140 | 141 | p13 <- ggplot(dat6, aes(xmin=xmin, xmax=xmax, ymax=ymax, ymin=ymin)) + 142 | geom_rect(position='waterfall') 143 | ggplot_build(p13)[["data"]] 144 | 145 | p14 <- ggplot(dat6, aes(xmin=xmin, xmax=xmax, y=y)) + 146 | ggbg:::geom_null(position=position_waterfall(vjust=1)) 147 | ggplot_build(p14)[["data"]] 148 | 149 | p15 <- ggplot(dat6, aes(xmin=xmin, y=y)) + 150 | ggbg:::geom_null(position="waterfall") 151 | ggplot_build(p15)[["data"]] 152 | 153 | p16 <- ggplot(dat6, aes(xmin=xmin, x=x, y=y)) + 154 | ggbg:::geom_null(position="waterfall") 155 | ggplot_build(p16)[["data"]] 156 | 157 | p17 <- ggplot(dat6, aes(x=x, y=y, ymin=ymin)) + 158 | ggbg:::geom_null(position="waterfall") 159 | ggplot_build(p17)[["data"]] 160 | 161 | # conflicting widths 162 | 163 | dat7 <- data.frame( 164 | x=rep(2:1, 2), y=1:4, grp=rep(c('A', 'B'), each=2), width=1:4 165 | ) 166 | p18 <- ggplot(dat7, aes(x=x, y=y, fill=grp)) + 167 | geom_col(position=position_waterfall(width=0.5)) + 168 | geom_point(position=position_waterfall(width=0.5)) 169 | ggplot_build(p18)[["data"]] 170 | 171 | p19 <- ggplot(dat7, aes(x=x, y=y, fill=grp, width=width)) + 172 | geom_col(position=position_waterfall(width=0.5)) + 173 | geom_point(position=position_waterfall(width=0.5)) 174 | ggplot_build(p19)[["data"]] 175 | 176 | p20 <- ggplot(dat7, aes(x=x, y=y, fill=grp)) + 177 | geom_col(position=position_waterfall(width=0.5), width=2) + 178 | geom_point(position=position_waterfall(width=0.5)) 179 | ggplot_build(p20)[["data"]] 180 | 181 | p21 <- ggplot(dat7, aes(x=x, y=y, fill=grp)) + 182 | geom_col(position='waterfall', width=2) 183 | ggplot_build(p21)[["data"]] 184 | }) 185 | unitizer_sect("errors F", { 186 | gb.0 + geom_col(position=position_waterfall(signif=-1)) 187 | }) 188 | -------------------------------------------------------------------------------- /tests/unitizer/waterfall.unitizer/data.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brodieG/ggbg/4ff916d231d6eabda56a6c9e2ce4fc43813ce278/tests/unitizer/waterfall.unitizer/data.rds -------------------------------------------------------------------------------- /vignettes/extensions.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Writing Ggplot2 Extensions" 3 | author: "Brodie Gaslam" 4 | output: 5 | rmarkdown::html_vignette: 6 | toc: true 7 | css: styles.css 8 | 9 | vignette: > 10 | %\VignetteIndexEntry{extensions} 11 | %\VignetteEngine{knitr::rmarkdown} 12 | %\usepackage[utf8]{inputenc} 13 | --- 14 | 15 | ```{r global_options, echo=FALSE} 16 | knitr::opts_chunk$set(error=TRUE, comment=NA) 17 | library(ggbg) 18 | ``` 19 | 20 | This document is under development. 21 | 22 | ## Introduction 23 | 24 | I wrote this document while figuring out how to write my own custom geoms for 25 | `ggplot`. The existing documentation available through `?Geom` and 26 | `browseVignettes('ggplot2')` was a great starting point for me, but I still 27 | had to dive pretty deep into the `ggplot2` sources before I felt I had all the 28 | information I needed to write extension. 29 | 30 | This is unofficial documentation based on my interpretation of the sources as of 31 | ~4/18/2018. It may become out-of-date as `ggplot2` evolves. 32 | 33 | This document assumes the reader has some basic familiarity with: 34 | 35 | * how to use `ggplot2` for plotting purposes. 36 | * `ggproto`, the object system used by `ggplot2` (see `?ggplot2::ggproto`, and 37 | `browseVignettes('proto')` as `ggproto` is derived from `proto`). 38 | * `grid` 39 | 40 | 41 | ## Ggplot Basics - an Extension Developer Perspective 42 | 43 | ### It's The Printing, Stupid 44 | 45 | In order to create and display a `ggplot` plot we go through two major steps: 46 | 47 | * Specification (`ggplot() + ...`): user specifies what the plot 48 | should look like with `geom_*`, `stat_*`, `scale_*`, etc.. 49 | * Generation and rendering, typically triggered by printing a `ggplot` 50 | object. 51 | 52 | As a `ggplot` user you are mostly concerned with the specification of the plot. 53 | As a `ggplot` extension developer, you are also concerned with the plot 54 | generation step. This is were `ggplot` does most of the work. You can look at 55 | `ggplot2:::print.ggplot` to see the major steps involved, which can be 56 | summarized as: 57 | 58 | 1. Build (`ggplot_build`): analyzes and transforms data into a format suited for 59 | translation into graphical objects: 60 | 1. Assigns facet panels. 61 | 2. Computes aesthetic values from data and `aes(...)` specifications. 62 | 3. Assigns groups. 63 | 4. Rescales data if non linear scales are specified. 64 | 5. Computes and maps statistics. 65 | 6. Transforms special aesthetics (e.g. `width` to `xmin`-`xmax`) 66 | (Layer$compute_geom_1 -> Geom$setup_data, check_required_aesthetic) 67 | 7. Adjusts positions (e.g. dodging/stacking with `$compute_positions`). 68 | (Layer$compute_positions -> 69 | Position$setup_params/setup_data/compute_layer) 70 | 8. Recompute scales. 71 | 9. Adds default aesthetics as needed. 72 | 2. Graphical Object Construction (`ggplot_gtable`): 73 | 1. Applies coordinate transformations (if necessary) 74 | 2. Translate the data into shapes, colors, positions, etc. 75 | 3. Rendering (`grid.draw`): display the resulting plot in a viewport. 76 | 77 | Need to discuss what is implemented via the `ggproto` objects. 78 | 79 | Each of the sub-steps in the build step is applied to every layer before moving 80 | to the next step with the `by_layer` function. Additionally, each proto driven 81 | calculation follows this hierarchy: 82 | 83 | `(draw|compute)_layer` 84 | `(draw|compute)_panel` 85 | `(draw|compute)_group` 86 | 87 | The `*_layer` functions typically split the data by panel and forward each 88 | chunk to the corresponding `*_panel` function. The base `ggproto` `*_panel` 89 | methods will split the data by group and forward it to the corresponding 90 | `*_group` function. This allows you to override either the `*_panel` or the 91 | `*_group` function depending on what you are trying to do. 92 | 93 | ## Understanding the `data` Object 94 | 95 | * Internal aesthetics 96 | * Special variables (`group`, `panel`) 97 | * `group` are "integer" values from 1 to n where n is the number of groups, or 98 | -1 if there aren't any groups. 99 | * Computed variables (`..level..`) 100 | 101 | ## Position Adjustments 102 | 103 | Step 1.7 is carried out by modifying the coordinates in the data objects. 104 | Both `position_dodge` and `position_stack` group data by `xmin` coordinate value 105 | (either user provided, or derived from `x` and `width` or some such), and then 106 | resolve any `xmin` overlaps by allocating the available width for each distinct 107 | `group` (or should it be element?) within. 108 | 109 | ## Alternate Docs 110 | 111 | ### On `gg_proto` 112 | 113 | Things to know: 114 | 115 | * Objects that contain member functions and data 116 | * The members can be accessed like list objects `object$member` 117 | * When member functions are invoked they are always automatically provided with 118 | a `self` object if `self` is part of the signature (you can probably still 119 | access `self` even if it isn't in the sig, need to test). 120 | 121 | ### Ggplot Basics 122 | 123 | ### Mapping 124 | 125 | 126 | ### Rendering 127 | 128 | 129 | 130 | Extending `ggplot` requires influencing how steps 1. and 2. are carried out. 131 | This is done by creating layer functions (e.g. `geom_*` or `stat_*`) functions that return layers containing custom `Geom*` or `Stat*` objects. 132 | 133 | 134 | 135 | ``` 136 | ## For reference, `sys.calls()` from a debugged `setup_data`: 137 | $ : language function (x, ...) UseMethod("print")(x) 138 | $ : language print.ggplot(x) 139 | $ : language ggplot_build(x) 140 | $ : language by_layer(function(l, d) l$compute_geom_1(d)) 141 | $ : language f(l = layers[[i]], d = data[[i]]) 142 | $ : language l$compute_geom_1(d) 143 | $ : language f(..., self = self) 144 | $ : language self$geom$setup_data(data, c(self$geom_params, self$aes_params)) 145 | $ : language f(...) 146 | ``` 147 | 148 | ### Geom Basics 149 | 150 | To implement a geom you need: 151 | 152 | * A `geom_*` standard function 153 | * A `Geom*` object, possibly re-used from an existing geom 154 | 155 | ### The `Geom*` Object 156 | 157 | `Geom*` objects such as `GeomPoint` or `GeomRect` are responsible for 158 | translating your data into graphical objects. In `ggplot` these graphical 159 | objects are called "grobs", short for Grid Graphical Objects, because they are 160 | encoded in a format defined by the `grid` R package. In order to create your 161 | own geoms you will need to learn how to use `grid`, or alternatively to re-use 162 | existing `Geom*` objects to generate grobs suited for your purposes. For 163 | example in this package we re-use `GeomRect` for `GeomWaterfall`. 164 | 165 | `Geom*` objects are implemented using `ggproto`, a `ggplot2` specific Object 166 | Oriented framework. `ggproto` is derived from the `proto` R OOP package. 167 | 168 | 169 | 170 | ## Extending Ggplot 2 vignette 171 | 172 | ### What `ggplot2` Does 173 | 174 | ### Creating Geoms 175 | 176 | The `setup_data` and `draw_panel` functions we referenced above are part of the 177 | `Geom*` `ggproto` objects. 178 | 179 | `setup_data` is used to convert parameters / aesthetics that are not in a format 180 | amenable to plotting, to one that is. One prime example is converting `width` 181 | and `height` to `xmin/xmax` and `ymin/ymax` values. 182 | 183 | 184 | In "Creating a new Geom", `draw_panel` is described as having 3 parameters, 185 | instead of the 4 + (in particular starting with `self`) in other docs and in the 186 | source. Additionally, the `panel_scales` param appears to actually be 187 | `panel_params`, at least in the sample fun we used (but `?Geom` also references 188 | `panel_scales`). 189 | 190 | For `draw_panel` sigs, we have 191 | 192 | * `GeomPoint`: `function(data, panel_params, coord, na.rm = FALSE)` 193 | * `GeomRect`: `function(self, data, panel_params, coord)` 194 | * `Geom`: `function(self, data, panel_params, coord, ...)` 195 | 196 | From what I can infer from docs and source your function must accept at least 197 | three arguments. `draw_layer` from `Geom` will call `draw_panel` with three 198 | unnamed parameters that in theory should match up to `data`, `panel_params`, 199 | `coord`. 200 | 201 | * `data`: A data.frame with all the aesthetic values as specified via `aes`. 202 | The column names corresponding to the aesthetic names, not the original data 203 | frame column names. Additionally contains `PANEL`, `group` (set to -1 if 204 | there are no groups), and any default aesthetics specified in 205 | `Geom*$default_aes`. 206 | * `panel_params`: named list, the intersection of parameters provided to the 207 | `geom_*` function with the formals of the `draw_panel` method, although you 208 | can customize `Geom` objects to return a specific eligible parameter list. 209 | * `coord`: coord transformation functions? 210 | 211 | Additionally, named parameters that are the intersection of the parameters 212 | provided to the `geom_*` function by the user and the parameters of `draw_panel` 213 | are supplied. 214 | 215 | Finally, since `draw_panel` is a `ggproto` method `self` will be provided if it 216 | is part of the `draw_panel` signature. 217 | 218 | ## Deprecated Stuff 219 | 220 | ### `setup_data` vs `reparametrise` 221 | 222 | It seems like both those functions can be used for the same purpose. For 223 | example, in `GeomRect`, `setup_data` is explicitly used to convert width to 224 | `xmax/xmin`. 225 | 226 | Actually, looks like `reparameterise` doesn't exist anymore? 227 | 228 | draw_legend / draw_key 229 | 230 | ## Grid Stuff 231 | 232 | You cannot use `gList` in the key/legend. However, `grobTree` works. 233 | 234 | -------------------------------------------------------------------------------- /vignettes/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styles primarily borrowed from rmarkdown/templates/html_vignette/resources/vignette.css 3 | at a time 12/2/2014 when rmarkdown was (and probably still is) under the GPL-3 4 | license 5 | */ 6 | 7 | body { 8 | background-color: #fff; 9 | margin: 1em auto; 10 | max-width: 700px; 11 | overflow: visible; 12 | padding-left: 2em; 13 | padding-right: 2em; 14 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | font-size: 14px; 16 | line-height: 1.5; 17 | } 18 | 19 | #header { 20 | text-align: center; 21 | } 22 | 23 | #TOC { 24 | clear: both; 25 | /*margin: 0 0 10px 10px;*/ 26 | padding: 4px; 27 | width: 100%; 28 | border: 1px solid #CCCCCC; 29 | border-radius: 5px; 30 | 31 | background-color: #f6f6f6; 32 | font-size: 13px; 33 | line-height: 1.3; 34 | } 35 | #TOC .toctitle { 36 | font-weight: bold; 37 | font-size: 15px; 38 | margin-left: 5px; 39 | } 40 | 41 | #TOC ul { 42 | padding-left: 40px; 43 | margin-left: -1.5em; 44 | margin-top: 5px; 45 | margin-bottom: 5px; 46 | } 47 | #TOC ul ul { 48 | margin-left: -2em; 49 | } 50 | #TOC li { 51 | line-height: 16px; 52 | } 53 | 54 | table { 55 | margin: 1em auto; 56 | border-width: 1px; 57 | border-color: #DDDDDD; 58 | border-style: outset; 59 | border-collapse: collapse; 60 | } 61 | table th { 62 | border-width: 2px; 63 | padding: 5px; 64 | border-style: inset; 65 | } 66 | table td { 67 | border-width: 1px; 68 | border-style: inset; 69 | line-height: 18px; 70 | padding: 5px 5px; 71 | } 72 | table, table th, table td { 73 | border-left-style: none; 74 | border-right-style: none; 75 | } 76 | table thead, table tr.even { 77 | background-color: #f7f7f7; 78 | } 79 | 80 | p { 81 | margin: 1em 0; 82 | } 83 | 84 | blockquote { 85 | background-color: #f6f6f6; 86 | padding: 0.25em 0.75em; 87 | } 88 | 89 | hr { 90 | border-style: solid; 91 | border: none; 92 | border-top: 1px solid #777; 93 | margin: 28px 0; 94 | } 95 | 96 | dl { 97 | margin-left: 0; 98 | } 99 | dl dd { 100 | margin-bottom: 13px; 101 | margin-left: 13px; 102 | } 103 | dl dt { 104 | font-weight: bold; 105 | } 106 | 107 | ul { 108 | margin-top: 0; 109 | } 110 | ul li { 111 | list-style: circle outside; 112 | } 113 | ul ul { 114 | margin-bottom: 0; 115 | } 116 | 117 | h3.subtitle { 118 | margin-top: -23px; 119 | } 120 | pre, code { 121 | background-color: #EEE; 122 | color: #333; 123 | white-space: pre-wrap; /* Wrap long lines */ 124 | /*border-radius: 3px;*/ 125 | } 126 | code {font-size: 85%;} 127 | pre { 128 | border: 2px solid #EEE; 129 | overflow: auto; 130 | /* 131 | border-radius: 3px; 132 | */ 133 | margin: 5px 0px; 134 | padding: 5px 10px; 135 | } 136 | pre:not([class]) { 137 | color: #353; 138 | /*border-radius: 0px 0px 3px 3px;*/ 139 | } 140 | div.sourceCode pre, div.sourceCode code { 141 | background-color: #FAFAFA; 142 | } 143 | div.sourceCode pre{ 144 | /*border-radius: 3px 3px 0px 0px;*/ 145 | } 146 | div.sourceCode + pre, 147 | div.sourceCode + div.diffobj_container { 148 | margin-top: -5px; 149 | } 150 | div.diffobj_container pre{ 151 | line-height: 1.3; 152 | } 153 | /* 154 | pre:not([class]) { 155 | background-color: #eee; 156 | } 157 | */ 158 | 159 | code { 160 | font-family: Consolas, Monaco, 'Courier New', monospace; 161 | } 162 | p > code, li > code, h1 > code, h2 > code, h3 > code, 163 | h4 > code, h5 > code, h6 > code { 164 | padding: 2px 0px; 165 | line-height: 1; 166 | font-weight: bold; 167 | } 168 | div.figure { 169 | text-align: center; 170 | } 171 | img { 172 | background-color: #FFFFFF; 173 | padding: 2px; 174 | border: 1px solid #DDDDDD; 175 | border-radius: 3px; 176 | border: 1px solid #CCCCCC; 177 | margin: 0 5px; 178 | } 179 | 180 | h1 { 181 | margin-top: 0; 182 | padding-bottom: 3px; 183 | font-size: 35px; 184 | line-height: 40px; 185 | border-bottom: 1px solid #999; 186 | } 187 | 188 | h2 { 189 | border-bottom: 1px solid #999; 190 | padding-top: 5px; 191 | padding-bottom: 2px; 192 | font-size: 145%; 193 | } 194 | 195 | h3 { 196 | padding-top: 5px; 197 | font-size: 120%; 198 | } 199 | 200 | h4 { 201 | /*border-bottom: 1px solid #f7f7f7;*/ 202 | color: #777; 203 | font-size: 105%; 204 | } 205 | h4.author {display: none;} 206 | h4.date {margin-top: -20px;} 207 | 208 | h5, h6 { 209 | /*border-bottom: 1px solid #ccc;*/ 210 | font-size: 105%; 211 | } 212 | 213 | a { 214 | color: #2255dd; 215 | font-weight: bold; 216 | text-decoration: none; 217 | } 218 | a:hover { 219 | color: #6666ff; } 220 | a:visited { 221 | color: #800080; } 222 | a:visited:hover { 223 | color: #BB00BB; } 224 | a[href^="http:"] { 225 | text-decoration: underline; } 226 | a[href^="https:"] { 227 | text-decoration: underline; } 228 | 229 | /* Class described in https://benjeffrey.com/posts/pandoc-syntax-highlighting-css 230 | Colours from https://gist.github.com/robsimmons/1172277 */ 231 | 232 | code > span.kw { color: #555; font-weight: bold; } /* Keyword */ 233 | code > span.dt { color: #902000; } /* DataType */ 234 | code > span.dv { color: #40a070; } /* DecVal (decimal values) */ 235 | code > span.bn { color: #555; } /* BaseN */ 236 | code > span.fl { color: #555; } /* Float */ 237 | code > span.ch { color: #555; } /* Char */ 238 | code > span.st { color: #40a070; } /* String */ 239 | code > span.co { color: #888888; font-style: italic; } /* Comment */ 240 | code > span.ot { color: #007020; } /* OtherToken */ 241 | code > span.al { color: #ff0000; font-weight: bold; } /* AlertToken */ 242 | code > span.fu { color: #900; font-weight: bold; } /* Function calls */ 243 | code > span.er { color: #a61717; background-color: #e3d2d2; } /* ErrorTok */ 244 | --------------------------------------------------------------------------------