Skip to main content

r/interpreter/graphics/
par.rs

1//! Graphical parameter state (`par()`) — stores R's graphical parameters
2//! per-interpreter, matching R's `par()` system.
3
4use crate::interpreter::value::*;
5use crate::interpreter::BuiltinContext;
6use minir_macros::interpreter_builtin;
7
8use super::color::RColor;
9
10// region: LineType and FontFace
11
12/// R line types, matching `par("lty")` values.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LineType {
15    Blank,
16    Solid,
17    Dashed,
18    Dotted,
19    DotDash,
20    LongDash,
21    TwoDash,
22}
23
24impl LineType {
25    pub fn from_r_value(val: &RValue) -> Result<Self, RError> {
26        match val {
27            RValue::Vector(rv) => match &rv.inner {
28                Vector::Character(c) => {
29                    let s = c.first().and_then(|o| o.as_deref()).unwrap_or("solid");
30                    Self::from_str(s)
31                }
32                Vector::Integer(i) => {
33                    let n = i.first_opt().unwrap_or(1);
34                    Self::from_int(n)
35                }
36                Vector::Double(d) => {
37                    let n = d.first_opt().map(|v| v as i64).unwrap_or(1);
38                    Self::from_int(n)
39                }
40                _ => Err(RError::new(
41                    RErrorKind::Other,
42                    "invalid line type specification",
43                )),
44            },
45            _ => Err(RError::new(
46                RErrorKind::Other,
47                "invalid line type specification",
48            )),
49        }
50    }
51
52    fn from_str(s: &str) -> Result<Self, RError> {
53        match s.to_lowercase().as_str() {
54            "blank" | "0" => Ok(LineType::Blank),
55            "solid" | "1" => Ok(LineType::Solid),
56            "dashed" | "2" => Ok(LineType::Dashed),
57            "dotted" | "3" => Ok(LineType::Dotted),
58            "dotdash" | "4" => Ok(LineType::DotDash),
59            "longdash" | "5" => Ok(LineType::LongDash),
60            "twodash" | "6" => Ok(LineType::TwoDash),
61            _ => Err(RError::new(
62                RErrorKind::Other,
63                format!("invalid line type '{s}'"),
64            )),
65        }
66    }
67
68    fn from_int(n: i64) -> Result<Self, RError> {
69        match n {
70            0 => Ok(LineType::Blank),
71            1 => Ok(LineType::Solid),
72            2 => Ok(LineType::Dashed),
73            3 => Ok(LineType::Dotted),
74            4 => Ok(LineType::DotDash),
75            5 => Ok(LineType::LongDash),
76            6 => Ok(LineType::TwoDash),
77            _ => Err(RError::new(
78                RErrorKind::Other,
79                format!("invalid line type integer {n}: must be 0–6"),
80            )),
81        }
82    }
83
84    #[allow(dead_code)] // used when serializing lty to integer for graphics devices
85    fn to_int(self) -> i64 {
86        match self {
87            LineType::Blank => 0,
88            LineType::Solid => 1,
89            LineType::Dashed => 2,
90            LineType::Dotted => 3,
91            LineType::DotDash => 4,
92            LineType::LongDash => 5,
93            LineType::TwoDash => 6,
94        }
95    }
96
97    fn to_str(self) -> &'static str {
98        match self {
99            LineType::Blank => "blank",
100            LineType::Solid => "solid",
101            LineType::Dashed => "dashed",
102            LineType::Dotted => "dotted",
103            LineType::DotDash => "dotdash",
104            LineType::LongDash => "longdash",
105            LineType::TwoDash => "twodash",
106        }
107    }
108}
109
110/// R font faces, matching `par("font")` values.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum FontFace {
113    Plain,
114    Bold,
115    Italic,
116    BoldItalic,
117    Symbol,
118}
119
120impl FontFace {
121    fn from_int(n: i64) -> Result<Self, RError> {
122        match n {
123            1 => Ok(FontFace::Plain),
124            2 => Ok(FontFace::Bold),
125            3 => Ok(FontFace::Italic),
126            4 => Ok(FontFace::BoldItalic),
127            5 => Ok(FontFace::Symbol),
128            _ => Err(RError::new(
129                RErrorKind::Other,
130                format!("invalid font face {n}: must be 1–5"),
131            )),
132        }
133    }
134
135    fn to_int(self) -> i64 {
136        match self {
137            FontFace::Plain => 1,
138            FontFace::Bold => 2,
139            FontFace::Italic => 3,
140            FontFace::BoldItalic => 4,
141            FontFace::Symbol => 5,
142        }
143    }
144}
145
146// endregion
147
148// region: ParState
149
150/// Full graphical parameter state, mirroring R's `par()` settings.
151#[derive(Debug, Clone)]
152pub struct ParState {
153    /// Foreground (drawing) color
154    pub col: RColor,
155    /// Background color
156    pub bg: RColor,
157    /// Line width
158    pub lwd: f64,
159    /// Line type
160    pub lty: LineType,
161    /// Plotting character (symbol)
162    pub pch: i64,
163    /// Character expansion factor
164    pub cex: f64,
165    /// Point size of text (in big points)
166    pub ps: f64,
167    /// Font face
168    pub font: FontFace,
169    /// Font family name
170    pub family: String,
171    /// Margins (bottom, left, top, right) in lines
172    pub mar: [f64; 4],
173    /// Multi-figure layout: rows, cols (set by mfrow)
174    pub mfrow: [i64; 2],
175    /// Multi-figure layout: rows, cols (set by mfcol)
176    pub mfcol: [i64; 2],
177    /// User coordinate limits: xmin, xmax, ymin, ymax
178    pub usr: [f64; 4],
179    /// Axis label style: 0=parallel, 1=horizontal, 2=perpendicular, 3=vertical
180    pub las: i64,
181    /// X-axis interval calculation style ("r" or "i")
182    pub xaxs: String,
183    /// Y-axis interval calculation style ("r" or "i")
184    pub yaxs: String,
185    /// Whether plot.new() has been called on the current device
186    pub new: bool,
187}
188
189impl Default for ParState {
190    fn default() -> Self {
191        ParState {
192            col: RColor::BLACK,
193            bg: RColor::WHITE,
194            lwd: 1.0,
195            lty: LineType::Solid,
196            pch: 1,
197            cex: 1.0,
198            ps: 12.0,
199            font: FontFace::Plain,
200            family: "sans".to_string(),
201            mar: [5.1, 4.1, 4.1, 2.1],
202            mfrow: [1, 1],
203            mfcol: [1, 1],
204            usr: [0.0, 1.0, 0.0, 1.0],
205            las: 0,
206            xaxs: "r".to_string(),
207            yaxs: "r".to_string(),
208            new: false,
209        }
210    }
211}
212
213impl ParState {
214    /// Get a graphical parameter value by name, returning it as an RValue.
215    pub fn get(&self, name: &str) -> Option<RValue> {
216        match name {
217            "col" => Some(RValue::vec(Vector::Character(
218                vec![Some(self.col.to_hex())].into(),
219            ))),
220            "bg" => Some(RValue::vec(Vector::Character(
221                vec![Some(self.bg.to_hex())].into(),
222            ))),
223            "lwd" => Some(RValue::vec(Vector::Double(vec![Some(self.lwd)].into()))),
224            "lty" => Some(RValue::vec(Vector::Character(
225                vec![Some(self.lty.to_str().to_string())].into(),
226            ))),
227            "pch" => Some(RValue::vec(Vector::Integer(vec![Some(self.pch)].into()))),
228            "cex" => Some(RValue::vec(Vector::Double(vec![Some(self.cex)].into()))),
229            "ps" => Some(RValue::vec(Vector::Double(vec![Some(self.ps)].into()))),
230            "font" => Some(RValue::vec(Vector::Integer(
231                vec![Some(self.font.to_int())].into(),
232            ))),
233            "family" => Some(RValue::vec(Vector::Character(
234                vec![Some(self.family.clone())].into(),
235            ))),
236            "mar" => Some(RValue::vec(Vector::Double(
237                self.mar.iter().map(|&v| Some(v)).collect::<Vec<_>>().into(),
238            ))),
239            "mfrow" => Some(RValue::vec(Vector::Integer(
240                self.mfrow
241                    .iter()
242                    .map(|&v| Some(v))
243                    .collect::<Vec<_>>()
244                    .into(),
245            ))),
246            "mfcol" => Some(RValue::vec(Vector::Integer(
247                self.mfcol
248                    .iter()
249                    .map(|&v| Some(v))
250                    .collect::<Vec<_>>()
251                    .into(),
252            ))),
253            "usr" => Some(RValue::vec(Vector::Double(
254                self.usr.iter().map(|&v| Some(v)).collect::<Vec<_>>().into(),
255            ))),
256            "las" => Some(RValue::vec(Vector::Integer(vec![Some(self.las)].into()))),
257            "xaxs" => Some(RValue::vec(Vector::Character(
258                vec![Some(self.xaxs.clone())].into(),
259            ))),
260            "yaxs" => Some(RValue::vec(Vector::Character(
261                vec![Some(self.yaxs.clone())].into(),
262            ))),
263            "new" => Some(RValue::vec(Vector::Logical(vec![Some(self.new)].into()))),
264            _ => None,
265        }
266    }
267
268    /// Set a graphical parameter by name. Returns the old value on success.
269    pub fn set(&mut self, name: &str, value: &RValue) -> Result<Option<RValue>, RError> {
270        let old = self.get(name);
271        match name {
272            "col" => {
273                self.col = parse_color_param(value, name)?;
274            }
275            "bg" => {
276                self.bg = parse_color_param(value, name)?;
277            }
278            "lwd" => {
279                self.lwd = parse_double_param(value, name)?;
280            }
281            "lty" => {
282                self.lty = LineType::from_r_value(value)?;
283            }
284            "pch" => {
285                self.pch = parse_int_param(value, name)?;
286            }
287            "cex" => {
288                self.cex = parse_double_param(value, name)?;
289            }
290            "ps" => {
291                self.ps = parse_double_param(value, name)?;
292            }
293            "font" => {
294                let n = parse_int_param(value, name)?;
295                self.font = FontFace::from_int(n)?;
296            }
297            "family" => {
298                self.family = parse_string_param(value, name)?;
299            }
300            "mar" => {
301                self.mar = parse_double4_param(value, name)?;
302            }
303            "mfrow" => {
304                self.mfrow = parse_int2_param(value, name)?;
305            }
306            "mfcol" => {
307                self.mfcol = parse_int2_param(value, name)?;
308            }
309            "usr" => {
310                self.usr = parse_double4_param(value, name)?;
311            }
312            "las" => {
313                self.las = parse_int_param(value, name)?;
314            }
315            "xaxs" => {
316                self.xaxs = parse_string_param(value, name)?;
317            }
318            "yaxs" => {
319                self.yaxs = parse_string_param(value, name)?;
320            }
321            "new" => {
322                self.new = parse_logical_param(value, name)?;
323            }
324            _ => {
325                return Err(RError::new(
326                    RErrorKind::Other,
327                    format!("'{name}' is not a graphical parameter"),
328                ));
329            }
330        }
331        Ok(old)
332    }
333
334    /// Return all parameter names that this state knows about.
335    pub fn known_params() -> &'static [&'static str] {
336        &[
337            "bg", "cex", "col", "family", "font", "las", "lty", "lwd", "mar", "mfcol", "mfrow",
338            "new", "pch", "ps", "usr", "xaxs", "yaxs",
339        ]
340    }
341}
342
343// endregion
344
345// region: Parameter parsing helpers
346
347fn parse_color_param(value: &RValue, name: &str) -> Result<RColor, RError> {
348    match value {
349        RValue::Vector(rv) => match &rv.inner {
350            Vector::Character(c) => {
351                let s = c.first().and_then(|o| o.as_deref()).ok_or_else(|| {
352                    RError::new(
353                        RErrorKind::Other,
354                        format!("invalid color for parameter '{name}'"),
355                    )
356                })?;
357                RColor::from_r_value(s, &[]).map_err(|e| {
358                    RError::new(
359                        RErrorKind::Other,
360                        format!("invalid color for parameter '{name}': {e}"),
361                    )
362                })
363            }
364            _ => Err(RError::new(
365                RErrorKind::Other,
366                format!("invalid color for parameter '{name}'"),
367            )),
368        },
369        _ => Err(RError::new(
370            RErrorKind::Other,
371            format!("invalid color for parameter '{name}'"),
372        )),
373    }
374}
375
376fn parse_double_param(value: &RValue, name: &str) -> Result<f64, RError> {
377    value
378        .as_vector()
379        .and_then(|v| v.as_double_scalar())
380        .ok_or_else(|| {
381            RError::new(
382                RErrorKind::Other,
383                format!("invalid value for parameter '{name}'"),
384            )
385        })
386}
387
388fn parse_int_param(value: &RValue, name: &str) -> Result<i64, RError> {
389    value
390        .as_vector()
391        .and_then(|v| v.as_integer_scalar())
392        .ok_or_else(|| {
393            RError::new(
394                RErrorKind::Other,
395                format!("invalid value for parameter '{name}'"),
396            )
397        })
398}
399
400fn parse_string_param(value: &RValue, name: &str) -> Result<String, RError> {
401    value
402        .as_vector()
403        .and_then(|v| v.as_character_scalar())
404        .ok_or_else(|| {
405            RError::new(
406                RErrorKind::Other,
407                format!("invalid value for parameter '{name}'"),
408            )
409        })
410}
411
412fn parse_logical_param(value: &RValue, name: &str) -> Result<bool, RError> {
413    value
414        .as_vector()
415        .and_then(|v| v.as_logical_scalar())
416        .ok_or_else(|| {
417            RError::new(
418                RErrorKind::Other,
419                format!("invalid value for parameter '{name}'"),
420            )
421        })
422}
423
424fn parse_double4_param(value: &RValue, name: &str) -> Result<[f64; 4], RError> {
425    let v = value.as_vector().ok_or_else(|| {
426        RError::new(
427            RErrorKind::Other,
428            format!("parameter '{name}' requires a numeric vector of length 4"),
429        )
430    })?;
431    let doubles = v.to_doubles();
432    if doubles.len() != 4 {
433        return Err(RError::new(
434            RErrorKind::Other,
435            format!(
436                "parameter '{name}' requires a numeric vector of length 4, got {}",
437                doubles.len()
438            ),
439        ));
440    }
441    let mut result = [0.0; 4];
442    for (i, d) in doubles.iter().enumerate() {
443        result[i] = d.ok_or_else(|| {
444            RError::new(RErrorKind::Other, format!("NA value in parameter '{name}'"))
445        })?;
446    }
447    Ok(result)
448}
449
450fn parse_int2_param(value: &RValue, name: &str) -> Result<[i64; 2], RError> {
451    let v = value.as_vector().ok_or_else(|| {
452        RError::new(
453            RErrorKind::Other,
454            format!("parameter '{name}' requires an integer vector of length 2"),
455        )
456    })?;
457    let ints = v.to_integers();
458    if ints.len() != 2 {
459        return Err(RError::new(
460            RErrorKind::Other,
461            format!(
462                "parameter '{name}' requires a vector of length 2, got {}",
463                ints.len()
464            ),
465        ));
466    }
467    let mut result = [0i64; 2];
468    for (i, val) in ints.iter().enumerate() {
469        result[i] = val.ok_or_else(|| {
470            RError::new(RErrorKind::Other, format!("NA value in parameter '{name}'"))
471        })?;
472    }
473    Ok(result)
474}
475
476// endregion
477
478// region: par() builtin
479
480/// Query or set graphical parameters.
481///
482/// When called with no arguments, returns all graphical parameters as a named
483/// list. When called with string arguments, returns those parameters. When
484/// called with named arguments, sets those parameters and returns the old values.
485///
486/// @param ... parameter names (as strings) or name=value pairs
487/// @return named list of (old) parameter values
488#[interpreter_builtin(namespace = "graphics")]
489fn interp_par(
490    args: &[RValue],
491    named: &[(String, RValue)],
492    ctx: &BuiltinContext,
493) -> Result<RValue, RError> {
494    let mut par = ctx.interpreter().par_state.borrow_mut();
495
496    // If no args and no named args, return all parameters
497    if args.is_empty() && named.is_empty() {
498        let mut entries: Vec<(Option<String>, RValue)> = Vec::new();
499        for &name in ParState::known_params() {
500            if let Some(val) = par.get(name) {
501                entries.push((Some(name.to_string()), val));
502            }
503        }
504        return Ok(RValue::List(RList::new(entries)));
505    }
506
507    let mut result_entries: Vec<(Option<String>, RValue)> = Vec::new();
508
509    // Handle positional string args: par("col") returns the value of "col"
510    for arg in args {
511        if let Some(v) = arg.as_vector() {
512            if let Some(name) = v.as_character_scalar() {
513                match par.get(&name) {
514                    Some(val) => {
515                        result_entries.push((Some(name), val));
516                    }
517                    None => {
518                        return Err(RError::new(
519                            RErrorKind::Other,
520                            format!("'{name}' is not a graphical parameter"),
521                        ));
522                    }
523                }
524            }
525        }
526    }
527
528    // Handle named args: par(col = "red") sets col and returns old value
529    for (name, value) in named {
530        let old = par.set(name, value)?;
531        if let Some(old_val) = old {
532            result_entries.push((Some(name.clone()), old_val));
533        }
534    }
535
536    if result_entries.is_empty() {
537        Ok(RValue::List(RList::new(vec![])))
538    } else if result_entries.len() == 1 && args.len() == 1 && named.is_empty() {
539        // When querying a single parameter by name, return just the value (not a list)
540        Ok(result_entries
541            .into_iter()
542            .next()
543            .expect("len() == 1 guarantees next()")
544            .1)
545    } else {
546        Ok(RValue::List(RList::new(result_entries)))
547    }
548}
549
550// endregion