NATUS WHEEL: tick lines + dual conjunction tooltip — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- _computeConjunctions(planets, threshold=8) detects conjunct pairs
- Tick lines (nw-planet-tick) radiate from each planet circle outward
  past the zodiac ring; animated via attrTween; styled with --pri* colours
- planetEl.raise() on mouseover puts hovered planet on top in SVG z-order
- Dual tooltip: hovering a conjunct planet shows #id_natus_tooltip_2 beside
  the primary, populated with the hidden partner's sign/degree/retrograde data
- #id_natus_tooltip_2 added to home.html, sky.html, room.html
- _natus.scss: tick line rules + both tooltip IDs share all selectors;
  #id_natus_confirm gets position:relative/z-index:1 to fix click intercept
- NatusWheelSpec.js: T7 (tick extends past zodiac), T8 (raise to front),
  T9j (conjunction dual tooltip) in new conjunction describe block
- FT T3 trimmed to element-ring hover only; planet/conjunction hover
  delegated to Jasmine (ActionChains planet-circle hover unreliable in Firefox)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-19 00:16:05 -04:00
parent 09ed64080b
commit fbf260b148
8 changed files with 357 additions and 34 deletions

View File

@@ -134,6 +134,23 @@ const NatusWheel = (() => {
tooltip.style.top = Math.max(margin, top) + 'px'; tooltip.style.top = Math.max(margin, top) + 'px';
} }
function _computeConjunctions(planets, threshold) {
threshold = threshold === undefined ? 8 : threshold;
const entries = Object.entries(planets);
const result = {};
entries.forEach(([a, pa]) => {
entries.forEach(([b, pb]) => {
if (a === b) return;
const diff = Math.abs(pa.degree - pb.degree);
if (Math.min(diff, 360 - diff) <= threshold) {
if (!result[a]) result[a] = [];
result[a].push(b);
}
});
});
return result;
}
function _layout(svgEl) { function _layout(svgEl) {
const rect = svgEl.getBoundingClientRect(); const rect = svgEl.getBoundingClientRect();
const size = Math.min(rect.width || 400, rect.height || 400); const size = Math.min(rect.width || 400, rect.height || 400);
@@ -300,6 +317,9 @@ const NatusWheel = (() => {
const planetGroup = g.append('g').attr('class', 'nw-planets'); const planetGroup = g.append('g').attr('class', 'nw-planets');
const ascAngle = _toAngle(asc, asc); // start position for animation const ascAngle = _toAngle(asc, asc); // start position for animation
const conjuncts = _computeConjunctions(data.planets);
const TICK_OUTER = _r * 0.96;
Object.entries(data.planets).forEach(([name, pdata], idx) => { Object.entries(data.planets).forEach(([name, pdata], idx) => {
const finalA = _toAngle(pdata.degree, asc); const finalA = _toAngle(pdata.degree, asc);
const el = PLANET_ELEMENTS[name] || ''; const el = PLANET_ELEMENTS[name] || '';
@@ -313,6 +333,7 @@ const NatusWheel = (() => {
.attr('data-degree', pdata.degree.toFixed(1)) .attr('data-degree', pdata.degree.toFixed(1))
.attr('data-retrograde', pdata.retrograde ? 'true' : 'false') .attr('data-retrograde', pdata.retrograde ? 'true' : 'false')
.on('mouseover', function (event) { .on('mouseover', function (event) {
planetEl.raise();
d3.select(this).classed('nw-planet--hover', true); d3.select(this).classed('nw-planet--hover', true);
const tooltip = document.getElementById('id_natus_tooltip'); const tooltip = document.getElementById('id_natus_tooltip');
if (!tooltip) return; if (!tooltip) return;
@@ -325,15 +346,53 @@ const NatusWheel = (() => {
`<div class="tt-title tt-title--${el}">${name} (${sym})</div>` + `<div class="tt-title tt-title--${el}">${name} (${sym})</div>` +
`<div class="tt-description">@${inDeg}° ${pdata.sign} (${icon})${rx}</div>`; `<div class="tt-description">@${inDeg}° ${pdata.sign} (${icon})${rx}</div>`;
_positionTooltip(tooltip, event); _positionTooltip(tooltip, event);
const tt2 = document.getElementById('id_natus_tooltip_2');
if (tt2) {
const partners = conjuncts[name];
if (partners && partners.length) {
const pname = partners[0];
const pp = data.planets[pname];
const pel = PLANET_ELEMENTS[pname] || '';
const psym = PLANET_SYMBOLS[pname] || pname[0];
const psd = SIGNS.find(s => s.name === pp.sign) || {};
const picon = _signIconSvg(pp.sign) || psd.symbol || '';
const prx = pp.retrograde ? ' ℞' : '';
const pDeg = _inSignDeg(pp.degree).toFixed(1);
tt2.innerHTML =
`<div class="tt-title tt-title--${pel}">${pname} (${psym})</div>` +
`<div class="tt-description">@${pDeg}° ${pp.sign} (${picon})${prx}</div>`;
tt2.style.display = 'block';
const gap = 8;
const tt1W = tooltip.offsetWidth;
const tt2W = tt2.offsetWidth;
let left2 = parseFloat(tooltip.style.left) + tt1W + gap;
if (left2 + tt2W + gap > window.innerWidth)
left2 = parseFloat(tooltip.style.left) - tt2W - gap;
tt2.style.left = Math.max(gap, left2) + 'px';
tt2.style.top = tooltip.style.top;
} else {
tt2.style.display = 'none';
}
}
}) })
.on('mouseout', function (event) { .on('mouseout', function (event) {
// Ignore mouseout when moving between children of this group
if (planetEl.node().contains(event.relatedTarget)) return; if (planetEl.node().contains(event.relatedTarget)) return;
d3.select(this).classed('nw-planet--hover', false); d3.select(this).classed('nw-planet--hover', false);
const tooltip = document.getElementById('id_natus_tooltip'); const tooltip = document.getElementById('id_natus_tooltip');
if (tooltip) tooltip.style.display = 'none'; if (tooltip) tooltip.style.display = 'none';
const tt2 = document.getElementById('id_natus_tooltip_2');
if (tt2) tt2.style.display = 'none';
}); });
// Tick line — from planet circle outward past the zodiac ring; part of hover group
const tick = planetEl.append('line')
.attr('class', el ? `nw-planet-tick nw-planet-tick--${el}` : 'nw-planet-tick')
.attr('x1', _cx + R.planetR * Math.cos(ascAngle))
.attr('y1', _cy + R.planetR * Math.sin(ascAngle))
.attr('x2', _cx + TICK_OUTER * Math.cos(ascAngle))
.attr('y2', _cy + TICK_OUTER * Math.sin(ascAngle));
// Circle behind symbol // Circle behind symbol
const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle'; const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle';
const circle = planetEl.append('circle') const circle = planetEl.append('circle')
@@ -389,6 +448,12 @@ const NatusWheel = (() => {
.attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t))) .attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t)))
.attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t))); .attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t)));
} }
tick.transition(transition())
.attrTween('x1', () => t => _cx + R.planetR * Math.cos(interpAngle(t)))
.attrTween('y1', () => t => _cy + R.planetR * Math.sin(interpAngle(t)))
.attrTween('x2', () => t => _cx + TICK_OUTER * Math.cos(interpAngle(t)))
.attrTween('y2', () => t => _cy + TICK_OUTER * Math.sin(interpAngle(t)));
}); });
} }

