Thomas 4 сар өмнө
parent
commit
cc7385b0b0
5 өөрчлөгдсөн 1506 нэмэгдсэн , 91 устгасан
  1. 454 0
      src/circ.rs
  2. 458 0
      src/circos/mod.rs
  3. 471 0
      src/circos/mod_old.rs
  4. 1 75
      src/cytoband.rs
  5. 122 16
      src/lib.rs

+ 454 - 0
src/circ.rs

@@ -0,0 +1,454 @@
+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,
+    End,
+    Both,
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum LabelMode {
+    Radial,
+    Perimeter,
+}
+
+
+/// A set of ranges that will be rendered consecutively on a track
+pub struct RangeSet {
+    pub name: String,
+    pub ranges: Vec<Range>,
+    pub data_start: u32,
+    pub data_end: u32,
+}
+
+impl RangeSet {
+    /// Compute total data span for this set (could be split, but normally contiguous)
+    pub fn span(&self) -> u32 {
+        self.data_end - self.data_start
+    }
+}
+
+/// Items that a track can render
+pub enum TrackItem {
+    /// Data-based arc: attached to a range set and a data interval within it
+    Arc {
+        set_idx: usize,
+        start: u32, // relative to set.data_start
+        end: u32,
+        ri: f64,
+        ro: f64,
+        category: String,
+        cap_style: CapStyle,
+    },
+    /// Label, either angle-based or data-based
+    Label {
+        mode: LabelAttach,
+        text: String,
+        r: f64,
+        label_mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    },
+}
+
+/// How a label is attached: either angle-based, or data-based (set+pos)
+pub enum LabelAttach {
+    Angle(f64),
+    Data { set_idx: usize, pos: u32 },
+}
+
+pub struct Track {
+    pub cx: f64,
+    pub cy: f64,
+    pub angle_start: f64,
+    pub angle_end: f64,
+    pub gap: f64,
+    pub range_sets: Vec<RangeSet>,
+    pub items: Vec<TrackItem>,
+}
+
+impl Track {
+    pub fn new(cx: f64, cy: f64, angle_start: f64, angle_end: f64, gap: f64) -> Self {
+        Track {
+            cx,
+            cy,
+            angle_start,
+            angle_end,
+            gap,
+            range_sets: Vec::new(),
+            items: Vec::new(),
+        }
+    }
+
+    /// Add a range set (e.g. a chromosome, block, or whatever)
+    pub fn add_range_set(&mut self, name: String, ranges: Vec<Range>) {
+        let data_start = ranges.first().map(|r| r.start).unwrap_or(0);
+        let data_end = ranges.last().map(|r| r.end).unwrap_or(0);
+        self.range_sets.push(RangeSet {
+            name,
+            ranges,
+            data_start,
+            data_end,
+        });
+    }
+
+    /// Helper: add all ranges of a given set as arcs with cap logic
+    pub fn add_arcs_for_set<F>(&mut self, set_idx: usize, ri: f64, ro: f64, cap_func: F)
+    where
+        F: Fn(usize, usize) -> CapStyle,
+    {
+        if let Some(set) = self.range_sets.get(set_idx) {
+            let n = set.ranges.len();
+            for (i, range) in set.ranges.iter().enumerate() {
+                self.items.push(TrackItem::Arc {
+                    set_idx,
+                    start: range.start,
+                    end: range.end,
+                    ri,
+                    ro,
+                    category: range.category.clone(),
+                    cap_style: cap_func(i, n),
+                });
+            }
+        }
+    }
+
+    /// Add a label, attached either by absolute angle or by (set, pos)
+    pub fn add_label(
+        &mut self,
+        mode: LabelAttach,
+        text: String,
+        r: f64,
+        label_mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    ) {
+        self.items.push(TrackItem::Label {
+            mode,
+            text,
+            r,
+            label_mode,
+            font_size,
+            background,
+            opacity,
+        });
+    }
+
+    /// Compute angle range for a given set (i.e., its angular span on the track)
+    pub fn set_angle_range(&self, set_idx: usize) -> Option<(f64, f64)> {
+        let n_sets = self.range_sets.len();
+        if n_sets == 0 || set_idx >= n_sets {
+            return None;
+        }
+        let total_gap = self.gap * n_sets as f64;
+        let total_angle = self.angle_end - self.angle_start - total_gap;
+        let set_sizes: Vec<f64> = self
+            .range_sets
+            .iter()
+            .map(|s| (s.data_end - s.data_start) as f64)
+            .collect();
+        let all_data: f64 = set_sizes.iter().sum();
+        let mut current_angle = self.angle_start + self.gap;
+        for (i, sz) in set_sizes.iter().enumerate() {
+            let set_angle = total_angle * (*sz / all_data);
+            if i == set_idx {
+                let a0 = current_angle;
+                let a1 = current_angle + set_angle;
+                return Some((a0, a1));
+            }
+            current_angle += set_angle + self.gap;
+        }
+        None
+    }
+
+    /// Map (set_idx, position in set) to an angle on the track
+    pub fn coordinate_to_angle(&self, set_idx: usize, pos: u32) -> Option<f64> {
+        let set = self.range_sets.get(set_idx)?;
+        let (a0, a1) = self.set_angle_range(set_idx)?;
+        let frac = (pos - set.data_start) as f64 / (set.data_end - set.data_start) as f64;
+        Some(a0 + frac * (a1 - a0))
+    }
+}
+
+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 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, track: Track) {
+        self.tracks.push(track);
+    }
+
+    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 {
+            for item in &track.items {
+                match item {
+                    TrackItem::Arc {
+                        set_idx,
+                        start,
+                        end,
+                        ri,
+                        ro,
+                        category,
+                        cap_style,
+                    } => {
+                        // Map start/end to angles using track.set_angle_range()
+                        if let Some((a0, a1)) = track.set_angle_range(*set_idx) {
+                            let set = &track.range_sets[*set_idx];
+                            let frac0 = (*start - set.data_start) as f64 / (set.data_end - set.data_start) as f64;
+                            let frac1 = (*end - set.data_start) as f64 / (set.data_end - set.data_start) as f64;
+                            let angle0 = a0 + frac0 * (a1 - a0);
+                            let angle1 = a0 + frac1 * (a1 - a0);
+                            Self::draw_arc(
+                                &mut state,
+                                track.cx,
+                                track.cy,
+                                *ri,
+                                *ro,
+                                angle0,
+                                angle1,
+                                *cap_style,
+                                category,
+                            );
+                        }
+                    }
+                    TrackItem::Label {
+                        mode,
+                        text,
+                        r,
+                        label_mode,
+                        font_size,
+                        background,
+                        opacity,
+                    } => {
+                        let angle = match mode {
+                            LabelAttach::Angle(a) => *a,
+                            LabelAttach::Data { set_idx, pos } => {
+                                track.coordinate_to_angle(*set_idx, *pos).unwrap_or(0.0)
+                            }
+                        };
+                        Self::draw_label(
+                            &mut state,
+                            track.cx,
+                            track.cy,
+                            text,
+                            angle,
+                            *r,
+                            *label_mode,
+                            *font_size,
+                            background.clone(),
+                            *opacity,
+                        );
+                    }
+                }
+            }
+        }
+        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,
+    ) {
+        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
+        ));
+    }
+
+    fn draw_label(
+        state: &mut RenderState,
+        cx: f64,
+        cy: f64,
+        text: &str,
+        angle: f64,
+        r: f64,
+        mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    ) {
+        let (x, y) = (cx + r * angle.cos(), cy + r * angle.sin());
+        let anchor = "middle";
+        let rotate = match mode {
+            LabelMode::Radial => angle.to_degrees() - 90.0,
+            LabelMode::Perimeter => angle.to_degrees(),
+        };
+        let rot = if mode == LabelMode::Perimeter
+            && (angle > std::f64::consts::FRAC_PI_2 && angle < 3.0 * std::f64::consts::FRAC_PI_2)
+        {
+            rotate + 180.0
+        } else {
+            rotate
+        };
+
+        // background box (optional)
+        if let Some(bgcolor) = background {
+            let width = text.chars().count() as f64 * font_size * 0.6;
+            let height = font_size * 1.2;
+            let rect_x = x - width / 2.0;
+            let rect_y = y - height / 2.0;
+            state.svg_elements.push(format!(
+                r#"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}" opacity="{:.2}" rx="2" transform="rotate({:.2},{:.2},{:.2})"/>"#,
+                rect_x, rect_y, width, height, bgcolor, opacity, rot, x, y
+            ));
+        }
+
+        state.svg_elements.push(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
+        ));
+    }
+
+    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(())
+    }
+}
+

