|
|
@@ -8,7 +8,50 @@ use std::{
|
|
|
use csv::ReaderBuilder;
|
|
|
use log::info;
|
|
|
|
|
|
-use crate::cytoband::Range;
|
|
|
+use crate::cytoband::{read_ranges, Range};
|
|
|
+
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct CircosConfig {
|
|
|
+ pub cytobands_bed: String,
|
|
|
+ pub r: f64,
|
|
|
+ pub cx: f64,
|
|
|
+ pub cy: f64,
|
|
|
+ pub angle_start: f64,
|
|
|
+ pub angle_end: f64,
|
|
|
+ pub gap: f64,
|
|
|
+}
|
|
|
+
|
|
|
+impl Default for CircosConfig {
|
|
|
+ fn default() -> Self {
|
|
|
+ let r = 1050.0;
|
|
|
+ let cx = 0.0;
|
|
|
+ let cy = 0.0;
|
|
|
+ let angle_start = -std::f64::consts::FRAC_PI_2;
|
|
|
+ let angle_end = angle_start + 2.0 * std::f64::consts::PI;
|
|
|
+ let gap = 0.012 * std::f64::consts::PI;
|
|
|
+
|
|
|
+ Self {
|
|
|
+ cytobands_bed: "/data/ref/hs1/cytoBandMapped.bed".to_string(),
|
|
|
+ r,
|
|
|
+ cx,
|
|
|
+ cy,
|
|
|
+ angle_start,
|
|
|
+ angle_end,
|
|
|
+ gap,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl CircosConfig {
|
|
|
+ /// Rotate the whole configuration by a given angle in degrees.
|
|
|
+ /// Positive values = clockwise, negative = counter-clockwise.
|
|
|
+ pub fn rotate(&mut self, degrees: f64) {
|
|
|
+ let radians = degrees.to_radians();
|
|
|
+ self.angle_start -= radians;
|
|
|
+ self.angle_end -= radians;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
const CAP_MINOR_RATIO: f64 = 0.3; // 1/3 of thickness
|
|
|
|
|
|
@@ -296,14 +339,17 @@ pub fn classic_caps(i: usize, len: usize) -> CapStyle {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[derive(Debug)]
|
|
|
+#[derive(Debug, Default)]
|
|
|
pub struct Circos {
|
|
|
pub tracks: Vec<Track>,
|
|
|
+ pub ref_track: Option<usize>,
|
|
|
pub svg_elements: Vec<String>,
|
|
|
pub min_x: f64,
|
|
|
pub min_y: f64,
|
|
|
pub max_x: f64,
|
|
|
pub max_y: f64,
|
|
|
+ pub chroms_ids: HashMap<String, usize>,
|
|
|
+ pub config: CircosConfig,
|
|
|
}
|
|
|
|
|
|
struct RenderState {
|
|
|
@@ -318,14 +364,165 @@ impl Circos {
|
|
|
pub fn new() -> Self {
|
|
|
Self {
|
|
|
tracks: Vec::new(),
|
|
|
+ ref_track: None,
|
|
|
svg_elements: Vec::new(),
|
|
|
min_x: f64::INFINITY,
|
|
|
min_y: f64::INFINITY,
|
|
|
max_x: f64::NEG_INFINITY,
|
|
|
max_y: f64::NEG_INFINITY,
|
|
|
+ chroms_ids: HashMap::new(),
|
|
|
+ config: CircosConfig::default(),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ pub fn new_with_chrom(chroms: &[String], cfg: CircosConfig) -> anyhow::Result<Self> {
|
|
|
+ let mut circos = Circos::default();
|
|
|
+ circos.config = cfg.clone();
|
|
|
+
|
|
|
+ let mut track = Track::new(cfg.cx, cfg.cy, cfg.angle_start, cfg.angle_end, cfg.gap);
|
|
|
+
|
|
|
+ for (set_idx, name) in chroms.iter().enumerate() {
|
|
|
+ let ranges = read_ranges(&cfg.cytobands_bed, name)?;
|
|
|
+
|
|
|
+ track.add_range_set((*name).to_string(), ranges.clone());
|
|
|
+ track.add_arcs_for_set(set_idx, cfg.r * 0.955555, cfg.r, classic_caps);
|
|
|
+ }
|
|
|
+
|
|
|
+ let chrom_name_radius = cfg.r + ( cfg.r * 0.6 );
|
|
|
+
|
|
|
+ // Add chromosome labels
|
|
|
+ for (set_idx, name) in chroms.iter().enumerate() {
|
|
|
+ circos.chroms_ids.insert(name.clone(), set_idx);
|
|
|
+ let ranges = read_ranges(&cfg.cytobands_bed, name)?;
|
|
|
+
|
|
|
+ let p = ranges
|
|
|
+ .iter()
|
|
|
+ .find_map(|r| {
|
|
|
+ if r.category.as_str() == "acen" {
|
|
|
+ Some(r.start + r.end.saturating_sub(r.start))
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .unwrap_or(0);
|
|
|
+
|
|
|
+ track.add_label(
|
|
|
+ Attach::Data {
|
|
|
+ set_idx,
|
|
|
+ start: p,
|
|
|
+ end: None,
|
|
|
+ },
|
|
|
+ chrom_name_radius,
|
|
|
+ LabelMode::Perimeter,
|
|
|
+ 28.0,
|
|
|
+ Some("white".to_string()),
|
|
|
+ 0.85,
|
|
|
+ (*name).to_string(),
|
|
|
+ );
|
|
|
+ track.add_arc(
|
|
|
+ Attach::Data {
|
|
|
+ set_idx,
|
|
|
+ start: p,
|
|
|
+ end: Some(p),
|
|
|
+ },
|
|
|
+ cfg.r + 10.0,
|
|
|
+ chrom_name_radius - 35.0,
|
|
|
+ "gpos50".to_string(),
|
|
|
+ CapStyle::None,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ circos.add_track(track);
|
|
|
+ circos.ref_track = Some(0);
|
|
|
+
|
|
|
+ Ok(circos)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Add perimeter labels to the reference track with automatic bumping to avoid overlaps.
|
|
|
+ ///
|
|
|
+ /// - `labels`: slice of `(label, chrom_id, pos_bp)`, expected sorted by chromosome and position.
|
|
|
+ /// - Labels on the same chromosome closer than 10 Mbp are bumped outward, with a connector drawn
|
|
|
+ /// from the true position to the bumped anchor.
|
|
|
+ /// - Labels are rendered on the perimeter at `self.config.r + 200.0`.
|
|
|
+ ///
|
|
|
+ /// No-op if no reference track is configured.
|
|
|
+ pub fn add_labels(&mut self, labels: &[(String, usize, u32)]) {
|
|
|
+ // Genomic distance (in bp) under which two consecutive labels on the same chromosome
|
|
|
+ // are considered “too close” and the latter is bumped.
|
|
|
+ const DIST_THRESHOLD_BP: u32 = 10_000_000;
|
|
|
+
|
|
|
+ // Radius at which labels are rendered on the perimeter.
|
|
|
+ let label_radius = self.config.r + ( self.config.r * 0.33);
|
|
|
+
|
|
|
+ // If there is no reference track configured, there is nothing to draw.
|
|
|
+ let Some(ref_track_idx) = self.ref_track else {
|
|
|
+ return;
|
|
|
+ };
|
|
|
+ let ref_track = &mut self.tracks[ref_track_idx];
|
|
|
+
|
|
|
+ // Build a sequence of (label, chrom_id, pos_bp, bumped_anchor_pos?)
|
|
|
+ // where bumped_anchor_pos is Some(synthetic_pos) if we decided to bump.
|
|
|
+ let mut planned: Vec<(&str, usize, u32, Option<u32>)> = Vec::with_capacity(labels.len());
|
|
|
+
|
|
|
+ for (label, chrom_id, pos_bp) in labels {
|
|
|
+ if let Some((_prev_label, prev_id, prev_pos, prev_bump)) = planned.last().copied() {
|
|
|
+ let last_ref_pos = prev_bump.unwrap_or(prev_pos);
|
|
|
+ // If same chromosome and too close to the previous label’s *reference* anchor,
|
|
|
+ // bump this one by shifting its anchor forward by the threshold distance.
|
|
|
+ if prev_id == *chrom_id && pos_bp.saturating_sub(last_ref_pos) < DIST_THRESHOLD_BP {
|
|
|
+ planned.push((
|
|
|
+ label.as_str(),
|
|
|
+ *chrom_id,
|
|
|
+ *pos_bp,
|
|
|
+ Some(last_ref_pos + DIST_THRESHOLD_BP),
|
|
|
+ ));
|
|
|
+ } else {
|
|
|
+ planned.push((label.as_str(), *chrom_id, *pos_bp, None));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // First label: never bumped.
|
|
|
+ planned.push((label.as_str(), *chrom_id, *pos_bp, None));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Emit labels and connector paths.
|
|
|
+ for (label, set_idx, pos_bp, bump_anchor) in planned {
|
|
|
+ // 1) Text label on the perimeter, attached at either the real or bumped anchor.
|
|
|
+ ref_track.add_label(
|
|
|
+ Attach::Data {
|
|
|
+ set_idx,
|
|
|
+ start: bump_anchor.unwrap_or(pos_bp),
|
|
|
+ end: None,
|
|
|
+ },
|
|
|
+ label_radius, // radial placement of the label
|
|
|
+ LabelMode::Perimeter, // mode
|
|
|
+ 18.0, // font size
|
|
|
+ Some("yellow".to_string()), // fill/background color for label
|
|
|
+ 0.95, // opacity
|
|
|
+ label.to_string(), // label text
|
|
|
+ );
|
|
|
+
|
|
|
+ // 2) Connector from true genomic pos to the (possibly bumped) anchor.
|
|
|
+ ref_track.add_path(
|
|
|
+ Attach::Data {
|
|
|
+ set_idx,
|
|
|
+ start: pos_bp,
|
|
|
+ end: None,
|
|
|
+ }, // from true position
|
|
|
+ Attach::Data {
|
|
|
+ set_idx,
|
|
|
+ start: bump_anchor.unwrap_or(pos_bp),
|
|
|
+ end: None,
|
|
|
+ }, // to anchor
|
|
|
+ self.config.r + 10.0, // inner radius for path
|
|
|
+ label_radius - 70.0, // outer radius for path
|
|
|
+ "black".to_string(), // stroke color
|
|
|
+ 1, // stroke width
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
pub fn new_wg(
|
|
|
chromosomes_ri: f64,
|
|
|
cytobands: &str,
|
|
|
@@ -953,9 +1150,10 @@ pub fn read_arcs(path: &str) -> anyhow::Result<Vec<ArcData>> {
|
|
|
|
|
|
/// Half-open intervals [start, end). For CLOSED intervals, change the `<=` test to `<`.
|
|
|
|
|
|
-
|
|
|
#[inline]
|
|
|
-fn len(a: &ArcData) -> u32 { a.end - a.start } // half-open; for closed use +1
|
|
|
+fn len(a: &ArcData) -> u32 {
|
|
|
+ a.end - a.start
|
|
|
+} // half-open; for closed use +1
|
|
|
|
|
|
/// Minimal tracks per chromosome, first-fit (left-justified),
|
|
|
/// and for intervals with the same `start`, place longer ones first.
|
|
|
@@ -975,7 +1173,9 @@ pub fn pack_tracks(mut arcs: Vec<ArcData>) -> Vec<Vec<ArcData>> {
|
|
|
while i < arcs.len() {
|
|
|
let chr = arcs[i].chr.clone();
|
|
|
let start_i = i;
|
|
|
- while i < arcs.len() && arcs[i].chr == chr { i += 1; }
|
|
|
+ while i < arcs.len() && arcs[i].chr == chr {
|
|
|
+ i += 1;
|
|
|
+ }
|
|
|
let slice = &arcs[start_i..i];
|
|
|
|
|
|
let local = pack_one_chr_first_fit(slice);
|