ruviz 0.6.0

High-performance 2D plotting library for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/// Line style enumeration for plot lines and borders
///
/// Defines different visual styles for drawing lines in plots.
///
/// # Example
///
/// ```rust,no_run
/// use ruviz::prelude::*;
/// use ruviz::render::LineStyle;
///
/// let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
/// let y: Vec<f64> = x.iter().map(|&v| v.sin()).collect();
///
/// Plot::new()
///     .line(&x, &y)
///     .line_style(LineStyle::Dashed)
///     .end_series()
///     .save("dashed_line.png")?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// ![Line styles](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/assets/rustdoc/line_styles.png)
#[derive(Debug, Clone, PartialEq)]
pub enum LineStyle {
    /// Solid continuous line (default)
    Solid,
    /// Dashed line with equal dash and gap lengths
    Dashed,
    /// Dotted line with small dots
    Dotted,
    /// Dash-dot pattern (long dash, short gap, dot, short gap)
    DashDot,
    /// Dash-dot-dot pattern (long dash, short gap, dot, short gap, dot, short gap)
    DashDotDot,
    /// Custom pattern defined by dash array
    Custom(Vec<f32>),
}

impl LineStyle {
    /// Convert to a baseline dash array.
    ///
    /// Values are defined at a 100-DPI baseline. Renderers should apply their
    /// own DPI scale so physical dash spacing remains consistent.
    ///
    /// Returns None for solid lines, Some(dash_array) for patterned lines.
    pub fn to_dash_array(&self) -> Option<Vec<f32>> {
        match self {
            LineStyle::Solid => None,
            LineStyle::Dashed => Some(vec![5.0, 5.0]),
            LineStyle::Dotted => Some(vec![1.0, 2.0]),
            LineStyle::DashDot => Some(vec![8.0, 3.0, 1.0, 3.0]),
            LineStyle::DashDotDot => Some(vec![8.0, 3.0, 1.0, 3.0, 1.0, 3.0]),
            LineStyle::Custom(pattern) => {
                if pattern.is_empty() {
                    None
                } else {
                    Some(pattern.clone())
                }
            }
        }
    }

    /// Create a custom line style from dash pattern
    ///
    /// Pattern should alternate between dash length and gap length.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use ruviz::prelude::*;
    ///
    /// // Custom dash-dot-dot pattern: long dash, short gap, dot, gap, dot, gap
    /// let custom_style = LineStyle::Custom(vec![10.0, 3.0, 2.0, 3.0, 2.0, 3.0]);
    ///
    /// Plot::new()
    ///     .line(&[1.0, 2.0, 3.0], &[1.0, 4.0, 9.0])
    ///     .line_style(custom_style)
    ///     .end_series()
    ///     .save("custom_line.png")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn custom<I>(pattern: I) -> Self
    where
        I: IntoIterator<Item = f32>,
    {
        let pattern: Vec<f32> = pattern
            .into_iter()
            .map(|x| x.abs()) // Ensure positive values
            .filter(|&x| x > 0.0) // Remove zero values
            .collect();

        LineStyle::Custom(pattern)
    }

    /// Get a descriptive name for the line style
    pub fn name(&self) -> &'static str {
        match self {
            LineStyle::Solid => "solid",
            LineStyle::Dashed => "dashed",
            LineStyle::Dotted => "dotted",
            LineStyle::DashDot => "dash-dot",
            LineStyle::DashDotDot => "dash-dot-dot",
            LineStyle::Custom(_) => "custom",
        }
    }

    /// Check if this is a solid line
    pub fn is_solid(&self) -> bool {
        matches!(self, LineStyle::Solid)
    }

    /// Check if this is a patterned (non-solid) line
    pub fn is_patterned(&self) -> bool {
        !self.is_solid()
    }

    /// Get pattern length (total length of one complete pattern cycle)
    pub fn pattern_length(&self) -> f32 {
        match self.to_dash_array() {
            None => 0.0, // Solid line has no pattern
            Some(pattern) => pattern.iter().sum(),
        }
    }

    /// Scale the pattern by a factor (useful for different line widths)
    ///
    /// # Example
    ///
    /// ```rust
    /// use ruviz::render::LineStyle;
    ///
    /// let dashed = LineStyle::Dashed;
    /// let wide_dashed = dashed.scaled(2.0);
    ///
    /// // Doubled pattern: [10.0, 10.0] instead of [5.0, 5.0]
    /// assert_eq!(wide_dashed.to_dash_array(), Some(vec![10.0, 10.0]));
    /// ```
    pub fn scaled(&self, factor: f32) -> Self {
        if factor <= 0.0 {
            return self.clone();
        }

        match self {
            LineStyle::Custom(pattern) => {
                LineStyle::Custom(pattern.iter().map(|&x| x * factor).collect())
            }
            _ => {
                // For predefined patterns, create scaled custom version
                if let Some(pattern) = self.to_dash_array() {
                    LineStyle::Custom(pattern.iter().map(|&x| x * factor).collect())
                } else {
                    self.clone() // Solid line unchanged
                }
            }
        }
    }
}

