Skip to content

Commit 93bc249

Browse files
committed
Auto-detect flipped charts in theme_grattan
Add automatic detection of horizontal/flipped charts when theme_grattan() is added to a plot. Introduces ggplot_add.grattan_theme (R/detect_flipped.R) which inspects coord_flip() and mappings (discrete y + continuous/NULL x) and rebuilds the theme with flipped = TRUE when detected; a throttled message is shown (once per 8 hours). Change flipped default to NULL, store original args on the theme object for detection, and strip the custom class before delegating to ggplot2.
1 parent a2a73c1 commit 93bc249

15 files changed

Lines changed: 425 additions & 123 deletions

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
S3method(ggplot_add,grattan_theme)
34
export(check_chart)
45
export(check_chart_aspect_ratio)
56
export(colour_text)
@@ -187,6 +188,7 @@ importFrom(dplyr,select)
187188
importFrom(dplyr,where)
188189
importFrom(ggplot2,.pt)
189190
importFrom(ggplot2,expand_scale)
191+
importFrom(ggplot2,ggplot_add)
190192
importFrom(ggplot2,last_plot)
191193
importFrom(ggplot2,scale_y_continuous)
192194
importFrom(ggplot2,update_geom_defaults)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# grattantheme 1.5
2+
* `theme_grattan()` now auto-detects horizontal/flipped charts and applies `flipped = TRUE` automatically. Detection triggers for `coord_flip()` and bar charts (discrete y + continuous x). A message is shown every 8 hours when auto-detection fires. Set `flipped = TRUE` explicitly to silence the message, or `flipped = FALSE` to opt out of auto-detection.
23
* Renamed `check_chart_aspect_ratio()` to `check_chart()`.
34
* Updated `grattan_point_filled` function signature for consistency with `geom_point`.
45
* Updates `export_latex_code` for consistency with `grattan_save_overleaf`.

R/detect_flipped.R

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Auto-detection of flipped/horizontal charts for theme_grattan()
2+
#
3+
# When theme_grattan() is added to a plot via `+`, the ggplot_add method
4+
# below inspects the plot to determine if it represents a horizontal/flipped
5+
# chart. If so, and the user hasn't explicitly set `flipped`, the theme is
6+
# rebuilt with flipped = TRUE and a throttled message is shown.
7+
8+
9+
#' @importFrom ggplot2 ggplot_add
10+
#' @export
11+
ggplot_add.grattan_theme <- function(object, plot, object_name) {
12+
args <- attr(object, "grattan_args")
13+
14+
# Only run detection if flipped was left as NULL (the default)
15+
# and chart_type is not "scatter" (scatter ignores flipped)
16+
if (!is.null(args) &&
17+
is.null(args$flipped) &&
18+
args$chart_type != "scatter") {
19+
20+
if (.detect_flipped_chart(plot)) {
21+
# Rebuild theme with flipped = TRUE
22+
object <- theme_grattan_normal(
23+
base_size = args$base_size,
24+
base_family = args$base_family,
25+
background = args$background,
26+
legend = args$legend,
27+
panel_borders = args$panel_borders,
28+
flipped = TRUE
29+
)
30+
31+
.notify_auto_flip()
32+
}
33+
}
34+
35+
# Strip custom class and attributes, delegate to standard theme adding
36+
attr(object, "grattan_args") <- NULL
37+
class(object) <- setdiff(class(object), "grattan_theme")
38+
39+
ggplot2::ggplot_add(object, plot, object_name)
40+
}
41+
42+
43+
# Detect if a plot represents a flipped/horizontal chart.
44+
# Returns TRUE only when confident; FALSE on any error or ambiguity.
45+
.detect_flipped_chart <- function(plot) {
46+
tryCatch({
47+
# 1. Check for coord_flip()
48+
if (inherits(plot$coordinates, "CoordFlip")) {
49+
return(TRUE)
50+
}
51+
52+
# 2. Check plot-level mappings: discrete y + continuous x
53+
if (.check_mappings_flipped(plot$mapping, plot$data)) {
54+
return(TRUE)
55+
}
56+
57+
# 3. Fallback: check first layer's mappings if plot-level is empty
58+
if (length(plot$mapping) == 0 && length(plot$layers) > 0) {
59+
layer <- plot$layers[[1]]
60+
layer_data <- if (is.data.frame(layer$data)) layer$data else plot$data
61+
if (is.data.frame(layer_data) && length(layer$mapping) > 0) {
62+
if (.check_mappings_flipped(layer$mapping, layer_data)) {
63+
return(TRUE)
64+
}
65+
}
66+
}
67+
68+
FALSE
69+
}, error = function(e) {
70+
FALSE
71+
})
72+
}
73+
74+
75+
# Check if a mapping + data combination indicates a flipped chart.
76+
# A chart is "flipped" when y maps to a discrete variable and x maps to
77+
# a continuous variable (or is absent/NULL).
78+
.check_mappings_flipped <- function(mapping, data) {
79+
if (is.null(mapping) || length(mapping) == 0) return(FALSE)
80+
if (!is.data.frame(data) || nrow(data) == 0) return(FALSE)
81+
82+
tryCatch({
83+
y_val <- if (!is.null(mapping$y)) {
84+
rlang::eval_tidy(mapping$y, data = data)
85+
}
86+
87+
x_val <- if (!is.null(mapping$x)) {
88+
rlang::eval_tidy(mapping$x, data = data)
89+
}
90+
91+
y_is_discrete <- !is.null(y_val) &&
92+
(is.factor(y_val) || is.character(y_val) || is.logical(y_val))
93+
94+
x_is_continuous_or_null <- is.null(x_val) || is.numeric(x_val)
95+
96+
y_is_discrete && x_is_continuous_or_null
97+
}, error = function(e) {
98+
FALSE
99+
})
100+
}
101+
102+
103+
# Show a message when auto-flip is applied, throttled to once per 8 hours.
104+
# Uses an environment variable to track the last message time, following
105+
# the same pattern as overleaf_utils.R.
106+
.notify_auto_flip <- function() {
107+
last_str <- Sys.getenv("GRATTANTHEME_FLIP_LAST_MESSAGE", unset = "")
108+
109+
should_message <- TRUE
110+
111+
if (last_str != "") {
112+
tryCatch({
113+
last_time <- as.POSIXct(last_str)
114+
if (difftime(Sys.time(), last_time, units = "hours") < 8) {
115+
should_message <- FALSE
116+
}
117+
}, error = function(e) {
118+
# If timestamp can't be parsed, show the message
119+
})
120+
}
121+
122+
if (should_message) {
123+
message(
124+
"grattantheme: Auto-detected a horizontal/flipped chart and applied ",
125+
"`flipped = TRUE`. Set `theme_grattan(flipped = TRUE)` explicitly to ",
126+
"silence this message."
127+
)
128+
Sys.setenv(GRATTANTHEME_FLIP_LAST_MESSAGE = as.character(Sys.time()))
129+
}
130+
}

