{"id":13711056,"url":"https://mjskay.github.io/ggblend/","last_synced_at":"2025-05-06T20:31:45.323Z","repository":{"id":40306719,"uuid":"488832644","full_name":"mjskay/ggblend","owner":"mjskay","description":"Support for blend modes in ggplot2","archived":false,"fork":false,"pushed_at":"2023-05-23T20:01:11.000Z","size":26128,"stargazers_count":161,"open_issues_count":5,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-05-01T02:03:00.767Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://mjskay.github.io/ggblend/","language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mjskay.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-05-05T04:36:27.000Z","updated_at":"2024-04-27T21:10:05.000Z","dependencies_parsed_at":"2024-01-13T07:25:57.048Z","dependency_job_id":"b682c8ea-b776-4cd3-8137-fb3c83a4f371","html_url":"https://github.com/mjskay/ggblend","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjskay%2Fggblend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjskay%2Fggblend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjskay%2Fggblend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjskay%2Fggblend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mjskay","download_url":"https://codeload.github.com/mjskay/ggblend/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224219726,"owners_count":17275477,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T23:01:03.986Z","updated_at":"2024-11-13T21:31:27.690Z","avatar_url":"https://github.com/mjskay.png","language":"R","funding_links":[],"categories":["Plot layers"],"sub_categories":[],"readme":"---\noutput: github_document\n---\n\n```{r, include = FALSE}\nknitr::opts_chunk$set(\n  fig.path = \"man/figures/README-\",\n  dev.args = list(png = list(type = \"cairo\")),\n  fig.retina = 2\n)\n```\n\n# ggblend: Blending and compositing algebra for ggplot2\n\n\u003c!-- badges: start --\u003e\n[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)\n[![CRAN status](https://www.r-pkg.org/badges/version/ggblend)](https://CRAN.R-project.org/package=ggblend)\n[![Codecov test coverage](https://codecov.io/gh/mjskay/ggblend/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mjskay/ggblend?branch=main)\n[![R-CMD-check](https://github.com/mjskay/ggblend/workflows/R-CMD-check/badge.svg)](https://github.com/mjskay/ggblend/actions)\n[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7963886.svg)](https://doi.org/10.5281/zenodo.7963886)\n\u003c!-- badges: end --\u003e\n\n*ggblend* is a small algebra of operations for blending, copying, adjusting, and \ncompositing layers in *ggplot2*. It allows you to easily copy and adjust the \naesthetics or parameters of an existing layer, to partition a layer into multiple\npieces for re-composition, and to combine layers (or partitions of layers) using\nblend modes (like `\"multiply\"`, `\"overlay\"`, etc). \n\n*ggblend* requires R ≥ 4.2, as blending and compositing support was added in that \nversion of R.\n\n## Installation\n\nYou can install *ggblend* from CRAN as follows:\n\n```r\ninstall.packages(\"ggblend\")\n```\n\nYou can install the development version of *ggblend* using:\n\n```r\nremotes::install_github(\"mjskay/ggblend\")\n```\n\n## Blending within one geometry\n\nWe'll construct a simple dataset with two semi-overlapping point clouds. We'll\nhave two versions of the dataset: one with all the `\"a\"` points listed first,\nand one with all the `\"b\"` points listed first.\n\n```{r data, message=FALSE, warning=FALSE}\nlibrary(ggplot2)\nlibrary(ggblend)\ntheme_set(ggdist::theme_ggdist() + theme(\n  plot.title = element_text(size = rel(1), lineheight = 1.1, face = \"bold\"),\n  plot.subtitle = element_text(face = \"italic\"),\n  panel.border = element_rect(color = \"gray75\", fill = NA)\n))\n\nset.seed(1234)\ndf_a = data.frame(x = rnorm(500, 0), y = rnorm(500, 1), set = \"a\")\ndf_b = data.frame(x = rnorm(500, 1), y = rnorm(500, 2), set = \"b\")\n\ndf_ab = rbind(df_a, df_b) |\u003e\n  transform(order = \"draw a then b\")\n\ndf_ba = rbind(df_b, df_a) |\u003e\n  transform(order = \"draw b then a\")\n\ndf = rbind(df_ab, df_ba)\n```\n\nA typical scatterplot of such data suffers from the problem that how many \npoints appear to be in each group depends on the drawing order (*a then b* \nversus *b then a*):\n\n```{r scatter_noblend}\ndf |\u003e\n  ggplot(aes(x, y, color = set)) +\n  geom_point(size = 3, alpha = 0.5) +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(title = \"geom_point() without blending\", subtitle = \"Draw order matters.\")\n```\n\nA *commutative* blend mode, like `\"multiply\"` or `\"darken\"`, is one potential\nsolution that does not depend on drawing order. We can apply a `blend()` \noperation to geom_point()` to achieve this. There three ways to do this:\n\n- `blend(geom_point(...), \"multiply\")` (normal function application)\n- `geom_point(...) |\u003e blend(\"multiply\")` (piping)\n- `geom_point(...) * blend(\"multiply\")` (algebraic operations)\n\nFunction application and piping are equivalent. **In this case**, all three\napproaches are equivalent. As we will see later, the multiplication approach\nis useful when we want a shorthand for applying the same operation to multiple\nlayers in a list without combining those layers first (in other words, \nmultiplication of operations over layers is *distributive* in an algebraic \nsense).\n\n```{r scatter_blend}\ndf |\u003e\n  ggplot(aes(x, y, color = set)) +\n  geom_point(size = 3, alpha = 0.5) |\u003e blend(\"multiply\") +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(\n    title = \"geom_point(alpha = 0.5) |\u003e blend('multiply')\",\n    subtitle = \"Draw order does not matter, but color is too dark.\"\n  )\n```\n\nNow the output is identical no matter the draw order, although the output is quite dark.\n\n## Partitioning layers\n\nPart of the reason the output is very dark above is that all of the points are being\nmultiply-blended together. When many objects (here, individual points) are multiply-blended on top of each\nother, the output tends to get dark very quickly.\n\nHowever, we really only need the two sets to be multiply-blended with each other.\nWithin each set, we can use regular alpha blending. To do that, we can partition the geometry\nby `set` and then blend. Each partition will be blended normally within the set, and\nthen the resulting sets will be multiply-blended together just once:\n\n```{r scatter_partition_blend}\ndf |\u003e\n  ggplot(aes(x, y, color = set)) +\n  geom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend(\"multiply\") +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(\n    title = \"geom_point(alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend('multiply')\",\n    subtitle = \"Light outside the intersection, but still dark inside the intersection.\"\n  )\n```\n\nThat's getting there: points outside the intersection of the two sets look good,\nbut the intersection is still a bit dark.\n\nLet's try combining two blend modes to address this: we'll use a `\"lighten\"`\nblend mode (which is also commutative) to make the overlapping regions\nlighter, and then draw the `\"multiply\"`-blended version on top at an `alpha`\nof less than 1:\n\n```{r scatter_lighten_multiply}\ndf |\u003e\n  ggplot(aes(x, y, color = set)) +\n  geom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend(\"lighten\") +\n  geom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend(\"multiply\", alpha = 0.5) +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(\n    title = \n      \"geom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend('lighten') + \\ngeom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) |\u003e blend('multiply', alpha = 0.5)\",\n    subtitle = 'A good compromise, but a long specification.'\n  ) +\n  theme(plot.subtitle = element_text(lineheight = 1.2))\n```\n\nNow it's a little easier to see both overlap and density, and the output remains independent of draw order.\n\nHowever, it is a little verbose to need to copy out a layer multiple times:\n\n```r\ngeom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) * blend(\"lighten\") +\ngeom_point(size = 3, alpha = 0.5) |\u003e partition(vars(set)) * blend(\"multiply\", alpha = 0.5) +\n```\n\nWe can simplify this is two ways: first, `partition(vars(set))` is equivalent \nto setting `aes(partition = set)`, so we can move the partition specification\ninto the global plot aesthetics, since it is the same on every layer.\n\nSecond, operations and layers in *ggblend* act as a small algebra. Operations and sums\nof operations can be multiplied by layers and lists of layers, and those\noperations are distributed over the layers (This is where `*` and `|\u003e` differ:\n`|\u003e` does not distribute operations like `blend()` over layers, which is\nuseful if you want to use a blend to combine multiple layers together, rather\nthan applying that blend to each layer individually).\n\nThus, we can \"factor out\"\n`geom_point(size = 3, alpha = 0.5)` from the above expression, yielding this:\n\n```r\ngeom_point(size = 3, alpha = 0.5) * (blend(\"lighten\") + blend(\"multiply\", alpha = 0.5))\n```\n\nBoth expressions are equivalent. Thus we can rewrite the previous example\nlike so:\n\n```{r scatter_lighten_multiply_stacked}\ndf |\u003e\n  ggplot(aes(x, y, color = set, partition = set)) +\n  geom_point(size = 3, alpha = 0.5) * (blend(\"lighten\") + blend(\"multiply\", alpha = 0.5)) +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(\n    title = \"geom_point(aes(partition = set)) * (blend('lighten') + blend('multiply', alpha = 0.5))\",\n    subtitle = \"Two order-independent blends on one layer using the distributive law.\"\n  ) +\n  theme(plot.subtitle = element_text(lineheight = 1.2))\n```\n\n## Blending multiple geometries\n\nWe can also blend geometries together by passing a list of geometries to `blend()`.\nThese lists can include already-blended geometries:\n\n```{r scatter_blend_geom_incorrect}\ndf |\u003e\n  ggplot(aes(x, y, color = set, partition = set)) +\n  list(\n    geom_point(size = 3, alpha = 0.5) * (blend(\"lighten\") + blend(\"multiply\", alpha = 0.5)),\n    geom_vline(xintercept = 0, color = \"gray75\", linewidth = 1.5),\n    geom_hline(yintercept = 0, color = \"gray75\", linewidth = 1.5)\n  ) |\u003e blend(\"hard.light\") +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(\n    title = \"Blending multiple geometries together in a list\",\n    subtitle = \"Careful! The point layer blend is incorrect!\"\n  )\n```\n\nWhoops!! If you look closely, the blending of the `geom_point()` layers appears to\nhave changed. Recall that this expression:\n\n```r\ngeom_point(size = 3, alpha = 0.5) * (blend(\"lighten\") + blend(\"multiply\", alpha = 0.5))\n```\n\nIs equivalent to specifying two separate layers, one with `blend(\"lighten\")`\nand the other with `blend(\"multiply\", alpha = 0.65))`. Thus, when you apply\n`|\u003e blend(\"hard.light\")` to the `list()` of layers, it will use a hard light\nblend mode to blend these two layers together, when previously they would be\nblended using the normal (or `\"over\"`) blend mode.\n\nWe can gain back the original appearance by blending these two layers together\nwith `|\u003e blend()` prior to applying the hard light blend:\n\n```{r scatter_blend_geom}\ndf |\u003e\n  ggplot(aes(x, y, color = set, partition = set)) +\n  list(\n    geom_point(size = 3, alpha = 0.5) * (blend(\"lighten\") + blend(\"multiply\", alpha = 0.5)) |\u003e blend(),\n    geom_vline(xintercept = 0, color = \"gray75\", linewidth = 1.5),\n    geom_hline(yintercept = 0, color = \"gray75\", linewidth = 1.5)\n  ) |\u003e blend(\"hard.light\") +\n  scale_color_brewer(palette = \"Set1\") +\n  facet_grid(~ order) +\n  labs(title = \"Blending multiple geometries together\")\n```\n\n\n## Partitioning and blending lineribbons\n\nAnother case where it's useful to have finer-grained control of blending within a given\ngeometry is when drawing overlapping uncertainty bands. Here, we'll show how to use `blend()` with `stat_lineribbon()`\nfrom [ggdist](https://mjskay.github.io/ggdist/)\nto create overlapping gradient ribbons depicting uncertainty.\n\nWe'll fit a model:\n\n```{r m_mpg}\nm_mpg = lm(mpg ~ hp * cyl, data = mtcars)\n```\n\nAnd generate some confidence distributions for the mean using [distributional](https://pkg.mitchelloharawild.com/distributional/):\n\n```{r lineribbon}\npredictions = unique(mtcars[, c(\"cyl\", \"hp\")])\n\npredictions$mu_hat = with(predict(m_mpg, newdata = predictions, se.fit = TRUE), \n  distributional::dist_student_t(df = df, mu = fit, sigma = se.fit)\n)\n\npredictions\n```\n\nA basic plot based on examples in `vignette(\"freq-uncertainty-vis\", package = \"ggdist\")` and\n`vignette(\"lineribbon\", package = \"ggdist\")` may have issues when lineribbons overlap:\n\n```{r lineribbon_noblend}\npredictions |\u003e\n  ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) +\n  ggdist::stat_lineribbon(\n    aes(ydist = mu_hat, fill_ramp = after_stat(.width)),\n    .width = ppoints(40)\n  ) +\n  geom_point(aes(y = mpg), data = mtcars) +\n  scale_fill_brewer(palette = \"Set2\") +\n  scale_color_brewer(palette = \"Dark2\") +\n  ggdist::scale_fill_ramp_continuous(range = c(1, 0)) +\n  labs(\n    title = \"ggdist::stat_lineribbon()\",\n    subtitle = \"Overlapping lineribbons obscure each other.\", \n    color = \"cyl\", fill = \"cyl\", y = \"mpg\"\n  )\n```\n\nNotice the overlap of the orange (`cyl = 6`) and purple (`cyl = 8`) lines.\n\nIf we add a `partition = cyl` aesthetic mapping, we can blend the geometries\nfor the different levels of `cyl` together with a `blend()` call around\n`ggdist::stat_lineribbon()`. \n\nThere are many ways we could add the partition to the plot:\n\n1. Add `partition = cyl` to the existing `aes(...)` call. However, this\n   leaves the partitioning information far from the call to `blend()`, so the\n   relationship between them is less clear.\n2. Add `aes(partition = cyl)` to the `stat_lineribbon(...)` call. This is \n   a more localized change (better!), but will raise a warning if `stat_lineribbon()`\n   itself does not recognized the `partition` aesthetic.\n3. Add `|\u003e adjust(aes(partition = cyl))` after `stat_lineribbon(...)` to\n   add the `partition` aesthetic to it (this will bypass the warning).\n4. Add `|\u003e partition(vars(cyl))` after `stat_lineribbon(...)` to add the \n   `partition` aesthetic. This is an alias for the `adjust()` approach that is\n   intended to be clearer. It takes a specification for a partition that is \n   similar to `facet_wrap()`: either a one-sided formula or a call to `vars()`.\n\nLet's try the fourth approach:\n\n```{r lineribbon_blend}\npredictions |\u003e\n  ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) +\n  ggdist::stat_lineribbon(\n    aes(ydist = mu_hat, fill_ramp = after_stat(.width)),\n    .width = ppoints(40)\n  ) |\u003e partition(vars(cyl)) |\u003e blend(\"multiply\") +\n  geom_point(aes(y = mpg), data = mtcars) +\n  scale_fill_brewer(palette = \"Set2\") +\n  scale_color_brewer(palette = \"Dark2\") +\n  ggdist::scale_fill_ramp_continuous(range = c(1, 0)) +\n  labs(\n    title = \"ggdist::stat_lineribbon() |\u003e partition(vars(cyl)) |\u003e blend('multiply')\",\n    subtitle = \"Overlapping lineribbons blend together independent of draw order.\",\n    color = \"cyl\", fill = \"cyl\", y = \"mpg\"\n  ) \n```\n\nNow the overlapping ribbons are blended together.\n\n## Highlighting geoms using `copy_under()`\n\nA common visualization technique to make a layer more salient (especially in the\npresence of many other competing layers) is to add a small outline around \nit. For some geometries (like `geom_point()`) this is easy; but for others (like `geom_line()`), \nthere's no easy way to do this without manually copying the layer.\n\nThe *ggblend* layer algebra makes this straightforward using the `adjust()` operation\ncombined with operator addition and multiplication. For example, given a layer\nlike:\n\n```r\ngeom_line(linewidth = 1)\n```\n\nTo add a white outline, you might want something like:\n\n```r\ngeom_line(color = \"white\", linewidth = 2.5) + geom_line(linewidth = 1)\n```\n\nHowever, we'd rather not have to write the `geom_line()` specification twice\nIf we factor out the differences between the first and second layer, we can use\nthe `adjust()` operation (which lets you change the aesthetics and parameters \nof a layer) along with the distributive law to factor out \n`geom_line(linewidth = 1)` and write the above specification as:\n\n```r\ngeom_line(linewidth = 1) * (adjust(color = \"white\", linewidth = 2.5) + 1)\n```\n\nThe `copy_under(...)` operation, which is a synonym for `adjust(...) + 1`, \nalso implements this pattern:\n\n```r\ngeom_line(linewidth = 1) * copy_under(color = \"white\", linewidth = 2.5)\n```\n\nHere's an example highlighting the fit lines from our previous lineribbon example:\n\n```{r lineribbon_blend_highlight}\npredictions |\u003e\n  ggplot(aes(x = hp, fill = ordered(cyl), color = ordered(cyl))) +\n  ggdist::stat_ribbon(\n    aes(ydist = mu_hat, fill_ramp = after_stat(.width)),\n    .width = ppoints(40)\n  ) |\u003e partition(vars(cyl)) |\u003e blend(\"multiply\") +\n  geom_line(aes(y = median(mu_hat)), linewidth = 1) |\u003e copy_under(color = \"white\", linewidth = 2.5) +\n  geom_point(aes(y = mpg), data = mtcars) +\n  scale_fill_brewer(palette = \"Set2\") +\n  scale_color_brewer(palette = \"Dark2\") +\n  ggdist::scale_fill_ramp_continuous(range = c(1, 0)) +\n  labs(\n    title = \"geom_line() |\u003e copy_under(color = 'white', linewidth = 2.5)\", \n    subtitle = \"Highlights the line layer without manually copying its specification.\",\n    color = \"cyl\", fill = \"cyl\", y = \"mpg\"\n  ) \n```\n\nNote that the implementation of `copy_under(...)` is simply a synonym for\n`adjust(...) + 1`; we can see this if we look at `copy_under()` itself:\n\n```{r}\ncopy_under()\n```\n\nIn fact, not that it is particularly useful, but addition and multiplication\nof layer operations is expanded appropriately:\n\n```{r}\n(adjust() + 3) * 2\n```\n\nI hesitate to imagine what that feature might be useful for...\n\n## Compatibility with other packages\n\nIn theory *ggblend* should be compatible with other packages, though in more\ncomplex cases (blending lists of geoms or using the `partition` aesthetic)\nit is possible it may fail, as these features are a bit more hackish. I have \ndone some testing with a few other layer-manipulating packages---including\n[gganimate](https://gganimate.com/), [ggnewscale](https://eliocamp.github.io/ggnewscale/),\nand [relayer](https://github.com/clauswilke/relayer)---and they appear to be\ncompatible.\n\nAs a hard test, here is all three features applied to a modified version of the\nGapminder example used in the [gganimate documentation](https://gganimate.com/):\n\n```{r gapminder, message=FALSE, warning=FALSE}\nlibrary(gganimate)\nlibrary(gapminder)\n\np = gapminder |\u003e\n  ggplot(aes(gdpPercap, lifeExp, size = pop, color = continent)) +\n  list(\n    geom_point(show.legend = c(size = FALSE)) |\u003e partition(vars(continent)) |\u003e blend(\"multiply\"),\n    geom_hline(yintercept = 70, linewidth = 1.5, color = \"gray75\")\n  ) |\u003e blend(\"hard.light\") +\n  scale_color_manual(\n    # same as colorspace::lighten(continent_colors, 0.35)\n    values = c(\n      Africa = \"#BE7658\", Americas = \"#E95866\", Asia = \"#7C5C86\", \n      Europe = \"#659C5D\", Oceania = \"#7477CA\"\n    ),\n    guide = guide_legend(override.aes = list(size = 4))\n  ) +\n  scale_size(range = c(2, 12)) +\n  scale_x_log10(labels = scales::label_dollar(scale_cut = scales::cut_short_scale())) +\n  scale_y_continuous(breaks = seq(20, 80, by = 10)) +\n  labs(\n    title = 'Gapminder with gganimate and ggblend', \n    subtitle = 'Year: {frame_time}', \n    x = 'GDP per capita', \n    y = 'Life expectancy'\n  )  +\n  transition_time(year) +\n  ease_aes('linear')\n\nanimate(p, type = \"cairo\", width = 600, height = 400, res = 100)\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/mjskay.github.io%2Fggblend%2F","html_url":"https://awesome.ecosyste.ms/projects/mjskay.github.io%2Fggblend%2F","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/mjskay.github.io%2Fggblend%2F/lists"}