Skip to main content

bland/
markers.rs

1//! Scatter-point markers in the monochrome technical-drawing tradition.
2//!
3//! Each marker is drawn as a self-contained SVG fragment at a pixel
4//! point. Presets alternate between *open* (stroke-only) and *filled*
5//! shapes so overlapping series stay distinguishable without color.
6
7use crate::svg;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Marker {
11    CircleOpen,
12    CircleFilled,
13    SquareOpen,
14    SquareFilled,
15    TriangleOpen,
16    TriangleFilled,
17    DiamondOpen,
18    DiamondFilled,
19    Cross,
20    Plus,
21    Asterisk,
22    Dot,
23}
24
25impl Marker {
26    pub const CYCLE: [Marker; 12] = [
27        Marker::CircleOpen,
28        Marker::SquareOpen,
29        Marker::TriangleOpen,
30        Marker::DiamondOpen,
31        Marker::Cross,
32        Marker::Plus,
33        Marker::CircleFilled,
34        Marker::SquareFilled,
35        Marker::TriangleFilled,
36        Marker::DiamondFilled,
37        Marker::Asterisk,
38        Marker::Dot,
39    ];
40
41    pub fn cycle(index: usize) -> Marker {
42        Self::CYCLE[index % Self::CYCLE.len()]
43    }
44}
45
46/// Append SVG for the marker centered at `(cx, cy)` to `buf`.
47///
48/// `size` is a radius-like parameter in px. `stroke_width` controls open
49/// markers; filled markers ignore it.
50pub fn draw(buf: &mut String, marker: Marker, cx: f64, cy: f64, size: f64, stroke_width: f64) {
51    match marker {
52        Marker::CircleOpen => {
53            let attrs = format!(
54                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
55                svg::num(stroke_width)
56            );
57            svg::circle(buf, cx, cy, size, &attrs);
58        }
59        Marker::CircleFilled => {
60            svg::circle(buf, cx, cy, size, " fill=\"black\"");
61        }
62        Marker::SquareOpen => {
63            let attrs = format!(
64                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
65                svg::num(stroke_width)
66            );
67            svg::rect(buf, cx - size, cy - size, 2.0 * size, 2.0 * size, &attrs);
68        }
69        Marker::SquareFilled => {
70            svg::rect(
71                buf,
72                cx - size,
73                cy - size,
74                2.0 * size,
75                2.0 * size,
76                " fill=\"black\"",
77            );
78        }
79        Marker::TriangleOpen => {
80            let pts = triangle_points(cx, cy, size);
81            let attrs = format!(
82                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\" stroke-linejoin=\"miter\"",
83                svg::num(stroke_width)
84            );
85            svg::polygon(buf, &pts, &attrs);
86        }
87        Marker::TriangleFilled => {
88            let pts = triangle_points(cx, cy, size);
89            svg::polygon(buf, &pts, " fill=\"black\"");
90        }
91        Marker::DiamondOpen => {
92            let pts = diamond_points(cx, cy, size);
93            let attrs = format!(
94                " fill=\"none\" stroke=\"black\" stroke-width=\"{}\"",
95                svg::num(stroke_width)
96            );
97            svg::polygon(buf, &pts, &attrs);
98        }
99        Marker::DiamondFilled => {
100            let pts = diamond_points(cx, cy, size);
101            svg::polygon(buf, &pts, " fill=\"black\"");
102        }
103        Marker::Cross => {
104            let attrs = format!(
105                " stroke=\"black\" stroke-width=\"{}\"",
106                svg::num(stroke_width)
107            );
108            svg::line(buf, cx - size, cy - size, cx + size, cy + size, &attrs);
109            svg::line(buf, cx - size, cy + size, cx + size, cy - size, &attrs);
110        }
111        Marker::Plus => {
112            let attrs = format!(
113                " stroke=\"black\" stroke-width=\"{}\"",
114                svg::num(stroke_width)
115            );
116            svg::line(buf, cx - size, cy, cx + size, cy, &attrs);
117            svg::line(buf, cx, cy - size, cx, cy + size, &attrs);
118        }
119        Marker::Asterisk => {
120            let attrs = format!(
121                " stroke=\"black\" stroke-width=\"{}\"",
122                svg::num(stroke_width)
123            );
124            svg::line(buf, cx - size, cy, cx + size, cy, &attrs);
125            svg::line(
126                buf,
127                cx - size * 0.5,
128                cy - size * 0.866,
129                cx + size * 0.5,
130                cy + size * 0.866,
131                &attrs,
132            );
133            svg::line(
134                buf,
135                cx - size * 0.5,
136                cy + size * 0.866,
137                cx + size * 0.5,
138                cy - size * 0.866,
139                &attrs,
140            );
141        }
142        Marker::Dot => {
143            svg::circle(buf, cx, cy, 0.8, " fill=\"black\"");
144        }
145    }
146}
147
148fn triangle_points(cx: f64, cy: f64, s: f64) -> [(f64, f64); 3] {
149    [
150        (cx, cy - s),
151        (cx + s * 0.866, cy + s * 0.5),
152        (cx - s * 0.866, cy + s * 0.5),
153    ]
154}
155
156fn diamond_points(cx: f64, cy: f64, s: f64) -> [(f64, f64); 4] {
157    [(cx, cy - s), (cx + s, cy), (cx, cy + s), (cx - s, cy)]
158}