+ 458 - 0
src/circos/mod.rs

@@ -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(())
+    }
+}

+ 471 - 0
src/circos/mod_old.rs

@@ -0,0 +1,471 @@
+use std::io::Write;
+
+const CAP_MINOR_RATIO: f64 = 0.3; // 1/3 of thickness
+
+#[derive(Clone, Copy)]
+pub enum CapStyle {
+    None,
+    Start,
+    End,
+    Both,
+}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum LabelMode {
+    Radial,
+    Perimeter,
+}
+
+pub struct Range {
+    pub start: u32,
+    pub end: u32,
+    pub category: String,
+}
+
+pub enum TrackCoordinateMapping {
+    DataRange { start: u32, end: u32 }, // data coords mapped to angles
+    Angle, // user supplies angle directly
+}
+
+pub enum TrackItem {
+    Arc {
+        start_angle: f64,
+        end_angle: f64,
+        ri: f64,
+        ro: f64,
+        category: String,
+        cap_style: CapStyle,
+        range_start: u32, // for get_angle_for_position
+        range_end: u32,
+    },
+    Label {
+        text: String,
+        angle: f64,
+        r: f64,
+        mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    },
+}
+
+pub struct Track {
+    pub cx: f64,
+    pub cy: f64,
+    pub angle_start: f64,
+    pub angle_end: f64,
+    pub gap: f64,
+    pub mapping: TrackCoordinateMapping,
+    pub items: Vec<TrackItem>,
+}
+
+impl Track {
+    pub fn new(
+        cx: f64,
+        cy: f64,
+        angle_start: f64,
+        angle_end: f64,
+        gap: f64,
+        mapping: TrackCoordinateMapping,
+    ) -> Self {
+        Track {
+            cx,
+            cy,
+            angle_start,
+            angle_end,
+            gap,
+            mapping,
+            items: Vec::new(),
+        }
+    }
+
+    /// Map a data position to angle, for this track (if DataRange mapping)
+    pub fn data_to_angle(&self, value: u32) -> f64 {
+        match self.mapping {
+            TrackCoordinateMapping::DataRange { start, end } => {
+                let frac = (value as f64 - start as f64) / (end as f64 - start as f64);
+                self.angle_start + frac * (self.angle_end - self.angle_start)
+            }
+            TrackCoordinateMapping::Angle => value as f64, // not used, only for completeness
+        }
+    }
+
+    pub fn add_arc(
+        &mut self,
+        start_angle: f64,
+        end_angle: f64,
+        ri: f64,
+        ro: f64,
+        category: String,
+        cap_style: CapStyle,
+        range_start: u32,
+        range_end: u32,
+    ) {
+        self.items.push(TrackItem::Arc {
+            start_angle,
+            end_angle,
+            ri,
+            ro,
+            category,
+            cap_style,
+            range_start,
+            range_end,
+        });
+    }
+
+    pub fn add_label(
+        &mut self,
+        text: String,
+        angle: f64,
+        r: f64,
+        mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    ) {
+        self.items.push(TrackItem::Label {
+            text,
+            angle,
+            r,
+            mode,
+            font_size,
+            background,
+            opacity,
+        });
+    }
+
+    /// Add a set of Ranges (data) as arcs, using mapping and cap logic
+    pub fn add_ranges_to_track_with_caps<F>(
+        &mut self,
+        ranges: Vec<Range>,
+        ri: f64,
+        ro: f64,
+        cap_func: F,
+    )
+    where
+        F: Fn(usize, usize) -> CapStyle,
+    {
+        let n = ranges.len();
+        if n == 0 {
+            return;
+        }
+        let total_length: f64 = ranges.iter().map(|r| (r.end - r.start) as f64).sum();
+        if total_length == 0.0 {
+            return;
+        }
+        let total_gap = self.gap * n as f64;
+        let total_angle = self.angle_end - self.angle_start - total_gap;
+        let mut current_angle = self.angle_start + self.gap;
+        for (i, range) in ranges.into_iter().enumerate() {
+            let len = (range.end - range.start) as f64;
+            let arc_angle = total_angle * (len / total_length);
+            let arc_start = current_angle;
+            let arc_end = current_angle + arc_angle;
+            self.add_arc(
+                arc_start,
+                arc_end,
+                ri,
+                ro,
+                range.category,
+                cap_func(i, n),
+                range.start,
+                range.end,
+            );
+            current_angle = arc_end + self.gap;
+        }
+    }
+
+    /// Map a data coordinate to angle, searching the arcs for the right mapping
+    pub fn get_angle_for_position(&self, target_position: u32) -> Option<f64> {
+        // Only valid for DataRange mapping
+        let mut arcs: Vec<(&TrackItem, u32, u32)> = Vec::new();
+        let mut total_data_len = 0u64;
+        for item in &self.items {
+            if let TrackItem::Arc { range_start, range_end, .. } = item {
+                let len = (*range_end as u64).saturating_sub(*range_start as u64);
+                arcs.push((item, *range_start, *range_end));
+                total_data_len += len;
+            }
+        }
+        if arcs.is_empty() || total_data_len == 0 {
+            return None;
+        }
+        let n = arcs.len();
+        let total_gap = self.gap * n as f64;
+        let total_angle = self.angle_end - self.angle_start - total_gap;
+        let mut current_angle = self.angle_start + self.gap;
+        for (i, (item, start, end)) in arcs.iter().enumerate() {
+            let len = (*end as u64).saturating_sub(*start as u64);
+            if target_position >= *start && target_position < *end {
+                let frac = (target_position - *start) as f64 / (len as f64);
+                let arc_angle = total_angle * (len as f64 / total_data_len as f64);
+                let arc_start = current_angle;
+                let angle = arc_start + arc_angle * frac;
+                return Some(angle);
+            }
+            current_angle += total_angle * (len as f64 / total_data_len as f64) + self.gap;
+        }
+        None
+    }
+}
+
+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 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, track: Track) {
+        self.tracks.push(track);
+    }
+
+    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 {
+            for item in &track.items {
+                match item {
+                    TrackItem::Arc {
+                        start_angle,
+                        end_angle,
+                        ri,
+                        ro,
+                        category,
+                        cap_style,
+                        ..
+                    } => {
+                        Self::draw_arc(
+                            &mut state,
+                            track.cx,
+                            track.cy,
+                            *ri,
+                            *ro,
+                            *start_angle,
+                            *end_angle,
+                            *cap_style,
+                            category,
+                        );
+                    }
+                    TrackItem::Label {
+                        text,
+                        angle,
+                        r,
+                        mode,
+                        font_size,
+                        background,
+                        opacity,
+                    } => {
+                        Self::draw_label(
+                            &mut state,
+                            track.cx,
+                            track.cy,
+                            text,
+                            *angle,
+                            *r,
+                            *mode,
+                            *font_size,
+                            background.clone(),
+                            *opacity,
+                        );
+                    }
+                }
+            }
+        }
+        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,
+    ) {
+        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
+        ));
+    }
+
+    fn draw_label(
+        state: &mut RenderState,
+        cx: f64,
+        cy: f64,
+        text: &str,
+        angle: f64,
+        r: f64,
+        mode: LabelMode,
+        font_size: f64,
+        background: Option<String>,
+        opacity: f64,
+    ) {
+        let (x, y) = (cx + r * angle.cos(), cy + r * angle.sin());
+        let anchor = "middle";
+        let rotate = match mode {
+            LabelMode::Radial => angle.to_degrees() - 90.0,
+            LabelMode::Perimeter => angle.to_degrees(),
+        };
+        let rot = if mode == LabelMode::Perimeter
+            && (angle > std::f64::consts::FRAC_PI_2 && angle < 3.0 * std::f64::consts::FRAC_PI_2)
+        {
+            rotate + 180.0
+        } else {
+            rotate
+        };
+
+        // background box (optional)
+        if let Some(bgcolor) = background {
+            let width = text.chars().count() as f64 * font_size * 0.6;
+            let height = font_size * 1.2;
+            let rect_x = x - width / 2.0;
+            let rect_y = y - height / 2.0;
+            state.svg_elements.push(format!(
+                r#"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{}" opacity="{:.2}" rx="2" transform="rotate({:.2},{:.2},{:.2})"/>"#,
+                rect_x, rect_y, width, height, bgcolor, opacity, rot, x, y
+            ));
+        }
+
+        state.svg_elements.push(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
+        ));
+    }
+
+    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(())
+    }
+}
+

