From e7e89e86b53fbd22213911357e1a2cb69fa628a6 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Wed, 1 Jul 2026 14:18:47 +0100 Subject: [PATCH 01/13] init --- .github/workflows/format-lint-comment.yml | 105 ++++++ .github/workflows/format-lint.yml | 67 ++++ .github/workflows/templates/all-pass.md | 2 + .github/workflows/templates/jarl-only.md | 14 + .github/workflows/templates/panache-only.md | 8 + ggplot/index.qmd | 338 +++++++++++++++----- 6 files changed, 461 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/format-lint-comment.yml create mode 100644 .github/workflows/format-lint.yml create mode 100644 .github/workflows/templates/all-pass.md create mode 100644 .github/workflows/templates/jarl-only.md create mode 100644 .github/workflows/templates/panache-only.md diff --git a/.github/workflows/format-lint-comment.yml b/.github/workflows/format-lint-comment.yml new file mode 100644 index 0000000..5421614 --- /dev/null +++ b/.github/workflows/format-lint-comment.yml @@ -0,0 +1,105 @@ +# This takes the results of "format-lint.yaml" and creates a comment on the PR to explain +# them. +# +# See "format-lint.yaml" for more details on why this workflow split is needed. + +name: PR comment + +on: + workflow_run: + workflows: [Format and lint] + types: [completed] + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + permissions: + pull-requests: write + actions: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Download panache results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: panache-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: /tmp/panache + + - name: Download jarl results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: jarl-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: /tmp/jarl + + - name: Read results + id: results + run: | + echo "pr_number=$(cat /tmp/panache/pr-number.txt 2>/dev/null)" >> "$GITHUB_OUTPUT" + echo "panache_result=$(cat /tmp/panache/panache-result.txt 2>/dev/null)" >> "$GITHUB_OUTPUT" + echo "jarl_result=$(cat /tmp/jarl/jarl-result.txt 2>/dev/null)" >> "$GITHUB_OUTPUT" + + # After reading the results of Panache and Jarl, we have several possible outcomes: + # - both pass: if we had created a comment before because at least one of them was + # failing, then we update this comment. Otherwise, we don't open a comment just to + # say that this is passing. + # - Jarl, Panache, or both fail: we create a comment with a custom template to explain + # what failed and how to fix it locally. This should help new contributors when + # they see that CI for linting and formatting fails. + + # This is needed to know if we need to create or update a comment. + - name: Find existing comment + if: steps.results.outputs.pr_number != '' + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 + id: find-comment + with: + issue-number: ${{ steps.results.outputs.pr_number }} + comment-author: 'github-actions[bot]' + body-includes: '' + + # Build the comment body depending on Panache and Jarl results. For the case where both + # fail, we concatenate the two single-failure templates. + # + # Note that the case with both succeeding produces a template only if we have + # "$COMMENT_ID", i.e. if we already created a comment before. If we didn't, then + # `steps.build.outputs.ready` is false and the last step isn't triggered. + - name: Build comment + id: build + if: steps.results.outputs.pr_number != '' + env: + AIR_RESULT: ${{ steps.results.outputs.panache_result }} + JARL_RESULT: ${{ steps.results.outputs.jarl_result }} + COMMENT_ID: ${{ steps.find-comment.outputs.comment-id }} + # The "echo" between the two "cat" ensures a new line between the two templates. + run: | + if [[ "$AIR_RESULT" == "failure" && "$JARL_RESULT" == "failure" ]]; then + { + cat .github/workflows/templates/panache-only.md + echo + echo "---" + echo + cat .github/workflows/templates/jarl-only.md + } > /tmp/comment.md + elif [[ "$AIR_RESULT" == "failure" ]]; then + cp .github/workflows/templates/panache-only.md /tmp/comment.md + elif [[ "$JARL_RESULT" == "failure" ]]; then + cp .github/workflows/templates/jarl-only.md /tmp/comment.md + elif [[ -n "$COMMENT_ID" ]]; then + cp .github/workflows/templates/all-pass.md /tmp/comment.md + fi + [[ -f /tmp/comment.md ]] && echo "ready=true" >> "$GITHUB_OUTPUT" || echo "ready=false" >> "$GITHUB_OUTPUT" + + - name: Create or update comment + if: steps.build.outputs.ready == 'true' && steps.results.outputs.pr_number != '' + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ steps.results.outputs.pr_number }} + body-path: /tmp/comment.md + edit-mode: replace \ No newline at end of file diff --git a/.github/workflows/format-lint.yml b/.github/workflows/format-lint.yml new file mode 100644 index 0000000..5ff71e1 --- /dev/null +++ b/.github/workflows/format-lint.yml @@ -0,0 +1,67 @@ +# This workflow runs Panache for formatting and Jarl for linting. It stores artifacts about the +# outcome of each tool and the PR number. Those artifacts are then used in the workflow +# "format-lint-pr-comment.yaml" to post a comment so that it's clearer for external +# contributors. +# +# This could have been done in a single workflow with a "pull_request_target" trigger but +# this might have some security issues (this is unlikely in practice but still better to +# be ahead of it): +# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + +name: Format and lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + format-panache: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - name: Install Panache + uses: jolars/panache-action@a1e4e68c74a41e6b9dade9c93095a262a23aa48c # v1 + with: + lint: "false" + continue-on-error: true + - name: Save results + if: github.event_name == 'pull_request' + run: | + echo "${{ steps.check.outcome }}" > /tmp/panache-result.txt + echo "${{ github.event.pull_request.number }}" > /tmp/pr-number.txt + - name: Upload artifacts + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: panache-results + path: | + /tmp/panache-result.txt + /tmp/pr-number.txt + - if: steps.check.outcome == 'failure' + run: exit 1 + + lint-jarl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - name: Check + id: check + uses: etiennebacher/setup-jarl@eabf7e572f2991d165059007531b9bbe2987e39c # v0.1.1 + continue-on-error: true + - name: Save result + if: github.event_name == 'pull_request' + run: echo "${{ steps.check.outcome }}" > /tmp/jarl-result.txt + - name: Upload artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: jarl-results + path: /tmp/jarl-result.txt + - if: steps.check.outcome == 'failure' + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/templates/all-pass.md b/.github/workflows/templates/all-pass.md new file mode 100644 index 0000000..0fa95eb --- /dev/null +++ b/.github/workflows/templates/all-pass.md @@ -0,0 +1,2 @@ + +:white_check_mark: All formatting and linting checks passed! diff --git a/.github/workflows/templates/jarl-only.md b/.github/workflows/templates/jarl-only.md new file mode 100644 index 0000000..24f9c0b --- /dev/null +++ b/.github/workflows/templates/jarl-only.md @@ -0,0 +1,14 @@ + +:x: This Pull Request failed our automated **linting checks**. Please resolve these prior to requesting review. We use Jarl to automatically lint R code. Please [install it](https://jarl.etiennebacher.com/#installation) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to find linting issues: + +```sh +jarl check . +``` + +Some of these issues might be automatically fixed with the following command: + +```sh +jarl check . --fix +``` + +This comment will be automatically updated once linting checks pass. diff --git a/.github/workflows/templates/panache-only.md b/.github/workflows/templates/panache-only.md new file mode 100644 index 0000000..8657307 --- /dev/null +++ b/.github/workflows/templates/panache-only.md @@ -0,0 +1,8 @@ + +:x: This Pull Request failed our automated **formatting checks**. Please resolve these prior to requesting review. We use Air to automatically format R code. Please [install it](https://posit-dev.github.io/air/cli.html) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to reformat the code: + +```sh +air format . +``` + +This comment will be automatically updated once formatting checks pass. diff --git a/ggplot/index.qmd b/ggplot/index.qmd index 68c72c7..7097dcf 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -14,100 +14,164 @@ execute: warning: false message: false freeze: auto -editor: - markdown: +editor: + markdown: wrap: 72 --- # Introduction -While you can make plots with just the packages that come bundled with base R, many R users make their visualizations entirely using the [`ggplot2`](https://ggplot2.tidyverse.org/index.html) package and an [ecosystem of packages](https://exts.ggplot2.tidyverse.org/gallery/) designed around it. +While you can make plots with just the packages that come bundled with base R, +many R users make their visualizations entirely using the +[`ggplot2`](https://ggplot2.tidyverse.org/index.html) package and an [ecosystem +of packages](https://exts.ggplot2.tidyverse.org/gallery/) designed around it. + +```{r} +#| label: ggplot2 -```{r ggplot2} # load the ggplot2 package library(ggplot2) ``` -As with the previous sessions, we'll be using the Palmer penguins dataset. While we built our own combined dataset in the introduction session, now we're going to use the built-in cleaned dataset. First, let's load the dataset. Then let's inspect the data using the `glimpse()` function. +As with the previous sessions, we'll be using the Palmer penguins dataset. While +we built our own combined dataset in the introduction session, now we're going +to use the built-in cleaned dataset. First, let's load the dataset. Then let's +inspect the data using the `glimpse()` function. + +```{r} +#| label: palmerpenguins -```{r palmerpenguins} data(penguins) dplyr::glimpse(penguins) ``` -As we discovered before, this dataset includes many different measurements for individual penguins from three different studies. The studies cover both sexes of three different species of penguins from three different islands in the Palmer Archipelago. +As we discovered before, this dataset includes many different measurements for +individual penguins from three different studies. The studies cover both sexes +of three different species of penguins from three different islands in the +Palmer Archipelago. # The ggplot2 basics -The most important function in the `ggplot2` package is `ggplot()`. Note that this function doesn't include the "2" of the package name. Let's go ahead and try using this function on our penguins data. +The most important function in the `ggplot2` package is `ggplot()`. Note that +this function doesn't include the "2" of the package name. Let's go ahead and +try using this function on our penguins data. + +```{r} +#| label: ggplot-raw -```{r ggplot-raw} ggplot(penguins) ``` -You'll notice that the `ggplot()` function doesn't actually do much by itself. Here, we provide it with the penguins dataset, but the result looks like someone started making a plot and then stopped after the first step of making the rectangle for the plot area. This is because `ggplot2` is designed around the "grammar of graphics". Therefore, it expects you to build a sentence-like structure out of its functions. A single word (i.e., the call to `ggplot()` above) doesn't make much of a sentence, so let's start building up a real sentence. +You'll notice that the `ggplot()` function doesn't actually do much by itself. +Here, we provide it with the penguins dataset, but the result looks like someone +started making a plot and then stopped after the first step of making the +rectangle for the plot area. This is because `ggplot2` is designed around the +"grammar of graphics". Therefore, it expects you to build a sentence-like +structure out of its functions. A single word (i.e., the call to `ggplot()` +above) doesn't make much of a sentence, so let's start building up a real +sentence. + +By using the `ggplot()` function, we are essentially stating that we are +beginning a plotting "sentence". We then combine this with other "words" +(function calls) using the `+` operator. The next component you usually want to +specify in a `ggplot` "sentence" is our "aesthetic" mappings. These specify the +columns of the dataset that correspond to each axis of the plot, including the +x/y axes, but also the axes of color, shape, etc. We do this with the `aes()` +function: -By using the `ggplot()` function, we are essentially stating that we are beginning a plotting "sentence". We then combine this with other "words" (function calls) using the `+` operator. The next component you usually want to specify in a `ggplot` "sentence" is our "aesthetic" mappings. These specify the columns of the dataset that correspond to each axis of the plot, including the x/y axes, but also the axes of color, shape, etc. We do this with the `aes()` function: +```{r} +#| label: ggplot-aes -```{r ggplot-aes} ggplot(penguins) + aes(x = body_mass, y = flipper_len) ``` -Hey, it's starting to look like a plot now! Except there isn't any actual data being plotted. Let's fix that. We'll start off with a simple scatter plot by using the `geom_point()` function: +Hey, it's starting to look like a plot now! Except there isn't any actual data +being plotted. Let's fix that. We'll start off with a simple scatter plot by +using the `geom_point()` function: + +```{r} +#| label: geom_point -```{r geom_point} ggplot(penguins) + aes(x = body_mass, y = flipper_len) + geom_point() ``` -And there we go! You'll notice that with just a few lines, we've already made a pretty nice visualization of this penguin data. `ggplot2` does most of the work for us once we specify our dataset and our `x` and `y` variables. +And there we go! You'll notice that with just a few lines, we've already made a +pretty nice visualization of this penguin data. `ggplot2` does most of the work +for us once we specify our dataset and our `x` and `y` variables. -:::: {.callout-note} +::: {.callout-note} ## Missing data -You may have noticed a warning that some rows of the dataset contain missing values. There are two penguins without any measurements in the dataset. How might we remove them using `dplyr` so we don't get this warning over and over? -::: {.callout-caution collapse="true" appearance="simple" icon="false"} +You may have noticed a warning that some rows of the dataset contain missing +values. There are two penguins without any measurements in the dataset. How +might we remove them using `dplyr` so we don't get this warning over and over? + +::::: {.callout-caution collapse="true" appearance="simple" icon="false"} ##### Solution + ```{r} penguins <- penguins |> dplyr::filter(!is.na(body_mass)) ``` +::::: ::: -:::: -Now, let's go a step further and color the points by another variable (e.g., the island of the penguins). With `ggplot2`, all that requires is specifying another aesthetic: +Now, let's go a step further and color the points by another variable (e.g., the +island of the penguins). With `ggplot2`, all that requires is specifying another +aesthetic: + +```{r} +#| label: geom_point-color -```{r geom_point-color} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + geom_point() ``` -Notice that `ggplot2` comes with its own default color scheme. However, I would strongly discourage you from using the default colors, especially as the number of categories increases (with only 3 categories here it isn't too bad). Let's try out some of the more accessible color palettes that are also available in R. +Notice that `ggplot2` comes with its own default color scheme. However, I would +strongly discourage you from using the default colors, especially as the number +of categories increases (with only 3 categories here it isn't too bad). Let's +try out some of the more accessible color palettes that are also available in R. + +First, let's try one of the +[`viridis`](https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html) +color palettes. Since this palette is included in `ggplot2`, all we need to do +is add the proper "scale" to our `ggplot()` call. Scales tell ggplot how to +handle a particular aesthetic, and are usually of the form +`scale_[aesthetic]_[type]()`. -First, let's try one of the [`viridis`](https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html) color palettes. Since this palette is included in `ggplot2`, all we need to do is add the proper "scale" to our `ggplot()` call. Scales tell ggplot how to handle a particular aesthetic, and are usually of the form `scale_[aesthetic]_[type]()`. +```{r} +#| label: viridis -```{r viridis} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + geom_point() + scale_color_viridis_d(end = 0.7) # avoid yellow at the end of the palette ``` -Now let's try one of the [brewer color palettes](https://r-graph-gallery.com/38-rcolorbrewers-palettes.html). +Now let's try one of the [brewer color +palettes](https://r-graph-gallery.com/38-rcolorbrewers-palettes.html). + +```{r} +#| label: brewer -```{r brewer} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + geom_point() + scale_color_brewer(palette = "Set1") ``` -Outside of color, there are many other [aspects of the graph](https://ggplot2.tidyverse.org/articles/ggplot2-specs.html) that we can modify using aesthetics and "scale"s. For example, we can modify the shape of the points: +Outside of color, there are many other [aspects of the +graph](https://ggplot2.tidyverse.org/articles/ggplot2-specs.html) that we can +modify using aesthetics and "scale"s. For example, we can modify the shape of +the points: + +```{r} +#| label: shape-aes -```{r shape-aes} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -117,9 +181,16 @@ ggplot(penguins) + ::: {.callout-note} ### More missing data -Urgh, more missing data! It appears that nine of the penguins are missing sex identification data. We can ignore the warning message, but that extra "NA" category in the legend is quite annoying. We can remove this category from the legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could also use `scale_shape_manual()` if you wanted to supply your own shapes): -```{r na.translate} +Urgh, more missing data! It appears that nine of the penguins are missing sex +identification data. We can ignore the warning message, but that extra "NA" +category in the legend is quite annoying. We can remove this category from the +legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could +also use `scale_shape_manual()` if you wanted to supply your own shapes): + +```{r} +#| label: na.translate + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -131,7 +202,9 @@ ggplot(penguins) + And the x/y axes: -```{r axis-scales} +```{r} +#| label: axis-scales + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -144,9 +217,15 @@ ggplot(penguins) + # Theming -The last basic thing you might want to do with `ggplot2` is modify the style of the visualization. This is extremely customizable, but the first place to start is with a [built-in theme](https://ggplot2.tidyverse.org/reference/ggtheme.html). I personally prefer the classic theme, which looks very similar to base R plots: +The last basic thing you might want to do with `ggplot2` is modify the style of +the visualization. This is extremely customizable, but the first place to start +is with a [built-in +theme](https://ggplot2.tidyverse.org/reference/ggtheme.html). I personally +prefer the classic theme, which looks very similar to base R plots: + +```{r} +#| label: theme_classic -```{r theme_classic} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -158,9 +237,16 @@ ggplot(penguins) + theme_classic() ``` -Using this built-in theme has changed many visual aspects of the graph, including changing the plot background color, adding axis lines, and removing the internal grid lines. If you look very closely, however, the axis tick labels are still a slight grey. We can use the `theme()` function to further customize the appearance and change this. In this case, we'll make the axis text elements have a black color instead of the default gray. +Using this built-in theme has changed many visual aspects of the graph, +including changing the plot background color, adding axis lines, and removing +the internal grid lines. If you look very closely, however, the axis tick labels +are still a slight grey. We can use the `theme()` function to further customize +the appearance and change this. In this case, we'll make the axis text elements +have a black color instead of the default gray. + +```{r} +#| label: theming -```{r theming} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -173,21 +259,33 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -And there we have it! With just 10 lines we've created what I would say is a publication quality graph! `ggplot` does a lot of the tedious work for us, giving us time to focus on the more important aspects, such as labeling and color. Admittedly, I've spent a LOT of time on these aspects in the past... +And there we have it! With just 10 lines we've created what I would say is a +publication quality graph! `ggplot` does a lot of the tedious work for us, +giving us time to focus on the more important aspects, such as labeling and +color. Admittedly, I've spent a LOT of time on these aspects in the past... -More information about all of the hierarchical theme components that you can customize is available [here](https://ggplot2.tidyverse.org/reference/theme.html). In order to change many of these components, you need to use theme elements like we did above with `element_text()`. That and other theme elements are documented [here](https://ggplot2.tidyverse.org/reference/element.html). +More information about all of the hierarchical theme components that you can +customize is available +[here](https://ggplot2.tidyverse.org/reference/theme.html). In order to change +many of these components, you need to use theme elements like we did above with +`element_text()`. That and other theme elements are documented +[here](https://ggplot2.tidyverse.org/reference/element.html). # More complex features ## Other layers -There are many other [types of plots](https://ggplot2.tidyverse.org/reference/#layers) that we can make with `ggplot2`. +There are many other [types of +plots](https://ggplot2.tidyverse.org/reference/#layers) that we can make with +`ggplot2`. ### Histograms We can visualize the density of values for a single variable with a histogram: -```{r geom_histogram} +```{r} +#| label: geom_histogram + ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram() + @@ -196,9 +294,13 @@ ggplot(penguins) + ``` ::: {.callout-note} -Histograms don't require a y-axis aesthetic by default. The counts are tabulated for you. If you specify a "fill" aesthetic, the default is to stack the bars which can sometimes be a bit misleading. You can also dodge them to fix this: +Histograms don't require a y-axis aesthetic by default. The counts are tabulated +for you. If you specify a "fill" aesthetic, the default is to stack the bars +which can sometimes be a bit misleading. You can also dodge them to fix this: + +```{r} +#| label: hist-dodge -```{r hist-dodge} ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram(position = "dodge") + @@ -209,9 +311,12 @@ ggplot(penguins) + ### Boxplots and Violin Plotss -We can visualize the density of values for a single variable across a discrete variable with boxplots or violin plots: +We can visualize the density of values for a single variable across a discrete +variable with boxplots or violin plots: + +```{r} +#| label: geom_boxplot -```{r geom_boxplot} ggplot(penguins) + aes(x = island, y = bill_len) + geom_boxplot() + @@ -219,7 +324,9 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -```{r geom_violin} +```{r} +#| label: geom_violin + ggplot(penguins) + aes(x = island, y = bill_len) + geom_violin(scale = "width", draw_quantiles = c(0.25, 0.5, 0.75)) + @@ -229,23 +336,34 @@ ggplot(penguins) + ::: {.callout-note} #### Geom options -Note that many of these "geom"s have lots of options. For example, here we've decided to `scale` all of the violin plots to the same width and to draw the quartiles on them (mimicking the boxplots above). You can see all of the options for a geom by checking out it's help page (`?geom_violin`) or on the ggplot [website](https://ggplot2.tidyverse.org/reference/geom_violin.html). + +Note that many of these "geom"s have lots of options. For example, here we've +decided to `scale` all of the violin plots to the same width and to draw the +quartiles on them (mimicking the boxplots above). You can see all of the options +for a geom by checking out it's help page (`?geom_violin`) or on the ggplot +[website](https://ggplot2.tidyverse.org/reference/geom_violin.html). ::: ### 2D Contours -We can also visualize the density of values across two continuous variables using a 2D contour: +We can also visualize the density of values across two continuous variables +using a 2D contour: + +```{r} +#| label: geom_density_2d -```{r geom_density_2d} ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + theme_classic() + theme(axis.text = element_text(color = "black")) ``` -Note that sometimes you may need to expand the axes a little bit to better show the contours: +Note that sometimes you may need to expand the axes a little bit to better show +the contours: + +```{r} +#| label: geom_density_2d_expand -```{r geom_density_2d_expand} ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + @@ -256,18 +374,26 @@ ggplot(penguins) + ### Time Series -Since there isn't really any time series data in the penguins dataset, we'll take a quick detour and use the built-in `economics` dataset to explore visualizing a time series. In this case, we are looking at unemployment over time: +Since there isn't really any time series data in the penguins dataset, we'll +take a quick detour and use the built-in `economics` dataset to explore +visualizing a time series. In this case, we are looking at unemployment over +time: + +```{r} +#| label: geom_line -```{r geom_line} ggplot(economics, aes(x = date, y = unemploy)) + geom_line() + theme_classic() + theme(axis.text = element_text(color = "black")) ``` -`geom_path()` lets you explore how two variables are related over time. For example, unemployment and personal savings rate: +`geom_path()` lets you explore how two variables are related over time. For +example, unemployment and personal savings rate: + +```{r} +#| label: geom_path -```{r geom_path} ggplot(economics, aes(x = unemploy / pop, y = psavert)) + geom_path(aes(colour = as.numeric(date))) + theme_classic() + @@ -276,14 +402,21 @@ ggplot(economics, aes(x = unemploy / pop, y = psavert)) + ::: {.callout-note} #### Multiple columns for individual aesthetics -Note how we've used multiple columns of the data to define the x-axis here. You can use any sort of mathematical operators to combine multiple columns into a single aesthetic, as long as you are doing row-wise math. + +Note how we've used multiple columns of the data to define the x-axis here. You +can use any sort of mathematical operators to combine multiple columns into a +single aesthetic, as long as you are doing row-wise math. ::: ## Combining layers -We can also combine multiple layers to show the same data in different ways in the same plot. For example, we could show the raw data for the above contour plot in addition to the contours: +We can also combine multiple layers to show the same data in different ways in +the same plot. For example, we could show the raw data for the above contour +plot in addition to the contours: + +```{r} +#| label: combined-layers -```{r combined-layers} ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_point() + @@ -298,14 +431,22 @@ ggplot(penguins) + ::: {.callout-note} ### Layer order -When combining layers, the layers are added to the plot in order, so in this case the points are the bottom layer and the contour lines are the top layer. We changed the alpha of the middle layer to prevent the points from being blocked. I've also used the `coord_cartesian()` function to remove the default axis expansion. This way the background color reaches both axes and doesn't have a white gap. + +When combining layers, the layers are added to the plot in order, so in this +case the points are the bottom layer and the contour lines are the top layer. We +changed the alpha of the middle layer to prevent the points from being blocked. +I've also used the `coord_cartesian()` function to remove the default axis +expansion. This way the background color reaches both axes and doesn't have a +white gap. ::: ## Facetting Let's take our scatterplot example from earlier: -```{r scatter-full} +```{r} +#| label: scatter-full + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -318,9 +459,15 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -Now, what if we wanted to also split the data by the species of the penguins? We're already using color and shape, so what other aesthetic could we use? We could possible use some shapes that have both a fill and outline color, but that sounds messy. Instead of using another aesthetic, we could also use a `facet`. This splits the chart into multiple panels: +Now, what if we wanted to also split the data by the species of the penguins? +We're already using color and shape, so what other aesthetic could we use? We +could possible use some shapes that have both a fill and outline color, but that +sounds messy. Instead of using another aesthetic, we could also use a `facet`. +This splits the chart into multiple panels: + +```{r} +#| label: scatter-facet -```{r scatter-facet} ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -336,7 +483,9 @@ ggplot(penguins) + We can get even crazier by faceting by multiple variables: -```{r scatter-facet-grid} +```{r} +#| label: scatter-facet-grid + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -353,15 +502,28 @@ ggplot(penguins) + OK, maybe we've gone a little too far here, but you get the picture! # Combining plots -When publishing results, often we need to combine multiple figures into a single visualization. There are lots of packages for accomplishing this (even my own [deeptime](https://williamgearty.com/deeptime) package has some functionality for it), but today we'll check out the [`patchwork`](https://patchwork.data-imaginist.com/) package which extends the "grammar of graphics" to combining plots (you may need to install it if you haven't done so already). -```{r patchwork} +When publishing results, often we need to combine multiple figures into a single +visualization. There are lots of packages for accomplishing this (even my own +[deeptime](https://williamgearty.com/deeptime) package has some functionality +for it), but today we'll check out the +[`patchwork`](https://patchwork.data-imaginist.com/) package which extends the +"grammar of graphics" to combining plots (you may need to install it if you +haven't done so already). + +```{r} +#| label: patchwork + library(patchwork) ``` -First, let's go ahead and make some plots. Instead of plotting them, though, we'll save them as objects in our environment. Note that when you save a plot to an object it isn't displayed in the "Plots" tab. +First, let's go ahead and make some plots. Instead of plotting them, though, +we'll save them as objects in our environment. Note that when you save a plot to +an object it isn't displayed in the "Plots" tab. + +```{r} +#| label: plot-objects -```{r plot-objects} g1 <- ggplot(penguins) + aes(x = body_mass, y = flipper_len) + geom_point() + @@ -380,15 +542,23 @@ g2 <- ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -Now, in order to combine these, all we need to do is combine them using the `+` operator, like we did with the individual elements of the plots. +Now, in order to combine these, all we need to do is combine them using the `+` +operator, like we did with the individual elements of the plots. + +```{r} +#| label: add-plots -```{r add-plots} g1 + g2 ``` -You can see that `patchwork` does all of the work for us, lining up the different components of the plots. If we have more plots to combine, we can then use the `|` (side-by-side) and `/` (above-and-below) operators to make more complex arrangements of plots. +You can see that `patchwork` does all of the work for us, lining up the +different components of the plots. If we have more plots to combine, we can then +use the `|` (side-by-side) and `/` (above-and-below) operators to make more +complex arrangements of plots. + +```{r} +#| label: patchwork-complex -```{r patchwork-complex} g3 <- ggplot(penguins) + aes(x = island, y = bill_len) + geom_boxplot() + @@ -398,15 +568,37 @@ g3 <- ggplot(penguins) + (g1 | g2) / g3 ``` -There's a lot more you can do with `patchwork`, including adjusting the widths and heights, but we'll stop here for now. +There's a lot more you can do with `patchwork`, including adjusting the widths +and heights, but we'll stop here for now. # Saving plots -The last big thing you'll need to know about plotting is how to save your plots. `ggplot2` comes with a nifty `ggsave()` function which you can use to save your plots in a number of different formats. Here we'll save our most recent combined plot as both a PDF and a JPEG. The former is a vector format, meaning all of the elements of the figure as geometric shapes, and, because of this, none of the data is lost (aka "lossless"). The latter is a raster format, meaning the figure is converted to a 2-D array of colored pixels of a desired size, and because of this, some of the data is lost in the process (aka "lossy"). This results in the pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what format you want based on the file extension, so we just need to specify the file location and the object to be saved (and optionally the dimensions of the output file). It's usually a good idea to keep all figures in their own folder (which was already created for you). -```{r ggsave, eval=FALSE} +The last big thing you'll need to know about plotting is how to save your plots. +`ggplot2` comes with a nifty `ggsave()` function which you can use to save your +plots in a number of different formats. Here we'll save our most recent combined +plot as both a PDF and a JPEG. The former is a vector format, meaning all of the +elements of the figure as geometric shapes, and, because of this, none of the +data is lost (aka "lossless"). The latter is a raster format, meaning the figure +is converted to a 2-D array of colored pixels of a desired size, and because of +this, some of the data is lost in the process (aka "lossy"). This results in the +pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what +format you want based on the file extension, so we just need to specify the file +location and the object to be saved (and optionally the dimensions of the output +file). It's usually a good idea to keep all figures in their own folder (which +was already created for you). + +```{r} +#| label: ggsave +#| eval: false + gg <- (g1 | g2) / g3 ggsave("figures/penguins_1.pdf", gg, height = 10, width = 10) ggsave("figures/penguins_1.jpg", gg, height = 10, width = 10) ``` -Now we'll once again use GitHub Desktop to commit these files and push them to our GitHub repository. You should see all of the files that have been modified since we last committed. In this case, you should see your code file (modified from our last session) and your two new figures. Make sure the checkboxes are checked for these three files, write a succinct commit summary, then click the Commit button, then the Push button. And that's that! +Now we'll once again use GitHub Desktop to commit these files and push them to +our GitHub repository. You should see all of the files that have been modified +since we last committed. In this case, you should see your code file (modified +from our last session) and your two new figures. Make sure the checkboxes are +checked for these three files, write a succinct commit summary, then click the +Commit button, then the Push button. And that's that! From facc640ec9b4a487e1b651790db6b893234c2527 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Wed, 1 Jul 2026 14:23:59 +0100 Subject: [PATCH 02/13] tweak comment --- .github/workflows/format-lint-comment.yml | 6 +++--- .github/workflows/templates/panache-only.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/format-lint-comment.yml b/.github/workflows/format-lint-comment.yml index 5421614..be8bbaf 100644 --- a/.github/workflows/format-lint-comment.yml +++ b/.github/workflows/format-lint-comment.yml @@ -73,12 +73,12 @@ jobs: id: build if: steps.results.outputs.pr_number != '' env: - AIR_RESULT: ${{ steps.results.outputs.panache_result }} + PANACHE_RESULT: ${{ steps.results.outputs.panache_result }} JARL_RESULT: ${{ steps.results.outputs.jarl_result }} COMMENT_ID: ${{ steps.find-comment.outputs.comment-id }} # The "echo" between the two "cat" ensures a new line between the two templates. run: | - if [[ "$AIR_RESULT" == "failure" && "$JARL_RESULT" == "failure" ]]; then + if [[ "$PANACHE_RESULT" == "failure" && "$JARL_RESULT" == "failure" ]]; then { cat .github/workflows/templates/panache-only.md echo @@ -86,7 +86,7 @@ jobs: echo cat .github/workflows/templates/jarl-only.md } > /tmp/comment.md - elif [[ "$AIR_RESULT" == "failure" ]]; then + elif [[ "$PANACHE_RESULT" == "failure" ]]; then cp .github/workflows/templates/panache-only.md /tmp/comment.md elif [[ "$JARL_RESULT" == "failure" ]]; then cp .github/workflows/templates/jarl-only.md /tmp/comment.md diff --git a/.github/workflows/templates/panache-only.md b/.github/workflows/templates/panache-only.md index 8657307..b04478d 100644 --- a/.github/workflows/templates/panache-only.md +++ b/.github/workflows/templates/panache-only.md @@ -1,8 +1,8 @@ -:x: This Pull Request failed our automated **formatting checks**. Please resolve these prior to requesting review. We use Air to automatically format R code. Please [install it](https://posit-dev.github.io/air/cli.html) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to reformat the code: +:x: This Pull Request failed our automated **formatting checks**. Please resolve these prior to requesting review. We use Panache to automatically format R code chunks in Quarto files. Please [install it](https://panache.bz/getting-started.html#installation) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to reformat the code: ```sh -air format . +panache format **/*.qmd ``` This comment will be automatically updated once formatting checks pass. From 04d60ee4816460a9727b4b9c0e184e98b4a0de3c Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Thu, 16 Jul 2026 14:52:51 +0100 Subject: [PATCH 03/13] fix step id --- .github/workflows/format-lint.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format-lint.yml b/.github/workflows/format-lint.yml index 5ff71e1..610f881 100644 --- a/.github/workflows/format-lint.yml +++ b/.github/workflows/format-lint.yml @@ -25,7 +25,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - name: Install Panache + - name: Run Panache + id: check uses: jolars/panache-action@a1e4e68c74a41e6b9dade9c93095a262a23aa48c # v1 with: lint: "false" From 71674b43a3ec839635614ec8c4fb212b445fe675 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Thu, 16 Jul 2026 14:53:01 +0100 Subject: [PATCH 04/13] do not wrap text, but still format code --- ggplot/index.qmd | 201 +++++++++-------------------------------------- panache.toml | 2 + 2 files changed, 41 insertions(+), 162 deletions(-) create mode 100644 panache.toml diff --git a/ggplot/index.qmd b/ggplot/index.qmd index 7097dcf..e78d40f 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -21,10 +21,7 @@ editor: # Introduction -While you can make plots with just the packages that come bundled with base R, -many R users make their visualizations entirely using the -[`ggplot2`](https://ggplot2.tidyverse.org/index.html) package and an [ecosystem -of packages](https://exts.ggplot2.tidyverse.org/gallery/) designed around it. +While you can make plots with just the packages that come bundled with base R, many R users make their visualizations entirely using the [`ggplot2`](https://ggplot2.tidyverse.org/index.html) package and an [ecosystem of packages](https://exts.ggplot2.tidyverse.org/gallery/) designed around it. ```{r} #| label: ggplot2 @@ -33,10 +30,7 @@ of packages](https://exts.ggplot2.tidyverse.org/gallery/) designed around it. library(ggplot2) ``` -As with the previous sessions, we'll be using the Palmer penguins dataset. While -we built our own combined dataset in the introduction session, now we're going -to use the built-in cleaned dataset. First, let's load the dataset. Then let's -inspect the data using the `glimpse()` function. +As with the previous sessions, we'll be using the Palmer penguins dataset. While we built our own combined dataset in the introduction session, now we're going to use the built-in cleaned dataset. First, let's load the dataset. Then let's inspect the data using the `glimpse()` function. ```{r} #| label: palmerpenguins @@ -45,16 +39,11 @@ data(penguins) dplyr::glimpse(penguins) ``` -As we discovered before, this dataset includes many different measurements for -individual penguins from three different studies. The studies cover both sexes -of three different species of penguins from three different islands in the -Palmer Archipelago. +As we discovered before, this dataset includes many different measurements for individual penguins from three different studies. The studies cover both sexes of three different species of penguins from three different islands in the Palmer Archipelago. # The ggplot2 basics -The most important function in the `ggplot2` package is `ggplot()`. Note that -this function doesn't include the "2" of the package name. Let's go ahead and -try using this function on our penguins data. +The most important function in the `ggplot2` package is `ggplot()`. Note that this function doesn't include the "2" of the package name. Let's go ahead and try using this function on our penguins data. ```{r} #| label: ggplot-raw @@ -62,22 +51,9 @@ try using this function on our penguins data. ggplot(penguins) ``` -You'll notice that the `ggplot()` function doesn't actually do much by itself. -Here, we provide it with the penguins dataset, but the result looks like someone -started making a plot and then stopped after the first step of making the -rectangle for the plot area. This is because `ggplot2` is designed around the -"grammar of graphics". Therefore, it expects you to build a sentence-like -structure out of its functions. A single word (i.e., the call to `ggplot()` -above) doesn't make much of a sentence, so let's start building up a real -sentence. - -By using the `ggplot()` function, we are essentially stating that we are -beginning a plotting "sentence". We then combine this with other "words" -(function calls) using the `+` operator. The next component you usually want to -specify in a `ggplot` "sentence" is our "aesthetic" mappings. These specify the -columns of the dataset that correspond to each axis of the plot, including the -x/y axes, but also the axes of color, shape, etc. We do this with the `aes()` -function: +You'll notice that the `ggplot()` function doesn't actually do much by itself. Here, we provide it with the penguins dataset, but the result looks like someone started making a plot and then stopped after the first step of making the rectangle for the plot area. This is because `ggplot2` is designed around the "grammar of graphics". Therefore, it expects you to build a sentence-like structure out of its functions. A single word (i.e., the call to `ggplot()` above) doesn't make much of a sentence, so let's start building up a real sentence. + +By using the `ggplot()` function, we are essentially stating that we are beginning a plotting "sentence". We then combine this with other "words" (function calls) using the `+` operator. The next component you usually want to specify in a `ggplot` "sentence" is our "aesthetic" mappings. These specify the columns of the dataset that correspond to each axis of the plot, including the x/y axes, but also the axes of color, shape, etc. We do this with the `aes()` function: ```{r} #| label: ggplot-aes @@ -86,9 +62,7 @@ ggplot(penguins) + aes(x = body_mass, y = flipper_len) ``` -Hey, it's starting to look like a plot now! Except there isn't any actual data -being plotted. Let's fix that. We'll start off with a simple scatter plot by -using the `geom_point()` function: +Hey, it's starting to look like a plot now! Except there isn't any actual data being plotted. Let's fix that. We'll start off with a simple scatter plot by using the `geom_point()` function: ```{r} #| label: geom_point @@ -98,16 +72,12 @@ ggplot(penguins) + geom_point() ``` -And there we go! You'll notice that with just a few lines, we've already made a -pretty nice visualization of this penguin data. `ggplot2` does most of the work -for us once we specify our dataset and our `x` and `y` variables. +And there we go! You'll notice that with just a few lines, we've already made a pretty nice visualization of this penguin data. `ggplot2` does most of the work for us once we specify our dataset and our `x` and `y` variables. ::: {.callout-note} ## Missing data -You may have noticed a warning that some rows of the dataset contain missing -values. There are two penguins without any measurements in the dataset. How -might we remove them using `dplyr` so we don't get this warning over and over? +You may have noticed a warning that some rows of the dataset contain missing values. There are two penguins without any measurements in the dataset. How might we remove them using `dplyr` so we don't get this warning over and over? ::::: {.callout-caution collapse="true" appearance="simple" icon="false"} ##### Solution @@ -119,9 +89,7 @@ penguins <- penguins |> ::::: ::: -Now, let's go a step further and color the points by another variable (e.g., the -island of the penguins). With `ggplot2`, all that requires is specifying another -aesthetic: +Now, let's go a step further and color the points by another variable (e.g., the island of the penguins). With `ggplot2`, all that requires is specifying another aesthetic: ```{r} #| label: geom_point-color @@ -131,17 +99,9 @@ ggplot(penguins) + geom_point() ``` -Notice that `ggplot2` comes with its own default color scheme. However, I would -strongly discourage you from using the default colors, especially as the number -of categories increases (with only 3 categories here it isn't too bad). Let's -try out some of the more accessible color palettes that are also available in R. +Notice that `ggplot2` comes with its own default color scheme. However, I would strongly discourage you from using the default colors, especially as the number of categories increases (with only 3 categories here it isn't too bad). Let's try out some of the more accessible color palettes that are also available in R. -First, let's try one of the -[`viridis`](https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html) -color palettes. Since this palette is included in `ggplot2`, all we need to do -is add the proper "scale" to our `ggplot()` call. Scales tell ggplot how to -handle a particular aesthetic, and are usually of the form -`scale_[aesthetic]_[type]()`. +First, let's try one of the [`viridis`](https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html) color palettes. Since this palette is included in `ggplot2`, all we need to do is add the proper "scale" to our `ggplot()` call. Scales tell ggplot how to handle a particular aesthetic, and are usually of the form `scale_[aesthetic]_[type]()`. ```{r} #| label: viridis @@ -152,8 +112,7 @@ ggplot(penguins) + scale_color_viridis_d(end = 0.7) # avoid yellow at the end of the palette ``` -Now let's try one of the [brewer color -palettes](https://r-graph-gallery.com/38-rcolorbrewers-palettes.html). +Now let's try one of the [brewer color palettes](https://r-graph-gallery.com/38-rcolorbrewers-palettes.html). ```{r} #| label: brewer @@ -164,10 +123,7 @@ ggplot(penguins) + scale_color_brewer(palette = "Set1") ``` -Outside of color, there are many other [aspects of the -graph](https://ggplot2.tidyverse.org/articles/ggplot2-specs.html) that we can -modify using aesthetics and "scale"s. For example, we can modify the shape of -the points: +Outside of color, there are many other [aspects of the graph](https://ggplot2.tidyverse.org/articles/ggplot2-specs.html) that we can modify using aesthetics and "scale"s. For example, we can modify the shape of the points: ```{r} #| label: shape-aes @@ -182,11 +138,7 @@ ggplot(penguins) + ::: {.callout-note} ### More missing data -Urgh, more missing data! It appears that nine of the penguins are missing sex -identification data. We can ignore the warning message, but that extra "NA" -category in the legend is quite annoying. We can remove this category from the -legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could -also use `scale_shape_manual()` if you wanted to supply your own shapes): +Urgh, more missing data! It appears that nine of the penguins are missing sex identification data. We can ignore the warning message, but that extra "NA" category in the legend is quite annoying. We can remove this category from the legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could also use `scale_shape_manual()` if you wanted to supply your own shapes): ```{r} #| label: na.translate @@ -217,11 +169,7 @@ ggplot(penguins) + # Theming -The last basic thing you might want to do with `ggplot2` is modify the style of -the visualization. This is extremely customizable, but the first place to start -is with a [built-in -theme](https://ggplot2.tidyverse.org/reference/ggtheme.html). I personally -prefer the classic theme, which looks very similar to base R plots: +The last basic thing you might want to do with `ggplot2` is modify the style of the visualization. This is extremely customizable, but the first place to start is with a [built-in theme](https://ggplot2.tidyverse.org/reference/ggtheme.html). I personally prefer the classic theme, which looks very similar to base R plots: ```{r} #| label: theme_classic @@ -237,12 +185,7 @@ ggplot(penguins) + theme_classic() ``` -Using this built-in theme has changed many visual aspects of the graph, -including changing the plot background color, adding axis lines, and removing -the internal grid lines. If you look very closely, however, the axis tick labels -are still a slight grey. We can use the `theme()` function to further customize -the appearance and change this. In this case, we'll make the axis text elements -have a black color instead of the default gray. +Using this built-in theme has changed many visual aspects of the graph, including changing the plot background color, adding axis lines, and removing the internal grid lines. If you look very closely, however, the axis tick labels are still a slight grey. We can use the `theme()` function to further customize the appearance and change this. In this case, we'll make the axis text elements have a black color instead of the default gray. ```{r} #| label: theming @@ -259,25 +202,15 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -And there we have it! With just 10 lines we've created what I would say is a -publication quality graph! `ggplot` does a lot of the tedious work for us, -giving us time to focus on the more important aspects, such as labeling and -color. Admittedly, I've spent a LOT of time on these aspects in the past... +And there we have it! With just 10 lines we've created what I would say is a publication quality graph! `ggplot` does a lot of the tedious work for us, giving us time to focus on the more important aspects, such as labeling and color. Admittedly, I've spent a LOT of time on these aspects in the past... -More information about all of the hierarchical theme components that you can -customize is available -[here](https://ggplot2.tidyverse.org/reference/theme.html). In order to change -many of these components, you need to use theme elements like we did above with -`element_text()`. That and other theme elements are documented -[here](https://ggplot2.tidyverse.org/reference/element.html). +More information about all of the hierarchical theme components that you can customize is available [here](https://ggplot2.tidyverse.org/reference/theme.html). In order to change many of these components, you need to use theme elements like we did above with `element_text()`. That and other theme elements are documented [here](https://ggplot2.tidyverse.org/reference/element.html). # More complex features ## Other layers -There are many other [types of -plots](https://ggplot2.tidyverse.org/reference/#layers) that we can make with -`ggplot2`. +There are many other [types of plots](https://ggplot2.tidyverse.org/reference/#layers) that we can make with `ggplot2`. ### Histograms @@ -294,9 +227,7 @@ ggplot(penguins) + ``` ::: {.callout-note} -Histograms don't require a y-axis aesthetic by default. The counts are tabulated -for you. If you specify a "fill" aesthetic, the default is to stack the bars -which can sometimes be a bit misleading. You can also dodge them to fix this: +Histograms don't require a y-axis aesthetic by default. The counts are tabulated for you. If you specify a "fill" aesthetic, the default is to stack the bars which can sometimes be a bit misleading. You can also dodge them to fix this: ```{r} #| label: hist-dodge @@ -311,8 +242,7 @@ ggplot(penguins) + ### Boxplots and Violin Plotss -We can visualize the density of values for a single variable across a discrete -variable with boxplots or violin plots: +We can visualize the density of values for a single variable across a discrete variable with boxplots or violin plots: ```{r} #| label: geom_boxplot @@ -337,17 +267,12 @@ ggplot(penguins) + ::: {.callout-note} #### Geom options -Note that many of these "geom"s have lots of options. For example, here we've -decided to `scale` all of the violin plots to the same width and to draw the -quartiles on them (mimicking the boxplots above). You can see all of the options -for a geom by checking out it's help page (`?geom_violin`) or on the ggplot -[website](https://ggplot2.tidyverse.org/reference/geom_violin.html). +Note that many of these "geom"s have lots of options. For example, here we've decided to `scale` all of the violin plots to the same width and to draw the quartiles on them (mimicking the boxplots above). You can see all of the options for a geom by checking out it's help page (`?geom_violin`) or on the ggplot [website](https://ggplot2.tidyverse.org/reference/geom_violin.html). ::: ### 2D Contours -We can also visualize the density of values across two continuous variables -using a 2D contour: +We can also visualize the density of values across two continuous variables using a 2D contour: ```{r} #| label: geom_density_2d @@ -358,8 +283,7 @@ ggplot(penguins) + theme_classic() + theme(axis.text = element_text(color = "black")) ``` -Note that sometimes you may need to expand the axes a little bit to better show -the contours: +Note that sometimes you may need to expand the axes a little bit to better show the contours: ```{r} #| label: geom_density_2d_expand @@ -374,10 +298,7 @@ ggplot(penguins) + ### Time Series -Since there isn't really any time series data in the penguins dataset, we'll -take a quick detour and use the built-in `economics` dataset to explore -visualizing a time series. In this case, we are looking at unemployment over -time: +Since there isn't really any time series data in the penguins dataset, we'll take a quick detour and use the built-in `economics` dataset to explore visualizing a time series. In this case, we are looking at unemployment over time: ```{r} #| label: geom_line @@ -388,8 +309,7 @@ ggplot(economics, aes(x = date, y = unemploy)) + theme(axis.text = element_text(color = "black")) ``` -`geom_path()` lets you explore how two variables are related over time. For -example, unemployment and personal savings rate: +`geom_path()` lets you explore how two variables are related over time. For example, unemployment and personal savings rate: ```{r} #| label: geom_path @@ -403,16 +323,12 @@ ggplot(economics, aes(x = unemploy / pop, y = psavert)) + ::: {.callout-note} #### Multiple columns for individual aesthetics -Note how we've used multiple columns of the data to define the x-axis here. You -can use any sort of mathematical operators to combine multiple columns into a -single aesthetic, as long as you are doing row-wise math. +Note how we've used multiple columns of the data to define the x-axis here. You can use any sort of mathematical operators to combine multiple columns into a single aesthetic, as long as you are doing row-wise math. ::: ## Combining layers -We can also combine multiple layers to show the same data in different ways in -the same plot. For example, we could show the raw data for the above contour -plot in addition to the contours: +We can also combine multiple layers to show the same data in different ways in the same plot. For example, we could show the raw data for the above contour plot in addition to the contours: ```{r} #| label: combined-layers @@ -432,12 +348,7 @@ ggplot(penguins) + ::: {.callout-note} ### Layer order -When combining layers, the layers are added to the plot in order, so in this -case the points are the bottom layer and the contour lines are the top layer. We -changed the alpha of the middle layer to prevent the points from being blocked. -I've also used the `coord_cartesian()` function to remove the default axis -expansion. This way the background color reaches both axes and doesn't have a -white gap. +When combining layers, the layers are added to the plot in order, so in this case the points are the bottom layer and the contour lines are the top layer. We changed the alpha of the middle layer to prevent the points from being blocked. I've also used the `coord_cartesian()` function to remove the default axis expansion. This way the background color reaches both axes and doesn't have a white gap. ::: ## Facetting @@ -459,11 +370,7 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -Now, what if we wanted to also split the data by the species of the penguins? -We're already using color and shape, so what other aesthetic could we use? We -could possible use some shapes that have both a fill and outline color, but that -sounds messy. Instead of using another aesthetic, we could also use a `facet`. -This splits the chart into multiple panels: +Now, what if we wanted to also split the data by the species of the penguins? We're already using color and shape, so what other aesthetic could we use? We could possible use some shapes that have both a fill and outline color, but that sounds messy. Instead of using another aesthetic, we could also use a `facet`. This splits the chart into multiple panels: ```{r} #| label: scatter-facet @@ -503,13 +410,7 @@ OK, maybe we've gone a little too far here, but you get the picture! # Combining plots -When publishing results, often we need to combine multiple figures into a single -visualization. There are lots of packages for accomplishing this (even my own -[deeptime](https://williamgearty.com/deeptime) package has some functionality -for it), but today we'll check out the -[`patchwork`](https://patchwork.data-imaginist.com/) package which extends the -"grammar of graphics" to combining plots (you may need to install it if you -haven't done so already). +When publishing results, often we need to combine multiple figures into a single visualization. There are lots of packages for accomplishing this (even my own [deeptime](https://williamgearty.com/deeptime) package has some functionality for it), but today we'll check out the [`patchwork`](https://patchwork.data-imaginist.com/) package which extends the "grammar of graphics" to combining plots (you may need to install it if you haven't done so already). ```{r} #| label: patchwork @@ -517,9 +418,7 @@ haven't done so already). library(patchwork) ``` -First, let's go ahead and make some plots. Instead of plotting them, though, -we'll save them as objects in our environment. Note that when you save a plot to -an object it isn't displayed in the "Plots" tab. +First, let's go ahead and make some plots. Instead of plotting them, though, we'll save them as objects in our environment. Note that when you save a plot to an object it isn't displayed in the "Plots" tab. ```{r} #| label: plot-objects @@ -542,8 +441,7 @@ g2 <- ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -Now, in order to combine these, all we need to do is combine them using the `+` -operator, like we did with the individual elements of the plots. +Now, in order to combine these, all we need to do is combine them using the `+` operator, like we did with the individual elements of the plots. ```{r} #| label: add-plots @@ -551,10 +449,7 @@ operator, like we did with the individual elements of the plots. g1 + g2 ``` -You can see that `patchwork` does all of the work for us, lining up the -different components of the plots. If we have more plots to combine, we can then -use the `|` (side-by-side) and `/` (above-and-below) operators to make more -complex arrangements of plots. +You can see that `patchwork` does all of the work for us, lining up the different components of the plots. If we have more plots to combine, we can then use the `|` (side-by-side) and `/` (above-and-below) operators to make more complex arrangements of plots. ```{r} #| label: patchwork-complex @@ -568,24 +463,11 @@ g3 <- ggplot(penguins) + (g1 | g2) / g3 ``` -There's a lot more you can do with `patchwork`, including adjusting the widths -and heights, but we'll stop here for now. +There's a lot more you can do with `patchwork`, including adjusting the widths and heights, but we'll stop here for now. # Saving plots -The last big thing you'll need to know about plotting is how to save your plots. -`ggplot2` comes with a nifty `ggsave()` function which you can use to save your -plots in a number of different formats. Here we'll save our most recent combined -plot as both a PDF and a JPEG. The former is a vector format, meaning all of the -elements of the figure as geometric shapes, and, because of this, none of the -data is lost (aka "lossless"). The latter is a raster format, meaning the figure -is converted to a 2-D array of colored pixels of a desired size, and because of -this, some of the data is lost in the process (aka "lossy"). This results in the -pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what -format you want based on the file extension, so we just need to specify the file -location and the object to be saved (and optionally the dimensions of the output -file). It's usually a good idea to keep all figures in their own folder (which -was already created for you). +The last big thing you'll need to know about plotting is how to save your plots. `ggplot2` comes with a nifty `ggsave()` function which you can use to save your plots in a number of different formats. Here we'll save our most recent combined plot as both a PDF and a JPEG. The former is a vector format, meaning all of the elements of the figure as geometric shapes, and, because of this, none of the data is lost (aka "lossless"). The latter is a raster format, meaning the figure is converted to a 2-D array of colored pixels of a desired size, and because of this, some of the data is lost in the process (aka "lossy"). This results in the pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what format you want based on the file extension, so we just need to specify the file location and the object to be saved (and optionally the dimensions of the output file). It's usually a good idea to keep all figures in their own folder (which was already created for you). ```{r} #| label: ggsave @@ -596,9 +478,4 @@ ggsave("figures/penguins_1.pdf", gg, height = 10, width = 10) ggsave("figures/penguins_1.jpg", gg, height = 10, width = 10) ``` -Now we'll once again use GitHub Desktop to commit these files and push them to -our GitHub repository. You should see all of the files that have been modified -since we last committed. In this case, you should see your code file (modified -from our last session) and your two new figures. Make sure the checkboxes are -checked for these three files, write a succinct commit summary, then click the -Commit button, then the Push button. And that's that! +Now we'll once again use GitHub Desktop to commit these files and push them to our GitHub repository. You should see all of the files that have been modified since we last committed. In this case, you should see your code file (modified from our last session) and your two new figures. Make sure the checkboxes are checked for these three files, write a succinct commit summary, then click the Commit button, then the Push button. And that's that! diff --git a/panache.toml b/panache.toml new file mode 100644 index 0000000..c9b394a --- /dev/null +++ b/panache.toml @@ -0,0 +1,2 @@ +[format] +wrap = "preserve" \ No newline at end of file From 96c871d3a09acb3725aeffaa91b433ba382a0a62 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:33:38 +0100 Subject: [PATCH 05/13] I hate git conflicts --- ggplot/index.qmd | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ggplot/index.qmd b/ggplot/index.qmd index 6227082..3e6fa0e 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -71,6 +71,7 @@ By using the `ggplot()` function, we are essentially stating that we are beginni ::: ```{r ggplot-aes} +#| code-line-numbers: "2" ggplot(penguins) + aes(x = body_mass, y = flipper_len) ``` @@ -80,6 +81,7 @@ Hey, it's starting to look like a plot now! Except there isn't any actual data b ::: ```{r geom_point} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = body_mass, y = flipper_len) + geom_point() @@ -109,6 +111,7 @@ Now, let's go a step further and color the points by another variable (e.g., the ::: ```{r geom_point-color} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -122,6 +125,7 @@ First, let's try one of the [`viridis`](https://cran.r-project.org/web/packages/ ::: ```{r viridis} +#| code-line-numbers: "5" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -134,6 +138,7 @@ Now let's try one of the [brewer color palettes](https://r-graph-gallery.com/38- ::: ```{r brewer} +#| code-line-numbers: "5" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -146,6 +151,7 @@ Outside of color, there are many other [aspects of the graph](https://ggplot2.ti ::: ```{r shape-aes} +#| code-line-numbers: "4" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -160,6 +166,7 @@ ggplot(penguins) + Urgh, more missing data! It appears that nine of the penguins are missing sex identification data. We can ignore the warning message, but that extra "NA" category in the legend is quite annoying. We can remove this category from the legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could also use `scale_shape_manual()` if you wanted to supply your own shapes): ```{r na.translate} +#| code-line-numbers: "7" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -175,6 +182,7 @@ And the x/y axes: ::: ```{r axis-scales} +#| code-line-numbers: "8-9" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -193,6 +201,7 @@ The last basic thing you might want to do with `ggplot2` is modify the style of ::: ```{r theme_classic} +#| code-line-numbers: "10" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -210,6 +219,7 @@ Using this built-in theme has changed many visual aspects of the graph, includin ::: ```{r theming} +#| code-line-numbers: "11" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -244,6 +254,7 @@ We can visualize the density of values for a single variable with a histogram: ::: ```{r geom_histogram} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram() + @@ -255,6 +266,7 @@ ggplot(penguins) + Histograms don't require a y-axis aesthetic by default. The counts are tabulated for you. If you specify a "fill" aesthetic, the default is to stack the bars which can sometimes be a bit misleading. You can also dodge them to fix this: ```{r hist-dodge} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram(position = "dodge") + @@ -270,6 +282,7 @@ We can visualize the density of values for a single variable across a discrete v ::: ```{r geom_boxplot} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = island, y = bill_len) + geom_boxplot() + @@ -278,6 +291,7 @@ ggplot(penguins) + ``` ```{r geom_violin} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = island, y = bill_len) + geom_violin(scale = "width", draw_quantiles = c(0.25, 0.5, 0.75)) + @@ -298,6 +312,7 @@ We can also visualize the density of values across two continuous variables usin ::: ```{r geom_density_2d} +#| code-line-numbers: "3" ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + @@ -310,6 +325,7 @@ Note that sometimes you may need to expand the axes a little bit to better show ::: ```{r geom_density_2d_expand} +#| code-line-numbers: "4" ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + @@ -401,6 +417,7 @@ Now, what if we wanted to also split the data by the species of the penguins? We ::: ```{r scatter-facet} +#| code-line-numbers: "9" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -419,6 +436,7 @@ We can get even crazier by faceting by multiple variables: ::: ```{r scatter-facet-grid} +#| code-line-numbers: "9" ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + From a46047e2e0de6d30345c4f14b7916dca677c6215 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:34:30 +0100 Subject: [PATCH 06/13] same --- ggplot/index.qmd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ggplot/index.qmd b/ggplot/index.qmd index 3e6fa0e..c94dbf7 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -517,9 +517,12 @@ g3 <- ggplot(penguins) + (g1 | g2) / g3 ``` -There's a lot more you can do with `patchwork`, including adjusting the widths and heights, but we'll stop here for now. +::: {.narration} +There's a lot more you can do with `patchwork`, including [adjusting the widths and heights](https://patchwork.data-imaginist.com/articles/guides/layout.html) and [adding annotations and styling](https://patchwork.data-imaginist.com/articles/guides/annotation.html), but we'll stop here for now. +::: + +## Saving plots -# Saving plots The last big thing you'll need to know about plotting is how to save your plots. `ggplot2` comes with a nifty `ggsave()` function which you can use to save your plots in a number of different formats. Here we'll save our most recent combined plot as both a PDF and a JPEG. The former is a vector format, meaning all of the elements of the figure as geometric shapes, and, because of this, none of the data is lost (aka "lossless"). The latter is a raster format, meaning the figure is converted to a 2-D array of colored pixels of a desired size, and because of this, some of the data is lost in the process (aka "lossy"). This results in the pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what format you want based on the file extension, so we just need to specify the file location and the object to be saved (and optionally the dimensions of the output file). It's usually a good idea to keep all figures in their own folder (which was already created for you). ::: From 499c056383219354f9ed5a5bbade5b1bc6cf13d8 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:35:17 +0100 Subject: [PATCH 07/13] same --- ggplot/index.qmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ggplot/index.qmd b/ggplot/index.qmd index c94dbf7..08f76b5 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -456,7 +456,7 @@ OK, maybe we've gone a little too far here, but you get the picture! ## Combining plots -# Combining plots +::: {.narration} When publishing results, often we need to combine multiple figures into a single visualization. There are lots of packages for accomplishing this (even my own [deeptime](https://williamgearty.com/deeptime) package has some functionality for it), but today we'll check out the [`patchwork`](https://patchwork.data-imaginist.com/) package which extends the "grammar of graphics" to combining plots (you may need to install it if you haven't done so already). ::: @@ -523,6 +523,7 @@ There's a lot more you can do with `patchwork`, including [adjusting the widths ## Saving plots +::: {.narration} The last big thing you'll need to know about plotting is how to save your plots. `ggplot2` comes with a nifty `ggsave()` function which you can use to save your plots in a number of different formats. Here we'll save our most recent combined plot as both a PDF and a JPEG. The former is a vector format, meaning all of the elements of the figure as geometric shapes, and, because of this, none of the data is lost (aka "lossless"). The latter is a raster format, meaning the figure is converted to a 2-D array of colored pixels of a desired size, and because of this, some of the data is lost in the process (aka "lossy"). This results in the pixellation that you see when you zoom in on a JPEG. `ggsave()` detects what format you want based on the file extension, so we just need to specify the file location and the object to be saved (and optionally the dimensions of the output file). It's usually a good idea to keep all figures in their own folder (which was already created for you). ::: From c509b271a4b1296f19074292b7b42dd89436a8f1 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:35:46 +0100 Subject: [PATCH 08/13] typo in file name --- .github/workflows/format-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format-lint.yml b/.github/workflows/format-lint.yml index 610f881..e18b087 100644 --- a/.github/workflows/format-lint.yml +++ b/.github/workflows/format-lint.yml @@ -1,6 +1,6 @@ # This workflow runs Panache for formatting and Jarl for linting. It stores artifacts about the # outcome of each tool and the PR number. Those artifacts are then used in the workflow -# "format-lint-pr-comment.yaml" to post a comment so that it's clearer for external +# "format-lint-comment.yaml" to post a comment so that it's clearer for external # contributors. # # This could have been done in a single workflow with a "pull_request_target" trigger but From 152520a0ba9b7d3da7d2993611faffcf2c1ea4e4 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:36:28 +0100 Subject: [PATCH 09/13] add dependabot --- .github/dependabot.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..96ab73a --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + cooldown: + default-days: 14 + groups: + ci: + patterns: + - '*' \ No newline at end of file From 55195119bdf86f6db128da156ff9fad499909c83 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:37:27 +0100 Subject: [PATCH 10/13] run panache format --- _templates/template_long_format.qmd | 1 + _templates/template_single_file.qmd | 6 +- _templates/template_single_file_draft.qmd | 14 ++-- .../template_single_file_interleaved.qmd | 15 ++-- _templates/template_single_file_live.qmd | 4 +- _templates/template_slides.qmd | 33 ++++---- ggplot/index.qmd | 81 ++++++++++++++----- 7 files changed, 99 insertions(+), 55 deletions(-) diff --git a/_templates/template_long_format.qmd b/_templates/template_long_format.qmd index 8057699..a474df8 100644 --- a/_templates/template_long_format.qmd +++ b/_templates/template_long_format.qmd @@ -41,6 +41,7 @@ Same code chunk but not executed: ```{r} #| eval: false + plot(mtcars) ``` diff --git a/_templates/template_single_file.qmd b/_templates/template_single_file.qmd index 9199e40..374e361 100644 --- a/_templates/template_single_file.qmd +++ b/_templates/template_single_file.qmd @@ -40,8 +40,8 @@ the page and separate notes on the slide. ::: ::: {.slides-only} -- a bullet summary for the slide -- which would be redundant on the page +- a bullet summary for the slide +- which would be redundant on the page ::: ::: {.html-only} @@ -112,11 +112,13 @@ A callout gets a slide of its own. The content is un-boxed and the slide is titl ::: {.callout-note} ## A note + This box is dropped on the slide, which lets a figure inside it stretch to fit. ::: ::: {.callout-caution collapse="true"} ## An exercise solution + A *collapsed* callout is the exception: it keeps its box. Since clicking on slides can be awkward, the collapsed content is held back as a fragment and revealed when you advance. On the website it stays a click-to-open box. diff --git a/_templates/template_single_file_draft.qmd b/_templates/template_single_file_draft.qmd index 5829b1d..a98e4a1 100644 --- a/_templates/template_single_file_draft.qmd +++ b/_templates/template_single_file_draft.qmd @@ -50,18 +50,20 @@ plot(mtcars$hp, mtcars$mpg) Lists, tables, images and blockquotes are left alone by the script so they appear in both outputs as-is: -- a list item -- another list item +- a list item +- another list item A callout is left alone too, and gets a slide of its own: ::: {.callout-note} ## A note + Give a callout a heading like this and the heading becomes the slide title. ::: ::: {.callout-caution collapse="true"} ## An exercise solution + A collapsed callout stays collapsed: on the website it is a click-to-open box, and on the slides it keeps its box with the content hidden until you advance. ::: @@ -70,10 +72,10 @@ on the slides it keeps its box with the content hidden until you advance. Once you have run the script, look through the output for: -1. headings the script flagged as awkward for slides, -2. places where the two outputs should differ: add `::: {.slides-only}` or - `::: {.html-only}` blocks by hand, -3. narration blocks that should be split or merged. +1. headings the script flagged as awkward for slides, +2. places where the two outputs should differ: add `::: {.slides-only}` or + `::: {.html-only}` blocks by hand, +3. narration blocks that should be split or merged. From then on, edit the converted file. Keep this draft only if you would rather keep iterating on the prose and re-generate. diff --git a/_templates/template_single_file_interleaved.qmd b/_templates/template_single_file_interleaved.qmd index 0c4afd8..c429f32 100644 --- a/_templates/template_single_file_interleaved.qmd +++ b/_templates/template_single_file_interleaved.qmd @@ -37,8 +37,8 @@ Consecutive paragraphs can share one block. ::: {.content-visible when-format="revealjs"} Content for the slides only goes in a `.content-visible when-format="revealjs"` block. -- a bullet summary for the slide -- which would be redundant on the page +- a bullet summary for the slide +- which would be redundant on the page ::: ::: {.content-hidden when-format="revealjs"} @@ -79,8 +79,8 @@ A first point. A second point, one advance later. ::: {.incremental} -- then this -- then this +- then this +- then this ::: ## Handling a long slide @@ -112,16 +112,17 @@ visible. If you want to hide a solution until you advance, wrap it in a ::: {.callout-note} ## A note + Visible in both outputs, box and all. ::: -:::: {.callout-caution} +::: {.callout-caution} ## An exercise solution -::: {.fragment} +::::: {.fragment} Wrapped in a `.fragment`, so it appears on the next advance in the deck while staying plain prose on the website. +::::: ::: -:::: ## More info diff --git a/_templates/template_single_file_live.qmd b/_templates/template_single_file_live.qmd index 12357fa..20064b1 100644 --- a/_templates/template_single_file_live.qmd +++ b/_templates/template_single_file_live.qmd @@ -51,8 +51,8 @@ the file(s). You can install various packages in the `webr.packages` list. ::: ::: {.slides-only} -- readers can edit and run the code here -- nothing is installed, nothing is sent to a server +- readers can edit and run the code here +- nothing is installed, nothing is sent to a server ::: ## An editable cell diff --git a/_templates/template_slides.qmd b/_templates/template_slides.qmd index 72c8794..485775a 100644 --- a/_templates/template_slides.qmd +++ b/_templates/template_slides.qmd @@ -110,7 +110,6 @@ Use `![](path/to/file.png)` to add an image: ![](img/2d_density.png) - ## Images
@@ -123,7 +122,6 @@ e.g. `{width="25%" height="25%"}`: ![](img/2d_density.png){width="25%" height="25%"} - ## Code formatting
@@ -138,7 +136,6 @@ We can write code chunks with three backticks and the name of the language, like mean(x) ``` - ## Code evaluation
@@ -152,11 +149,11 @@ Below, I hide the original code but show its output: ```{r} #| echo: false #| eval: true + x <- 1:5 mean(x) ``` - ## Highlight lines of code
@@ -169,6 +166,7 @@ Using `#| code-line-numbers: "1|2-5|6|"` will highlight the first line, then lin ```{r} #| code-line-numbers: "1|2-5|6|" + x <- 1 y <- paste( "hello there", @@ -219,18 +217,17 @@ Right column
-:::: {.columns} +::: {.columns} -::: {.column width="40%"} +::::: {.column width="40%"} Left column -::: +::::: -::: {.column width="60%"} +::::: {.column width="60%"} Right column -::: - -:::: +::::: +::: ## Write on several columns @@ -240,22 +237,23 @@ You can put any content in each column, such as plots:
-:::: {.columns} +::: {.columns} -::: {.column width="40%"} +::::: {.column width="40%"} Left column -::: +::::: -::: {.column width="60%"} +::::: {.column width="60%"} ```{r} #| eval: true + plot(iris) ``` -::: +::::: -:::: +::: ## More info @@ -263,4 +261,3 @@ plot(iris)
[https://quarto.org/docs/presentations/revealjs/](https://quarto.org/docs/presentations/revealjs/) - diff --git a/ggplot/index.qmd b/ggplot/index.qmd index 08f76b5..3820380 100644 --- a/ggplot/index.qmd +++ b/ggplot/index.qmd @@ -24,6 +24,7 @@ filters: - at: pre-ast path: web_and_slides_autogenerated.lua --- + ## Introduction ::: {.narration} @@ -70,8 +71,10 @@ You'll notice that the `ggplot()` function doesn't actually do much by itself. H By using the `ggplot()` function, we are essentially stating that we are beginning a plotting "sentence". We then combine this with other "words" (function calls) using the `+` operator. The next component you usually want to specify in a `ggplot` "sentence" is our "aesthetic" mappings. These specify the columns of the dataset that correspond to each axis of the plot, including the x/y axes, but also the axes of color, shape, etc. We do this with the `aes()` function: ::: -```{r ggplot-aes} +```{r} +#| label: ggplot-aes #| code-line-numbers: "2" + ggplot(penguins) + aes(x = body_mass, y = flipper_len) ``` @@ -80,8 +83,10 @@ ggplot(penguins) + Hey, it's starting to look like a plot now! Except there isn't any actual data being plotted. Let's fix that. We'll start off with a simple scatter plot by using the `geom_point()` function: ::: -```{r geom_point} +```{r} +#| label: geom_point #| code-line-numbers: "3" + ggplot(penguins) + aes(x = body_mass, y = flipper_len) + geom_point() @@ -110,8 +115,10 @@ penguins <- penguins |> Now, let's go a step further and color the points by another variable (e.g., the island of the penguins). With `ggplot2`, all that requires is specifying another aesthetic: ::: -```{r geom_point-color} +```{r} +#| label: geom_point-color #| code-line-numbers: "3" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -124,8 +131,10 @@ Notice that `ggplot2` comes with its own default color scheme. However, I would First, let's try one of the [`viridis`](https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html) color palettes. Since this palette is included in `ggplot2`, all we need to do is add the proper "scale" to our `ggplot()` call. Scales tell ggplot how to handle a particular aesthetic, and are usually of the form `scale_[aesthetic]_[type]()`. ::: -```{r viridis} +```{r} +#| label: viridis #| code-line-numbers: "5" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -137,8 +146,10 @@ ggplot(penguins) + Now let's try one of the [brewer color palettes](https://r-graph-gallery.com/38-rcolorbrewers-palettes.html). ::: -```{r brewer} +```{r} +#| label: brewer #| code-line-numbers: "5" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island) + @@ -150,8 +161,10 @@ ggplot(penguins) + Outside of color, there are many other [aspects of the graph](https://ggplot2.tidyverse.org/articles/ggplot2-specs.html) that we can modify using aesthetics and "scale"s. For example, we can modify the shape of the points: ::: -```{r shape-aes} +```{r} +#| label: shape-aes #| code-line-numbers: "4" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -165,8 +178,10 @@ ggplot(penguins) + Urgh, more missing data! It appears that nine of the penguins are missing sex identification data. We can ignore the warning message, but that extra "NA" category in the legend is quite annoying. We can remove this category from the legend using `na.translate = FALSE` within `scale_shape_discrete()` (you could also use `scale_shape_manual()` if you wanted to supply your own shapes): -```{r na.translate} +```{r} +#| label: na.translate #| code-line-numbers: "7" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -181,8 +196,10 @@ ggplot(penguins) + And the x/y axes: ::: -```{r axis-scales} +```{r} +#| label: axis-scales #| code-line-numbers: "8-9" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -200,8 +217,10 @@ ggplot(penguins) + The last basic thing you might want to do with `ggplot2` is modify the style of the visualization. This is extremely customizable, but the first place to start is with a [built-in theme](https://ggplot2.tidyverse.org/reference/ggtheme.html). I personally prefer the classic theme, which looks very similar to base R plots: ::: -```{r theme_classic} +```{r} +#| label: theme_classic #| code-line-numbers: "10" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -218,8 +237,10 @@ ggplot(penguins) + Using this built-in theme has changed many visual aspects of the graph, including changing the plot background color, adding axis lines, and removing the internal grid lines. If you look very closely, however, the axis tick labels are still a slight grey. We can use the `theme()` function to further customize the appearance and change this. In this case, we'll make the axis text elements have a black color instead of the default gray. ::: -```{r theming} +```{r} +#| label: theming #| code-line-numbers: "11" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, @@ -253,8 +274,10 @@ There are many other [types of plots](https://ggplot2.tidyverse.org/reference/#l We can visualize the density of values for a single variable with a histogram: ::: -```{r geom_histogram} +```{r} +#| label: geom_histogram #| code-line-numbers: "3" + ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram() + @@ -265,8 +288,10 @@ ggplot(penguins) + ::: {.callout-note} Histograms don't require a y-axis aesthetic by default. The counts are tabulated for you. If you specify a "fill" aesthetic, the default is to stack the bars which can sometimes be a bit misleading. You can also dodge them to fix this: -```{r hist-dodge} +```{r} +#| label: hist-dodge #| code-line-numbers: "3" + ggplot(penguins) + aes(x = body_mass, fill = species) + geom_histogram(position = "dodge") + @@ -281,8 +306,10 @@ ggplot(penguins) + We can visualize the density of values for a single variable across a discrete variable with boxplots or violin plots: ::: -```{r geom_boxplot} +```{r} +#| label: geom_boxplot #| code-line-numbers: "3" + ggplot(penguins) + aes(x = island, y = bill_len) + geom_boxplot() + @@ -290,8 +317,10 @@ ggplot(penguins) + theme(axis.text = element_text(color = "black")) ``` -```{r geom_violin} +```{r} +#| label: geom_violin #| code-line-numbers: "3" + ggplot(penguins) + aes(x = island, y = bill_len) + geom_violin(scale = "width", draw_quantiles = c(0.25, 0.5, 0.75)) + @@ -311,8 +340,10 @@ Note that many of these "geom"s have lots of options. For example, here we've de We can also visualize the density of values across two continuous variables using a 2D contour: ::: -```{r geom_density_2d} +```{r} +#| label: geom_density_2d #| code-line-numbers: "3" + ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + @@ -324,8 +355,10 @@ ggplot(penguins) + Note that sometimes you may need to expand the axes a little bit to better show the contours: ::: -```{r geom_density_2d_expand} +```{r} +#| label: geom_density_2d_expand #| code-line-numbers: "4" + ggplot(penguins) + aes(x = bill_len, y = bill_dep) + geom_density_2d(linewidth = 0.25, colour = "black") + @@ -340,7 +373,9 @@ ggplot(penguins) + Since there isn't really any time series data in the penguins dataset, we'll take a quick detour and use the built-in `economics` dataset to explore visualizing a time series. In this case, we are looking at unemployment over time: ::: -```{r geom_line} +```{r} +#| label: geom_line + ggplot(economics, aes(x = date, y = unemploy)) + geom_line() + theme_classic() + @@ -351,7 +386,9 @@ ggplot(economics, aes(x = date, y = unemploy)) + `geom_path()` lets you explore how two variables are related over time. For example, unemployment and personal savings rate: ::: -```{r geom_path} +```{r} +#| label: geom_path + ggplot(economics, aes(x = unemploy / pop, y = psavert)) + geom_path(aes(colour = as.numeric(date))) + theme_classic() + @@ -416,8 +453,10 @@ ggplot(penguins) + Now, what if we wanted to also split the data by the species of the penguins? We're already using color and shape, so what other aesthetic could we use? We could possible use some shapes that have both a fill and outline color, but that sounds messy. Instead of using another aesthetic, we could also use a `facet`. This splits the chart into multiple panels: ::: -```{r scatter-facet} +```{r} +#| label: scatter-facet #| code-line-numbers: "9" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + @@ -435,8 +474,10 @@ ggplot(penguins) + We can get even crazier by faceting by multiple variables: ::: -```{r scatter-facet-grid} +```{r} +#| label: scatter-facet-grid #| code-line-numbers: "9" + ggplot(penguins) + aes(x = body_mass, y = flipper_len, color = island, shape = sex) + From 2368caebea1588d715bd84f86b04ee5b6bc991a7 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:38:21 +0100 Subject: [PATCH 11/13] jarl --- web_and_slides.r | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_and_slides.r b/web_and_slides.r index ad4fcdc..b2d0075 100644 --- a/web_and_slides.r +++ b/web_and_slides.r @@ -255,7 +255,7 @@ write_lines(c(yaml, out), output) report_headings <- function(yaml, body, offset, ref) { sl <- 2L # revealjs default slide-level m <- str_match(yaml, "^\\s*slide-level:\\s*(\\d+)") - if (any(!is.na(m[, 2]))) sl <- as.integer(m[!is.na(m[, 2]), 2][1]) + if (!all(is.na(m[, 2]))) sl <- as.integer(m[!is.na(m[, 2]), 2][1]) in_code <- FALSE; depth <- 0L real <- rep(FALSE, length(body)); lvl <- integer(length(body)) From 07f5c1619e8ae1251ae230780ae149670c972dc1 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:40:23 +0100 Subject: [PATCH 12/13] panache again --- _templates/template_single_file.qmd | 2 +- _templates/template_single_file_live.qmd | 2 +- _templates/template_slides.qmd | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/_templates/template_single_file.qmd b/_templates/template_single_file.qmd index 374e361..a6709ec 100644 --- a/_templates/template_single_file.qmd +++ b/_templates/template_single_file.qmd @@ -120,7 +120,7 @@ This box is dropped on the slide, which lets a figure inside it stretch to fit. ## An exercise solution A *collapsed* callout is the exception: it keeps its box. Since clicking on -slides can be awkward, the collapsed content is held back as a fragment and +slides can be awkward, the collapsed content is held back as a fragment and revealed when you advance. On the website it stays a click-to-open box. ::: diff --git a/_templates/template_single_file_live.qmd b/_templates/template_single_file_live.qmd index 20064b1..1c9e29b 100644 --- a/_templates/template_single_file_live.qmd +++ b/_templates/template_single_file_live.qmd @@ -45,7 +45,7 @@ The only cost to the user is a WebAssembly download on the first run, including package dependencies. Notice that you need to use different `format`s for the two -outputs and set `engine` to `knitr`. You also must install the `r-wasm/live` +outputs and set `engine` to `knitr`. You also must install the `r-wasm/live` extension in the module's directory and include the `_knitr.qmd` file at the top of the file(s). You can install various packages in the `webr.packages` list. ::: diff --git a/_templates/template_slides.qmd b/_templates/template_slides.qmd index 485775a..26a82c2 100644 --- a/_templates/template_slides.qmd +++ b/_templates/template_slides.qmd @@ -218,7 +218,6 @@ Right column
::: {.columns} - ::::: {.column width="40%"} Left column ::::: @@ -226,7 +225,6 @@ Left column ::::: {.column width="60%"} Right column ::::: - ::: ## Write on several columns @@ -238,21 +236,17 @@ You can put any content in each column, such as plots:
::: {.columns} - ::::: {.column width="40%"} Left column ::::: ::::: {.column width="60%"} - ```{r} #| eval: true plot(iris) ``` - ::::: - ::: ## More info From 20a7632adac6a37488533b70a297ed0c4c977d92 Mon Sep 17 00:00:00 2001 From: etiennebacher Date: Mon, 14 Sep 2026 17:43:05 +0100 Subject: [PATCH 13/13] also format readme --- README.md | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ed6a6d0..ac2e310 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ Directory of teaching modules built by the Palaeoverse team. Every module should contain Quarto (.qmd) content that renders a long-form website page (for async learning) plus a reveal.js slide deck (for live teaching). There are many ways to accomplish this; we've outlined and templated the three strategies that we think work well below. Pick one of them and start from its template in [`_templates`](_templates): -| | Strategy | Start from | -| --- | --- | --- | -| 1 | **One file, slide layout by filter.** A single `index.qmd` registers the [`web_and_slides.lua`](web_and_slides.lua) filter, which turns your prose into speaker notes and breaks the remaining content into slides for you (ideal if you are new to Quarto). | [`template_single_file.qmd`](_templates/template_single_file.qmd) | -| 2 | **One file, interleaved by hand.** A single `index.qmd` using plain Quarto conditional content (ideal if you are comfortable with Quarto syntax and don't have too many customizations). | [`template_single_file_interleaved.qmd`](_templates/template_single_file_interleaved.qmd) | -| 3 | **Two separate files.** A long-form document and a slide deck, maintained side by side (ideal if you really need customized content for both formats). | [`template_long_format.qmd`](_templates/template_long_format.qmd) + [`template_slides.qmd`](_templates/template_slides.qmd) | + | | Strategy | Start from | + | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | + | 1 | **One file, slide layout by filter.** A single `index.qmd` registers the [`web_and_slides.lua`](web_and_slides.lua) filter, which turns your prose into speaker notes and breaks the remaining content into slides for you (ideal if you are new to Quarto). | [`template_single_file.qmd`](_templates/template_single_file.qmd) | + | 2 | **One file, interleaved by hand.** A single `index.qmd` using plain Quarto conditional content (ideal if you are comfortable with Quarto syntax and don't have too many customizations). | [`template_single_file_interleaved.qmd`](_templates/template_single_file_interleaved.qmd) | + | 3 | **Two separate files.** A long-form document and a slide deck, maintained side by side (ideal if you really need customized content for both formats). | [`template_long_format.qmd`](_templates/template_long_format.qmd) + [`template_slides.qmd`](_templates/template_slides.qmd) | Strategies 1 and 2 render one file to both formats, so Quarto links the page and the deck for you under "Other Formats". Strategy 3 renders two independent @@ -32,6 +32,7 @@ layout it applies). You can get there two ways: - **Tag as you go.** Copy [`template_single_file.qmd`](_templates/template_single_file.qmd) and write with the blocks from the start. + - **Write long-form first, then convert.** Draft the module as an ordinary written tutorial (see [`template_single_file_draft.qmd`](_templates/template_single_file_draft.qmd) for an example) and run `web_and_slides.r` over it once: @@ -53,11 +54,11 @@ regenerate. Once the filter is registered and copied into the module directory, you get the following automatic behavior: - - slides close after each figure - - deeper headings become their own slides - - callouts keep their boxes (and collapsed callouts stay collapsed) - - content on long slides is shrunk as needed - - back-to-back code chunks are revealed one at a time +- slides close after each figure +- deeper headings become their own slides +- callouts keep their boxes (and collapsed callouts stay collapsed) +- content on long slides is shrunk as needed +- back-to-back code chunks are revealed one at a time ## 2. One file, interleaved by hand @@ -67,11 +68,11 @@ the way._ Same single-file idea, but using Quarto's own conditional content instead of our classes, so there is nothing to generate and nothing to register: -| Class | Behavior | -| --- | --- | -| `::: {.notes}` | Quarto renders it as prose on the page and as speaker notes in the slide deck | -| `::: {.content-visible when-format="revealjs"}` | Content is only rendered on the slides, not on the page | -| `::: {.content-hidden when-format="revealjs"}` | Content is only rendered on the page, not on the slides | + | Class | Behavior | + | ----------------------------------------------- | ----------------------------------------------------------------------------- | + | `::: {.notes}` | Quarto renders it as prose on the page and as speaker notes in the slide deck | + | `::: {.content-visible when-format="revealjs"}` | Content is only rendered on the slides, not on the page | + | `::: {.content-hidden when-format="revealjs"}` | Content is only rendered on the page, not on the slides | In exchange for the control you take on the work the filter was doing: slides do not close after a figure, deeper headings do not @@ -126,10 +127,10 @@ This section covers **strategy 1** in detail: the front matter is much the same whichever strategy you pick, but the classes and the automation below come from the two files at the root of this repo. -| File | Role | -| --- | --- | -| `web_and_slides.lua` | Pandoc/Quarto filter, applied at render time. Decides what appears on the website, what appears on the slides, and how the slides are broken up. | -| `web_and_slides.r` | One-shot authoring helper. Converts a finished long-form document into the tagged form the filter expects. | + | File | Role | + | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | + | `web_and_slides.lua` | Pandoc/Quarto filter, applied at render time. Decides what appears on the website, what appears on the slides, and how the slides are broken up. | + | `web_and_slides.r` | One-shot authoring helper. Converts a finished long-form document into the tagged form the filter expects. | ## Front matter @@ -168,11 +169,11 @@ everything below; `web_and_slides.r` adds it for you, or you can copy it. Three fenced-div (`:::`) classes control where content lands: -| Class | Website Tutorial | Slides | -| --- | --- | --- | -| `.narration` | normal prose | speaker notes | -| `.slides-only` | dropped | shown on the slide | -| `.html-only` | shown | dropped | + | Class | Website Tutorial | Slides | + | -------------- | ---------------- | ------------------ | + | `.narration` | normal prose | speaker notes | + | `.slides-only` | dropped | shown on the slide | + | `.html-only` | shown | dropped | Anything not wrapped in one of these appears in both outputs. So the usual shape of a module is: headings and code chunks shared by both outputs, the connecting