plotcli-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CLAUDE.md +51 -0
- LICENSE +21 -0
- PKG-INFO +358 -0
- README.md +340 -0
- main.py +6 -0
- plotcli-original/.Rbuildignore +18 -0
- plotcli-original/.github/workflows/deploy_docs.yml +43 -0
- plotcli-original/.gitignore +46 -0
- plotcli-original/DESCRIPTION +25 -0
- plotcli-original/NAMESPACE +60 -0
- plotcli-original/NEWS.md +112 -0
- plotcli-original/R/ascii_escape.r +13 -0
- plotcli-original/R/canvas.r +586 -0
- plotcli-original/R/class_functions.r +114 -0
- plotcli-original/R/geom_registry.r +1376 -0
- plotcli-original/R/ggplotcli.r +234 -0
- plotcli-original/R/ggplotcli_helpers.r +1099 -0
- plotcli-original/R/helper_functions.r +351 -0
- plotcli-original/R/plotcli.r +963 -0
- plotcli-original/R/plotcli_grid.r +1 -0
- plotcli-original/R/plotcli_wrappers.r +416 -0
- plotcli-original/R/zzz.r +15 -0
- plotcli-original/README.md +192 -0
- plotcli-original/docs/ascii.png +0 -0
- plotcli-original/docs/bar.png +0 -0
- plotcli-original/docs/block.png +0 -0
- plotcli-original/docs/boxplot.png +0 -0
- plotcli-original/docs/density.png +0 -0
- plotcli-original/docs/facet.png +0 -0
- plotcli-original/docs/facet_grid.png +0 -0
- plotcli-original/docs/generate_png.sh +137 -0
- plotcli-original/docs/heatmap.png +0 -0
- plotcli-original/docs/histogram.png +0 -0
- plotcli-original/docs/line.png +0 -0
- plotcli-original/docs/noborder.png +0 -0
- plotcli-original/docs/scatter.png +0 -0
- plotcli-original/docs/showcase.R +182 -0
- plotcli-original/inst/doc/ggplotcli.R +231 -0
- plotcli-original/inst/doc/ggplotcli.Rmd +329 -0
- plotcli-original/inst/doc/ggplotcli.html +1078 -0
- plotcli-original/inst/doc/plotcli_class.R +98 -0
- plotcli-original/inst/doc/plotcli_class.Rmd +121 -0
- plotcli-original/inst/doc/plotcli_class.html +564 -0
- plotcli-original/inst/doc/plotcli_wrappers.R +35 -0
- plotcli-original/inst/doc/plotcli_wrappers.Rmd +62 -0
- plotcli-original/inst/doc/plotcli_wrappers.html +546 -0
- plotcli-original/man/AsciiCanvas.Rd +116 -0
- plotcli-original/man/BlockCanvas.Rd +132 -0
- plotcli-original/man/BrailleCanvas.Rd +146 -0
- plotcli-original/man/Canvas.Rd +492 -0
- plotcli-original/man/GeomRegistry.Rd +9 -0
- plotcli-original/man/add_legend_to_output.Rd +12 -0
- plotcli-original/man/braille_dot_bit.Rd +29 -0
- plotcli-original/man/braille_set_dot.Rd +21 -0
- plotcli-original/man/bresenham.Rd +27 -0
- plotcli-original/man/build_plot_output.Rd +28 -0
- plotcli-original/man/build_plot_output_v2.Rd +41 -0
- plotcli-original/man/cat_plot_matrix.Rd +17 -0
- plotcli-original/man/cbind.plotcli.Rd +19 -0
- plotcli-original/man/cbind_plots.Rd +17 -0
- plotcli-original/man/color_to_term.Rd +18 -0
- plotcli-original/man/create_canvas.Rd +21 -0
- plotcli-original/man/create_panel_scales.Rd +29 -0
- plotcli-original/man/create_scales.Rd +27 -0
- plotcli-original/man/dot-geom_registry.Rd +16 -0
- plotcli-original/man/draw_border.Rd +12 -0
- plotcli-original/man/draw_grid.Rd +12 -0
- plotcli-original/man/extract_legend_info.Rd +12 -0
- plotcli-original/man/extract_plot_labels.Rd +12 -0
- plotcli-original/man/extract_plot_style.Rd +12 -0
- plotcli-original/man/format_axis_label.Rd +18 -0
- plotcli-original/man/format_four_chars.Rd +21 -0
- plotcli-original/man/geom_area_handler.Rd +12 -0
- plotcli-original/man/geom_bar_handler.Rd +12 -0
- plotcli-original/man/geom_boxplot_handler.Rd +13 -0
- plotcli-original/man/geom_density_handler.Rd +12 -0
- plotcli-original/man/geom_histogram_handler.Rd +12 -0
- plotcli-original/man/geom_hline_handler.Rd +12 -0
- plotcli-original/man/geom_line_handler.Rd +12 -0
- plotcli-original/man/geom_path_handler.Rd +12 -0
- plotcli-original/man/geom_point_handler.Rd +12 -0
- plotcli-original/man/geom_rect_handler.Rd +12 -0
- plotcli-original/man/geom_segment_handler.Rd +12 -0
- plotcli-original/man/geom_smooth_handler.Rd +12 -0
- plotcli-original/man/geom_text_handler.Rd +12 -0
- plotcli-original/man/geom_vline_handler.Rd +12 -0
- plotcli-original/man/get_color_hue.Rd +18 -0
- plotcli-original/man/get_data_subset.Rd +23 -0
- plotcli-original/man/get_facet_info.Rd +18 -0
- plotcli-original/man/get_geom_handler.Rd +17 -0
- plotcli-original/man/get_term_colors.Rd +21 -0
- plotcli-original/man/ggplotcli.Rd +83 -0
- plotcli-original/man/init_color_mapping.Rd +15 -0
- plotcli-original/man/is_braille.Rd +20 -0
- plotcli-original/man/is_geom_registered.Rd +17 -0
- plotcli-original/man/list_registered_geoms.Rd +14 -0
- plotcli-original/man/make_colored.Rd +23 -0
- plotcli-original/man/make_unique_names.Rd +20 -0
- plotcli-original/man/normalize_data.Rd +27 -0
- plotcli-original/man/pclib.Rd +48 -0
- plotcli-original/man/pclibx.Rd +46 -0
- plotcli-original/man/pclid.Rd +44 -0
- plotcli-original/man/pclih.Rd +50 -0
- plotcli-original/man/pclil.Rd +48 -0
- plotcli-original/man/pclis.Rd +48 -0
- plotcli-original/man/pixel_to_braille.Rd +23 -0
- plotcli-original/man/plotcli.Rd +598 -0
- plotcli-original/man/plotcli_bar.Rd +48 -0
- plotcli-original/man/plotcli_box.Rd +46 -0
- plotcli-original/man/plotcli_density.Rd +44 -0
- plotcli-original/man/plotcli_histogram.Rd +50 -0
- plotcli-original/man/plotcli_line.Rd +48 -0
- plotcli-original/man/plotcli_options.Rd +18 -0
- plotcli-original/man/plotcli_scatter.Rd +48 -0
- plotcli-original/man/plus-.plotcli.Rd +19 -0
- plotcli-original/man/rbind.plotcli.Rd +19 -0
- plotcli-original/man/rbind_plots.Rd +17 -0
- plotcli-original/man/register_geom.Rd +21 -0
- plotcli-original/man/remove_color_codes.Rd +21 -0
- plotcli-original/man/render_faceted_plot.Rd +25 -0
- plotcli-original/man/render_single_panel.Rd +12 -0
- plotcli-original/man/safe_aes_name.Rd +18 -0
- plotcli-original/tests/testthat/test-new-geoms.R +136 -0
- plotcli-original/tests/testthat/test-plotcli.R +69 -0
- plotcli-original/tests/testthat.R +4 -0
- plotcli-original/vignettes/ggplotcli.Rmd +329 -0
- plotcli-original/vignettes/plotcli_class.R +98 -0
- plotcli-original/vignettes/plotcli_class.Rmd +121 -0
- plotcli-original/vignettes/plotcli_wrappers.R +35 -0
- plotcli-original/vignettes/plotcli_wrappers.Rmd +62 -0
- plotcli.egg-info/PKG-INFO +11 -0
- plotcli.egg-info/SOURCES.txt +7 -0
- plotcli.egg-info/dependency_links.txt +1 -0
- plotcli.egg-info/entry_points.txt +3 -0
- plotcli.egg-info/top_level.txt +1 -0
- plotcli.py +978 -0
- plotcli_py-0.1.0.dist-info/METADATA +358 -0
- plotcli_py-0.1.0.dist-info/RECORD +143 -0
- plotcli_py-0.1.0.dist-info/WHEEL +4 -0
- plotcli_py-0.1.0.dist-info/entry_points.txt +2 -0
- plotcli_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyproject.toml +31 -0
- uv.lock +8 -0
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
#' Null-coalescing operator (if rlang not available)
|
|
2
|
+
#' @keywords internal
|
|
3
|
+
`%||%` <- function(x, y) if (is.null(x)) y else x
|
|
4
|
+
|
|
5
|
+
#' Extract Plot Style from ggplot Theme
|
|
6
|
+
#' @keywords internal
|
|
7
|
+
extract_plot_style <- function(built, border, grid, legend) {
|
|
8
|
+
theme <- built$plot$theme
|
|
9
|
+
|
|
10
|
+
# Determine border style
|
|
11
|
+
if (border == "auto") {
|
|
12
|
+
# Check if theme has panel.border
|
|
13
|
+
has_border <- !is.null(theme$panel.border) &&
|
|
14
|
+
!inherits(theme$panel.border, "element_blank")
|
|
15
|
+
# Or axis lines (like theme_classic)
|
|
16
|
+
has_axis_line <- !is.null(theme$axis.line) &&
|
|
17
|
+
!inherits(theme$axis.line, "element_blank")
|
|
18
|
+
border <- has_border || has_axis_line
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Determine grid style
|
|
22
|
+
if (grid == "auto") {
|
|
23
|
+
has_major <- !is.null(theme$panel.grid.major) &&
|
|
24
|
+
!inherits(theme$panel.grid.major, "element_blank")
|
|
25
|
+
has_minor <- !is.null(theme$panel.grid.minor) &&
|
|
26
|
+
!inherits(theme$panel.grid.minor, "element_blank")
|
|
27
|
+
|
|
28
|
+
if (has_major && has_minor) {
|
|
29
|
+
grid <- "both"
|
|
30
|
+
} else if (has_major) {
|
|
31
|
+
grid <- "major"
|
|
32
|
+
} else if (has_minor) {
|
|
33
|
+
grid <- "minor"
|
|
34
|
+
} else {
|
|
35
|
+
grid <- "none"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Determine legend position
|
|
40
|
+
if (legend == "auto") {
|
|
41
|
+
if (!is.null(theme$legend.position)) {
|
|
42
|
+
pos <- theme$legend.position
|
|
43
|
+
if (is.character(pos)) {
|
|
44
|
+
legend <- pos
|
|
45
|
+
} else {
|
|
46
|
+
legend <- "right" # Default for numeric positions
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
legend <- "right"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
list(
|
|
54
|
+
border = border,
|
|
55
|
+
grid = grid,
|
|
56
|
+
legend = legend
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#' Extract Plot Labels from ggplot
|
|
62
|
+
#' @keywords internal
|
|
63
|
+
extract_plot_labels <- function(built, title, subtitle, caption, axis_labels) {
|
|
64
|
+
labels <- built$plot$labels
|
|
65
|
+
|
|
66
|
+
list(
|
|
67
|
+
title = if (!is.null(title)) title else labels$title,
|
|
68
|
+
subtitle = if (subtitle) labels$subtitle else NULL,
|
|
69
|
+
caption = if (caption) labels$caption else NULL,
|
|
70
|
+
x = if (axis_labels) labels$x else NULL,
|
|
71
|
+
y = if (axis_labels) labels$y else NULL,
|
|
72
|
+
colour = labels$colour,
|
|
73
|
+
fill = labels$fill
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#' Extract Legend Information from ggplot
|
|
79
|
+
#' @keywords internal
|
|
80
|
+
extract_legend_info <- function(built) {
|
|
81
|
+
# Try to get color/colour scale
|
|
82
|
+
color_scale <- built$plot$scales$get_scales("colour")
|
|
83
|
+
fill_scale <- built$plot$scales$get_scales("fill")
|
|
84
|
+
|
|
85
|
+
legend_items <- list()
|
|
86
|
+
|
|
87
|
+
# Helper to extract colors from a scale (handles both discrete and continuous)
|
|
88
|
+
get_scale_colors <- function(scale, n) {
|
|
89
|
+
# First try direct palette call (works for discrete scales)
|
|
90
|
+
colors <- tryCatch(scale$palette(n), error = function(e) NULL)
|
|
91
|
+
|
|
92
|
+
# If that returned NA or NULL, try continuous scale approach
|
|
93
|
+
if (is.null(colors) || (length(colors) == 1 && is.na(colors[1])) ||
|
|
94
|
+
all(is.na(colors))) {
|
|
95
|
+
# For continuous scales, palette expects values in [0, 1]
|
|
96
|
+
colors <- tryCatch(
|
|
97
|
+
scale$palette(seq(0, 1, length.out = n)),
|
|
98
|
+
error = function(e) NULL
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
colors
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Extract from color scale
|
|
106
|
+
if (!is.null(color_scale)) {
|
|
107
|
+
tryCatch({
|
|
108
|
+
breaks <- color_scale$get_breaks()
|
|
109
|
+
labels <- color_scale$get_labels()
|
|
110
|
+
n <- length(breaks)
|
|
111
|
+
if (n > 0) {
|
|
112
|
+
colors <- get_scale_colors(color_scale, n)
|
|
113
|
+
if (!is.null(colors) && !all(is.na(colors))) {
|
|
114
|
+
legend_items$colour <- list(
|
|
115
|
+
title = built$plot$labels$colour %||% "colour",
|
|
116
|
+
labels = labels,
|
|
117
|
+
colors = colors
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, error = function(e) NULL)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Extract from fill scale
|
|
125
|
+
if (!is.null(fill_scale)) {
|
|
126
|
+
tryCatch({
|
|
127
|
+
breaks <- fill_scale$get_breaks()
|
|
128
|
+
labels <- fill_scale$get_labels()
|
|
129
|
+
n <- length(breaks)
|
|
130
|
+
if (n > 0) {
|
|
131
|
+
colors <- get_scale_colors(fill_scale, n)
|
|
132
|
+
if (!is.null(colors) && !all(is.na(colors))) {
|
|
133
|
+
legend_items$fill <- list(
|
|
134
|
+
title = built$plot$labels$fill %||% "fill",
|
|
135
|
+
labels = labels,
|
|
136
|
+
colors = colors
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, error = function(e) NULL)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
legend_items
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#' Render a single panel (non-faceted) plot
|
|
148
|
+
#' @keywords internal
|
|
149
|
+
render_single_panel <- function(built, width, height, canvas_type, style_opts) {
|
|
150
|
+
labels <- style_opts$labels
|
|
151
|
+
show_axes <- style_opts$show_axes
|
|
152
|
+
|
|
153
|
+
# Calculate margins
|
|
154
|
+
left_margin <- if (show_axes) 7 else 0
|
|
155
|
+
right_margin <- 1
|
|
156
|
+
bottom_margin <- if (show_axes) 2 else 0
|
|
157
|
+
if (!is.null(labels$x)) bottom_margin <- bottom_margin + 1
|
|
158
|
+
|
|
159
|
+
# Top margin for title/subtitle
|
|
160
|
+
top_margin <- 0
|
|
161
|
+
if (!is.null(labels$title)) top_margin <- top_margin + 1
|
|
162
|
+
if (!is.null(labels$subtitle)) top_margin <- top_margin + 1
|
|
163
|
+
|
|
164
|
+
# Bottom for caption
|
|
165
|
+
caption_margin <- if (!is.null(labels$caption)) 1 else 0
|
|
166
|
+
|
|
167
|
+
# Calculate plot area dimensions
|
|
168
|
+
plot_width <- width - left_margin - right_margin
|
|
169
|
+
plot_height <- height - bottom_margin - top_margin - caption_margin
|
|
170
|
+
|
|
171
|
+
if (plot_width < 5 || plot_height < 3) {
|
|
172
|
+
warning("Plot area too small")
|
|
173
|
+
return(invisible(NULL))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Create canvas for plot area
|
|
177
|
+
canvas <- create_canvas(plot_width, plot_height, canvas_type)
|
|
178
|
+
|
|
179
|
+
# Create scales (with border padding if needed, using canvas multipliers)
|
|
180
|
+
scales <- create_scales(built, canvas$pixel_width, canvas$pixel_height,
|
|
181
|
+
has_border = style_opts$border,
|
|
182
|
+
x_mult = canvas$x_mult, y_mult = canvas$y_mult)
|
|
183
|
+
|
|
184
|
+
# Draw grid lines first (behind data)
|
|
185
|
+
if (style_opts$grid != "none") {
|
|
186
|
+
draw_grid(canvas, scales, style_opts$grid)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Process each layer
|
|
190
|
+
for (i in seq_along(built$data)) {
|
|
191
|
+
layer_data <- built$data[[i]]
|
|
192
|
+
layer <- built$plot$layers[[i]]
|
|
193
|
+
|
|
194
|
+
# Get geom class name
|
|
195
|
+
geom_class <- class(layer$geom)[1]
|
|
196
|
+
|
|
197
|
+
# Get handler
|
|
198
|
+
handler <- get_geom_handler(geom_class)
|
|
199
|
+
|
|
200
|
+
if (is.null(handler)) {
|
|
201
|
+
warning(sprintf("No handler registered for geom: %s", geom_class))
|
|
202
|
+
next
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Get layer parameters
|
|
206
|
+
params <- layer$aes_params
|
|
207
|
+
|
|
208
|
+
# Call handler (pass style_opts for geoms that need it, like boxplot)
|
|
209
|
+
tryCatch({
|
|
210
|
+
handler(layer_data, canvas, scales, params, style_opts)
|
|
211
|
+
}, error = function(e) {
|
|
212
|
+
warning(sprintf("Error rendering %s: %s", geom_class, e$message))
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Draw border if requested
|
|
217
|
+
if (style_opts$border) {
|
|
218
|
+
draw_border(canvas)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Extract legend information
|
|
222
|
+
legend_info <- extract_legend_info(built)
|
|
223
|
+
|
|
224
|
+
# Build the final output matrix
|
|
225
|
+
output <- build_plot_output_v2(
|
|
226
|
+
canvas = canvas,
|
|
227
|
+
scales = scales,
|
|
228
|
+
width = width,
|
|
229
|
+
height = height,
|
|
230
|
+
style_opts = style_opts,
|
|
231
|
+
left_margin = left_margin,
|
|
232
|
+
top_margin = top_margin,
|
|
233
|
+
legend_info = legend_info
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Print
|
|
237
|
+
cat("\n")
|
|
238
|
+
for (i in seq_len(nrow(output))) {
|
|
239
|
+
cat(paste(output[i, ], collapse = ""), "\n")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
invisible(canvas)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#' Draw Grid Lines on Canvas
|
|
247
|
+
#' @keywords internal
|
|
248
|
+
draw_grid <- function(canvas, scales, grid_type) {
|
|
249
|
+
# Use a subtle character for grid
|
|
250
|
+
grid_color <- "silver"
|
|
251
|
+
|
|
252
|
+
# Major grid at tick positions
|
|
253
|
+
if (grid_type %in% c("major", "both")) {
|
|
254
|
+
# Vertical lines at x ticks
|
|
255
|
+
x_ticks <- pretty(scales$x_range, n = 5)
|
|
256
|
+
x_ticks <- x_ticks[x_ticks > scales$x_range[1] & x_ticks < scales$x_range[2]]
|
|
257
|
+
|
|
258
|
+
for (tick in x_ticks) {
|
|
259
|
+
x <- scales$x(tick)
|
|
260
|
+
canvas$draw_vline(round(x), color = grid_color)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Horizontal lines at y ticks
|
|
264
|
+
y_ticks <- pretty(scales$y_range, n = 5)
|
|
265
|
+
y_ticks <- y_ticks[y_ticks > scales$y_range[1] & y_ticks < scales$y_range[2]]
|
|
266
|
+
|
|
267
|
+
for (tick in y_ticks) {
|
|
268
|
+
y <- scales$y(tick)
|
|
269
|
+
canvas$draw_hline(round(y), color = grid_color)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Minor grid (more lines)
|
|
274
|
+
if (grid_type %in% c("minor", "both")) {
|
|
275
|
+
x_ticks <- pretty(scales$x_range, n = 10)
|
|
276
|
+
x_ticks <- x_ticks[x_ticks > scales$x_range[1] & x_ticks < scales$x_range[2]]
|
|
277
|
+
|
|
278
|
+
for (tick in x_ticks) {
|
|
279
|
+
x <- scales$x(tick)
|
|
280
|
+
# Only draw if not already a major grid line
|
|
281
|
+
if (grid_type == "minor" || !(tick %in% pretty(scales$x_range, n = 5))) {
|
|
282
|
+
canvas$draw_vline(round(x), color = grid_color)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
y_ticks <- pretty(scales$y_range, n = 10)
|
|
287
|
+
y_ticks <- y_ticks[y_ticks > scales$y_range[1] & y_ticks < scales$y_range[2]]
|
|
288
|
+
|
|
289
|
+
for (tick in y_ticks) {
|
|
290
|
+
y <- scales$y(tick)
|
|
291
|
+
if (grid_type == "minor" || !(tick %in% pretty(scales$y_range, n = 5))) {
|
|
292
|
+
canvas$draw_hline(round(y), color = grid_color)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
#' Draw Border Around Canvas
|
|
300
|
+
#' @keywords internal
|
|
301
|
+
draw_border <- function(canvas) {
|
|
302
|
+
# Draw rectangle around the entire canvas
|
|
303
|
+
canvas$draw_rect(1, 1, canvas$pixel_width, canvas$pixel_height, color = NULL)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
#' Build Plot Output with Axes and Title (v2)
|
|
308
|
+
#'
|
|
309
|
+
#' @param canvas The rendered canvas
|
|
310
|
+
#' @param scales The scales object
|
|
311
|
+
#' @param width Total width
|
|
312
|
+
#' @param height Total height
|
|
313
|
+
#' @param style_opts Style options
|
|
314
|
+
#' @param left_margin Left margin size
|
|
315
|
+
#' @param top_margin Top margin size
|
|
316
|
+
#' @param legend_info Legend information from extract_legend_info
|
|
317
|
+
#' @return Character matrix
|
|
318
|
+
#' @keywords internal
|
|
319
|
+
build_plot_output_v2 <- function(canvas, scales, width, height, style_opts,
|
|
320
|
+
left_margin, top_margin, legend_info = NULL) {
|
|
321
|
+
# Get rendered canvas
|
|
322
|
+
rendered <- canvas$render()
|
|
323
|
+
labels <- style_opts$labels
|
|
324
|
+
show_axes <- style_opts$show_axes
|
|
325
|
+
title_align <- style_opts$title_align
|
|
326
|
+
legend_position <- style_opts$legend
|
|
327
|
+
|
|
328
|
+
# Create output matrix
|
|
329
|
+
output <- matrix(" ", nrow = height, ncol = width)
|
|
330
|
+
|
|
331
|
+
current_row <- 1
|
|
332
|
+
|
|
333
|
+
# Add title if present
|
|
334
|
+
if (!is.null(labels$title)) {
|
|
335
|
+
title_text <- substr(labels$title, 1, width - 2)
|
|
336
|
+
title_chars <- strsplit(title_text, "")[[1]]
|
|
337
|
+
|
|
338
|
+
if (title_align == "center") {
|
|
339
|
+
start_col <- max(1, floor((width - length(title_chars)) / 2))
|
|
340
|
+
} else {
|
|
341
|
+
start_col <- left_margin + 1
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (i in seq_along(title_chars)) {
|
|
345
|
+
if (start_col + i - 1 <= width) {
|
|
346
|
+
output[current_row, start_col + i - 1] <- title_chars[i]
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
current_row <- current_row + 1
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Add subtitle if present
|
|
353
|
+
if (!is.null(labels$subtitle)) {
|
|
354
|
+
sub_text <- substr(labels$subtitle, 1, width - 2)
|
|
355
|
+
sub_chars <- strsplit(sub_text, "")[[1]]
|
|
356
|
+
|
|
357
|
+
if (title_align == "center") {
|
|
358
|
+
start_col <- max(1, floor((width - length(sub_chars)) / 2))
|
|
359
|
+
} else {
|
|
360
|
+
start_col <- left_margin + 1
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (i in seq_along(sub_chars)) {
|
|
364
|
+
if (start_col + i - 1 <= width) {
|
|
365
|
+
output[current_row, start_col + i - 1] <- sub_chars[i]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
current_row <- current_row + 1
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Copy canvas to output (offset by margins)
|
|
372
|
+
for (i in seq_len(nrow(rendered))) {
|
|
373
|
+
for (j in seq_len(ncol(rendered))) {
|
|
374
|
+
out_row <- i + top_margin
|
|
375
|
+
out_col <- j + left_margin
|
|
376
|
+
if (out_row <= height && out_col <= width) {
|
|
377
|
+
output[out_row, out_col] <- rendered[i, j]
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Draw Y axis values
|
|
383
|
+
if (show_axes) {
|
|
384
|
+
# Check if we have discrete y labels
|
|
385
|
+
if (!is.null(scales$y_labels) && length(scales$y_labels) > 0) {
|
|
386
|
+
# Use discrete labels for y-axis
|
|
387
|
+
for (i in seq_along(scales$y_labels)) {
|
|
388
|
+
pos <- scales$y_label_positions[i]
|
|
389
|
+
y_frac <- (pos - scales$y_range[1]) / (scales$y_range[2] - scales$y_range[1])
|
|
390
|
+
row <- round(nrow(rendered) - y_frac * (nrow(rendered) - 1)) + top_margin
|
|
391
|
+
|
|
392
|
+
if (row >= 1 && row <= height) {
|
|
393
|
+
label <- scales$y_labels[i]
|
|
394
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
395
|
+
|
|
396
|
+
start_col <- max(1, left_margin - length(label_chars))
|
|
397
|
+
for (j in seq_along(label_chars)) {
|
|
398
|
+
if (start_col + j - 1 < left_margin) {
|
|
399
|
+
output[row, start_col + j - 1] <- label_chars[j]
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
# Use numeric ticks for y-axis
|
|
406
|
+
y_ticks <- pretty(scales$y_range, n = 5)
|
|
407
|
+
y_ticks <- y_ticks[y_ticks >= scales$y_range[1] & y_ticks <= scales$y_range[2]]
|
|
408
|
+
|
|
409
|
+
for (tick in y_ticks) {
|
|
410
|
+
y_frac <- (tick - scales$y_range[1]) / (scales$y_range[2] - scales$y_range[1])
|
|
411
|
+
row <- round(nrow(rendered) - y_frac * (nrow(rendered) - 1)) + top_margin
|
|
412
|
+
|
|
413
|
+
if (row >= 1 && row <= height) {
|
|
414
|
+
label <- format_axis_label(tick)
|
|
415
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
416
|
+
|
|
417
|
+
start_col <- max(1, left_margin - length(label_chars))
|
|
418
|
+
for (j in seq_along(label_chars)) {
|
|
419
|
+
if (start_col + j - 1 < left_margin) {
|
|
420
|
+
output[row, start_col + j - 1] <- label_chars[j]
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
# Draw X axis values
|
|
428
|
+
x_row <- top_margin + nrow(rendered) + 1
|
|
429
|
+
|
|
430
|
+
# Check if we have discrete labels
|
|
431
|
+
if (!is.null(scales$x_labels) && length(scales$x_labels) > 0) {
|
|
432
|
+
# Use discrete labels
|
|
433
|
+
for (i in seq_along(scales$x_labels)) {
|
|
434
|
+
pos <- scales$x_label_positions[i]
|
|
435
|
+
x_frac <- (pos - scales$x_range[1]) / (scales$x_range[2] - scales$x_range[1])
|
|
436
|
+
col <- round(x_frac * (ncol(rendered) - 1)) + left_margin + 1
|
|
437
|
+
|
|
438
|
+
if (col >= left_margin && col <= width && x_row <= height) {
|
|
439
|
+
label <- scales$x_labels[i]
|
|
440
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
441
|
+
|
|
442
|
+
start_col <- col - floor(length(label_chars) / 2)
|
|
443
|
+
for (j in seq_along(label_chars)) {
|
|
444
|
+
if (start_col + j - 1 >= 1 && start_col + j - 1 <= width) {
|
|
445
|
+
output[x_row, start_col + j - 1] <- label_chars[j]
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
# Use numeric ticks
|
|
452
|
+
x_ticks <- pretty(scales$x_range, n = 5)
|
|
453
|
+
x_ticks <- x_ticks[x_ticks >= scales$x_range[1] & x_ticks <= scales$x_range[2]]
|
|
454
|
+
|
|
455
|
+
for (tick in x_ticks) {
|
|
456
|
+
x_frac <- (tick - scales$x_range[1]) / (scales$x_range[2] - scales$x_range[1])
|
|
457
|
+
col <- round(x_frac * (ncol(rendered) - 1)) + left_margin + 1
|
|
458
|
+
|
|
459
|
+
if (col >= left_margin && col <= width && x_row <= height) {
|
|
460
|
+
label <- format_axis_label(tick)
|
|
461
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
462
|
+
|
|
463
|
+
start_col <- col - floor(length(label_chars) / 2)
|
|
464
|
+
for (j in seq_along(label_chars)) {
|
|
465
|
+
if (start_col + j - 1 >= 1 && start_col + j - 1 <= width) {
|
|
466
|
+
output[x_row, start_col + j - 1] <- label_chars[j]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Add Y axis label (rotated - shown vertically on left)
|
|
475
|
+
if (!is.null(labels$y) && left_margin >= 3) {
|
|
476
|
+
y_label <- labels$y
|
|
477
|
+
# For terminal, just show abbreviated label at top-left
|
|
478
|
+
y_chars <- strsplit(substr(y_label, 1, min(nchar(y_label), nrow(rendered))), "")[[1]]
|
|
479
|
+
label_start <- top_margin + floor((nrow(rendered) - length(y_chars)) / 2)
|
|
480
|
+
for (i in seq_along(y_chars)) {
|
|
481
|
+
row <- label_start + i
|
|
482
|
+
if (row >= 1 && row <= height) {
|
|
483
|
+
output[row, 1] <- y_chars[i]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# Add X axis label (centered below x values)
|
|
489
|
+
if (!is.null(labels$x)) {
|
|
490
|
+
x_row <- top_margin + nrow(rendered) + 2
|
|
491
|
+
if (x_row <= height) {
|
|
492
|
+
x_label <- substr(labels$x, 1, ncol(rendered))
|
|
493
|
+
x_chars <- strsplit(x_label, "")[[1]]
|
|
494
|
+
start_col <- left_margin + floor((ncol(rendered) - length(x_chars)) / 2)
|
|
495
|
+
for (i in seq_along(x_chars)) {
|
|
496
|
+
if (start_col + i - 1 >= 1 && start_col + i - 1 <= width) {
|
|
497
|
+
output[x_row, start_col + i - 1] <- x_chars[i]
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# Add caption (bottom right)
|
|
504
|
+
if (!is.null(labels$caption)) {
|
|
505
|
+
cap_row <- height
|
|
506
|
+
cap_text <- substr(labels$caption, 1, width - 2)
|
|
507
|
+
cap_chars <- strsplit(cap_text, "")[[1]]
|
|
508
|
+
start_col <- width - length(cap_chars)
|
|
509
|
+
for (i in seq_along(cap_chars)) {
|
|
510
|
+
if (start_col + i - 1 >= 1) {
|
|
511
|
+
output[cap_row, start_col + i - 1] <- cap_chars[i]
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Add legend if present and not "none"
|
|
517
|
+
if (!is.null(legend_info) && length(legend_info) > 0 &&
|
|
518
|
+
!identical(legend_position, "none")) {
|
|
519
|
+
output <- add_legend_to_output(output, legend_info, legend_position,
|
|
520
|
+
top_margin, nrow(rendered))
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return(output)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
#' Add Legend to Output Matrix
|
|
528
|
+
#' @keywords internal
|
|
529
|
+
add_legend_to_output <- function(output, legend_info, position, top_margin, plot_height) {
|
|
530
|
+
# Get the first legend (colour or fill)
|
|
531
|
+
legend <- legend_info$colour %||% legend_info$fill
|
|
532
|
+
if (is.null(legend)) return(output)
|
|
533
|
+
|
|
534
|
+
n_items <- length(legend$labels)
|
|
535
|
+
if (n_items == 0) return(output)
|
|
536
|
+
|
|
537
|
+
# Calculate legend dimensions - include title width
|
|
538
|
+
max_label_len <- max(nchar(legend$labels))
|
|
539
|
+
title_len <- if (!is.null(legend$title)) nchar(legend$title) else 0
|
|
540
|
+
legend_width <- max(max_label_len + 3, title_len + 1) # "* label" or "Title "
|
|
541
|
+
|
|
542
|
+
height <- nrow(output)
|
|
543
|
+
width <- ncol(output)
|
|
544
|
+
|
|
545
|
+
if (position %in% c("right", "auto")) {
|
|
546
|
+
# Add legend to the right side
|
|
547
|
+
# Create legend column
|
|
548
|
+
legend_col <- matrix(" ", nrow = height, ncol = legend_width)
|
|
549
|
+
|
|
550
|
+
# Center legend vertically in plot area
|
|
551
|
+
legend_start_row <- top_margin + max(1, floor((plot_height - n_items - 1) / 2))
|
|
552
|
+
|
|
553
|
+
# Add title if present
|
|
554
|
+
if (!is.null(legend$title) && nchar(legend$title) > 0) {
|
|
555
|
+
title_chars <- strsplit(substr(legend$title, 1, legend_width - 1), "")[[1]]
|
|
556
|
+
for (i in seq_along(title_chars)) {
|
|
557
|
+
if (legend_start_row <= height) {
|
|
558
|
+
legend_col[legend_start_row, i] <- title_chars[i]
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
legend_start_row <- legend_start_row + 1
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
# Add each legend item
|
|
565
|
+
for (i in seq_len(n_items)) {
|
|
566
|
+
row <- legend_start_row + i - 1
|
|
567
|
+
if (row <= height && row >= 1) {
|
|
568
|
+
# Color indicator (use terminal color)
|
|
569
|
+
term_color <- color_to_term(legend$colors[i])
|
|
570
|
+
legend_col[row, 1] <- make_colored("*", term_color)
|
|
571
|
+
|
|
572
|
+
# Label
|
|
573
|
+
label_chars <- strsplit(legend$labels[i], "")[[1]]
|
|
574
|
+
for (j in seq_along(label_chars)) {
|
|
575
|
+
if (j + 2 <= legend_width) {
|
|
576
|
+
legend_col[row, j + 2] <- label_chars[j]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# Append legend to output
|
|
583
|
+
output <- cbind(output, legend_col)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
output
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
#' Build Plot Output with Axes and Title (legacy)
|
|
591
|
+
#'
|
|
592
|
+
#' @param canvas The rendered canvas
|
|
593
|
+
#' @param scales The scales object
|
|
594
|
+
#' @param width Total width
|
|
595
|
+
#' @param height Total height
|
|
596
|
+
#' @param show_axes Whether to show axes
|
|
597
|
+
#' @param title Optional title
|
|
598
|
+
#' @return Character matrix
|
|
599
|
+
#' @keywords internal
|
|
600
|
+
build_plot_output <- function(canvas, scales, width, height, show_axes, title) {
|
|
601
|
+
# Legacy wrapper - convert to new style
|
|
602
|
+
style_opts <- list(
|
|
603
|
+
show_axes = show_axes,
|
|
604
|
+
title_align = "center",
|
|
605
|
+
legend = "none",
|
|
606
|
+
labels = list(title = title, subtitle = NULL, caption = NULL, x = NULL, y = NULL)
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
left_margin <- if (show_axes) 6 else 0
|
|
610
|
+
top_margin <- if (!is.null(title)) 1 else 0
|
|
611
|
+
|
|
612
|
+
build_plot_output_v2(canvas, scales, width, height, style_opts, left_margin, top_margin, NULL)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
#' Format Axis Label
|
|
617
|
+
#'
|
|
618
|
+
#' @param value Numeric value
|
|
619
|
+
#' @return Formatted string
|
|
620
|
+
#' @keywords internal
|
|
621
|
+
format_axis_label <- function(value) {
|
|
622
|
+
# Handle exact zero
|
|
623
|
+
if (value == 0) {
|
|
624
|
+
return("0")
|
|
625
|
+
}
|
|
626
|
+
# Use scientific notation for very small or very large numbers
|
|
627
|
+
if (abs(value) < 0.01 || abs(value) >= 10000) {
|
|
628
|
+
sprintf("%.1e", value)
|
|
629
|
+
} else if (abs(value) < 1) {
|
|
630
|
+
sprintf("%.2f", value)
|
|
631
|
+
} else if (abs(value) < 100) {
|
|
632
|
+
sprintf("%.1f", value)
|
|
633
|
+
} else {
|
|
634
|
+
sprintf("%.0f", value)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# ============================================================================
|
|
640
|
+
# Faceting Support
|
|
641
|
+
# ============================================================================
|
|
642
|
+
|
|
643
|
+
#' Get Facet Information from Layout
|
|
644
|
+
#'
|
|
645
|
+
#' @param layout The layout object from ggplot_build
|
|
646
|
+
#' @return List with facet info
|
|
647
|
+
#' @keywords internal
|
|
648
|
+
get_facet_info <- function(layout) {
|
|
649
|
+
# Check for faceting
|
|
650
|
+
facet <- layout$facet
|
|
651
|
+
facet_class <- class(facet)[1]
|
|
652
|
+
|
|
653
|
+
# Get panel layout
|
|
654
|
+
panel_layout <- layout$layout
|
|
655
|
+
|
|
656
|
+
if (facet_class == "FacetNull" || is.null(panel_layout) || nrow(panel_layout) <= 1) {
|
|
657
|
+
return(list(
|
|
658
|
+
has_facets = FALSE,
|
|
659
|
+
type = "none",
|
|
660
|
+
n_panels = 1,
|
|
661
|
+
n_rows = 1,
|
|
662
|
+
n_cols = 1,
|
|
663
|
+
layout = NULL,
|
|
664
|
+
facet_vars = NULL
|
|
665
|
+
))
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
# Determine facet type and dimensions
|
|
669
|
+
n_panels <- nrow(panel_layout)
|
|
670
|
+
n_rows <- max(panel_layout$ROW)
|
|
671
|
+
n_cols <- max(panel_layout$COL)
|
|
672
|
+
|
|
673
|
+
# Get facet variable names
|
|
674
|
+
facet_vars <- setdiff(names(panel_layout), c("PANEL", "ROW", "COL", "SCALE_X", "SCALE_Y"))
|
|
675
|
+
|
|
676
|
+
# Get facet labels for each panel
|
|
677
|
+
panel_labels <- lapply(seq_len(n_panels), function(i) {
|
|
678
|
+
row <- panel_layout[i, ]
|
|
679
|
+
labels <- sapply(facet_vars, function(v) as.character(row[[v]]))
|
|
680
|
+
paste(labels, collapse = ", ")
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
list(
|
|
684
|
+
has_facets = TRUE,
|
|
685
|
+
type = facet_class,
|
|
686
|
+
n_panels = n_panels,
|
|
687
|
+
n_rows = n_rows,
|
|
688
|
+
n_cols = n_cols,
|
|
689
|
+
layout = panel_layout,
|
|
690
|
+
facet_vars = facet_vars,
|
|
691
|
+
panel_labels = panel_labels
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
#' Render Faceted Plot
|
|
697
|
+
#'
|
|
698
|
+
#' @param built Result from ggplot_build
|
|
699
|
+
#' @param facet_info Facet information from get_facet_info
|
|
700
|
+
#' @param width Total width
|
|
701
|
+
#' @param height Total height
|
|
702
|
+
#' @param canvas_type Canvas type
|
|
703
|
+
#' @param style_opts Style options
|
|
704
|
+
#' @keywords internal
|
|
705
|
+
render_faceted_plot <- function(built, facet_info, width, height, canvas_type,
|
|
706
|
+
style_opts) {
|
|
707
|
+
n_rows <- facet_info$n_rows
|
|
708
|
+
n_cols <- facet_info$n_cols
|
|
709
|
+
panel_layout <- facet_info$layout
|
|
710
|
+
labels <- style_opts$labels
|
|
711
|
+
show_axes <- style_opts$show_axes
|
|
712
|
+
|
|
713
|
+
# Calculate dimensions for each panel
|
|
714
|
+
# Reserve space for: title, subtitle, facet labels, axes, x label
|
|
715
|
+
top_margin <- 1 # At least 1 for facet labels
|
|
716
|
+
if (!is.null(labels$title)) top_margin <- top_margin + 1
|
|
717
|
+
if (!is.null(labels$subtitle)) top_margin <- top_margin + 1
|
|
718
|
+
bottom_margin <- 2 # For x-axis values
|
|
719
|
+
if (!is.null(labels$x)) bottom_margin <- bottom_margin + 1 # Extra row for x label
|
|
720
|
+
left_margin <- if (show_axes) 7 else 0
|
|
721
|
+
if (!is.null(labels$y) && left_margin > 0) left_margin <- left_margin + 1 # Extra col for y label
|
|
722
|
+
|
|
723
|
+
# Calculate panel dimensions
|
|
724
|
+
panel_width <- floor((width - left_margin) / n_cols)
|
|
725
|
+
panel_height <- floor((height - top_margin - bottom_margin) / n_rows)
|
|
726
|
+
|
|
727
|
+
# Create output matrix
|
|
728
|
+
output <- matrix(" ", nrow = height, ncol = width)
|
|
729
|
+
|
|
730
|
+
# Add title if present
|
|
731
|
+
current_row <- 1
|
|
732
|
+
if (!is.null(labels$title)) {
|
|
733
|
+
title_chars <- strsplit(substr(labels$title, 1, width), "")[[1]]
|
|
734
|
+
if (style_opts$title_align == "center") {
|
|
735
|
+
start_col <- max(1, floor((width - length(title_chars)) / 2))
|
|
736
|
+
} else {
|
|
737
|
+
start_col <- left_margin + 1
|
|
738
|
+
}
|
|
739
|
+
for (i in seq_along(title_chars)) {
|
|
740
|
+
output[current_row, start_col + i - 1] <- title_chars[i]
|
|
741
|
+
}
|
|
742
|
+
current_row <- current_row + 1
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
# Add subtitle if present
|
|
746
|
+
if (!is.null(labels$subtitle)) {
|
|
747
|
+
sub_chars <- strsplit(substr(labels$subtitle, 1, width), "")[[1]]
|
|
748
|
+
if (style_opts$title_align == "center") {
|
|
749
|
+
start_col <- max(1, floor((width - length(sub_chars)) / 2))
|
|
750
|
+
} else {
|
|
751
|
+
start_col <- left_margin + 1
|
|
752
|
+
}
|
|
753
|
+
for (i in seq_along(sub_chars)) {
|
|
754
|
+
output[current_row, start_col + i - 1] <- sub_chars[i]
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
# Render each panel
|
|
759
|
+
for (panel_idx in seq_len(facet_info$n_panels)) {
|
|
760
|
+
panel_row <- panel_layout$ROW[panel_idx]
|
|
761
|
+
panel_col <- panel_layout$COL[panel_idx]
|
|
762
|
+
panel_id <- panel_layout$PANEL[panel_idx]
|
|
763
|
+
|
|
764
|
+
# Calculate panel position in output
|
|
765
|
+
out_row_start <- top_margin + (panel_row - 1) * panel_height
|
|
766
|
+
out_col_start <- left_margin + (panel_col - 1) * panel_width
|
|
767
|
+
|
|
768
|
+
# Create canvas for this panel (slightly smaller for facet label)
|
|
769
|
+
canvas_height <- panel_height - 1 # Reserve 1 row for facet label
|
|
770
|
+
canvas_width <- panel_width - 1 # Small gap between panels
|
|
771
|
+
|
|
772
|
+
if (canvas_height < 3 || canvas_width < 5) {
|
|
773
|
+
warning("Panel too small to render")
|
|
774
|
+
next
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
canvas <- create_canvas(canvas_width, canvas_height, canvas_type)
|
|
778
|
+
|
|
779
|
+
# Create scales for this panel (with border padding if needed, using canvas multipliers)
|
|
780
|
+
panel_params <- built$layout$panel_params[[panel_idx]]
|
|
781
|
+
scales <- create_panel_scales(panel_params, canvas$pixel_width, canvas$pixel_height,
|
|
782
|
+
has_border = style_opts$border,
|
|
783
|
+
x_mult = canvas$x_mult, y_mult = canvas$y_mult)
|
|
784
|
+
|
|
785
|
+
# Draw grid lines first (behind data)
|
|
786
|
+
if (style_opts$grid != "none") {
|
|
787
|
+
draw_grid(canvas, scales, style_opts$grid)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
# Render layers for this panel
|
|
791
|
+
for (layer_idx in seq_along(built$data)) {
|
|
792
|
+
layer_data <- built$data[[layer_idx]]
|
|
793
|
+
layer <- built$plot$layers[[layer_idx]]
|
|
794
|
+
|
|
795
|
+
# Filter data for this panel
|
|
796
|
+
if ("PANEL" %in% names(layer_data)) {
|
|
797
|
+
panel_data <- layer_data[layer_data$PANEL == panel_id, ]
|
|
798
|
+
} else {
|
|
799
|
+
panel_data <- layer_data
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (nrow(panel_data) == 0) next
|
|
803
|
+
|
|
804
|
+
# Get geom handler
|
|
805
|
+
geom_class <- class(layer$geom)[1]
|
|
806
|
+
handler <- get_geom_handler(geom_class)
|
|
807
|
+
|
|
808
|
+
if (is.null(handler)) next
|
|
809
|
+
|
|
810
|
+
# Render (pass style_opts for geoms that need it, like boxplot)
|
|
811
|
+
tryCatch({
|
|
812
|
+
handler(panel_data, canvas, scales, layer$aes_params, style_opts)
|
|
813
|
+
}, error = function(e) {
|
|
814
|
+
warning(sprintf("Error rendering %s in panel %d: %s",
|
|
815
|
+
geom_class, panel_idx, e$message))
|
|
816
|
+
})
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
# Draw border if requested
|
|
820
|
+
if (style_opts$border) {
|
|
821
|
+
draw_border(canvas)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
# Get rendered canvas
|
|
825
|
+
rendered <- canvas$render()
|
|
826
|
+
|
|
827
|
+
# Add facet label
|
|
828
|
+
label <- facet_info$panel_labels[[panel_idx]]
|
|
829
|
+
label <- substr(label, 1, canvas_width) # Truncate if needed
|
|
830
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
831
|
+
label_start <- out_col_start + max(0, floor((canvas_width - length(label_chars)) / 2))
|
|
832
|
+
for (i in seq_along(label_chars)) {
|
|
833
|
+
if (label_start + i - 1 <= width) {
|
|
834
|
+
output[out_row_start, label_start + i - 1] <- label_chars[i]
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
# Copy canvas to output
|
|
839
|
+
for (i in seq_len(nrow(rendered))) {
|
|
840
|
+
for (j in seq_len(ncol(rendered))) {
|
|
841
|
+
out_r <- out_row_start + i
|
|
842
|
+
out_c <- out_col_start + j - 1
|
|
843
|
+
if (out_r <= height && out_c <= width && out_c >= 1) {
|
|
844
|
+
output[out_r, out_c] <- rendered[i, j]
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
# Add Y axis for leftmost panels
|
|
850
|
+
if (panel_col == 1 && show_axes) {
|
|
851
|
+
# Check if we have discrete y labels
|
|
852
|
+
if (!is.null(scales$y_labels) && length(scales$y_labels) > 0) {
|
|
853
|
+
# Use discrete labels for y-axis
|
|
854
|
+
for (i in seq_along(scales$y_labels)) {
|
|
855
|
+
pos <- scales$y_label_positions[i]
|
|
856
|
+
y_frac <- (pos - scales$y_range[1]) / (scales$y_range[2] - scales$y_range[1])
|
|
857
|
+
row <- out_row_start + 1 + round((1 - y_frac) * (canvas_height - 1))
|
|
858
|
+
|
|
859
|
+
if (row >= 1 && row <= height) {
|
|
860
|
+
label <- scales$y_labels[i]
|
|
861
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
862
|
+
start_col <- max(1, left_margin - length(label_chars))
|
|
863
|
+
for (j in seq_along(label_chars)) {
|
|
864
|
+
if (start_col + j - 1 <= left_margin && start_col + j - 1 >= 1) {
|
|
865
|
+
output[row, start_col + j - 1] <- label_chars[j]
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
# Use numeric ticks for y-axis
|
|
872
|
+
y_ticks <- pretty(scales$y_range, n = 3)
|
|
873
|
+
y_ticks <- y_ticks[y_ticks >= scales$y_range[1] & y_ticks <= scales$y_range[2]]
|
|
874
|
+
|
|
875
|
+
for (tick in y_ticks) {
|
|
876
|
+
y_frac <- (tick - scales$y_range[1]) / (scales$y_range[2] - scales$y_range[1])
|
|
877
|
+
row <- out_row_start + 1 + round((1 - y_frac) * (canvas_height - 1))
|
|
878
|
+
|
|
879
|
+
if (row >= 1 && row <= height) {
|
|
880
|
+
label <- format_axis_label(tick)
|
|
881
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
882
|
+
start_col <- max(1, left_margin - length(label_chars))
|
|
883
|
+
for (j in seq_along(label_chars)) {
|
|
884
|
+
if (start_col + j - 1 <= left_margin && start_col + j - 1 >= 1) {
|
|
885
|
+
output[row, start_col + j - 1] <- label_chars[j]
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
# Add X axis for bottom panels
|
|
894
|
+
if (panel_row == n_rows && show_axes) {
|
|
895
|
+
x_row <- out_row_start + canvas_height + 1
|
|
896
|
+
if (x_row <= height) {
|
|
897
|
+
# Check if we have discrete labels
|
|
898
|
+
if (!is.null(scales$x_labels) && length(scales$x_labels) > 0) {
|
|
899
|
+
# Use discrete labels
|
|
900
|
+
for (i in seq_along(scales$x_labels)) {
|
|
901
|
+
pos <- scales$x_label_positions[i]
|
|
902
|
+
x_frac <- (pos - scales$x_range[1]) / (scales$x_range[2] - scales$x_range[1])
|
|
903
|
+
col <- out_col_start + round(x_frac * (canvas_width - 1))
|
|
904
|
+
|
|
905
|
+
if (col >= 1 && col <= width) {
|
|
906
|
+
label <- scales$x_labels[i]
|
|
907
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
908
|
+
start_col <- col - floor(length(label_chars) / 2)
|
|
909
|
+
for (j in seq_along(label_chars)) {
|
|
910
|
+
if (start_col + j - 1 >= 1 && start_col + j - 1 <= width) {
|
|
911
|
+
output[x_row, start_col + j - 1] <- label_chars[j]
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
# Use numeric ticks
|
|
918
|
+
x_ticks <- pretty(scales$x_range, n = 3)
|
|
919
|
+
x_ticks <- x_ticks[x_ticks >= scales$x_range[1] & x_ticks <= scales$x_range[2]]
|
|
920
|
+
|
|
921
|
+
for (tick in x_ticks) {
|
|
922
|
+
x_frac <- (tick - scales$x_range[1]) / (scales$x_range[2] - scales$x_range[1])
|
|
923
|
+
col <- out_col_start + round(x_frac * (canvas_width - 1))
|
|
924
|
+
|
|
925
|
+
if (col >= 1 && col <= width) {
|
|
926
|
+
label <- format_axis_label(tick)
|
|
927
|
+
label_chars <- strsplit(label, "")[[1]]
|
|
928
|
+
start_col <- col - floor(length(label_chars) / 2)
|
|
929
|
+
for (j in seq_along(label_chars)) {
|
|
930
|
+
if (start_col + j - 1 >= 1 && start_col + j - 1 <= width) {
|
|
931
|
+
output[x_row, start_col + j - 1] <- label_chars[j]
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
# Add Y axis label (vertically on the left)
|
|
942
|
+
if (!is.null(labels$y) && left_margin >= 2) {
|
|
943
|
+
y_label <- labels$y
|
|
944
|
+
y_chars <- strsplit(substr(y_label, 1, min(nchar(y_label), panel_height * n_rows)), "")[[1]]
|
|
945
|
+
label_start <- top_margin + floor((panel_height * n_rows - length(y_chars)) / 2)
|
|
946
|
+
for (i in seq_along(y_chars)) {
|
|
947
|
+
row <- label_start + i
|
|
948
|
+
if (row >= 1 && row <= height) {
|
|
949
|
+
output[row, 1] <- y_chars[i]
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
# Add X axis label (centered at bottom)
|
|
955
|
+
if (!is.null(labels$x)) {
|
|
956
|
+
x_row <- top_margin + panel_height * n_rows + 2
|
|
957
|
+
if (x_row <= height) {
|
|
958
|
+
x_label <- substr(labels$x, 1, width - left_margin)
|
|
959
|
+
x_chars <- strsplit(x_label, "")[[1]]
|
|
960
|
+
# Center across all panels
|
|
961
|
+
plot_area_width <- panel_width * n_cols
|
|
962
|
+
start_col <- left_margin + floor((plot_area_width - length(x_chars)) / 2)
|
|
963
|
+
for (i in seq_along(x_chars)) {
|
|
964
|
+
if (start_col + i - 1 >= 1 && start_col + i - 1 <= width) {
|
|
965
|
+
output[x_row, start_col + i - 1] <- x_chars[i]
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# Add legend if present
|
|
972
|
+
legend_info <- extract_legend_info(built)
|
|
973
|
+
if (!is.null(legend_info) && length(legend_info) > 0 &&
|
|
974
|
+
!identical(style_opts$legend, "none")) {
|
|
975
|
+
# Calculate plot height for legend centering
|
|
976
|
+
plot_height <- panel_height * n_rows
|
|
977
|
+
output <- add_legend_to_output(output, legend_info, style_opts$legend,
|
|
978
|
+
top_margin, plot_height)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
# Print
|
|
982
|
+
cat("\n")
|
|
983
|
+
for (i in seq_len(nrow(output))) {
|
|
984
|
+
cat(paste(output[i, ], collapse = ""), "\n")
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
invisible(NULL)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
#' Create Scales for a Single Panel
|
|
992
|
+
#'
|
|
993
|
+
#' @param panel_params Panel parameters from ggplot_build
|
|
994
|
+
#' @param plot_width Pixel width
|
|
995
|
+
#' @param plot_height Pixel height
|
|
996
|
+
#' @param has_border Whether a border will be drawn (adds padding)
|
|
997
|
+
#' @return List with scale functions
|
|
998
|
+
#' @keywords internal
|
|
999
|
+
create_panel_scales <- function(panel_params, plot_width, plot_height, has_border = FALSE,
|
|
1000
|
+
x_mult = 1, y_mult = 1) {
|
|
1001
|
+
|
|
1002
|
+
# Get x and y ranges from panel params
|
|
1003
|
+
x_range <- panel_params$x.range
|
|
1004
|
+
y_range <- panel_params$y.range
|
|
1005
|
+
|
|
1006
|
+
# Fallback if ranges not available
|
|
1007
|
+
if (is.null(x_range)) x_range <- c(0, 1)
|
|
1008
|
+
if (is.null(y_range)) y_range <- c(0, 1)
|
|
1009
|
+
|
|
1010
|
+
# Add padding if border is present to prevent data from overlapping border
|
|
1011
|
+
# Padding must be at least the canvas multiplier to ensure data stays in
|
|
1012
|
+
# a different character cell than the border (e.g., braille is 2x4 per char)
|
|
1013
|
+
if (has_border) {
|
|
1014
|
+
x_padding <- x_mult + 1 # Move past border character cell
|
|
1015
|
+
y_padding <- y_mult + 1
|
|
1016
|
+
} else {
|
|
1017
|
+
x_padding <- 0
|
|
1018
|
+
y_padding <- 0
|
|
1019
|
+
}
|
|
1020
|
+
x_min <- 1 + x_padding
|
|
1021
|
+
x_max <- plot_width - x_padding
|
|
1022
|
+
y_min <- 1 + y_padding
|
|
1023
|
+
y_max <- plot_height - y_padding
|
|
1024
|
+
|
|
1025
|
+
# Ensure we have valid ranges
|
|
1026
|
+
if (x_max <= x_min) {
|
|
1027
|
+
x_min <- 1
|
|
1028
|
+
x_max <- plot_width
|
|
1029
|
+
}
|
|
1030
|
+
if (y_max <= y_min) {
|
|
1031
|
+
y_min <- 1
|
|
1032
|
+
y_max <- plot_height
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
# Create scaling functions
|
|
1036
|
+
x_scale <- function(x) {
|
|
1037
|
+
((x - x_range[1]) / (x_range[2] - x_range[1])) * (x_max - x_min) + x_min
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
y_scale <- function(y) {
|
|
1041
|
+
y_max - ((y - y_range[1]) / (y_range[2] - y_range[1])) * (y_max - y_min)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
# Check for discrete x-axis labels (only use labels for discrete scales)
|
|
1045
|
+
x_labels <- NULL
|
|
1046
|
+
x_label_positions <- NULL
|
|
1047
|
+
x_is_discrete <- !is.null(panel_params$x) &&
|
|
1048
|
+
!is.null(panel_params$x$is_discrete) &&
|
|
1049
|
+
tryCatch(panel_params$x$is_discrete(), error = function(e) FALSE)
|
|
1050
|
+
|
|
1051
|
+
if (x_is_discrete && !is.null(panel_params$x$breaks)) {
|
|
1052
|
+
x_labels <- panel_params$x$get_labels()
|
|
1053
|
+
x_label_positions <- attr(panel_params$x$breaks, "pos")
|
|
1054
|
+
if (is.null(x_label_positions)) {
|
|
1055
|
+
x_label_positions <- seq_along(x_labels)
|
|
1056
|
+
}
|
|
1057
|
+
# Filter out NA values from labels and positions
|
|
1058
|
+
if (!is.null(x_labels) && length(x_labels) > 0) {
|
|
1059
|
+
valid <- !is.na(x_labels)
|
|
1060
|
+
x_labels <- x_labels[valid]
|
|
1061
|
+
x_label_positions <- x_label_positions[valid]
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
# Check for discrete y-axis labels (only use labels for discrete scales)
|
|
1066
|
+
y_labels <- NULL
|
|
1067
|
+
y_label_positions <- NULL
|
|
1068
|
+
y_is_discrete <- !is.null(panel_params$y) &&
|
|
1069
|
+
!is.null(panel_params$y$is_discrete) &&
|
|
1070
|
+
tryCatch(panel_params$y$is_discrete(), error = function(e) FALSE)
|
|
1071
|
+
|
|
1072
|
+
if (y_is_discrete && !is.null(panel_params$y$breaks)) {
|
|
1073
|
+
y_labels <- panel_params$y$get_labels()
|
|
1074
|
+
y_label_positions <- attr(panel_params$y$breaks, "pos")
|
|
1075
|
+
if (is.null(y_label_positions)) {
|
|
1076
|
+
y_label_positions <- seq_along(y_labels)
|
|
1077
|
+
}
|
|
1078
|
+
# Filter out NA values from labels and positions
|
|
1079
|
+
if (!is.null(y_labels) && length(y_labels) > 0) {
|
|
1080
|
+
valid <- !is.na(y_labels)
|
|
1081
|
+
y_labels <- y_labels[valid]
|
|
1082
|
+
y_label_positions <- y_label_positions[valid]
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
list(
|
|
1087
|
+
x = x_scale,
|
|
1088
|
+
y = y_scale,
|
|
1089
|
+
x_range = x_range,
|
|
1090
|
+
y_range = y_range,
|
|
1091
|
+
width = plot_width,
|
|
1092
|
+
height = plot_height,
|
|
1093
|
+
x_labels = x_labels,
|
|
1094
|
+
x_label_positions = x_label_positions,
|
|
1095
|
+
y_labels = y_labels,
|
|
1096
|
+
y_label_positions = y_label_positions
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
|