Skip to content

Commit fde96f7

Browse files
brandonlukasclaude
andcommitted
Add rasterization support for simple heatmap annotations
Previously only the heatmap body (matrix) supported rasterization via use_raster. This adds the same capability to simple annotations (vector/matrix values drawn as colored cells via anno_simple), reducing PDF/SVG file sizes for large annotations. Changes: - Add rasterize_in_viewport() helper to utils.R, encapsulating the temp-device rasterization pattern from draw_heatmap_body() - Add raster_param slot to SingleAnnotation class with use_raster, raster_device, raster_quality, and related parameters - Wrap annotation drawing in SingleAnnotation draw() with rasterization when use_raster=TRUE (annotation names remain vector graphics) - Pass raster params through HeatmapAnnotation constructor to each SingleAnnotation - Add annotation_use_raster global option to ht_opt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dea9f1d commit fde96f7

4 files changed

Lines changed: 161 additions & 11 deletions

File tree

R/HeatmapAnnotation-class.R

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,14 @@ HeatmapAnnotation = function(...,
116116
height = NULL,
117117
width = NULL,
118118
simple_anno_size = ht_opt$simple_anno_size,
119-
simple_anno_size_adjust = FALSE
119+
simple_anno_size_adjust = FALSE,
120+
121+
use_raster = FALSE,
122+
raster_device = NULL,
123+
raster_quality = 1,
124+
raster_device_param = list(),
125+
raster_by_magick = requireNamespace("magick", quietly = TRUE),
126+
raster_magick_filter = NULL
120127
) {
121128

122129
dev.null()
@@ -136,6 +143,10 @@ HeatmapAnnotation = function(...,
136143

137144
fun_args = names(as.list(environment()))
138145

146+
if(missing(use_raster) && !is.null(ht_opt$annotation_use_raster)) {
147+
use_raster = ht_opt$annotation_use_raster
148+
}
149+
139150
verbose = ht_opt$verbose
140151

141152
.Object = new("HeatmapAnnotation")
@@ -326,12 +337,18 @@ HeatmapAnnotation = function(...,
326337
i_anno = i_anno + 1
327338
arg_list = list(name = ag, which = which,
328339
label = annotation_label[[i_anno]],
329-
show_name = show_annotation_name[[i_anno]],
330-
name_gp = subset_gp(annotation_name_gp, i_anno),
331-
name_offset = annotation_name_offset[[i_anno]],
332-
name_side = annotation_name_side[i_anno],
340+
show_name = show_annotation_name[[i_anno]],
341+
name_gp = subset_gp(annotation_name_gp, i_anno),
342+
name_offset = annotation_name_offset[[i_anno]],
343+
name_side = annotation_name_side[i_anno],
333344
name_rot = annotation_name_rot[[i_anno]],
334-
border = border[i_anno])
345+
border = border[i_anno],
346+
use_raster = use_raster,
347+
raster_device = raster_device,
348+
raster_quality = raster_quality,
349+
raster_device_param = raster_device_param,
350+
raster_by_magick = raster_by_magick,
351+
raster_magick_filter = raster_magick_filter)
335352

336353
if(inherits(anno_value_list[[ag]], c("function", "AnnotationFunction"))) {
337354
arg_list$fun = anno_value_list[[ag]]

R/SingleAnnotation-class.R

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ SingleAnnotation = setClass("SingleAnnotation",
3636
width = "ANY",
3737
height = "ANY",
3838
extended = "ANY",
39-
subsettable = "logical"
39+
subsettable = "logical",
40+
raster_param = "list"
4041
),
4142
prototype = list(
4243
color_mapping = NULL,
@@ -45,7 +46,8 @@ SingleAnnotation = setClass("SingleAnnotation",
4546
color_is_random = FALSE,
4647
name_to_data_vp = FALSE,
4748
extended = unit(c(0, 0, 0, 0), "mm"),
48-
subsettable = FALSE
49+
subsettable = FALSE,
50+
raster_param = list(use_raster = FALSE)
4951
)
5052
)
5153

@@ -156,7 +158,13 @@ SingleAnnotation = function(name, value, col, fun,
156158
name_side = ifelse(which == "column", "right", "bottom"),
157159
name_rot = NULL,
158160
simple_anno_size = ht_opt$simple_anno_size,
159-
width = NULL, height = NULL) {
161+
width = NULL, height = NULL,
162+
use_raster = FALSE,
163+
raster_device = NULL,
164+
raster_quality = 1,
165+
raster_device_param = list(),
166+
raster_by_magick = requireNamespace("magick", quietly = TRUE),
167+
raster_magick_filter = NULL) {
160168

161169
.ENV$current_annotation_which = NULL
162170
which = match.arg(which)[1]
@@ -582,6 +590,22 @@ SingleAnnotation = function(name, value, col, fun,
582590
.Object@subsettable = .Object@fun@subsettable
583591
}
584592

593+
if(is.null(raster_device)) {
594+
if(requireNamespace("Cairo", quietly = TRUE)) {
595+
raster_device = "CairoPNG"
596+
} else {
597+
raster_device = "png"
598+
}
599+
}
600+
.Object@raster_param = list(
601+
use_raster = use_raster,
602+
raster_device = raster_device,
603+
raster_quality = raster_quality,
604+
raster_device_param = raster_device_param,
605+
raster_by_magick = raster_by_magick,
606+
raster_magick_filter = raster_magick_filter
607+
)
608+
585609
return(.Object)
586610
}
587611

@@ -666,8 +690,21 @@ setMethod(f = "draw",
666690
xscale = data_scale$x, yscale = data_scale$y))
667691

668692
if(verbose) qqcat("execute annotation function\n")
669-
draw(object@fun, index = index, k = k, n = n)
670-
693+
use_raster = isTRUE(object@raster_param$use_raster)
694+
if(use_raster) {
695+
rp = object@raster_param
696+
rasterize_in_viewport(
697+
draw_fun = function() draw(object@fun, index = index, k = k, n = n),
698+
raster_device = rp$raster_device,
699+
raster_quality = rp$raster_quality,
700+
raster_device_param = rp$raster_device_param,
701+
raster_by_magick = rp$raster_by_magick,
702+
raster_magick_filter = rp$raster_magick_filter
703+
)
704+
} else {
705+
draw(object@fun, index = index, k = k, n = n)
706+
}
707+
671708
# add annotation name
672709
draw_name = object@name_param$show
673710
if(object@name_param$show && n > 1) {

R/global.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ ht_opt = setGlobalOptions(
209209
.value = FALSE
210210
),
211211
"validate_names" = TRUE,
212+
annotation_use_raster = list(
213+
.value = NULL,
214+
.class = "logical",
215+
.length = 1
216+
),
212217
raster_temp_image_max_width = 30000,
213218
raster_temp_image_max_height = 30000,
214219
COLOR = c("blue", "#EEEEEE", "red")

R/utils.R

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,97 @@ setAs("list", "HeatmapList", function(from) {
11371137
})
11381138

11391139

1140+
rasterize_in_viewport = function(draw_fun,
1141+
raster_device = "png",
1142+
raster_quality = 1,
1143+
raster_device_param = list(),
1144+
raster_by_magick = FALSE,
1145+
raster_magick_filter = NULL) {
1146+
1147+
# calculate current viewport size in pixels
1148+
vp_width_pt = max(1, ceiling(convertWidth(unit(1, "npc"), "bigpts", valueOnly = TRUE)))
1149+
vp_height_pt = max(1, ceiling(convertHeight(unit(1, "npc"), "bigpts", valueOnly = TRUE)))
1150+
1151+
if(raster_quality < 1) raster_quality = 1
1152+
vp_width_pt = ceiling(vp_width_pt * raster_quality)
1153+
vp_height_pt = ceiling(vp_height_pt * raster_quality)
1154+
1155+
# if viewport is too small, fall back to vector
1156+
if(vp_width_pt < 1 || vp_height_pt < 1) {
1157+
draw_fun()
1158+
return(invisible(NULL))
1159+
}
1160+
1161+
device_info = switch(raster_device,
1162+
png = c("grDevices", "png", "readPNG"),
1163+
jpeg = c("grDevices", "jpeg", "readJPEG"),
1164+
tiff = c("grDevices", "tiff", "readTIFF"),
1165+
CairoPNG = c("Cairo", "png", "readPNG"),
1166+
CairoJPEG = c("Cairo", "jpeg", "readJPEG"),
1167+
CairoTIFF = c("Cairo", "tiff", "readTIFF"),
1168+
agg_png = c("ragg", "png", "readPNG")
1169+
)
1170+
1171+
if(!requireNamespace(device_info[1], quietly = TRUE)) {
1172+
stop_wrap(paste0("Need ", device_info[1], " package to write image."))
1173+
}
1174+
if(!requireNamespace(device_info[2], quietly = TRUE)) {
1175+
stop_wrap(paste0("Need ", device_info[2], " package to read image."))
1176+
}
1177+
1178+
if(raster_device %in% c("png", "jpeg", "tiff")) {
1179+
if(!"type" %in% names(raster_device_param)) {
1180+
if(capabilities("cairo")) {
1181+
raster_device_param$type = "cairo"
1182+
}
1183+
}
1184+
}
1185+
1186+
temp_image_width = as.integer(ceiling(max(vp_width_pt, 1)))
1187+
temp_image_height = as.integer(ceiling(max(vp_height_pt, 1)))
1188+
1189+
if(!is.na(ht_opt$raster_temp_image_max_width)) {
1190+
temp_image_width = min(temp_image_width, ht_opt$raster_temp_image_max_width)
1191+
}
1192+
if(!is.na(ht_opt$raster_temp_image_max_height)) {
1193+
temp_image_height = min(temp_image_height, ht_opt$raster_temp_image_max_height)
1194+
}
1195+
1196+
temp_dir = tempdir()
1197+
temp_image = tempfile(pattern = ".annotation_raster_", tmpdir = temp_dir,
1198+
fileext = paste0(".", device_info[2]))
1199+
device_fun = getFromNamespace(raster_device, ns = device_info[1])
1200+
1201+
oe = try(do.call(device_fun, c(list(filename = temp_image,
1202+
width = temp_image_width, height = temp_image_height), raster_device_param)))
1203+
if(inherits(oe, "try-error")) {
1204+
stop_wrap(qq("The temporary image size for annotation rasterization is too large (@{temp_image_width} x @{temp_image_height} px)."))
1205+
}
1206+
1207+
draw_fun()
1208+
dev.off2()
1209+
1210+
if(raster_by_magick) {
1211+
if(!requireNamespace("magick", quietly = TRUE)) {
1212+
stop_wrap("'magick' package should be installed.")
1213+
}
1214+
image = magick::image_read(temp_image)
1215+
image = magick::image_resize(image,
1216+
paste0(vp_width_pt, "x", vp_height_pt, "!"),
1217+
filter = raster_magick_filter)
1218+
image = as.raster(image)
1219+
} else {
1220+
image = getFromNamespace(device_info[3], ns = device_info[2])(temp_image)
1221+
}
1222+
1223+
grid.raster(image, width = unit(1, "npc"), height = unit(1, "npc"), interpolate = FALSE)
1224+
1225+
file.remove(temp_image)
1226+
1227+
invisible(NULL)
1228+
}
1229+
1230+
11401231
draw_heatmap_in_jupyter = function(ht, ...) {
11411232
width = getOption("repr.plot.width")
11421233
height = getOption("repr.plot.height")

0 commit comments

Comments
 (0)