-
Notifications
You must be signed in to change notification settings - Fork 10
/
funky_heatmap.R
278 lines (267 loc) Β· 10.4 KB
/
funky_heatmap.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#' Generate a funky heatmaps for benchmarks
#'
#' Allows generating heatmap-like visualisations for benchmark data
#' frames. Funky heatmaps can be fine-tuned by providing annotations of the
#' columns and rows, which allows assigning multiple palettes or geometries
#' or grouping rows and columns together in categories.
#'
#' @param data A data frame with items by row and features in the columns.
#' Must contain one column named `"id"`.
#'
#' @param column_info A data frame describing which columns in `data` to
#' plot. This data frame should contain the following columns:
#'
#' * `id` (`character`, required): A column name in `data` to plot. Determines the
#' size of the resulting geoms, and also the color unless `color` is
#' specified.
#'
#' * `id_color` (`character`): A column name in `data` to use for the color
#' of the resulting geoms. If `NA`, the `id` column will be used.
#'
#' * `id_size` (`character`): A column name in `data` to use for the size
#' of the resulting geoms. If `NA`, the `id` column will be used.
#'
#' * `name` (`character`): A label for the column. If `NA` or `""`,
#' no label will be plotted. If this column is missing, `id` will
#' be used to generate the `name` column.
#'
#' * `geom` (`character`): The geom of the column. Must be one of:
#' `"funkyrect"`, `"circle"`, `"rect"`, `"bar"`, `"pie"`, `"text"` or `"image"`.
#' For `"text"`, the corresponding column in `data` must be a `character`.
#' For `"pie"`, the column must be a list of named numeric vectors.
#' For all other geoms, the column must be a `numeric`.
#'
#' * `group` (`character`): The grouping id of each column, must match with
#' `column_groups$group`. If this column is missing or all values are `NA`,
#' columns are assumed not to be grouped.
#'
#' * `palette` (`character`): Which palette to colour the geom by.
#' Each value should have a matching value in `palettes$palette`.
#'
#' * `width`: Custom width for this column (default: 1).
#'
#' * `overlay`: Whether to overlay this column over the previous column.
#' If so, the width of that column will be inherited.
#'
#' * `legend`: Whether or not to add a legend for this column.
#'
#' * `hjust`: Horizontal alignment of the bar, must be between \[0,1\]
#' (only for `geom = "bar"`).
#'
#' * `vjust`: Vertical alignment of the label, must be between \[0,1\]
#' (only for `geom = "text"`).
#'
#' * `size`: Size of the label, must be a numeric value
#' (only for `geom = "text"`).
#'
#' * `label`: Which column to use as a label (only for `geom = "text"`).
#'
#' * `directory`: Which directory to use to find the images (only for `geom = "image"`).
#'
#' * `extension`: The extension of the images (only for `geom = "image"`).
#'
#' * `draw_outline`: Whether or not to draw bounding guides (only for `geom == "bar"`).
#' Default: `TRUE`.
#'
#' * `options` (`list` or `json`): Any of the options above. Any values in this
#' column will be spread across the other columns. This is useful for
#' not having to provide a data frame with 1000s of columns.
#' This column can be a json string.
#'
#' @param row_info A data frame describing the rows of `data`.
#' This data should contain two columns:
#'
#' * `id` (`character`): Corresponds to the column `data$id`.
#'
#' * `group` (`character`): The group of the row.
#' If all are `NA`, the rows will not be split up into groups.
#'
#' @param column_groups A data frame describing of how to group the columns
#' in `column_info`. Can consist of the following columns:
#'
#' * `group` (`character`): The corresponding group in `column_info$group`.
#' * `palette` (`character`, optional): The palette used to colour the
#' column group backgrounds.
#' * `level1` (`character`): The label at the highest level.
#' * `level2` (`character`, optional): The label at the middle level.
#' * `level3` (`character`, optional): The label at the lowest level
#' (not recommended).
#'
#' @param row_groups A data frame describing of how to group the rows
#' in `row_info`. Can consist of the following columns:
#'
#' * `group` (`character`): The corresponding group in `row_info$group`.
#' * `level1` (`character`): The label at the highest level.
#' * `level2` (`character`, optional): The label at the middle level.
#' * `level3` (`character`, optional): The label at the lowest level
#' (not recommended).
#'
#' @param palettes A named list of palettes. Each entry in `column_info$palette`
#' should have an entry in this object. If an entry is missing, the type
#' of the column will be inferred (categorical or numerical) and one of the
#' default palettes will be applied. Alternatively, the name of one of the
#' standard palette names can be used:
#'
#' * `numerical`: `"Greys"`, `"Blues"`, `"Reds"`, `"YlOrBr"`, `"Greens"`
#' * `categorical`: `"Set3"`, `"Set1"`, `"Set2"`, `"Dark2"`
#'
#' @param legends A list of legends to add to the plot. Each entry in
#' `column_info$legend` should have a corresponding entry in this object.
#' Each entry should be a list with the following names:
#' * `palette` (`character`): The palette to use for the legend. Must be
#' a value in `palettes`.
#' * `geom` (`character`): The geom of the legend. Must be one of:
#' `"funkyrect"`, `"circle"`, `"rect"`, `"bar"`, `"pie"`, `"text"`, `"image"`.
#' * `title` (`character`, optional): The title of the legend. Defaults
#' to the palette name.
#' * `enabled` (`logical`, optional): Whether or not to add the legend.
#' Defaults to `TRUE`.
#' * `labels` (`character`, optional): The labels to use for the legend.
#' The defaults depend on the selected geom.
#' * `size` (`numeric`, optional): The size of the listed geoms.
#' The defaults depend on the selected geom.
#' * `color` (`character`, optional): The color of the listed geoms.
#' The defaults depend on the selected geom.
#' * `values` (optional): Used as values for the text and image geoms.
#' * `label_width` (`numeric`, optional): The width of the labels
#' (only when geom is `text` or `pie`). Defaults to `1`
#' for text and `2` for images.
#' * `value_width` (`numeric`, optional): The width of the values
#' (only for `geom = "text"`). Defaults to `2`.
#' * `label_hjust` (`numeric`, optional): The horizontal alignment of the
#' labels (only when geom is `circle`, `rect` or `funkyrect`).
#' Defaults to `0.5`.
#'
#' @param position_args Sets parameters that affect positioning within a
#' plot, such as row and column dimensions, annotation details, and the
#' expansion directions of the plot. See `position_arguments()` for more information.
#'
#' @param scale_column Whether or not to apply min-max scaling to each
#' numerical column.
#'
#' @param add_abc Whether or not to add subfigure labels to the different
#' columns groups.
#'
#' @param col_annot_offset DEPRECATED: use `position_args = position_arguments(col_annot_offset = ...)` instead.
#' @param col_annot_angle DEPRECATED: use `position_args = position_arguments(col_annot_angle = ...)` instead.
#' @param expand DEPRECATED: use `position_args = position_arguments(expand_* = ...)` instead.
#'
#' @importFrom ggforce geom_arc_bar geom_circle geom_arc
#' @importFrom cowplot theme_nothing
#' @importFrom patchwork wrap_plots plot_spacer
#'
#' @returns A ggplot. `.$width` and `.$height` are suggested dimensions for
#' storing the plot with [ggsave()].
#'
#' @export
#'
#' @examples
#' library(tibble, warn.conflicts = FALSE)
#'
#' data("mtcars")
#'
#' data <- rownames_to_column(mtcars, "id")
#'
#' funky_heatmap(data)
funky_heatmap <- function(
data,
column_info = NULL,
row_info = NULL,
column_groups = NULL,
row_groups = NULL,
palettes = NULL,
legends = NULL,
position_args = position_arguments(),
scale_column = TRUE,
add_abc = TRUE,
col_annot_offset,
col_annot_angle,
expand) {
# validate input objects
data <- verify_data(data)
column_info <- verify_column_info(column_info, data)
row_info <- verify_row_info(row_info, data)
column_groups <- verify_column_groups(column_groups, column_info)
row_groups <- verify_row_groups(row_groups, row_info)
palettes <- verify_palettes(palettes, column_info, data)
legends <- verify_legends(legends, palettes, column_info, data)
# check deprecated arguments
if (!missing(col_annot_offset)) {
warning("Argument `col_annot_offset` is deprecated. Use `position_arguments(col_annot_offset = ...)` instead.")
position_args$col_annot_offset <- col_annot_offset
}
if (!missing(col_annot_angle)) {
warning("Argument `col_annot_angle` is deprecated. Use `position_arguments(col_annot_angle = ...)` instead.")
position_args$col_annot_angle <- col_annot_angle
}
if (!missing(expand)) {
warning("Argument `expand` is deprecated. Use `position_arguments(expand_* = ...)` instead.")
for (name in names(expand)) {
position_args[[paste0("expand_", name)]] <- expand[[name]]
}
}
# todo: add column groups to verify_palettes
geom_positions <- calculate_geom_positions(
data,
column_info,
row_info,
column_groups,
row_groups,
palettes,
position_args,
scale_column,
add_abc
)
main_plot <- compose_ggplot(
geom_positions,
position_args
)
# start plotting legends
geom_legend_funs <- list(
funkyrect = create_funkyrect_legend,
circle = create_circle_legend,
rect = create_rect_legend,
pie = create_pie_legend,
text = create_text_legend
# image = create_image_legend
# todo: add text legend
# todo: add bar legend
)
legend_plots <- list()
for (legend in legends) {
if (legend$enabled) {
legend_fun <- geom_legend_funs[[legend$geom]]
legend_args <- legend
legend_args$geom <- NULL
legend_args$enabled <- NULL
legend_args$palette <- NULL
legend_args$position_args <- position_args
legend_plot <- do.call(legend_fun, legend_args)
legend_plots <- c(legend_plots, list(legend_plot))
}
}
if (length(legend_plots) == 0) {
return(main_plot)
}
legend_widths <- map_dbl(legend_plots, ~ .x$width)
legend_heights <- map_dbl(legend_plots, ~ .x$height)
heights <- main_plot$height
width <- main_plot$width
heights <- c(heights, .1, max(legend_heights))
width <- max(width, sum(legend_widths))
out <- patchwork::wrap_plots(
main_plot,
patchwork::plot_spacer(),
patchwork::wrap_plots(
legend_plots,
nrow = 1,
widths = legend_widths
),
ncol = 1,
heights = heights
)
# TODO: fix this heuristic
out$width <- width
out$height <- sum(heights)
out
}