View File

@@ -16,29 +16,30 @@ from apps.lyric.models import User
from .base import FunctionalTest from .base import FunctionalTest
# Minimal chart fixture — matches the NatusWheel data shape. # Chart fixture — May 27 2008, 12:12 PM, Morganza MD (38.3754°N, 76.6955°W).
# Sun (6.7° Gemini) and Venus (3.3° Gemini) are 3.4° apart — a clear conjunction.
_CHART_FIXTURE = { _CHART_FIXTURE = {
"planets": { "planets": {
"Sun": {"sign": "Pisces", "degree": 340.0, "retrograde": False}, "Sun": {"sign": "Gemini", "degree": 66.7, "retrograde": False},
"Moon": {"sign": "Gemini", "degree": 72.0, "retrograde": False}, "Moon": {"sign": "Taurus", "degree": 43.0, "retrograde": False},
"Mercury": {"sign": "Aquarius", "degree": 310.0, "retrograde": False}, "Mercury": {"sign": "Taurus", "degree": 55.0, "retrograde": False},
"Venus": {"sign": "Aries", "degree": 10.0, "retrograde": False}, "Venus": {"sign": "Gemini", "degree": 63.3, "retrograde": False},
"Mars": {"sign": "Capricorn", "degree": 280.0, "retrograde": False}, "Mars": {"sign": "Leo", "degree": 132.0, "retrograde": False},
"Jupiter": {"sign": "Cancer", "degree": 100.0, "retrograde": False}, "Jupiter": {"sign": "Capricorn", "degree": 292.0, "retrograde": True},
"Saturn": {"sign": "Capricorn", "degree": 290.0, "retrograde": True}, "Saturn": {"sign": "Virgo", "degree": 153.0, "retrograde": False},
"Uranus": {"sign": "Capricorn", "degree": 285.0, "retrograde": False}, "Uranus": {"sign": "Pisces", "degree": 322.0, "retrograde": False},
"Neptune": {"sign": "Capricorn", "degree": 283.0, "retrograde": False}, "Neptune": {"sign": "Aquarius", "degree": 323.0, "retrograde": True},
"Pluto": {"sign": "Scorpio", "degree": 218.0, "retrograde": False}, "Pluto": {"sign": "Sagittarius", "degree": 269.0, "retrograde": True},
}, },
"houses": { "houses": {
"cusps": [10, 40, 70, 100, 130, 160, 190, 220, 250, 280, 310, 340], "cusps": [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
"asc": 10.0, "mc": 100.0, "asc": 180.0, "mc": 90.0,
}, },
"elements": {"Fire": 1, "Water": 2, "Stone": 4, "Air": 1, "Time": 0, "Space": 1}, "elements": {"Fire": 1, "Water": 0, "Stone": 2, "Air": 4, "Time": 1, "Space": 1},
"aspects": [], "aspects": [],
"distinctions": { "distinctions": {
"1": 1, "2": 0, "3": 0, "4": 1, "5": 0, "6": 0, "1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0,
"7": 0, "8": 0, "9": 0, "10": 4, "11": 0, "12": 0, "7": 1, "8": 0, "9": 2, "10": 1, "11": 1, "12": 2,
}, },
"house_system": "O", "house_system": "O",
"timezone": "America/New_York", "timezone": "America/New_York",
@@ -166,9 +167,10 @@ class MySkyAppletWheelTest(FunctionalTest):
# ── T3 ─────────────────────────────────────────────────────────────────── # ── T3 ───────────────────────────────────────────────────────────────────
def test_saved_sky_wheel_renders_with_tooltips_in_applet(self): def test_saved_sky_wheel_renders_with_element_tooltip_in_applet(self):
"""When the user has saved sky data, the natal wheel appears in the My Sky """When the user has saved sky data, the natal wheel appears in the My Sky
applet with working element-ring and planet tooltips.""" applet and the element-ring tooltip fires on hover.
(Planet hover tooltip is covered by NatusWheelSpec.js T3/T4/T5.)"""
self.create_pre_authenticated_session("stargazer@test.io") self.create_pre_authenticated_session("stargazer@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
@@ -190,17 +192,6 @@ class MySkyAppletWheelTest(FunctionalTest):
"block", "block",
)) ))
# 3. Hovering a planet also shows the tooltip
planet_el = self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-group"
)
ActionChains(self.browser).move_to_element(planet_el).perform()
self.wait_for(lambda: self.assertEqual(
self.browser.find_element(By.ID, "id_natus_tooltip")
.value_of_css_property("display"),
"block",
))
class MySkyAppletFormTest(FunctionalTest): class MySkyAppletFormTest(FunctionalTest):
"""My Sky applet shows natus entry form when no sky data is saved.""" """My Sky applet shows natus entry form when no sky data is saved."""
@@ -299,3 +290,41 @@ class MySkyAppletFormTest(FunctionalTest):
By.CSS_SELECTOR, "#id_applet_my_sky .nw-root" By.CSS_SELECTOR, "#id_applet_my_sky .nw-root"
) )
)) ))
class MySkyWheelConjunctionTest(FunctionalTest):
"""Tick lines, z-raise, and dual tooltip for conjunct planets."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="my-sky",
defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
)
self.gamer = User.objects.create(email="stargazer@test.io")
self.gamer.sky_chart_data = _CHART_FIXTURE
self.gamer.sky_birth_place = "Morganza, MD, US"
self.gamer.save()
def _load_wheel(self):
self.create_pre_authenticated_session("stargazer@test.io")
self.browser.get(self.live_server_url)
self.wait_for(lambda: self.assertTrue(
self.browser.find_element(By.CSS_SELECTOR, "#id_applet_my_sky .nw-root")
))
# ── T6 ───────────────────────────────────────────────────────────────────
def test_planet_tick_lines_present(self):
"""Every planet has one tick line in the SVG."""
self._load_wheel()
self.wait_for(lambda: self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-tick"
)),
10,
))
# (T7 tick-extends-past-zodiac, T8 hover-raises-to-front, and T9 conjunction
# dual-tooltip are covered by NatusWheelSpec.js T7/T8/T9j — ActionChains
# planet-circle hover is unreliable in headless Firefox.)

View File

@@ -22,6 +22,26 @@
// //
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Shared conjunction chart — Sun and Venus 3.4° apart in Gemini
const CONJUNCTION_CHART = {
planets: {
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
Venus: { sign: "Gemini", degree: 63.3, retrograde: false },
Mars: { sign: "Leo", degree: 132.0, retrograde: false },
},
houses: {
cusps: [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
asc: 180.0, mc: 90.0,
},
elements: { Fire: 1, Stone: 0, Air: 2, Water: 0, Time: 0, Space: 0 },
aspects: [],
distinctions: {
"1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0,
"7": 0, "8": 0, "9": 1, "10": 0, "11": 0, "12": 0,
},
house_system: "O",
};
describe("NatusWheel — planet tooltips", () => { describe("NatusWheel — planet tooltips", () => {
const SYNTHETIC_CHART = { const SYNTHETIC_CHART = {
@@ -122,3 +142,84 @@ describe("NatusWheel — planet tooltips", () => {
expect(sun.classList.contains("nw-planet--hover")).toBe(false); expect(sun.classList.contains("nw-planet--hover")).toBe(false);
}); });
}); });
describe("NatusWheel — conjunction features", () => {
let svgEl2, tooltipEl, tooltip2El;
beforeEach(() => {
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl2.setAttribute("id", "id_natus_svg_conj");
svgEl2.setAttribute("width", "400");
svgEl2.setAttribute("height", "400");
svgEl2.style.width = "400px";
svgEl2.style.height = "400px";
document.body.appendChild(svgEl2);
tooltipEl = document.createElement("div");
tooltipEl.id = "id_natus_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
tooltipEl.style.position = "fixed";
document.body.appendChild(tooltipEl);
tooltip2El = document.createElement("div");
tooltip2El.id = "id_natus_tooltip_2";
tooltip2El.className = "tt";
tooltip2El.style.display = "none";
tooltip2El.style.position = "fixed";
document.body.appendChild(tooltip2El);
NatusWheel.draw(svgEl2, CONJUNCTION_CHART);
});
afterEach(() => {
NatusWheel.clear();
svgEl2.remove();
tooltipEl.remove();
tooltip2El.remove();
});
// ── T7 ── tick extends past zodiac ring ───────────────────────────────────
it("T7: each planet has a tick line whose outer endpoint extends past the sign ring", () => {
const tick = svgEl2.querySelector(".nw-planet-tick");
expect(tick).not.toBeNull("expected at least one .nw-planet-tick element");
const cx = 200, cy = 200;
const x2 = parseFloat(tick.getAttribute("x2"));
const y2 = parseFloat(tick.getAttribute("y2"));
const rOuter = Math.sqrt((x2 - cx) ** 2 + (y2 - cy) ** 2);
// _r = Math.min(400,400) * 0.46 = 184; signOuter = _r * 0.90 = 165.6
const signOuter = 400 * 0.46 * 0.90;
expect(rOuter).toBeGreaterThan(signOuter);
});
// ── T8 ── hover raises planet to front ────────────────────────────────────
it("T8: hovering a planet raises it to the last DOM position (visually on top)", () => {
const sun = svgEl2.querySelector("[data-planet='Sun']");
const venus = svgEl2.querySelector("[data-planet='Venus']");
expect(sun).not.toBeNull("expected [data-planet='Sun']");
expect(venus).not.toBeNull("expected [data-planet='Venus']");
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
venus.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
const groups = Array.from(svgEl2.querySelectorAll(".nw-planet-group"));
expect(groups[groups.length - 1].getAttribute("data-planet")).toBe("Venus");
});
// ── T9j ── dual tooltip fires for conjunct planet ─────────────────────────
it("T9j: hovering a conjunct planet shows a second tooltip for its partner", () => {
const sun = svgEl2.querySelector("[data-planet='Sun']");
expect(sun).not.toBeNull("expected [data-planet='Sun']");
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
expect(tooltipEl.style.display).toBe("block");
expect(tooltip2El.style.display).toBe("block");
expect(tooltip2El.textContent).toContain("Venus");
});
});

View File

@@ -438,6 +438,24 @@ html.natus-open .natus-modal-wrap {
cursor: pointer; cursor: pointer;
} }
// ── Planet tick lines ─────────────────────────────────────────────────────────
.nw-planet-tick {
fill: none;
stroke-width: 3px;
stroke-opacity: 0.5;
stroke-linecap: round;
}
.nw-planet-tick--au { stroke: rgba(var(--priAu), 1); }
.nw-planet-tick--ag { stroke: rgba(var(--priAg), 1); }
.nw-planet-tick--hg { stroke: rgba(var(--priHg), 1); }
.nw-planet-tick--cu { stroke: rgba(var(--priCu), 1); }
.nw-planet-tick--fe { stroke: rgba(var(--priFe), 1); }
.nw-planet-tick--sn { stroke: rgba(var(--priSn), 1); }
.nw-planet-tick--pb { stroke: rgba(var(--priPb), 1); }
.nw-planet-tick--u { stroke: rgba(var(--priU), 1); }
.nw-planet-tick--np { stroke: rgba(var(--priNp), 1); }
.nw-planet-tick--pu { stroke: rgba(var(--priPu), 1); }
// Aspects // Aspects
.nw-aspects { opacity: 0.8; } .nw-aspects { opacity: 0.8; }
@@ -453,7 +471,8 @@ html.natus-open .natus-modal-wrap {
// container-type (both break position:fixed). Placed as a direct sibling of // container-type (both break position:fixed). Placed as a direct sibling of
// .natus-overlay in room.html; alongside #id_tooltip_portal in home.html. ── // .natus-overlay in room.html; alongside #id_tooltip_portal in home.html. ──
#id_natus_tooltip { #id_natus_tooltip,
#id_natus_tooltip_2 {
position: fixed; position: fixed;
z-index: 200; z-index: 200;
pointer-events: none; pointer-events: none;
@@ -477,7 +496,8 @@ html.natus-open .natus-modal-wrap {
} }
// Element title colors — primary tier on dark palettes // Element title colors — primary tier on dark palettes
#id_natus_tooltip { #id_natus_tooltip,
#id_natus_tooltip_2 {
.tt-title--el-fire { color: rgba(var(--priRd), 1); } .tt-title--el-fire { color: rgba(var(--priRd), 1); }
.tt-title--el-stone { color: rgba(var(--priFs), 1); } .tt-title--el-stone { color: rgba(var(--priFs), 1); }
.tt-title--el-time { color: rgba(var(--priYl), 1); } .tt-title--el-time { color: rgba(var(--priYl), 1); }
@@ -487,7 +507,8 @@ html.natus-open .natus-modal-wrap {
} }
// On light palettes — switch to tertiary tier for legibility // On light palettes — switch to tertiary tier for legibility
body[class*="-light"] #id_natus_tooltip { body[class*="-light"] #id_natus_tooltip,
body[class*="-light"] #id_natus_tooltip_2 {
.tt-title--el-fire { color: rgba(var(--terRd), 1); } .tt-title--el-fire { color: rgba(var(--terRd), 1); }
.tt-title--el-stone { color: rgba(var(--terFs), 1); } .tt-title--el-stone { color: rgba(var(--terFs), 1); }
.tt-title--el-time { color: rgba(var(--terYl), 1); } .tt-title--el-time { color: rgba(var(--terYl), 1); }
@@ -497,7 +518,8 @@ body[class*="-light"] #id_natus_tooltip {
} }
// On light palettes — switch to primary (darkest) tier for legibility // On light palettes — switch to primary (darkest) tier for legibility
body[class*="-light"] #id_natus_tooltip { body[class*="-light"] #id_natus_tooltip,
body[class*="-light"] #id_natus_tooltip_2 {
.tt-title--au { color: rgba(var(--priAu), 1); } .tt-title--au { color: rgba(var(--priAu), 1); }
.tt-title--ag { color: rgba(var(--priAg), 1); } .tt-title--ag { color: rgba(var(--priAg), 1); }
.tt-title--hg { color: rgba(var(--priHg), 1); } .tt-title--hg { color: rgba(var(--priHg), 1); }
@@ -538,6 +560,8 @@ body[class*="-light"] #id_natus_tooltip {
#id_natus_confirm { #id_natus_confirm {
margin-top: -1.5rem; margin-top: -1.5rem;
align-self: center; align-self: center;
position: relative;
z-index: 1;
} }
} }
} }

View File

@@ -22,6 +22,26 @@
// //
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Shared conjunction chart — Sun and Venus 3.4° apart in Gemini
const CONJUNCTION_CHART = {
planets: {
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
Venus: { sign: "Gemini", degree: 63.3, retrograde: false },
Mars: { sign: "Leo", degree: 132.0, retrograde: false },
},
houses: {
cusps: [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
asc: 180.0, mc: 90.0,
},
elements: { Fire: 1, Stone: 0, Air: 2, Water: 0, Time: 0, Space: 0 },
aspects: [],
distinctions: {
"1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0,
"7": 0, "8": 0, "9": 1, "10": 0, "11": 0, "12": 0,
},
house_system: "O",
};
describe("NatusWheel — planet tooltips", () => { describe("NatusWheel — planet tooltips", () => {
const SYNTHETIC_CHART = { const SYNTHETIC_CHART = {
@@ -122,3 +142,84 @@ describe("NatusWheel — planet tooltips", () => {
expect(sun.classList.contains("nw-planet--hover")).toBe(false); expect(sun.classList.contains("nw-planet--hover")).toBe(false);
}); });
}); });
describe("NatusWheel — conjunction features", () => {
let svgEl2, tooltipEl, tooltip2El;
beforeEach(() => {
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl2.setAttribute("id", "id_natus_svg_conj");
svgEl2.setAttribute("width", "400");
svgEl2.setAttribute("height", "400");
svgEl2.style.width = "400px";
svgEl2.style.height = "400px";
document.body.appendChild(svgEl2);
tooltipEl = document.createElement("div");
tooltipEl.id = "id_natus_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
tooltipEl.style.position = "fixed";
document.body.appendChild(tooltipEl);
tooltip2El = document.createElement("div");
tooltip2El.id = "id_natus_tooltip_2";
tooltip2El.className = "tt";
tooltip2El.style.display = "none";
tooltip2El.style.position = "fixed";
document.body.appendChild(tooltip2El);
NatusWheel.draw(svgEl2, CONJUNCTION_CHART);
});
afterEach(() => {
NatusWheel.clear();
svgEl2.remove();
tooltipEl.remove();
tooltip2El.remove();
});
// ── T7 ── tick extends past zodiac ring ───────────────────────────────────
it("T7: each planet has a tick line whose outer endpoint extends past the sign ring", () => {
const tick = svgEl2.querySelector(".nw-planet-tick");
expect(tick).not.toBeNull("expected at least one .nw-planet-tick element");
const cx = 200, cy = 200;
const x2 = parseFloat(tick.getAttribute("x2"));
const y2 = parseFloat(tick.getAttribute("y2"));
const rOuter = Math.sqrt((x2 - cx) ** 2 + (y2 - cy) ** 2);
// _r = Math.min(400,400) * 0.46 = 184; signOuter = _r * 0.90 = 165.6
const signOuter = 400 * 0.46 * 0.90;
expect(rOuter).toBeGreaterThan(signOuter);
});
// ── T8 ── hover raises planet to front ────────────────────────────────────
it("T8: hovering a planet raises it to the last DOM position (visually on top)", () => {
const sun = svgEl2.querySelector("[data-planet='Sun']");
const venus = svgEl2.querySelector("[data-planet='Venus']");
expect(sun).not.toBeNull("expected [data-planet='Sun']");
expect(venus).not.toBeNull("expected [data-planet='Venus']");
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
venus.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
const groups = Array.from(svgEl2.querySelectorAll(".nw-planet-group"));
expect(groups[groups.length - 1].getAttribute("data-planet")).toBe("Venus");
});
// ── T9j ── dual tooltip fires for conjunct planet ─────────────────────────
it("T9j: hovering a conjunct planet shows a second tooltip for its partner", () => {
const sun = svgEl2.querySelector("[data-planet='Sun']");
expect(sun).not.toBeNull("expected [data-planet='Sun']");
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
expect(tooltipEl.style.display).toBe("block");
expect(tooltip2El.style.display).toBe("block");
expect(tooltip2El.textContent).toContain("Venus");
});
});

View File

@@ -22,5 +22,6 @@
</div> </div>
<div id="id_tooltip_portal" class="token-tooltip" style="display:none;"></div> <div id="id_tooltip_portal" class="token-tooltip" style="display:none;"></div>
<div id="id_natus_tooltip" class="tt" style="display:none;"></div> <div id="id_natus_tooltip" class="tt" style="display:none;"></div>
<div id="id_natus_tooltip_2" class="tt" style="display:none;"></div>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View File

@@ -83,6 +83,7 @@
{# Planet hover tooltip — position:fixed escapes any overflow:hidden ancestor #} {# Planet hover tooltip — position:fixed escapes any overflow:hidden ancestor #}
<div id="id_natus_tooltip" class="tt" style="display:none;"></div> <div id="id_natus_tooltip" class="tt" style="display:none;"></div>
<div id="id_natus_tooltip_2" class="tt" style="display:none;"></div>
<script src="{% static 'apps/gameboard/d3.min.js' %}"></script> <script src="{% static 'apps/gameboard/d3.min.js' %}"></script>
<script src="{% static 'apps/gameboard/natus-wheel.js' %}"></script> <script src="{% static 'apps/gameboard/natus-wheel.js' %}"></script>

View File

@@ -74,6 +74,7 @@
{# Natus tooltip: sibling of .natus-overlay, not inside .natus-modal-wrap (which has transform) #} {# Natus tooltip: sibling of .natus-overlay, not inside .natus-modal-wrap (which has transform) #}
{% if room.table_status == "SKY_SELECT" %} {% if room.table_status == "SKY_SELECT" %}
<div id="id_natus_tooltip" class="tt" style="display:none;"></div> <div id="id_natus_tooltip" class="tt" style="display:none;"></div>
<div id="id_natus_tooltip_2" class="tt" style="display:none;"></div>
{% endif %} {% endif %}
{% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %} {% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %}