use crate::{ChartData, SIGN_ABBREV, safe_longitude, svg_escape};
use std::fmt::Write;
const SIZE: f64 = 400.0;
const HALF: f64 = SIZE / 2.0;
const PAD: f64 = 10.0;
const CELL: f64 = (SIZE - 2.0 * PAD) / 4.0;
const BG_FILL: &str = "#FFFEF5";
const STROKE: &str = "#2C1810";
const SIGN_COLOR: &str = "#8B7355";
const ASC_COLOR: &str = "#C4783E";
const FAINT: &str = "#999";
const LABEL_COLOR: &str = "#666";
fn sign_grid_pos(sign_idx: usize) -> (usize, usize) {
match sign_idx {
0 => (1, 0), 1 => (2, 0), 2 => (3, 0), 3 => (3, 1), 4 => (3, 2), 5 => (3, 3), 6 => (2, 3), 7 => (1, 3), 8 => (0, 3), 9 => (0, 2), 10 => (0, 1), 11 => (0, 0), _ => (0, 0),
}
}
fn sign_index_for_house(asc_sign: usize, house: usize) -> usize {
let h = house.clamp(1, 12);
(asc_sign % 12 + h - 1) % 12
}
pub(crate) fn render(data: &ChartData) -> String {
let mut svg = String::with_capacity(4096);
write!(svg,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {s} {s}\" width=\"{s}\" height=\"{s}\" font-family=\"sans-serif\">",
s = SIZE,
).unwrap();
write!(
svg,
"<rect x=\"0\" y=\"0\" width=\"{s}\" height=\"{s}\" fill=\"{bg}\" stroke=\"none\"/>",
s = SIZE,
bg = BG_FILL,
)
.unwrap();
for row in 0..4 {
for col in 0..4 {
if (row == 1 || row == 2) && (col == 1 || col == 2) {
continue;
}
let x = PAD + col as f64 * CELL;
let y = PAD + row as f64 * CELL;
write!(svg,
"<rect x=\"{x}\" y=\"{y}\" width=\"{c}\" height=\"{c}\" fill=\"none\" stroke=\"{s}\" stroke-width=\"1.5\"/>",
c = CELL, s = STROKE,
).unwrap();
}
}
let full = 4.0 * CELL;
write!(svg,
"<rect x=\"{p}\" y=\"{p}\" width=\"{f}\" height=\"{f}\" fill=\"none\" stroke=\"{s}\" stroke-width=\"2\"/>",
p = PAD, f = full, s = STROKE,
).unwrap();
let ix = PAD + CELL;
let iy = PAD + CELL;
let iw = 2.0 * CELL;
write!(svg,
"<rect x=\"{ix}\" y=\"{iy}\" width=\"{iw}\" height=\"{iw}\" fill=\"none\" stroke=\"{s}\" stroke-width=\"1.5\"/>",
s = STROKE,
).unwrap();
let mut sign_planets: [Vec<&str>; 12] = Default::default();
for pp in &data.planet_positions {
let si = sign_index_for_house(data.ascendant_sign_index, pp.house);
sign_planets[si].push(&pp.abbreviation);
}
let asc_sign = data.ascendant_sign_index;
for sign_idx in 0..12usize {
let (col, row) = sign_grid_pos(sign_idx);
let x = PAD + col as f64 * CELL;
let y = PAD + row as f64 * CELL;
let cell_cx = x + CELL / 2.0;
let cell_cy = y + CELL / 2.0;
let tx = x + 4.0;
let ty = y + 13.0;
let abbr = SIGN_ABBREV[sign_idx];
write!(
svg,
"<text x=\"{tx}\" y=\"{ty}\" font-size=\"10\" fill=\"{c}\">{abbr}</text>",
c = SIGN_COLOR,
)
.unwrap();
if sign_idx == asc_sign {
let lx1 = x;
let ly1 = y + 20.0;
let lx2 = x + 20.0;
let ly2 = y;
write!(svg,
"<line x1=\"{lx1}\" y1=\"{ly1}\" x2=\"{lx2}\" y2=\"{ly2}\" stroke=\"{c}\" stroke-width=\"2\"/>",
c = ASC_COLOR,
).unwrap();
}
let planets = &sign_planets[sign_idx];
if !planets.is_empty() {
let text = svg_escape(&planets.join(" "));
let py = cell_cy + 5.0;
write!(svg,
"<text x=\"{cell_cx}\" y=\"{py}\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"bold\" fill=\"{c}\">{text}</text>",
c = STROKE,
).unwrap();
}
}
let cy1 = HALF - 5.0;
write!(svg,
"<text x=\"{h}\" y=\"{cy1}\" text-anchor=\"middle\" font-size=\"11\" fill=\"{c}\">Rashi Chart</text>",
h = HALF, c = LABEL_COLOR,
).unwrap();
let cy2 = HALF + 10.0;
write!(svg,
"<text x=\"{h}\" y=\"{cy2}\" text-anchor=\"middle\" font-size=\"9\" fill=\"{c}\">Ayan: {ayan:.2}</text>",
h = HALF, c = FAINT, ayan = safe_longitude(data.ayanamsa_deg),
).unwrap();
svg.push_str("</svg>");
svg
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ChartData, PlanetPosition};
#[test]
fn grid_positions_unique() {
let mut seen = std::collections::HashSet::new();
for i in 0..12 {
let pos = sign_grid_pos(i);
assert!(
seen.insert(pos),
"duplicate grid pos for sign {i}: {:?}",
pos
);
}
}
#[test]
fn grid_positions_within_4x4() {
for i in 0..12 {
let (col, row) = sign_grid_pos(i);
assert!(
col < 4 && row < 4,
"sign {i} at ({col},{row}) out of 4x4 grid"
);
}
}
#[test]
fn pisces_top_left() {
assert_eq!(sign_grid_pos(11), (0, 0), "Pisces should be at (0,0)");
}
#[test]
fn renders_rashi_chart_label() {
let chart = ChartData {
planet_positions: vec![],
house_cusps_deg: [0.0; 12],
ascendant_sign_index: 0,
ayanamsa_deg: 24.0,
};
let svg = render(&chart);
assert!(svg.contains("Rashi Chart"));
}
#[test]
fn ascendant_line_present() {
let chart = ChartData {
planet_positions: vec![PlanetPosition {
abbreviation: "Su".into(),
house: 1,
longitude_deg: 15.0,
}],
house_cusps_deg: [0.0; 12],
ascendant_sign_index: 3,
ayanamsa_deg: 24.0,
};
let svg = render(&chart);
assert!(svg.contains(ASC_COLOR), "ascendant marker color missing");
}
}