Skip to main content

mermaid_text/
quadrant_chart.rs

1//! Data model for Mermaid `quadrantChart` diagrams.
2//!
3//! A quadrant chart is a 2x2 priority matrix with labeled quadrants and
4//! plotted points on a unit-square coordinate system ([0, 1] x [0, 1]).
5//! Quadrant numbering follows Mermaid convention: Q1 = top-right,
6//! Q2 = top-left, Q3 = bottom-left, Q4 = bottom-right.
7//!
8//! Example source:
9//!
10//! ```text
11//! quadrantChart
12//!     title Reach and engagement of campaigns
13//!     x-axis Low Reach --> High Reach
14//!     y-axis Low Engagement --> High Engagement
15//!     quadrant-1 We should expand
16//!     quadrant-2 Need to promote
17//!     quadrant-3 Re-evaluate
18//!     quadrant-4 May be improved
19//!     Campaign A: [0.3, 0.6]
20//!     Campaign B: [0.45, 0.23]
21//! ```
22//!
23//! Constructed by [`crate::parser::quadrant_chart::parse`] and consumed by
24//! [`crate::render::quadrant_chart::render`].
25//!
26//! ## Phase 1 limitations
27//!
28//! - Custom point styling (colour, radius) is not supported; all points render
29//!   as a `·` marker followed by the point name and coordinates.
30//! - Background quadrant colours/gradients are not rendered.
31//! - `accDescr` / `accTitle` accessibility metadata is silently ignored.
32//! - Points that are close together may overlap in the text output.
33
34/// Axis label pair for one dimension of the quadrant chart.
35///
36/// `low` is the label at the origin-side of the axis;
37/// `high` is the label at the far end.
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct AxisLabels {
40    pub low: String,
41    pub high: String,
42}
43
44/// Labels for each of the four quadrants.
45///
46/// Mermaid numbers quadrants starting from top-right and going
47/// counter-clockwise: Q1 = top-right, Q2 = top-left,
48/// Q3 = bottom-left, Q4 = bottom-right.
49#[derive(Debug, Clone, PartialEq, Eq, Default)]
50pub struct QuadrantLabels {
51    pub q1: Option<String>,
52    pub q2: Option<String>,
53    pub q3: Option<String>,
54    pub q4: Option<String>,
55}
56
57/// A single plotted data point in the quadrant chart.
58///
59/// Coordinates `x` and `y` must be in [0, 1]; the parser rejects values
60/// outside this range. Overlapping points are possible in Phase 1 when two
61/// points map to the same terminal cell.
62#[derive(Debug, Clone, PartialEq)]
63pub struct QuadrantPoint {
64    pub name: String,
65    /// Horizontal position in [0, 1]; 0 = left edge, 1 = right edge.
66    pub x: f64,
67    /// Vertical position in [0, 1]; 0 = bottom edge, 1 = top edge.
68    pub y: f64,
69}
70
71/// A parsed `quadrantChart` diagram.
72///
73/// Constructed by [`crate::parser::quadrant_chart::parse`] and consumed by
74/// [`crate::render::quadrant_chart::render`].
75#[derive(Debug, Clone, PartialEq, Default)]
76pub struct QuadrantChart {
77    /// Optional diagram title.
78    pub title: Option<String>,
79    /// Optional x-axis labels (low end and high end).
80    pub x_axis: Option<AxisLabels>,
81    /// Optional y-axis labels (low end and high end).
82    pub y_axis: Option<AxisLabels>,
83    /// Labels for each of the four quadrants (all optional).
84    pub quadrants: QuadrantLabels,
85    /// Data points to plot on the chart.
86    pub points: Vec<QuadrantPoint>,
87}
88
89impl QuadrantChart {
90    /// Total number of data points in the chart.
91    pub fn point_count(&self) -> usize {
92        self.points.len()
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Tests
98// ---------------------------------------------------------------------------
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn default_chart_has_zero_points() {
106        let chart = QuadrantChart::default();
107        assert_eq!(chart.point_count(), 0);
108        assert!(chart.title.is_none());
109        assert!(chart.x_axis.is_none());
110        assert!(chart.y_axis.is_none());
111        assert!(chart.quadrants.q1.is_none());
112        assert!(chart.quadrants.q4.is_none());
113        assert!(chart.points.is_empty());
114    }
115
116    #[test]
117    fn point_count_reflects_number_of_points() {
118        let chart = QuadrantChart {
119            points: vec![
120                QuadrantPoint {
121                    name: "A".to_string(),
122                    x: 0.3,
123                    y: 0.6,
124                },
125                QuadrantPoint {
126                    name: "B".to_string(),
127                    x: 0.7,
128                    y: 0.2,
129                },
130                QuadrantPoint {
131                    name: "C".to_string(),
132                    x: 0.5,
133                    y: 0.5,
134                },
135            ],
136            ..Default::default()
137        };
138        assert_eq!(chart.point_count(), 3);
139    }
140
141    #[test]
142    fn equality_holds_for_identical_charts() {
143        let a = QuadrantChart {
144            title: Some("My Chart".to_string()),
145            x_axis: Some(AxisLabels {
146                low: "Low".to_string(),
147                high: "High".to_string(),
148            }),
149            points: vec![QuadrantPoint {
150                name: "P".to_string(),
151                x: 0.5,
152                y: 0.5,
153            }],
154            ..Default::default()
155        };
156        let b = a.clone();
157        assert_eq!(a, b);
158
159        let c = QuadrantChart {
160            title: Some("Other".to_string()),
161            ..Default::default()
162        };
163        assert_ne!(a, c);
164    }
165
166    #[test]
167    fn partial_eq_works_for_f64_coordinates() {
168        let p1 = QuadrantPoint {
169            name: "X".to_string(),
170            x: 0.123_456_789,
171            y: 0.987_654_321,
172        };
173        let p2 = p1.clone();
174        assert_eq!(p1, p2);
175
176        let p3 = QuadrantPoint {
177            name: "X".to_string(),
178            x: 0.123_456_789,
179            y: 0.1,
180        };
181        assert_ne!(p1, p3);
182    }
183}