impl Default for LineStyle {
    /// Default line style is solid
    fn default() -> Self {
        LineStyle::Solid
    }
}

impl std::fmt::Display for LineStyle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LineStyle::Solid => write!(f, "solid"),
            LineStyle::Dashed => write!(f, "dashed"),
            LineStyle::Dotted => write!(f, "dotted"),
            LineStyle::DashDot => write!(f, "dash-dot"),
            LineStyle::DashDotDot => write!(f, "dash-dot-dot"),
            LineStyle::Custom(pattern) => {
                write!(f, "custom(")?;
                for (i, &value) in pattern.iter().enumerate() {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{:.1}", value)?;
                }
                write!(f, ")")
            }
        }
    }
}

/// Marker style for scatter plots and data points
///
/// # Example
///
/// ```rust,no_run
/// use ruviz::prelude::*;
/// use ruviz::render::MarkerStyle;
///
/// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
/// let y = vec![1.0, 4.0, 2.0, 5.0, 3.0];
///
/// Plot::new()
///     .scatter(&x, &y)
///     .marker(MarkerStyle::Star)
///     .marker_size(12.0)
///     .end_series()
///     .save("star_markers.png")?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// ![Marker styles](https://raw.githubusercontent.com/Ameyanagi/ruviz/main/docs/assets/rustdoc/marker_styles.png)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MarkerStyle {
    /// Filled circle (default)
    Circle,
    /// Filled square
    Square,
    /// Filled triangle pointing up
    Triangle,
    /// Filled triangle pointing down
    TriangleDown,
    /// Filled diamond
    Diamond,
    /// Plus sign (+)
    Plus,
    /// X mark
    Cross,
    /// Eight-pointed asterisk star (`+` and `x` overlaid)
    ///
    /// Drawn as four strokes, so it has no interior: it is not
    /// [`is_filled`](MarkerStyle::is_filled) and it
    /// [takes no edge](MarkerStyle::takes_edge).
    Star,
    /// Hollow circle
    CircleOpen,
    /// Hollow square
    SquareOpen,
    /// Hollow triangle
    TriangleOpen,
    /// Hollow diamond
    DiamondOpen,
}

impl MarkerStyle {
    /// Get a descriptive name for the marker style
    pub fn name(&self) -> &'static str {
        match self {
            MarkerStyle::Circle => "circle",
            MarkerStyle::Square => "square",
            MarkerStyle::Triangle => "triangle",
            MarkerStyle::TriangleDown => "triangle-down",
            MarkerStyle::Diamond => "diamond",
            MarkerStyle::Plus => "plus",
            MarkerStyle::Cross => "cross",
            MarkerStyle::Star => "star",
            MarkerStyle::CircleOpen => "circle-open",
            MarkerStyle::SquareOpen => "square-open",
            MarkerStyle::TriangleOpen => "triangle-open",
            MarkerStyle::DiamondOpen => "diamond-open",
        }
    }

    /// Check if this is a filled marker
    ///
    /// True only for the closed shapes every backend paints as a filled area.
    /// [`MarkerStyle::Star`] is excluded: it is an asterisk built from four
    /// strokes, with no interior to fill.
    pub fn is_filled(&self) -> bool {
        matches!(
            self,
            MarkerStyle::Circle
                | MarkerStyle::Square
                | MarkerStyle::Triangle
                | MarkerStyle::TriangleDown
                | MarkerStyle::Diamond
        )
    }

    /// Check whether an edge (rim) can be stroked around this marker
    ///
    /// Only a closed filled shape has an interior for an edge to bound, so this
    /// is exactly [`is_filled`](Self::is_filled): the open styles already *are*
    /// an outline, and the line-drawn styles (plus, cross, star) have no
    /// interior at all.
    ///
    /// Every backend asks this same question, so it lives here rather than
    /// being re-derived per renderer.
    pub fn takes_edge(&self) -> bool {
        self.is_filled()
    }

    /// Check if this is a hollow/open marker
    pub fn is_hollow(&self) -> bool {
        matches!(
            self,
            MarkerStyle::CircleOpen
                | MarkerStyle::SquareOpen
                | MarkerStyle::TriangleOpen
                | MarkerStyle::DiamondOpen
        )
    }

    /// Check if this is a line-based marker (plus, cross)
    pub fn is_line_based(&self) -> bool {
        matches!(self, MarkerStyle::Plus | MarkerStyle::Cross)
    }
}

impl Default for MarkerStyle {
    /// Default marker style is filled circle
    fn default() -> Self {
        MarkerStyle::Circle
    }
}

