fastqc_rust/modules/
n_content.rs1use std::io;
5
6use crate::config::{Limits, LimitsExt};
7use crate::modules::QCModule;
8use crate::report::charts::line_graph::{render_line_graph, LineGraphData};
9use crate::report::charts::scaled_chart_width;
10use crate::sequence::Sequence;
11use crate::utils::base_counts::{BASE_INDEX, IDX_N};
12use crate::utils::base_group::BaseGroup;
13use crate::utils::format::java_format_double;
14
15pub struct NContent {
16 n_counts: Vec<u64>,
17 not_n_counts: Vec<u64>,
18 nogroup: bool,
19 expgroup: bool,
20 limits: Limits,
21}
22
23impl NContent {
24 pub fn new(limits: &Limits, nogroup: bool, expgroup: bool) -> Self {
25 NContent {
26 n_counts: Vec::new(),
27 not_n_counts: Vec::new(),
28 nogroup,
29 expgroup,
30 limits: limits.clone(),
31 }
32 }
33
34 fn calculate(&self) -> NContentData {
35 let groups = BaseGroup::make_base_groups(self.n_counts.len(), self.nogroup, self.expgroup);
36
37 let mut x_categories = Vec::with_capacity(groups.len());
38 let mut percentages = vec![0.0f64; groups.len()];
39
40 for (i, group) in groups.iter().enumerate() {
41 x_categories.push(group.label());
42
43 let mut n_count: u64 = 0;
44 let mut total: u64 = 0;
45
46 for bp in group.lower_count..=group.upper_count {
49 n_count += self.n_counts[bp];
50 total += self.n_counts[bp];
51 total += self.not_n_counts[bp];
52 }
53
54 percentages[i] = 100.0 * (n_count as f64 / total as f64);
56 }
57
58 NContentData {
59 x_categories,
60 percentages,
61 }
62 }
63}
64
65impl NContent {
66 fn build_chart_svg(&self) -> String {
67 let data = self.calculate();
68
69 render_line_graph(&LineGraphData {
71 width: scaled_chart_width(data.x_categories.len()),
72 data: vec![data.percentages],
73 min_y: 0.0,
74 max_y: 100.0,
75 x_label: "Position in read (bp)".to_string(),
76 series_names: vec!["%N".to_string()],
77 x_categories: data.x_categories,
78 title: "N content across all bases".to_string(),
79 })
80 }
81}
82
83impl QCModule for NContent {
84 fn cost_hint(&self) -> u32 {
85 8
86 }
87
88 fn process_sequence(&mut self, sequence: &Sequence) {
89 let seq = &sequence.sequence;
90
91 if self.n_counts.len() < seq.len() {
93 self.n_counts.resize(seq.len(), 0);
94 self.not_n_counts.resize(seq.len(), 0);
95 }
96
97 for (i, &b) in seq.iter().enumerate() {
99 if BASE_INDEX[b as usize] as usize == IDX_N {
100 self.n_counts[i] += 1;
101 } else {
102 self.not_n_counts[i] += 1;
103 }
104 }
105 }
106
107 fn name(&self) -> &str {
108 "Per base N content"
109 }
110
111 fn description(&self) -> &str {
112 "Shows the percentage of bases at each position which are not being called"
113 }
114
115 fn reset(&mut self) {
116 self.n_counts.clear();
117 self.not_n_counts.clear();
118 }
119
120 fn raises_error(&self) -> bool {
121 let threshold = self.limits.threshold("n_content\terror", 20.0);
122 let data = self.calculate();
123 data.percentages.iter().any(|&p| p > threshold)
124 }
125
126 fn raises_warning(&self) -> bool {
127 let threshold = self.limits.threshold("n_content\twarn", 5.0);
128 let data = self.calculate();
129 data.percentages.iter().any(|&p| p > threshold)
130 }
131
132 fn ignore_filtered_sequences(&self) -> bool {
133 true
134 }
135
136 fn ignore_in_report(&self) -> bool {
137 self.limits.is_ignored("n_content")
138 }
139
140 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
141 let data = self.calculate();
142
143 writeln!(writer, "#Base\tN-Count")?;
145
146 for i in 0..data.x_categories.len() {
147 writeln!(
148 writer,
149 "{}\t{}",
150 data.x_categories[i],
151 java_format_double(data.percentages[i]),
152 )?;
153 }
154
155 Ok(())
156 }
157
158 fn chart_image_name(&self) -> Option<&str> {
160 Some("per_base_n_content")
161 }
162 fn chart_alt_text(&self) -> Option<&str> {
163 Some("N content graph")
164 }
165 fn generate_chart_svg(&self) -> Option<String> {
166 Some(self.build_chart_svg())
167 }
168}
169
170struct NContentData {
171 x_categories: Vec<String>,
172 percentages: Vec<f64>,
173}