├── .codecov.yml ├── src ├── .gitignore ├── length.h ├── gridtext_types.h ├── null-box.h ├── grid-renderer.cpp ├── penalty.h ├── text-box.h ├── grid.h ├── glue.h ├── layout.h ├── vbox.h ├── grid-renderer.h ├── raster-box.h ├── grid.cpp └── par-box.h ├── tests ├── testthat │ ├── .gitignore │ ├── helper-vdiffr.R │ ├── test-get_file.R │ ├── test-null-box.R │ ├── test-text-details.R │ ├── test-vbox.R │ ├── test-grid-renderer.R │ ├── test-raster-box.R │ └── test-grid-constructors.R ├── figs │ ├── deps.txt │ ├── test_image.png │ ├── grid-renderer │ │ ├── text-in-different-stylings.svg │ │ └── mixing-text-and-boxes.svg │ ├── richtext-grob │ │ ├── aligned-heights.svg │ │ ├── aligned-widths.svg │ │ ├── various-text-boxes-w-debug.svg │ │ └── various-text-boxes.svg │ └── textbox-grob │ │ ├── multiple-boxes-internal-alignment.svg │ │ └── box-spanning-entire-viewport-with-margins.svg └── testthat.R ├── LICENSE ├── inst └── extdata │ └── Rlogo.png ├── docs ├── reference │ ├── Rplot001.png │ ├── Rplot002.png │ ├── Rplot003.png │ ├── Rplot004.png │ ├── richtext_grob-1.png │ ├── richtext_grob-2.png │ ├── textbox_grob-1.png │ ├── textbox_grob-2.png │ ├── textbox_grob-3.png │ ├── figures │ │ ├── README-unnamed-chunk-4-1.png │ │ ├── README-unnamed-chunk-5-1.png │ │ ├── README-unnamed-chunk-6-1.png │ │ ├── README-unnamed-chunk-7-1.png │ │ └── README-unnamed-chunk-8-1.png │ ├── gridtext.html │ └── index.html ├── pkgdown.yml ├── link.svg ├── bootstrap-toc.css ├── docsearch.js ├── pkgdown.js ├── bootstrap-toc.js ├── LICENSE-text.html ├── 404.html ├── authors.html ├── ISSUE_TEMPLATE.html ├── LICENSE.html └── news │ └── index.html ├── man ├── figures │ ├── README-unnamed-chunk-4-1.png │ ├── README-unnamed-chunk-5-1.png │ ├── README-unnamed-chunk-6-1.png │ ├── README-unnamed-chunk-7-1.png │ └── README-unnamed-chunk-8-1.png ├── gridtext.Rd └── richtext_grob.Rd ├── _pkgdown.yml ├── cran-comments.md ├── .Rbuildignore ├── R ├── gridtext.R ├── grob-zero.R ├── recycle-gpar.R ├── read-image.R ├── parse-css.R ├── grid-utils.R ├── drawing-context.R ├── text-details.R ├── RcppExports.R └── process-tags.R ├── gridtext.Rproj ├── NAMESPACE ├── .gitignore ├── ISSUE_TEMPLATE.md ├── NEWS.md ├── LICENSE.md ├── DESCRIPTION ├── .github └── workflows │ ├── test-coverage.yaml │ └── R-CMD-check.yaml └── README.Rmd /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | 5 | -------------------------------------------------------------------------------- /tests/testthat/.gitignore: -------------------------------------------------------------------------------- 1 | Rplots.pdf 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: Claus O. Wilke 3 | -------------------------------------------------------------------------------- /inst/extdata/Rlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/inst/extdata/Rlogo.png -------------------------------------------------------------------------------- /tests/figs/deps.txt: -------------------------------------------------------------------------------- 1 | - vdiffr-svg-engine: 1.0 2 | - vdiffr: 0.3.3 3 | - freetypeharfbuzz: 0.2.5 4 | -------------------------------------------------------------------------------- /tests/figs/test_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/tests/figs/test_image.png -------------------------------------------------------------------------------- /docs/reference/Rplot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/Rplot001.png -------------------------------------------------------------------------------- /docs/reference/Rplot002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/Rplot002.png -------------------------------------------------------------------------------- /docs/reference/Rplot003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/Rplot003.png -------------------------------------------------------------------------------- /docs/reference/Rplot004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/Rplot004.png -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(grid) 3 | library(gridtext) 4 | 5 | test_check("gridtext") 6 | -------------------------------------------------------------------------------- /docs/reference/richtext_grob-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/richtext_grob-1.png -------------------------------------------------------------------------------- /docs/reference/richtext_grob-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/richtext_grob-2.png -------------------------------------------------------------------------------- /docs/reference/textbox_grob-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/textbox_grob-1.png -------------------------------------------------------------------------------- /docs/reference/textbox_grob-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/textbox_grob-2.png -------------------------------------------------------------------------------- /docs/reference/textbox_grob-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/textbox_grob-3.png -------------------------------------------------------------------------------- /docs/pkgdown.yml: -------------------------------------------------------------------------------- 1 | pandoc: 2.10.1 2 | pkgdown: 1.6.0 3 | pkgdown_sha: ~ 4 | articles: [] 5 | last_built: 2020-12-01T16:52Z 6 | 7 | -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/man/figures/README-unnamed-chunk-4-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/man/figures/README-unnamed-chunk-5-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/man/figures/README-unnamed-chunk-6-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/man/figures/README-unnamed-chunk-7-1.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/man/figures/README-unnamed-chunk-8-1.png -------------------------------------------------------------------------------- /docs/reference/figures/README-unnamed-chunk-4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/figures/README-unnamed-chunk-4-1.png -------------------------------------------------------------------------------- /docs/reference/figures/README-unnamed-chunk-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/figures/README-unnamed-chunk-5-1.png -------------------------------------------------------------------------------- /docs/reference/figures/README-unnamed-chunk-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/figures/README-unnamed-chunk-6-1.png -------------------------------------------------------------------------------- /docs/reference/figures/README-unnamed-chunk-7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/figures/README-unnamed-chunk-7-1.png -------------------------------------------------------------------------------- /docs/reference/figures/README-unnamed-chunk-8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilkelab/gridtext/HEAD/docs/reference/figures/README-unnamed-chunk-8-1.png -------------------------------------------------------------------------------- /tests/testthat/helper-vdiffr.R: -------------------------------------------------------------------------------- 1 | expect_doppelganger <- function(title, fig, path = NULL, ...) { 2 | testthat::skip_if_not_installed("vdiffr") 3 | vdiffr::expect_doppelganger(title, fig, path = path, ...) 4 | } 5 | -------------------------------------------------------------------------------- /src/length.h: -------------------------------------------------------------------------------- 1 | #ifndef LENGTH_H 2 | #define LENGTH_H 3 | 4 | // for now, we just typedef Length as a double. 5 | // in the future, something more elaborate may be 6 | // needed, as in TeX 7 | typedef double Length; 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | destination: docs 2 | 3 | reference: 4 | - title: Formatted text labels and text boxes 5 | desc: The gridtext package provides two grobs that can draw formatted text, without and with word wrapping. 6 | contents: 7 | - richtext_grob 8 | - textbox_grob 9 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | * Maintainer changes from Claus Wilke to Brenton Wiernik. 2 | 3 | ## Test environments 4 | * ubuntu 20.04, devel and release 5 | * windows, release 6 | * macOS, release 7 | 8 | ## R CMD check results 9 | 10 | 0 errors | 0 warnings | 0 notes 11 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.travis\.yml$ 4 | ^\.codecov\.yml$ 5 | ^\.github$ 6 | ^_pkgdown\.yml$ 7 | ^README\.Rmd$ 8 | ^cran-comments\.md$ 9 | ^ISSUE_TEMPLATE\.md$ 10 | ^TODO\.Rmd$ 11 | ^LICENSE\.md$ 12 | ^docs$ 13 | ^revdep$ 14 | ^appveyor\.yml$ 15 | ^CRAN-RELEASE$ 16 | -------------------------------------------------------------------------------- /src/gridtext_types.h: -------------------------------------------------------------------------------- 1 | #ifndef GRIDTEXT_TYPES_H 2 | #define GRIDTEXT_TYPES_H 3 | 4 | /* By naming this file _types.h, we can make sure 5 | * it is automatically included in the autogenerated RcppExports.cpp. 6 | * 7 | * The file includes exactly those classes that are exported via 8 | * an XPtr. It is not used anywhere else in the C++ part of the code. 9 | */ 10 | 11 | #include "layout.h" 12 | #include "grid-renderer.h" 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /R/gridtext.R: -------------------------------------------------------------------------------- 1 | #' Improved text rendering support for grid graphics 2 | #' 3 | #' The gridtext package provides two new grobs, [`richtext_grob()`] and 4 | #' [`textbox_grob()`], which support drawing of formatted text labels and 5 | #' formatted text boxes, respectively. 6 | #' @name gridtext 7 | #' @docType package 8 | #' @useDynLib gridtext, .registration = TRUE 9 | #' @importFrom Rcpp sourceCpp 10 | #' @import grid 11 | #' @import rlang 12 | #' @importFrom xml2 read_html 13 | NULL 14 | -------------------------------------------------------------------------------- /man/gridtext.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/gridtext.R 3 | \docType{package} 4 | \name{gridtext} 5 | \alias{gridtext} 6 | \title{Improved text rendering support for grid graphics} 7 | \description{ 8 | The gridtext package provides two new grobs, \code{\link[=richtext_grob]{richtext_grob()}} and 9 | \code{\link[=textbox_grob]{textbox_grob()}}, which support drawing of formatted text labels and 10 | formatted text boxes, respectively. 11 | } 12 | -------------------------------------------------------------------------------- /tests/testthat/test-get_file.R: -------------------------------------------------------------------------------- 1 | test_that("get_file works", { 2 | # skip test on cran because the url could be broken in the future 3 | skip_on_cran() 4 | # get_file returns raw data if it's an url and a character path if it's 5 | # a local path. That's why we test it with the function read_image that calls it 6 | expect_identical( 7 | read_image("https://upload.wikimedia.org/wikipedia/commons/6/62/Biedronka.drs.png"), 8 | read_image("../figs/test_image.png") 9 | ) 10 | }) 11 | -------------------------------------------------------------------------------- /gridtext.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /R/grob-zero.R: -------------------------------------------------------------------------------- 1 | # An empty grob of no extent 2 | # 3 | # An empty grob of no extent. Useful when a grob is needed but no 4 | # content is desired. 5 | # @keywords internal 6 | # @export 7 | zeroGrob <- function() .zeroGrob 8 | 9 | .zeroGrob <- grid::grob(cl = "zeroGrob", name = "NULL") 10 | 11 | widthDetails.zeroGrob <- function(x) unit(0, "cm") 12 | heightDetails.zeroGrob <- function(x) unit(0, "cm") 13 | grobWidth.zeroGrob <- function(x) unit(0, "cm") 14 | grobHeight.zeroGrob <- function(x) unit(0, "cm") 15 | drawDetails.zeroGrob <- function(x, recording) {} 16 | -------------------------------------------------------------------------------- /R/recycle-gpar.R: -------------------------------------------------------------------------------- 1 | # takes a graphical parameters object gp and returns a list of 2 | # length n of appropriately recycled elements from gp 3 | recycle_gpar <- function(gp = NULL, n = 1) { 4 | make_gpar <- function(n, ...) { 5 | structure( 6 | list(...), 7 | class = "gpar" 8 | ) 9 | } 10 | 11 | args <- c(list(make_gpar, n = 1:n), gp, list(SIMPLIFY = FALSE)) 12 | do.call(mapply, args) 13 | } 14 | 15 | # converts a unit vector into a list of individual unit objects 16 | unit_to_list <- function(u) 17 | { 18 | lapply(seq_along(u), function(i) u[i]) 19 | } 20 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(ascentDetails,richtext_grob) 4 | S3method(ascentDetails,textbox_grob) 5 | S3method(descentDetails,richtext_grob) 6 | S3method(descentDetails,textbox_grob) 7 | S3method(heightDetails,richtext_grob) 8 | S3method(heightDetails,textbox_grob) 9 | S3method(makeContent,textbox_grob) 10 | S3method(makeContext,textbox_grob) 11 | S3method(widthDetails,richtext_grob) 12 | S3method(widthDetails,textbox_grob) 13 | export(richtext_grob) 14 | export(textbox_grob) 15 | import(grid) 16 | import(rlang) 17 | importFrom(Rcpp,sourceCpp) 18 | importFrom(xml2,read_html) 19 | useDynLib(gridtext, .registration = TRUE) 20 | -------------------------------------------------------------------------------- /R/read-image.R: -------------------------------------------------------------------------------- 1 | read_image <- function(path) { 2 | if (isTRUE(grepl("\\.png$", path, ignore.case = TRUE))) { 3 | img <- png::readPNG(get_file(path), native = TRUE) 4 | } else if (isTRUE(grepl("(\\.jpg$)|(\\.jpeg)", path, ignore.case = TRUE))) { 5 | img <- jpeg::readJPEG(get_file(path), native = TRUE) 6 | } else { 7 | warning(paste0("Image type not supported: ", path), call. = FALSE) 8 | img <- grDevices::as.raster(matrix(0, 10, 10)) 9 | } 10 | } 11 | 12 | get_file <- function(path) { 13 | if (is_url(path)) { 14 | curl::curl_fetch_memory(path)$content 15 | } else { 16 | path 17 | } 18 | } 19 | 20 | is_url <- function(path) 21 | { 22 | grepl("https?://", path) 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # History files 2 | .Rhistory 3 | .Rapp.history 4 | 5 | # Session Data files 6 | .RData 7 | 8 | # Example code in package build process 9 | *-Ex.R 10 | 11 | # Output files from R CMD build 12 | /*.tar.gz 13 | 14 | # Output files from R CMD check 15 | /*.Rcheck/ 16 | 17 | # RStudio files 18 | .Rproj.user/ 19 | 20 | # produced vignettes 21 | vignettes/*.html 22 | vignettes/*.pdf 23 | 24 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 25 | .httr-oauth 26 | 27 | # knitr and R markdown default cache directories 28 | /*_cache/ 29 | /cache/ 30 | 31 | # Temporary files created by R markdown 32 | *.utf8.md 33 | *.knit.md 34 | 35 | # Shiny token, see https://shiny.rstudio.com/articles/shinyapps.html 36 | rsconnect/ 37 | .Rproj.user 38 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issues are meant to report bugs or request features. If you have questions about how to correctly use this package, please don't use the issue system. Such questions can be asked on stackoverflow https://stackoverflow.com/ or the RStudio community https://community.rstudio.com/. If you are not sure where to go, please try https://stackoverflow.com/ first. 2 | 3 | Issues must contain reproducible code examples. Please use the reprex package to create your example (see here: http://reprex.tidyverse.org/). Issues without reprex may be closed without comment. 4 | 5 | Please delete these instructions after you have read them. 6 | 7 | ------ 8 | 9 | Brief description of the problem or desired feature. 10 | 11 | ```{r} 12 | # insert reprex here 13 | ``` -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # gridtext 0.1.5 2 | 3 | - Transition to curl package and drop RCurl dependency 4 | - Fix fontface not being processed and words spaced properly in R 4.2.0 5 | - Maintainer changes to Brenton Wiernik 6 | - Removed LazyData from package DESCRIPTION to fix CRAN NOTE 7 | 8 | # gridtext 0.1.4 9 | 10 | - Make sure tests don't fail if vdiffr is missing. 11 | 12 | # gridtext 0.1.3 13 | 14 | - Remove unneeded systemfonts dependency. 15 | 16 | # gridtext 0.1.2 17 | 18 | - Fix build for testthat 3.0. 19 | 20 | # gridtext 0.1.1 21 | 22 | - `richtext_grob()` and `textbox_grob()` now gracefully handle empty strings 23 | and NAs. 24 | 25 | # gridtext 0.1.0 26 | 27 | First public release. Provides the two grobs `richtext_grob()` and `textbox_grob()` for formatted text rendering without and with word wrapping, respectively. 28 | -------------------------------------------------------------------------------- /docs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /src/null-box.h: -------------------------------------------------------------------------------- 1 | #ifndef NULL_BOX_H 2 | #define NULL_BOX_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include "layout.h" 8 | 9 | /* The NullBox draws nothing. If given a width or a height, 10 | * it can be used as a spacer. The reference point of 11 | * the NullBox is the lower left corner. 12 | */ 13 | 14 | template 15 | class NullBox : public Box { 16 | private: 17 | Length m_width; 18 | Length m_height; 19 | 20 | public: 21 | NullBox(Length width = 0, Length height = 0) : 22 | m_width(width), m_height(height) {} 23 | ~NullBox() {} 24 | 25 | Length width() { return m_width; } 26 | Length ascent() { return m_height; } 27 | Length descent() { return 0; } 28 | Length voff() { return 0; } 29 | 30 | // nothing to be done 31 | void calc_layout(Length, Length) {} 32 | 33 | // nothing to be done 34 | void place(Length, Length) {} 35 | 36 | // nothing to be done 37 | void render(Renderer &, Length, Length) {} 38 | }; 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /tests/figs/grid-renderer/text-in-different-stylings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | blue 16 | red bold 17 | roman 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Claus O. Wilke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/testthat/test-null-box.R: -------------------------------------------------------------------------------- 1 | test_that("basic features", { 2 | # null box with no extent 3 | nb <- bl_make_null_box() 4 | expect_identical(bl_box_width(nb), 0) 5 | expect_identical(bl_box_height(nb), 0) 6 | expect_identical(bl_box_ascent(nb), 0) 7 | expect_identical(bl_box_descent(nb), 0) 8 | expect_identical(bl_box_voff(nb), 0) 9 | 10 | g <- bl_render(nb, 100, 200) 11 | expect_identical(length(g), 0L) 12 | 13 | # null box with defined extent 14 | nb <- bl_make_null_box(100, 200) 15 | expect_identical(bl_box_width(nb), 100) 16 | expect_identical(bl_box_height(nb), 200) 17 | expect_identical(bl_box_ascent(nb), 200) 18 | expect_identical(bl_box_descent(nb), 0) 19 | expect_identical(bl_box_voff(nb), 0) 20 | 21 | g <- bl_render(nb, 100, 200) 22 | expect_identical(length(g), 0L) 23 | 24 | # null box transmits its extent to enclosing rect box 25 | rb <- bl_make_rect_box( 26 | nb, 0, 0, margin = rep(0, 4), padding = rep(0, 4), 27 | gp = gpar(), width_policy = "native", height_policy = "native" 28 | ) 29 | bl_calc_layout(rb, 0, 0) 30 | g <- bl_render(rb, 100, 200) 31 | outer <- g[[1]] 32 | expect_identical(outer$x, unit(100, "pt")) 33 | expect_identical(outer$y, unit(200, "pt")) 34 | expect_identical(outer$width, unit(100, "pt")) 35 | expect_identical(outer$height, unit(200, "pt")) 36 | }) 37 | -------------------------------------------------------------------------------- /src/grid-renderer.cpp: -------------------------------------------------------------------------------- 1 | /* R bindings to grid renderer, for unit testing */ 2 | 3 | #include "grid-renderer.h" 4 | 5 | // [[Rcpp::export]] 6 | XPtr grid_renderer() { 7 | XPtr gr(new GridRenderer()); 8 | 9 | return gr; 10 | } 11 | 12 | // [[Rcpp::export]] 13 | void grid_renderer_text(XPtr gr, const CharacterVector &label, Length x, Length y, List gp) { 14 | return gr->text(label, x, y, gp); 15 | } 16 | 17 | // [[Rcpp::export]] 18 | List grid_renderer_text_details(const CharacterVector &label, List gp) { 19 | TextDetails td = GridRenderer::text_details(label, gp); 20 | 21 | List out = List::create( 22 | _["width_pt"] = td.width, _["ascent_pt"] = td.ascent, 23 | _["descent_pt"] = td.descent, _["space_pt"] = td.space 24 | ); 25 | 26 | return out; 27 | } 28 | 29 | // [[Rcpp::export]] 30 | void grid_renderer_raster(XPtr gr, RObject image, Length x, Length y, Length width, Length height, bool interpolate = true) { 31 | return gr->raster(image, x, y, width, height, interpolate); 32 | } 33 | 34 | // [[Rcpp::export]] 35 | void grid_renderer_rect(XPtr gr, Length x, Length y, Length width, Length height, List gp, Length r = 0) { 36 | return gr->rect(x, y, width, height, gp, r); 37 | } 38 | 39 | // [[Rcpp::export]] 40 | List grid_renderer_collect_grobs(XPtr gr) { 41 | return gr->collect_grobs(); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: gridtext 2 | Type: Package 3 | Title: Improved Text Rendering Support for 'Grid' Graphics 4 | Version: 0.1.5 5 | Authors@R: 6 | c( 7 | person( 8 | given = "Claus O.", 9 | family = "Wilke", 10 | role = c("aut"), 11 | email = "wilke@austin.utexas.edu", 12 | comment = c(ORCID = "0000-0002-7470-9261") 13 | ), 14 | person( 15 | given = "Brenton M.", 16 | family = "Wiernik", 17 | role = c("aut", "cre"), 18 | email = "brenton@wiernik.org", 19 | comment = c(ORCID = "0000-0001-9560-6336", Twitter = "@bmwiernik") 20 | ) 21 | ) 22 | Description: Provides support for rendering of formatted text using 'grid' graphics. Text can be 23 | formatted via a minimal subset of 'Markdown', 'HTML', and inline 'CSS' directives, and it can be 24 | rendered both with and without word wrap. 25 | URL: https://wilkelab.org/gridtext/ 26 | BugReports: https://github.com/wilkelab/gridtext/issues 27 | License: MIT + file LICENSE 28 | Depends: 29 | R (>= 3.5) 30 | Imports: 31 | curl, 32 | grid, 33 | grDevices, 34 | markdown, 35 | rlang, 36 | Rcpp, 37 | png, 38 | jpeg, 39 | stringr, 40 | xml2 41 | Suggests: 42 | covr, 43 | knitr, 44 | rmarkdown, 45 | testthat, 46 | vdiffr 47 | LinkingTo: 48 | Rcpp 49 | Encoding: UTF-8 50 | RoxygenNote: 7.1.1 51 | Roxygen: list(markdown = TRUE) 52 | SystemRequirements: C++11 53 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - master 6 | pull_request: 7 | branches: 8 | - main 9 | - master 10 | 11 | name: test-coverage 12 | 13 | jobs: 14 | test-coverage: 15 | runs-on: macOS-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: r-lib/actions/setup-r@v1 21 | 22 | - uses: r-lib/actions/setup-pandoc@v1 23 | 24 | - name: Query dependencies 25 | run: | 26 | install.packages('remotes') 27 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 28 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 29 | shell: Rscript {0} 30 | 31 | - name: Cache R packages 32 | uses: actions/cache@v2 33 | with: 34 | path: ${{ env.R_LIBS_USER }} 35 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 36 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 37 | 38 | - name: Install dependencies 39 | run: | 40 | install.packages(c("remotes")) 41 | remotes::install_deps(dependencies = TRUE) 42 | remotes::install_cran("covr") 43 | shell: Rscript {0} 44 | 45 | - name: Test coverage 46 | run: covr::codecov() 47 | shell: Rscript {0} 48 | 49 | -------------------------------------------------------------------------------- /src/penalty.h: -------------------------------------------------------------------------------- 1 | #ifndef PENALTY_H 2 | #define PENALTY_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include "layout.h" 8 | 9 | template class Penalty : public BoxNode { 10 | private: 11 | int m_penalty; 12 | Length m_width; 13 | bool m_flagged; 14 | 15 | public: 16 | static constexpr int infinity = 10000; // maximum penalty 17 | 18 | Penalty(int penalty = 0, Length width = 0, bool flagged = false) : 19 | m_penalty(penalty), m_width(width), m_flagged(flagged) {} 20 | virtual ~Penalty() {} 21 | NodeType type() {return NodeType::penalty;} 22 | 23 | Length width() {return m_width;} 24 | Length ascent() {return 0;} 25 | Length descent() {return 0;} 26 | Length voff() {return 0;} 27 | 28 | void calc_layout(Length, Length) {} 29 | void place(Length, Length) {} 30 | void render(Renderer &, Length, Length) {} 31 | 32 | /* The remaining functions are not virtual, 33 | * don't override. 34 | */ 35 | 36 | int penalty() {return m_penalty;} 37 | bool flagged() {return m_flagged;} 38 | }; 39 | 40 | // Penalty that causes a forced break 41 | template class ForcedBreakPenalty : public Penalty { 42 | public: 43 | ForcedBreakPenalty() : Penalty(-1*Penalty::infinity) {} 44 | }; 45 | 46 | // Penalty that prevents a break at this position 47 | template class NeverBreakPenalty : public Penalty { 48 | public: 49 | NeverBreakPenalty() : Penalty(Penalty::infinity) {} 50 | }; 51 | 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /tests/testthat/test-text-details.R: -------------------------------------------------------------------------------- 1 | test_that("text_details() calculates info correctly", { 2 | # descent and space are independent of string 3 | gp1 <- gpar(fontfamily = "Helvetica", fontface = "plain", fontsize = 10) 4 | t1 <- text_details("abcd", gp = gp1) 5 | t2 <- text_details("gjqp", gp = gp1) 6 | expect_equal(t1$descent_pt, t2$descent_pt) 7 | expect_equal(t1$space_pt, t2$space_pt) 8 | 9 | # recalculating the same details gives same results (tests caching) 10 | t2 <- text_details("abcd", gp = gp1) 11 | expect_equal(t1$width_pt, t2$width_pt) 12 | expect_equal(t1$ascent_pt, t2$ascent_pt) 13 | expect_equal(t1$descent_pt, t2$descent_pt) 14 | expect_equal(t1$space_pt, t2$space_pt) 15 | 16 | # all parameters scale with font size 17 | gp2 <- gpar(fontfamily = "Helvetica", fontface = "plain", fontsize = 20) 18 | t2 <- text_details("abcd", gp = gp2) 19 | expect_equal(2 * t1$width_pt, t2$width_pt) 20 | expect_equal(2 * t1$ascent_pt, t2$ascent_pt) 21 | expect_equal(2 * t1$descent_pt, t2$descent_pt) 22 | expect_equal(2 * t1$space_pt, t2$space_pt) 23 | 24 | # parameters change with font 25 | gp2 <- gpar(fontfamily = "Times", fontface = "plain", fontsize = 10) 26 | t2 <- text_details("abcd", gp = gp2) 27 | expect_false(t1$width_pt == t2$width_pt) 28 | expect_false(t1$ascent_pt == t2$ascent_pt) 29 | expect_false(t1$descent_pt == t2$descent_pt) 30 | expect_false(t1$space_pt == t2$space_pt) 31 | 32 | # font details are identical to what we would get from an actual grob 33 | g <- textGrob("Qbcd", gp = gp1) 34 | t1 <- text_details("Qbcd", gp = gp1) 35 | expect_equal(t1$ascent_pt, convertHeight(grobHeight(g), "pt", valueOnly = TRUE)) 36 | expect_equal(t1$width_pt, convertWidth(grobWidth(g), "pt", valueOnly = TRUE)) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/figs/grid-renderer/mixing-text-and-boxes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | text 1, square box blue 18 | text 2, rounded box filled 19 | 20 | -------------------------------------------------------------------------------- /src/text-box.h: -------------------------------------------------------------------------------- 1 | #ifndef TEXT_BOX_H 2 | #define TEXT_BOX_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include "layout.h" 8 | 9 | // A box holding a single text label 10 | template 11 | class TextBox : public Box { 12 | private: 13 | CharacterVector m_label; 14 | typename Renderer::GraphicsContext m_gp; 15 | Length m_width; 16 | Length m_ascent; 17 | Length m_descent; 18 | Length m_voff; 19 | // position of the box in enclosing box, modulo vertical offset (voff), 20 | // which gets added to m_y; 21 | // the box reference point is the leftmost point of the baseline. 22 | Length m_x, m_y; 23 | 24 | public: 25 | TextBox(const CharacterVector &label, const typename Renderer::GraphicsContext &gp, Length voff = 0) : 26 | m_label(label), m_gp(gp), m_width(0), m_ascent(0), m_descent(0), m_voff(voff), 27 | m_x(0), m_y(0) {} 28 | ~TextBox() {} 29 | 30 | Length width() { return m_width; } 31 | Length ascent() { return m_ascent; } 32 | Length descent() { return m_descent; } 33 | Length voff() { return m_voff; } 34 | 35 | // width and height are only defined once `calc_layout()` has been called 36 | void calc_layout(Length, Length) { 37 | TextDetails td = Renderer::text_details(m_label, m_gp); 38 | m_width = td.width; 39 | m_ascent = td.ascent; 40 | m_descent = td.descent; 41 | } 42 | 43 | // place box in internal coordinates used in enclosing box 44 | void place(Length x, Length y) { 45 | m_x = x; 46 | m_y = y; 47 | } 48 | 49 | // render into absolute coordinates, using the reference coordinates 50 | // from the enclosing box 51 | void render(Renderer &r, Length xref, Length yref) { 52 | Length x = m_x + xref; 53 | Length y = m_y + m_voff + yref; 54 | 55 | r.text(m_label, x, y, m_gp); 56 | } 57 | }; 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | 6 | /* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */ 7 | 8 | /* All levels of nav */ 9 | nav[data-toggle='toc'] .nav > li > a { 10 | display: block; 11 | padding: 4px 20px; 12 | font-size: 13px; 13 | font-weight: 500; 14 | color: #767676; 15 | } 16 | nav[data-toggle='toc'] .nav > li > a:hover, 17 | nav[data-toggle='toc'] .nav > li > a:focus { 18 | padding-left: 19px; 19 | color: #563d7c; 20 | text-decoration: none; 21 | background-color: transparent; 22 | border-left: 1px solid #563d7c; 23 | } 24 | nav[data-toggle='toc'] .nav > .active > a, 25 | nav[data-toggle='toc'] .nav > .active:hover > a, 26 | nav[data-toggle='toc'] .nav > .active:focus > a { 27 | padding-left: 18px; 28 | font-weight: bold; 29 | color: #563d7c; 30 | background-color: transparent; 31 | border-left: 2px solid #563d7c; 32 | } 33 | 34 | /* Nav: second level (shown on .active) */ 35 | nav[data-toggle='toc'] .nav .nav { 36 | display: none; /* Hide by default, but at >768px, show it */ 37 | padding-bottom: 10px; 38 | } 39 | nav[data-toggle='toc'] .nav .nav > li > a { 40 | padding-top: 1px; 41 | padding-bottom: 1px; 42 | padding-left: 30px; 43 | font-size: 12px; 44 | font-weight: normal; 45 | } 46 | nav[data-toggle='toc'] .nav .nav > li > a:hover, 47 | nav[data-toggle='toc'] .nav .nav > li > a:focus { 48 | padding-left: 29px; 49 | } 50 | nav[data-toggle='toc'] .nav .nav > .active > a, 51 | nav[data-toggle='toc'] .nav .nav > .active:hover > a, 52 | nav[data-toggle='toc'] .nav .nav > .active:focus > a { 53 | padding-left: 28px; 54 | font-weight: 500; 55 | } 56 | 57 | /* from https://github.com/twbs/bootstrap/blob/e38f066d8c203c3e032da0ff23cd2d6098ee2dd6/docs/assets/css/src/docs.css#L631-L634 */ 58 | nav[data-toggle='toc'] .nav > .active > ul { 59 | display: block; 60 | } 61 | -------------------------------------------------------------------------------- /docs/docsearch.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | // register a handler to move the focus to the search bar 4 | // upon pressing shift + "/" (i.e. "?") 5 | $(document).on('keydown', function(e) { 6 | if (e.shiftKey && e.keyCode == 191) { 7 | e.preventDefault(); 8 | $("#search-input").focus(); 9 | } 10 | }); 11 | 12 | $(document).ready(function() { 13 | // do keyword highlighting 14 | /* modified from https://jsfiddle.net/julmot/bL6bb5oo/ */ 15 | var mark = function() { 16 | 17 | var referrer = document.URL ; 18 | var paramKey = "q" ; 19 | 20 | if (referrer.indexOf("?") !== -1) { 21 | var qs = referrer.substr(referrer.indexOf('?') + 1); 22 | var qs_noanchor = qs.split('#')[0]; 23 | var qsa = qs_noanchor.split('&'); 24 | var keyword = ""; 25 | 26 | for (var i = 0; i < qsa.length; i++) { 27 | var currentParam = qsa[i].split('='); 28 | 29 | if (currentParam.length !== 2) { 30 | continue; 31 | } 32 | 33 | if (currentParam[0] == paramKey) { 34 | keyword = decodeURIComponent(currentParam[1].replace(/\+/g, "%20")); 35 | } 36 | } 37 | 38 | if (keyword !== "") { 39 | $(".contents").unmark({ 40 | done: function() { 41 | $(".contents").mark(keyword); 42 | } 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | mark(); 49 | }); 50 | }); 51 | 52 | /* Search term highlighting ------------------------------*/ 53 | 54 | function matchedWords(hit) { 55 | var words = []; 56 | 57 | var hierarchy = hit._highlightResult.hierarchy; 58 | // loop to fetch from lvl0, lvl1, etc. 59 | for (var idx in hierarchy) { 60 | words = words.concat(hierarchy[idx].matchedWords); 61 | } 62 | 63 | var content = hit._highlightResult.content; 64 | if (content) { 65 | words = words.concat(content.matchedWords); 66 | } 67 | 68 | // return unique words 69 | var words_uniq = [...new Set(words)]; 70 | return words_uniq; 71 | } 72 | 73 | function updateHitURL(hit) { 74 | 75 | var words = matchedWords(hit); 76 | var url = ""; 77 | 78 | if (hit.anchor) { 79 | url = hit.url_without_anchor + '?q=' + escape(words.join(" ")) + '#' + hit.anchor; 80 | } else { 81 | url = hit.url + '?q=' + escape(words.join(" ")); 82 | } 83 | 84 | return url; 85 | } 86 | -------------------------------------------------------------------------------- /R/parse-css.R: -------------------------------------------------------------------------------- 1 | # Parse css 2 | # 3 | # A very simple css parser that can parse `key:value;` pairs. 4 | # 5 | # @param text The css text to parse 6 | parse_css <- function(text) { 7 | # break into separate lines; for now, ignore the possibility of 8 | # quoted or escaped semicolon 9 | lines <- strsplit(text, ";", fixed = TRUE)[[1]] 10 | 11 | # parse each line and return list of key--value pairs 12 | unlist(lapply(lines, parse_css_line), recursive = FALSE) 13 | } 14 | 15 | parse_css_line <- function(line) { 16 | pattern <- "\\s*(\\S+)\\s*:\\s*(\"(.*)\"|'(.*)'|(\\S*))\\s*" 17 | m <- attributes(regexpr(pattern, line, perl = TRUE)) 18 | if (m$capture.start[1] > 0) { 19 | key <- substr(line, m$capture.start[1], m$capture.start[1] + m$capture.length[1] - 1) 20 | } else key <- NULL 21 | 22 | if (m$capture.start[3] > 0) { 23 | value <- substr(line, m$capture.start[3], m$capture.start[3] + m$capture.length[3] - 1) 24 | } else if (m$capture.start[4] > 0) { 25 | value <- substr(line, m$capture.start[4], m$capture.start[4] + m$capture.length[4] - 1) 26 | } else if (m$capture.start[5] > 0) { 27 | value <- substr(line, m$capture.start[5], m$capture.start[5] + m$capture.length[5] - 1) 28 | } else value <- NULL 29 | 30 | if (is.null(key)) list() 31 | else list2(!!key := value) 32 | } 33 | 34 | parse_css_unit <- function(x) { 35 | pattern <- "^((-?\\d+\\.?\\d*)(%|[a-zA-Z]+)|(0))$" 36 | m <- attributes(regexpr(pattern, x, perl = TRUE)) 37 | if (m$capture.start[4] > 0) { 38 | # matched null value 39 | return(list(value = 0, unit = "pt")) 40 | } else { 41 | if (m$capture.start[2] > 0) { 42 | value <- as.numeric( 43 | substr(x, m$capture.start[2], m$capture.start[2] + m$capture.length[2] - 1) 44 | ) 45 | if (m$capture.start[3] > 0) { 46 | unit <- substr(x, m$capture.start[3], m$capture.start[3] + m$capture.length[3] - 1) 47 | return(list(value = value, unit = unit)) 48 | } 49 | } 50 | } 51 | stop(paste0("The string '", x, "' does not represent a valid CSS unit."), call. = FALSE) 52 | } 53 | 54 | convert_css_unit_pt <- function(x) { 55 | u <- parse_css_unit(x) 56 | switch( 57 | u$unit, 58 | pt = u$value, 59 | px = (72/96)*u$value, 60 | `in` = 72*u$value, 61 | cm = (72/2.54)*u$value, 62 | mm = (72/25.4)*u$value, 63 | stop(paste0("Cannot convert ", u$value, u$unit, " to pt."), call. = FALSE) 64 | ) 65 | } 66 | 67 | -------------------------------------------------------------------------------- /R/grid-utils.R: -------------------------------------------------------------------------------- 1 | # various functions to help with grid functionality 2 | 3 | # from: https://github.com/thomasp85/ggforce/blob/cba71550606d18b4f4b245cb097aee5eeeec52a8/R/textbox.R#L290-L295 4 | # code taken with permission (https://twitter.com/thomasp85/status/1160989815657119747) 5 | with_unit <- function(x, default) { 6 | if (!is.null(x) && !is.unit(x)) { 7 | x <- unit(x, default) 8 | } 9 | x 10 | } 11 | 12 | # calculate the current width of a grob, in pt 13 | # @flip are width and height flipped? 14 | # @convert_null should null values be converted or kept as they are? 15 | current_width_pt <- function(grob = NULL, width = NULL, flip = FALSE, convert_null = TRUE) { 16 | if (is.null(width)) { 17 | if (isTRUE(convert_null)) { 18 | width <- unit(1, 'npc') 19 | } else { 20 | return(NULL) 21 | } 22 | } 23 | 24 | if (isTRUE(flip)) { 25 | convert <- convertHeight 26 | } else { 27 | convert <- convertWidth 28 | } 29 | 30 | if (is.null(grob$vp)) { 31 | width_pt <- convert(width, 'pt', TRUE) 32 | } else { 33 | # If the grob has its own viewport then we need to push it and 34 | # afterwards pop it. For this to work in the general case 35 | # (stacked viewports, etc), we need to keep track of the depth 36 | # of the current viewport stack and pop appropriately. 37 | n <- current.vpPath()$n %||% 0 38 | pushViewport(grob$vp) 39 | width_pt <- convert(width, 'pt', TRUE) 40 | popViewport(current.vpPath()$n - n) 41 | } 42 | 43 | width_pt 44 | } 45 | 46 | # calculate the current height of a grob, in pt 47 | current_height_pt <- function(grob = NULL, height = NULL, flip = FALSE, convert_null = TRUE) { 48 | if (is.null(height)) { 49 | if (isTRUE(convert_null)) { 50 | height <- unit(1, 'npc') 51 | } else { 52 | return(NULL) 53 | } 54 | } 55 | 56 | if (isTRUE(flip)) { 57 | convert <- convertWidth 58 | } else { 59 | convert <- convertHeight 60 | } 61 | 62 | if (is.null(grob$vp)) { 63 | height_pt <- convert(height, 'pt', TRUE) 64 | } else { 65 | # If the grob has its own viewport then we need to push it and 66 | # afterwards pop it. For this to work in the general case 67 | # (stacked viewports, etc), we need to keep track of the depth 68 | # of the current viewport stack and pop appropriately. 69 | n <- current.vpPath()$n %||% 0 70 | pushViewport(grob$vp) 71 | height_pt <- convert(height, 'pt', TRUE) 72 | popViewport(current.vpPath()$n - n) 73 | } 74 | 75 | height_pt 76 | } 77 | -------------------------------------------------------------------------------- /src/grid.h: -------------------------------------------------------------------------------- 1 | #ifndef GRID_H 2 | #define GRID_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include 8 | using namespace std; 9 | 10 | #include "length.h" 11 | 12 | // This file defines a number of convenience functions that allow for the rapid construction 13 | // or manipulation of grid units or grobs. Each could be replaced by a simple R call to a 14 | // corresponding grid function (e.g., unit_pt(x) is equivalent to unit(x, "pt")), but in general 15 | // the C++ version here is much faster, in particular because it skips extensive input validation. 16 | 17 | // replacement for unit(x, "pt") 18 | // [[Rcpp::export]] 19 | NumericVector unit_pt(NumericVector x); 20 | 21 | // Overloaded version for Length 22 | NumericVector unit_pt(Length x); 23 | 24 | // replacement for gpar() with no arguments 25 | // [[Rcpp::export]] 26 | List gpar_empty(); 27 | 28 | // replacement for textGrop(label, x_pt, y_pt, gp = gpar(), hjust = 0, vjust = 0, default.units = "pt", name = NULL) 29 | // [[Rcpp::export]] 30 | List text_grob(CharacterVector label, NumericVector x_pt = 0, NumericVector y_pt = 0, 31 | RObject gp = R_NilValue, RObject name = R_NilValue); 32 | 33 | // replacement for rasterGrop(image, x_pt, y_pt, width_pt, height_pt, gp = gpar(), hjust = 0, vjust = 0, default.units = "pt", interpolate = TRUE, name = NULL) 34 | // [[Rcpp::export]] 35 | List raster_grob(RObject image, NumericVector x_pt = 0, NumericVector y_pt = 0, NumericVector width_pt = 0, NumericVector height_pt = 0, 36 | LogicalVector interpolate = true, RObject gp = R_NilValue, RObject name = R_NilValue); 37 | 38 | // replacement for rectGrop(x_pt, y_pt, width_pt, height_pt, gp = gpar(), hjust = 0, vjust = 0, default.units = "pt", name = NULL) 39 | // [[Rcpp::export]] 40 | List rect_grob(NumericVector x_pt = 0, NumericVector y_pt = 0, NumericVector width_pt = 0, NumericVector height_pt = 0, 41 | RObject gp = R_NilValue, RObject name = R_NilValue); 42 | 43 | // replacement for roundrectGrop(x_pt, y_pt, width_pt, height_pt, r = unit(r_pt, "pt), gp = gpar(), just = c(0, 0), default.units = "pt", name = NULL) 44 | // [[Rcpp::export]] 45 | List roundrect_grob(NumericVector x_pt = 0, NumericVector y_pt = 0, NumericVector width_pt = 0, NumericVector height_pt = 0, 46 | NumericVector r_pt = 5, RObject gp = R_NilValue, RObject name = R_NilValue); 47 | 48 | // replacement for editGrob(grob, x = x, y = y) 49 | // [[Rcpp::export]] 50 | RObject set_grob_coords(RObject grob, NumericVector x, NumericVector y); 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/glue.h: -------------------------------------------------------------------------------- 1 | #ifndef GLUE_H 2 | #define GLUE_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include "layout.h" 8 | 9 | template class Glue : public BoxNode { 10 | protected: 11 | Length m_width, m_stretch, m_shrink; 12 | double m_r; // adjustment ratio 13 | 14 | public: 15 | static constexpr double infinity = 1e9; // maximum adjustment ratio 16 | 17 | Glue(Length width = 0, Length stretch = 0, Length shrink = 0) : 18 | m_width(width), m_stretch(stretch), m_shrink(shrink), m_r(0) {} 19 | virtual ~Glue() {} 20 | NodeType type() {return NodeType::glue;} 21 | 22 | Length width() {return compute_width(m_r);} 23 | Length ascent() {return 0;} 24 | Length descent() {return 0;} 25 | Length voff() {return 0;} 26 | 27 | void calc_layout(Length, Length) {} 28 | void place(Length, Length) {} 29 | void render(Renderer &, Length, Length) {} 30 | 31 | /* The remaining functions are not virtual, 32 | * don't override. 33 | */ 34 | 35 | Length default_width() {return m_width;} 36 | Length stretch() {return m_stretch;} 37 | Length shrink() {return m_shrink;} 38 | 39 | void set_r(double r) {m_r = r;} 40 | 41 | // calculate the width of the glue for given adjustment ratio 42 | Length compute_width(double r) { 43 | if (r < 0) { 44 | return m_width + r*m_shrink; 45 | } else { 46 | return m_width + r*m_stretch; 47 | } 48 | } 49 | }; 50 | 51 | 52 | // Glue corresponding to a regular space in text 53 | template 54 | class RegularSpaceGlue : public Glue { 55 | private: 56 | typename Renderer::GraphicsContext m_gp; 57 | double m_stretch_ratio, m_shrink_ratio; // used to convert width of space character into stretch and shrink 58 | 59 | // pull protected members from superclass explicitly into scope 60 | using Glue::m_width; 61 | using Glue::m_stretch; 62 | using Glue::m_shrink; 63 | 64 | public: 65 | RegularSpaceGlue(const typename Renderer::GraphicsContext &gp, 66 | double stretch_ratio = 0.5, double shrink_ratio = 0.333333) : 67 | m_gp(gp), m_stretch_ratio(stretch_ratio), m_shrink_ratio(shrink_ratio) {} 68 | ~RegularSpaceGlue() {} 69 | 70 | // width, stretch, and shrink are only defined once `calc_layout()` has been called 71 | void calc_layout(Length, Length) { 72 | TextDetails td = Renderer::text_details(" ", m_gp); 73 | m_width = td.space; 74 | m_stretch = m_width * m_stretch_ratio; 75 | m_shrink = m_width * m_shrink_ratio; 76 | } 77 | }; 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - master 6 | pull_request: 7 | branches: 8 | - main 9 | - master 10 | 11 | name: R-CMD-check 12 | 13 | jobs: 14 | R-CMD-check: 15 | runs-on: ${{ matrix.config.os }} 16 | 17 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | config: 23 | - {os: windows-latest, r: 'release', vdiffr: true} 24 | - {os: macOS-latest, r: 'release', vdiffr: true} 25 | - {os: ubuntu-20.04, r: 'release', vdiffr: true, rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 26 | - {os: ubuntu-20.04, r: 'devel', vdiffr: false, rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 27 | 28 | env: 29 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 30 | RSPM: ${{ matrix.config.rspm }} 31 | VDIFFR_RUN_TESTS: ${{ matrix.config.vdiffr }} 32 | VDIFFR_LOG_PATH: "../vdiffr.Rout.fail" 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - uses: r-lib/actions/setup-r@v1 38 | with: 39 | r-version: ${{ matrix.config.r }} 40 | 41 | - uses: r-lib/actions/setup-pandoc@v1 42 | 43 | - name: Query dependencies 44 | run: | 45 | install.packages('remotes') 46 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 47 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 48 | shell: Rscript {0} 49 | 50 | - name: Cache R packages 51 | if: runner.os != 'Windows' 52 | uses: actions/cache@v2 53 | with: 54 | path: ${{ env.R_LIBS_USER }} 55 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 56 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 57 | 58 | - name: Install system dependencies 59 | if: runner.os == 'Linux' 60 | run: | 61 | while read -r cmd 62 | do 63 | eval sudo $cmd 64 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 65 | 66 | - name: Install dependencies 67 | run: | 68 | remotes::install_deps(dependencies = TRUE) 69 | remotes::install_cran("rcmdcheck") 70 | shell: Rscript {0} 71 | 72 | - name: Check 73 | env: 74 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 75 | run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 76 | shell: Rscript {0} 77 | 78 | - name: Upload check results 79 | if: failure() 80 | uses: actions/upload-artifact@main 81 | with: 82 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 83 | path: check 84 | -------------------------------------------------------------------------------- /R/drawing-context.R: -------------------------------------------------------------------------------- 1 | # create drawing context with defined state 2 | # halign defines horizontal text alignment (0 = left aligned, 0.5 = centered, 1 = right aligned) 3 | setup_context <- function(fontsize = 12, fontfamily = "", fontface = "plain", color = "black", 4 | lineheight = 1.2, halign = 0, word_wrap = TRUE, gp = NULL) { 5 | if (is.null(gp)) { 6 | gp <- gpar( 7 | fontsize = fontsize, fontfamily = fontfamily, fontface = fontface, 8 | col = color, cex = 1, lineheight = lineheight 9 | ) 10 | } 11 | gp <- update_gpar(get.gpar(), gp) 12 | 13 | set_context_gp(list(yoff_pt = 0, halign = halign, word_wrap = word_wrap), gp) 14 | } 15 | 16 | # update a given drawing context with the values provided via ... 17 | update_context <- function(drawing_context, ...) { 18 | dc_new <- list(...) 19 | names_new <- names(dc_new) 20 | names_old <- names(drawing_context) 21 | drawing_context[intersect(names_old, names_new)] <- NULL 22 | c(drawing_context, dc_new) 23 | } 24 | 25 | set_style <- function(drawing_context, style = NULL) { 26 | if (is.null(style)) return(drawing_context) 27 | 28 | css <- parse_css(style) 29 | 30 | if (!is.null(css$`font-size`)) { 31 | font_size = convert_css_unit_pt(css$`font-size`) 32 | } else { 33 | font_size = NULL 34 | } 35 | 36 | drawing_context <- set_context_gp( 37 | drawing_context, 38 | gpar(col = css$color, fontfamily = css$`font-family`, fontsize = font_size) 39 | ) 40 | } 41 | 42 | 43 | # helper functions -------------------------------------------------------- 44 | 45 | # update a gpar object with new values 46 | update_gpar <- function(gp, gp_new) { 47 | names_new <- names(gp_new) 48 | names_old <- names(gp) 49 | gp[c(intersect(names_old, names_new), "fontface")] <- NULL 50 | gp_new["fontface"] <- NULL 51 | do.call(gpar, c(gp, gp_new)) 52 | } 53 | 54 | # update the gpar object of a drawing context 55 | set_context_gp <- function(drawing_context, gp = NULL) { 56 | gp <- update_gpar(drawing_context$gp, gp) 57 | font_info <- text_details("", gp) 58 | linespacing_pt <- gp$lineheight * gp$fontsize 59 | em_pt <- gp$fontsize 60 | 61 | update_context( 62 | drawing_context, 63 | gp = gp, 64 | ascent_pt = font_info$ascent_pt, 65 | descent_pt = font_info$descent_pt, 66 | linespacing_pt = linespacing_pt, 67 | em_pt = em_pt 68 | ) 69 | } 70 | 71 | # update the fontface of a drawing context 72 | set_context_fontface <- function(drawing_context, fontface = "plain", overwrite = FALSE) { 73 | font_old <- drawing_context$gp$font 74 | 75 | # combine bold and italic if needed 76 | if (!isTRUE(overwrite)) { 77 | if (isTRUE(fontface == "italic") && isTRUE(font_old == 2)) { # see ?grid::gpar for fontface codes 78 | fontface <- "bold.italic" 79 | } else if (isTRUE(fontface == "bold") && isTRUE(font_old == 3)) { 80 | fontface <- "bold.italic" 81 | } 82 | } 83 | 84 | set_context_gp(drawing_context, gpar(fontface = fontface)) 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/layout.h: -------------------------------------------------------------------------------- 1 | #ifndef LAYOUT_H 2 | #define LAYOUT_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include 8 | #include 9 | using namespace std; 10 | 11 | #include "length.h" 12 | 13 | enum class NodeType { 14 | none, 15 | box, 16 | glue, 17 | penalty 18 | }; 19 | 20 | enum class SizePolicy { 21 | fixed, // box size is fixed upon construction 22 | native, // box determines its own ideal size 23 | expand, // box expands as much as possible 24 | relative /* box takes up a set proportion of the size hint 25 | * provided to calc_layout(); in this case 26 | * Length units are interpreted as percent, i.e., 27 | * a Length of 100 means full size 28 | */ 29 | }; 30 | 31 | 32 | // base class for a generic node in the 33 | // layout tree 34 | template class BoxNode { 35 | public: 36 | BoxNode() {} 37 | virtual ~BoxNode() {} 38 | 39 | // returns the node type (box, glue, penalty) 40 | virtual NodeType type() = 0; 41 | 42 | // width of the box 43 | virtual Length width() = 0; 44 | // ascent of the box (height measured from baseline) 45 | virtual Length ascent() = 0; 46 | // descent of the box (height below the baseline) 47 | virtual Length descent() = 0; 48 | // vertical offset (vertical shift of baseline) 49 | virtual Length height() { 50 | return ascent() + descent(); 51 | } 52 | virtual Length voff() = 0; 53 | 54 | // calculate the internal layout of the box 55 | // in the general case, we may provide the box with a width and 56 | // a height to render into, though boxes may ignore these 57 | virtual void calc_layout(Length width_hint = 0, Length height_hint = 0) = 0; 58 | 59 | // place box in internal coordinates used in enclosing box 60 | virtual void place(Length x, Length y) = 0; 61 | 62 | // render into absolute coordinates, using the reference coordinates 63 | // from the enclosing box 64 | virtual void render(Renderer &r, Length xref, Length yref) = 0; 65 | }; 66 | 67 | template class Box : public BoxNode { 68 | Length m_width, m_stretch, m_shrink; 69 | 70 | public: 71 | Box() {} 72 | ~Box() {} 73 | NodeType type() {return NodeType::box;} 74 | }; 75 | 76 | // box list (vector of pointers to boxes) 77 | template 78 | using BoxPtr = XPtr>; 79 | 80 | template 81 | using BoxList = vector>; 82 | 83 | // struct that holds width, ascent, etc. data for text labels 84 | struct TextDetails { 85 | Length width; // width of the label 86 | Length ascent; // ascent from baseline 87 | Length descent; // descent below baseline 88 | Length space; // width of a space 89 | 90 | TextDetails(Length w = 0, Length a = 0, Length d = 0, Length s = 0) : 91 | width(w), ascent(a), descent(d), space(s) {} 92 | }; 93 | 94 | // struct that holds margin or padding info, in the form top, right, bottom, left 95 | struct Margin { 96 | Length top; 97 | Length right; 98 | Length bottom; 99 | Length left; 100 | 101 | Margin(Length t = 0, Length r = 0, Length b = 0, Length l = 0) : 102 | top(t), right(r), bottom(b), left(l) {} 103 | }; 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /src/vbox.h: -------------------------------------------------------------------------------- 1 | #ifndef VBOX_H 2 | #define VBOX_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include "layout.h" 8 | 9 | /* The VBox class takes a list of boxes and lays them out 10 | * horizontally, breaking lines if necessary. The reference point 11 | * is the lower left corner of the box. 12 | */ 13 | 14 | template 15 | class VBox : public Box { 16 | private: 17 | BoxList m_nodes; 18 | Length m_width; 19 | Length m_height; 20 | SizePolicy m_width_policy; // width policy; height policy is always "native" 21 | // reference point of the box 22 | Length m_x, m_y; 23 | // justification of box relative to reference 24 | Length m_hjust, m_vjust; 25 | double m_rel_width; // used to store relative width when needed 26 | 27 | public: 28 | VBox(const BoxList& nodes, Length width = 0, double hjust = 0, double vjust = 1, 29 | SizePolicy width_policy = SizePolicy::native) : 30 | m_nodes(nodes), 31 | m_width(width), m_height(0), 32 | m_width_policy(width_policy), 33 | m_x(0), m_y(0), 34 | m_hjust(hjust), m_vjust(vjust), 35 | m_rel_width(0) { 36 | if (m_width_policy == SizePolicy::relative) { 37 | m_rel_width = m_width/100; 38 | } 39 | } 40 | ~VBox() {}; 41 | 42 | Length width() { return m_width; } 43 | Length ascent() { return m_height; } 44 | Length descent() { return 0; } 45 | Length voff() { return 0; } 46 | 47 | void calc_layout(Length width_hint, Length height_hint) { 48 | switch(m_width_policy) { 49 | case SizePolicy::expand: 50 | m_width = width_hint; 51 | break; 52 | case SizePolicy::relative: 53 | m_width = width_hint * m_rel_width; 54 | width_hint = m_width; 55 | break; 56 | case SizePolicy::fixed: 57 | width_hint = m_width; 58 | break; 59 | case SizePolicy::native: 60 | default: 61 | // nothing to be done for native policy, will be handled below 62 | break; 63 | } 64 | 65 | // y offset as we layout 66 | Length y_off = 0; 67 | // calculated box width 68 | Length width = 0; 69 | 70 | for (auto i_node = m_nodes.begin(); i_node != m_nodes.end(); i_node++) { 71 | auto b = (*i_node); 72 | // we propagate width and height hints to all child nodes, 73 | // in case they are useful there 74 | b->calc_layout(width_hint, height_hint); 75 | y_off -= b->ascent(); 76 | // place node, ignoring any vertical offset from baseline 77 | // (we stack boxes vertically, baselines don't matter here) 78 | b->place(0, y_off - b->voff()); 79 | y_off -= b->descent(); // account for box descent if any 80 | 81 | // record width 82 | if (b->width() > width) { 83 | width = b->width(); 84 | } 85 | } 86 | 87 | if (m_width_policy == SizePolicy::native) { 88 | // we record the calculated width for native width policy 89 | // in all other cases, it's already set 90 | m_width = width; 91 | } 92 | m_height = -y_off; 93 | } 94 | 95 | void place(Length x, Length y) { 96 | m_x = x; 97 | m_y = y; 98 | } 99 | 100 | void render(Renderer &r, Length xref, Length yref) { 101 | // render all grobs in the list 102 | for (auto i_node = m_nodes.begin(); i_node != m_nodes.end(); i_node++) { 103 | (*i_node)->render( 104 | r, 105 | xref + m_x - m_hjust*m_width, 106 | yref + m_height + m_y - m_vjust*m_height 107 | ); 108 | } 109 | } 110 | }; 111 | 112 | #endif 113 | -------------------------------------------------------------------------------- /R/text-details.R: -------------------------------------------------------------------------------- 1 | #' Calculate text details for a given text label 2 | #' 3 | #' Calculate text details for a given text label 4 | #' @param label Character vector containing the label. Can handle only one label at a time. 5 | #' @param gp Grid graphical parameters defining the font (`fontfamily`, `fontface`, and 6 | #' `fontsize` should be defined). 7 | #' @examples 8 | #' text_details("Hello world!", grid::gpar(fontfamily = "", fontface = "plain", fontsize = 12)) 9 | #' text_details("Hello world!", grid::gpar(fontfamily = "", fontface = "plain", fontsize = 24)) 10 | #' text_details( 11 | #' "Hello world\nwith newline", 12 | #' grid::gpar(fontfamily = "", fontface = "plain", fontsize = 12) 13 | #' ) 14 | #' @noRd 15 | text_details <- function(label, gp = gpar()) { 16 | fontfamily <- gp$fontfamily %||% grid::get.gpar("fontfamily")$fontfamily 17 | font <- gp$font %||% grid::get.gpar("font")$font 18 | fontsize <- gp$fontsize %||% grid::get.gpar("fontsize")$fontsize 19 | 20 | devname <- names(grDevices::dev.cur()) 21 | fontkey <- paste0(devname, fontfamily, font, fontsize) 22 | if (devname == "null device") { 23 | cache <- FALSE # don't cache if no device open 24 | } else { 25 | cache <- TRUE 26 | } 27 | 28 | if (length(fontkey) != 1 || length(label) != 1) { 29 | stop("Function `text_details()` is not vectorized.", call. = FALSE) 30 | } 31 | 32 | # ascent and width depend on label and font 33 | l1 <- text_info(label, fontkey, fontfamily, font, fontsize, cache) 34 | # descent and space width depend only on font 35 | l2 <- font_info(fontkey, fontfamily, font, fontsize, cache) 36 | 37 | # concatenate, result is a list with four members, width_pt, ascent_pt, descent_pt, space_pt 38 | c(l1, l2) 39 | } 40 | 41 | font_info_cache <- new.env(parent = emptyenv()) 42 | font_info <- function(fontkey, fontfamily, font, fontsize, cache) { 43 | info <- font_info_cache[[fontkey]] 44 | 45 | if (is.null(info)) { 46 | descent_pt <- convertHeight(grobDescent(textGrob( 47 | label = "gjpqyQ", 48 | gp = gpar( 49 | fontsize = fontsize, 50 | fontfamily = fontfamily, 51 | font = font, 52 | cex = 1 53 | ) 54 | )), "pt", valueOnly = TRUE) 55 | 56 | space_pt <- convertWidth(grobWidth(textGrob( 57 | label = " ", 58 | gp = gpar( 59 | fontsize = fontsize, 60 | fontfamily = fontfamily, 61 | font = font, 62 | cex = 1 63 | ) 64 | )), "pt", valueOnly = TRUE) 65 | 66 | info <- list(descent_pt = descent_pt, space_pt = space_pt) 67 | 68 | if (cache) { 69 | font_info_cache[[fontkey]] <- info 70 | } 71 | } 72 | info 73 | } 74 | 75 | text_info_cache <- new.env(parent = emptyenv()) 76 | text_info <- function(label, fontkey, fontfamily, font, fontsize, cache) { 77 | key <- paste0(label, fontkey) 78 | info <- text_info_cache[[key]] 79 | 80 | if (is.null(info)) { 81 | ascent_pt <- convertHeight(grobHeight(textGrob( 82 | label = label, 83 | gp = gpar( 84 | fontsize = fontsize, 85 | fontfamily = fontfamily, 86 | font = font, 87 | cex = 1 88 | ) 89 | )), "pt", valueOnly = TRUE) 90 | 91 | width_pt <- convertWidth(grobWidth(textGrob( 92 | label = label, 93 | gp = gpar( 94 | fontsize = fontsize, 95 | fontfamily = fontfamily, 96 | font = font, 97 | cex = 1 98 | ) 99 | )), "pt", valueOnly = TRUE) 100 | 101 | info <- list(width_pt = width_pt, ascent_pt = ascent_pt) 102 | 103 | if (cache) { 104 | text_info_cache[[key]] <- info 105 | } 106 | } 107 | info 108 | } 109 | -------------------------------------------------------------------------------- /docs/pkgdown.js: -------------------------------------------------------------------------------- 1 | /* http://gregfranko.com/blog/jquery-best-practices/ */ 2 | (function($) { 3 | $(function() { 4 | 5 | $('.navbar-fixed-top').headroom(); 6 | 7 | $('body').css('padding-top', $('.navbar').height() + 10); 8 | $(window).resize(function(){ 9 | $('body').css('padding-top', $('.navbar').height() + 10); 10 | }); 11 | 12 | $('[data-toggle="tooltip"]').tooltip(); 13 | 14 | var cur_path = paths(location.pathname); 15 | var links = $("#navbar ul li a"); 16 | var max_length = -1; 17 | var pos = -1; 18 | for (var i = 0; i < links.length; i++) { 19 | if (links[i].getAttribute("href") === "#") 20 | continue; 21 | // Ignore external links 22 | if (links[i].host !== location.host) 23 | continue; 24 | 25 | var nav_path = paths(links[i].pathname); 26 | 27 | var length = prefix_length(nav_path, cur_path); 28 | if (length > max_length) { 29 | max_length = length; 30 | pos = i; 31 | } 32 | } 33 | 34 | // Add class to parent
  • , and enclosing
  • if in dropdown 35 | if (pos >= 0) { 36 | var menu_anchor = $(links[pos]); 37 | menu_anchor.parent().addClass("active"); 38 | menu_anchor.closest("li.dropdown").addClass("active"); 39 | } 40 | }); 41 | 42 | function paths(pathname) { 43 | var pieces = pathname.split("/"); 44 | pieces.shift(); // always starts with / 45 | 46 | var end = pieces[pieces.length - 1]; 47 | if (end === "index.html" || end === "") 48 | pieces.pop(); 49 | return(pieces); 50 | } 51 | 52 | // Returns -1 if not found 53 | function prefix_length(needle, haystack) { 54 | if (needle.length > haystack.length) 55 | return(-1); 56 | 57 | // Special case for length-0 haystack, since for loop won't run 58 | if (haystack.length === 0) { 59 | return(needle.length === 0 ? 0 : -1); 60 | } 61 | 62 | for (var i = 0; i < haystack.length; i++) { 63 | if (needle[i] != haystack[i]) 64 | return(i); 65 | } 66 | 67 | return(haystack.length); 68 | } 69 | 70 | /* Clipboard --------------------------*/ 71 | 72 | function changeTooltipMessage(element, msg) { 73 | var tooltipOriginalTitle=element.getAttribute('data-original-title'); 74 | element.setAttribute('data-original-title', msg); 75 | $(element).tooltip('show'); 76 | element.setAttribute('data-original-title', tooltipOriginalTitle); 77 | } 78 | 79 | if(ClipboardJS.isSupported()) { 80 | $(document).ready(function() { 81 | var copyButton = ""; 82 | 83 | $(".examples, div.sourceCode").addClass("hasCopyButton"); 84 | 85 | // Insert copy buttons: 86 | $(copyButton).prependTo(".hasCopyButton"); 87 | 88 | // Initialize tooltips: 89 | $('.btn-copy-ex').tooltip({container: 'body'}); 90 | 91 | // Initialize clipboard: 92 | var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { 93 | text: function(trigger) { 94 | return trigger.parentNode.textContent; 95 | } 96 | }); 97 | 98 | clipboardBtnCopies.on('success', function(e) { 99 | changeTooltipMessage(e.trigger, 'Copied!'); 100 | e.clearSelection(); 101 | }); 102 | 103 | clipboardBtnCopies.on('error', function() { 104 | changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy'); 105 | }); 106 | }); 107 | } 108 | })(window.jQuery || window.$) 109 | -------------------------------------------------------------------------------- /tests/testthat/test-vbox.R: -------------------------------------------------------------------------------- 1 | test_that("vertical stacking works", { 2 | nb <- bl_make_null_box() 3 | rb1 <- bl_make_rect_box(nb, 100, 100, rep(0, 4), rep(0, 4), gp = gpar()) 4 | rb2 <- bl_make_rect_box(nb, 50, 50, rep(10, 4), rep(0, 4), gp = gpar()) 5 | rb3 <- bl_make_rect_box(nb, 50, 10, rep(0, 4), rep(0, 4), gp = gpar(), width_policy = "expand") 6 | 7 | vb <- bl_make_vbox(list(rb1, rb2, rb3), width = 200, hjust = 0, vjust = 0, width_policy = "fixed") 8 | 9 | bl_calc_layout(vb, 0, 0) 10 | bl_place(vb, 0, 0) 11 | 12 | expect_identical(bl_box_width(vb), 200) 13 | expect_identical(bl_box_height(vb), 160) 14 | expect_identical(bl_box_voff(vb), 0) 15 | 16 | g <- bl_render(vb, 200, 100) 17 | 18 | out1 <- g[[1]] 19 | expect_identical(out1$x, unit(200, "pt")) 20 | expect_identical(out1$y, unit(100 + 10 + 50, "pt")) 21 | 22 | out2 <- g[[2]] 23 | expect_identical(out2$x, unit(210, "pt")) 24 | expect_identical(out2$y, unit(100 + 10 + 10, "pt")) 25 | 26 | out3 <- g[[3]] 27 | expect_identical(out3$x, unit(200, "pt")) 28 | expect_identical(out3$y, unit(100, "pt")) 29 | expect_identical(out3$width, unit(200, "pt")) 30 | 31 | # alternatve hjust, vjust, x, y 32 | vb <- bl_make_vbox(list(rb1, rb2, rb3), width = 200, hjust = 1, vjust = 1, width_policy = "fixed") 33 | 34 | bl_calc_layout(vb, 0, 0) 35 | bl_place(vb, 15, 27) 36 | 37 | expect_identical(bl_box_width(vb), 200) 38 | expect_identical(bl_box_height(vb), 160) 39 | 40 | g <- bl_render(vb, 200, 100) 41 | 42 | out1 <- g[[1]] 43 | expect_identical(out1$x, unit(15 + 0, "pt")) 44 | expect_identical(out1$y, unit(27 - 60 + 10 + 50, "pt")) 45 | 46 | out2 <- g[[2]] 47 | expect_identical(out2$x, unit(15 + 10, "pt")) 48 | expect_identical(out2$y, unit(27 - 60 + 10 + 10, "pt")) 49 | 50 | out3 <- g[[3]] 51 | expect_identical(out3$x, unit(15 + 0, "pt")) 52 | expect_identical(out3$y, unit(27 - 60, "pt")) 53 | expect_identical(out3$width, unit(200, "pt")) 54 | }) 55 | 56 | 57 | test_that("size policies", { 58 | nb <- bl_make_null_box() 59 | rb1 <- bl_make_rect_box(nb, 100, 100, rep(0, 4), rep(0, 4), gp = gpar()) 60 | rb2 <- bl_make_rect_box(nb, 50, 50, rep(10, 4), rep(0, 4), gp = gpar()) 61 | vb <- bl_make_vbox(list(rb1, rb2), width = 200, hjust = 0.5, vjust = 0.5, width_policy = "native") 62 | 63 | bl_calc_layout(vb, 0, 0) 64 | expect_identical(bl_box_width(vb), 100) 65 | expect_identical(bl_box_height(vb), 150) 66 | 67 | vb <- bl_make_vbox(list(rb1, rb2), width = 200, hjust = 0.5, vjust = 0.5, width_policy = "relative") 68 | 69 | bl_calc_layout(vb, 70, 0) 70 | expect_identical(bl_box_width(vb), 140) 71 | expect_identical(bl_box_height(vb), 150) 72 | 73 | vb <- bl_make_vbox(list(rb1, rb2), width = 200, hjust = 0.5, vjust = 0.5, width_policy = "expand") 74 | 75 | bl_calc_layout(vb, 300, 0) 76 | expect_identical(bl_box_width(vb), 300) 77 | expect_identical(bl_box_height(vb), 150) 78 | }) 79 | 80 | test_that("vertical offset is ignored in vertical stacking", { 81 | tb1 <- bl_make_text_box("string1", gp = gpar(fontsize = 10)) 82 | tb2 <- bl_make_text_box("string2", gp = gpar(fontsize = 20), voff = -10) 83 | tb3 <- bl_make_text_box("string2", gp = gpar(fontsize = 20), voff = 0) 84 | tb4 <- bl_make_text_box("string3", gp = gpar(fontsize = 15)) 85 | vb1 <- bl_make_vbox(list(tb1, tb2, tb4), hjust = 0, vjust = 0) 86 | bl_calc_layout(vb1, 100, 100) 87 | bl_place(vb1, 17, 24) 88 | vb2 <- bl_make_vbox(list(tb1, tb3, tb4), hjust = 0, vjust = 0) 89 | bl_calc_layout(vb2, 100, 100) 90 | bl_place(vb2, 17, 24) 91 | g1 <- bl_render(vb1, 0, 0) 92 | g2 <- bl_render(vb2, 0, 0) 93 | 94 | extract <- function(x, name) {x[[name]]} 95 | 96 | expect_identical( 97 | lapply(g1, extract, name = "x"), 98 | lapply(g2, extract, name = "x") 99 | ) 100 | 101 | expect_identical( 102 | lapply(g1, extract, name = "y"), 103 | lapply(g2, extract, name = "y") 104 | ) 105 | 106 | expect_identical( 107 | lapply(g1, extract, name = "label"), 108 | lapply(g2, extract, name = "label") 109 | ) 110 | }) 111 | -------------------------------------------------------------------------------- /tests/testthat/test-grid-renderer.R: -------------------------------------------------------------------------------- 1 | context("grid-renderer") 2 | 3 | test_that("basic functioning", { 4 | r <- grid_renderer() 5 | g <- grid_renderer_collect_grobs(r) 6 | 7 | # without any grobs rendered, we get an empty list of class gList 8 | expect_equal(length(g), 0) 9 | expect_true(inherits(g, "gList")) 10 | 11 | # grobs get added in order 12 | grid_renderer_text(r, "abcd", 100, 100, gpar()) 13 | grid_renderer_rect(r, 100, 100, 200, 200, gpar()) 14 | g <- grid_renderer_collect_grobs(r) 15 | expect_equal(length(g), 2) 16 | expect_true(inherits(g, "gList")) 17 | expect_true(inherits(g[[1]], "text")) 18 | expect_true(inherits(g[[2]], "rect")) 19 | 20 | # internal state gets reset after calling collect_grobs() 21 | g <- grid_renderer_collect_grobs(r) 22 | expect_equal(length(g), 0) 23 | expect_true(inherits(g, "gList")) 24 | }) 25 | 26 | test_that("smart rendering of rects", { 27 | r <- grid_renderer() 28 | # add normal rect 29 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar()) 30 | # add rect with rounded corners 31 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(), r = 5) 32 | # add rect that is invisible, gets removed automatically 33 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(col = NA)) 34 | g <- grid_renderer_collect_grobs(r) 35 | expect_equal(length(g), 2) 36 | expect_true(inherits(g, "gList")) 37 | expect_true(inherits(g[[1]], "rect")) 38 | expect_true(inherits(g[[2]], "roundrect")) 39 | 40 | # more extensive testing variations for dropping unneeded rects 41 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(lty = 0)) 42 | g <- grid_renderer_collect_grobs(r) 43 | expect_equal(length(g), 0) 44 | 45 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(col = NA, lty = 1)) 46 | g <- grid_renderer_collect_grobs(r) 47 | expect_equal(length(g), 0) 48 | 49 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(col = "black", lty = 0)) 50 | g <- grid_renderer_collect_grobs(r) 51 | expect_equal(length(g), 0) 52 | 53 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(fill = NA, lty = 0)) 54 | g <- grid_renderer_collect_grobs(r) 55 | expect_equal(length(g), 0) 56 | 57 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(fill = NA, col = "black", lty = 0)) 58 | g <- grid_renderer_collect_grobs(r) 59 | expect_equal(length(g), 0) 60 | 61 | grid_renderer_rect(r, 100, 100, 200, 200, gp = gpar(fill = NA, col = NA, lty = 1)) 62 | g <- grid_renderer_collect_grobs(r) 63 | expect_equal(length(g), 0) 64 | }) 65 | 66 | test_that("visual tests", { 67 | draw_grob <- function(g) { 68 | function() { 69 | grid.newpage() 70 | grid.draw(g) 71 | invisible() 72 | } 73 | } 74 | 75 | r <- grid_renderer() 76 | grid_renderer_text(r, "blue", 10, 400, gp = gpar(col = "blue", fontsize = 12)) 77 | grid_renderer_text(r, "red bold", 20, 380, gp = gpar(col = "red", fontsize = 12, fontface = "bold")) 78 | grid_renderer_text(r, "roman", 30, 360, gp = gpar(fontsize = 12, fontfamily = "Times")) 79 | g <- grid_renderer_collect_grobs(r) 80 | expect_doppelganger("Text in different stylings", draw_grob(g)) 81 | 82 | grid_renderer_rect(r, 100, 400, 200, 20, gp = gpar(col = "blue")) 83 | grid_renderer_rect(r, 100, 200, 300, 30, gp = gpar(fill = "cornsilk"), r = 8) 84 | grid_renderer_text(r, "text 1, square box blue", 100, 400, gp = gpar(fontsize = 20)) 85 | grid_renderer_text(r, "text 2, rounded box filled", 100, 200, gp = gpar(fontsize = 20)) 86 | g <- grid_renderer_collect_grobs(r) 87 | expect_doppelganger("Mixing text and boxes", draw_grob(g)) 88 | 89 | logo_file <- system.file("extdata", "Rlogo.png", package = "gridtext") 90 | logo <- png::readPNG(logo_file, native = TRUE) 91 | width <- ncol(logo) 92 | height <- nrow(logo) 93 | grid_renderer_raster(r, logo, 10, 10, width, height) 94 | g <- grid_renderer_collect_grobs(r) 95 | expect_doppelganger("Rendering raster data", draw_grob(g)) 96 | }) 97 | 98 | 99 | test_that("text details are calculated correctly", { 100 | gp = gpar(fontsize = 20) 101 | td <- text_details("abcd", gp) 102 | 103 | td2 <- grid_renderer_text_details("abcd", gp) 104 | expect_identical(td, td2) 105 | }) 106 | -------------------------------------------------------------------------------- /src/grid-renderer.h: -------------------------------------------------------------------------------- 1 | #ifndef GRID_RENDERER_H 2 | #define GRID_RENDERER_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include 8 | 9 | #include "grid.h" 10 | #include "length.h" 11 | #include "layout.h" 12 | 13 | class GridRenderer { 14 | public: 15 | // GridRenderer stores its graphics context in a grid gpar() list 16 | typedef List GraphicsContext; 17 | 18 | private: 19 | vector m_grobs; 20 | 21 | RObject gpar_lookup(List gp, const char* element) { 22 | if (!gp.containsElementNamed(element)) { 23 | return R_NilValue; 24 | } else { 25 | return gp[element]; 26 | } 27 | } 28 | 29 | public: 30 | GridRenderer() { 31 | } 32 | 33 | static TextDetails text_details(const CharacterVector &label, GraphicsContext gp) { 34 | // call R function to look up text info 35 | Environment env = Environment::namespace_env("gridtext"); 36 | 37 | Function td = env["text_details"]; 38 | List info = td(label, gp); 39 | RObject width_pt = info["width_pt"]; 40 | RObject ascent_pt = info["ascent_pt"]; 41 | RObject descent_pt = info["descent_pt"]; 42 | RObject space_pt = info["space_pt"]; 43 | return TextDetails( 44 | NumericVector(width_pt)[0], 45 | NumericVector(ascent_pt)[0], 46 | NumericVector(descent_pt)[0], 47 | NumericVector(space_pt)[0] 48 | ); 49 | } 50 | 51 | void text(const CharacterVector &label, Length x, Length y, const GraphicsContext &gp) { 52 | m_grobs.push_back(text_grob(label, NumericVector(1, x), NumericVector(1, y), gp)); 53 | } 54 | 55 | void raster(RObject image, Length x, Length y, Length width, Length height, bool interpolate = true, 56 | const GraphicsContext &gp = R_NilValue) { 57 | if (!image.isNULL()) { 58 | m_grobs.push_back( 59 | raster_grob( 60 | image, NumericVector(1, x), NumericVector(1, y), 61 | NumericVector(1, width), NumericVector(1, height), LogicalVector(1, interpolate, gp) 62 | ) 63 | ); 64 | } 65 | } 66 | 67 | void rect(Length x, Length y, Length width, Length height, const GraphicsContext &gp, Length r = 0) { 68 | // skip drawing if nothing would show anyways 69 | 70 | // default assumption is we don't have a fill color but we do have line color and type 71 | bool have_fill_col = false; 72 | bool have_line_col = true; 73 | bool have_line_type = true; 74 | RObject fill_obj = gpar_lookup(gp, "fill"); 75 | 76 | if (!fill_obj.isNULL()) { 77 | CharacterVector fill(fill_obj); 78 | if (fill.size() > 0 && !CharacterVector::is_na(fill[0])) { 79 | have_fill_col = true; 80 | } 81 | } 82 | 83 | // if we have a fill color, further checks don't matter 84 | if (!have_fill_col) { 85 | RObject color = gpar_lookup(gp, "col"); 86 | if (!color.isNULL()) { 87 | CharacterVector col(color); 88 | if (col.size() == 0 || CharacterVector::is_na(col[0])) { 89 | have_line_col = false; 90 | } 91 | } 92 | } 93 | 94 | // if we don't have a fill color but do have a line color, 95 | // need to check line type 96 | if (!have_fill_col && have_line_col) { 97 | RObject linetype = gpar_lookup(gp, "lty"); 98 | if (!linetype.isNULL()) { 99 | NumericVector lty(linetype); 100 | if (lty.size() == 0 || lty[0] == 0) { 101 | have_line_type = false; 102 | } 103 | } 104 | } 105 | 106 | if (!have_fill_col && (!have_line_col || !have_line_type)) { 107 | return; 108 | } 109 | 110 | // now that we know we should draw, go ahead 111 | 112 | NumericVector xv(1, x), yv(1, y), widthv(1, width), heightv(1, height); 113 | 114 | // draw simple rect grob or rounded rect grob depending on provided radius 115 | if (r < 0.01) { 116 | m_grobs.push_back(rect_grob(xv, yv, widthv, heightv, gp)); 117 | } else { 118 | NumericVector rv(1, r); 119 | m_grobs.push_back(roundrect_grob(xv, yv, widthv, heightv, rv, gp)); 120 | } 121 | } 122 | 123 | 124 | List collect_grobs() { 125 | // turn vector of grobs into list; doing it this way avoids 126 | // List.push_back() which is slow. 127 | List out(m_grobs.size()); 128 | 129 | size_t i = 0; 130 | for (auto i_grob = m_grobs.begin(); i_grob != m_grobs.end(); i_grob++) { 131 | out[i] = *i_grob; 132 | i++; 133 | } 134 | // clear internal grobs list; the renderer is reset with each collect_grobs() call 135 | m_grobs.clear(); 136 | 137 | // turn list into gList to keep grid happy 138 | out.attr("class") = "gList"; 139 | 140 | return out; 141 | } 142 | }; 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | bl_make_null_box <- function(width_pt = 0, height_pt = 0) { 5 | .Call(`_gridtext_bl_make_null_box`, width_pt, height_pt) 6 | } 7 | 8 | bl_make_par_box <- function(node_list, vspacing_pt, width_policy = "native", hjust = NULL) { 9 | .Call(`_gridtext_bl_make_par_box`, node_list, vspacing_pt, width_policy, hjust) 10 | } 11 | 12 | bl_make_rect_box <- function(content, width_pt, height_pt, margin, padding, gp, content_hjust = 0, content_vjust = 1, width_policy = "fixed", height_policy = "fixed", r = 0) { 13 | .Call(`_gridtext_bl_make_rect_box`, content, width_pt, height_pt, margin, padding, gp, content_hjust, content_vjust, width_policy, height_policy, r) 14 | } 15 | 16 | bl_make_text_box <- function(label, gp, voff_pt = 0) { 17 | .Call(`_gridtext_bl_make_text_box`, label, gp, voff_pt) 18 | } 19 | 20 | bl_make_raster_box <- function(image, width_pt = 0, height_pt = 0, width_policy = "native", height_policy = "native", respect_aspect = TRUE, interpolate = TRUE, dpi = 150, gp = NULL) { 21 | .Call(`_gridtext_bl_make_raster_box`, image, width_pt, height_pt, width_policy, height_policy, respect_aspect, interpolate, dpi, gp) 22 | } 23 | 24 | bl_make_vbox <- function(node_list, width_pt = 0, hjust = 0, vjust = 1, width_policy = "native") { 25 | .Call(`_gridtext_bl_make_vbox`, node_list, width_pt, hjust, vjust, width_policy) 26 | } 27 | 28 | bl_make_regular_space_glue <- function(gp, stretch_ratio = 0.5, shrink_ratio = 0.333333) { 29 | .Call(`_gridtext_bl_make_regular_space_glue`, gp, stretch_ratio, shrink_ratio) 30 | } 31 | 32 | bl_make_forced_break_penalty <- function() { 33 | .Call(`_gridtext_bl_make_forced_break_penalty`) 34 | } 35 | 36 | bl_make_never_break_penalty <- function() { 37 | .Call(`_gridtext_bl_make_never_break_penalty`) 38 | } 39 | 40 | bl_box_width <- function(node) { 41 | .Call(`_gridtext_bl_box_width`, node) 42 | } 43 | 44 | bl_box_height <- function(node) { 45 | .Call(`_gridtext_bl_box_height`, node) 46 | } 47 | 48 | bl_box_ascent <- function(node) { 49 | .Call(`_gridtext_bl_box_ascent`, node) 50 | } 51 | 52 | bl_box_descent <- function(node) { 53 | .Call(`_gridtext_bl_box_descent`, node) 54 | } 55 | 56 | bl_box_voff <- function(node) { 57 | .Call(`_gridtext_bl_box_voff`, node) 58 | } 59 | 60 | bl_calc_layout <- function(node, width_pt = 0, height_pt = 0) { 61 | invisible(.Call(`_gridtext_bl_calc_layout`, node, width_pt, height_pt)) 62 | } 63 | 64 | bl_place <- function(node, x_pt, y_pt) { 65 | invisible(.Call(`_gridtext_bl_place`, node, x_pt, y_pt)) 66 | } 67 | 68 | bl_render <- function(node, x_pt = 0, y_pt = 0) { 69 | .Call(`_gridtext_bl_render`, node, x_pt, y_pt) 70 | } 71 | 72 | grid_renderer <- function() { 73 | .Call(`_gridtext_grid_renderer`) 74 | } 75 | 76 | grid_renderer_text <- function(gr, label, x, y, gp) { 77 | invisible(.Call(`_gridtext_grid_renderer_text`, gr, label, x, y, gp)) 78 | } 79 | 80 | grid_renderer_text_details <- function(label, gp) { 81 | .Call(`_gridtext_grid_renderer_text_details`, label, gp) 82 | } 83 | 84 | grid_renderer_raster <- function(gr, image, x, y, width, height, interpolate = TRUE) { 85 | invisible(.Call(`_gridtext_grid_renderer_raster`, gr, image, x, y, width, height, interpolate)) 86 | } 87 | 88 | grid_renderer_rect <- function(gr, x, y, width, height, gp, r = 0L) { 89 | invisible(.Call(`_gridtext_grid_renderer_rect`, gr, x, y, width, height, gp, r)) 90 | } 91 | 92 | grid_renderer_collect_grobs <- function(gr) { 93 | .Call(`_gridtext_grid_renderer_collect_grobs`, gr) 94 | } 95 | 96 | unit_pt <- function(x) { 97 | .Call(`_gridtext_unit_pt`, x) 98 | } 99 | 100 | gpar_empty <- function() { 101 | .Call(`_gridtext_gpar_empty`) 102 | } 103 | 104 | text_grob <- function(label, x_pt = 0L, y_pt = 0L, gp = NULL, name = NULL) { 105 | .Call(`_gridtext_text_grob`, label, x_pt, y_pt, gp, name) 106 | } 107 | 108 | raster_grob <- function(image, x_pt = 0L, y_pt = 0L, width_pt = 0L, height_pt = 0L, interpolate = TRUE, gp = NULL, name = NULL) { 109 | .Call(`_gridtext_raster_grob`, image, x_pt, y_pt, width_pt, height_pt, interpolate, gp, name) 110 | } 111 | 112 | rect_grob <- function(x_pt = 0L, y_pt = 0L, width_pt = 0L, height_pt = 0L, gp = NULL, name = NULL) { 113 | .Call(`_gridtext_rect_grob`, x_pt, y_pt, width_pt, height_pt, gp, name) 114 | } 115 | 116 | roundrect_grob <- function(x_pt = 0L, y_pt = 0L, width_pt = 0L, height_pt = 0L, r_pt = 5L, gp = NULL, name = NULL) { 117 | .Call(`_gridtext_roundrect_grob`, x_pt, y_pt, width_pt, height_pt, r_pt, gp, name) 118 | } 119 | 120 | set_grob_coords <- function(grob, x, y) { 121 | .Call(`_gridtext_set_grob_coords`, grob, x, y) 122 | } 123 | 124 | -------------------------------------------------------------------------------- /man/richtext_grob.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/richtext-grob.R 3 | \name{richtext_grob} 4 | \alias{richtext_grob} 5 | \title{Draw formatted text labels} 6 | \usage{ 7 | richtext_grob( 8 | text, 9 | x = unit(0.5, "npc"), 10 | y = unit(0.5, "npc"), 11 | hjust = 0.5, 12 | vjust = 0.5, 13 | halign = hjust, 14 | valign = vjust, 15 | rot = 0, 16 | default.units = "npc", 17 | margin = unit(c(0, 0, 0, 0), "pt"), 18 | padding = unit(c(0, 0, 0, 0), "pt"), 19 | r = unit(0, "pt"), 20 | align_widths = FALSE, 21 | align_heights = FALSE, 22 | name = NULL, 23 | gp = gpar(), 24 | box_gp = gpar(col = NA), 25 | vp = NULL, 26 | use_markdown = TRUE, 27 | debug = FALSE 28 | ) 29 | } 30 | \arguments{ 31 | \item{text}{Character vector containing Markdown/HTML strings to draw.} 32 | 33 | \item{x, y}{Unit objects specifying the location of the reference point.} 34 | 35 | \item{hjust, vjust}{Numerical values specifying the justification 36 | of the text boxes relative to \code{x} and \code{y}. These justification parameters 37 | are specified in the internal reference frame of the text boxes, so that, 38 | for example, \code{hjust} adjusts the vertical justification when the 39 | text is rotated 90 degrees to the left or right.} 40 | 41 | \item{halign, valign}{Numerical values specifying the text justification 42 | inside the text boxes. If not specified, these default to \code{hjust} and 43 | \code{vjust}.} 44 | 45 | \item{rot}{Angle of rotation for text, in degrees.} 46 | 47 | \item{default.units}{Units of \code{x} and \code{y} if these are provided only as 48 | numerical values.} 49 | 50 | \item{margin, padding}{Unit vectors of four elements each indicating the 51 | margin and padding around each text label in the order top, right, 52 | bottom, left. Margins are drawn outside the enclosing box (if any), 53 | and padding is drawn inside. To avoid rendering artifacts, it is best 54 | to specify these values in absolute units (such as points, mm, or inch) 55 | rather than in relative units (such as npc).} 56 | 57 | \item{r}{The radius of the rounded corners. To avoid rendering artifacts, 58 | it is best to specify this in absolute units (such as points, mm, or inch) 59 | rather than in relative units (such as npc).} 60 | 61 | \item{align_widths, align_heights}{Should the widths and heights of all 62 | the text boxes be aligned? Default is no.} 63 | 64 | \item{name}{Name of the grob.} 65 | 66 | \item{gp}{Other graphical parameters for drawing.} 67 | 68 | \item{box_gp}{Graphical parameters for the enclosing box around each text label.} 69 | 70 | \item{vp}{Viewport.} 71 | 72 | \item{use_markdown}{Should the \code{text} input be treated as markdown? Default 73 | is yes.} 74 | 75 | \item{debug}{Should debugging info be drawn? Default is no.} 76 | } 77 | \value{ 78 | A grid \code{\link{grob}} that represents the formatted text. 79 | } 80 | \description{ 81 | This grob acts mostly as a drop-in replacement for \code{\link[grid:grid.text]{grid::textGrob()}} 82 | but provides more sophisticated formatting. The grob can handle basic 83 | markdown and HTML formatting directives, and it can also draw 84 | boxes around each piece of text. Note that this grob \strong{does not} draw 85 | \link{plotmath} expressions. 86 | } 87 | \examples{ 88 | library(grid) 89 | 90 | text <- c( 91 | "Some text **in bold.**", "Linebreaks
    Linebreaks
    Linebreaks", 92 | "*x*2 + 5*x* + *C**i*", 93 | "Some blue text **in bold.**
    And *italics text.*
    94 | And some large text." 95 | ) 96 | 97 | x <- c(.2, .1, .7, .9) 98 | y <- c(.8, .4, .1, .5) 99 | rot <- c(0, 0, 45, -45) 100 | gp = gpar(col = c("black", "red"), fontfamily = c("Palatino", "Courier", "Times", "Helvetica")) 101 | box_gp = gpar(col = "black", fill = c("cornsilk", NA, "lightblue1", NA), lty = c(0, 1, 1, 1)) 102 | hjust <- c(0.5, 0, 0, 1) 103 | vjust <- c(0.5, 1, 0, 0.5) 104 | 105 | g <- richtext_grob( 106 | text, x, y, hjust = hjust, vjust = vjust, rot = rot, 107 | padding = unit(c(6, 6, 4, 6), "pt"), 108 | r = unit(c(0, 2, 4, 8), "pt"), 109 | gp = gp, box_gp = box_gp 110 | ) 111 | grid.newpage() 112 | grid.draw(g) 113 | grid.points(x, y, default.units = "npc", pch = 19, size = unit(5, "pt")) 114 | 115 | # multiple text labels with aligned boxes 116 | text <- c("January", "February", "March", "April", "May") 117 | x <- (1:5)/6 + 1/24 118 | y <- rep(0.8, 5) 119 | g <- richtext_grob( 120 | text, x, y, halign = 0, hjust = 1, 121 | rot = 45, 122 | padding = unit(c(3, 6, 1, 3), "pt"), 123 | r = unit(4, "pt"), 124 | align_widths = TRUE, 125 | box_gp = gpar(col = "black", fill = "cornsilk") 126 | ) 127 | grid.newpage() 128 | grid.draw(g) 129 | grid.points(x, y, default.units = "npc", pch = 19, size = unit(5, "pt")) 130 | } 131 | \seealso{ 132 | \code{\link[=textbox_grob]{textbox_grob()}} 133 | } 134 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | (function() { 6 | 'use strict'; 7 | 8 | window.Toc = { 9 | helpers: { 10 | // return all matching elements in the set, or their descendants 11 | findOrFilter: function($el, selector) { 12 | // http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/ 13 | // http://stackoverflow.com/a/12731439/358804 14 | var $descendants = $el.find(selector); 15 | return $el.filter(selector).add($descendants).filter(':not([data-toc-skip])'); 16 | }, 17 | 18 | generateUniqueIdBase: function(el) { 19 | var text = $(el).text(); 20 | var anchor = text.trim().toLowerCase().replace(/[^A-Za-z0-9]+/g, '-'); 21 | return anchor || el.tagName.toLowerCase(); 22 | }, 23 | 24 | generateUniqueId: function(el) { 25 | var anchorBase = this.generateUniqueIdBase(el); 26 | for (var i = 0; ; i++) { 27 | var anchor = anchorBase; 28 | if (i > 0) { 29 | // add suffix 30 | anchor += '-' + i; 31 | } 32 | // check if ID already exists 33 | if (!document.getElementById(anchor)) { 34 | return anchor; 35 | } 36 | } 37 | }, 38 | 39 | generateAnchor: function(el) { 40 | if (el.id) { 41 | return el.id; 42 | } else { 43 | var anchor = this.generateUniqueId(el); 44 | el.id = anchor; 45 | return anchor; 46 | } 47 | }, 48 | 49 | createNavList: function() { 50 | return $(''); 51 | }, 52 | 53 | createChildNavList: function($parent) { 54 | var $childList = this.createNavList(); 55 | $parent.append($childList); 56 | return $childList; 57 | }, 58 | 59 | generateNavEl: function(anchor, text) { 60 | var $a = $(''); 61 | $a.attr('href', '#' + anchor); 62 | $a.text(text); 63 | var $li = $('
  • '); 64 | $li.append($a); 65 | return $li; 66 | }, 67 | 68 | generateNavItem: function(headingEl) { 69 | var anchor = this.generateAnchor(headingEl); 70 | var $heading = $(headingEl); 71 | var text = $heading.data('toc-text') || $heading.text(); 72 | return this.generateNavEl(anchor, text); 73 | }, 74 | 75 | // Find the first heading level (`

    `, then `

    `, etc.) that has more than one element. Defaults to 1 (for `

    `). 76 | getTopLevel: function($scope) { 77 | for (var i = 1; i <= 6; i++) { 78 | var $headings = this.findOrFilter($scope, 'h' + i); 79 | if ($headings.length > 1) { 80 | return i; 81 | } 82 | } 83 | 84 | return 1; 85 | }, 86 | 87 | // returns the elements for the top level, and the next below it 88 | getHeadings: function($scope, topLevel) { 89 | var topSelector = 'h' + topLevel; 90 | 91 | var secondaryLevel = topLevel + 1; 92 | var secondarySelector = 'h' + secondaryLevel; 93 | 94 | return this.findOrFilter($scope, topSelector + ',' + secondarySelector); 95 | }, 96 | 97 | getNavLevel: function(el) { 98 | return parseInt(el.tagName.charAt(1), 10); 99 | }, 100 | 101 | populateNav: function($topContext, topLevel, $headings) { 102 | var $context = $topContext; 103 | var $prevNav; 104 | 105 | var helpers = this; 106 | $headings.each(function(i, el) { 107 | var $newNav = helpers.generateNavItem(el); 108 | var navLevel = helpers.getNavLevel(el); 109 | 110 | // determine the proper $context 111 | if (navLevel === topLevel) { 112 | // use top level 113 | $context = $topContext; 114 | } else if ($prevNav && $context === $topContext) { 115 | // create a new level of the tree and switch to it 116 | $context = helpers.createChildNavList($prevNav); 117 | } // else use the current $context 118 | 119 | $context.append($newNav); 120 | 121 | $prevNav = $newNav; 122 | }); 123 | }, 124 | 125 | parseOps: function(arg) { 126 | var opts; 127 | if (arg.jquery) { 128 | opts = { 129 | $nav: arg 130 | }; 131 | } else { 132 | opts = arg; 133 | } 134 | opts.$scope = opts.$scope || $(document.body); 135 | return opts; 136 | } 137 | }, 138 | 139 | // accepts a jQuery object, or an options object 140 | init: function(opts) { 141 | opts = this.helpers.parseOps(opts); 142 | 143 | // ensure that the data attribute is in place for styling 144 | opts.$nav.attr('data-toggle', 'toc'); 145 | 146 | var $topContext = this.helpers.createChildNavList(opts.$nav); 147 | var topLevel = this.helpers.getTopLevel(opts.$scope); 148 | var $headings = this.helpers.getHeadings(opts.$scope, topLevel); 149 | this.helpers.populateNav($topContext, topLevel, $headings); 150 | } 151 | }; 152 | 153 | $(function() { 154 | $('nav[data-toggle="toc"]').each(function(i, el) { 155 | var $nav = $(el); 156 | Toc.init($nav); 157 | }); 158 | }); 159 | })(); 160 | -------------------------------------------------------------------------------- /src/raster-box.h: -------------------------------------------------------------------------------- 1 | #ifndef RASTER_BOX_H 2 | #define RASTER_BOX_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include // for pair<> 8 | using namespace std; 9 | 10 | #include "layout.h" 11 | 12 | inline pair image_dimensions(RObject image) { 13 | Environment env = Environment::namespace_env("base"); 14 | Function dim = env["dim"]; 15 | 16 | NumericVector dims = dim(image); 17 | if (dims.size() < 2) { 18 | stop("Cannot extract image dimensions. Image must be a matrix, raster, or nativeRaster object."); 19 | } 20 | 21 | // first dimension is rows (height), second is columns (width) 22 | return pair(dims[1], dims[0]); 23 | } 24 | 25 | 26 | // A box holding a single image 27 | template 28 | class RasterBox : public Box { 29 | private: 30 | RObject m_image; 31 | typename Renderer::GraphicsContext m_gp; 32 | Length m_width, m_height; 33 | SizePolicy m_width_policy, m_height_policy; 34 | // position of the box in enclosing box 35 | // the box reference point is the leftmost point of the baseline. 36 | Length m_x, m_y; 37 | bool m_respect_asp; // if `true`, always plots image with correct aspect ratio, regardless of box dimensions 38 | bool m_interpolate; // if `true`, interpolates raster images 39 | double m_dpi; // dots per inch to determine native image sizes 40 | double m_rel_width, m_rel_height; // used to store relative width and height when needed 41 | Length m_native_width, m_native_height; // native width and height of image, in pt 42 | 43 | public: 44 | RasterBox(RObject image, Length width, Length height, const typename Renderer::GraphicsContext &gp, 45 | SizePolicy width_policy = SizePolicy::native, SizePolicy height_policy = SizePolicy::native, 46 | bool respect_aspect = true, bool interpolate = true, double dpi = 150) : 47 | m_image(image), m_gp(gp), m_width(width), m_height(height), 48 | m_width_policy(width_policy), m_height_policy(height_policy), 49 | m_x(0), m_y(0), m_respect_asp(respect_aspect), m_interpolate(interpolate), 50 | m_dpi(dpi), m_rel_width(0), m_rel_height(0), 51 | m_native_width(0), m_native_height(0) { 52 | pair d = image_dimensions(image); 53 | 54 | // there are 72.27 pt in each in 55 | m_native_width = d.first * 72.27 / m_dpi; 56 | m_native_height = d.second * 72.27 / m_dpi; 57 | 58 | if (m_width_policy == SizePolicy::relative) { 59 | m_rel_width = m_width/100; 60 | } 61 | if (m_height_policy == SizePolicy::relative) { 62 | m_rel_height = m_height/100; 63 | } 64 | } 65 | ~RasterBox() {}; 66 | 67 | Length width() { return m_width; } 68 | Length ascent() { return m_height; } 69 | Length descent() { return 0; } 70 | Length voff() { return 0; } 71 | 72 | void calc_layout(Length width_hint, Length height_hint) { 73 | if (m_width_policy == SizePolicy::native && m_height_policy == SizePolicy::native) { 74 | m_width = m_native_width; 75 | m_height = m_native_height; 76 | return; 77 | } 78 | 79 | switch(m_width_policy) { 80 | case SizePolicy::expand: 81 | m_width = width_hint; 82 | break; 83 | case SizePolicy::relative: 84 | m_width = width_hint * m_rel_width; 85 | break; 86 | case SizePolicy::fixed: 87 | default: 88 | break; 89 | } 90 | 91 | switch(m_height_policy) { 92 | case SizePolicy::expand: 93 | m_height = height_hint; 94 | break; 95 | case SizePolicy::relative: 96 | m_height = height_hint * m_rel_height; 97 | break; 98 | case SizePolicy::native: 99 | m_height = m_width * m_native_height / m_native_width; 100 | break; 101 | case SizePolicy::fixed: 102 | default: 103 | break; 104 | } 105 | 106 | // can only do this calculation after height is set 107 | if (m_width_policy == SizePolicy::native) { 108 | m_width = m_height * m_native_width / m_native_height; 109 | } 110 | } 111 | 112 | // place box in internal coordinates used in enclosing box 113 | void place(Length x, Length y) { 114 | m_x = x; 115 | m_y = y; 116 | } 117 | 118 | // render into absolute coordinates, using the reference coordinates 119 | // from the enclosing box 120 | void render(Renderer &r, Length xref, Length yref) { 121 | Length x = m_x + xref; 122 | Length y = m_y + yref; 123 | 124 | // adjust for aspect ratio if necessary 125 | if (!m_respect_asp || (m_width/m_height == m_native_width/m_native_height)) { 126 | r.raster(m_image, x, y, m_width, m_height, m_interpolate, m_gp); 127 | } else { 128 | // do we need to adjust the height or the width of the image? 129 | if (m_height_policy == SizePolicy::native || 130 | (m_width/m_height > m_native_width/m_native_height && 131 | !(m_width_policy == SizePolicy::native))) { 132 | // adjust imate width if box is wider than image or native image height is requested 133 | Length width = m_height * m_native_width / m_native_height; 134 | Length xoff = (m_width - width)/2; 135 | r.raster(m_image, x + xoff, y, width, m_height, m_interpolate, m_gp); 136 | } else { 137 | // otherwise adjust image height 138 | Length height = m_width * m_native_height / m_native_width; 139 | Length yoff = (m_height - height)/2; 140 | r.raster(m_image, x, y + yoff, m_width, height, m_interpolate, m_gp); 141 | } 142 | } 143 | } 144 | }; 145 | 146 | #endif 147 | -------------------------------------------------------------------------------- /docs/LICENSE-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | License • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 |
    YEAR: 2020
    117 | COPYRIGHT HOLDER: Claus O. Wilke
    118 | 
    119 | 120 |
    121 | 122 | 127 | 128 |
    129 | 130 | 131 | 132 |
    133 | 136 | 137 |
    138 |

    Site built with pkgdown 1.6.0.

    139 |
    140 | 141 |
    142 |
    143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found (404) • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 | Content not found. Please use links in the navbar. 117 | 118 |
    119 | 120 | 125 | 126 |
    127 | 128 | 129 | 130 |
    131 | 134 | 135 |
    136 |

    Site built with pkgdown 1.6.0.

    137 |
    138 | 139 |
    140 |
    141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /docs/authors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Authors • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 |
      117 |
    • 118 |

      Claus O. Wilke. Author, maintainer. 119 |

      120 |
    • 121 |
    122 | 123 |
    124 | 125 |
    126 | 127 | 128 | 129 |
    130 | 133 | 134 |
    135 |

    Site built with pkgdown 1.6.0.

    136 |
    137 | 138 |
    139 |
    140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/grid.cpp: -------------------------------------------------------------------------------- 1 | #include "grid.h" 2 | 3 | NumericVector unit_pt(NumericVector x) { 4 | // create unit vector by calling back to R 5 | Environment env = Environment::namespace_env("grid"); 6 | Function unit = env["unit"]; 7 | return unit(x, "pt"); 8 | } 9 | 10 | NumericVector unit_pt(Length x) { 11 | NumericVector out(1, x); 12 | return unit_pt(out); 13 | } 14 | 15 | List gpar_empty() { 16 | List out; 17 | out.attr("class") = "gpar"; 18 | 19 | return out; 20 | } 21 | 22 | List text_grob(CharacterVector label, NumericVector x_pt, NumericVector y_pt, RObject gp, RObject name) { 23 | if (label.size() != 1 || x_pt.size() != 1 || y_pt.size() != 1) { 24 | stop("Function text_grob() is not vectorized.\n"); 25 | } 26 | 27 | if (gp.isNULL()) { 28 | gp = gpar_empty(); 29 | } 30 | 31 | // need to produce a unique name for each grob, otherwise grid gets grumpy 32 | static int tg_count = 0; 33 | if (name.isNULL()) { 34 | tg_count += 1; 35 | string s("gridtext.text."); 36 | s = s + to_string(tg_count); 37 | CharacterVector vs; 38 | vs.push_back(s); 39 | name = vs; 40 | } 41 | 42 | List out = List::create( 43 | _["label"] = label, _["x"] = unit_pt(x_pt), _["y"] = unit_pt(y_pt), _["just"] = "centre", 44 | _["hjust"] = 0., _["vjust"] = 0., _["rot"] = 0., 45 | _["check.overlap"] = false, _["name"] = name, _["gp"] = gp, _["vp"] = R_NilValue 46 | ); 47 | 48 | Rcpp::StringVector cl(3); 49 | cl(0) = "text"; 50 | cl(1) = "grob"; 51 | cl(2) = "gDesc"; 52 | 53 | out.attr("class") = cl; 54 | 55 | return out; 56 | } 57 | 58 | List raster_grob(RObject image, NumericVector x_pt, NumericVector y_pt, NumericVector width_pt, NumericVector height_pt, 59 | LogicalVector interpolate, RObject gp, RObject name) { 60 | if (x_pt.size() != 1 || y_pt.size() != 1 || width_pt.size() != 1 || height_pt.size() != 1) { 61 | stop("Function raster_grob() is not vectorized.\n"); 62 | } 63 | 64 | // need to produce a unique name for each grob, otherwise grid gets grumpy 65 | static int tg_count = 0; 66 | if (name.isNULL()) { 67 | tg_count += 1; 68 | string s("gridtext.raster."); 69 | s = s + to_string(tg_count); 70 | CharacterVector vs; 71 | vs.push_back(s); 72 | name = vs; 73 | } 74 | 75 | RObject raster = image; 76 | if (!raster.inherits("nativeRaster")) { 77 | // convert to raster by calling grDevices::as.raster() 78 | Environment env = Environment::namespace_env("grDevices"); 79 | Function as_raster = env["as.raster"]; 80 | raster = as_raster(image); 81 | } 82 | 83 | List out = List::create( 84 | _["raster"] = raster, 85 | _["x"] = unit_pt(x_pt), _["y"] = unit_pt(y_pt), 86 | _["width"] = unit_pt(width_pt), _["height"] = unit_pt(height_pt), 87 | _["just"] = "centre", _["hjust"] = 0., _["vjust"] = 0., 88 | _["interpolate"] = interpolate, 89 | _["name"] = name, _["gp"] = gp, _["vp"] = R_NilValue 90 | ); 91 | 92 | Rcpp::StringVector cl(3); 93 | cl(0) = "rastergrob"; 94 | cl(1) = "grob"; 95 | cl(2) = "gDesc"; 96 | 97 | out.attr("class") = cl; 98 | 99 | return out; 100 | } 101 | 102 | 103 | 104 | List rect_grob(NumericVector x_pt, NumericVector y_pt, NumericVector width_pt, NumericVector height_pt, 105 | RObject gp, RObject name) { 106 | if (x_pt.size() != 1 || y_pt.size() != 1 || width_pt.size() != 1 || height_pt.size() != 1) { 107 | stop("Function rect_grob() is not vectorized.\n"); 108 | } 109 | 110 | if (gp.isNULL()) { 111 | gp = gpar_empty(); 112 | } 113 | 114 | // need to produce a unique name for each grob, otherwise grid gets grumpy 115 | static int tg_count = 0; 116 | if (name.isNULL()) { 117 | tg_count += 1; 118 | string s("gridtext.rect."); 119 | s = s + to_string(tg_count); 120 | CharacterVector vs; 121 | vs.push_back(s); 122 | name = vs; 123 | } 124 | 125 | List out = List::create( 126 | _["x"] = unit_pt(x_pt), _["y"] = unit_pt(y_pt), 127 | _["width"] = unit_pt(width_pt), _["height"] = unit_pt(height_pt), 128 | _["just"] = "centre", _["hjust"] = 0., _["vjust"] = 0., 129 | _["name"] = name, _["gp"] = gp, _["vp"] = R_NilValue 130 | ); 131 | 132 | Rcpp::StringVector cl(3); 133 | cl(0) = "rect"; 134 | cl(1) = "grob"; 135 | cl(2) = "gDesc"; 136 | 137 | out.attr("class") = cl; 138 | 139 | return out; 140 | } 141 | 142 | List roundrect_grob(NumericVector x_pt, NumericVector y_pt, NumericVector width_pt, NumericVector height_pt, 143 | NumericVector r_pt, RObject gp, RObject name) { 144 | if (x_pt.size() != 1 || y_pt.size() != 1 || width_pt.size() != 1 || height_pt.size() != 1 || r_pt.size() != 1) { 145 | stop("Function roundrect_grob() is not vectorized.\n"); 146 | } 147 | 148 | if (gp.isNULL()) { 149 | gp = gpar_empty(); 150 | } 151 | 152 | // need to produce a unique name for each grob, otherwise grid gets grumpy 153 | static int tg_count = 0; 154 | if (name.isNULL()) { 155 | tg_count += 1; 156 | string s("gridtext.roundrect."); 157 | s = s + to_string(tg_count); 158 | CharacterVector vs; 159 | vs.push_back(s); 160 | name = vs; 161 | } 162 | 163 | NumericVector justv(2); // c(0, 0) 164 | 165 | List out = List::create( 166 | _["x"] = unit_pt(x_pt), _["y"] = unit_pt(y_pt), 167 | _["width"] = unit_pt(width_pt), _["height"] = unit_pt(height_pt), 168 | _["r"] = unit_pt(r_pt), 169 | _["just"] = justv, 170 | _["name"] = name, _["gp"] = gp, _["vp"] = R_NilValue 171 | ); 172 | 173 | Rcpp::StringVector cl(3); 174 | cl(0) = "roundrect"; 175 | cl(1) = "grob"; 176 | cl(2) = "gDesc"; 177 | 178 | out.attr("class") = cl; 179 | 180 | return out; 181 | } 182 | 183 | 184 | RObject set_grob_coords(RObject grob, NumericVector x, NumericVector y) { 185 | as(grob)["x"] = x; 186 | as(grob)["y"] = y; 187 | 188 | return grob; 189 | } 190 | -------------------------------------------------------------------------------- /R/process-tags.R: -------------------------------------------------------------------------------- 1 | process_text <- function(node, drawing_context) { 2 | tokens <- stringr::str_split(stringr::str_squish(node), "[[:space:]]+")[[1]] 3 | 4 | # make interior boxes 5 | boxes <- lapply(tokens, 6 | function(token) { 7 | list( 8 | bl_make_text_box(token, drawing_context$gp, drawing_context$yoff_pt), 9 | bl_make_regular_space_glue(drawing_context$gp) 10 | ) 11 | } 12 | ) 13 | 14 | # if node starts with space, add glue at beginning 15 | if (isTRUE(grepl("^[[:space:]]", node))) { 16 | boxes <- c(list(bl_make_regular_space_glue(drawing_context$gp)), boxes) 17 | } 18 | 19 | boxes <- unlist(boxes, recursive = FALSE) 20 | 21 | # if node doesn't end with space, remove glue at end 22 | if (!isTRUE(grepl("[[:space:]]$", node))) { 23 | boxes[[length(boxes)]] <- NULL 24 | } 25 | boxes 26 | } 27 | 28 | process_tag_b <- function(node, drawing_context) { 29 | attr <- attributes(node) 30 | drawing_context <- set_style(drawing_context, attr$style) 31 | 32 | process_tags(node, set_context_fontface(drawing_context, "bold")) 33 | } 34 | 35 | process_tag_br <- function(node, drawing_context) { 36 | list( 37 | bl_make_text_box("", drawing_context$gp), 38 | bl_make_forced_break_penalty() 39 | ) 40 | } 41 | 42 | process_tag_i <- function(node, drawing_context) { 43 | attr <- attributes(node) 44 | drawing_context <- set_style(drawing_context, attr$style) 45 | 46 | process_tags(node, set_context_fontface(drawing_context, "italic")) 47 | } 48 | 49 | process_tag_img <- function(node, drawing_context) { 50 | attr <- attributes(node) 51 | 52 | height <- attr$height 53 | if (is.null(height)) { 54 | height <- 0 55 | height_policy <- "native" 56 | } else { 57 | height <- as.numeric(height) 58 | height_policy <- "fixed" 59 | } 60 | 61 | width <- attr$width 62 | if (is.null(width)) { 63 | width <- 0 64 | width_policy <- "native" 65 | } else { 66 | width <- as.numeric(width) 67 | width_policy <- "fixed" 68 | } 69 | 70 | if (height_policy == "fixed" && width_policy == "fixed") { 71 | respect_asp <- FALSE 72 | } else { 73 | respect_asp <- TRUE 74 | } 75 | 76 | # read image 77 | img <- read_image(attr$src) 78 | 79 | # dpi = 72.27 turns lengths in pixels to lengths in pt 80 | rb <- bl_make_raster_box( 81 | img, width, height, width_policy, height_policy, 82 | respect_aspect = respect_asp, dpi = 72.27 83 | ) 84 | 85 | list(rb) 86 | } 87 | 88 | process_tag_p <- function(node, drawing_context) { 89 | attr <- attributes(node) 90 | drawing_context <- set_style(drawing_context, attr$style) 91 | 92 | boxes <- unlist( 93 | list( 94 | process_tags(node, drawing_context), 95 | process_tag_br(NULL, drawing_context) 96 | ), 97 | recursive = FALSE 98 | ) 99 | 100 | # word wrapping corresponds to width_policy = "relative". 101 | if (isTRUE(drawing_context$word_wrap)) { 102 | bl_make_par_box( 103 | boxes, drawing_context$linespacing_pt, width_policy = "relative", 104 | hjust = drawing_context$halign 105 | ) 106 | } else { 107 | bl_make_par_box( 108 | boxes, drawing_context$linespacing_pt, width_policy = "native", 109 | hjust = drawing_context$halign 110 | ) 111 | } 112 | } 113 | 114 | process_tag_span <- function(node, drawing_context) { 115 | attr <- attributes(node) 116 | drawing_context <- set_style(drawing_context, attr$style) 117 | 118 | process_tags(node, drawing_context) 119 | } 120 | 121 | process_tag_sup <- function(node, drawing_context) { 122 | # modify fontsize before processing style, to allow for manual overriding 123 | drawing_context <- set_context_gp(drawing_context, gpar(fontsize = 0.8*drawing_context$gp$fontsize)) 124 | attr <- attributes(node) 125 | drawing_context <- set_style(drawing_context, attr$style) 126 | 127 | # move drawing half a character above baseline 128 | drawing_context$yoff_pt <- drawing_context$yoff_pt + drawing_context$ascent_pt / 2 129 | process_tags(node, drawing_context) 130 | } 131 | 132 | process_tag_sub <- function(node, drawing_context) { 133 | # modify fontsize before processing style, to allow for manual overriding 134 | drawing_context <- set_context_gp(drawing_context, gpar(fontsize = 0.8*drawing_context$gp$fontsize)) 135 | attr <- attributes(node) 136 | drawing_context <- set_style(drawing_context, attr$style) 137 | 138 | # move drawing half a character below baseline 139 | drawing_context$yoff_pt <- drawing_context$yoff_pt - drawing_context$ascent_pt / 2 140 | process_tags(node, drawing_context) 141 | } 142 | 143 | dispatch_tag <- function(node, tag, drawing_context) { 144 | if (is.null(tag) || tag == "") { 145 | process_text(node, drawing_context) 146 | } else { 147 | switch( 148 | tag, 149 | "b" = process_tag_b(node, drawing_context), 150 | "strong" = process_tag_b(node, drawing_context), 151 | "br" = process_tag_br(node, drawing_context), 152 | "i" = process_tag_i(node, drawing_context), 153 | "img" = process_tag_img(node, drawing_context), 154 | "em" = process_tag_i(node, drawing_context), 155 | "p" = process_tag_p(node, drawing_context), 156 | "span" = process_tag_span(node, drawing_context), 157 | "sup" = process_tag_sup(node, drawing_context), 158 | "sub" = process_tag_sub(node, drawing_context), 159 | stop( 160 | paste0("gridtext has encountered a tag that isn't supported yet: <", tag, ">\n", 161 | "Only a very limited number of tags are currently supported."), 162 | call. = FALSE 163 | ) 164 | ) 165 | } 166 | } 167 | 168 | 169 | process_tags <- function(node, drawing_context) { 170 | tags <- names(node) 171 | boxes <- list() 172 | for (i in seq_along(node)) { 173 | boxes[[i]] <- dispatch_tag(node[[i]], tags[i], drawing_context) 174 | } 175 | unlist(boxes, recursive = FALSE) 176 | } 177 | 178 | -------------------------------------------------------------------------------- /tests/figs/richtext-grob/aligned-heights.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | Some 17 | text 18 | in 19 | bold. 20 | (centered) 21 | 22 | Linebreaks 23 | Linebreaks 24 | Linebreaks 25 | 26 | x 27 | 2 28 | + 29 | 5 30 | x 31 | + 32 | C 33 | i 34 | a 35 | = 36 | 5 37 | Box heights aligned, content centered 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/figs/richtext-grob/aligned-widths.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | Some 17 | text 18 | in 19 | bold. 20 | (centered) 21 | 22 | Linebreaks 23 | Linebreaks 24 | Linebreaks 25 | 26 | x 27 | 2 28 | + 29 | 5 30 | x 31 | + 32 | C 33 | i 34 | a 35 | = 36 | 5 37 | Box widths aligned, content centered 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/par-box.h: -------------------------------------------------------------------------------- 1 | #ifndef PAR_BOX_H 2 | #define PAR_BOX_H 3 | 4 | #include 5 | using namespace Rcpp; 6 | 7 | #include 8 | 9 | #include "grid.h" 10 | #include "layout.h" 11 | //#include "glue.h" 12 | //#include "penalty.h" 13 | #include "line-breaker.h" 14 | 15 | 16 | /* The ParBox class takes a list of boxes and lays them out 17 | * horizontally, breaking lines if necessary. The reference point 18 | * is the left end point of the baseline of the last line. 19 | */ 20 | 21 | template 22 | class ParBox : public Box { 23 | private: 24 | BoxList m_nodes; 25 | Length m_vspacing; 26 | Length m_width; 27 | Length m_ascent; 28 | Length m_descent; 29 | Length m_voff; 30 | SizePolicy m_width_policy; 31 | double m_hjust; // horizontal adjustment; can be used to override text adjustment 32 | bool m_use_hjust; // should text adjustment be overridden or not? 33 | // vertical shift if paragraph contains more than one line; is used to make sure the 34 | // bottom line in the box is used as the box baseline (all lines above are folded 35 | // into the ascent) 36 | Length m_multiline_shift; 37 | // calculated left baseline corner of the box after layouting 38 | Length m_x, m_y; 39 | 40 | public: 41 | ParBox(const BoxList& nodes, Length vspacing, SizePolicy width_policy = SizePolicy::native, 42 | double hjust = 0, bool use_hjust = false) : 43 | m_nodes(nodes), m_vspacing(vspacing), 44 | m_width(0), m_ascent(0), m_descent(0), m_voff(0), 45 | m_width_policy(width_policy), 46 | m_hjust(hjust), m_use_hjust(use_hjust), 47 | m_multiline_shift(0), m_x(0), m_y(0) { 48 | } 49 | ~ParBox() {}; 50 | 51 | Length width() { return m_width; } 52 | Length ascent() { return m_ascent; } 53 | Length descent() { return m_descent; } 54 | Length voff() { return m_voff; } 55 | 56 | void calc_layout(Length width_hint, Length height_hint) { 57 | // first make sure all child nodes are in a defined state 58 | // we propagate width and height hints to all child nodes, 59 | // in case they are useful there 60 | for (auto i_node = m_nodes.begin(); i_node != m_nodes.end(); i_node++) { 61 | (*i_node)->calc_layout(width_hint, height_hint); 62 | } 63 | 64 | // choose breaking parameters based on size policy 65 | bool word_wrap = true; 66 | if (m_width_policy == SizePolicy::native) { 67 | // for native policy, we don't wrap words and we allow lines to be arbitrarily long 68 | word_wrap = false; 69 | width_hint = Glue::infinity; 70 | } 71 | 72 | // calculate line breaks 73 | vector line_lengths = {width_hint}; 74 | LineBreaker lb(m_nodes, line_lengths, word_wrap); 75 | vector line_breaks; 76 | lb.compute_line_breaks(line_breaks); 77 | 78 | // now get the true line length for native size policy, 79 | // by finding the longest line 80 | if (m_width_policy == SizePolicy::native) { 81 | width_hint = 0; 82 | for (auto i_line = line_breaks.begin(); i_line != line_breaks.end(); i_line++) { 83 | if (width_hint < i_line->width) { 84 | width_hint = i_line->width; 85 | } 86 | } 87 | } 88 | 89 | // now place all nodes according to line breaks 90 | Length x_off = 0, y_off = 0; // x and y offset as we layout 91 | 92 | int lines = 0; 93 | Length first_ascent = 0; // ascent of the first line 94 | Length descent = 0; 95 | 96 | for (auto i_line = line_breaks.begin(); i_line != line_breaks.end(); i_line++) { 97 | // reset x_off for new line, potentially overriding alignment 98 | if (m_use_hjust) { 99 | x_off = m_hjust*(width_hint - i_line->width); 100 | } else { 101 | x_off = 0; 102 | } 103 | 104 | // we first get the ascent of each box in the line, to make sure there is 105 | // vertical space if some boxes are very tall 106 | Length ascent = 0; 107 | for (size_t i = i_line->start; i != i_line->end; i++) { 108 | auto node = m_nodes[i]; 109 | Length ascent_new = node->ascent() + node->voff(); 110 | if (ascent_new > ascent) { 111 | ascent = ascent_new; 112 | } 113 | } 114 | if (lines == 0) { // are we rendering the first line? 115 | // yes, record ascent for first line 116 | first_ascent = ascent; 117 | } else { 118 | // no, adjust y_offset as needed 119 | if (ascent + descent > m_vspacing) { 120 | y_off = y_off - (ascent + descent); 121 | } else { 122 | y_off = y_off - m_vspacing; 123 | } 124 | } 125 | 126 | // reset descent for new line 127 | descent = 0; 128 | 129 | // now loop over all boxes in each line and place 130 | for (size_t i = i_line->start; i != i_line->end; i++) { 131 | auto node = m_nodes[i]; 132 | node->place(x_off, y_off); 133 | x_off += node->width(); 134 | 135 | // record new descent 136 | Length descent_new = node->descent() - node->voff(); 137 | if (descent_new > descent) { 138 | descent = descent_new; 139 | } 140 | } 141 | 142 | // advance line 143 | lines += 1; 144 | } 145 | 146 | if (lines > 0) { // at least one line? 147 | m_multiline_shift = -1 * y_off; // multi-line boxes need to be shifted upwards 148 | m_ascent = first_ascent - y_off; 149 | m_descent = descent; 150 | m_width = width_hint; 151 | } else { 152 | m_multiline_shift = 0; 153 | m_ascent = 0; 154 | m_descent = 0; 155 | m_width = width_hint; 156 | } 157 | } 158 | 159 | void place(Length x, Length y) { 160 | m_x = x; 161 | m_y = y; 162 | } 163 | 164 | void render(Renderer &r, Length xref, Length yref) { 165 | // render all grobs in the list 166 | for (auto i_node = m_nodes.begin(); i_node != m_nodes.end(); i_node++) { 167 | (*i_node)->render(r, xref + m_x, yref + m_voff + m_y + m_multiline_shift); 168 | } 169 | } 170 | }; 171 | 172 | #endif 173 | -------------------------------------------------------------------------------- /tests/testthat/test-raster-box.R: -------------------------------------------------------------------------------- 1 | test_that("image dimensions are used", { 2 | logo_file <- system.file("extdata", "Rlogo.png", package = "gridtext") 3 | logo <- png::readPNG(logo_file, native = FALSE) 4 | 5 | # default size policy is native for both height and width 6 | # dpi = 72.27 turns lengths in pixels to lengths in pt 7 | rb <- bl_make_raster_box(logo, dpi = 72.27) 8 | bl_calc_layout(rb, 100, 100) 9 | bl_place(rb, 30, 5) 10 | g <- bl_render(rb, 10, 20) 11 | 12 | img <- g[[1]] 13 | expect_identical(img$x, unit(40, "pt")) 14 | expect_identical(img$y, unit(25, "pt")) 15 | expect_equal(img$width, unit(ncol(logo), "pt")) 16 | expect_equal(img$height, unit(nrow(logo), "pt")) 17 | 18 | # test now with raster object 19 | logo2 <- as.raster(logo) 20 | rb <- bl_make_raster_box(logo2, dpi = 72.27) 21 | bl_calc_layout(rb, 100, 100) 22 | g <- bl_render(rb, 10, 20) 23 | 24 | img <- g[[1]] 25 | expect_identical(img$x, unit(10, "pt")) 26 | expect_identical(img$y, unit(20, "pt")) 27 | expect_equal(img$width, unit(ncol(logo), "pt")) 28 | expect_equal(img$height, unit(nrow(logo), "pt")) 29 | 30 | # test now with nativeRaster object 31 | logo3 <- png::readPNG(logo_file, native = TRUE) 32 | rb <- bl_make_raster_box(logo3, dpi = 72.27) 33 | bl_calc_layout(rb, 100, 100) 34 | g <- bl_render(rb, 10, 20) 35 | 36 | img <- g[[1]] 37 | expect_identical(img$x, unit(10, "pt")) 38 | expect_identical(img$y, unit(20, "pt")) 39 | expect_equal(img$width, unit(ncol(logo), "pt")) 40 | expect_equal(img$height, unit(nrow(logo), "pt")) 41 | 42 | # dimensions are reported correctly 43 | expect_equal(bl_box_width(rb), ncol(logo)) 44 | expect_equal(bl_box_height(rb), nrow(logo)) 45 | expect_equal(bl_box_ascent(rb), nrow(logo)) 46 | expect_identical(bl_box_descent(rb), 0) 47 | expect_identical(bl_box_voff(rb), 0) 48 | 49 | m <- 1:10 50 | dim(m) <- 10 51 | 52 | expect_error( 53 | bl_make_raster_box(m), 54 | "Cannot extract image dimensions." 55 | ) 56 | }) 57 | 58 | 59 | test_that("size policies, respect_aspect = FALSE", { 60 | logo_file <- system.file("extdata", "Rlogo.png", package = "gridtext") 61 | logo <- png::readPNG(logo_file, native = TRUE) 62 | 63 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 64 | width_policy = "fixed", height_policy = "fixed", 65 | respect_aspect = FALSE) 66 | bl_calc_layout(rb, 200, 100) 67 | g <- bl_render(rb, 10, 20) 68 | 69 | img <- g[[1]] 70 | expect_identical(img$x, unit(10, "pt")) 71 | expect_identical(img$y, unit(20, "pt")) 72 | expect_identical(img$width, unit(50, "pt")) 73 | expect_identical(img$height, unit(80, "pt")) 74 | 75 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 76 | width_policy = "relative", height_policy = "expand", 77 | respect_aspect = FALSE) 78 | bl_calc_layout(rb, 200, 100) 79 | g <- bl_render(rb, 10, 20) 80 | 81 | img <- g[[1]] 82 | expect_identical(img$x, unit(10, "pt")) 83 | expect_identical(img$y, unit(20, "pt")) 84 | expect_identical(img$width, unit(100, "pt")) 85 | expect_identical(img$height, unit(100, "pt")) 86 | 87 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 88 | width_policy = "expand", height_policy = "relative", 89 | respect_aspect = FALSE) 90 | bl_calc_layout(rb, 200, 100) 91 | g <- bl_render(rb, 10, 20) 92 | 93 | img <- g[[1]] 94 | expect_identical(img$x, unit(10, "pt")) 95 | expect_identical(img$y, unit(20, "pt")) 96 | expect_identical(img$width, unit(200, "pt")) 97 | expect_identical(img$height, unit(80, "pt")) 98 | 99 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 100 | width_policy = "fixed", height_policy = "native", 101 | respect_aspect = FALSE) 102 | bl_calc_layout(rb, 200, 100) 103 | g <- bl_render(rb, 10, 20) 104 | 105 | img <- g[[1]] 106 | expect_identical(img$x, unit(10, "pt")) 107 | expect_identical(img$y, unit(20, "pt")) 108 | expect_identical(img$width, unit(50, "pt")) 109 | expect_equal(img$height, unit(50*nrow(logo)/ncol(logo), "pt")) 110 | 111 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 112 | width_policy = "native", height_policy = "fixed", 113 | respect_aspect = FALSE) 114 | bl_calc_layout(rb, 200, 100) 115 | g <- bl_render(rb, 10, 20) 116 | 117 | img <- g[[1]] 118 | expect_identical(img$x, unit(10, "pt")) 119 | expect_identical(img$y, unit(20, "pt")) 120 | expect_equal(img$width, unit(80*ncol(logo)/nrow(logo), "pt")) 121 | expect_identical(img$height, unit(80, "pt")) 122 | }) 123 | 124 | 125 | test_that("size policies, respect_aspect = TRUE", { 126 | logo_file <- system.file("extdata", "Rlogo.png", package = "gridtext") 127 | logo <- png::readPNG(logo_file, native = TRUE) 128 | 129 | rb <- bl_make_raster_box(logo, width = 50, height = 80, 130 | width_policy = "fixed", height_policy = "fixed") 131 | bl_calc_layout(rb, 200, 100) 132 | g <- bl_render(rb, 10, 20) 133 | 134 | nr <- nrow(logo) 135 | nc <- ncol(logo) 136 | img_height <- 50*nr/nc 137 | yoff <- (80 - img_height)/2 138 | 139 | img <- g[[1]] 140 | expect_identical(img$x, unit(10, "pt")) 141 | expect_equal(img$y, unit(20 + yoff, "pt")) 142 | expect_identical(img$width, unit(50, "pt")) 143 | expect_equal(img$height, unit(img_height, "pt")) 144 | 145 | rb <- bl_make_raster_box(logo, width = 80, height = 50, 146 | width_policy = "fixed", height_policy = "fixed") 147 | bl_calc_layout(rb, 200, 100) 148 | g <- bl_render(rb, 10, 20) 149 | 150 | nr <- nrow(logo) 151 | nc <- ncol(logo) 152 | img_width <- 50*nc/nr 153 | xoff <- (80 - img_width)/2 154 | 155 | img <- g[[1]] 156 | expect_equal(img$x, unit(10 + xoff, "pt")) 157 | expect_identical(img$y, unit(20, "pt")) 158 | expect_equal(img$width, unit(img_width, "pt")) 159 | expect_identical(img$height, unit(50, "pt")) 160 | }) 161 | -------------------------------------------------------------------------------- /tests/figs/richtext-grob/various-text-boxes-w-debug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | Some 18 | text 19 | in 20 | bold. 21 | (centered) 22 | 23 | Linebreaks 24 | Linebreaks 25 | Linebreaks 26 | 27 | x 28 | 2 29 | + 30 | 5 31 | x 32 | + 33 | C 34 | i 35 | a 36 | = 37 | 5 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/figs/textbox-grob/multiple-boxes-internal-alignment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | The 17 | quick 18 | brown 19 | fox 20 | jumps 21 | over 22 | the 23 | lazy 24 | dog. 25 | 26 | The 27 | quick 28 | brown 29 | fox 30 | jumps 31 | over 32 | the 33 | lazy 34 | dog. 35 | 36 | The 37 | quick 38 | brown 39 | fox 40 | jumps 41 | over 42 | the 43 | lazy 44 | dog. 45 | 46 | The 47 | quick 48 | brown 49 | fox 50 | jumps 51 | over 52 | the 53 | lazy 54 | dog. 55 | 56 | -------------------------------------------------------------------------------- /docs/reference/gridtext.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Improved text rendering support for grid graphics — gridtext • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
    65 |
    66 | 108 | 109 | 110 | 111 |
    112 | 113 |
    114 |
    115 | 120 | 121 |
    122 |

    The gridtext package provides two new grobs, richtext_grob() and 123 | textbox_grob(), which support drawing of formatted text labels and 124 | formatted text boxes, respectively.

    125 |
    126 | 127 | 128 | 129 | 130 |
    131 | 136 |
    137 | 138 | 139 |
    140 | 143 | 144 |
    145 |

    Site built with pkgdown 1.6.0.

    146 |
    147 | 148 |
    149 |
    150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NA • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 | 117 |

    Issues are meant to report bugs or request features. If you have questions about how to correctly use this package, please don’t use the issue system. Such questions can be asked on stackoverflow https://stackoverflow.com/ or the RStudio community https://community.rstudio.com/. If you are not sure where to go, please try https://stackoverflow.com/ first.

    118 |

    Issues must contain reproducible code examples. Please use the reprex package to create your example (see here: http://reprex.tidyverse.org/). Issues without reprex may be closed without comment.

    119 |

    Please delete these instructions after you have read them.

    120 |
    121 |

    Brief description of the problem or desired feature.

    122 |
    # insert reprex here
    123 | 124 | 125 |
    126 | 127 | 132 | 133 |
    134 | 135 | 136 | 137 |
    138 | 141 | 142 |
    143 |

    Site built with pkgdown 1.6.0.

    144 |
    145 | 146 |
    147 |
    148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /tests/testthat/test-grid-constructors.R: -------------------------------------------------------------------------------- 1 | test_that("unit_pt", { 2 | expect_equal( 3 | unit_pt(10), 4 | grid::unit(10, "pt") 5 | ) 6 | 7 | expect_identical( 8 | unit_pt(1:10), 9 | grid::unit(1:10, "pt") 10 | ) 11 | }) 12 | 13 | test_that("gpar_empty", { 14 | expect_identical( 15 | gpar_empty(), 16 | grid::gpar() 17 | ) 18 | }) 19 | 20 | test_that("text_grob", { 21 | # basic functionality, gp is set to gpar() if not provided 22 | expect_identical( 23 | text_grob("test", 10, 20, name = "abc"), 24 | textGrob( 25 | "test", 26 | x = unit(10, "pt"), y = unit(20, "pt"), 27 | hjust = 0, vjust = 0, 28 | gp = gpar(), 29 | name = "abc" 30 | ) 31 | ) 32 | 33 | # basic functionality, x and y are set to 0 if not provided 34 | expect_identical( 35 | text_grob("test", name = "abc"), 36 | textGrob( 37 | "test", 38 | x = unit(0, "pt"), y = unit(0, "pt"), 39 | hjust = 0, vjust = 0, 40 | gp = gpar(), 41 | name = "abc" 42 | ) 43 | ) 44 | 45 | # gp is set as requested 46 | gp <- gpar(col = "blue", fill = "red") 47 | expect_identical( 48 | text_grob("test", 10, 20, gp = gp, name = "abc"), 49 | textGrob( 50 | "test", 51 | x = unit(10, "pt"), y = unit(20, "pt"), 52 | hjust = 0, vjust = 0, 53 | gp = gp, 54 | name = "abc" 55 | ) 56 | ) 57 | 58 | # if no name is provided, different names are assigned 59 | g1 <- text_grob("test") 60 | g2 <- text_grob("test") 61 | expect_false(identical(g1$name, g2$name)) 62 | 63 | # function is not vectorized 64 | expect_error( 65 | text_grob(c("test", "test"), 10, 20), 66 | "not vectorized" 67 | ) 68 | 69 | expect_error( 70 | text_grob("test", 1:5, 20), 71 | "not vectorized" 72 | ) 73 | 74 | expect_error( 75 | text_grob("test", 10, 1:5), 76 | "not vectorized" 77 | ) 78 | 79 | # arguments of length 0 are also disallowed 80 | expect_error( 81 | text_grob("test", numeric(0), 5), 82 | "not vectorized" 83 | ) 84 | }) 85 | 86 | test_that("raster_grob", { 87 | # basic functionality 88 | image <- matrix(0:1, ncol = 5, nrow = 4) 89 | 90 | expect_identical( 91 | raster_grob(image, 10, 20, 50, 40, gp = gpar(), name = "abc"), 92 | rasterGrob( 93 | image, 94 | x = unit(10, "pt"), y = unit(20, "pt"), 95 | width = unit(50, "pt"), height = unit(40, "pt"), 96 | hjust = 0, vjust = 0, 97 | interpolate = TRUE, 98 | gp = gpar(), 99 | name = "abc" 100 | ) 101 | ) 102 | 103 | # interpolate is set as requested, gp default is NULL 104 | expect_identical( 105 | raster_grob(image, 10, 20, 50, 40, interpolate = FALSE, name = "abc"), 106 | rasterGrob( 107 | image, 108 | x = unit(10, "pt"), y = unit(20, "pt"), 109 | width = unit(50, "pt"), height = unit(40, "pt"), 110 | hjust = 0, vjust = 0, 111 | interpolate = FALSE, 112 | gp = NULL, 113 | name = "abc" 114 | ) 115 | ) 116 | 117 | # if no name is provided, different names are assigned 118 | g1 <- raster_grob(image) 119 | g2 <- raster_grob(image) 120 | expect_false(identical(g1$name, g2$name)) 121 | 122 | # function is not vectorized 123 | expect_error( 124 | raster_grob(image, c(10, 20), 20, 100, 140), 125 | "not vectorized" 126 | ) 127 | 128 | expect_error( 129 | raster_grob(image, 10, numeric(0), 100, 140), 130 | "not vectorized" 131 | ) 132 | }) 133 | 134 | 135 | test_that("rect_grob", { 136 | # basic functionality, gp is set to gpar() if not provided 137 | expect_identical( 138 | rect_grob(10, 20, 100, 140, name = "abc"), 139 | rectGrob( 140 | x = unit(10, "pt"), y = unit(20, "pt"), 141 | width = unit(100, "pt"), height = unit(140, "pt"), 142 | hjust = 0, vjust = 0, 143 | gp = gpar(), 144 | name = "abc" 145 | ) 146 | ) 147 | 148 | # gp is set as requested 149 | gp <- gpar(col = "blue", fill = "red") 150 | expect_identical( 151 | rect_grob(10, 20, 100, 140, gp = gp, name = "abc"), 152 | rectGrob( 153 | x = unit(10, "pt"), y = unit(20, "pt"), 154 | width = unit(100, "pt"), height = unit(140, "pt"), 155 | hjust = 0, vjust = 0, 156 | gp = gp, 157 | name = "abc" 158 | ) 159 | ) 160 | 161 | # if no name is provided, different names are assigned 162 | g1 <- rect_grob() 163 | g2 <- rect_grob() 164 | expect_false(identical(g1$name, g2$name)) 165 | 166 | # function is not vectorized 167 | expect_error( 168 | rect_grob(c(10, 20), 20, 100, 140), 169 | "not vectorized" 170 | ) 171 | 172 | expect_error( 173 | rect_grob(10, numeric(0), 100, 140), 174 | "not vectorized" 175 | ) 176 | }) 177 | 178 | test_that("roundrect_grob", { 179 | # basic functionality, gp is set to gpar() if not provided 180 | expect_identical( 181 | roundrect_grob(10, 20, 100, 140, 10, name = "abc"), 182 | roundrectGrob( 183 | x = unit(10, "pt"), y = unit(20, "pt"), 184 | width = unit(100, "pt"), height = unit(140, "pt"), 185 | r = unit(10, "pt"), 186 | just = c(0, 0), 187 | gp = gpar(), 188 | name = "abc" 189 | ) 190 | ) 191 | 192 | # gp is set as requested 193 | gp <- gpar(col = "blue", fill = "red") 194 | expect_identical( 195 | roundrect_grob(10, 20, 100, 140, 20, gp = gp, name = "abc"), 196 | roundrectGrob( 197 | x = unit(10, "pt"), y = unit(20, "pt"), 198 | width = unit(100, "pt"), height = unit(140, "pt"), 199 | r = unit(20, "pt"), 200 | just = c(0, 0), 201 | gp = gp, 202 | name = "abc" 203 | ) 204 | ) 205 | 206 | # if no name is provided, different names are assigned 207 | g1 <- roundrect_grob() 208 | g2 <- roundrect_grob() 209 | expect_false(identical(g1$name, g2$name)) 210 | 211 | # function is not vectorized 212 | expect_error( 213 | roundrect_grob(c(10, 20), 20, 100, 140, 20), 214 | "not vectorized" 215 | ) 216 | 217 | expect_error( 218 | roundrect_grob(10, numeric(0), 100, 140, 20), 219 | "not vectorized" 220 | ) 221 | }) 222 | 223 | 224 | test_that("set_grob_coords", { 225 | g <- list(x = 0, y = 0) 226 | 227 | # setting coords as numbers 228 | expect_identical( 229 | set_grob_coords(g, x = 20, y = 40), 230 | list(x = 20, y = 40) 231 | ) 232 | 233 | # setting coords as units 234 | expect_identical( 235 | set_grob_coords(g, x = unit_pt(20), y = unit_pt(40)), 236 | list(x = unit_pt(20), y = unit_pt(40)) 237 | ) 238 | }) 239 | -------------------------------------------------------------------------------- /docs/LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | MIT License • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 |
    117 | 118 |

    Copyright (c) 2019 Claus O. Wilke

    119 |

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    120 |

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    121 |

    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    122 |
    123 | 124 |
    125 | 126 | 131 | 132 |
    133 | 134 | 135 | 136 |
    137 | 140 | 141 |
    142 |

    Site built with pkgdown 1.6.0.

    143 |
    144 | 145 |
    146 |
    147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /docs/reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Function reference • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 141 | 142 | 143 | 144 | 147 | 148 | 149 | 150 |
    127 |

    Formatted text labels and text boxes

    128 |

    The gridtext package provides two grobs that can draw formatted text, without and with word wrapping.

    129 |
    139 |

    richtext_grob()

    140 |

    Draw formatted text labels

    145 |

    textbox_grob()

    146 |

    Draw formatted multi-line text with word wrap

    151 |
    152 | 153 | 158 |
    159 | 160 | 161 |
    162 | 165 | 166 |
    167 |

    Site built with pkgdown 1.6.0.

    168 |
    169 | 170 |
    171 |
    172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /tests/figs/richtext-grob/various-text-boxes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | Various 16 | text 17 | boxes 18 | in 19 | different 20 | stylings 21 | 22 | Some 23 | text 24 | in 25 | bold. 26 | (centered) 27 | 28 | Linebreaks 29 | Linebreaks 30 | Linebreaks 31 | 32 | x 33 | 2 34 | + 35 | 5 36 | x 37 | + 38 | C 39 | i 40 | a 41 | = 42 | 5 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/news/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Changelog • gridtext 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 116 | 117 |
    118 |

    119 | gridtext 0.1.3 Unreleased 120 |

    121 |
      122 |
    • Remove unneeded systemfonts dependency.
    • 123 |
    124 |
    125 |
    126 |

    127 | gridtext 0.1.2 2020-12-01 128 |

    129 |
      130 |
    • Fix build for testthat 3.0.
    • 131 |
    132 |
    133 |
    134 |

    135 | gridtext 0.1.1 2020-02-24 136 |

    137 | 141 |
    142 |
    143 |

    144 | gridtext 0.1.0 2020-01-24 145 |

    146 |

    First public release. Provides the two grobs richtext_grob() and textbox_grob() for formatted text rendering without and with word wrapping, respectively.

    147 |
    148 |
    149 | 150 | 155 | 156 |
    157 | 158 | 159 |
    160 | 163 | 164 |
    165 |

    Site built with pkgdown 1.6.0.

    166 |
    167 | 168 |
    169 |
    170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /tests/figs/textbox-grob/box-spanning-entire-viewport-with-margins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | The 17 | quick 18 | brown 19 | fox 20 | jumps 21 | over 22 | the 23 | lazy 24 | dog. 25 | The 26 | quick 27 | brown 28 | fox 29 | jumps 30 | over 31 | the 32 | lazy 33 | dog. 34 | The 35 | quick 36 | brown 37 | fox 38 | jumps 39 | over 40 | the 41 | lazy 42 | dog. 43 | The 44 | quick 45 | brown 46 | fox 47 | jumps 48 | over 49 | the 50 | lazy 51 | dog. 52 | Box spanning entire viewport, with margins 53 | 54 | 55 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, echo = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-" 12 | ) 13 | ``` 14 | 15 | # gridtext 16 | 17 | 18 | [![R build 19 | status](https://github.com/wilkelab/gridtext/workflows/R-CMD-check/badge.svg)](https://github.com/wilkelab/gridtext/actions) 20 | [![Coverage Status](https://img.shields.io/codecov/c/github/wilkelab/gridtext/master.svg)](https://codecov.io/github/wilkelab/gridtext?branch=master) 21 | [![CRAN status](https://www.r-pkg.org/badges/version/gridtext)](https://cran.r-project.org/package=gridtext) 22 | [![Lifecycle: maturing](https://img.shields.io/badge/lifecycle-maturing-blue.svg)](https://lifecycle.r-lib.org/articles/stages.html#maturing) 23 | 24 | 25 | Improved text rendering support for grid graphics in R. 26 | 27 | 28 | ## Installation 29 | 30 | You can install the current release from CRAN with `install.packages()`: 31 | 32 | ```{r eval = FALSE} 33 | install.packages("gridtext") 34 | ``` 35 | 36 | 37 | To install the latest development version of this package, please run the following line in your R console: 38 | 39 | ```{r eval = FALSE} 40 | remotes::install_github("wilkelab/gridtext") 41 | ``` 42 | 43 | 44 | ## Examples 45 | 46 | The gridtext package provides two new grobs, `richtext_grob()` and `textbox_grob()`, which support drawing of formatted text labels and formatted text boxes, respectively. Both grobs understand an extremely limited subset of Markdown, HTML, and CSS directives. The idea is to provide a minimally useful subset of features. These currently include italics, bold, super- and subscript, as well as changing text color, font, and font size via inline CSS. Extremely limited support for images is also provided. 47 | 48 | Note that all text rendering is performed through a custom-built rendering pipeline that is part of the gridtext package. This approach has several advantages, including minimal dependencies, good performance, and compatibility with all R graphics devices (to the extent that the graphics devices support the fonts you want to use). The downside of this approach is the severely limited feature set. Don't expect this package to support the fancy CSS and javascript tricks you're used to when designing web pages. 49 | 50 | ### Richtext grob 51 | 52 | The function `richtext_grob()` serves as a replacement for `textGrob()`. It is vectorized and can draw multiple text labels with one call. Labels can be drawn with padding, margins, and at arbitrary angles. Markdown and HTML parsing is turned on by default. 53 | 54 | ```{r fig.width = 6, fig.height = 4} 55 | library(grid) 56 | library(gridtext) 57 | 58 | text <- c( 59 | "Some text **in bold.**", 60 | "Linebreaks
    Linebreaks
    Linebreaks", 61 | "*x*2 + 5*x* + *C**i*", 62 | "Some blue text **in bold.**
    And *italics text.*
    And some large text." 63 | ) 64 | 65 | x <- c(.2, .1, .7, .9) 66 | y <- c(.8, .4, .1, .5) 67 | rot <- c(0, 0, 45, -45) 68 | gp = gpar( 69 | col = c("black", "red"), 70 | fontfamily = c("Palatino", "Courier", "Times", "Helvetica") 71 | ) 72 | box_gp = gpar( 73 | col = "black", fill = c(NA, "cornsilk", "lightblue1", NA), 74 | lty = c(0, 1, 1, 1) 75 | ) 76 | hjust <- c(0.5, 0, 0, 1) 77 | vjust <- c(0.5, 1, 0, 0.5) 78 | 79 | grid.newpage() 80 | g <- richtext_grob( 81 | text, x, y, hjust = hjust, vjust = vjust, rot = rot, 82 | padding = unit(c(6, 6, 4, 6), "pt"), 83 | r = unit(c(0, 2, 4, 8), "pt"), 84 | gp = gp, box_gp = box_gp 85 | ) 86 | grid.draw(g) 87 | grid.points(x, y, default.units = "npc", pch = 19, size = unit(5, "pt")) 88 | ``` 89 | 90 | The boxes around text labels can be set to have matching widths and/or heights, and alignment of text inside the box (specified via `hjust` and `vjust`) is separate from alignment of the box relative to a reference point (specified via `box_hjust` and `box_vjust`). 91 | 92 | ```{r} 93 | text <- c("January", "February", "March", "April", "May") 94 | x <- (1:5)/6 + 1/24 95 | y <- rep(0.8, 5) 96 | g <- richtext_grob( 97 | text, x, y, halign = 0, hjust = 1, 98 | rot = 45, 99 | padding = unit(c(3, 6, 1, 3), "pt"), 100 | r = unit(4, "pt"), 101 | align_widths = TRUE, 102 | box_gp = gpar(col = "black", fill = "cornsilk") 103 | ) 104 | grid.newpage() 105 | grid.draw(g) 106 | grid.points(x, y, default.units = "npc", pch = 19, size = unit(5, "pt")) 107 | ``` 108 | 109 | Basic support for images is available as well. As of now, images will always be vertically aligned with the baseline of the text. 110 | ```{r fig.width = 6, fig.height = 4} 111 | grid.newpage() 112 | 113 | img_src <- system.file("extdata", "Rlogo.png", package = "gridtext") 114 | text <- glue::glue("Image with native aspect ratio: And some more text.") 115 | grid.draw(richtext_grob(text, x = 0.9, y = 0.7, hjust = 1)) 116 | 117 | text <- glue::glue("Image with forced size: And some more text.") 118 | grid.draw(richtext_grob(text, x = 0.9, y = 0.3, hjust = 1)) 119 | 120 | ``` 121 | 122 | 123 | ### Textbox grob 124 | 125 | The function `textbox_grob()` is intended to render multi-line text labels that require automatic word wrapping. It is similar to `richtext_grob()`, but there are a few important differences. First, while `richtext_grob()` is vectorized, `textbox_grob()` is not. It can draw only a single text box at a time. Second, `textbox_grob()` doesn't support rendering the text box at arbitrary angles. Only four different orientations are supported, corresponding to a rotation by 0, 90, 180, and 270 degrees. 126 | 127 | ```{r} 128 | g <- textbox_grob( 129 | "**The quick brown fox jumps over the lazy dog.**

    130 | The quick brown fox jumps over the lazy dog. 131 | The **quick brown fox** jumps over the lazy dog. 132 | The quick brown fox jumps over the lazy dog.", 133 | x = unit(0.5, "npc"), y = unit(0.7, "npc"), 134 | gp = gpar(fontsize = 15), 135 | box_gp = gpar(col = "black", fill = "lightcyan1"), 136 | r = unit(5, "pt"), 137 | padding = unit(c(10, 10, 10, 10), "pt"), 138 | margin = unit(c(0, 10, 0, 10), "pt") 139 | ) 140 | grid.newpage() 141 | grid.draw(g) 142 | ``` 143 | 144 | The alignment parameters `hjust`, `vjust`, `halign`, and `valign` function just like they do in `richtext_grob()`. 145 | 146 | ```{r} 147 | g <- textbox_grob( 148 | "**The quick brown fox jumps over the lazy dog.**

    149 | The quick brown fox jumps over the lazy dog. 150 | The **quick brown fox** jumps over the lazy dog. 151 | The quick brown fox jumps over the lazy dog.", 152 | x = unit(0.2, "npc"), y = unit(0.5, "npc"), 153 | hjust = 0.5, vjust = 1, halign = 1, 154 | gp = gpar(fontsize = 15), 155 | box_gp = gpar(col = "black", fill = "lightcyan1"), 156 | r = unit(5, "pt"), 157 | padding = unit(c(10, 10, 10, 10), "pt"), 158 | margin = unit(c(0, 10, 0, 10), "pt"), 159 | orientation = "left-rotated" 160 | ) 161 | grid.newpage() 162 | grid.draw(g) 163 | ``` 164 | 165 | ## Acknowledgments 166 | 167 | This project received [financial support](https://www.r-consortium.org/all-projects/awarded-projects) from the [R consortium.](https://www.r-consortium.org) 168 | 169 | 170 | 171 | --------------------------------------------------------------------------------