| Title: | Construct Advanced Donut Charts |
|---|---|
| Description: | Build donut/pie charts with 'ggplot2' layer by layer, exploiting the advantages of polar symmetry. Leverage layouts to distribute labels effectively. Connect labels to donut segments using pins. Streamline annotation and highlighting. |
| Authors: | Dmitry Kibalnikov [aut, cre, cph] |
| Maintainer: | Dmitry Kibalnikov <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.1 |
| Built: | 2026-05-18 08:21:41 UTC |
| Source: | https://github.com/dkibalnikov/donutsk |
Create pie or donut charts while retaining ggplot flexibility, such as leveraging faceting and palettes, and fine-tuning appearance
The function geom_donut_int() creates visually internal donut layer as aggregation of passed values
The function geom_donut_ext() creates visually external donut layer of passed values
geom_donut_int0() and geom_donut_ext() are generic geoms not supporting highlight feature
geom_donut_int0( mapping = NULL, data = NULL, stat = "donut_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r_int = 0, r_ext = 1, hl_shift = 0.1, ... ) geom_donut_int(..., hl_col = "firebrick") geom_donut_ext0( mapping = NULL, data = NULL, stat = "donut_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r_int = 1.5, r_ext = 2, hl_shift = 0.1, ... ) geom_donut_ext(..., hl_col = "firebrick")geom_donut_int0( mapping = NULL, data = NULL, stat = "donut_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r_int = 0, r_ext = 1, hl_shift = 0.1, ... ) geom_donut_int(..., hl_col = "firebrick") geom_donut_ext0( mapping = NULL, data = NULL, stat = "donut_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r_int = 1.5, r_ext = 2, hl_shift = 0.1, ... ) geom_donut_ext(..., hl_col = "firebrick")
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer, either as a |
position |
Position adjustment, either as a string naming the adjustment
(e.g. |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
r_int |
Internal donut radius |
r_ext |
External pie or donut radius |
hl_shift |
Sets the spacing to show highlighted segments |
... |
Other arguments passed on to |
hl_col |
Sets the color for highlighted segments. It's possible to use both simultaneously |
An object of class StatDonutInt (inherits from Stat, ggproto, gg) of length 4.
An object of class StatDonutIntHl (inherits from Stat, ggproto, gg) of length 4.
An object of class StatDonutExt (inherits from Stat, ggproto, gg) of length 4.
An object of class StatDonutExtHl (inherits from Stat, ggproto, gg) of length 4.
There are two additional aesthetics possible to use:
highlight - optional aesthetic which expects logical (TRUE/FALSE) variable in order to highlight particular donut segments
opacity - operates pretty much the same as alpha but ensure more contrast colors and removes legend. Once alpha is set opacity does not affect a chart
None
# Create an example set.seed(1605) n <- 20 df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.7, .3))) |> dplyr::mutate(highlight_int = ifelse(lvl1 == "A", TRUE, FALSE)) # Create a simple pie chart ggplot(df, aes(value = value, fill=lvl1)) + geom_donut_int(alpha=.6) + coord_polar(theta = "y") # Create a simple donut chart that can handle more granular data # and highlight specific segments ggplot(df, aes(value = value, fill=lvl2, highlight=highlight_ext)) + geom_donut_int(r_int=.5, alpha=.6, linewidth=.2) + coord_polar(theta = "y") + xlim(0, 1.5) # Perform data preparation tasks with `packing()` # and apply specific color packing(df, value) |> ggplot(aes(value = value, fill=lvl2, highlight=highlight_ext)) + geom_donut_int(r_int=.5, alpha=.6, linewidth=.2, col = "gray20") + coord_polar(theta = "y") + xlim(0, 1.5) # Built combined donut chart with interanl and external layers dplyr::bind_rows( # arrange by value `arrange()` = dplyr::arrange(df, lvl1, lvl2, value), # pack values for better space management `packing()` = packing(df, value, lvl1), .id = "prep_type") |> ggplot(aes(value = value, fill=lvl1)) + geom_donut_int(aes(highlight=highlight_int), alpha=.6) + geom_donut_ext(aes(opacity=lvl2, highlight=highlight_int)) + # apply facets facet_grid(~prep_type) + # style chart with palette and theme scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + theme_void() + coord_polar(theta = "y") + xlim(0, 2.5)# Create an example set.seed(1605) n <- 20 df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.7, .3))) |> dplyr::mutate(highlight_int = ifelse(lvl1 == "A", TRUE, FALSE)) # Create a simple pie chart ggplot(df, aes(value = value, fill=lvl1)) + geom_donut_int(alpha=.6) + coord_polar(theta = "y") # Create a simple donut chart that can handle more granular data # and highlight specific segments ggplot(df, aes(value = value, fill=lvl2, highlight=highlight_ext)) + geom_donut_int(r_int=.5, alpha=.6, linewidth=.2) + coord_polar(theta = "y") + xlim(0, 1.5) # Perform data preparation tasks with `packing()` # and apply specific color packing(df, value) |> ggplot(aes(value = value, fill=lvl2, highlight=highlight_ext)) + geom_donut_int(r_int=.5, alpha=.6, linewidth=.2, col = "gray20") + coord_polar(theta = "y") + xlim(0, 1.5) # Built combined donut chart with interanl and external layers dplyr::bind_rows( # arrange by value `arrange()` = dplyr::arrange(df, lvl1, lvl2, value), # pack values for better space management `packing()` = packing(df, value, lvl1), .id = "prep_type") |> ggplot(aes(value = value, fill=lvl1)) + geom_donut_int(aes(highlight=highlight_int), alpha=.6) + geom_donut_ext(aes(opacity=lvl2, highlight=highlight_int)) + # apply facets facet_grid(~prep_type) + # style chart with palette and theme scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + theme_void() + coord_polar(theta = "y") + xlim(0, 2.5)
The set of annotation functions utilizes layout functions to effectively distribute labels within the available space
Annotations are streamlined by leveraging pre-calculated special variables such as .sum, .mean, and .n (see Details).
The function geom_label_int() creates geom_label-like internal donut layer as aggregation of passed values
The function geom_text_int() creates geom_text-like internal donut layer as aggregation of passed values
The function geom_label_ext() creates geom_label-like external donut layer of passed values
The function geom_text_ext() creates geom_text-like external donut layer of passed values
geom_label_int( mapping = NULL, data = NULL, stat = "label_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1, ... ) geom_text_int( mapping = NULL, data = NULL, stat = "text_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1, ... ) geom_label_ext( mapping = NULL, data = NULL, stat = "label_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, layout = circle(), ... ) geom_text_ext( mapping = NULL, data = NULL, stat = "text_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, layout = circle(), ... )geom_label_int( mapping = NULL, data = NULL, stat = "label_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1, ... ) geom_text_int( mapping = NULL, data = NULL, stat = "text_int", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1, ... ) geom_label_ext( mapping = NULL, data = NULL, stat = "label_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, layout = circle(), ... ) geom_text_ext( mapping = NULL, data = NULL, stat = "text_ext", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, layout = circle(), ... )
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer, either as a |
position |
Position adjustment, either as a string, or the result of
a call to a position adjustment function. Cannot be jointly specified with
|
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
r |
Sets the radius to place label or text for internal donut |
... |
Other arguments passed on to |
layout |
The layout function to effectively display text and labels |
An object of class StatLabelInt (inherits from Stat, ggproto, gg) of length 3.
An object of class StatTextInt (inherits from Stat, ggproto, gg) of length 3.
An object of class StatLabelExt (inherits from Stat, ggproto, gg) of length 3.
An object of class StatTextExt (inherits from Stat, ggproto, gg) of length 3.
The label functions supports glue::glue() for convenient label construction like Total: {.sum},
where .sum is pre-calculated variable. You can still use glue::glue() or paste()
functions to pass data.frame fields for label construction.
In addition to generic aesthetics like color, fill, alpha, etc., the following list of pre-calculated variables
is available for geom_label_int() and geom_text_int():
.sum: Summation of the value field
.mean: Mean of the value field
.median: Median of the value field
.n: Observation count of the value field
.prc: Percentage of the value field
For geom_label_ext() and geom_text_ext(), which are suitable for external donut labels, the following list of
pre-calculated variables is available:
.prc: Percentage of the value field for the entire multiplicity
.prc_grp: Percentage of the value field for the group defined by fill
None
# Create an example data set n <- 30 set.seed(2021) df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.9, .1))) |> dplyr::mutate(highlight_int = dplyr::if_else(lvl1 == "A", TRUE, FALSE)) # Starting plot with doubled donuts and annotations for internal one p <- dplyr::group_by(df, lvl1, lvl2, highlight_ext, highlight_int) |> dplyr::summarise(value = sum(value), .groups = "drop") |> packing(value, lvl1) |> ggplot(aes(value = value, fill = lvl1)) + geom_donut_int(aes(highlight = highlight_int), alpha=.5, r_int=.25) + geom_text_int(lineheight = .8, r=1.2, show.legend = FALSE, aes(label = "Sum {fill}:\n{.sum}-{scales::percent(.prc)}", col=lvl1)) + geom_donut_ext(aes(opacity = lvl2, highlight = highlight_ext)) + scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + scale_color_viridis_d(option = "inferno", begin = .1, end = .7) + guides(alpha=guide_legend(ncol = 2), fill=guide_legend(ncol = 2)) + theme_void() + theme(legend.position = "inside", legend.position.inside = c(0.1, 0.9)) p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) # Add labels to external donut as percent inside group p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white") # Leverage ggplot2 feature for labels p + coord_radial(theta = "y", expand = FALSE, rotate_angle = TRUE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc)}")), show.legend = FALSE, size=3, col="white", angle=90, layout = circle()) # Leverage another layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = tv(thinner = TRUE, thinner_gap = 0.15))# Create an example data set n <- 30 set.seed(2021) df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.9, .1))) |> dplyr::mutate(highlight_int = dplyr::if_else(lvl1 == "A", TRUE, FALSE)) # Starting plot with doubled donuts and annotations for internal one p <- dplyr::group_by(df, lvl1, lvl2, highlight_ext, highlight_int) |> dplyr::summarise(value = sum(value), .groups = "drop") |> packing(value, lvl1) |> ggplot(aes(value = value, fill = lvl1)) + geom_donut_int(aes(highlight = highlight_int), alpha=.5, r_int=.25) + geom_text_int(lineheight = .8, r=1.2, show.legend = FALSE, aes(label = "Sum {fill}:\n{.sum}-{scales::percent(.prc)}", col=lvl1)) + geom_donut_ext(aes(opacity = lvl2, highlight = highlight_ext)) + scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + scale_color_viridis_d(option = "inferno", begin = .1, end = .7) + guides(alpha=guide_legend(ncol = 2), fill=guide_legend(ncol = 2)) + theme_void() + theme(legend.position = "inside", legend.position.inside = c(0.1, 0.9)) p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) # Add labels to external donut as percent inside group p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white") # Leverage ggplot2 feature for labels p + coord_radial(theta = "y", expand = FALSE, rotate_angle = TRUE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc)}")), show.legend = FALSE, size=3, col="white", angle=90, layout = circle()) # Leverage another layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label=paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = tv(thinner = TRUE, thinner_gap = 0.15))
A pre-processed subset of GDP data from the World Bank
GDP, PPP means gross domestic product based on purchasing power parity
current international $ means actual (not adjusted to inflation) US dollars
GDPGDP
GDPA data frame with 6,004 rows and 5 columns:
Year
Country name
Region hierarchy
3 letter ISO region codes
GDP, PPP (current international $)
...
https://data.worldbank.org/indicator/NY.GDP.MKTP.PP.CD
The layout functions help to streamline displaying text and labels geoms without overlapping effectively leveraging space available for pie and donut charts
tv() - The function builds layout resembled an old-fashioned TV screen
petal() - The function builds layout resembled flower with petals
circle() - The function builds circle layout
eye() - The function builds two-sided layout
tv( scale_x = 1.5, scale_y = 1.5, bend_x = 1, bend_y = 1, thinner = FALSE, thinner_gap = 0.1 ) petal( rotate = 0, n = 4, scale = 2.5, bend = 0.3, thinner = FALSE, thinner_gap = 0.1 ) circle(r = 2.5, thinner = FALSE, thinner_gap = 0.1) eye(scale_x = 2, bend_x = 1, alpha = 90, clove = 0.5)tv( scale_x = 1.5, scale_y = 1.5, bend_x = 1, bend_y = 1, thinner = FALSE, thinner_gap = 0.1 ) petal( rotate = 0, n = 4, scale = 2.5, bend = 0.3, thinner = FALSE, thinner_gap = 0.1 ) circle(r = 2.5, thinner = FALSE, thinner_gap = 0.1) eye(scale_x = 2, bend_x = 1, alpha = 90, clove = 0.5)
scale_x |
Scales the layout in horizontal perspective |
scale_y |
Scales the layout in vertical perspective |
bend_x |
Adjusts the bend level in horizontal perspective |
bend_y |
Adjusts the bend level in vertical perspective |
thinner |
Distributes text or label elements across two different levels |
thinner_gap |
Sets the spacing between thinner levels |
rotate |
Rotates the layout clockwise |
n |
Sets the number of petals in the layout |
scale |
Scales the layout |
bend |
Manages the bending level |
r |
Sets the radius of the layout circle |
alpha |
Defines the angle of distribution in horizontal perspective. Pick up value from degree interval (0, 180) |
clove |
Determines the distribution proportion between the left and right-hand parts. Default value is 0.5. There should be numeric value from interval (0, 1) e.g. 0.4 denotes 40% cases on the right hand and 60% cases on the left hand |
Layout functions return layout function i.e. a function that takes a vector of angles and returns a numeric radius vector giving a position for each input value.
Layout functions are designed to be used with the layout argument of donutsk functions.
Utilized in the following functions: geom_label_ext, geom_text_ext, geom_pin
# Render multiple layouts simultaneously list(petal_2n = petal(n = 2), petal_3n = petal(n = 3, rotate = 180), petal_4n = petal(n = 4), tv_base = tv(), tv_ext = tv(bend_x = 0, bend_y = 0, thinner = TRUE)) |> lapply(function(x){ rlang::exec(x, 1:300/300) |> dplyr::tibble(r = _) |> dplyr::mutate(theta = 1:300/300) }) |> dplyr::bind_rows(.id = "layouts") |> ggplot(aes(x=r, y=theta, col = layouts)) + geom_point(alpha = .3) + coord_polar(theta = "y") + xlim(0, 3.5) # The eye() layout generates table as an output n <- 20 theta <- 1:n/n dplyr::tibble( theta = theta, lbl = paste0("sample: ", sample(LETTERS, n, TRUE)) ) |> dplyr::bind_cols(lt = eye()(theta)) |> ggplot(aes(x=x, y=y)) + geom_point(aes(x=1, y=theta)) + geom_point() + geom_segment(aes(x=1, xend=x, y=theta, yend=y), linewidth=.2) + geom_label(aes(label=lbl, hjust=dplyr::if_else(theta > 0.5, 1, 0)), nudge_x =.2) + coord_polar(theta = "y") + xlim(0, 5) + ylim(0, 1)# Render multiple layouts simultaneously list(petal_2n = petal(n = 2), petal_3n = petal(n = 3, rotate = 180), petal_4n = petal(n = 4), tv_base = tv(), tv_ext = tv(bend_x = 0, bend_y = 0, thinner = TRUE)) |> lapply(function(x){ rlang::exec(x, 1:300/300) |> dplyr::tibble(r = _) |> dplyr::mutate(theta = 1:300/300) }) |> dplyr::bind_rows(.id = "layouts") |> ggplot(aes(x=r, y=theta, col = layouts)) + geom_point(alpha = .3) + coord_polar(theta = "y") + xlim(0, 3.5) # The eye() layout generates table as an output n <- 20 theta <- 1:n/n dplyr::tibble( theta = theta, lbl = paste0("sample: ", sample(LETTERS, n, TRUE)) ) |> dplyr::bind_cols(lt = eye()(theta)) |> ggplot(aes(x=x, y=y)) + geom_point(aes(x=1, y=theta)) + geom_point() + geom_segment(aes(x=1, xend=x, y=theta, yend=y), linewidth=.2) + geom_label(aes(label=lbl, hjust=dplyr::if_else(theta > 0.5, 1, 0)), nudge_x =.2) + coord_polar(theta = "y") + xlim(0, 5) + ylim(0, 1)
Arrange data to distribute small values further apart from each other
packing(.data, value, level = NULL)packing(.data, value, level = NULL)
.data |
A data frame, data frame extension (e.g. a tibble), or a lazy data frame (e.g. from dbplyr or dtplyr). |
value |
A .data field which contains values to distribute |
level |
A .data grouping field for distribution |
An object of the same type as .data.
# Create an example n <- 20 df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE) ) # Arrange all values packing(df, value) # Arrange values within values packing(df, value, lvl1)# Create an example n <- 20 df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE) ) # Arrange all values packing(df, value) # Arrange values within values packing(df, value, lvl1)
The set of functions served to connect text or labels with donut segments
geom_pin_line() - builds curved line to linl label with donut segment
geom_pin_head() - builds stylish point heads for pins
geom_pin() - handy wrapper for geom_pin_line() and geom_pin_head()
geom_pin_line( mapping = NULL, data = NULL, stat = "pin", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1.5, cut = 0.1, layout = circle(), ... ) geom_pin_head( mapping = NULL, data = NULL, stat = "point", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1.5, cut = 0.1, layout = circle(), ... ) geom_pin(..., head = TRUE)geom_pin_line( mapping = NULL, data = NULL, stat = "pin", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1.5, cut = 0.1, layout = circle(), ... ) geom_pin_head( mapping = NULL, data = NULL, stat = "point", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, r = 1.5, cut = 0.1, layout = circle(), ... ) geom_pin(..., head = TRUE)
mapping |
Set of aesthetic mappings created by |
data |
The data to be displayed in this layer. There are three options: If A A |
stat |
The statistical transformation to use on the data for this
layer, either as a |
position |
Position adjustment, either as a string naming the adjustment
(e.g. |
na.rm |
If |
show.legend |
logical. Should this layer be included in the legends?
|
inherit.aes |
If |
r |
The radius where donut is placed |
cut |
Sets additional two-sided gap for pins |
layout |
The layout function to effectively display text and labels.
Obviously it's better to have the same as for |
... |
Parameters to be passed to |
head |
Boolean - defines whether to add pin head |
An object of class StatPinLine (inherits from Stat, ggproto, gg) of length 3.
An object of class StatPinHead (inherits from Stat, ggproto, gg) of length 3.
None
n <- 30 set.seed(2021) df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.9, .1))) |> dplyr::mutate(highlight_int = dplyr::if_else(lvl1 == "A", TRUE, FALSE)) # Starting plot with doubled donuts and annotations for internal one p <- dplyr::group_by(df, lvl1, lvl2, highlight_ext, highlight_int) |> dplyr::summarise(value = sum(value), .groups = "drop") |> packing(value, lvl1) |> ggplot(aes(value = value, fill = lvl1)) + geom_donut_int(aes(highlight = highlight_int), alpha=.5, r_int = .25) + geom_label_int(aes(label = "Sum {fill}:\n{.sum}-{scales::percent(.prc)}"), alpha = .6, col = "white", size = 3, r=1.2) + geom_donut_ext(aes(opacity = lvl2, highlight = highlight_ext)) + scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + guides(alpha = guide_legend(ncol = 2), fill = guide_legend(ncol = 2)) + theme_void() + theme(legend.position = "none") p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) # Add labels to external donut as percent inside group p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white") + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, cut = .2) # Leverage tv() layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ":{scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = tv(thinner = TRUE, thinner_gap = .15)) + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, cut = .2, layout = tv(thinner = TRUE, thinner_gap = .15)) # Leverage another layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = eye()) + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, layout = eye())n <- 30 set.seed(2021) df <- dplyr::tibble( lvl1 = sample(LETTERS[1:5], n, TRUE), lvl2 = sample(LETTERS[6:24], n, TRUE), value = sample(1:20, n, TRUE), highlight_ext = sample(c(FALSE,TRUE), n, TRUE, c(.9, .1))) |> dplyr::mutate(highlight_int = dplyr::if_else(lvl1 == "A", TRUE, FALSE)) # Starting plot with doubled donuts and annotations for internal one p <- dplyr::group_by(df, lvl1, lvl2, highlight_ext, highlight_int) |> dplyr::summarise(value = sum(value), .groups = "drop") |> packing(value, lvl1) |> ggplot(aes(value = value, fill = lvl1)) + geom_donut_int(aes(highlight = highlight_int), alpha=.5, r_int = .25) + geom_label_int(aes(label = "Sum {fill}:\n{.sum}-{scales::percent(.prc)}"), alpha = .6, col = "white", size = 3, r=1.2) + geom_donut_ext(aes(opacity = lvl2, highlight = highlight_ext)) + scale_fill_viridis_d(option = "inferno", begin = .1, end = .7) + guides(alpha = guide_legend(ncol = 2), fill = guide_legend(ncol = 2)) + theme_void() + theme(legend.position = "none") p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) # Add labels to external donut as percent inside group p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white") + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, cut = .2) # Leverage tv() layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ":{scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = tv(thinner = TRUE, thinner_gap = .15)) + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, cut = .2, layout = tv(thinner = TRUE, thinner_gap = .15)) # Leverage another layout p + coord_radial(theta = "y", expand = FALSE, rotate_angle = FALSE) + geom_label_ext(aes(label = paste0(lvl2, ": {scales::percent(.prc_grp)}")), show.legend = FALSE, size=3, col="white", layout = eye()) + geom_pin(size = .5, linewidth=.1, show.legend = FALSE, layout = eye())