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,1376 @@
1
+ #' Geom Registry and Dispatch System
2
+ #'
3
+ #' This module provides a registry for geom rendering functions and
4
+ #' a dispatch system for converting ggplot2 geoms to terminal plots.
5
+ #'
6
+ #' @name GeomRegistry
7
+ #' @importFrom grDevices col2rgb
8
+ #' @importFrom stats density
9
+ #' @import R6
10
+ #' @import ggplot2
11
+ NULL
12
+
13
+ #' Geom Registry Environment
14
+ #'
15
+ #' Internal environment storing registered geom handlers
16
+ #' @keywords internal
17
+ .geom_registry <- new.env(parent = emptyenv())
18
+
19
+ #' Register a Geom Handler
20
+ #'
21
+ #' Register a function that can render a specific ggplot2 geom to a canvas.
22
+ #'
23
+ #' @param geom_name Name of the geom (e.g., "GeomPoint", "GeomLine")
24
+ #' @param handler Function that takes (data, canvas, scales, params) and draws to canvas
25
+ #' @export
26
+ #'
27
+ #' @examples
28
+ #' register_geom("GeomPoint", function(data, canvas, scales, params) {
29
+ #' # Draw points on canvas
30
+ #' })
31
+ register_geom <- function(geom_name, handler) {
32
+ if (!is.function(handler)) {
33
+ stop("handler must be a function")
34
+ }
35
+ .geom_registry[[geom_name]] <- handler
36
+ invisible(NULL)
37
+ }
38
+
39
+ #' Get a Geom Handler
40
+ #'
41
+ #' Retrieve the registered handler for a geom, or NULL if not found.
42
+ #'
43
+ #' @param geom_name Name of the geom
44
+ #' @return The handler function or NULL
45
+ #' @export
46
+ get_geom_handler <- function(geom_name) {
47
+ if (exists(geom_name, envir = .geom_registry)) {
48
+ return(.geom_registry[[geom_name]])
49
+ }
50
+ return(NULL)
51
+ }
52
+
53
+ #' List Registered Geoms
54
+ #'
55
+ #' @return Character vector of registered geom names
56
+ #' @export
57
+ list_registered_geoms <- function() {
58
+ ls(envir = .geom_registry)
59
+ }
60
+
61
+ #' Check if a Geom is Registered
62
+ #'
63
+ #' @param geom_name Name of the geom
64
+ #' @return Logical
65
+ #' @export
66
+ is_geom_registered <- function(geom_name) {
67
+ exists(geom_name, envir = .geom_registry)
68
+ }
69
+
70
+
71
+ # ============================================================================
72
+ # Scale Helpers for Geom Handlers
73
+ # ============================================================================
74
+
75
+ #' Create Scale Object from ggplot_build data
76
+ #'
77
+ #' @param built Result from ggplot_build()
78
+ #' @param plot_width Canvas pixel width
79
+ #' @param plot_height Canvas pixel height
80
+ #' @param has_border Whether a border will be drawn (adds padding)
81
+ #' @return List with x_scale and y_scale functions
82
+ #' @export
83
+ create_scales <- function(built, plot_width, plot_height, has_border = FALSE,
84
+ x_mult = 1, y_mult = 1) {
85
+ # Get the panel parameters (contains scale ranges)
86
+ layout <- built$layout
87
+ panel_params <- layout$panel_params[[1]]
88
+
89
+ # X scale
90
+ x_range <- panel_params$x.range
91
+ if (is.null(x_range)) {
92
+ x_range <- range(built$data[[1]]$x, na.rm = TRUE)
93
+ }
94
+
95
+ # Y scale
96
+ y_range <- panel_params$y.range
97
+ if (is.null(y_range)) {
98
+ y_range <- range(built$data[[1]]$y, na.rm = TRUE)
99
+ }
100
+
101
+ # Add padding if border is present to prevent data from overlapping border
102
+ # Padding must be at least the canvas multiplier to ensure data stays in
103
+ # a different character cell than the border (e.g., braille is 2x4 per char)
104
+ if (has_border) {
105
+ x_padding <- x_mult + 1 # Move past border character cell
106
+ y_padding <- y_mult + 1
107
+ } else {
108
+ x_padding <- 0
109
+ y_padding <- 0
110
+ }
111
+ x_min <- 1 + x_padding
112
+ x_max <- plot_width - x_padding
113
+ y_min <- 1 + y_padding
114
+ y_max <- plot_height - y_padding
115
+
116
+ # Ensure we have valid ranges
117
+ if (x_max <= x_min) {
118
+ x_min <- 1
119
+ x_max <- plot_width
120
+ }
121
+ if (y_max <= y_min) {
122
+ y_min <- 1
123
+ y_max <- plot_height
124
+ }
125
+
126
+ # Create scaling functions
127
+ x_scale <- function(x) {
128
+ ((x - x_range[1]) / (x_range[2] - x_range[1])) * (x_max - x_min) + x_min
129
+ }
130
+
131
+ y_scale <- function(y) {
132
+ # Invert Y because canvas has origin at top-left
133
+ y_max - ((y - y_range[1]) / (y_range[2] - y_range[1])) * (y_max - y_min)
134
+ }
135
+
136
+ # Check for discrete x-axis labels (only use for discrete scales)
137
+ x_labels <- NULL
138
+ x_label_positions <- NULL
139
+ x_is_discrete <- !is.null(panel_params$x) &&
140
+ !is.null(panel_params$x$is_discrete) &&
141
+ tryCatch(panel_params$x$is_discrete(), error = function(e) FALSE)
142
+
143
+ if (x_is_discrete && !is.null(panel_params$x$breaks)) {
144
+ x_labels <- panel_params$x$get_labels()
145
+ x_label_positions <- attr(panel_params$x$breaks, "pos")
146
+ if (is.null(x_label_positions)) {
147
+ x_label_positions <- seq_along(x_labels)
148
+ }
149
+ # Filter out NA values
150
+ if (!is.null(x_labels) && length(x_labels) > 0) {
151
+ valid <- !is.na(x_labels)
152
+ x_labels <- x_labels[valid]
153
+ x_label_positions <- x_label_positions[valid]
154
+ }
155
+ }
156
+
157
+ # Check for discrete y-axis labels (only use for discrete scales)
158
+ y_labels <- NULL
159
+ y_label_positions <- NULL
160
+ y_is_discrete <- !is.null(panel_params$y) &&
161
+ !is.null(panel_params$y$is_discrete) &&
162
+ tryCatch(panel_params$y$is_discrete(), error = function(e) FALSE)
163
+
164
+ if (y_is_discrete && !is.null(panel_params$y$breaks)) {
165
+ y_labels <- panel_params$y$get_labels()
166
+ y_label_positions <- attr(panel_params$y$breaks, "pos")
167
+ if (is.null(y_label_positions)) {
168
+ y_label_positions <- seq_along(y_labels)
169
+ }
170
+ # Filter out NA values
171
+ if (!is.null(y_labels) && length(y_labels) > 0) {
172
+ valid <- !is.na(y_labels)
173
+ y_labels <- y_labels[valid]
174
+ y_label_positions <- y_label_positions[valid]
175
+ }
176
+ }
177
+
178
+ list(
179
+ x = x_scale,
180
+ y = y_scale,
181
+ x_range = x_range,
182
+ y_range = y_range,
183
+ width = plot_width,
184
+ height = plot_height,
185
+ x_labels = x_labels,
186
+ x_label_positions = x_label_positions,
187
+ y_labels = y_labels,
188
+ y_label_positions = y_label_positions
189
+ )
190
+ }
191
+
192
+
193
+ # ============================================================================
194
+ # Built-in Geom Handlers
195
+ # ============================================================================
196
+
197
+ #' GeomPoint Handler
198
+ #'
199
+ #' Renders points as individual pixels or small shapes
200
+ #' @keywords internal
201
+ geom_point_handler <- function(data, canvas, scales, params, style_opts = NULL) {
202
+ # Get color mapping
203
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
204
+
205
+ for (i in seq_len(nrow(data))) {
206
+ x <- scales$x(data$x[i])
207
+ y <- scales$y(data$y[i])
208
+
209
+ # Get color for this point
210
+ color <- colors[i]
211
+ if (!is.null(color) && !is.na(color)) {
212
+ color <- color_to_term(color)
213
+ } else {
214
+ color <- NULL
215
+ }
216
+
217
+ # Draw point (could add size support later)
218
+ canvas$set_pixel(round(x), round(y), color)
219
+ }
220
+ }
221
+
222
+ #' GeomLine Handler
223
+ #'
224
+ #' Renders connected lines
225
+ #' @keywords internal
226
+ geom_line_handler <- function(data, canvas, scales, params, style_opts = NULL) {
227
+ # Sort by x to ensure proper line connections
228
+ data <- data[order(data$x), ]
229
+
230
+ # Group by colour/group if present
231
+ if ("group" %in% names(data)) {
232
+ groups <- unique(data$group)
233
+ } else {
234
+ groups <- 1
235
+ data$group <- 1
236
+ }
237
+
238
+ for (grp in groups) {
239
+ grp_data <- data[data$group == grp, ]
240
+ if (nrow(grp_data) < 2) next
241
+
242
+ # Get color
243
+ color <- if ("colour" %in% names(grp_data)) {
244
+ color_to_term(grp_data$colour[1])
245
+ } else {
246
+ NULL
247
+ }
248
+
249
+ # Scale coordinates
250
+ xs <- sapply(grp_data$x, scales$x)
251
+ ys <- sapply(grp_data$y, scales$y)
252
+
253
+ # Draw polyline
254
+ canvas$draw_polyline(xs, ys, color)
255
+ }
256
+ }
257
+
258
+ #' GeomPath Handler
259
+ #'
260
+ #' Renders connected paths (order by data, not x)
261
+ #' @keywords internal
262
+ geom_path_handler <- function(data, canvas, scales, params, style_opts = NULL) {
263
+ # Group by colour/group if present
264
+ if ("group" %in% names(data)) {
265
+ groups <- unique(data$group)
266
+ } else {
267
+ groups <- 1
268
+ data$group <- 1
269
+ }
270
+
271
+ for (grp in groups) {
272
+ grp_data <- data[data$group == grp, ]
273
+ if (nrow(grp_data) < 2) next
274
+
275
+ # Get color
276
+ color <- if ("colour" %in% names(grp_data)) {
277
+ color_to_term(grp_data$colour[1])
278
+ } else {
279
+ NULL
280
+ }
281
+
282
+ # Scale coordinates (keep original order)
283
+ xs <- sapply(grp_data$x, scales$x)
284
+ ys <- sapply(grp_data$y, scales$y)
285
+
286
+ # Draw polyline
287
+ canvas$draw_polyline(xs, ys, color)
288
+ }
289
+ }
290
+
291
+ #' GeomBar/GeomCol Handler
292
+ #'
293
+ #' Renders bar charts
294
+ #' @keywords internal
295
+ geom_bar_handler <- function(data, canvas, scales, params, style_opts = NULL) {
296
+ # Get colors
297
+ colors <- if ("fill" %in% names(data)) data$fill else rep("white", nrow(data))
298
+
299
+ for (i in seq_len(nrow(data))) {
300
+ # Bar coordinates
301
+ xmin <- scales$x(data$xmin[i])
302
+ xmax <- scales$x(data$xmax[i])
303
+ ymin <- scales$y(data$ymin[i])
304
+ ymax <- scales$y(data$ymax[i])
305
+
306
+ # Get color
307
+ color <- colors[i]
308
+ if (!is.null(color) && !is.na(color)) {
309
+ color <- color_to_term(color)
310
+ } else {
311
+ color <- NULL
312
+ }
313
+
314
+ # Fill rectangle (note: y is inverted)
315
+ canvas$fill_rect(round(xmin), round(ymax), round(xmax), round(ymin), color)
316
+ }
317
+ }
318
+
319
+ #' GeomArea Handler
320
+ #'
321
+ #' Renders filled areas
322
+ #' @keywords internal
323
+ geom_area_handler <- function(data, canvas, scales, params, style_opts = NULL) {
324
+ # Sort by x
325
+ data <- data[order(data$x), ]
326
+
327
+ # Get color
328
+ color <- if ("fill" %in% names(data)) {
329
+ color_to_term(data$fill[1])
330
+ } else {
331
+ NULL
332
+ }
333
+
334
+ # Scale coordinates
335
+ xs <- sapply(data$x, scales$x)
336
+ ys <- sapply(data$y, scales$y)
337
+
338
+ # Fill area
339
+ canvas$fill_area(xs, ys, color)
340
+ }
341
+
342
+ #' GeomSegment Handler
343
+ #'
344
+ #' Renders line segments
345
+ #' @keywords internal
346
+ geom_segment_handler <- function(data, canvas, scales, params, style_opts = NULL) {
347
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
348
+
349
+ for (i in seq_len(nrow(data))) {
350
+ x0 <- scales$x(data$x[i])
351
+ y0 <- scales$y(data$y[i])
352
+ x1 <- scales$x(data$xend[i])
353
+ y1 <- scales$y(data$yend[i])
354
+
355
+ color <- colors[i]
356
+ if (!is.null(color) && !is.na(color)) {
357
+ color <- color_to_term(color)
358
+ } else {
359
+ color <- NULL
360
+ }
361
+
362
+ canvas$draw_segment(x0, y0, x1, y1, arrow_end = FALSE, color = color)
363
+ }
364
+ }
365
+
366
+ #' GeomHline Handler
367
+ #'
368
+ #' Renders horizontal lines
369
+ #' @keywords internal
370
+ geom_hline_handler <- function(data, canvas, scales, params, style_opts = NULL) {
371
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
372
+
373
+ for (i in seq_len(nrow(data))) {
374
+ y <- scales$y(data$yintercept[i])
375
+
376
+ color <- colors[i]
377
+ if (!is.null(color) && !is.na(color)) {
378
+ color <- color_to_term(color)
379
+ } else {
380
+ color <- NULL
381
+ }
382
+
383
+ canvas$draw_hline(round(y), color = color)
384
+ }
385
+ }
386
+
387
+ #' GeomVline Handler
388
+ #'
389
+ #' Renders vertical lines
390
+ #' @keywords internal
391
+ geom_vline_handler <- function(data, canvas, scales, params, style_opts = NULL) {
392
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
393
+
394
+ for (i in seq_len(nrow(data))) {
395
+ x <- scales$x(data$xintercept[i])
396
+
397
+ color <- colors[i]
398
+ if (!is.null(color) && !is.na(color)) {
399
+ color <- color_to_term(color)
400
+ } else {
401
+ color <- NULL
402
+ }
403
+
404
+ canvas$draw_vline(round(x), color = color)
405
+ }
406
+ }
407
+
408
+ #' GeomRect Handler
409
+ #'
410
+ #' Renders rectangles
411
+ #' @keywords internal
412
+ geom_rect_handler <- function(data, canvas, scales, params, style_opts = NULL) {
413
+ colors <- if ("fill" %in% names(data)) data$fill else rep("white", nrow(data))
414
+
415
+ for (i in seq_len(nrow(data))) {
416
+ xmin <- scales$x(data$xmin[i])
417
+ xmax <- scales$x(data$xmax[i])
418
+ ymin <- scales$y(data$ymin[i])
419
+ ymax <- scales$y(data$ymax[i])
420
+
421
+ color <- colors[i]
422
+ if (!is.null(color) && !is.na(color)) {
423
+ color <- color_to_term(color)
424
+ } else {
425
+ color <- NULL
426
+ }
427
+
428
+ # Note: y is inverted
429
+ canvas$fill_rect(round(xmin), round(ymax), round(xmax), round(ymin), color)
430
+ }
431
+ }
432
+
433
+ #' GeomSmooth Handler
434
+ #'
435
+ #' Renders smoothed lines (just draws the line, ignores confidence interval)
436
+ #' @keywords internal
437
+ geom_smooth_handler <- function(data, canvas, scales, params, style_opts = NULL) {
438
+ # Sort by x
439
+ data <- data[order(data$x), ]
440
+
441
+ # Get color
442
+ color <- if ("colour" %in% names(data)) {
443
+ color_to_term(data$colour[1])
444
+ } else {
445
+ NULL
446
+ }
447
+
448
+ # Scale coordinates
449
+ xs <- sapply(data$x, scales$x)
450
+ ys <- sapply(data$y, scales$y)
451
+
452
+ # Draw polyline
453
+ canvas$draw_polyline(xs, ys, color)
454
+ }
455
+
456
+ #' GeomDensity Handler
457
+ #'
458
+ #' Renders density curves
459
+ #' @keywords internal
460
+ geom_density_handler <- function(data, canvas, scales, params, style_opts = NULL) {
461
+ # Group by group if present
462
+ if ("group" %in% names(data)) {
463
+ groups <- unique(data$group)
464
+ } else {
465
+ groups <- 1
466
+ data$group <- 1
467
+ }
468
+
469
+ for (grp in groups) {
470
+ grp_data <- data[data$group == grp, ]
471
+ grp_data <- grp_data[order(grp_data$x), ]
472
+
473
+ if (nrow(grp_data) < 2) next
474
+
475
+ # Get color - prefer fill over colour for density plots
476
+ # (density plots typically use fill aesthetic for distinction)
477
+ color <- if ("fill" %in% names(grp_data) &&
478
+ !is.na(grp_data$fill[1]) &&
479
+ grp_data$fill[1] != "grey20") {
480
+ color_to_term(grp_data$fill[1])
481
+ } else if ("colour" %in% names(grp_data) &&
482
+ !is.na(grp_data$colour[1]) &&
483
+ grp_data$colour[1] != "black") {
484
+ color_to_term(grp_data$colour[1])
485
+ } else {
486
+ NULL
487
+ }
488
+
489
+ # Use density as y
490
+ xs <- sapply(grp_data$x, scales$x)
491
+ ys <- sapply(grp_data$density, scales$y)
492
+
493
+ # Draw polyline
494
+ canvas$draw_polyline(xs, ys, color)
495
+ }
496
+ }
497
+
498
+ #' GeomHistogram Handler
499
+ #'
500
+ #' Renders histograms (same as bar)
501
+ #' @keywords internal
502
+ geom_histogram_handler <- geom_bar_handler
503
+
504
+
505
+ #' GeomBoxplot Handler
506
+ #'
507
+ #' Renders boxplots with whiskers, box, median line, and outliers.
508
+ #' Supports two styles: "ascii" (box-drawing characters) and "braille" (Braille dots).
509
+ #' @keywords internal
510
+ geom_boxplot_handler <- function(data, canvas, scales, params, style_opts = NULL) {
511
+ # Get boxplot style (default to "ascii" for classic look)
512
+ boxplot_style <- if (!is.null(style_opts) && !is.null(style_opts$boxplot_style)) {
513
+ style_opts$boxplot_style
514
+ } else {
515
+ "ascii"
516
+ }
517
+
518
+ # Get colors
519
+ fill_colors <- if ("fill" %in% names(data)) data$fill else rep("white", nrow(data))
520
+ outline_colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
521
+
522
+ for (i in seq_len(nrow(data))) {
523
+ # Get boxplot statistics
524
+ x <- data$x[i]
525
+ xmin <- data$xmin[i]
526
+ xmax <- data$xmax[i]
527
+ ymin <- data$ymin[i] # Lower whisker
528
+ lower <- data$lower[i] # Q1
529
+ middle <- data$middle[i] # Median
530
+ upper <- data$upper[i] # Q3
531
+ ymax <- data$ymax[i] # Upper whisker
532
+ outliers <- data$outliers[[i]]
533
+
534
+ # Get color
535
+ fill_color <- fill_colors[i]
536
+ if (!is.null(fill_color) && !is.na(fill_color)) {
537
+ fill_color <- color_to_term(fill_color)
538
+ } else {
539
+ fill_color <- NULL
540
+ }
541
+
542
+ outline_color <- outline_colors[i]
543
+ if (!is.null(outline_color) && !is.na(outline_color)) {
544
+ outline_color <- color_to_term(outline_color)
545
+ } else {
546
+ outline_color <- fill_color
547
+ }
548
+
549
+ # Scale coordinates
550
+ sx <- scales$x(x)
551
+ sxmin <- scales$x(xmin)
552
+ sxmax <- scales$x(xmax)
553
+ symin <- scales$y(ymin)
554
+ slower <- scales$y(lower)
555
+ smiddle <- scales$y(middle)
556
+ supper <- scales$y(upper)
557
+ symax <- scales$y(ymax)
558
+
559
+ if (boxplot_style == "ascii") {
560
+ # ASCII style: use box-drawing characters directly on the character grid
561
+ # Convert pixel coordinates to character coordinates
562
+ # Use the center x coordinate as the reference point
563
+ char_x <- round(sx / canvas$x_mult)
564
+ char_ymin <- round(symin / canvas$y_mult)
565
+ char_lower <- round(slower / canvas$y_mult)
566
+ char_middle <- round(smiddle / canvas$y_mult)
567
+ char_upper <- round(supper / canvas$y_mult)
568
+ char_ymax <- round(symax / canvas$y_mult)
569
+
570
+ # Calculate box width in character units from the data
571
+ box_half_width <- round((sxmax - sxmin) / canvas$x_mult / 2)
572
+ if (box_half_width < 2) box_half_width <- 2 # Minimum width for visible box
573
+
574
+ # Box-drawing characters
575
+ horiz <- "\u2500" # horizontal line
576
+ vert <- "\u2502" # vertical line
577
+ top_left <- "\u250c" # top left corner
578
+ top_right <- "\u2510" # top right corner
579
+ bottom_left <- "\u2514" # bottom left corner
580
+ bottom_right <- "\u2518" # bottom right corner
581
+
582
+ # Get canvas matrix dimensions
583
+ n_rows <- nrow(canvas$matrix)
584
+ n_cols <- ncol(canvas$matrix)
585
+
586
+ # Clamp center to valid range
587
+ char_x <- max(1, min(n_cols, char_x))
588
+ char_ymin <- max(1, min(n_rows, char_ymin))
589
+ char_lower <- max(1, min(n_rows, char_lower))
590
+ char_middle <- max(1, min(n_rows, char_middle))
591
+ char_upper <- max(1, min(n_rows, char_upper))
592
+ char_ymax <- max(1, min(n_rows, char_ymax))
593
+
594
+ # Calculate box boundaries from center
595
+ box_left <- max(1, char_x - box_half_width)
596
+ box_right <- min(n_cols, char_x + box_half_width)
597
+ box_top <- min(char_lower, char_upper)
598
+ box_bottom <- max(char_lower, char_upper)
599
+
600
+ # Draw whiskers (vertical lines) - use char_x (true center from data)
601
+ # Only draw whiskers OUTSIDE the box (between ymin and lower, between upper and ymax)
602
+ # Lower whisker: from ymin to just before lower (box_bottom)
603
+ if (char_ymin != box_bottom) {
604
+ whisker_rows_lower <- seq(min(box_bottom, char_ymin), max(box_bottom, char_ymin))
605
+ # Exclude the box edge itself
606
+ whisker_rows_lower <- whisker_rows_lower[whisker_rows_lower != box_bottom]
607
+ for (row in whisker_rows_lower) {
608
+ if (row >= 1 && row <= n_rows && char_x >= 1 && char_x <= n_cols) {
609
+ canvas$matrix[row, char_x] <- make_colored(vert, fill_color)
610
+ }
611
+ }
612
+ }
613
+ # Upper whisker: from just after upper (box_top) to ymax
614
+ if (char_ymax != box_top) {
615
+ whisker_rows_upper <- seq(min(box_top, char_ymax), max(box_top, char_ymax))
616
+ # Exclude the box edge itself
617
+ whisker_rows_upper <- whisker_rows_upper[whisker_rows_upper != box_top]
618
+ for (row in whisker_rows_upper) {
619
+ if (row >= 1 && row <= n_rows && char_x >= 1 && char_x <= n_cols) {
620
+ canvas$matrix[row, char_x] <- make_colored(vert, fill_color)
621
+ }
622
+ }
623
+ }
624
+
625
+ # Draw whisker caps (horizontal lines) - same width as median (inside box)
626
+ # Only draw caps if they differ from the box edges
627
+ for (col in (box_left + 1):(box_right - 1)) {
628
+ if (col >= 1 && col <= n_cols) {
629
+ if (char_ymin >= 1 && char_ymin <= n_rows && char_ymin != box_bottom) {
630
+ canvas$matrix[char_ymin, col] <- make_colored(horiz, fill_color)
631
+ }
632
+ if (char_ymax >= 1 && char_ymax <= n_rows && char_ymax != box_top) {
633
+ canvas$matrix[char_ymax, col] <- make_colored(horiz, fill_color)
634
+ }
635
+ }
636
+ }
637
+
638
+ # Top and bottom of box
639
+ for (col in box_left:box_right) {
640
+ if (box_top >= 1 && box_top <= n_rows) {
641
+ canvas$matrix[box_top, col] <- make_colored(horiz, fill_color)
642
+ }
643
+ if (box_bottom >= 1 && box_bottom <= n_rows) {
644
+ canvas$matrix[box_bottom, col] <- make_colored(horiz, fill_color)
645
+ }
646
+ }
647
+
648
+ # Sides of box
649
+ for (row in box_top:box_bottom) {
650
+ if (row >= 1 && row <= n_rows) {
651
+ if (box_left >= 1 && box_left <= n_cols) {
652
+ canvas$matrix[row, box_left] <- make_colored(vert, fill_color)
653
+ }
654
+ if (box_right >= 1 && box_right <= n_cols) {
655
+ canvas$matrix[row, box_right] <- make_colored(vert, fill_color)
656
+ }
657
+ }
658
+ }
659
+
660
+ # Corners
661
+ if (box_top >= 1 && box_top <= n_rows) {
662
+ if (box_left >= 1 && box_left <= n_cols) {
663
+ canvas$matrix[box_top, box_left] <- make_colored(top_left, fill_color)
664
+ }
665
+ if (box_right >= 1 && box_right <= n_cols) {
666
+ canvas$matrix[box_top, box_right] <- make_colored(top_right, fill_color)
667
+ }
668
+ }
669
+ if (box_bottom >= 1 && box_bottom <= n_rows) {
670
+ if (box_left >= 1 && box_left <= n_cols) {
671
+ canvas$matrix[box_bottom, box_left] <- make_colored(bottom_left, fill_color)
672
+ }
673
+ if (box_right >= 1 && box_right <= n_cols) {
674
+ canvas$matrix[box_bottom, box_right] <- make_colored(bottom_right, fill_color)
675
+ }
676
+ }
677
+
678
+ # Median line - only inside the box (not including the border)
679
+ if (char_middle >= 1 && char_middle <= n_rows) {
680
+ for (col in (box_left + 1):(box_right - 1)) {
681
+ if (col >= 1 && col <= n_cols) {
682
+ canvas$matrix[char_middle, col] <- make_colored(horiz, fill_color)
683
+ }
684
+ }
685
+ }
686
+
687
+ # Outliers - use char_x (true center from data)
688
+ if (length(outliers) > 0 && !all(is.na(outliers))) {
689
+ for (out in outliers) {
690
+ if (!is.na(out)) {
691
+ char_y_out <- round(scales$y(out) / canvas$y_mult)
692
+ if (char_y_out >= 1 && char_y_out <= n_rows &&
693
+ char_x >= 1 && char_x <= n_cols) {
694
+ canvas$matrix[char_y_out, char_x] <- make_colored("*", fill_color)
695
+ }
696
+ }
697
+ }
698
+ }
699
+
700
+ } else {
701
+ # Braille style: use canvas drawing methods (high resolution)
702
+ # Draw whiskers (vertical lines from box to whisker ends)
703
+ canvas$draw_segment(round(sx), round(slower), round(sx), round(symin), color = fill_color)
704
+ canvas$draw_segment(round(sx), round(supper), round(sx), round(symax), color = fill_color)
705
+
706
+ # Draw whisker caps (horizontal lines at whisker ends)
707
+ cap_width <- (sxmax - sxmin) / 2
708
+ canvas$draw_segment(round(sx - cap_width/2), round(symin), round(sx + cap_width/2), round(symin), color = fill_color)
709
+ canvas$draw_segment(round(sx - cap_width/2), round(symax), round(sx + cap_width/2), round(symax), color = fill_color)
710
+
711
+ # Draw box (rectangle from Q1 to Q3)
712
+ canvas$draw_rect(round(sxmin), round(supper), round(sxmax), round(slower), color = fill_color)
713
+
714
+ # Draw median line
715
+ canvas$draw_segment(round(sxmin), round(smiddle), round(sxmax), round(smiddle), color = fill_color)
716
+
717
+ # Draw outliers
718
+ if (length(outliers) > 0 && !all(is.na(outliers))) {
719
+ for (out in outliers) {
720
+ if (!is.na(out)) {
721
+ sy_out <- scales$y(out)
722
+ canvas$set_pixel(round(sx), round(sy_out), fill_color)
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+ }
729
+
730
+
731
+ #' GeomText Handler
732
+ #'
733
+ #' Renders text labels
734
+ #' @keywords internal
735
+ geom_text_handler <- function(data, canvas, scales, params, style_opts = NULL) {
736
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
737
+
738
+ for (i in seq_len(nrow(data))) {
739
+ x <- scales$x(data$x[i])
740
+ y <- scales$y(data$y[i])
741
+ label <- as.character(data$label[i])
742
+
743
+ color <- colors[i]
744
+ if (!is.null(color) && !is.na(color)) {
745
+ color <- color_to_term(color)
746
+ } else {
747
+ color <- NULL
748
+ }
749
+
750
+ # Convert pixel position to character position for text
751
+ char_x <- round(x / canvas$x_mult)
752
+ char_y <- round(y / canvas$y_mult)
753
+
754
+ canvas$draw_text(char_x, char_y, label, color)
755
+ }
756
+ }
757
+
758
+
759
+ # ============================================================================
760
+ # Color Conversion
761
+ # ============================================================================
762
+
763
+ # ============================================================================
764
+ # Color Mapping System
765
+ # ============================================================================
766
+
767
+ # Environment to store color mappings for the current plot
768
+ .color_map_env <- new.env(parent = emptyenv())
769
+
770
+ #' Initialize color mapping for a set of ggplot colors
771
+ #'
772
+ #' This function takes all unique colors from a ggplot and assigns terminal
773
+ #' colors to minimize repetition while respecting hue similarity.
774
+ #'
775
+ #' @param ggplot_colors Vector of unique colors from ggplot
776
+ #' @export
777
+ init_color_mapping <- function(ggplot_colors) {
778
+ # Available chromatic terminal colors (in hue order: 0, 60, 120, 180, 240, 300)
779
+ term_colors <- c("red", "yellow", "green", "cyan", "blue", "magenta")
780
+ n_term <- length(term_colors)
781
+
782
+ # Filter out NULL/NA and get unique colors
783
+ ggplot_colors <- unique(ggplot_colors[!is.na(ggplot_colors) & !is.null(ggplot_colors)])
784
+ n_colors <- length(ggplot_colors)
785
+
786
+ if (n_colors == 0) {
787
+ .color_map_env$mapping <- list()
788
+ return(invisible(NULL))
789
+ }
790
+
791
+ # Calculate hue for each ggplot color
792
+ hues <- sapply(ggplot_colors, get_color_hue)
793
+
794
+ # Sort colors by hue
795
+ hue_order <- order(hues)
796
+ sorted_colors <- ggplot_colors[hue_order]
797
+ sorted_hues <- hues[hue_order]
798
+
799
+ # Assign terminal colors to minimize repetition
800
+ # Strategy: distribute terminal colors evenly across the sorted hue spectrum
801
+ mapping <- list()
802
+
803
+ if (n_colors <= n_term) {
804
+ # We have enough terminal colors - assign each ggplot color a unique one
805
+ # Use the hue-sorted order to assign colors that are spread out
806
+ term_indices <- round(seq(1, n_term, length.out = n_colors))
807
+ for (i in seq_along(sorted_colors)) {
808
+ mapping[[sorted_colors[i]]] <- term_colors[term_indices[i]]
809
+ }
810
+ } else {
811
+ # More ggplot colors than terminal colors - minimize repetition
812
+ # Each terminal color will be used ceiling(n_colors/n_term) or floor times
813
+ # Distribute evenly across the hue-sorted colors
814
+ for (i in seq_along(sorted_colors)) {
815
+ # Cycle through terminal colors
816
+ term_idx <- ((i - 1) %% n_term) + 1
817
+ mapping[[sorted_colors[i]]] <- term_colors[term_idx]
818
+ }
819
+ }
820
+
821
+ .color_map_env$mapping <- mapping
822
+ invisible(NULL)
823
+ }
824
+
825
+ #' Get the hue of a color (0-360 degrees)
826
+ #'
827
+ #' @param color A color value
828
+ #' @return Hue in degrees (0-360) or NA for grayscale
829
+ #' @keywords internal
830
+ get_color_hue <- function(color) {
831
+ if (is.null(color) || is.na(color)) return(NA)
832
+
833
+ tryCatch({
834
+ rgb_val <- col2rgb(color)
835
+ r <- rgb_val[1, 1]
836
+ g <- rgb_val[2, 1]
837
+ b <- rgb_val[3, 1]
838
+
839
+ max_val <- max(r, g, b)
840
+ min_val <- min(r, g, b)
841
+ chroma <- max_val - min_val
842
+
843
+ if (chroma == 0) return(NA) # Grayscale
844
+
845
+ if (max_val == r) {
846
+ hue <- 60 * (((g - b) / chroma) %% 6)
847
+ } else if (max_val == g) {
848
+ hue <- 60 * ((b - r) / chroma + 2)
849
+ } else {
850
+ hue <- 60 * ((r - g) / chroma + 4)
851
+ }
852
+
853
+ if (hue < 0) hue <- hue + 360
854
+ return(hue)
855
+ }, error = function(e) {
856
+ return(NA)
857
+ })
858
+ }
859
+
860
+ #' Convert ggplot2 color to terminal color name
861
+ #'
862
+ #' If init_color_mapping() was called, uses the pre-computed mapping.
863
+ #' Otherwise falls back to simple hue-based matching.
864
+ #'
865
+ #' @param color A color value (hex, name, or R color)
866
+ #' @return A terminal color name (blue, red, green, etc.) or NULL
867
+ #' @export
868
+ color_to_term <- function(color) {
869
+ if (is.null(color) || is.na(color)) return(NULL)
870
+
871
+ # If already a terminal color name, return as-is
872
+ term_colors <- c("blue", "red", "green", "yellow", "magenta", "cyan", "white", "black", "silver")
873
+ if (tolower(color) %in% term_colors) {
874
+ return(tolower(color))
875
+ }
876
+
877
+ # Check if we have a pre-computed mapping
878
+ if (exists("mapping", envir = .color_map_env) &&
879
+ length(.color_map_env$mapping) > 0 &&
880
+ color %in% names(.color_map_env$mapping)) {
881
+ return(.color_map_env$mapping[[color]])
882
+ }
883
+
884
+ # Fallback: simple hue-based matching
885
+ tryCatch({
886
+ rgb_val <- col2rgb(color)
887
+ r <- rgb_val[1, 1]
888
+ g <- rgb_val[2, 1]
889
+ b <- rgb_val[3, 1]
890
+
891
+ # Check for near-black or near-white first
892
+ max_val <- max(r, g, b)
893
+ min_val <- min(r, g, b)
894
+
895
+ if (max_val < 40) return("black")
896
+ if (min_val > 220) return("white")
897
+
898
+ # Check for grayscale (low saturation)
899
+ if (max_val - min_val < 25) {
900
+ if (max_val > 170) return("white")
901
+ if (max_val > 85) return("silver")
902
+ return("black")
903
+ }
904
+
905
+ # Get hue and map to terminal color
906
+ hue <- get_color_hue(color)
907
+ if (is.na(hue)) return("silver")
908
+
909
+ # Map hue to terminal colors (60-degree segments)
910
+ if (hue < 30 || hue >= 330) {
911
+ return("red")
912
+ } else if (hue < 90) {
913
+ return("yellow")
914
+ } else if (hue < 150) {
915
+ return("green")
916
+ } else if (hue < 210) {
917
+ return("cyan")
918
+ } else if (hue < 270) {
919
+ return("blue")
920
+ } else {
921
+ return("magenta")
922
+ }
923
+ }, error = function(e) {
924
+ return(NULL)
925
+ })
926
+ }
927
+
928
+
929
+ #' GeomTile Handler
930
+ #'
931
+ #' Renders tiles/heatmaps. Tiles use center (x, y) coordinates with width/height.
932
+ #' @keywords internal
933
+ geom_tile_handler <- function(data, canvas, scales, params, style_opts = NULL) {
934
+ # Get fill colors
935
+ colors <- if ("fill" %in% names(data)) data$fill else rep("white", nrow(data))
936
+
937
+ # Default tile dimensions (usually 1 for discrete scales)
938
+ default_width <- if ("width" %in% names(data) && !is.na(data$width[1])) data$width[1] else 1
939
+ default_height <- if ("height" %in% names(data) && !is.na(data$height[1])) data$height[1] else 1
940
+
941
+ for (i in seq_len(nrow(data))) {
942
+ # Get center coordinates - skip if NA
943
+ x_center <- data$x[i]
944
+ y_center <- data$y[i]
945
+ if (is.na(x_center) || is.na(y_center)) next
946
+
947
+ # Get tile dimensions
948
+ tile_width <- if ("width" %in% names(data) && !is.na(data$width[i])) {
949
+ data$width[i]
950
+ } else {
951
+ default_width
952
+ }
953
+ tile_height <- if ("height" %in% names(data) && !is.na(data$height[i])) {
954
+ data$height[i]
955
+ } else {
956
+ default_height
957
+ }
958
+
959
+ # Calculate corners from center
960
+ xmin <- x_center - tile_width / 2
961
+ xmax <- x_center + tile_width / 2
962
+ ymin <- y_center - tile_height / 2
963
+ ymax <- y_center + tile_height / 2
964
+
965
+ # Scale to canvas coordinates
966
+ xmin_scaled <- scales$x(xmin)
967
+ xmax_scaled <- scales$x(xmax)
968
+ ymin_scaled <- scales$y(ymin)
969
+ ymax_scaled <- scales$y(ymax)
970
+
971
+ # Check for valid coordinates
972
+ if (any(is.na(c(xmin_scaled, xmax_scaled, ymin_scaled, ymax_scaled)))) next
973
+
974
+ # Get color
975
+ color <- colors[i]
976
+ if (!is.null(color) && !is.na(color)) {
977
+ color <- color_to_term(color)
978
+ } else {
979
+ color <- NULL
980
+ }
981
+
982
+ # Draw filled rectangle (note: y is inverted in canvas coordinates)
983
+ canvas$fill_rect(round(xmin_scaled), round(ymax_scaled),
984
+ round(xmax_scaled), round(ymin_scaled), color)
985
+ }
986
+ }
987
+
988
+
989
+ # ============================================================================
990
+ # Additional Geom Handlers
991
+ # ============================================================================
992
+
993
+ #' GeomStep Handler
994
+ #'
995
+ #' Renders step functions (staircase lines)
996
+ #' @keywords internal
997
+ geom_step_handler <- function(data, canvas, scales, params, style_opts = NULL) {
998
+ # Group by group if present
999
+ if ("group" %in% names(data)) {
1000
+ groups <- unique(data$group)
1001
+ } else {
1002
+ groups <- 1
1003
+ data$group <- 1
1004
+ }
1005
+
1006
+ for (grp in groups) {
1007
+ grp_data <- data[data$group == grp, ]
1008
+ grp_data <- grp_data[order(grp_data$x), ]
1009
+ if (nrow(grp_data) < 2) next
1010
+
1011
+ color <- if ("colour" %in% names(grp_data)) {
1012
+ color_to_term(grp_data$colour[1])
1013
+ } else {
1014
+ NULL
1015
+ }
1016
+
1017
+ # Build step coordinates: horizontal then vertical
1018
+ xs <- numeric(0)
1019
+ ys <- numeric(0)
1020
+ for (i in seq_len(nrow(grp_data))) {
1021
+ sx <- scales$x(grp_data$x[i])
1022
+ sy <- scales$y(grp_data$y[i])
1023
+ if (i > 1) {
1024
+ # Horizontal segment to new x at previous y
1025
+ xs <- c(xs, sx)
1026
+ ys <- c(ys, ys[length(ys)])
1027
+ }
1028
+ xs <- c(xs, sx)
1029
+ ys <- c(ys, sy)
1030
+ }
1031
+
1032
+ canvas$draw_polyline(xs, ys, color)
1033
+ }
1034
+ }
1035
+
1036
+ #' GeomAbline Handler
1037
+ #'
1038
+ #' Renders diagonal reference lines (slope + intercept)
1039
+ #' @keywords internal
1040
+ geom_abline_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1041
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1042
+
1043
+ for (i in seq_len(nrow(data))) {
1044
+ intercept <- data$intercept[i]
1045
+ slope <- data$slope[i]
1046
+
1047
+ color <- colors[i]
1048
+ if (!is.null(color) && !is.na(color)) {
1049
+ color <- color_to_term(color)
1050
+ } else {
1051
+ color <- NULL
1052
+ }
1053
+
1054
+ # Compute line endpoints from x_range
1055
+ x0 <- scales$x_range[1]
1056
+ x1 <- scales$x_range[2]
1057
+ y0 <- intercept + slope * x0
1058
+ y1 <- intercept + slope * x1
1059
+
1060
+ # Clamp to y_range
1061
+ y0 <- max(scales$y_range[1], min(scales$y_range[2], y0))
1062
+ y1 <- max(scales$y_range[1], min(scales$y_range[2], y1))
1063
+
1064
+ canvas$draw_line(
1065
+ round(scales$x(x0)), round(scales$y(y0)),
1066
+ round(scales$x(x1)), round(scales$y(y1)),
1067
+ color
1068
+ )
1069
+ }
1070
+ }
1071
+
1072
+ #' GeomRibbon Handler
1073
+ #'
1074
+ #' Renders filled ribbons between ymin and ymax
1075
+ #' @keywords internal
1076
+ geom_ribbon_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1077
+ if ("group" %in% names(data)) {
1078
+ groups <- unique(data$group)
1079
+ } else {
1080
+ groups <- 1
1081
+ data$group <- 1
1082
+ }
1083
+
1084
+ for (grp in groups) {
1085
+ grp_data <- data[data$group == grp, ]
1086
+ grp_data <- grp_data[order(grp_data$x), ]
1087
+ if (nrow(grp_data) < 2) next
1088
+
1089
+ color <- if ("fill" %in% names(grp_data)) {
1090
+ color_to_term(grp_data$fill[1])
1091
+ } else if ("colour" %in% names(grp_data)) {
1092
+ color_to_term(grp_data$colour[1])
1093
+ } else {
1094
+ NULL
1095
+ }
1096
+
1097
+ # Fill between ymin and ymax by drawing vertical segments
1098
+ for (i in seq_len(nrow(grp_data))) {
1099
+ x_px <- round(scales$x(grp_data$x[i]))
1100
+ ymin_px <- round(scales$y(grp_data$ymin[i]))
1101
+ ymax_px <- round(scales$y(grp_data$ymax[i]))
1102
+ # y is inverted: ymax_px < ymin_px in pixel space
1103
+ top <- min(ymin_px, ymax_px)
1104
+ bottom <- max(ymin_px, ymax_px)
1105
+ for (py in top:bottom) {
1106
+ canvas$set_pixel(x_px, py, color)
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ #' GeomErrorbar Handler
1113
+ #'
1114
+ #' Renders vertical error bars with horizontal caps
1115
+ #' @keywords internal
1116
+ geom_errorbar_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1117
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1118
+
1119
+ for (i in seq_len(nrow(data))) {
1120
+ color <- colors[i]
1121
+ if (!is.null(color) && !is.na(color)) {
1122
+ color <- color_to_term(color)
1123
+ } else {
1124
+ color <- NULL
1125
+ }
1126
+
1127
+ x_px <- round(scales$x(data$x[i]))
1128
+ ymin_px <- round(scales$y(data$ymin[i]))
1129
+ ymax_px <- round(scales$y(data$ymax[i]))
1130
+
1131
+ # Vertical line
1132
+ canvas$draw_line(x_px, ymin_px, x_px, ymax_px, color)
1133
+
1134
+ # Horizontal caps using xmin/xmax if available, otherwise approximate
1135
+ if ("xmin" %in% names(data) && "xmax" %in% names(data)) {
1136
+ xmin_px <- round(scales$x(data$xmin[i]))
1137
+ xmax_px <- round(scales$x(data$xmax[i]))
1138
+ } else {
1139
+ cap_half <- max(2, round(scales$width * 0.01))
1140
+ xmin_px <- x_px - cap_half
1141
+ xmax_px <- x_px + cap_half
1142
+ }
1143
+
1144
+ canvas$draw_line(xmin_px, ymin_px, xmax_px, ymin_px, color)
1145
+ canvas$draw_line(xmin_px, ymax_px, xmax_px, ymax_px, color)
1146
+ }
1147
+ }
1148
+
1149
+ #' GeomLinerange Handler
1150
+ #'
1151
+ #' Renders vertical line ranges (no caps)
1152
+ #' @keywords internal
1153
+ geom_linerange_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1154
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1155
+
1156
+ for (i in seq_len(nrow(data))) {
1157
+ color <- colors[i]
1158
+ if (!is.null(color) && !is.na(color)) {
1159
+ color <- color_to_term(color)
1160
+ } else {
1161
+ color <- NULL
1162
+ }
1163
+
1164
+ x_px <- round(scales$x(data$x[i]))
1165
+ ymin_px <- round(scales$y(data$ymin[i]))
1166
+ ymax_px <- round(scales$y(data$ymax[i]))
1167
+
1168
+ canvas$draw_line(x_px, ymin_px, x_px, ymax_px, color)
1169
+ }
1170
+ }
1171
+
1172
+ #' GeomPointrange Handler
1173
+ #'
1174
+ #' Renders point with vertical line range
1175
+ #' @keywords internal
1176
+ geom_pointrange_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1177
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1178
+
1179
+ for (i in seq_len(nrow(data))) {
1180
+ color <- colors[i]
1181
+ if (!is.null(color) && !is.na(color)) {
1182
+ color <- color_to_term(color)
1183
+ } else {
1184
+ color <- NULL
1185
+ }
1186
+
1187
+ x_px <- round(scales$x(data$x[i]))
1188
+ y_px <- round(scales$y(data$y[i]))
1189
+ ymin_px <- round(scales$y(data$ymin[i]))
1190
+ ymax_px <- round(scales$y(data$ymax[i]))
1191
+
1192
+ # Line range
1193
+ canvas$draw_line(x_px, ymin_px, x_px, ymax_px, color)
1194
+ # Point at center
1195
+ canvas$set_pixel(x_px, y_px, color)
1196
+ }
1197
+ }
1198
+
1199
+ #' GeomCrossbar Handler
1200
+ #'
1201
+ #' Renders crossbar (box with middle line, no whiskers)
1202
+ #' @keywords internal
1203
+ geom_crossbar_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1204
+ fill_colors <- if ("fill" %in% names(data)) data$fill else rep(NA, nrow(data))
1205
+ outline_colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1206
+
1207
+ for (i in seq_len(nrow(data))) {
1208
+ color <- outline_colors[i]
1209
+ if (!is.null(color) && !is.na(color)) {
1210
+ color <- color_to_term(color)
1211
+ } else {
1212
+ color <- NULL
1213
+ }
1214
+
1215
+ fill_color <- fill_colors[i]
1216
+ if (!is.null(fill_color) && !is.na(fill_color)) {
1217
+ fill_color <- color_to_term(fill_color)
1218
+ } else {
1219
+ fill_color <- NULL
1220
+ }
1221
+
1222
+ xmin_px <- round(scales$x(data$xmin[i]))
1223
+ xmax_px <- round(scales$x(data$xmax[i]))
1224
+ y_px <- round(scales$y(data$y[i]))
1225
+ ymin_px <- round(scales$y(data$ymin[i]))
1226
+ ymax_px <- round(scales$y(data$ymax[i]))
1227
+
1228
+ # Fill if fill color given
1229
+ if (!is.null(fill_color)) {
1230
+ canvas$fill_rect(xmin_px, min(ymin_px, ymax_px), xmax_px, max(ymin_px, ymax_px), fill_color)
1231
+ }
1232
+
1233
+ # Outline rectangle
1234
+ canvas$draw_rect(xmin_px, min(ymin_px, ymax_px), xmax_px, max(ymin_px, ymax_px), color)
1235
+ # Middle line (median)
1236
+ canvas$draw_line(xmin_px, y_px, xmax_px, y_px, color)
1237
+ }
1238
+ }
1239
+
1240
+ #' GeomRug Handler
1241
+ #'
1242
+ #' Renders rug marks (short tick marks along axes)
1243
+ #' @keywords internal
1244
+ geom_rug_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1245
+ colors <- if ("colour" %in% names(data)) data$colour else rep("white", nrow(data))
1246
+ rug_length <- max(2, round(scales$height * 0.03))
1247
+
1248
+ for (i in seq_len(nrow(data))) {
1249
+ color <- colors[i]
1250
+ if (!is.null(color) && !is.na(color)) {
1251
+ color <- color_to_term(color)
1252
+ } else {
1253
+ color <- NULL
1254
+ }
1255
+
1256
+ # X rug: short vertical tick at bottom
1257
+ if ("x" %in% names(data) && !is.na(data$x[i])) {
1258
+ x_px <- round(scales$x(data$x[i]))
1259
+ canvas$draw_line(x_px, scales$height, x_px, scales$height - rug_length, color)
1260
+ }
1261
+
1262
+ # Y rug: short horizontal tick at left
1263
+ if ("y" %in% names(data) && !is.na(data$y[i])) {
1264
+ y_px <- round(scales$y(data$y[i]))
1265
+ canvas$draw_line(1, y_px, 1 + rug_length, y_px, color)
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ #' GeomLabel Handler
1271
+ #'
1272
+ #' Renders text labels (same as geom_text for terminal)
1273
+ #' @keywords internal
1274
+ geom_label_handler <- geom_text_handler
1275
+
1276
+ #' GeomRaster Handler
1277
+ #'
1278
+ #' Renders raster images (same as tile for terminal)
1279
+ #' @keywords internal
1280
+ geom_raster_handler <- geom_tile_handler
1281
+
1282
+ #' GeomViolin Handler
1283
+ #'
1284
+ #' Renders violin plots (mirrored density curves)
1285
+ #' @keywords internal
1286
+ geom_violin_handler <- function(data, canvas, scales, params, style_opts = NULL) {
1287
+ if ("group" %in% names(data)) {
1288
+ groups <- unique(data$group)
1289
+ } else {
1290
+ groups <- 1
1291
+ data$group <- 1
1292
+ }
1293
+
1294
+ for (grp in groups) {
1295
+ grp_data <- data[data$group == grp, ]
1296
+ grp_data <- grp_data[order(grp_data$y), ]
1297
+ if (nrow(grp_data) < 2) next
1298
+
1299
+ fill_color <- if ("fill" %in% names(grp_data) && !is.na(grp_data$fill[1])) {
1300
+ color_to_term(grp_data$fill[1])
1301
+ } else {
1302
+ NULL
1303
+ }
1304
+
1305
+ outline_color <- if ("colour" %in% names(grp_data) && !is.na(grp_data$colour[1])) {
1306
+ color_to_term(grp_data$colour[1])
1307
+ } else {
1308
+ fill_color
1309
+ }
1310
+
1311
+ # Compute actual violin edges from violinwidth
1312
+ # (xmin/xmax in data are group bounds, not the per-slice shape)
1313
+ x_center <- grp_data$x
1314
+ vw <- grp_data$violinwidth
1315
+ xminv <- x_center - vw * (x_center - grp_data$xmin)
1316
+ xmaxv <- x_center + vw * (grp_data$xmax - x_center)
1317
+
1318
+ # Fill violin body
1319
+ for (i in seq_len(nrow(grp_data))) {
1320
+ xmin_px <- round(scales$x(xminv[i]))
1321
+ xmax_px <- round(scales$x(xmaxv[i]))
1322
+ y_px <- round(scales$y(grp_data$y[i]))
1323
+ if (xmin_px <= xmax_px) {
1324
+ for (px in xmin_px:xmax_px) {
1325
+ canvas$set_pixel(px, y_px, fill_color)
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ # Draw outline polylines for left and right edges
1331
+ xs_left <- sapply(xminv, scales$x)
1332
+ xs_right <- sapply(xmaxv, scales$x)
1333
+ ys <- sapply(grp_data$y, scales$y)
1334
+ canvas$draw_polyline(xs_left, ys, outline_color)
1335
+ canvas$draw_polyline(xs_right, ys, outline_color)
1336
+ }
1337
+ }
1338
+
1339
+
1340
+ # ============================================================================
1341
+ # Register Built-in Geoms
1342
+ # ============================================================================
1343
+
1344
+ .onLoad_geoms <- function() {
1345
+ register_geom("GeomPoint", geom_point_handler)
1346
+ register_geom("GeomLine", geom_line_handler)
1347
+ register_geom("GeomPath", geom_path_handler)
1348
+ register_geom("GeomBar", geom_bar_handler)
1349
+ register_geom("GeomCol", geom_bar_handler)
1350
+ register_geom("GeomArea", geom_area_handler)
1351
+ register_geom("GeomSegment", geom_segment_handler)
1352
+ register_geom("GeomHline", geom_hline_handler)
1353
+ register_geom("GeomVline", geom_vline_handler)
1354
+ register_geom("GeomRect", geom_rect_handler)
1355
+ register_geom("GeomTile", geom_tile_handler)
1356
+ register_geom("GeomSmooth", geom_smooth_handler)
1357
+ register_geom("GeomDensity", geom_density_handler)
1358
+ register_geom("GeomHistogram", geom_histogram_handler)
1359
+ register_geom("GeomText", geom_text_handler)
1360
+ register_geom("GeomBoxplot", geom_boxplot_handler)
1361
+ register_geom("GeomStep", geom_step_handler)
1362
+ register_geom("GeomAbline", geom_abline_handler)
1363
+ register_geom("GeomRibbon", geom_ribbon_handler)
1364
+ register_geom("GeomErrorbar", geom_errorbar_handler)
1365
+ register_geom("GeomLinerange", geom_linerange_handler)
1366
+ register_geom("GeomPointrange", geom_pointrange_handler)
1367
+ register_geom("GeomCrossbar", geom_crossbar_handler)
1368
+ register_geom("GeomRug", geom_rug_handler)
1369
+ register_geom("GeomLabel", geom_label_handler)
1370
+ register_geom("GeomRaster", geom_raster_handler)
1371
+ register_geom("GeomViolin", geom_violin_handler)
1372
+ }
1373
+
1374
+ # Register geoms when the file is sourced
1375
+ .onLoad_geoms()
1376
+