1pub mod color;
7
8use super::CallArgs;
9use crate::interpreter::graphics::plot_data::{BoxSpread, PlotItem, PlotState};
10use crate::interpreter::value::*;
11use crate::interpreter::BuiltinContext;
12use minir_macros::{builtin, interpreter_builtin};
13
14fn parse_color(value: &RValue) -> [u8; 4] {
21 match value {
22 RValue::Vector(rv) => {
23 if let Some(s) = rv.inner.as_character_scalar() {
24 parse_color_string(&s)
25 } else if let Some(i) = rv.inner.as_integer_scalar() {
26 default_palette_color(i)
27 } else {
28 [0, 0, 0, 255] }
30 }
31 _ => [0, 0, 0, 255],
32 }
33}
34
35fn parse_color_string(s: &str) -> [u8; 4] {
37 match s.to_lowercase().as_str() {
38 "black" => [0, 0, 0, 255],
39 "white" => [255, 255, 255, 255],
40 "red" => [255, 0, 0, 255],
41 "green" | "green3" => [0, 205, 0, 255],
42 "blue" => [0, 0, 255, 255],
43 "cyan" => [0, 255, 255, 255],
44 "magenta" => [255, 0, 255, 255],
45 "yellow" => [255, 255, 0, 255],
46 "gray" | "grey" => [190, 190, 190, 255],
47 "orange" => [255, 165, 0, 255],
48 "purple" => [160, 32, 240, 255],
49 "brown" => [165, 42, 42, 255],
50 "pink" => [255, 192, 203, 255],
51 "darkred" => [139, 0, 0, 255],
52 "darkgreen" => [0, 100, 0, 255],
53 "darkblue" | "navyblue" | "navy" => [0, 0, 128, 255],
54 "lightblue" => [173, 216, 230, 255],
55 "lightgreen" => [144, 238, 144, 255],
56 "lightgray" | "lightgrey" => [211, 211, 211, 255],
57 "darkgray" | "darkgrey" => [169, 169, 169, 255],
58 "transparent" => [0, 0, 0, 0],
59 _ if s.starts_with('#') => parse_hex_color(s),
60 _ => [0, 0, 0, 255], }
62}
63
64fn parse_hex_color(s: &str) -> [u8; 4] {
66 let hex = s.trim_start_matches('#');
67 let parse_byte =
68 |offset: usize| -> u8 { u8::from_str_radix(&hex[offset..offset + 2], 16).unwrap_or(0) };
69 match hex.len() {
70 6 => [parse_byte(0), parse_byte(2), parse_byte(4), 255],
71 8 => [parse_byte(0), parse_byte(2), parse_byte(4), parse_byte(6)],
72 _ => [0, 0, 0, 255],
73 }
74}
75
76fn default_palette_color(index: i64) -> [u8; 4] {
78 const PALETTE: [[u8; 4]; 8] = [
79 [0, 0, 0, 255], [255, 0, 0, 255], [0, 205, 0, 255], [0, 0, 255, 255], [0, 255, 255, 255], [255, 0, 255, 255], [255, 255, 0, 255], [190, 190, 190, 255], ];
88 if index < 1 {
89 return [0, 0, 0, 255];
90 }
91 let idx = usize::try_from(index - 1).unwrap_or(0) % PALETTE.len();
92 PALETTE[idx]
93}
94
95fn extract_doubles(value: &RValue) -> Result<Vec<f64>, RError> {
101 match value.as_vector() {
102 Some(v) => Ok(v.to_doubles().into_iter().flatten().collect()),
103 None => Err(RError::new(
104 RErrorKind::Argument,
105 "expected a numeric vector".to_string(),
106 )),
107 }
108}
109
110fn try_extract_doubles(value: Option<&RValue>) -> Result<Option<Vec<f64>>, RError> {
112 match value {
113 Some(RValue::Null) | None => Ok(None),
114 Some(v) => Ok(Some(extract_doubles(v)?)),
115 }
116}
117
118fn extract_limits(value: Option<&RValue>) -> Option<(f64, f64)> {
120 let v = value?;
121 if matches!(v, RValue::Null) {
122 return None;
123 }
124 let vec = v.as_vector()?;
125 let doubles = vec.to_doubles();
126 if doubles.len() >= 2 {
127 match (doubles[0], doubles[1]) {
128 (Some(lo), Some(hi)) => Some((lo, hi)),
129 _ => None,
130 }
131 } else {
132 None
133 }
134}
135
136#[cfg(feature = "plot")]
146fn send_current_plot(ctx: &BuiltinContext) -> Result<(), RError> {
147 if ctx.interpreter().file_device.borrow().is_some() {
149 return Ok(());
150 }
151 let state = ctx.interpreter().current_plot.borrow().clone();
152 if let Some(plot_state) = state {
153 let tx = ctx.interpreter().plot_tx.borrow();
154 if let Some(tx) = tx.as_ref() {
155 tx.send(crate::interpreter::graphics::egui_device::PlotMessage::Show(plot_state))
156 .map_err(|e| {
157 RError::new(
158 RErrorKind::Other,
159 format!("failed to send plot to GUI thread: {e}"),
160 )
161 })?;
162 }
163 *ctx.interpreter().current_plot.borrow_mut() = None;
164 }
165 Ok(())
166}
167
168#[cfg(not(feature = "plot"))]
169fn send_current_plot(ctx: &BuiltinContext) -> Result<(), RError> {
170 if ctx.interpreter().file_device.borrow().is_some() {
171 return Ok(());
172 }
173 if ctx.interpreter().current_plot.borrow().is_some() {
174 ctx.write_err(
175 "plot() requires the 'plot' feature. Build with: cargo build --features plot\n",
176 );
177 *ctx.interpreter().current_plot.borrow_mut() = None;
178 }
179 Ok(())
180}
181
182pub fn flush_plot(interp: &crate::interpreter::Interpreter) {
185 let state = interp.current_plot.borrow().clone();
186 if let Some(plot_state) = state {
187 #[cfg(feature = "plot")]
188 {
189 let tx = interp.plot_tx.borrow();
190 if let Some(tx) = tx.as_ref() {
191 let _ = tx
192 .send(crate::interpreter::graphics::egui_device::PlotMessage::Show(plot_state));
193 }
194 }
195 #[cfg(not(feature = "plot"))]
196 drop(plot_state);
197 *interp.current_plot.borrow_mut() = None;
198 }
199}
200
201fn ensure_plot<'a>(ctx: &'a BuiltinContext<'_>) -> std::cell::RefMut<'a, Option<PlotState>> {
203 let mut plot = ctx.interpreter().current_plot.borrow_mut();
204 if plot.is_none() {
205 *plot = Some(PlotState::new());
206 }
207 plot
208}
209
210#[interpreter_builtin(namespace = "grDevices")]
222fn interp_pdf(
223 args: &[RValue],
224 named: &[(String, RValue)],
225 context: &BuiltinContext,
226) -> Result<RValue, RError> {
227 let filename = args
228 .first()
229 .and_then(|v| v.as_vector())
230 .and_then(|v| v.as_character_scalar())
231 .unwrap_or_else(|| "Rplots.pdf".to_string());
232 let ca = CallArgs::new(args, named);
233 let width = ca
234 .named("width")
235 .and_then(|v| v.as_vector()?.as_double_scalar())
236 .unwrap_or(7.0);
237 let height = ca
238 .named("height")
239 .and_then(|v| v.as_vector()?.as_double_scalar())
240 .unwrap_or(7.0);
241
242 *context.interpreter().file_device.borrow_mut() =
243 Some(crate::interpreter::graphics::FileDevice {
244 filename,
245 format: crate::interpreter::graphics::FileFormat::Pdf,
246 width,
247 height,
248 jpeg_quality: 75,
249 });
250 *context.interpreter().current_plot.borrow_mut() = Some(PlotState::new());
251 context.interpreter().set_invisible();
252 Ok(RValue::Null)
253}
254
255#[interpreter_builtin(namespace = "grDevices")]
262fn interp_png(
263 args: &[RValue],
264 named: &[(String, RValue)],
265 context: &BuiltinContext,
266) -> Result<RValue, RError> {
267 let filename = args
268 .first()
269 .and_then(|v| v.as_vector())
270 .and_then(|v| v.as_character_scalar())
271 .unwrap_or_else(|| "Rplot.png".to_string());
272 let ca = CallArgs::new(args, named);
273 let width_px = ca
275 .named("width")
276 .and_then(|v| v.as_vector()?.as_double_scalar())
277 .unwrap_or(480.0);
278 let height_px = ca
279 .named("height")
280 .and_then(|v| v.as_vector()?.as_double_scalar())
281 .unwrap_or(480.0);
282
283 *context.interpreter().file_device.borrow_mut() =
284 Some(crate::interpreter::graphics::FileDevice {
285 filename,
286 format: crate::interpreter::graphics::FileFormat::Png,
287 width: width_px / 96.0,
288 height: height_px / 96.0,
289 jpeg_quality: 75,
290 });
291 *context.interpreter().current_plot.borrow_mut() = Some(PlotState::new());
292 context.interpreter().set_invisible();
293 Ok(RValue::Null)
294}
295
296#[interpreter_builtin(namespace = "grDevices")]
306fn interp_svg(
307 args: &[RValue],
308 named: &[(String, RValue)],
309 context: &BuiltinContext,
310) -> Result<RValue, RError> {
311 let filename = args
312 .first()
313 .and_then(|v| v.as_vector())
314 .and_then(|v| v.as_character_scalar())
315 .ok_or_else(|| {
316 RError::new(
317 RErrorKind::Argument,
318 "svg() requires a filename argument".to_string(),
319 )
320 })?;
321 let ca = CallArgs::new(args, named);
322 let width = ca
323 .named("width")
324 .and_then(|v| v.as_vector()?.as_double_scalar())
325 .unwrap_or(7.0);
326 let height = ca
327 .named("height")
328 .and_then(|v| v.as_vector()?.as_double_scalar())
329 .unwrap_or(7.0);
330
331 *context.interpreter().file_device.borrow_mut() =
332 Some(crate::interpreter::graphics::FileDevice {
333 filename,
334 format: crate::interpreter::graphics::FileFormat::Svg,
335 width,
336 height,
337 jpeg_quality: 75,
338 });
339 *context.interpreter().current_plot.borrow_mut() =
341 Some(crate::interpreter::graphics::plot_data::PlotState::new());
342 context.interpreter().set_invisible();
343 Ok(RValue::Null)
344}
345
346#[interpreter_builtin(namespace = "grDevices")]
354fn interp_jpeg(
355 args: &[RValue],
356 named: &[(String, RValue)],
357 context: &BuiltinContext,
358) -> Result<RValue, RError> {
359 let filename = args
360 .first()
361 .and_then(|v| v.as_vector())
362 .and_then(|v| v.as_character_scalar())
363 .unwrap_or_else(|| "Rplot.jpeg".to_string());
364 let ca = CallArgs::new(args, named);
365 let width_px = ca
366 .named("width")
367 .and_then(|v| v.as_vector()?.as_double_scalar())
368 .unwrap_or(480.0);
369 let height_px = ca
370 .named("height")
371 .and_then(|v| v.as_vector()?.as_double_scalar())
372 .unwrap_or(480.0);
373 let quality = ca
374 .named("quality")
375 .and_then(|v| v.as_vector()?.as_double_scalar())
376 .unwrap_or(75.0) as u8;
377
378 *context.interpreter().file_device.borrow_mut() =
379 Some(crate::interpreter::graphics::FileDevice {
380 filename,
381 format: crate::interpreter::graphics::FileFormat::Jpeg,
382 width: width_px / 96.0,
383 height: height_px / 96.0,
384 jpeg_quality: quality,
385 });
386 *context.interpreter().current_plot.borrow_mut() = Some(PlotState::new());
387 context.interpreter().set_invisible();
388 Ok(RValue::Null)
389}
390
391#[interpreter_builtin(namespace = "grDevices")]
398fn interp_bmp(
399 args: &[RValue],
400 named: &[(String, RValue)],
401 context: &BuiltinContext,
402) -> Result<RValue, RError> {
403 let filename = args
404 .first()
405 .and_then(|v| v.as_vector())
406 .and_then(|v| v.as_character_scalar())
407 .unwrap_or_else(|| "Rplot.bmp".to_string());
408 let ca = CallArgs::new(args, named);
409 let width_px = ca
410 .named("width")
411 .and_then(|v| v.as_vector()?.as_double_scalar())
412 .unwrap_or(480.0);
413 let height_px = ca
414 .named("height")
415 .and_then(|v| v.as_vector()?.as_double_scalar())
416 .unwrap_or(480.0);
417
418 *context.interpreter().file_device.borrow_mut() =
419 Some(crate::interpreter::graphics::FileDevice {
420 filename,
421 format: crate::interpreter::graphics::FileFormat::Bmp,
422 width: width_px / 96.0,
423 height: height_px / 96.0,
424 jpeg_quality: 75,
425 });
426 *context.interpreter().current_plot.borrow_mut() = Some(PlotState::new());
427 context.interpreter().set_invisible();
428 Ok(RValue::Null)
429}
430
431#[interpreter_builtin(name = "dev.off", namespace = "grDevices")]
437fn interp_dev_off(
438 _args: &[RValue],
439 _named: &[(String, RValue)],
440 context: &BuiltinContext,
441) -> Result<RValue, RError> {
442 let file_dev = context.interpreter().file_device.borrow().clone();
444 if let Some(dev) = file_dev {
445 let plot_state = context.interpreter().current_plot.borrow().clone();
446 if let Some(state) = plot_state {
447 #[cfg(feature = "svg-device")]
448 {
449 let svg_str = crate::interpreter::graphics::svg_device::render_svg(
450 &state, dev.width, dev.height,
451 );
452 match dev.format {
453 crate::interpreter::graphics::FileFormat::Svg => {
454 std::fs::write(&dev.filename, &svg_str).map_err(|e| {
455 RError::new(
456 RErrorKind::Other,
457 format!("failed to write SVG file '{}': {e}", dev.filename),
458 )
459 })?;
460 }
461 crate::interpreter::graphics::FileFormat::Png => {
462 #[cfg(feature = "raster-device")]
463 {
464 let width_px = (dev.width * 96.0) as u32;
465 let height_px = (dev.height * 96.0) as u32;
466 let pixmap = crate::interpreter::graphics::raster::svg_to_raster(
467 &svg_str, width_px, height_px,
468 )?;
469 pixmap.save_png(&dev.filename).map_err(|e| {
470 RError::new(
471 RErrorKind::Other,
472 format!("failed to write PNG '{}': {e}", dev.filename),
473 )
474 })?;
475 }
476 #[cfg(not(feature = "raster-device"))]
477 {
478 let svg_filename =
479 dev.filename.strip_suffix(".png").unwrap_or(&dev.filename);
480 let svg_path = format!("{svg_filename}.svg");
481 std::fs::write(&svg_path, &svg_str).map_err(|e| {
482 RError::new(
483 RErrorKind::Other,
484 format!("failed to write '{}': {e}", svg_path),
485 )
486 })?;
487 context.write_err(&format!(
488 "Note: PNG rasterization requires 'raster-device' feature. \
489 SVG written to '{}' instead.\n",
490 svg_path
491 ));
492 }
493 }
494 crate::interpreter::graphics::FileFormat::Jpeg => {
495 #[cfg(feature = "raster-device")]
496 {
497 let width_px = (dev.width * 96.0) as u32;
498 let height_px = (dev.height * 96.0) as u32;
499 let pixmap = crate::interpreter::graphics::raster::svg_to_raster(
500 &svg_str, width_px, height_px,
501 )?;
502 let jpeg_bytes = crate::interpreter::graphics::raster::pixmap_to_jpeg(
503 &pixmap,
504 dev.jpeg_quality,
505 )?;
506 std::fs::write(&dev.filename, &jpeg_bytes).map_err(|e| {
507 RError::new(
508 RErrorKind::Other,
509 format!("failed to write JPEG '{}': {e}", dev.filename),
510 )
511 })?;
512 }
513 #[cfg(not(feature = "raster-device"))]
514 {
515 context.write_err("JPEG output requires the 'raster-device' feature\n");
516 }
517 }
518 crate::interpreter::graphics::FileFormat::Bmp => {
519 #[cfg(feature = "raster-device")]
520 {
521 let width_px = (dev.width * 96.0) as u32;
522 let height_px = (dev.height * 96.0) as u32;
523 let pixmap = crate::interpreter::graphics::raster::svg_to_raster(
524 &svg_str, width_px, height_px,
525 )?;
526 let bmp_bytes =
527 crate::interpreter::graphics::raster::pixmap_to_bmp(&pixmap)?;
528 std::fs::write(&dev.filename, &bmp_bytes).map_err(|e| {
529 RError::new(
530 RErrorKind::Other,
531 format!("failed to write BMP '{}': {e}", dev.filename),
532 )
533 })?;
534 }
535 #[cfg(not(feature = "raster-device"))]
536 {
537 context.write_err("BMP output requires the 'raster-device' feature\n");
538 }
539 }
540 crate::interpreter::graphics::FileFormat::Pdf => {
541 #[cfg(feature = "pdf-device")]
542 {
543 let width_pt = (dev.width * 96.0) as f32;
544 let height_pt = (dev.height * 96.0) as f32;
545 let pdf_bytes = crate::interpreter::graphics::pdf::svg_to_pdf(
546 &svg_str, width_pt, height_pt,
547 )?;
548 std::fs::write(&dev.filename, &pdf_bytes).map_err(|e| {
549 RError::new(
550 RErrorKind::Other,
551 format!("failed to write PDF '{}': {e}", dev.filename),
552 )
553 })?;
554 }
555 #[cfg(not(feature = "pdf-device"))]
556 {
557 let svg_name =
558 dev.filename.strip_suffix(".pdf").unwrap_or(&dev.filename);
559 let svg_path = format!("{svg_name}.svg");
560 std::fs::write(&svg_path, &svg_str).map_err(|e| {
561 RError::new(
562 RErrorKind::Other,
563 format!("failed to write '{}': {e}", svg_path),
564 )
565 })?;
566 context.write_err(&format!(
567 "Note: PDF requires 'pdf-device' feature. SVG written to '{svg_path}'\n"
568 ));
569 }
570 }
571 }
572 }
573 #[cfg(not(feature = "svg-device"))]
574 {
575 context.write_err("File device output requires the 'svg-device' feature\n");
576 }
577 }
578 *context.interpreter().file_device.borrow_mut() = None;
579 }
580
581 #[cfg(feature = "plot")]
583 {
584 let tx = context.interpreter().plot_tx.borrow();
585 if let Some(tx) = tx.as_ref() {
586 drop(tx.send(crate::interpreter::graphics::egui_device::PlotMessage::Close));
587 }
588 }
589 *context.interpreter().current_plot.borrow_mut() = None;
591 context.interpreter().set_invisible();
592 Ok(RValue::vec(Vector::Integer(vec![Some(1i64)].into())))
593}
594
595#[interpreter_builtin(name = "dev.cur", namespace = "grDevices")]
602fn interp_dev_cur(
603 _args: &[RValue],
604 _named: &[(String, RValue)],
605 context: &BuiltinContext,
606) -> Result<RValue, RError> {
607 let num = if context.interpreter().current_plot.borrow().is_some() {
608 2i64
609 } else {
610 1i64
611 };
612 Ok(RValue::vec(Vector::Integer(vec![Some(num)].into())))
613}
614
615#[interpreter_builtin(name = "dev.new", namespace = "grDevices")]
621fn interp_dev_new(
622 _args: &[RValue],
623 _named: &[(String, RValue)],
624 context: &BuiltinContext,
625) -> Result<RValue, RError> {
626 *context.interpreter().current_plot.borrow_mut() = Some(PlotState::new());
627 Ok(RValue::vec(Vector::Integer(vec![Some(2i64)].into())))
628}
629
630#[interpreter_builtin(name = "dev.list", namespace = "grDevices")]
637fn interp_dev_list(
638 _args: &[RValue],
639 _named: &[(String, RValue)],
640 context: &BuiltinContext,
641) -> Result<RValue, RError> {
642 let has_device = context.interpreter().current_plot.borrow().is_some()
643 || context.interpreter().file_device.borrow().is_some();
644 if has_device {
645 let dev_name = {
646 let fd = context.interpreter().file_device.borrow();
647 match fd.as_ref().map(|d| d.format) {
648 Some(crate::interpreter::graphics::FileFormat::Png) => "png",
649 Some(crate::interpreter::graphics::FileFormat::Jpeg) => "jpeg",
650 Some(crate::interpreter::graphics::FileFormat::Bmp) => "bmp",
651 Some(crate::interpreter::graphics::FileFormat::Svg) => "svg",
652 Some(crate::interpreter::graphics::FileFormat::Pdf) => "pdf",
653 None => {
654 if cfg!(target_os = "macos") {
655 "quartz"
656 } else {
657 "X11"
658 }
659 }
660 }
661 };
662 let mut rv = RValue::vec(Vector::Integer(vec![Some(2i64)].into()));
663 if let RValue::Vector(ref mut v) = rv {
664 v.set_attr(
665 "names".to_string(),
666 RValue::vec(Vector::Character(vec![Some(dev_name.to_string())].into())),
667 );
668 }
669 Ok(rv)
670 } else {
671 Ok(RValue::vec(Vector::Integer(vec![].into())))
673 }
674}
675
676#[interpreter_builtin(namespace = "graphics", min_args = 1)]
700fn interp_plot(
701 args: &[RValue],
702 named: &[(String, RValue)],
703 context: &BuiltinContext,
704) -> Result<RValue, RError> {
705 let ca = CallArgs::new(args, named);
706
707 let log_spec = ca
709 .named("log")
710 .and_then(|v| v.as_vector()?.as_character_scalar())
711 .unwrap_or_default();
712 for ch in log_spec.chars() {
713 if ch != 'x' && ch != 'y' {
714 return Err(RError::new(
715 RErrorKind::Argument,
716 format!(
717 "invalid 'log' specification '{}': must contain only 'x' and/or 'y'",
718 log_spec
719 ),
720 ));
721 }
722 }
723 let log_x = log_spec.contains('x');
724 let log_y = log_spec.contains('y');
725
726 let first = &args[0];
728 let (x_data, y_data) = if let RValue::Language(lang) = first {
729 if let crate::parser::ast::Expr::Formula { lhs, rhs } = &*lang.inner {
730 let y_name = rhs_symbol_name(lhs.as_deref()).ok_or_else(|| {
732 RError::new(
733 RErrorKind::Argument,
734 "formula lhs must be a simple variable name".to_string(),
735 )
736 })?;
737 let x_name = rhs_symbol_name(rhs.as_deref()).ok_or_else(|| {
738 RError::new(
739 RErrorKind::Argument,
740 "formula rhs must be a simple variable name".to_string(),
741 )
742 })?;
743 let data_arg = ca.named("data").or_else(|| args.get(1));
744 if let Some(data_val) = data_arg {
745 let df = match data_val {
746 RValue::List(list) => list,
747 _ => {
748 return Err(RError::new(
749 RErrorKind::Type,
750 "'data' argument must be a data frame (list)".to_string(),
751 ))
752 }
753 };
754 let x_col = df_get_column(df, &x_name).ok_or_else(|| {
755 RError::new(
756 RErrorKind::Name,
757 format!("variable '{}' not found in data", x_name),
758 )
759 })?;
760 let y_col = df_get_column(df, &y_name).ok_or_else(|| {
761 RError::new(
762 RErrorKind::Name,
763 format!("variable '{}' not found in data", y_name),
764 )
765 })?;
766 (extract_doubles(x_col)?, extract_doubles(y_col)?)
767 } else {
768 let env = context.env();
770 let x_val = env.get(&x_name).ok_or_else(|| {
771 RError::new(RErrorKind::Name, format!("object '{}' not found", x_name))
772 })?;
773 let y_val = env.get(&y_name).ok_or_else(|| {
774 RError::new(RErrorKind::Name, format!("object '{}' not found", y_name))
775 })?;
776 (extract_doubles(&x_val)?, extract_doubles(&y_val)?)
777 }
778 } else {
779 let second = ca.value("y", 1);
781 if let Some(y_val) = second {
782 (extract_doubles(first)?, extract_doubles(y_val)?)
783 } else {
784 let y = extract_doubles(first)?;
785 let x: Vec<f64> = (1..=y.len()).map(|i| i as f64).collect();
786 (x, y)
787 }
788 }
789 } else {
790 let second = ca.value("y", 1);
791 if let Some(y_val) = second {
792 (extract_doubles(first)?, extract_doubles(y_val)?)
793 } else {
794 let y = extract_doubles(first)?;
795 let x: Vec<f64> = (1..=y.len()).map(|i| i as f64).collect();
796 (x, y)
797 }
798 };
799
800 let x_data: Vec<f64> = if log_x {
802 x_data
803 .into_iter()
804 .filter(|v| *v > 0.0)
805 .map(|v| v.ln())
806 .collect()
807 } else {
808 x_data
809 };
810 let y_data: Vec<f64> = if log_y {
811 y_data
812 .into_iter()
813 .filter(|v| *v > 0.0)
814 .map(|v| v.ln())
815 .collect()
816 } else {
817 y_data
818 };
819
820 let len = x_data.len().min(y_data.len());
822 let x_data: Vec<f64> = x_data.into_iter().take(len).collect();
823 let y_data: Vec<f64> = y_data.into_iter().take(len).collect();
824
825 let plot_type = ca
827 .optional_string("type", 2)
828 .unwrap_or_else(|| "p".to_string());
829 let title = ca.optional_string("main", 3);
830 let xlab = ca.optional_string("xlab", 4);
831 let ylab = ca.optional_string("ylab", 5);
832 let color = ca
833 .value("col", 6)
834 .map(parse_color)
835 .unwrap_or([0, 0, 0, 255]);
836 let pch = u8::try_from(ca.integer_or("pch", 7, 1)).unwrap_or(1);
837 let cex = ca
838 .value("cex", 8)
839 .and_then(|v| v.as_vector()?.as_double_scalar())
840 .unwrap_or(1.0);
841 let lwd = ca
842 .value("lwd", 9)
843 .and_then(|v| v.as_vector()?.as_double_scalar())
844 .unwrap_or(1.0);
845 let xlim = extract_limits(ca.value("xlim", 10));
846 let ylim = extract_limits(ca.value("ylim", 11));
847
848 let mut state = PlotState::new();
850 state.title = title;
851 state.x_label = xlab;
852 state.y_label = ylab;
853 state.x_lim = xlim;
854 state.y_lim = ylim;
855
856 let point_size = (3.0 * cex) as f32;
858 let line_width = lwd as f32;
859
860 match plot_type.as_str() {
861 "p" => {
862 state.items.push(PlotItem::Points {
863 x: x_data,
864 y: y_data,
865 color,
866 size: point_size,
867 shape: pch,
868 label: None,
869 });
870 }
871 "l" => {
872 state.items.push(PlotItem::Line {
873 x: x_data,
874 y: y_data,
875 color,
876 width: line_width,
877 label: None,
878 });
879 }
880 "b" | "o" => {
881 state.items.push(PlotItem::Line {
882 x: x_data.clone(),
883 y: y_data.clone(),
884 color,
885 width: line_width,
886 label: None,
887 });
888 state.items.push(PlotItem::Points {
889 x: x_data,
890 y: y_data,
891 color,
892 size: point_size,
893 shape: pch,
894 label: None,
895 });
896 }
897 "h" => {
898 for (&xi, &yi) in x_data.iter().zip(y_data.iter()) {
900 state.items.push(PlotItem::Line {
901 x: vec![xi, xi],
902 y: vec![0.0, yi],
903 color,
904 width: line_width,
905 label: None,
906 });
907 }
908 }
909 "n" => {
910 }
912 _ => {
913 return Err(RError::new(
914 RErrorKind::Argument,
915 format!(
916 "invalid plot type '{plot_type}': expected 'p', 'l', 'b', 'o', 'h', or 'n'"
917 ),
918 ));
919 }
920 }
921
922 *context.interpreter().current_plot.borrow_mut() = Some(state);
924 send_current_plot(context)?;
925
926 Ok(RValue::Null)
927}
928
929#[interpreter_builtin(namespace = "graphics", min_args = 1)]
938fn interp_hist(
939 args: &[RValue],
940 named: &[(String, RValue)],
941 context: &BuiltinContext,
942) -> Result<RValue, RError> {
943 let ca = CallArgs::new(args, named);
944 let data = extract_doubles(&args[0])?;
945
946 if data.is_empty() {
947 return Err(RError::new(
948 RErrorKind::Argument,
949 "'x' must have at least one non-NA value".to_string(),
950 ));
951 }
952
953 let breaks_val = ca.value("breaks", 1);
955 let n_bins = match breaks_val {
956 Some(v) => {
957 if let Some(n) = v.as_vector().and_then(|vec| vec.as_integer_scalar()) {
959 usize::try_from(n.max(1)).unwrap_or(10)
960 } else {
961 10
962 }
963 }
964 None => 10,
965 };
966
967 let min_val = data.iter().copied().fold(f64::INFINITY, f64::min);
969 let max_val = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
970 let range = max_val - min_val;
971 let bin_width = if range == 0.0 {
972 1.0
973 } else {
974 range / n_bins as f64
975 };
976
977 let mut break_points: Vec<f64> = (0..=n_bins)
978 .map(|i| min_val + i as f64 * bin_width)
979 .collect();
980 if let Some(last) = break_points.last_mut() {
982 *last = max_val + bin_width * 0.001;
983 }
984
985 let mut counts = vec![0usize; n_bins];
987 for &val in &data {
988 for (i, window) in break_points.windows(2).enumerate() {
989 if val >= window[0] && val < window[1] {
990 counts[i] += 1;
991 break;
992 }
993 }
994 }
995
996 let mids: Vec<f64> = break_points
998 .windows(2)
999 .map(|w| (w[0] + w[1]) / 2.0)
1000 .collect();
1001 let heights: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
1002
1003 let color = ca
1004 .value("col", 2)
1005 .map(parse_color)
1006 .unwrap_or([173, 216, 230, 255]); let title = ca.optional_string("main", 3);
1008 let xlab = ca.optional_string("xlab", 4);
1009
1010 let mut state = PlotState::new();
1011 state.title = title.or_else(|| Some("Histogram of x".to_string()));
1012 state.x_label = xlab;
1013 state.y_label = Some("Frequency".to_string());
1014
1015 state.items.push(PlotItem::Bars {
1016 x: mids.clone(),
1017 heights: heights.clone(),
1018 color,
1019 width: bin_width * 0.9,
1020 label: None,
1021 });
1022
1023 *context.interpreter().current_plot.borrow_mut() = Some(state);
1024 send_current_plot(context)?;
1025
1026 let breaks_rv = RValue::vec(Vector::Double(
1028 break_points
1029 .iter()
1030 .map(|&v| Some(v))
1031 .collect::<Vec<_>>()
1032 .into(),
1033 ));
1034 let counts_rv = RValue::vec(Vector::Integer(
1035 counts
1036 .iter()
1037 .map(|&c| Some(i64::try_from(c).unwrap_or(0)))
1038 .collect::<Vec<_>>()
1039 .into(),
1040 ));
1041 let mids_rv = RValue::vec(Vector::Double(
1042 mids.iter().map(|&v| Some(v)).collect::<Vec<_>>().into(),
1043 ));
1044
1045 let result = RList::new(vec![
1046 (Some("breaks".to_string()), breaks_rv),
1047 (Some("counts".to_string()), counts_rv),
1048 (Some("mids".to_string()), mids_rv),
1049 ]);
1050 Ok(RValue::List(result))
1051}
1052
1053#[interpreter_builtin(namespace = "graphics", min_args = 1)]
1061fn interp_barplot(
1062 args: &[RValue],
1063 named: &[(String, RValue)],
1064 context: &BuiltinContext,
1065) -> Result<RValue, RError> {
1066 let ca = CallArgs::new(args, named);
1067 let heights = extract_doubles(&args[0])?;
1068
1069 if heights.is_empty() {
1070 return Err(RError::new(
1071 RErrorKind::Argument,
1072 "'height' must have at least one value".to_string(),
1073 ));
1074 }
1075
1076 let color = ca
1077 .value("col", 1)
1078 .map(parse_color)
1079 .unwrap_or([173, 216, 230, 255]); let title = ca.optional_string("main", 2);
1081
1082 let x_positions: Vec<f64> = (1..=heights.len()).map(|i| i as f64).collect();
1084
1085 let mut state = PlotState::new();
1086 state.title = title;
1087
1088 state.items.push(PlotItem::Bars {
1089 x: x_positions.clone(),
1090 heights,
1091 color,
1092 width: 0.8,
1093 label: None,
1094 });
1095
1096 *context.interpreter().current_plot.borrow_mut() = Some(state);
1097 send_current_plot(context)?;
1098
1099 Ok(RValue::vec(Vector::Double(
1101 x_positions
1102 .iter()
1103 .map(|&v| Some(v))
1104 .collect::<Vec<_>>()
1105 .into(),
1106 )))
1107}
1108
1109#[interpreter_builtin(namespace = "graphics", min_args = 1)]
1119fn interp_boxplot(
1120 args: &[RValue],
1121 named: &[(String, RValue)],
1122 context: &BuiltinContext,
1123) -> Result<RValue, RError> {
1124 let ca = CallArgs::new(args, named);
1125
1126 let color = ca
1127 .named("col")
1128 .map(parse_color)
1129 .unwrap_or([173, 216, 230, 255]);
1130 let title = ca.named_string("main");
1131
1132 let mut positions = Vec::new();
1133 let mut spreads = Vec::new();
1134
1135 for (i, arg) in args.iter().enumerate() {
1136 let mut data = extract_doubles(arg)?;
1137 if data.is_empty() {
1138 continue;
1139 }
1140 data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1141
1142 let n = data.len();
1143 let median = percentile(&data, 50.0);
1144 let q1 = percentile(&data, 25.0);
1145 let q3 = percentile(&data, 75.0);
1146 let iqr = q3 - q1;
1147 let lower_fence = q1 - 1.5 * iqr;
1148 let upper_fence = q3 + 1.5 * iqr;
1149 let lower_whisker = data
1150 .iter()
1151 .copied()
1152 .find(|&v| v >= lower_fence)
1153 .unwrap_or(data[0]);
1154 let upper_whisker = data
1155 .iter()
1156 .rev()
1157 .copied()
1158 .find(|&v| v <= upper_fence)
1159 .unwrap_or(data[n - 1]);
1160
1161 positions.push((i + 1) as f64);
1162 spreads.push(BoxSpread {
1163 lower_whisker,
1164 q1,
1165 median,
1166 q3,
1167 upper_whisker,
1168 });
1169 }
1170
1171 let mut state = PlotState::new();
1172 state.title = title;
1173
1174 state.items.push(PlotItem::BoxPlot {
1175 positions,
1176 spreads,
1177 color,
1178 });
1179
1180 *context.interpreter().current_plot.borrow_mut() = Some(state);
1181 send_current_plot(context)?;
1182
1183 Ok(RValue::Null)
1184}
1185
1186fn percentile(sorted: &[f64], p: f64) -> f64 {
1188 if sorted.is_empty() {
1189 return f64::NAN;
1190 }
1191 if sorted.len() == 1 {
1192 return sorted[0];
1193 }
1194 let rank = p / 100.0 * (sorted.len() - 1) as f64;
1195 let lo = rank.floor() as usize;
1196 let hi = rank.ceil() as usize;
1197 let frac = rank - lo as f64;
1198 if lo == hi {
1199 sorted[lo]
1200 } else {
1201 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
1202 }
1203}
1204
1205#[interpreter_builtin(namespace = "graphics", min_args = 1)]
1218fn interp_points(
1219 args: &[RValue],
1220 named: &[(String, RValue)],
1221 context: &BuiltinContext,
1222) -> Result<RValue, RError> {
1223 let ca = CallArgs::new(args, named);
1224
1225 let first = &args[0];
1226 let second = ca.value("y", 1);
1227
1228 let (x_data, y_data) = if let Some(y_val) = second {
1229 (extract_doubles(first)?, extract_doubles(y_val)?)
1230 } else {
1231 let y = extract_doubles(first)?;
1232 let x: Vec<f64> = (1..=y.len()).map(|i| i as f64).collect();
1233 (x, y)
1234 };
1235
1236 let color = ca
1237 .value("col", 2)
1238 .map(parse_color)
1239 .unwrap_or([0, 0, 0, 255]);
1240 let pch = u8::try_from(ca.integer_or("pch", 3, 1)).unwrap_or(1);
1241 let cex = ca
1242 .value("cex", 4)
1243 .and_then(|v| v.as_vector()?.as_double_scalar())
1244 .unwrap_or(1.0);
1245
1246 let mut plot = ensure_plot(context);
1247 if let Some(ref mut state) = *plot {
1248 state.items.push(PlotItem::Points {
1249 x: x_data,
1250 y: y_data,
1251 color,
1252 size: (3.0 * cex) as f32,
1253 shape: pch,
1254 label: None,
1255 });
1256 }
1257
1258 Ok(RValue::Null)
1259}
1260
1261#[interpreter_builtin(namespace = "graphics", min_args = 1)]
1269fn interp_lines(
1270 args: &[RValue],
1271 named: &[(String, RValue)],
1272 context: &BuiltinContext,
1273) -> Result<RValue, RError> {
1274 let ca = CallArgs::new(args, named);
1275
1276 let first = &args[0];
1277 let second = ca.value("y", 1);
1278
1279 let (x_data, y_data) = if let Some(y_val) = second {
1280 (extract_doubles(first)?, extract_doubles(y_val)?)
1281 } else {
1282 let y = extract_doubles(first)?;
1283 let x: Vec<f64> = (1..=y.len()).map(|i| i as f64).collect();
1284 (x, y)
1285 };
1286
1287 let color = ca
1288 .value("col", 2)
1289 .map(parse_color)
1290 .unwrap_or([0, 0, 0, 255]);
1291 let lwd = ca
1292 .value("lwd", 3)
1293 .and_then(|v| v.as_vector()?.as_double_scalar())
1294 .unwrap_or(1.0);
1295
1296 let mut plot = ensure_plot(context);
1297 if let Some(ref mut state) = *plot {
1298 state.items.push(PlotItem::Line {
1299 x: x_data,
1300 y: y_data,
1301 color,
1302 width: lwd as f32,
1303 label: None,
1304 });
1305 }
1306
1307 Ok(RValue::Null)
1308}
1309
1310#[interpreter_builtin(namespace = "graphics")]
1320fn interp_abline(
1321 _args: &[RValue],
1322 named: &[(String, RValue)],
1323 context: &BuiltinContext,
1324) -> Result<RValue, RError> {
1325 let ca = CallArgs::new(&[], named);
1326
1327 let color = ca.named("col").map(parse_color).unwrap_or([0, 0, 0, 255]);
1328 let lwd = ca
1329 .named("lwd")
1330 .and_then(|v| v.as_vector()?.as_double_scalar())
1331 .unwrap_or(1.0);
1332
1333 let mut plot = ensure_plot(context);
1334 if let Some(ref mut state) = *plot {
1335 if let Some(h_vals) = try_extract_doubles(ca.named("h"))? {
1337 for h in h_vals {
1338 state.items.push(PlotItem::HLine {
1339 y: h,
1340 color,
1341 width: lwd as f32,
1342 });
1343 }
1344 }
1345
1346 if let Some(v_vals) = try_extract_doubles(ca.named("v"))? {
1348 for v in v_vals {
1349 state.items.push(PlotItem::VLine {
1350 x: v,
1351 color,
1352 width: lwd as f32,
1353 });
1354 }
1355 }
1356
1357 let a_val = ca
1360 .named("a")
1361 .and_then(|v| v.as_vector()?.as_double_scalar());
1362 let b_val = ca
1363 .named("b")
1364 .and_then(|v| v.as_vector()?.as_double_scalar());
1365 if let (Some(a), Some(b)) = (a_val, b_val) {
1366 let x_lo = -1e6_f64;
1367 let x_hi = 1e6_f64;
1368 state.items.push(PlotItem::Line {
1369 x: vec![x_lo, x_hi],
1370 y: vec![a + b * x_lo, a + b * x_hi],
1371 color,
1372 width: lwd as f32,
1373 label: None,
1374 });
1375 }
1376 }
1377
1378 Ok(RValue::Null)
1379}
1380
1381#[interpreter_builtin(namespace = "graphics")]
1388fn interp_legend(
1389 _args: &[RValue],
1390 _named: &[(String, RValue)],
1391 context: &BuiltinContext,
1392) -> Result<RValue, RError> {
1393 let mut plot = ensure_plot(context);
1394 if let Some(ref mut state) = *plot {
1395 state.show_legend = true;
1396 }
1397 Ok(RValue::Null)
1398}
1399
1400#[interpreter_builtin(namespace = "graphics")]
1408fn interp_title(
1409 _args: &[RValue],
1410 named: &[(String, RValue)],
1411 context: &BuiltinContext,
1412) -> Result<RValue, RError> {
1413 let ca = CallArgs::new(&[], named);
1414
1415 let mut plot = ensure_plot(context);
1416 if let Some(ref mut state) = *plot {
1417 if let Some(main) = ca.named_string("main") {
1418 state.title = Some(main);
1419 }
1420 if let Some(xlab) = ca.named_string("xlab") {
1421 state.x_label = Some(xlab);
1422 }
1423 if let Some(ylab) = ca.named_string("ylab") {
1424 state.y_label = Some(ylab);
1425 }
1426 }
1427
1428 Ok(RValue::Null)
1429}
1430
1431#[builtin(namespace = "graphics")]
1439fn builtin_axis(_args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
1440 Ok(RValue::Null)
1442}
1443
1444fn rhs_symbol_name(expr: Option<&crate::parser::ast::Expr>) -> Option<String> {
1460 match expr {
1461 Some(crate::parser::ast::Expr::Symbol(name)) => Some(name.clone()),
1462 _ => None,
1463 }
1464}
1465
1466fn df_get_column<'a>(df: &'a RList, name: &str) -> Option<&'a RValue> {
1468 for (col_name, val) in &df.values {
1469 if col_name.as_deref() == Some(name) {
1470 return Some(val);
1471 }
1472 }
1473 None
1474}
1475
1476#[interpreter_builtin(namespace = "graphics")]
1485fn interp_pairs(
1486 args: &[RValue],
1487 _named: &[(String, RValue)],
1488 ctx: &BuiltinContext,
1489) -> Result<RValue, RError> {
1490 let x = args.first().ok_or_else(|| {
1491 RError::new(
1492 RErrorKind::Argument,
1493 "pairs() requires at least one argument".to_string(),
1494 )
1495 })?;
1496
1497 match x {
1499 RValue::List(list) if super::has_class(x, "data.frame") => {
1500 let numeric_count = list
1502 .values
1503 .iter()
1504 .filter(|(_, v)| {
1505 matches!(
1506 v,
1507 RValue::Vector(rv)
1508 if matches!(
1509 rv.inner,
1510 Vector::Double(_) | Vector::Integer(_) | Vector::Logical(_)
1511 )
1512 )
1513 })
1514 .count();
1515 if numeric_count < 2 {
1516 return Err(RError::new(
1517 RErrorKind::Argument,
1518 format!(
1519 "pairs() needs at least 2 numeric columns, got {numeric_count}. \
1520 Non-numeric columns are skipped."
1521 ),
1522 ));
1523 }
1524 }
1525 RValue::Vector(rv) => {
1526 let dims = super::get_dim_ints(rv.get_attr("dim"));
1527 if dims.is_none() {
1528 return Err(RError::new(
1529 RErrorKind::Type,
1530 "pairs() requires a data frame or matrix, not a plain vector. \
1531 Use matrix() or data.frame() first."
1532 .to_string(),
1533 ));
1534 }
1535 if !matches!(
1536 rv.inner,
1537 Vector::Double(_) | Vector::Integer(_) | Vector::Logical(_)
1538 ) {
1539 return Err(RError::new(
1540 RErrorKind::Type,
1541 "pairs() requires a numeric matrix".to_string(),
1542 ));
1543 }
1544 }
1545 RValue::List(_) => {
1546 return Err(RError::new(
1547 RErrorKind::Type,
1548 "pairs() requires a data frame (not a plain list). Use as.data.frame() first."
1549 .to_string(),
1550 ));
1551 }
1552 _ => {
1553 return Err(RError::new(
1554 RErrorKind::Type,
1555 "pairs() requires a data frame or numeric matrix".to_string(),
1556 ));
1557 }
1558 }
1559
1560 let columns: Vec<(String, Vec<f64>)> = match x {
1562 RValue::List(list) => list
1563 .values
1564 .iter()
1565 .filter_map(|(name, val)| {
1566 if let RValue::Vector(rv) = val {
1567 if matches!(
1568 rv.inner,
1569 Vector::Double(_) | Vector::Integer(_) | Vector::Logical(_)
1570 ) {
1571 let doubles: Vec<f64> = rv
1572 .to_doubles()
1573 .into_iter()
1574 .map(|d| d.unwrap_or(f64::NAN))
1575 .collect();
1576 return Some((name.clone().unwrap_or_else(|| "?".to_string()), doubles));
1577 }
1578 }
1579 None
1580 })
1581 .collect(),
1582 RValue::Vector(rv) => {
1583 let dims = super::get_dim_ints(rv.get_attr("dim")).unwrap_or_default();
1584 let nrow = dims.first().and_then(|d| *d).unwrap_or(0) as usize;
1585 let ncol = dims.get(1).and_then(|d| *d).unwrap_or(0) as usize;
1586 let all_doubles = rv.to_doubles();
1587 (0..ncol)
1588 .map(|c| {
1589 let col: Vec<f64> = (0..nrow)
1590 .map(|r| all_doubles[r + c * nrow].unwrap_or(f64::NAN))
1591 .collect();
1592 (format!("V{}", c + 1), col)
1593 })
1594 .collect()
1595 }
1596 _ => unreachable!(), };
1598
1599 send_current_plot(ctx)?; let mut state = PlotState::new();
1602 state.title = Some("Scatterplot Matrix".to_string());
1603 state.show_legend = true;
1604
1605 let colors: &[[u8; 4]] = &[
1606 [0, 0, 0, 255],
1607 [255, 0, 0, 255],
1608 [0, 0, 255, 255],
1609 [0, 128, 0, 255],
1610 [255, 165, 0, 255],
1611 [128, 0, 128, 255],
1612 ];
1613 let mut color_idx = 0;
1614 for i in 0..columns.len() {
1615 for j in (i + 1)..columns.len() {
1616 let (ref xname, ref xdata) = columns[i];
1617 let (ref yname, ref ydata) = columns[j];
1618 let len = xdata.len().min(ydata.len());
1619 state.items.push(PlotItem::Points {
1620 x: xdata[..len].to_vec(),
1621 y: ydata[..len].to_vec(),
1622 color: colors[color_idx % colors.len()],
1623 size: 3.0,
1624 shape: 1,
1625 label: Some(format!("{xname} vs {yname}")),
1626 });
1627 color_idx += 1;
1628 }
1629 }
1630
1631 *ctx.interpreter().current_plot.borrow_mut() = Some(state);
1632 send_current_plot(ctx)?;
1633 ctx.interpreter().set_invisible();
1634 Ok(RValue::Null)
1635}
1636
1637