use std::collections::HashMap;
pub trait IntoRowOffset {
fn into_row_offset(self) -> Option<f64>;
}
impl IntoRowOffset for f64 {
fn into_row_offset(self) -> Option<f64> { Some(self) }
}
impl IntoRowOffset for Option<f64> {
fn into_row_offset(self) -> Option<f64> { self }
}
fn canonical_rotation(s: &str) -> String {
let n = s.len();
if n == 0 { return String::new(); }
let doubled = format!("{}{}", s, s);
(0..n)
.map(|i| &doubled[i..i + n])
.min()
.expect("range 0..n is non-empty when n > 0")
.to_string()
}
#[derive(Debug, Clone)]
pub struct BrickTemplate {
pub template: HashMap<char, String>,
}
impl Default for BrickTemplate {
fn default() -> Self { Self::new() }
}
impl BrickTemplate {
pub fn new() -> Self {
Self {
template: HashMap::new(),
}
}
pub fn dna(mut self) -> Self {
self.template.insert('A', "rgb(0,150,0)".into());
self.template.insert('C', "rgb(0,0,255)".into());
self.template.insert('G', "rgb(209,113,5)".into());
self.template.insert('T', "rgb(255,0,0)".into());
self
}
pub fn rna(mut self) -> Self {
self.template.insert('A', "green".into());
self.template.insert('C', "blue".into());
self.template.insert('G', "orange".into());
self.template.insert('U', "red".into());
self
}
}
#[derive(Debug, Clone)]
pub struct BrickPlot {
pub sequences: Vec<String>,
pub names: Vec<String>,
pub strigars: Option<Vec<(String, String)>>,
pub motifs: Option<HashMap<char, String>>,
pub strigar_exp: Option<Vec<String>>,
pub template: Option<HashMap<char, String>>,
pub x_offset: f64,
pub x_offsets: Option<Vec<Option<f64>>>,
pub x_origin: f64,
pub motif_lengths: Option<HashMap<char, usize>>,
pub show_values: bool,
}
impl Default for BrickPlot {
fn default() -> Self { Self::new() }
}
impl BrickPlot {
pub fn new() -> Self {
Self {
sequences: vec![],
names: vec![],
strigars: None,
motifs: None,
strigar_exp: None,
template: Some(HashMap::new()),
motif_lengths: None,
x_offset: 0.0,
x_offsets: None,
x_origin: 0.0,
show_values: false,
}
}
pub fn with_sequences<T, I>(mut self, sequences: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
self.sequences = sequences.into_iter().map(|x| x.into()).collect();
self
}
pub fn with_names<T, I>(mut self, names: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
self.names = names.into_iter().map(|x| x.into()).collect();
self
}
pub fn with_strigars<T, U, I>(mut self, strigars: I) -> Self
where
I: IntoIterator<Item = (T, U)>,
T: Into<String>,
U: Into<String>,
{
self.strigars = Some(strigars.into_iter()
.map(|(motif, strigar)| (motif.into(), strigar.into()))
.collect());
let parse_gap = |seg: &str| -> Option<usize> {
let s = seg.trim();
if s.ends_with('@') && s.len() > 1 {
let num_part = &s[..s.len() - 1];
if num_part.chars().all(|c| c.is_ascii_digit()) {
return num_part.parse().ok();
}
}
None
};
let parse_motif_seg = |seg: &str| -> HashMap<char, String> {
seg.split(',')
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.filter_map(|pair| {
let mut parts = pair.splitn(2, ':');
let kmer = parts.next()?.trim();
let letter_field = parts.next()?.trim();
let letter = letter_field.chars().next()?;
Some((letter, kmer.to_string()))
})
.collect()
};
let strigars_ref = self.strigars.as_ref().expect("strigars just set");
let mut canonical_freq: HashMap<String, usize> = HashMap::new();
let mut rotation_freq: HashMap<String, HashMap<String, usize>> = HashMap::new();
for (motif_str, strigar_str) in strigars_ref {
let motif_segs: Vec<&str> = motif_str.split('|').map(str::trim)
.filter(|s| !s.is_empty()).collect();
let strigar_segs: Vec<&str> = strigar_str.split('|').map(str::trim)
.filter(|s| !s.is_empty()).collect();
let mut motif_idx = 0usize;
for strigar_seg in &strigar_segs {
if parse_gap(strigar_seg).is_some() {
let is_small_gap = motif_idx < motif_segs.len()
&& motif_segs[motif_idx].trim_start_matches(|c: char| c.is_whitespace())
.starts_with("@:");
if is_small_gap { motif_idx += 1; }
} else {
if motif_idx < motif_segs.len() {
for (_, kmer) in parse_motif_seg(motif_segs[motif_idx]) {
let canon = canonical_rotation(&kmer);
*canonical_freq.entry(canon.clone()).or_insert(0) += 1;
*rotation_freq.entry(canon).or_default()
.entry(kmer).or_insert(0) += 1;
}
motif_idx += 1;
}
}
}
}
let mut sorted_canonicals: Vec<(String, usize)> = canonical_freq.into_iter().collect();
sorted_canonicals.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let mut canonical_to_global: HashMap<String, char> = HashMap::new();
let mut global_to_display: HashMap<char, String> = HashMap::new();
let mut global_to_length: HashMap<char, usize> = HashMap::new();
for (idx, (canon, _freq)) in sorted_canonicals.iter().enumerate() {
let global_letter = (b'A' + idx as u8) as char;
canonical_to_global.insert(canon.clone(), global_letter);
let rotations = rotation_freq.get(canon).expect("canon derived from rotation_freq keys");
let display = rotations.iter()
.max_by(|a, b| a.1.cmp(b.1).then_with(|| b.0.cmp(a.0)))
.expect("rotation_freq entry is non-empty")
.0.clone();
global_to_display.insert(global_letter, display.clone());
global_to_length.insert(global_letter, display.len());
}
let mut expanded_strigars: Vec<String> = Vec::new();
let mut has_gaps = false;
for (motif_str, strigar_str) in strigars_ref {
let motif_segs: Vec<&str> = motif_str.split('|').map(str::trim)
.filter(|s| !s.is_empty()).collect();
let strigar_segs: Vec<&str> = strigar_str.split('|').map(str::trim)
.filter(|s| !s.is_empty()).collect();
let mut expanded = String::new();
let mut motif_idx = 0usize;
for strigar_seg in &strigar_segs {
if let Some(gap_n) = parse_gap(strigar_seg) {
let is_small_gap = motif_idx < motif_segs.len()
&& motif_segs[motif_idx].trim_start_matches(|c: char| c.is_whitespace())
.starts_with("@:");
let gap_nt = if is_small_gap {
let gap_seq = motif_segs[motif_idx].split_once(':')
.map(|x| x.1.trim()).unwrap_or("");
motif_idx += 1;
gap_seq.len() * gap_n } else {
gap_n };
for _ in 0..gap_nt { expanded.push('@'); }
has_gaps = true;
} else {
if motif_idx < motif_segs.len() {
let local_map = parse_motif_seg(motif_segs[motif_idx]);
motif_idx += 1;
let mut local_to_global: HashMap<char, char> = HashMap::new();
for (local_letter, kmer) in &local_map {
let canon = canonical_rotation(kmer);
if let Some(&global) = canonical_to_global.get(&canon) {
local_to_global.insert(*local_letter, global);
}
}
let mut chars = strigar_seg.chars().peekable();
while chars.peek().is_some() {
let mut num_str = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() { num_str.push(chars.next().unwrap()); }
else { break; }
}
if let Some(letter_char) = chars.next() {
let count: usize = num_str.parse()
.expect("STRIGAR repeat count is a valid integer");
let global = *local_to_global.get(&letter_char)
.unwrap_or(&letter_char);
expanded.push_str(&global.to_string().repeat(count));
}
}
}
}
}
expanded_strigars.push(expanded);
}
if has_gaps {
global_to_display.entry('@').or_insert_with(|| "@".to_string());
}
let motif_colors: &[&str] = &[
"rgb(31,119,180)", "rgb(255,127,14)", "rgb(44,160,44)", "rgb(214,39,40)", "rgb(148,103,189)", "rgb(140,86,75)", "rgb(227,119,194)", "rgb(127,127,127)", "rgb(188,189,34)", "rgb(23,190,207)", ];
let mut auto_template: HashMap<char, String> = HashMap::new();
for (idx, (canon, _)) in sorted_canonicals.iter().enumerate() {
let global_letter = canonical_to_global[canon];
auto_template.insert(global_letter, motif_colors[idx % motif_colors.len()].to_string());
}
if has_gaps {
auto_template.insert('@', "rgb(200,200,200)".to_string());
}
self.template = Some(auto_template);
self.motifs = Some(global_to_display);
self.strigar_exp = Some(expanded_strigars);
self.motif_lengths = Some(global_to_length);
self
}
pub fn with_template(mut self, template: HashMap<char, String>) -> Self {
self.template = Some(template);
self
}
pub fn with_x_offset(mut self, x_offset: f64) -> Self {
self.x_offset = x_offset;
self
}
pub fn with_x_offsets<T, I>(mut self, offsets: I) -> Self
where
I: IntoIterator<Item = T>,
T: IntoRowOffset,
{
self.x_offsets = Some(offsets.into_iter().map(|x| x.into_row_offset()).collect());
self
}
pub fn with_start_positions<T, I>(self, positions: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<f64>,
{
let offsets: Vec<Option<f64>> = positions.into_iter()
.map(|p| Some(-p.into()))
.collect();
self.with_x_offsets(offsets)
}
pub fn with_x_origin(mut self, origin: f64) -> Self {
self.x_origin = origin;
self
}
pub fn with_values(mut self) -> Self {
self.show_values = true;
self
}
}