|
|
@@ -0,0 +1,458 @@
|
|
|
+use std::io::Write;
|
|
|
+
|
|
|
+use crate::cytoband::Range;
|
|
|
+
|
|
|
+const CAP_MINOR_RATIO: f64 = 0.3; // 1/3 of thickness
|
|
|
+
|
|
|
+#[derive(Clone, Copy)]
|
|
|
+pub enum CapStyle {
|
|
|
+ None,
|
|
|
+ Start, // round only start
|
|
|
+ End, // round only end
|
|
|
+ Both, // round both
|
|
|
+}
|
|
|
+
|
|
|
+// Utility for cap logic: classic "first=Start, last=End, only=Both"
|
|
|
+pub fn classic_caps(i: usize, len: usize) -> CapStyle {
|
|
|
+ match (i, len) {
|
|
|
+ (0, 1) => CapStyle::Both,
|
|
|
+ (0, _) => CapStyle::Start,
|
|
|
+ (n, l) if n == l - 1 => CapStyle::End,
|
|
|
+ _ => CapStyle::None,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+pub fn no_caps(_i: usize, _len: usize) -> CapStyle {
|
|
|
+ CapStyle::None
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(PartialEq, Eq)]
|
|
|
+pub enum LabelMode {
|
|
|
+ Radial, // Text baseline points outward, at arc midpoint
|
|
|
+ Perimeter, // Text is tangent to arc, follows perimeter
|
|
|
+}
|
|
|
+
|
|
|
+pub struct AngleRange {
|
|
|
+ pub angle_start: f64,
|
|
|
+ pub angle_end: f64,
|
|
|
+ pub category: String,
|
|
|
+ pub label: Option<String>,
|
|
|
+ pub label_mode: LabelMode,
|
|
|
+}
|
|
|
+
|
|
|
+pub struct Track {
|
|
|
+ pub cx: f64,
|
|
|
+ pub cy: f64,
|
|
|
+ pub ri: f64,
|
|
|
+ pub ro: f64,
|
|
|
+ pub angle_start: f64,
|
|
|
+ pub angle_end: f64,
|
|
|
+ pub gap: f64,
|
|
|
+ pub rangesets: Vec<Vec<(Range, CapStyle)>>, // (Range, CapStyle) for each segment
|
|
|
+ pub rangesets_angle: Vec<Vec<(AngleRange, CapStyle)>>,
|
|
|
+}
|
|
|
+
|
|
|
+pub struct Circos {
|
|
|
+ pub tracks: Vec<Track>,
|
|
|
+ pub svg_elements: Vec<String>,
|
|
|
+ pub min_x: f64,
|
|
|
+ pub min_y: f64,
|
|
|
+ pub max_x: f64,
|
|
|
+ pub max_y: f64,
|
|
|
+}
|
|
|
+struct RenderState {
|
|
|
+ svg_elements: Vec<String>,
|
|
|
+ min_x: f64,
|
|
|
+ min_y: f64,
|
|
|
+ max_x: f64,
|
|
|
+ max_y: f64,
|
|
|
+}
|
|
|
+impl Circos {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self {
|
|
|
+ tracks: Vec::new(),
|
|
|
+ svg_elements: Vec::new(),
|
|
|
+ min_x: f64::INFINITY,
|
|
|
+ min_y: f64::INFINITY,
|
|
|
+ max_x: f64::NEG_INFINITY,
|
|
|
+ max_y: f64::NEG_INFINITY,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn add_track(
|
|
|
+ &mut self,
|
|
|
+ cx: f64,
|
|
|
+ cy: f64,
|
|
|
+ ri: f64,
|
|
|
+ ro: f64,
|
|
|
+ angle_start: f64,
|
|
|
+ angle_end: f64,
|
|
|
+ gap: f64,
|
|
|
+ ) -> usize {
|
|
|
+ self.tracks.push(Track {
|
|
|
+ cx,
|
|
|
+ cy,
|
|
|
+ ri,
|
|
|
+ ro,
|
|
|
+ angle_start,
|
|
|
+ angle_end,
|
|
|
+ gap,
|
|
|
+ rangesets: Vec::new(),
|
|
|
+ rangesets_angle: Vec::new(),
|
|
|
+ });
|
|
|
+ self.tracks.len() - 1
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Add a set of ranges to a track, with a closure that defines the CapStyle for each range
|
|
|
+ pub fn add_ranges_to_track_with_caps<F>(
|
|
|
+ &mut self,
|
|
|
+ track_id: usize,
|
|
|
+ ranges: Vec<Range>,
|
|
|
+ cap_func: F,
|
|
|
+ ) where
|
|
|
+ F: Fn(usize, usize) -> CapStyle,
|
|
|
+ {
|
|
|
+ if let Some(track) = self.tracks.get_mut(track_id) {
|
|
|
+ let len = ranges.len();
|
|
|
+ let cap_ranges = ranges
|
|
|
+ .into_iter()
|
|
|
+ .enumerate()
|
|
|
+ .map(|(i, r)| (r, cap_func(i, len)))
|
|
|
+ .collect();
|
|
|
+ track.rangesets.push(cap_ranges);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /// Calculates the angle for a given data position within a specific track.
|
|
|
+ ///
|
|
|
+ /// # Arguments
|
|
|
+ ///
|
|
|
+ /// * `track_id` - The index of the track to search within.
|
|
|
+ /// * `target_position` - The data position (e.g., genomic coordinate) to find the angle for.
|
|
|
+ ///
|
|
|
+ /// # Returns
|
|
|
+ ///
|
|
|
+ /// An `Option<f64>` containing the angle in radians if the position is found, otherwise `None`.
|
|
|
+ pub fn get_angle_for_position(&self, track_id: usize, target_position: u32) -> Option<f64> {
|
|
|
+ let target_position: u64 = target_position.into();
|
|
|
+ let track = self.tracks.get(track_id)?;
|
|
|
+
|
|
|
+ let n_sets = track.rangesets.len();
|
|
|
+ if n_sets == 0 {
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+
|
|
|
+ let total_gap = track.gap * n_sets as f64;
|
|
|
+ let total_angle = track.angle_end - track.angle_start - total_gap;
|
|
|
+
|
|
|
+ let set_sizes: Vec<f64> = track
|
|
|
+ .rangesets
|
|
|
+ .iter()
|
|
|
+ .map(|ranges| ranges.iter().map(|(r, _)| (r.end - r.start) as f64).sum())
|
|
|
+ .collect();
|
|
|
+ let all_data: f64 = set_sizes.iter().sum();
|
|
|
+ if all_data == 0.0 {
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut current_angle = track.angle_start + track.gap;
|
|
|
+
|
|
|
+ for (rangeset, set_size) in track.rangesets.iter().zip(set_sizes.iter()) {
|
|
|
+ if *set_size == 0.0 {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let set_angle = total_angle * (set_size / all_data);
|
|
|
+ let angle_per_unit = set_angle / set_size;
|
|
|
+
|
|
|
+ let mut cumulative_length_in_set: u64 = 0;
|
|
|
+ for (range, _) in rangeset {
|
|
|
+ let range_start = range.start as u64;
|
|
|
+ let range_end = range.end as u64;
|
|
|
+ let range_length = range_end - range_start;
|
|
|
+
|
|
|
+ if target_position >= range_start && target_position < range_end {
|
|
|
+ let offset_in_range = target_position - range_start;
|
|
|
+ let position_in_set = cumulative_length_in_set + offset_in_range;
|
|
|
+ let angle_offset = position_in_set as f64 * angle_per_unit;
|
|
|
+ return Some(current_angle + angle_offset);
|
|
|
+ }
|
|
|
+ cumulative_length_in_set += range_length;
|
|
|
+ }
|
|
|
+
|
|
|
+ current_angle += set_angle + track.gap;
|
|
|
+ }
|
|
|
+
|
|
|
+ None // Position not found in any range
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Add a set of angle-based ranges to a track, with cap function
|
|
|
+ pub fn add_angle_ranges_to_track_with_caps<F>(
|
|
|
+ &mut self,
|
|
|
+ track_id: usize,
|
|
|
+ angle_ranges: Vec<AngleRange>, // (angle_start, angle_end, category)
|
|
|
+ cap_func: F,
|
|
|
+ ) where
|
|
|
+ F: Fn(usize, usize) -> CapStyle,
|
|
|
+ {
|
|
|
+ if let Some(track) = self.tracks.get_mut(track_id) {
|
|
|
+ let len = angle_ranges.len();
|
|
|
+ let cap_ranges: Vec<_> = angle_ranges
|
|
|
+ .into_iter()
|
|
|
+ .enumerate()
|
|
|
+ .map(|(i, a)| (a, cap_func(i, len)))
|
|
|
+ .collect();
|
|
|
+ // New vector of angle-based "ranges"
|
|
|
+ if !track.rangesets_angle.is_empty() {
|
|
|
+ track.rangesets_angle.push(cap_ranges);
|
|
|
+ } else {
|
|
|
+ track.rangesets_angle = vec![cap_ranges];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Renders all tracks/ranges to svg_elements and updates the bounding box.
|
|
|
+ pub fn render_svg(&mut self) {
|
|
|
+ let mut state = RenderState {
|
|
|
+ svg_elements: Vec::new(),
|
|
|
+ min_x: f64::INFINITY,
|
|
|
+ min_y: f64::INFINITY,
|
|
|
+ max_x: f64::NEG_INFINITY,
|
|
|
+ max_y: f64::NEG_INFINITY,
|
|
|
+ };
|
|
|
+
|
|
|
+ for track in &self.tracks {
|
|
|
+ // 1. Data-based (rangesets)
|
|
|
+ let n_sets = track.rangesets.len();
|
|
|
+ if n_sets > 0 {
|
|
|
+ let total_gap = track.gap * n_sets as f64;
|
|
|
+ let total_angle = track.angle_end - track.angle_start - total_gap;
|
|
|
+ let set_sizes: Vec<f64> = track
|
|
|
+ .rangesets
|
|
|
+ .iter()
|
|
|
+ .map(|ranges| ranges.iter().map(|(r, _)| (r.end - r.start) as f64).sum())
|
|
|
+ .collect();
|
|
|
+ let all_data: f64 = set_sizes.iter().sum();
|
|
|
+ if all_data > 0.0 {
|
|
|
+ let mut current_angle = track.angle_start + track.gap;
|
|
|
+ for (ranges, set_size) in track.rangesets.iter().zip(set_sizes.iter()) {
|
|
|
+ if *set_size == 0.0 {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let set_angle = total_angle * (*set_size / all_data);
|
|
|
+ let set_angle_end = current_angle + set_angle;
|
|
|
+ let angle_range = set_angle_end - current_angle;
|
|
|
+ let total_length: f64 =
|
|
|
+ ranges.iter().map(|(r, _)| (r.end - r.start) as f64).sum();
|
|
|
+
|
|
|
+ for (range, cap_style) in ranges {
|
|
|
+ let start = range.start as f64;
|
|
|
+ let end = range.end as f64;
|
|
|
+ let a0 = current_angle + (start / total_length) * angle_range;
|
|
|
+ let a1 = current_angle + (end / total_length) * angle_range;
|
|
|
+ Self::draw_arc(
|
|
|
+ &mut state,
|
|
|
+ track.cx,
|
|
|
+ track.cy,
|
|
|
+ track.ri,
|
|
|
+ track.ro,
|
|
|
+ a0,
|
|
|
+ a1,
|
|
|
+ *cap_style,
|
|
|
+ &range.category,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ current_angle = set_angle_end + track.gap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Angle-based (rangesets_angle)
|
|
|
+ for angle_rangeset in &track.rangesets_angle {
|
|
|
+ for (range, cap_style) in angle_rangeset {
|
|
|
+ Self::draw_arc(
|
|
|
+ &mut state,
|
|
|
+ track.cx,
|
|
|
+ track.cy,
|
|
|
+ track.ri,
|
|
|
+ track.ro,
|
|
|
+ range.angle_start,
|
|
|
+ range.angle_end,
|
|
|
+ *cap_style,
|
|
|
+ &range.category,
|
|
|
+ );
|
|
|
+ if let Some(label) = &range.label {
|
|
|
+ let mid_angle = 0.5 * (range.angle_start + range.angle_end);
|
|
|
+ let r_text = match range.label_mode {
|
|
|
+ LabelMode::Radial => 0.5 * (track.ri + track.ro),
|
|
|
+ LabelMode::Perimeter => track.ro + 10.0, // or any offset you like
|
|
|
+ };
|
|
|
+ let (x, y) = (
|
|
|
+ track.cx + r_text * mid_angle.cos(),
|
|
|
+ track.cy + r_text * mid_angle.sin(),
|
|
|
+ );
|
|
|
+
|
|
|
+ let rotate = match range.label_mode {
|
|
|
+ LabelMode::Radial => mid_angle.to_degrees() - 90.0, // baseline points outward
|
|
|
+ LabelMode::Perimeter => mid_angle.to_degrees(),
|
|
|
+ };
|
|
|
+
|
|
|
+ let anchor = "middle";
|
|
|
+ // Optionally flip upside-down text on lower semicircle
|
|
|
+ let rot = if range.label_mode == LabelMode::Perimeter
|
|
|
+ && (mid_angle > std::f64::consts::FRAC_PI_2
|
|
|
+ && mid_angle < 3.0 * std::f64::consts::FRAC_PI_2)
|
|
|
+ {
|
|
|
+ rotate + 180.0
|
|
|
+ } else {
|
|
|
+ rotate
|
|
|
+ };
|
|
|
+
|
|
|
+ let font_size = 24.0;
|
|
|
+ let text = label; // String
|
|
|
+
|
|
|
+ let width = text.len() as f64 * font_size * 0.6;
|
|
|
+ let height = font_size * 1.2;
|
|
|
+
|
|
|
+ // Rectangle center at (x, y), so top-left is:
|
|
|
+ let rect_x = x - width / 2.0;
|
|
|
+ let rect_y = y - height / 2.0;
|
|
|
+
|
|
|
+ // Use same rotation as text
|
|
|
+ let rect_svg = format!(
|
|
|
+ r#"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="white" opacity="0.75" rx="2" transform="rotate({:.2},{:.2},{:.2})"/>"#,
|
|
|
+ rect_x, rect_y, width, height, rot, x, y
|
|
|
+ );
|
|
|
+
|
|
|
+ // Text SVG as before, *after* rect_svg
|
|
|
+ let text_svg = format!(
|
|
|
+ r#"<text x="{:.2}" y="{:.2}" text-anchor="{}" dominant-baseline="middle" font-size="{:.1}" transform="rotate({:.2},{:.2},{:.2})">{}</text>"#,
|
|
|
+ x, y, anchor, font_size, rot, x, y, text
|
|
|
+ );
|
|
|
+
|
|
|
+ state.svg_elements.push(rect_svg);
|
|
|
+ state.svg_elements.push(text_svg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // At end, commit back to self
|
|
|
+ self.svg_elements = state.svg_elements;
|
|
|
+ self.min_x = state.min_x;
|
|
|
+ self.max_x = state.max_x;
|
|
|
+ self.min_y = state.min_y;
|
|
|
+ self.max_y = state.max_y;
|
|
|
+ }
|
|
|
+
|
|
|
+ fn draw_arc(
|
|
|
+ state: &mut RenderState,
|
|
|
+ cx: f64,
|
|
|
+ cy: f64,
|
|
|
+ ri: f64,
|
|
|
+ ro: f64,
|
|
|
+ a0: f64,
|
|
|
+ a1: f64,
|
|
|
+ cap_style: CapStyle,
|
|
|
+ category: &str,
|
|
|
+ ) {
|
|
|
+ // Arc endpoints, bbox, path as before (identical)
|
|
|
+ // Replace all `self.svg_elements` → `state.svg_elements`
|
|
|
+ // Replace all `self.min_x` etc. → `state.min_x` etc.
|
|
|
+ // (see code below)
|
|
|
+ let sox = cx + ro * a0.cos();
|
|
|
+ let soy = cy + ro * a0.sin();
|
|
|
+ let eox = cx + ro * a1.cos();
|
|
|
+ let eoy = cy + ro * a1.sin();
|
|
|
+ let six = cx + ri * a1.cos();
|
|
|
+ let siy = cy + ri * a1.sin();
|
|
|
+ let sax = cx + ri * a0.cos();
|
|
|
+ let say = cy + ri * a0.sin();
|
|
|
+
|
|
|
+ let thickness = ro - ri;
|
|
|
+ let cap_radius = thickness / 2.0;
|
|
|
+ let cap_minor = thickness * CAP_MINOR_RATIO;
|
|
|
+ let large_arc_flag = if (a1 - a0).abs() > std::f64::consts::PI {
|
|
|
+ 1
|
|
|
+ } else {
|
|
|
+ 0
|
|
|
+ };
|
|
|
+
|
|
|
+ for &radius in &[ri, ro] {
|
|
|
+ for &a in &[a0, a1] {
|
|
|
+ let x = cx + radius * a.cos();
|
|
|
+ let y = cy + radius * a.sin();
|
|
|
+ state.min_x = state.min_x.min(x);
|
|
|
+ state.max_x = state.max_x.max(x);
|
|
|
+ state.min_y = state.min_y.min(y);
|
|
|
+ state.max_y = state.max_y.max(y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut path = format!("M {:.2} {:.2} ", sox, soy);
|
|
|
+ path += &format!(
|
|
|
+ "A {:.2} {:.2} 0 {} 1 {:.2} {:.2} ",
|
|
|
+ ro, ro, large_arc_flag, eox, eoy
|
|
|
+ );
|
|
|
+ if matches!(cap_style, CapStyle::End | CapStyle::Both) {
|
|
|
+ let end_cap_angle_deg = a1.to_degrees();
|
|
|
+ path += &format!(
|
|
|
+ "A {:.2} {:.2} {:.2} 0 1 {:.2} {:.2} ",
|
|
|
+ cap_radius, cap_minor, end_cap_angle_deg, six, siy
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ path += &format!("L {:.2} {:.2} ", six, siy);
|
|
|
+ }
|
|
|
+ path += &format!(
|
|
|
+ "A {:.2} {:.2} 0 {} 0 {:.2} {:.2} ",
|
|
|
+ ri, ri, large_arc_flag, sax, say
|
|
|
+ );
|
|
|
+ if matches!(cap_style, CapStyle::Start | CapStyle::Both) {
|
|
|
+ let start_cap_angle_deg = a0.to_degrees();
|
|
|
+ path += &format!(
|
|
|
+ "A {:.2} {:.2} {:.2} 0 1 {:.2} {:.2} Z",
|
|
|
+ cap_radius, cap_minor, start_cap_angle_deg, sox, soy
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ path += &format!("L {:.2} {:.2} Z", sox, soy);
|
|
|
+ }
|
|
|
+
|
|
|
+ let color = match category {
|
|
|
+ "gneg" => "rgb(255,255,255)",
|
|
|
+ "gpos25" => "rgb(192,192,192)",
|
|
|
+ "gpos50" => "rgb(128,128,128)",
|
|
|
+ "gpos75" => "rgb(64,64,64)",
|
|
|
+ "gpos100" => "rgb(0,0,0)",
|
|
|
+ "acen" => "rgb(217,47,39)",
|
|
|
+ _ => "rgb(162,210,255)",
|
|
|
+ };
|
|
|
+ state.svg_elements.push(format!(
|
|
|
+ r#"<path d="{}" fill="{}" stroke="black" stroke-width="1"/>"#,
|
|
|
+ path, color
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn save_to_file(&mut self, path: &str, margin: f64) -> std::io::Result<()> {
|
|
|
+ self.render_svg();
|
|
|
+ let min_x = self.min_x - margin;
|
|
|
+ let min_y = self.min_y - margin;
|
|
|
+ let max_x = self.max_x + margin;
|
|
|
+ let max_y = self.max_y + margin;
|
|
|
+ let width = max_x - min_x;
|
|
|
+ let height = max_y - min_y;
|
|
|
+ let svg_header = format!(
|
|
|
+ r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="{vx} {vy} {vw} {vh}">"#,
|
|
|
+ w = width.ceil() as u32,
|
|
|
+ h = height.ceil() as u32,
|
|
|
+ vx = min_x,
|
|
|
+ vy = min_y,
|
|
|
+ vw = width,
|
|
|
+ vh = height
|
|
|
+ );
|
|
|
+ let mut file = std::fs::File::create(path)?;
|
|
|
+ file.write_all(svg_header.as_bytes())?;
|
|
|
+ for elem in &self.svg_elements {
|
|
|
+ file.write_all(elem.as_bytes())?;
|
|
|
+ }
|
|
|
+ file.write_all(b"</svg>")?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+}
|