+ 1 - 75
src/cytoband.rs

@@ -1,5 +1,3 @@
-use std::any::Any;
-use std::f64::consts::PI;
 use std::fs::File;
 use std::io::{BufRead, BufReader, Write};
 
@@ -299,6 +297,7 @@ fn create_chromosome_svg(
     svg_content
 }
 
+
 pub fn svg_chromosome(
     contig: &str,
     width: u32,
@@ -325,77 +324,4 @@ pub fn svg_chromosome(
     Ok(())
 }
 
-pub fn generate_chromosome_svg(
-    ranges: Vec<Range>,
-    cx: f64,
-    cy: f64,
-    r: f64,
-    start_angle: f64,
-    end_angle: f64,
-) -> String {
-    let mut svg = String::new();
-    svg.push_str(&format!(
-        r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
-        (2.0 * r) as u32,
-        (2.0 * r) as u32
-    ));
-
-    let angle_range = end_angle - start_angle;
-    let total_length = ranges
-        .iter()
-        .map(|range| range.end - range.start)
-        .sum::<u32>() as f64;
-
-    let mut current_angle = start_angle;
-
-    for range in ranges {
-        let start = range.start as f64;
-        let end = range.end as f64;
-        let category = &range.category;
-
-        let start_angle_rad = current_angle + (start / total_length) * angle_range;
-        let end_angle_rad = current_angle + (end / total_length) * angle_range;
-
-        let start_outer_x = cx + r * start_angle_rad.cos();
-        let start_outer_y = cy + r * start_angle_rad.sin();
-        let end_outer_x = cx + r * end_angle_rad.cos();
-        let end_outer_y = cy + r * end_angle_rad.sin();
-
-        let inner_r = r * 0.8; // Adjust this value to change the thickness of the chromosome
-        let start_inner_x = cx + inner_r * start_angle_rad.cos();
-        let start_inner_y = cy + inner_r * start_angle_rad.sin();
-        let end_inner_x = cx + inner_r * end_angle_rad.cos();
-        let end_inner_y = cy + inner_r * end_angle_rad.sin();
-
-        let large_arc_flag = if (end_angle_rad - start_angle_rad).abs() > PI {
-            1
-        } else {
-            0
-        };
-
-        let path_data = format!(
-            "M {:.2} {:.2} A {:.2} {:.2} 0 {} 1 {:.2} {:.2} L {:.2} {:.2} A {:.2} {:.2} 0 {} 0 {:.2} {:.2} Z",
-            start_outer_x, start_outer_y, r, r, large_arc_flag, end_outer_x, end_outer_y,
-            end_inner_x, end_inner_y, inner_r, inner_r, large_arc_flag, start_inner_x, start_inner_y
-        );
-
-        let color = match category.as_str() {
-            "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)",
-            _ => "rgb(162, 210, 255)",
-        };
-
-        svg.push_str(&format!(
-            r#"<path d="{}" fill="{}" stroke="black" stroke-width="1" />"#,
-            path_data, color
-        ));
 
-        current_angle = end_angle_rad;
-    }
-
-    svg.push_str("</svg>");
-    svg
-}

+ 122 - 16
src/lib.rs

@@ -1,17 +1,18 @@
+pub mod circ;
+pub mod circos;
 pub mod cytoband;
 pub mod report;
 pub mod theme;
 
 #[cfg(test)]
 mod tests {
-    use std::{fs::File, io::Write};
+    use std::{f64::consts::PI, fs::File, io::Write};
 
-    use cytoband::{
-        generate_chromosome_svg, read_ranges, svg_chromosome, AdditionalRect, Lollipop,
-        RectPosition,
-    };
+    use cytoband::{read_ranges, svg_chromosome, AdditionalRect, Lollipop, RectPosition};
     use report::compile_typst_report;
 
+    use crate::circos::{classic_caps, no_caps, AngleRange, Circos, LabelMode};
+
     use super::*;
 
     #[test]
@@ -19,15 +20,21 @@ mod tests {
         let additional_rects = vec![
             AdditionalRect {
                 start: 0,
-                end: 250000,
+                end: 22_500_000,
                 color: String::from("red"),
                 position: RectPosition::Above(0),
             },
+            AdditionalRect {
+                start: 0,
+                end: 100_500_000,
+                color: String::from("white"),
+                position: RectPosition::Above(1),
+            },
             AdditionalRect {
                 start: 500000,
                 end: 1500000,
                 color: String::from("orange"),
-                position: RectPosition::Above(1),
+                position: RectPosition::Above(2),
             },
             AdditionalRect {
                 start: 2000000,
@@ -41,7 +48,6 @@ mod tests {
                 color: String::from("green"),
                 position: RectPosition::Below(1),
             },
-            // Add more rectangles as needed
         ];
 
         let lollipops = vec![
@@ -72,18 +78,118 @@ mod tests {
         .unwrap()
     }
 
+    // #[test]
+    // fn bend() -> anyhow::Result<()> {
+    //     let svg_path = "/data/bent.svg";
+    //     let ranges = read_ranges("/data/ref/hs1/cytoBandMapped.bed", "chr1")?;
+    //
+    //     let svg_content =
+    //         generate_chromosome_svg(ranges, 200.0, 200.0, 150.0, 0.0, 2.0 * std::f64::consts::PI);
+    //
+    //     let mut output_file = File::create(svg_path)?;
+    //     output_file.write_all(svg_content.as_bytes())?;
+    //
+    //     println!("SVG file created: {svg_path}");
+    //     Ok(())
+    // }
+
     #[test]
-    fn bend() -> anyhow::Result<()> {
-        let svg_path = "/data/bent.svg";
-        let ranges = read_ranges("/data/ref/hs1/cytoBandMapped.bed", "chr1")?;
+    fn oldcircos() -> anyhow::Result<()> {
+        let mut circos = Circos::new();
+        let r = 1050.0;
+        let cx = 0.0;
+        let cy = 0.0;
+        let angle_start = -std::f64::consts::FRAC_PI_2; // 12:00 direction
+        let angle_end = angle_start + 2.0 * std::f64::consts::PI;
+
+        let gap = 0.012 * std::f64::consts::PI;
+
+        let track_id = circos.add_track(cx, cy, r * 0.955555, r, angle_start, angle_end, gap);
+        (1..=22).for_each(|c| {
+            let ranges1 =
+                read_ranges("/data/ref/hs1/cytoBandMapped.bed", &format!("chr{c}")).unwrap();
+            circos.add_ranges_to_track_with_caps(track_id, ranges1, classic_caps);
+        });
+        let angle_ranges = vec![AngleRange {
+            angle_start: 40f64.to_radians(),
+            angle_end: 40f64.to_radians(),
+            category: "gpos50".to_string(),
+            label: Some("Hello".to_string()),
+            label_mode: LabelMode::Perimeter,
+        }];
+
+        let r = r + 50.0;
+        let track_id = circos.add_track(cx, cy, r * 0.955555, r, angle_start, angle_end, 0.0);
+        circos.add_angle_ranges_to_track_with_caps(track_id, angle_ranges, no_caps);
+
+        circos.save_to_file("/data/bentu.svg", 10.0)?;
+
+        Ok(())
+    }
+
+    #[test]
+    fn circos() -> anyhow::Result<()> {
+        let mut circos = circ::Circos::new();
+        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;
+
+        let mut band_track = circ::Track::new(cx, cy, angle_start, angle_end, gap);
+
+        // Add several range sets (e.g., chromosomes)
+        for c in 1..=22 {
+            let name = format!("chr{c}");
+            let ranges = read_ranges("/data/ref/hs1/cytoBandMapped.bed", &name)?;
+            band_track.add_range_set(name, ranges);
+            // For each set, add its arcs
+            let set_idx = band_track.range_sets.len() - 1;
+            band_track.add_arcs_for_set(set_idx, r * 0.955555, r, circ::classic_caps);
+        }
+
+        // Add a label at chr2 position 40_000_000
+        band_track.add_label(
+            circ::LabelAttach::Data {
+                set_idx: 1,
+                pos: 40_000_000,
+            },
+            "Hello".to_string(),
+            r + 40.0,
+            circ::LabelMode::Perimeter,
+            22.0,
+            Some("white".to_string()),
+            0.9,
+        );
+
+        // Add an annotation track with absolute-angle items
+        let r2 = r + 50.0;
+        let mut annot_track = circ::Track::new(cx, cy, angle_start, angle_end, 0.0);
+        annot_track.items.push(circ::TrackItem::Arc {
+            set_idx: 0, // dummy, not used for angle-based
+            start: 0,
+            end: 0,
+            ri: r2 * 0.955555,
+            ro: r2,
+            category: "gpos50".to_string(),
+            cap_style: circ::CapStyle::None,
+        });
+        annot_track.add_label(
+            circ::LabelAttach::Angle(50f64.to_radians()),
+            "AngleLabel".to_string(),
+            r2 + 20.0,
+            circ::LabelMode::Perimeter,
+            28.0,
+            Some("white".to_string()),
+            0.85,
+        );
 
-        let svg_content =
-            generate_chromosome_svg(ranges, 200.0, 200.0, 150.0, 0.0, 2.0 * std::f64::consts::PI);
+        circos.add_track(band_track);
+        circos.add_track(annot_track);
 
-        let mut output_file = File::create(svg_path)?;
-        output_file.write_all(svg_content.as_bytes())?;
+        circos.save_to_file("/data/bent.svg", 10.0)?;
 
-        println!("SVG file created: {svg_path}");
         Ok(())
     }