R/theme_grattan.R

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
#' style guide.
55
#' @param base_family Font family for text elements. Defaults to "sans".
66
#' @param chart_type "normal" by detault. Set to "scatter" for scatter plots.
7-
#' @param flipped FALSE by default. Set to TRUE if using coord_flip(). If set to
8-
#' TRUE, the theme will show a vertical axis line, ticks & panel grid, while
9-
#' hiding the horizontals. Ignored for type = "scatter".
7+
#' @param flipped NULL by default. Set to TRUE if using coord_flip() or creating
8+
#' horizontal bar charts. If set to TRUE, the theme will show a vertical axis
9+
#' line, ticks & panel grid, while hiding the horizontals. Ignored for
10+
#' type = "scatter". When NULL (the default), flipped formatting is
11+
#' auto-detected when the theme is added to a plot. Set to FALSE explicitly
12+
#' to override auto-detection.
1013
#' @param background "white" by default. Set to "orange" or "box" if you're
1114
#' making a chart to go in a Grattan report box.
1215
#' @param legend "off" by default. Set to "bottom", "left", "right" or "top" as
@@ -109,7 +112,7 @@
109112
theme_grattan <- function(base_size = 18,
110113
base_family = "sans",
111114
chart_type = "normal",
112-
flipped = FALSE,
115+
flipped = NULL,
113116
background = "white",
114117
legend = "none",
115118
panel_borders = FALSE) {
@@ -120,13 +123,17 @@ theme_grattan <- function(base_size = 18,
120123
chart_type <- "normal"
121124
}
122125

126+
# Resolve NULL → FALSE for building the theme; auto-detection
127+
# happens later in ggplot_add.grattan_theme() when flipped is NULL
128+
flipped_resolved <- isTRUE(flipped)
129+
123130
if (chart_type == "normal") {
124131
ret <- theme_grattan_normal(base_size = base_size,
125132
base_family = base_family,
126133
background = background,
127134
legend = legend,
128135
panel_borders = panel_borders,
129-
flipped = flipped)
136+
flipped = flipped_resolved)
130137
}
131138

132139
if (chart_type == "scatter") {
@@ -135,13 +142,26 @@ theme_grattan <- function(base_size = 18,
135142
background = background,
136143
legend = legend,
137144
panel_borders = panel_borders)
138-
if (flipped) message("Note that the 'flipped' argument is ignored for scatter plots.")
145+
if (isTRUE(flipped)) {
146+
message("Note that the 'flipped' argument is ignored for scatter plots.")
147+
}
139148
}
140149

141150
# Call a function that modifies various geom defaults
142151
grattanify_geom_defaults()
143152

144-
# Return
153+
# Store original arguments (including NULL flipped) for auto-detection
154+
attr(ret, "grattan_args") <- list(
155+
base_size = base_size,
156+
base_family = base_family,
157+
chart_type = chart_type,
158+
flipped = flipped,
159+
background = background,
160+
legend = legend,
161+
panel_borders = panel_borders
162+
)
163+
class(ret) <- c("grattan_theme", class(ret))
164+
145165
return(ret)
146166

147167
}

man/theme_grattan.Rd

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)