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.
Files changed (143) hide show
  1. CLAUDE.md +51 -0
  2. LICENSE +21 -0
  3. PKG-INFO +358 -0
  4. README.md +340 -0
  5. main.py +6 -0
  6. plotcli-original/.Rbuildignore +18 -0
  7. plotcli-original/.github/workflows/deploy_docs.yml +43 -0
  8. plotcli-original/.gitignore +46 -0
  9. plotcli-original/DESCRIPTION +25 -0
  10. plotcli-original/NAMESPACE +60 -0
  11. plotcli-original/NEWS.md +112 -0
  12. plotcli-original/R/ascii_escape.r +13 -0
  13. plotcli-original/R/canvas.r +586 -0
  14. plotcli-original/R/class_functions.r +114 -0
  15. plotcli-original/R/geom_registry.r +1376 -0
  16. plotcli-original/R/ggplotcli.r +234 -0
  17. plotcli-original/R/ggplotcli_helpers.r +1099 -0
  18. plotcli-original/R/helper_functions.r +351 -0
  19. plotcli-original/R/plotcli.r +963 -0
  20. plotcli-original/R/plotcli_grid.r +1 -0
  21. plotcli-original/R/plotcli_wrappers.r +416 -0
  22. plotcli-original/R/zzz.r +15 -0
  23. plotcli-original/README.md +192 -0
  24. plotcli-original/docs/ascii.png +0 -0
  25. plotcli-original/docs/bar.png +0 -0
  26. plotcli-original/docs/block.png +0 -0
  27. plotcli-original/docs/boxplot.png +0 -0
  28. plotcli-original/docs/density.png +0 -0
  29. plotcli-original/docs/facet.png +0 -0
  30. plotcli-original/docs/facet_grid.png +0 -0
  31. plotcli-original/docs/generate_png.sh +137 -0
  32. plotcli-original/docs/heatmap.png +0 -0
  33. plotcli-original/docs/histogram.png +0 -0
  34. plotcli-original/docs/line.png +0 -0
  35. plotcli-original/docs/noborder.png +0 -0
  36. plotcli-original/docs/scatter.png +0 -0
  37. plotcli-original/docs/showcase.R +182 -0
  38. plotcli-original/inst/doc/ggplotcli.R +231 -0
  39. plotcli-original/inst/doc/ggplotcli.Rmd +329 -0
  40. plotcli-original/inst/doc/ggplotcli.html +1078 -0
  41. plotcli-original/inst/doc/plotcli_class.R +98 -0
  42. plotcli-original/inst/doc/plotcli_class.Rmd +121 -0
  43. plotcli-original/inst/doc/plotcli_class.html +564 -0
  44. plotcli-original/inst/doc/plotcli_wrappers.R +35 -0
  45. plotcli-original/inst/doc/plotcli_wrappers.Rmd +62 -0
  46. plotcli-original/inst/doc/plotcli_wrappers.html +546 -0
  47. plotcli-original/man/AsciiCanvas.Rd +116 -0
  48. plotcli-original/man/BlockCanvas.Rd +132 -0
  49. plotcli-original/man/BrailleCanvas.Rd +146 -0
  50. plotcli-original/man/Canvas.Rd +492 -0
  51. plotcli-original/man/GeomRegistry.Rd +9 -0
  52. plotcli-original/man/add_legend_to_output.Rd +12 -0
  53. plotcli-original/man/braille_dot_bit.Rd +29 -0
  54. plotcli-original/man/braille_set_dot.Rd +21 -0
  55. plotcli-original/man/bresenham.Rd +27 -0
  56. plotcli-original/man/build_plot_output.Rd +28 -0
  57. plotcli-original/man/build_plot_output_v2.Rd +41 -0
  58. plotcli-original/man/cat_plot_matrix.Rd +17 -0
  59. plotcli-original/man/cbind.plotcli.Rd +19 -0
  60. plotcli-original/man/cbind_plots.Rd +17 -0
  61. plotcli-original/man/color_to_term.Rd +18 -0
  62. plotcli-original/man/create_canvas.Rd +21 -0
  63. plotcli-original/man/create_panel_scales.Rd +29 -0
  64. plotcli-original/man/create_scales.Rd +27 -0
  65. plotcli-original/man/dot-geom_registry.Rd +16 -0
  66. plotcli-original/man/draw_border.Rd +12 -0
  67. plotcli-original/man/draw_grid.Rd +12 -0
  68. plotcli-original/man/extract_legend_info.Rd +12 -0
  69. plotcli-original/man/extract_plot_labels.Rd +12 -0
  70. plotcli-original/man/extract_plot_style.Rd +12 -0
  71. plotcli-original/man/format_axis_label.Rd +18 -0
  72. plotcli-original/man/format_four_chars.Rd +21 -0
  73. plotcli-original/man/geom_area_handler.Rd +12 -0
  74. plotcli-original/man/geom_bar_handler.Rd +12 -0
  75. plotcli-original/man/geom_boxplot_handler.Rd +13 -0
  76. plotcli-original/man/geom_density_handler.Rd +12 -0
  77. plotcli-original/man/geom_histogram_handler.Rd +12 -0
  78. plotcli-original/man/geom_hline_handler.Rd +12 -0
  79. plotcli-original/man/geom_line_handler.Rd +12 -0
  80. plotcli-original/man/geom_path_handler.Rd +12 -0
  81. plotcli-original/man/geom_point_handler.Rd +12 -0
  82. plotcli-original/man/geom_rect_handler.Rd +12 -0
  83. plotcli-original/man/geom_segment_handler.Rd +12 -0
  84. plotcli-original/man/geom_smooth_handler.Rd +12 -0
  85. plotcli-original/man/geom_text_handler.Rd +12 -0
  86. plotcli-original/man/geom_vline_handler.Rd +12 -0
  87. plotcli-original/man/get_color_hue.Rd +18 -0
  88. plotcli-original/man/get_data_subset.Rd +23 -0
  89. plotcli-original/man/get_facet_info.Rd +18 -0
  90. plotcli-original/man/get_geom_handler.Rd +17 -0
  91. plotcli-original/man/get_term_colors.Rd +21 -0
  92. plotcli-original/man/ggplotcli.Rd +83 -0
  93. plotcli-original/man/init_color_mapping.Rd +15 -0
  94. plotcli-original/man/is_braille.Rd +20 -0
  95. plotcli-original/man/is_geom_registered.Rd +17 -0
  96. plotcli-original/man/list_registered_geoms.Rd +14 -0
  97. plotcli-original/man/make_colored.Rd +23 -0
  98. plotcli-original/man/make_unique_names.Rd +20 -0
  99. plotcli-original/man/normalize_data.Rd +27 -0
  100. plotcli-original/man/pclib.Rd +48 -0
  101. plotcli-original/man/pclibx.Rd +46 -0
  102. plotcli-original/man/pclid.Rd +44 -0
  103. plotcli-original/man/pclih.Rd +50 -0
  104. plotcli-original/man/pclil.Rd +48 -0
  105. plotcli-original/man/pclis.Rd +48 -0
  106. plotcli-original/man/pixel_to_braille.Rd +23 -0
  107. plotcli-original/man/plotcli.Rd +598 -0
  108. plotcli-original/man/plotcli_bar.Rd +48 -0
  109. plotcli-original/man/plotcli_box.Rd +46 -0
  110. plotcli-original/man/plotcli_density.Rd +44 -0
  111. plotcli-original/man/plotcli_histogram.Rd +50 -0
  112. plotcli-original/man/plotcli_line.Rd +48 -0
  113. plotcli-original/man/plotcli_options.Rd +18 -0
  114. plotcli-original/man/plotcli_scatter.Rd +48 -0
  115. plotcli-original/man/plus-.plotcli.Rd +19 -0
  116. plotcli-original/man/rbind.plotcli.Rd +19 -0
  117. plotcli-original/man/rbind_plots.Rd +17 -0
  118. plotcli-original/man/register_geom.Rd +21 -0
  119. plotcli-original/man/remove_color_codes.Rd +21 -0
  120. plotcli-original/man/render_faceted_plot.Rd +25 -0
  121. plotcli-original/man/render_single_panel.Rd +12 -0
  122. plotcli-original/man/safe_aes_name.Rd +18 -0
  123. plotcli-original/tests/testthat/test-new-geoms.R +136 -0
  124. plotcli-original/tests/testthat/test-plotcli.R +69 -0
  125. plotcli-original/tests/testthat.R +4 -0
  126. plotcli-original/vignettes/ggplotcli.Rmd +329 -0
  127. plotcli-original/vignettes/plotcli_class.R +98 -0
  128. plotcli-original/vignettes/plotcli_class.Rmd +121 -0
  129. plotcli-original/vignettes/plotcli_wrappers.R +35 -0
  130. plotcli-original/vignettes/plotcli_wrappers.Rmd +62 -0
  131. plotcli.egg-info/PKG-INFO +11 -0
  132. plotcli.egg-info/SOURCES.txt +7 -0
  133. plotcli.egg-info/dependency_links.txt +1 -0
  134. plotcli.egg-info/entry_points.txt +3 -0
  135. plotcli.egg-info/top_level.txt +1 -0
  136. plotcli.py +978 -0
  137. plotcli_py-0.1.0.dist-info/METADATA +358 -0
  138. plotcli_py-0.1.0.dist-info/RECORD +143 -0
  139. plotcli_py-0.1.0.dist-info/WHEEL +4 -0
  140. plotcli_py-0.1.0.dist-info/entry_points.txt +2 -0
  141. plotcli_py-0.1.0.dist-info/licenses/LICENSE +21 -0
  142. pyproject.toml +31 -0
  143. 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
+