Skip to main content

fastqc_rust/report/charts/
quality_boxplot.rs

1// Quality box plot chart rendering
2// Corresponds to Graphs/QualityBoxPlot.java
3//
4// Generates SVG output that visually matches Java FastQC's QualityBoxPlot.
5// This is the signature FastQC chart showing per-base quality with colored zones.
6
7use super::{
8    render_centered_title, svg_footer, svg_header, svg_line, svg_rect_filled, svg_rect_stroked,
9    ChartColor, ChartLayout,
10};
11
12/// Parameters for drawing a quality box plot.
13pub struct QualityBoxPlotData {
14    pub means: Vec<f64>,
15    pub medians: Vec<f64>,
16    pub lower_quartile: Vec<f64>,
17    pub upper_quartile: Vec<f64>,
18    /// 10th percentile (bottom whisker)
19    pub lowest: Vec<f64>,
20    /// 90th percentile (top whisker)
21    pub highest: Vec<f64>,
22    pub min_y: f64,
23    pub max_y: f64,
24    pub y_interval: f64,
25    pub x_labels: Vec<String>,
26    pub title: String,
27}
28
29// Quality zone colors from QualityBoxPlot.java
30// These exact RGB values match the Java source.
31const GOOD: ChartColor = ChartColor::new(195, 230, 195);
32const BAD: ChartColor = ChartColor::new(230, 220, 195);
33const UGLY: ChartColor = ChartColor::new(230, 195, 195);
34const GOOD_DARK: ChartColor = ChartColor::new(175, 230, 175);
35const BAD_DARK: ChartColor = ChartColor::new(230, 215, 175);
36const UGLY_DARK: ChartColor = ChartColor::new(230, 175, 175);
37
38// Box fill is yellow (240,240,0)
39const BOX_FILL: ChartColor = ChartColor::new(240, 240, 0);
40// Median line is red (200,0,0)
41const MEDIAN_COLOR: ChartColor = ChartColor::new(200, 0, 0);
42// Mean line is blue (0,0,200)
43const MEAN_COLOR: ChartColor = ChartColor::new(0, 0, 200);
44
45/// Render a quality box plot as SVG.
46///
47/// Layout closely follows QualityBoxPlot.java:paint():
48/// - Same 40px top/bottom margins
49/// - Green (>28), Yellow (20-28), Red (<20) background zones
50/// - Alternating light/dark within zones
51/// - Yellow boxes for IQR, whiskers for 10th/90th percentile
52/// - Red median line, blue mean line connecting all positions
53pub fn render_quality_boxplot(params: &QualityBoxPlotData) -> String {
54    let layout = ChartLayout::new(params.min_y, params.max_y, params.y_interval);
55
56    let num_positions = params.means.len();
57    let base_width = layout.base_width(num_positions);
58
59    let mut svg = svg_header(layout.width, layout.height);
60
61    // Render shared elements: background, Y-axis labels, title, X-axis labels, axes
62    // Match Java's QualityBoxPlot.paint() element order
63    layout.render_background(&mut svg);
64    layout.render_y_labels(&mut svg);
65    render_centered_title(&mut svg, &params.title, layout.x_offset, layout.width);
66
67    let black = ChartColor::new(0, 0, 0);
68
69    // Zone backgrounds + x-labels interleaved per position (matching Java paint order)
70    let mut last_x_label_end: f64 = 0.0;
71    for i in 0..num_positions {
72        let x = layout.x_offset + base_width * i as f64;
73
74        // Alternating colors - odd positions get the lighter variant
75        let (ugly, bad, good) = if i % 2 != 0 {
76            (&UGLY, &BAD, &GOOD)
77        } else {
78            (&UGLY_DARK, &BAD_DARK, &GOOD_DARK)
79        };
80
81        // Red zone: from yStart to quality 20
82        let ugly_top = layout.get_y(20.0);
83        let ugly_bottom = layout.get_y(layout.y_start);
84        if ugly_bottom > ugly_top {
85            svg.push_str(&svg_rect_filled(
86                x,
87                ugly_top,
88                base_width,
89                ugly_bottom - ugly_top,
90                ugly,
91            ));
92        }
93
94        // Yellow zone: from quality 20 to 28
95        let bad_top = layout.get_y(28.0);
96        let bad_bottom = layout.get_y(20.0);
97        if bad_bottom > bad_top {
98            svg.push_str(&svg_rect_filled(
99                x,
100                bad_top,
101                base_width,
102                bad_bottom - bad_top,
103                bad,
104            ));
105        }
106
107        // Green zone: from quality 28 to maxY
108        let good_top = layout.get_y(params.max_y);
109        let good_bottom = layout.get_y(28.0);
110        if good_bottom > good_top {
111            svg.push_str(&svg_rect_filled(
112                x,
113                good_top,
114                base_width,
115                good_bottom - good_top,
116                good,
117            ));
118        }
119
120        // X-category label for this position (interleaved with zone rects)
121        if i < params.x_labels.len() {
122            last_x_label_end = layout.render_x_category_label_at(
123                &mut svg,
124                &params.x_labels[i],
125                i,
126                base_width,
127                last_x_label_end,
128            );
129        }
130    }
131
132    // Axes and x-axis label (after zones + x-labels, matching Java)
133    layout.render_axes(&mut svg);
134    layout.render_x_axis_label(&mut svg, "Position in read (bp)");
135
136    // Draw box plots for each position
137    for i in 0..num_positions {
138        let box_x = layout.x_offset + base_width * i as f64;
139        let box_top_y = layout.get_y(params.upper_quartile[i]);
140        let box_bottom_y = layout.get_y(params.lower_quartile[i]);
141        let upper_whisker_y = layout.get_y(params.highest[i]);
142        let lower_whisker_y = layout.get_y(params.lowest[i]);
143        let median_y = layout.get_y(params.medians[i]);
144        let center_x = box_x + base_width / 2.0;
145
146        // Box body (yellow fill, black stroke), inset 2px from each side
147        let box_inset = 2.0;
148        let box_w = base_width - 4.0;
149        let box_h = box_bottom_y - box_top_y;
150        svg.push_str(&svg_rect_filled(
151            box_x + box_inset,
152            box_top_y,
153            box_w,
154            box_h,
155            &BOX_FILL,
156        ));
157        svg.push_str(&svg_rect_stroked(
158            box_x + box_inset,
159            box_top_y,
160            box_w,
161            box_h,
162            &black,
163        ));
164
165        // Upper whisker - vertical line from box top to whisker, horizontal cap
166        svg.push_str(&svg_line(
167            center_x,
168            upper_whisker_y,
169            center_x,
170            box_top_y,
171            &black,
172            1.0,
173        ));
174        svg.push_str(&svg_line(
175            box_x + box_inset,
176            upper_whisker_y,
177            box_x + base_width - box_inset,
178            upper_whisker_y,
179            &black,
180            1.0,
181        ));
182
183        // Lower whisker
184        svg.push_str(&svg_line(
185            center_x,
186            lower_whisker_y,
187            center_x,
188            box_bottom_y,
189            &black,
190            1.0,
191        ));
192        svg.push_str(&svg_line(
193            box_x + box_inset,
194            lower_whisker_y,
195            box_x + base_width - box_inset,
196            lower_whisker_y,
197            &black,
198            1.0,
199        ));
200
201        // Median line (red)
202        svg.push_str(&svg_line(
203            box_x + box_inset,
204            median_y,
205            box_x + base_width - box_inset,
206            median_y,
207            &MEDIAN_COLOR,
208            1.0,
209        ));
210    }
211
212    // Mean line (blue), connecting all positions as individual line segments
213    let half_bw = layout.half_base_width(num_positions);
214    if num_positions >= 2 {
215        let mut prev_x = 0i32;
216        let mut prev_y = 0i32;
217        for i in 0..num_positions {
218            let x = (half_bw + layout.x_offset + (base_width * i as f64)) as i32;
219            let y = layout.get_y(params.means[i]) as i32;
220            if i > 0 {
221                svg.push_str(&format!(
222                    "<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"{}\" stroke-width=\"1\"/>\n",
223                    prev_x, prev_y, x, y, MEAN_COLOR.to_rgb_string()
224                ));
225            }
226            prev_x = x;
227            prev_y = y;
228        }
229    }
230
231    svg.push_str(svg_footer());
232    svg
233}