Skip to content

Commit 0ffeb93

Browse files
committed
Add Shiny settings module for gridify rendering and downloads.
1 parent 45855f1 commit 0ffeb93

File tree

7 files changed

+617
-0
lines changed

7 files changed

+617
-0
lines changed

DESCRIPTION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Suggests:
3838
knitr,
3939
magrittr,
4040
rmarkdown,
41+
shiny,
4142
spelling,
4243
testthat (>= 3.0.0)
4344
Collate:
@@ -49,6 +50,7 @@ Collate:
4950
complex_layout.R
5051
pharma_layout.R
5152
get_layouts.R
53+
gridify_with_settings.R
5254
layout_issues.R
5355
pagination_utils.R
5456
VignetteBuilder: knitr

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export(gridifyCell)
88
export(gridifyCells)
99
export(gridifyLayout)
1010
export(gridifyObject)
11+
export(gridify_with_settings_srv)
12+
export(gridify_with_settings_ui)
1113
export(paginate_table)
1214
export(pharma_layout_A4)
1315
export(pharma_layout_base)

R/gridify_with_settings.R

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#' Shiny module to render a gridify object with size controls
2+
#'
3+
#' @name gridify_with_settings
4+
#' @rdname gridify_with_settings
5+
#'
6+
#' @description
7+
#' A Shiny module that renders a [`gridifyClass-class`] object (from [gridify()])
8+
#' with height and width sliders, plus PNG and PDF download buttons.
9+
#'
10+
#' @param id (`character(1)`) Shiny module id.
11+
#'
12+
#' @seealso [gridify()], [set_cell()], [gridifyLayout()]
13+
#'
14+
#' @examples
15+
#' if (interactive() && requireNamespace("shiny", quietly = TRUE) &&
16+
#' requireNamespace("ggplot2", quietly = TRUE)) {
17+
#' library(shiny)
18+
#' library(ggplot2)
19+
#' library(gridify)
20+
#'
21+
#' ui <- fluidPage(
22+
#' gridify_with_settings_ui("demo")
23+
#' )
24+
#'
25+
#' server <- function(input, output, session) {
26+
#' gridify_r <- reactive({
27+
#' gridify(
28+
#' object = ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) +
29+
#' ggplot2::geom_point(),
30+
#' layout = simple_layout()
31+
#' ) |>
32+
#' set_cell("title", "My Plot")
33+
#' })
34+
#'
35+
#' gridify_with_settings_srv("demo", gridify_r = gridify_r)
36+
#' }
37+
#'
38+
#' shinyApp(ui, server)
39+
#' }
40+
NULL
41+
42+
#' @noRd
43+
gridify_require_shiny <- function() {
44+
if (!requireNamespace("shiny", quietly = TRUE)) {
45+
stop(
46+
"The 'shiny' package is required to use gridify_with_settings.\n",
47+
"Install it with: install.packages(\"shiny\")",
48+
call. = FALSE
49+
)
50+
}
51+
}
52+
53+
#' @noRd
54+
assert_hwvec <- function(x, nm) {
55+
if (!is.numeric(x) || length(x) != 3L || any(!is.finite(x))) {
56+
stop(
57+
"'", nm, "' must be a numeric vector of length 3 (value, min, max).",
58+
call. = FALSE
59+
)
60+
}
61+
if (x[1L] < x[2L] || x[1L] > x[3L]) {
62+
stop(
63+
"'", nm, "' value (", x[1L], ") must be between min (", x[2L],
64+
") and max (", x[3L], ").",
65+
call. = FALSE
66+
)
67+
}
68+
}
69+
70+
#' @rdname gridify_with_settings
71+
#'
72+
#' @return
73+
#' `gridify_with_settings_ui()` returns a `shiny::sidebarLayout`.
74+
#'
75+
#' @export
76+
gridify_with_settings_ui <- function(id) {
77+
gridify_require_shiny()
78+
if (!is.character(id) || length(id) != 1L) {
79+
stop("'id' must be a single character string.", call. = FALSE)
80+
}
81+
82+
ns <- shiny::NS(id)
83+
84+
shiny::sidebarLayout(
85+
sidebarPanel = shiny::sidebarPanel(
86+
width = 3,
87+
shiny::sliderInput(
88+
inputId = ns("height"),
89+
label = "Height (px)",
90+
min = 200L,
91+
max = 2000L,
92+
value = 600L,
93+
step = 10L,
94+
ticks = FALSE
95+
),
96+
shiny::sliderInput(
97+
inputId = ns("width"),
98+
label = "Width (px)",
99+
min = 200L,
100+
max = 2000L,
101+
value = 800L,
102+
step = 10L,
103+
ticks = FALSE
104+
),
105+
shiny::tags$hr(),
106+
shiny::downloadButton(ns("dl_png"), "PNG", class = "btn-sm"),
107+
shiny::tags$span(" "),
108+
shiny::downloadButton(ns("dl_pdf"), "PDF", class = "btn-sm")
109+
),
110+
mainPanel = shiny::mainPanel(
111+
width = 9,
112+
shiny::uiOutput(ns("plot_ui"))
113+
)
114+
)
115+
}
116+
117+
#' @rdname gridify_with_settings
118+
#'
119+
#' @param gridify_r (`reactive` or `function`)
120+
#' A `shiny::reactive()` (or plain function) returning a [`gridifyClass-class`]
121+
#' object produced by [gridify()].
122+
#'
123+
#' @param height (`numeric(3)`)
124+
#' Height slider values `c(value, min, max)` in pixels.
125+
#'
126+
#' @param width (`numeric(3)`)
127+
#' Width slider values `c(value, min, max)` in pixels.
128+
#'
129+
#' @return
130+
#' `gridify_with_settings_srv()` invisibly returns `NULL`.
131+
#'
132+
#' @export
133+
gridify_with_settings_srv <- function(
134+
id,
135+
gridify_r,
136+
height = c(600L, 200L, 2000L),
137+
width = c(800L, 200L, 2000L)) {
138+
139+
gridify_require_shiny()
140+
141+
if (!is.character(id) || length(id) != 1L) {
142+
stop("'id' must be a single character string.", call. = FALSE)
143+
}
144+
if (!inherits(gridify_r, c("function", "reactive"))) {
145+
stop("'gridify_r' must be a shiny::reactive() or a plain function.",
146+
call. = FALSE)
147+
}
148+
149+
assert_hwvec(height, "height")
150+
assert_hwvec(width, "width")
151+
152+
shiny::moduleServer(id, function(input, output, session) {
153+
154+
shiny::observe({
155+
shiny::updateSliderInput(
156+
session, "height",
157+
min = height[2], max = height[3], value = height[1]
158+
)
159+
shiny::updateSliderInput(
160+
session, "width",
161+
min = width[2], max = width[3], value = width[1]
162+
)
163+
}) |> shiny::bindEvent(session$token, ignoreNULL = TRUE, once = TRUE)
164+
165+
get_obj <- shiny::reactive({
166+
obj <- if (inherits(gridify_r, "reactive")) gridify_r() else gridify_r()
167+
if (!methods::is(obj, "gridifyClass")) {
168+
stop(
169+
"gridify_with_settings_srv: 'gridify_r' must return a 'gridifyClass' object.\n",
170+
"Received class: ", paste(class(obj), collapse = ", "),
171+
call. = FALSE
172+
)
173+
}
174+
obj
175+
})
176+
177+
p_height <- shiny::reactive(as.integer(input$height))
178+
p_width <- shiny::reactive(as.integer(input$width))
179+
180+
output$plot_ui <- shiny::renderUI({
181+
shiny::plotOutput(
182+
outputId = shiny::NS(id)("plot_out"),
183+
height = paste0(p_height(), "px"),
184+
width = paste0(p_width(), "px")
185+
)
186+
})
187+
188+
output$plot_out <- shiny::renderPlot(
189+
expr = {
190+
methods::show(get_obj())
191+
},
192+
height = p_height,
193+
width = p_width
194+
)
195+
196+
output$dl_png <- shiny::downloadHandler(
197+
filename = function() {
198+
paste0("gridify_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".png")
199+
},
200+
content = function(file) {
201+
grDevices::png(
202+
filename = file,
203+
width = p_width(),
204+
height = p_height(),
205+
units = "px",
206+
res = 96L
207+
)
208+
print(get_obj())
209+
grDevices::dev.off()
210+
}
211+
)
212+
213+
output$dl_pdf <- shiny::downloadHandler(
214+
filename = function() {
215+
paste0("gridify_", format(Sys.time(), "%Y%m%d_%H%M%S"), ".pdf")
216+
},
217+
content = function(file) {
218+
grDevices::pdf(
219+
file = file,
220+
width = p_width() / 96,
221+
height = p_height() / 96
222+
)
223+
print(get_obj())
224+
grDevices::dev.off()
225+
}
226+
)
227+
228+
invisible(NULL)
229+
})
230+
}

_pkgdown.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ articles:
1515
- simple_examples
1616
- multi_page_examples
1717
- create_custom_layout
18+
- shiny_integration
1819
- transparency
1920

2021
reference:
@@ -47,6 +48,10 @@ reference:
4748
contents:
4849
- paginate_table
4950

51+
- subtitle: Shiny integration
52+
contents:
53+
- gridify_with_settings
54+
5055
- title: gridify custom layout development
5156
- subtitle: functions
5257
contents:

man/gridify_with_settings.Rd

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

0 commit comments

Comments
 (0)