{"id":22752586,"url":"https://github.com/friendly/ggsprings","last_synced_at":"2025-04-24T04:16:02.919Z","repository":{"id":265773008,"uuid":"871767999","full_name":"friendly/ggsprings","owner":"friendly","description":"ggplot2 extension to draw springs","archived":false,"fork":false,"pushed_at":"2025-04-19T17:18:09.000Z","size":4202,"stargazers_count":5,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-24T04:15:58.297Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/friendly.png","metadata":{"files":{"readme":"README.Rmd","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE.md","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,"publiccode":null,"codemeta":null}},"created_at":"2024-10-12T22:00:14.000Z","updated_at":"2025-04-19T17:18:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"dd60f378-1781-46ea-bf83-f6341b6c23ca","html_url":"https://github.com/friendly/ggsprings","commit_stats":null,"previous_names":["friendly/ggsprings"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/friendly%2Fggsprings","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/friendly%2Fggsprings/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/friendly%2Fggsprings/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/friendly%2Fggsprings/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/friendly","download_url":"https://codeload.github.com/friendly/ggsprings/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250560057,"owners_count":21450173,"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-12-11T05:12:40.669Z","updated_at":"2025-04-24T04:16:02.909Z","avatar_url":"https://github.com/friendly.png","language":"R","readme":"---\noutput: \n  github_document:\n    toc: TRUE\n---\n\n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n```{r, include = FALSE}\nknitr::opts_chunk$set(\n  message = FALSE,\n  warning = FALSE,\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"man/figures/README-\",\n  out.width = \"100%\"\n)\n\noptions(digits=3)\n\nlibrary(tidyverse)\n```\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\u003c!-- badges: end --\u003e\n\n# ggsprings\n\n\n`ggsprings` is designed to\nimplement an extension of `geom_path` which draws paths as springs instead of straight lines.\nAside from possible artistic use, the main impetus for this is to draw points connected by springs,\nwith properties of length, diameter and tension. The initial code for this comes from\n[ggplot2: Elegant Graphics for Data Analysis (3e), Ch. 21: A Case Study (springs) ](https://ggplot2-book.org/ext-springs)\n\nA leading example is to illustrate how least squares regression\nis \"solved\" by connecting data points to a rod, where the springs are constrained to be vertical.\nThe mathematics behind this are well-described in this [Math Stackexchange post](https://math.stackexchange.com/questions/2369673/proving-linear-regression-by-using-physical-springs-model),\nwhere the least squares estimates of intercept and slope are shown to be the equilibrium position that minimized the sum of forces\nand torques exerted by springs.\n\n![](man/figures/potential-energy.png)\n\nIf the springs are allowed to be free, the physical solution is the major PCA axis.\n\n\nHow to do this is described in the `ggplot2` book, https://ggplot2-book.org/ext-springs.\nThe current version here was copied/pasted from the book.\n\nA blog post by Joshua Loftus, [Least squares by springs](https://joshualoftus.com/posts/2020-11-23-least-squares-as-springs/least-squares-as-springs.html)\nillustrates this, citing [code from Thomas Lin Pederson](https://twitter.com/thomasp85/status/1331338379636649986).\nCode to reproduce the first example is contained in `examples/springs.R` and `examples/gapminder-ex.R`.\n\n### Illustrations\n\nThese images show the intent of  `ggsprings` package.\n\n**Least squares regression**\n\nA plot of `lifeExp` vs. `gdpPercap` from the `gapminder` data, with `gdpPercap` on a log10 scale, using the code in the `examples/` folder.\nSprings are connected between the observed value `y = lifeExp` and the fitted value on the regression line, `yend = yhat`, computed\nwith `predict()` for the linear model.\n`tension` was set to `5 + (lifeExp - yhat)^2)`.\nCode for this is in [examples/gapminder-ex.R](examples/gapminder-ex.R)\n\n```\nspring_plot \u003c- simple_plot +\n  geom_spring(aes(x = gdpPercap,\n                  xend = gdpPercap,\n                  y = lifeExp,\n                  yend = yhat,\n                  diameter = diameter,\n                  tension = tension), color = \"darkgray\") +\n  stat_smooth(method = \"lm\", se = FALSE) +\n  geom_point(size = 2)\n\nspring_plot\n```\n\n![](man/figures/loftus-springs-ex1.png){width=60%}\n\n**Principal components analysis**\n\nIn PCA, the first principal component maximizes the variance of the linear combination, or equivalently,\nminimizes the sum of squares of **perpendicular** distances of the points to the line.\n\n\n![](man/figures/loftus-springs-ex2.png){width=60%}\n\n**Animated version**\n\nThis [StatsExchange post](https://stats.stackexchange.com/questions/2691/making-sense-of-principal-component-analysis-eigenvectors-eigenvalues/140579#140579)\nshow an animation of the process of fitting PCA by springs.\n\nIt doesn't actually draw springs, but it gets the animation right. You can see that the forces of the\nsprings initially produce large changes in the fitted line, these cause the line to swing back and forth\nacross it's final position, and shortly the forces begin to balance out.\n\nThis animation is written in Matlib, using the code in [pca_animmation.m](https://gist.github.com/anonymous/7d888663c6ec679ea65428715b99bfdd).\n\n![](man/figures/pca-springs-cropped.gif)\n\n\n## Installation\n\nYou can install the current version of `ggsprings` from this repo,\n\n```\nremotes::install.github(\"friendly/ggsprings\")\n```\n\n## TODO\n\n* Finish documenting the package. I don't quite know how to document a `ggproto` or to use `@inheritParams` for ggplot2 extensions. Add some more examples illustrating spring aesthetics and\nfeatures.\n\n* Use the package to re-create the [gapminder example](examples/gapminder-ex.R).\n\n* Try to use `gganimate` for an animated example.\n\n* Make a hex logo\n\n* [begun] Write a vignette explaining the connection between least squares and springs better. In particular,\n\n  + Illustrate a sample mean by springs: This is the point where positive and negative deviations\n  sum to zero $\\Sigma (x - \\bar{x}) = 0$, and also minimizes the sum of squares, $\\Sigma (x - \\bar{x})^2$.\n  + Illustrate least squares regression in relation to the the normal equations, \n\n\n---\n\n## What's inside\n\n### create_spring.R\n\n```{r, code = readLines(\"R/create_spring.R\")}\n\n\n\n```\n\n### StatSpring.R\n\n```{r, code = readLines(\"R/StatSpring.R\")}\n\n\n\n```\n\n\n\n\n### GeomSpring.R \n\n[Documentation Q\u0026A from ](https://github.com/ggplot2-extenders/ggplot-extension-club/discussions/83#discussioncomment-12480523) @friendly and @teunbrand\n\n\u003e I don't quite know how to document a ggproto\n\nThey are usually accompanied by @export, @format NULL and @usage NULL roxygen tags and refer with @rdname to a pretty generic piece of documentation stating that these are ggproto classes used for extending ggplot2 and are not intended to be used by users directly.\nAn example of that from one of my extensions can be found here: https://github.com/teunbrand/ggh4x/blob/main/R/ggh4x_extensions.R\n\n\n```{r, code = readLines(\"R/GeomSpring.R\")}\n\n\n\n```\n\n\n### geom_spring.R (contains `stat_spring()`) \n\n[Documentation Q\u0026A from ](https://github.com/ggplot2-extenders/ggplot-extension-club/discussions/83#discussioncomment-12480523) @friendly and @teunbrand\n\n\u003e to use @inheritParams for ggplot2 extensions\n\nIf you're going for a geom_spring(), you can use something like @inheritParams ggplot2::geom_path or other geom that maximises overlap between arguments.\n\n\n```{r, code = readLines(\"R/geom_spring.R\")}\n\n\n\n```\n\n\n### StatSmoothFit\n\n```{r}\ncompute_group_smooth_fit \u003c- function(data, scales, method = NULL, formula = NULL,\n                           xseq = NULL,\n                           level = 0.95, method.args = list(),\n                           na.rm = FALSE, flipped_aes = NA){\n  \n  if(is.null(xseq)){ # predictions based on observations \n\n  StatSmooth$compute_group(data = data, scales = scales, \n                       method = method, formula = formula, \n                       se = FALSE, n= 80, span = 0.75, fullrange = FALSE,\n                       xseq = data$x, \n                       level = .95, method.args = method.args, \n                       na.rm = na.rm, flipped_aes = flipped_aes) |\u003e\n      dplyr::mutate(xend = data$x,\n                    yend = data$y)\n  \n  }else{  # predict specific input values\n    \n  StatSmooth$compute_group(data = data, scales = scales, \n                       method = method, formula = formula, \n                       se = FALSE, n= 80, span = 0.75, fullrange = FALSE,\n                       xseq = xseq, \n                       level = .95, method.args = method.args, \n                       na.rm = na.rm, flipped_aes = flipped_aes)   \n    \n  }\n  \n}\n```\n\n```{r}\nlibrary(ggplot2)\ncars |\u003e\n  select(x = speed, y = dist) |\u003e\n  compute_group_smooth_fit(method = lm, formula = y~ x) |\u003e\n  head()\n```\n\n```{r}\nStatSmoothFit \u003c- ggplot2::ggproto(\"StatSmoothFit\", \n                                  ggplot2::StatSmooth,\n                                  compute_group = compute_group_smooth_fit,\n                                  required_aes = c(\"x\", \"y\"))\n\naes_color_accent \u003c- GeomSmooth$default_aes[c(\"colour\")]\n\nGeomPointAccent \u003c- ggproto(\"GeomPointAccent\", GeomPoint, \n              default_aes = modifyList(GeomPoint$default_aes, \n                                       aes_color_accent))\n\nGeomSegmentAccent \u003c- ggproto(\"GeomSegmentAccent\", GeomSegment,\n                           default_aes = modifyList(GeomSegment$default_aes, \n                                                    aes_color_accent))\n\nGeomSpringAccent \u003c- ggproto(\"GeomSpringAccent\", GeomSpring,\n                           default_aes = modifyList(GeomSpring$default_aes,\n                                                    aes_color_accent))\n\nlayer_smooth_fit \u003c- function (mapping = NULL, data = NULL, stat = StatSmoothFit, geom = GeomPointAccent, position = \"identity\", \n    ..., show.legend = NA, inherit.aes = TRUE) \n{\n    layer(data = data, mapping = mapping, stat = stat, \n        geom = geom, position = position, show.legend = show.legend, \n        inherit.aes = inherit.aes, params = rlang::list2(na.rm = FALSE, \n            ...))\n}\n\nstat_smooth_fit \u003c- function(...){layer_smooth_fit(stat = StatSmoothFit, ...)}\ngeom_smooth_fit \u003c- function(...){layer_smooth_fit(geom = GeomPointAccent, ...)}\ngeom_residuals \u003c- function(...){layer_smooth_fit(geom = GeomSegmentAccent, ...)}\ngeom_residual_springs \u003c- function(...){layer_smooth_fit(geom = GeomSpringAccent, ...)}\n\n```\n\nTry it out:\n\n```{r}\nlibrary(ggsprings)\nmtcars %\u003e% \n  ggplot() + \n  aes(x = wt, y = mpg) + \n  geom_point() + \n  geom_smooth(method = \"lm\") + \n  geom_smooth_fit(method = \"lm\") + \n  geom_residuals(method = \"lm\")\n\n\n```\n\nUse `geom`residual_springs`\n\n```{r}\nmtcars %\u003e% \n  ggplot() + \n  aes(x = wt, y = mpg) + \n  geom_point() + \n  geom_smooth(method = \"lm\") + \n  geom_smooth_fit(method = \"lm\") + \n  geom_residual_springs(method = \"lm\")\n```\n\n## Example\n\nSome basic examples top show what is working:\n\n\n```{r example1, eval = T}\n# library(ggsprings)\nlibrary(ggplot2)\nlibrary(tibble)\n#library(dplyr)\n\nset.seed(421)\ndf \u003c- tibble(\n  x = runif(5, max = 10),\n  y = runif(5, max = 10),\n  xend = runif(5, max = 10),\n  yend = runif(5, max = 10),\n  class = sample(letters[1:2], 5, replace = TRUE)\n)\n\nggplot(df) +\n  geom_spring(aes(x = x, y = y,\n                  xend = xend, yend = yend,\n                  color = class),\n              linewidth = 2) \n\n```\n\nUsing tension and diameter as aesthetics\n\n```{r example2, eval = T}\ndf \u003c- tibble(\n  x = runif(5, max = 10),\n  y = runif(5, max = 10),\n  xend = runif(5, max = 10),\n  yend = runif(5, max = 10),\n  class = sample(letters[1:2], 5, replace = TRUE),\n  tension = runif(5),\n  diameter = runif(5, 0.25, 0.75)\n)\n\nggplot(df, aes(x, y, xend = xend, yend = yend)) +\n  geom_spring(aes(tension = tension,\n                  diameter = diameter,\n                  color = class),\n              linewidth = 1.2) \n```\n\n\n\n\n# Packaging\n\n```{r, eval = F}\ndevtools::check(\".\")\ndevtools::install(pkg = \".\", upgrade = \"never\") \n```\n\n# Vignettes\n\n## vignettes/least-squares.Rmd\n\n```{r, child = \"vignettes/least-squares.Rmd\"}\n\n```\n\nUsing tension and diameter defaults\n\n```{r}\nset.seed(1234)\nN \u003c- 10\ndf \u003c- tibble(\n  x = runif(N, 1, 10),\n  y = runif(N, 1, 10)\n)\n\nggplot(df) +\n  aes(x = x, y = y,\n      xend = mean(x),\n      yend = mean(y)) +\n  geom_point(size = 5, color = \"red\") +\n  geom_spring(color = \"blue\",\n              linewidth = 1.2) +\n  geom_point(aes(x = mean(x), y = mean(y)), \n             size = 7,\n             shape = 15,\n             color = \"black\") +\n  scale_x_continuous(breaks = 1:10) +\n  scale_y_continuous(breaks = 1:10) +\n  theme_minimal(base_size = 15) \n\n\n```\n\n## Related \n\n* An [interactive demo](https://www.desmos.com/calculator/90vaqtqpx6) by Trey Goesh allows you to \nvisualize the effect of moving points, changing spring parameters, etc.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffriendly%2Fggsprings","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffriendly%2Fggsprings","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffriendly%2Fggsprings/lists"}