impl std::fmt::Display for MarkerStyle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_line_style_dash_arrays() {
        assert_eq!(LineStyle::Solid.to_dash_array(), None);
        assert_eq!(LineStyle::Dashed.to_dash_array(), Some(vec![5.0, 5.0]));
        assert_eq!(LineStyle::Dotted.to_dash_array(), Some(vec![1.0, 2.0]));
        assert_eq!(
            LineStyle::DashDot.to_dash_array(),
            Some(vec![8.0, 3.0, 1.0, 3.0])
        );
    }

    #[test]
    fn test_custom_line_style() {
        let custom = LineStyle::custom(vec![4.0, 2.0, 1.0, 2.0]);
        assert_eq!(custom.to_dash_array(), Some(vec![4.0, 2.0, 1.0, 2.0]));

        // Test filtering of zero values and abs() of negative values
        // -2.0 becomes 2.0 (abs), 0.0 is filtered out
        let custom_filtered = LineStyle::custom(vec![4.0, 0.0, -2.0, 3.0]);
        assert_eq!(custom_filtered.to_dash_array(), Some(vec![4.0, 2.0, 3.0]));

        // Test empty pattern
        let empty = LineStyle::custom(Vec::<f32>::new());
        assert_eq!(empty.to_dash_array(), None);
    }

    #[test]
    fn test_line_style_properties() {
        assert!(LineStyle::Solid.is_solid());
        assert!(!LineStyle::Dashed.is_solid());
        assert!(LineStyle::Dashed.is_patterned());
        assert!(!LineStyle::Solid.is_patterned());
    }

    #[test]
    fn test_pattern_length() {
        assert_eq!(LineStyle::Solid.pattern_length(), 0.0);
        assert_eq!(LineStyle::Dashed.pattern_length(), 10.0); // 5.0 + 5.0
        assert_eq!(LineStyle::DashDot.pattern_length(), 15.0); // 8.0 + 3.0 + 1.0 + 3.0
    }

    #[test]
    fn test_line_style_scaling() {
        let scaled_dashed = LineStyle::Dashed.scaled(2.0);
        assert_eq!(scaled_dashed.to_dash_array(), Some(vec![10.0, 10.0]));

        let scaled_solid = LineStyle::Solid.scaled(2.0);
        assert_eq!(scaled_solid, LineStyle::Solid); // Solid unchanged

        let custom = LineStyle::custom(vec![2.0, 1.0]);
        let scaled_custom = custom.scaled(3.0);
        assert_eq!(scaled_custom.to_dash_array(), Some(vec![6.0, 3.0]));
    }

    #[test]
    fn test_line_style_names() {
        assert_eq!(LineStyle::Solid.name(), "solid");
        assert_eq!(LineStyle::DashDot.name(), "dash-dot");
        assert_eq!(LineStyle::custom(vec![1.0, 2.0]).name(), "custom");
    }

    #[test]
    fn test_marker_style_properties() {
        assert!(MarkerStyle::Circle.is_filled());
        assert!(!MarkerStyle::CircleOpen.is_filled());
        assert!(MarkerStyle::CircleOpen.is_hollow());
        assert!(!MarkerStyle::Circle.is_hollow());
        assert!(MarkerStyle::Plus.is_line_based());
        assert!(!MarkerStyle::Circle.is_line_based());
    }

    #[test]
    fn test_marker_takes_edge_only_for_closed_filled_shapes() {
        for style in [
            MarkerStyle::Circle,
            MarkerStyle::Square,
            MarkerStyle::Triangle,
            MarkerStyle::TriangleDown,
            MarkerStyle::Diamond,
        ] {
            assert!(style.takes_edge(), "{} encloses an area", style.name());
        }
        for style in [
            MarkerStyle::CircleOpen,
            MarkerStyle::SquareOpen,
            MarkerStyle::TriangleOpen,
            MarkerStyle::DiamondOpen,
            MarkerStyle::Plus,
            MarkerStyle::Cross,
            // Star is an asterisk of four strokes, so it has no interior.
            MarkerStyle::Star,
        ] {
            assert!(
                !style.takes_edge(),
                "{} has no interior for an edge to bound",
                style.name()
            );
            assert!(
                !style.is_filled(),
                "{} must not claim to be filled",
                style.name()
            );
        }
    }

    #[test]
    fn test_marker_style_names() {
        assert_eq!(MarkerStyle::Circle.name(), "circle");
        assert_eq!(MarkerStyle::TriangleDown.name(), "triangle-down");
        assert_eq!(MarkerStyle::CircleOpen.name(), "circle-open");
    }

    #[test]
    fn test_defaults() {
        assert_eq!(LineStyle::default(), LineStyle::Solid);
        assert_eq!(MarkerStyle::default(), MarkerStyle::Circle);
    }

    #[test]
    fn test_display() {
        assert_eq!(LineStyle::Solid.to_string(), "solid");
        assert_eq!(
            LineStyle::custom(vec![1.0, 2.5]).to_string(),
            "custom(1.0, 2.5)"
        );
        assert_eq!(MarkerStyle::Circle.to_string(), "circle");
    }
}