— tooltip portal (position:fixed on page)
//
-// Public API under test:
-// NatusWheel.draw(svgEl, data) — renders wheel; attaches hover listeners
-// NatusWheel.clear() — empties the SVG (used in afterEach)
-//
-// Hover contract:
-// mouseover on [data-planet] group → adds .nw-planet--hover class
-// shows #id_natus_tooltip with
-// planet name, in-sign degree, sign name
-// and ℞ if retrograde
-// mouseout on [data-planet] group → removes .nw-planet--hover
-// hides #id_natus_tooltip
+// Click-lock contract:
+// click on [data-planet] group → adds .nw-planet--active class
+// raises group to DOM front
+// shows #id_natus_tooltip with
+// planet name, in-sign degree, sign name,
+// ℞ if retrograde, and "n / total" index
+// click same planet again → removes .nw-planet--active; hides tooltip
+// PRV / NXT buttons in tooltip → cycle to adjacent planet by ecliptic degree
//
// In-sign degree: ecliptic_longitude % 30 (e.g. 338.4° → 8.4° Pisces)
//
// ─────────────────────────────────────────────────────────────────────────────
-// Shared conjunction chart — Sun and Venus 3.4° apart in Gemini
+// Shared chart — Sun (66.7°), Venus (63.3°), Mars (132.0°)
+// Descending-degree (clockwise) order: Mars (132.0) → Sun (66.7) → Venus (63.3)
const CONJUNCTION_CHART = {
planets: {
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
@@ -42,7 +40,7 @@ const CONJUNCTION_CHART = {
house_system: "O",
};
-describe("NatusWheel — planet tooltips", () => {
+describe("NatusWheel — planet click tooltips", () => {
const SYNTHETIC_CHART = {
planets: {
@@ -68,7 +66,6 @@ describe("NatusWheel — planet tooltips", () => {
let svgEl, tooltipEl;
beforeEach(() => {
- // SVG element — D3 draws into this
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.setAttribute("id", "id_natus_svg");
svgEl.setAttribute("width", "400");
@@ -77,7 +74,6 @@ describe("NatusWheel — planet tooltips", () => {
svgEl.style.height = "400px";
document.body.appendChild(svgEl);
- // Tooltip portal — same markup as _natus_overlay.html
tooltipEl = document.createElement("div");
tooltipEl.id = "id_natus_tooltip";
tooltipEl.className = "tt";
@@ -93,15 +89,15 @@ describe("NatusWheel — planet tooltips", () => {
tooltipEl.remove();
});
- // ── T3 ── hover planet shows name / sign / in-sign degree + glow ─────────
+ // ── T3 ── click planet shows name / sign / in-sign degree + glow ──────────
- it("T3: hovering a planet group adds the glow class and shows the tooltip with name, sign, and in-sign degree", () => {
+ it("T3: clicking a planet group adds the active class and shows the tooltip with name, sign, and in-sign degree", () => {
const sun = svgEl.querySelector("[data-planet='Sun']");
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
- sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
- expect(sun.classList.contains("nw-planet--hover")).toBe(true);
+ expect(sun.classList.contains("nw-planet--active")).toBe(true);
expect(tooltipEl.style.display).toBe("block");
const text = tooltipEl.textContent;
@@ -113,39 +109,45 @@ describe("NatusWheel — planet tooltips", () => {
// ── T4 ── retrograde planet shows ℞ ──────────────────────────────────────
- it("T4: hovering a retrograde planet shows ℞ in the tooltip", () => {
+ it("T4: clicking a retrograde planet shows ℞ in the tooltip", () => {
const mercury = svgEl.querySelector("[data-planet='Mercury']");
expect(mercury).not.toBeNull("expected [data-planet='Mercury'] to exist in the SVG");
- mercury.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
+ mercury.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
expect(tooltipEl.textContent).toContain("℞");
});
- // ── T5 ── mouseout hides tooltip and removes glow ─────────────────────────
+ // ── T5 ── clicking same planet again hides tooltip and removes active ──────
- it("T5: mouseout hides the tooltip and removes the glow class", () => {
+ it("T5: clicking the same planet again hides the tooltip and removes the active class", () => {
const sun = svgEl.querySelector("[data-planet='Sun']");
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
- sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
- // relatedTarget is document.body — outside the planet group
- sun.dispatchEvent(new MouseEvent("mouseout", {
- bubbles: true,
- relatedTarget: document.body,
- }));
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("none");
- expect(sun.classList.contains("nw-planet--hover")).toBe(false);
+ expect(sun.classList.contains("nw-planet--active")).toBe(false);
+ });
+
+ // ── T6 ── tooltip shows PRV / NXT buttons ─────────────────────────────────
+
+ it("T6: tooltip contains PRV and NXT buttons after a planet click", () => {
+ const sun = svgEl.querySelector("[data-planet='Sun']");
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ expect(tooltipEl.querySelector(".nw-tt-prv")).not.toBeNull("expected .nw-tt-prv button");
+ expect(tooltipEl.querySelector(".nw-tt-nxt")).not.toBeNull("expected .nw-tt-nxt button");
});
});
-describe("NatusWheel — conjunction features", () => {
+describe("NatusWheel — tick lines, raise, and cycle navigation", () => {
- let svgEl2, tooltipEl, tooltip2El;
+ let svgEl2, tooltipEl;
beforeEach(() => {
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -163,13 +165,6 @@ describe("NatusWheel — conjunction features", () => {
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);
});
@@ -177,10 +172,10 @@ describe("NatusWheel — conjunction features", () => {
NatusWheel.clear();
svgEl2.remove();
tooltipEl.remove();
- tooltip2El.remove();
});
- // ── T7 ── tick extends past zodiac ring ───────────────────────────────────
+ // ── T7 ── tick present in DOM and extends past the zodiac ring ───────────
+ // Visibility is CSS-controlled (opacity-0 by default, revealed on --active).
it("T7: each planet has a tick line whose outer endpoint extends past the sign ring", () => {
const tick = svgEl2.querySelector(".nw-planet-tick");
@@ -195,31 +190,195 @@ describe("NatusWheel — conjunction features", () => {
expect(rOuter).toBeGreaterThan(signOuter);
});
- // ── T8 ── hover raises planet to front ────────────────────────────────────
+ // ── T8 ── click raises planet to front ────────────────────────────────────
- it("T8: hovering a planet raises it to the last DOM position (visually on top)", () => {
+ it("T8: clicking 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 }));
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ venus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
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 ─────────────────────────
+ // ── T9c ── NXT cycles clockwise (to lower ecliptic degree) ──────────────
+ // Descending order: Mars [idx 0] → Sun [idx 1] → Venus [idx 2]
+ // Clicking Sun (idx 1) then NXT should activate Venus (idx 2, lower degree = clockwise).
- it("T9j: hovering a conjunct planet shows a second tooltip for its partner", () => {
+ it("T9c: clicking NXT from Sun shows Venus (next planet clockwise = lower degree)", () => {
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 }));
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
- expect(tooltip2El.style.display).toBe("block");
- expect(tooltip2El.textContent).toContain("Venus");
+ expect(tooltipEl.textContent).toContain("Sun");
+
+ const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
+ expect(nxtBtn).not.toBeNull("expected .nw-tt-nxt button in tooltip");
+
+ nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ expect(tooltipEl.style.display).toBe("block");
+ expect(tooltipEl.textContent).toContain("Venus");
+
+ const venus = svgEl2.querySelector("[data-planet='Venus']");
+ expect(venus.classList.contains("nw-planet--active")).toBe(true);
+ expect(sun.classList.contains("nw-planet--active")).toBe(false);
+ });
+
+ // ── T9n ── PRV cycles counterclockwise (to higher ecliptic degree) ────────
+
+ it("T9n: clicking PRV from Sun shows Mars (previous planet counterclockwise = higher degree)", () => {
+ const sun = svgEl2.querySelector("[data-planet='Sun']");
+ expect(sun).not.toBeNull("expected [data-planet='Sun']");
+
+ sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ const prvBtn = tooltipEl.querySelector(".nw-tt-prv");
+ prvBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ expect(tooltipEl.textContent).toContain("Mars");
+ const mars = svgEl2.querySelector("[data-planet='Mars']");
+ expect(mars.classList.contains("nw-planet--active")).toBe(true);
+ });
+
+ // ── T9w ── NXT wraps clockwise from the last (lowest-degree) planet ───────
+
+ it("T9w: cycling NXT from Venus (lowest degree) wraps clockwise to Mars (highest degree)", () => {
+ // Venus is idx 2 (lowest degree = furthest clockwise); NXT wraps to idx 0 = Mars
+ const venus = svgEl2.querySelector("[data-planet='Venus']");
+ venus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
+ nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+
+ expect(tooltipEl.textContent).toContain("Mars");
+ const mars = svgEl2.querySelector("[data-planet='Mars']");
+ expect(mars.classList.contains("nw-planet--active")).toBe(true);
+ });
+});
+
+// ── Half-wheel tooltip positioning ───────────────────────────────────────────
+//
+// Tooltip lands in the opposite vertical half, with horizontal edge anchored
+// to the item's screen edge on the same L/R side.
+//
+// SVG: 400×400 at viewport origin → centre = (200, 200).
+// Tooltip offsetWidth/Height: 0 in JSDOM (no layout engine) → ttW=ttH=0.
+// Clamping uses svgRect bounds (not window.inner*), so no viewport mock needed.
+// REM = 16 px. Item circle: 20×20 px around mock centre.
+//
+// Vertical results (item circle centre at y):
+// y ≥ 200 (lower half): top = svgCY - REM - ttH = 200 - 16 - 0 = 184
+// y < 200 (upper half): top = svgCY + REM = 200 + 16 = 216
+//
+// Horizontal results (item circle centre at x, radius=10):
+// x < 200 (left side): left = iRect.left = x - 10
+// x ≥ 200 (right side): left = iRect.right - ttW = x + 10 - 0 = x + 10
+// ─────────────────────────────────────────────────────────────────────────────
+
+xdescribe("NatusWheel — half-wheel tooltip positioning", () => {
+
+ const HALF_CHART = {
+ planets: {
+ // Vesta 90° → SVG (200, 274) — BELOW centre
+ // Ceres 270° → SVG (200, 126) — ABOVE centre
+ Vesta: { sign: "Cancer", degree: 90, retrograde: false },
+ Ceres: { sign: "Capricorn", degree: 270, retrograde: false },
+ },
+ houses: {
+ cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
+ asc: 0, mc: 270,
+ },
+ elements: { Fire: 0, Stone: 1, Air: 0, Water: 1, Time: 0, Space: 0 },
+ aspects: [],
+ distinctions: {
+ "1": 0, "2": 0, "3": 0, "4": 1,
+ "5": 0, "6": 0, "7": 0, "8": 0,
+ "9": 0, "10": 1, "11": 0, "12": 0,
+ },
+ house_system: "P",
+ };
+
+ let svgEl3, tooltipEl;
+
+ beforeEach(() => {
+ svgEl3 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svgEl3.setAttribute("id", "id_natus_svg_half");
+ svgEl3.setAttribute("width", "400");
+ svgEl3.setAttribute("height", "400");
+ svgEl3.style.width = "400px";
+ svgEl3.style.height = "400px";
+ document.body.appendChild(svgEl3);
+
+ tooltipEl = document.createElement("div");
+ tooltipEl.id = "id_natus_tooltip";
+ tooltipEl.className = "tt";
+ tooltipEl.style.display = "none";
+ tooltipEl.style.position = "fixed";
+ document.body.appendChild(tooltipEl);
+
+ // Simulate SVG occupying [0,400]×[0,400] in the viewport.
+ // Clamping uses svgRect bounds, so no need to mock window.inner*.
+ spyOn(svgEl3, "getBoundingClientRect").and.returnValue(
+ { left: 0, top: 0, width: 400, height: 400, right: 400, bottom: 400 }
+ );
+
+ NatusWheel.draw(svgEl3, HALF_CHART);
+ });
+
+ afterEach(() => {
+ NatusWheel.clear();
+ svgEl3.remove();
+ tooltipEl.remove();
+ });
+
+ function mockPlanetAt(name, screenX, screenY) {
+ const grp = svgEl3.querySelector(`[data-planet="${name}"]`);
+ const circle = grp && (grp.querySelector("circle") || grp);
+ if (circle) {
+ spyOn(circle, "getBoundingClientRect").and.returnValue({
+ left: screenX - 10, top: screenY - 10,
+ width: 20, height: 20,
+ right: screenX + 10, bottom: screenY + 10,
+ });
+ }
+ }
+
+ // T10a — lower half: lower edge of tooltip sits 1rem above centreline
+ it("T10a: planet in lower half places tooltip lower-edge 1rem above the centreline (top=184)", () => {
+ mockPlanetAt("Vesta", 200, 274);
+ svgEl3.querySelector("[data-planet='Vesta']")
+ .dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ expect(parseFloat(tooltipEl.style.top)).toBe(184);
+ });
+
+ // T10b — upper half: upper edge of tooltip sits 1rem below centreline
+ it("T10b: planet in upper half places tooltip upper-edge 1rem below the centreline (top=216)", () => {
+ mockPlanetAt("Ceres", 200, 126);
+ svgEl3.querySelector("[data-planet='Ceres']")
+ .dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ expect(parseFloat(tooltipEl.style.top)).toBe(216);
+ });
+
+ // T10c — left side: tooltip left edge aligns with item left edge (x=140 → left=130)
+ it("T10c: planet on left side of wheel aligns tooltip left edge with item left edge", () => {
+ mockPlanetAt("Vesta", 140, 274); // itemRect.left = 130
+ svgEl3.querySelector("[data-planet='Vesta']")
+ .dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ expect(parseFloat(tooltipEl.style.left)).toBe(130);
+ });
+
+ // T10d — right side: tooltip right edge aligns with item right edge (x=260 → left=270-ttW=270)
+ it("T10d: planet on right side of wheel aligns tooltip right edge with item right edge", () => {
+ mockPlanetAt("Vesta", 260, 274); // iRect.right=270, ttW=0 → left=270
+ svgEl3.querySelector("[data-planet='Vesta']")
+ .dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ expect(parseFloat(tooltipEl.style.left)).toBe(270);
});
});
diff --git a/src/static_src/scss/_natus.scss b/src/static_src/scss/_natus.scss
index 3f63983..8449f58 100644
--- a/src/static_src/scss/_natus.scss
+++ b/src/static_src/scss/_natus.scss
@@ -431,19 +431,30 @@ html.natus-open .natus-modal-wrap {
.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
.nw-rx { fill: rgba(var(--terUser), 1); }
-// Hover glow (--ninUser) — shared by planet groups and element slice groups
-.nw-planet--hover,
-.nw-element--hover {
+// Hover and active-lock glow — planet groups and element slice groups
+.nw-planet-group,
+.nw-element-group { cursor: pointer; }
+
+.nw-planet-group:hover,
+.nw-planet-group.nw-planet--active,
+.nw-element-group:hover,
+.nw-element-group.nw-element--active {
filter: drop-shadow(0 0 5px rgba(var(--ninUser), 0.9));
- cursor: pointer;
}
-// ── Planet tick lines ─────────────────────────────────────────────────────────
+// ── Planet tick lines — hidden until parent group is active ──────────────────
.nw-planet-tick {
fill: none;
- stroke-width: 3px;
- stroke-opacity: 0.5;
+ stroke-width: 1px;
+ stroke-opacity: 0;
stroke-linecap: round;
+ transition: stroke-opacity 0.15s ease;
+}
+.nw-planet-group.nw-planet--active .nw-planet-tick {
+ stroke: rgba(var(--terUser), 1);
+ stroke-opacity: 0.7;
+ filter: drop-shadow(0 0 3px rgba(var(--terUser), 0.8))
+ drop-shadow(0 0 6px rgba(var(--terUser), 0.4));
}
.nw-planet-tick--au { stroke: rgba(var(--priAu), 1); }
.nw-planet-tick--ag { stroke: rgba(var(--priAg), 1); }
@@ -475,13 +486,22 @@ html.natus-open .natus-modal-wrap {
#id_natus_tooltip_2 {
position: fixed;
z-index: 200;
- pointer-events: none;
+ pointer-events: auto;
padding: 0.75rem 1.5rem;
.tt-title { font-size: 1rem; font-weight: 700; }
.tt-description { font-size: 0.75rem; }
.tt-sign-icon { fill: currentColor; vertical-align: middle; margin-bottom: 0.1em; }
+ .nw-tt-prv,
+ .nw-tt-nxt {
+ position: absolute;
+ bottom: -1rem;
+ margin: 0;
+ }
+ .nw-tt-prv { left: -1rem; }
+ .nw-tt-nxt { right: -1rem; }
+
// Planet title colors — senary (brightest) tier on dark palettes
.tt-title--au { color: rgba(var(--sixAu), 1); } // Sun
.tt-title--ag { color: rgba(var(--sixAg), 1); } // Moon
diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss
index d940b9a..f5a24b4 100644
--- a/src/static_src/scss/rootvars.scss
+++ b/src/static_src/scss/rootvars.scss
@@ -118,15 +118,15 @@
--sixPu: 235, 211, 217;
/* Chroma Palette */
- // red
+ // red (A-Fire)
--priRd: 233, 53, 37;
--secRd: 193, 43, 28;
--terRd: 155, 31, 15;
- // orange
+ // orange (B-Fire)
--priOr: 225, 133, 40;
--secOr: 187, 111, 30;
--terOr: 150, 88, 17;
- // yellow
+ // yellow (
--priYl: 255, 207, 52;
--secYl: 211, 172, 44;
--terYl: 168, 138, 33;
diff --git a/src/static_src/tests/NatusWheelSpec.js b/src/static_src/tests/NatusWheelSpec.js
index bb75400..a3ba5f6 100644
--- a/src/static_src/tests/NatusWheelSpec.js
+++ b/src/static_src/tests/NatusWheelSpec.js
@@ -1,28 +1,26 @@
// ── NatusWheelSpec.js ─────────────────────────────────────────────────────────
//
-// Unit specs for natus-wheel.js — planet hover tooltips.
+// Unit specs for natus-wheel.js — planet/element click-to-lock tooltips.
//
// DOM contract assumed:
//