plotcli-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CLAUDE.md +51 -0
- LICENSE +21 -0
- PKG-INFO +358 -0
- README.md +340 -0
- main.py +6 -0
- plotcli-original/.Rbuildignore +18 -0
- plotcli-original/.github/workflows/deploy_docs.yml +43 -0
- plotcli-original/.gitignore +46 -0
- plotcli-original/DESCRIPTION +25 -0
- plotcli-original/NAMESPACE +60 -0
- plotcli-original/NEWS.md +112 -0
- plotcli-original/R/ascii_escape.r +13 -0
- plotcli-original/R/canvas.r +586 -0
- plotcli-original/R/class_functions.r +114 -0
- plotcli-original/R/geom_registry.r +1376 -0
- plotcli-original/R/ggplotcli.r +234 -0
- plotcli-original/R/ggplotcli_helpers.r +1099 -0
- plotcli-original/R/helper_functions.r +351 -0
- plotcli-original/R/plotcli.r +963 -0
- plotcli-original/R/plotcli_grid.r +1 -0
- plotcli-original/R/plotcli_wrappers.r +416 -0
- plotcli-original/R/zzz.r +15 -0
- plotcli-original/README.md +192 -0
- plotcli-original/docs/ascii.png +0 -0
- plotcli-original/docs/bar.png +0 -0
- plotcli-original/docs/block.png +0 -0
- plotcli-original/docs/boxplot.png +0 -0
- plotcli-original/docs/density.png +0 -0
- plotcli-original/docs/facet.png +0 -0
- plotcli-original/docs/facet_grid.png +0 -0
- plotcli-original/docs/generate_png.sh +137 -0
- plotcli-original/docs/heatmap.png +0 -0
- plotcli-original/docs/histogram.png +0 -0
- plotcli-original/docs/line.png +0 -0
- plotcli-original/docs/noborder.png +0 -0
- plotcli-original/docs/scatter.png +0 -0
- plotcli-original/docs/showcase.R +182 -0
- plotcli-original/inst/doc/ggplotcli.R +231 -0
- plotcli-original/inst/doc/ggplotcli.Rmd +329 -0
- plotcli-original/inst/doc/ggplotcli.html +1078 -0
- plotcli-original/inst/doc/plotcli_class.R +98 -0
- plotcli-original/inst/doc/plotcli_class.Rmd +121 -0
- plotcli-original/inst/doc/plotcli_class.html +564 -0
- plotcli-original/inst/doc/plotcli_wrappers.R +35 -0
- plotcli-original/inst/doc/plotcli_wrappers.Rmd +62 -0
- plotcli-original/inst/doc/plotcli_wrappers.html +546 -0
- plotcli-original/man/AsciiCanvas.Rd +116 -0
- plotcli-original/man/BlockCanvas.Rd +132 -0
- plotcli-original/man/BrailleCanvas.Rd +146 -0
- plotcli-original/man/Canvas.Rd +492 -0
- plotcli-original/man/GeomRegistry.Rd +9 -0
- plotcli-original/man/add_legend_to_output.Rd +12 -0
- plotcli-original/man/braille_dot_bit.Rd +29 -0
- plotcli-original/man/braille_set_dot.Rd +21 -0
- plotcli-original/man/bresenham.Rd +27 -0
- plotcli-original/man/build_plot_output.Rd +28 -0
- plotcli-original/man/build_plot_output_v2.Rd +41 -0
- plotcli-original/man/cat_plot_matrix.Rd +17 -0
- plotcli-original/man/cbind.plotcli.Rd +19 -0
- plotcli-original/man/cbind_plots.Rd +17 -0
- plotcli-original/man/color_to_term.Rd +18 -0
- plotcli-original/man/create_canvas.Rd +21 -0
- plotcli-original/man/create_panel_scales.Rd +29 -0
- plotcli-original/man/create_scales.Rd +27 -0
- plotcli-original/man/dot-geom_registry.Rd +16 -0
- plotcli-original/man/draw_border.Rd +12 -0
- plotcli-original/man/draw_grid.Rd +12 -0
- plotcli-original/man/extract_legend_info.Rd +12 -0
- plotcli-original/man/extract_plot_labels.Rd +12 -0
- plotcli-original/man/extract_plot_style.Rd +12 -0
- plotcli-original/man/format_axis_label.Rd +18 -0
- plotcli-original/man/format_four_chars.Rd +21 -0
- plotcli-original/man/geom_area_handler.Rd +12 -0
- plotcli-original/man/geom_bar_handler.Rd +12 -0
- plotcli-original/man/geom_boxplot_handler.Rd +13 -0
- plotcli-original/man/geom_density_handler.Rd +12 -0
- plotcli-original/man/geom_histogram_handler.Rd +12 -0
- plotcli-original/man/geom_hline_handler.Rd +12 -0
- plotcli-original/man/geom_line_handler.Rd +12 -0
- plotcli-original/man/geom_path_handler.Rd +12 -0
- plotcli-original/man/geom_point_handler.Rd +12 -0
- plotcli-original/man/geom_rect_handler.Rd +12 -0
- plotcli-original/man/geom_segment_handler.Rd +12 -0
- plotcli-original/man/geom_smooth_handler.Rd +12 -0
- plotcli-original/man/geom_text_handler.Rd +12 -0
- plotcli-original/man/geom_vline_handler.Rd +12 -0
- plotcli-original/man/get_color_hue.Rd +18 -0
- plotcli-original/man/get_data_subset.Rd +23 -0
- plotcli-original/man/get_facet_info.Rd +18 -0
- plotcli-original/man/get_geom_handler.Rd +17 -0
- plotcli-original/man/get_term_colors.Rd +21 -0
- plotcli-original/man/ggplotcli.Rd +83 -0
- plotcli-original/man/init_color_mapping.Rd +15 -0
- plotcli-original/man/is_braille.Rd +20 -0
- plotcli-original/man/is_geom_registered.Rd +17 -0
- plotcli-original/man/list_registered_geoms.Rd +14 -0
- plotcli-original/man/make_colored.Rd +23 -0
- plotcli-original/man/make_unique_names.Rd +20 -0
- plotcli-original/man/normalize_data.Rd +27 -0
- plotcli-original/man/pclib.Rd +48 -0
- plotcli-original/man/pclibx.Rd +46 -0
- plotcli-original/man/pclid.Rd +44 -0
- plotcli-original/man/pclih.Rd +50 -0
- plotcli-original/man/pclil.Rd +48 -0
- plotcli-original/man/pclis.Rd +48 -0
- plotcli-original/man/pixel_to_braille.Rd +23 -0
- plotcli-original/man/plotcli.Rd +598 -0
- plotcli-original/man/plotcli_bar.Rd +48 -0
- plotcli-original/man/plotcli_box.Rd +46 -0
- plotcli-original/man/plotcli_density.Rd +44 -0
- plotcli-original/man/plotcli_histogram.Rd +50 -0
- plotcli-original/man/plotcli_line.Rd +48 -0
- plotcli-original/man/plotcli_options.Rd +18 -0
- plotcli-original/man/plotcli_scatter.Rd +48 -0
- plotcli-original/man/plus-.plotcli.Rd +19 -0
- plotcli-original/man/rbind.plotcli.Rd +19 -0
- plotcli-original/man/rbind_plots.Rd +17 -0
- plotcli-original/man/register_geom.Rd +21 -0
- plotcli-original/man/remove_color_codes.Rd +21 -0
- plotcli-original/man/render_faceted_plot.Rd +25 -0
- plotcli-original/man/render_single_panel.Rd +12 -0
- plotcli-original/man/safe_aes_name.Rd +18 -0
- plotcli-original/tests/testthat/test-new-geoms.R +136 -0
- plotcli-original/tests/testthat/test-plotcli.R +69 -0
- plotcli-original/tests/testthat.R +4 -0
- plotcli-original/vignettes/ggplotcli.Rmd +329 -0
- plotcli-original/vignettes/plotcli_class.R +98 -0
- plotcli-original/vignettes/plotcli_class.Rmd +121 -0
- plotcli-original/vignettes/plotcli_wrappers.R +35 -0
- plotcli-original/vignettes/plotcli_wrappers.Rmd +62 -0
- plotcli.egg-info/PKG-INFO +11 -0
- plotcli.egg-info/SOURCES.txt +7 -0
- plotcli.egg-info/dependency_links.txt +1 -0
- plotcli.egg-info/entry_points.txt +3 -0
- plotcli.egg-info/top_level.txt +1 -0
- plotcli.py +978 -0
- plotcli_py-0.1.0.dist-info/METADATA +358 -0
- plotcli_py-0.1.0.dist-info/RECORD +143 -0
- plotcli_py-0.1.0.dist-info/WHEEL +4 -0
- plotcli_py-0.1.0.dist-info/entry_points.txt +2 -0
- plotcli_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyproject.toml +31 -0
- uv.lock +8 -0
|
@@ -0,0 +1,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
|
+
|