{"id":13401146,"url":"https://github.com/thomasp85/transformr","last_synced_at":"2025-07-30T16:34:45.366Z","repository":{"id":56936189,"uuid":"123027157","full_name":"thomasp85/transformr","owner":"thomasp85","description":"Smooth Polygon Transformations","archived":false,"fork":false,"pushed_at":"2024-02-26T16:00:46.000Z","size":10747,"stargazers_count":117,"open_issues_count":7,"forks_count":12,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-07-26T08:06:29.615Z","etag":null,"topics":["animation","data-visualization","interpolation","matching-shapes","rstats","tweening"],"latest_commit_sha":null,"homepage":null,"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/thomasp85.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,"dei":null}},"created_at":"2018-02-26T20:35:23.000Z","updated_at":"2025-04-01T04:16:28.000Z","dependencies_parsed_at":"2024-03-31T23:44:26.901Z","dependency_job_id":null,"html_url":"https://github.com/thomasp85/transformr","commit_stats":{"total_commits":75,"total_committers":1,"mean_commits":75.0,"dds":0.0,"last_synced_commit":"4a927bbc25cbdc420c6c5dccb3265e5a9e334d26"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/thomasp85/transformr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasp85%2Ftransformr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasp85%2Ftransformr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasp85%2Ftransformr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasp85%2Ftransformr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomasp85","download_url":"https://codeload.github.com/thomasp85/transformr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasp85%2Ftransformr/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267899173,"owners_count":24162994,"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","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["animation","data-visualization","interpolation","matching-shapes","rstats","tweening"],"created_at":"2024-07-30T19:00:59.205Z","updated_at":"2025-07-30T16:34:45.316Z","avatar_url":"https://github.com/thomasp85.png","language":"R","readme":"---\noutput: github_document\n---\n\n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n# transformr \u003cimg src=\"man/figures/logo.png\" align=\"right\"/\u003e\n\n\u003c!-- badges: start --\u003e\n[![R-CMD-check](https://github.com/thomasp85/transformr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/thomasp85/transformr/actions/workflows/R-CMD-check.yaml)\n[![CRAN_Status_Badge](https://www.r-pkg.org/badges/version-ago/transformr)](https://cran.r-project.org/package=transformr)\n[![CRAN_Download_Badge](https://cranlogs.r-pkg.org/badges/grand-total/transformr)](https://cran.r-project.org/package=transformr)\n\u003c!-- badges: end --\u003e\n\n```{r, echo = FALSE}\nknitr::opts_chunk$set(\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"man/figures/README-\",\n  dev = 'jpeg',\n  ffmpeg.format='gif',\n  interval = 1/15\n)\nlibrary(magrittr)\n```\n\nIf you've ever made animated data visualisations you'll know that arbitrary \npolygons and lines requires special considerations if the animation is to be \nsmooth and believable. `transformr` is able to remove all of these worries by\nexpanding [`tweenr`](https://github.com/thomasp85/tweenr) to understand spatial\ndata, and thus lets you focus on defining your animation steps. `transformr` \ntakes care of matching shapes between states, cutting some in bits if the number\ndoesn't match between the states, and ensures that each pair of matched shapes\ncontains the same number of anchor points and that these are paired up so as to\navoid rotation and inversion during animation.\n\n`transformr` supports both polygons (with holes), and paths either encoded as \nsimple x/y data.frames or as simpel features using the \n[`sf`](https://github.com/r-spatial/sf) package.\n\n## Installation\n\nYou can install transformr from CRAN using `install.packages('transformr')` or\ngrab the development version from github with:\n\n```{r gh-installation, eval = FALSE}\n# install.packages(\"devtools\")\ndevtools::install_github(\"thomasp85/transformr\")\n```\n\n## Examples\nThese are simple, contrieved examples showing how the API works. It scales \nsimply to more complicated shapes.\n\n### Polygon\nA polygon is simply a data.frame with an `x` and `y` column, where each row\ndemarcates an anchor point for the polygon. The polygon is not in closed form, \nthat is, the first point is not repeated in the end. If more polygons are wanted\nyou can provide an additional column that indicate the polygon membership of a\ncolumn (quite like `ggplot2::geom_polygon()` expects an `x`, `y`, and `group` \nvariable). If holed polygons are needed, holes should follow the main polygon \nand be separated with an `NA` row in the `x` and `y` column.\n\n```{r, fig.show='animate', cache=TRUE}\nlibrary(transformr)\nlibrary(tweenr)\nlibrary(ggplot2)\npolyplot \u003c- function(data) {\n  p \u003c- ggplot(data) + \n    geom_polygon(aes(x, y, group = id, fill = col)) +\n    scale_fill_identity() +\n    coord_fixed(xlim = c(-1.5, 1.5), ylim = c(-1.5, 1.5))\n  plot(p)\n}\n\nstar \u003c- poly_star()\nstar$col \u003c- 'steelblue'\ncircles \u003c- poly_circles()\ncircles$col \u003c- c('forestgreen', 'firebrick', 'goldenrod')[circles$id]\n\nanimation \u003c- tween_polygon(star, circles, 'cubic-in-out', 40, id) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), polyplot)\n```\n\nBy default the polygons are matched up based on their id. In the above example\nthere's a lack of polygons in the start-state, so these have to appear somehow.\nThis is governed by the `enter` function, which by default is `NULL` meaning new\npolygons just appear at the end of the animation. We can change this to get a\nnicer result:\n\n```{r, fig.show='animate', cache=TRUE}\n# Make new polygons appear 2 units below their end position\nfrom_below \u003c- function(data) {\n  data$y \u003c- data$y - 2\n  data\n}\nanimation \u003c- tween_polygon(star, circles, 'cubic-in-out', 40, id, enter = from_below) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), polyplot)\n```\n\nSimilar to the `enter` function it is possible to supply an `exit` function when\nthe start state has more polygons than the end state. These functions get a \nsingle polygon with the state it was/will be, that can then be manipulated at \nwill, as long as the same number of rows and columns are returned.\n\n\u003e The `enter` and `exit` functions have slightly different semantics here than\nin `tweenr::tween_state()` where it gets all entering/exiting rows in one go, \nand not one-by-one\n\nOur last option is to not match the polygons up, but simply say \"make everything\nin the first state, into everything in the last state... somehow\". This involves\ncutting up polygons in the state with fewest polygons and match polygons by\nminimizing the distance and area difference between pairs. All of this is \ncontrolled by setting `match = FALSE` in `tween_polygon()`, and `transformr` \nwill then do its magic:\n\n```{r, fig.show='animate', cache=TRUE}\nanimation \u003c- tween_polygon(star, circles, 'cubic-in-out', 40, id, match = FALSE) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), polyplot)\n```\n\n### Paths\nPaths are a lot like polygons, except that they don't *wrap-around*. Still, \nslight differences in how they are tweened exists. Chief among these are that \nthe winding order are not changed to minimize the travel-distance, because paths\noften have an implicit direction and this should not be tampered with. Further,\nwhen automatic matching paths (that is, `match = FALSE`), paths are matched to \nminimize the difference in length as well as the pair distance. The same \ninterpretation of the `enter`, `exit`, and `match` arguments remain, which can \nbe seen in the two examples below:\n\n```{r, fig.show='animate', cache=TRUE}\npathplot \u003c- function(data) {\n  p \u003c- ggplot(data) + \n    geom_path(aes(x, y, group = id)) +\n    coord_fixed(xlim = c(-1.5, 1.5), ylim = c(-1.5, 1.5))\n  plot(p)\n}\nspiral \u003c- path_spiral()\nwaves \u003c- path_waves()\n\nanimation \u003c- tween_path(spiral, waves, 'cubic-in-out', 40, id, enter = from_below) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), pathplot)\n```\n\n```{r, fig.show='animate', cache=TRUE}\nanimation \u003c- tween_path(spiral, waves, 'cubic-in-out', 40, id, match = FALSE) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), pathplot)\n```\n\n### Simple features\nThe `sf` package provides an implemention of simple features which are a way to\nencode any type of geometry in defined classes and operate on them. `transformr`\nsupports (multi)point, (multi)linestring, and (multi)polygon geometries which\nacount for most of the use cases. When using the `tween_sf()` function any\n`sfc` column will be tweened by itself, while the rest will be tweened by \n`tweenr::tween_state()`. For any *multi* type, the tweening progress as if\n`match = FALSE` in `tween_polygon()` and `tween_path()`, that is polygons/paths \nare cut and matched to even out the two states. For multipoint the most central\npoints are replicated to ensure the same number of points in each state. One\nnice thing about `sf` is that you can encode different geometry types in the \nsame data.frame and plot it all at once:\n\n```{r, fig.show='animate', cache=TRUE}\nsfplot \u003c- function(data) {\n  p \u003c- ggplot(data) + \n    geom_sf(aes(colour = col, geometry = geometry)) + \n    coord_sf(datum = NA) + # remove graticule\n    scale_colour_identity()\n  plot(p)\n}\nstar_hole \u003c- poly_star_hole(st = TRUE)\ncircles \u003c- poly_circles(st = TRUE)\nspiral \u003c- path_spiral(st = TRUE)\nwaves \u003c- path_waves(st = TRUE)\nrandom \u003c- point_random(st = TRUE)\ngrid \u003c- point_grid(st = TRUE)\ndf1 \u003c- data.frame(\n  geo = sf::st_sfc(star_hole, spiral, random),\n  col = c('steelblue', 'forestgreen', 'goldenrod')\n)\ndf2 \u003c- data.frame(\n  geo = sf::st_sfc(circles, waves, grid),\n  col = c('goldenrod', 'firebrick', 'steelblue')\n)\n\nanimation \u003c- tween_sf(df1, df2, 'cubic-in-out', 40) %\u003e% \n  keep_state(10)\n\nani \u003c- lapply(split(animation, animation$.frame), sfplot)\n```\n","funding_links":[],"categories":["R","ggplot"],"sub_categories":["Animations"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasp85%2Ftransformr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomasp85%2Ftransformr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasp85%2Ftransformr/lists"}