natus wheel: ASC/MC angles — tooltips, aspect lines, section headers, tooltip polish

- ASC/MC clickable w. DON/DOFF aspect lines (fixed: open w.o. lines; DON/DOFF
  both work; angles ring handled in _toggleAspects; lines origin at R.planetR)
- btn-disabled click-through fix: pointer-events:auto on DON/DOFF; bounding-rect
  workaround removed
- planet tooltip: applying ⇥ left, separating ↦ right; sign shown for angle partners
- sign tooltip: Planets + Cusps section headers; ordinal house + domain; em-dash fallback
- house tooltip: Planets header; Angular/Succedent/Cadent + phase labels; em-dash fallback
- element tooltips: Planets header for Fire/Stone/Air/Water; Stellium/Parade as
  section-header labels; compact single-stellium Tempo; Parade sign : planets format
- tt-ord: no negative margin in .tt-angle-house context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-22 00:58:19 -04:00
parent 5c05bd6552
commit 0b2320e39b
6 changed files with 752 additions and 109 deletions

View File

@@ -103,12 +103,44 @@ const NatusWheel = (() => {
'Cooperation', 'Regeneration', 'Enterprise', 'Career', 'Reward', 'Reprisal',
];
const ANGLE_DEFS = {
ASC: { label: 'Ascendant', sym: 'ASC', house: 1 },
MC: { label: 'Midheaven', sym: 'MC', house: 10 },
};
// Major aspect angles and their exact expected separations
const MAJOR_ASPECTS = [
{ type: 'Conjunction', angle: 0 },
{ type: 'Sextile', angle: 60 },
{ type: 'Square', angle: 90 },
{ type: 'Trine', angle: 120 },
{ type: 'Opposition', angle: 180 },
];
const ANGLE_ORB = 10;
// Cardinal / Fixed / Mutable — parallel index to SIGNS
const SIGN_MODALITIES = [
'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
];
// Angular / Succedent / Cadent — 1-indexed (index 0 unused)
const HOUSE_QUADRANT = [
'', 'Angular', 'Succedent', 'Cadent',
'Angular', 'Succedent', 'Cadent',
'Angular', 'Succedent', 'Cadent',
'Angular', 'Succedent', 'Cadent',
];
// Houses 13: Juvenescence; 46: Adolescence; 79: Maturescence; 1012: Senescence
const HOUSE_PHASE = [
'', 'Juvenescence', 'Juvenescence', 'Juvenescence',
'Adolescence', 'Adolescence', 'Adolescence',
'Maturescence', 'Maturescence', 'Maturescence',
'Senescence', 'Senescence', 'Senescence',
];
// ── State ─────────────────────────────────────────────────────────────────
let _svg = null;
@@ -125,12 +157,13 @@ const NatusWheel = (() => {
// ── Cycle state ────────────────────────────────────────────────────────────
let _activeRing = null; // 'planets' | 'elements' | 'signs' | 'houses' | null
let _activeRing = null; // 'planets' | 'elements' | 'signs' | 'houses' | 'angles' | null
let _activeIdx = null; // index within the active ring's sorted list
let _planetItems = []; // [{name, degree}] sorted by ecliptic degree ascending
let _elementItems = []; // [{key}] in ELEMENT_ORDER
let _signItems = []; // [{name, symbol, element}] in SIGNS order
let _houseItems = []; // [{num, label}] houses 112
let _angleItems = []; // [{name, label, house}] — ASC and MC
// Tooltip DOM refs — set by _injectTooltipControls() on each draw().
let _tooltipEl = null;
@@ -214,6 +247,29 @@ const NatusWheel = (() => {
return `<svg viewBox="0 0 640 640" width="1em" height="1em" class="tt-sign-icon" aria-hidden="true"><path d="${d}"/></svg>`;
}
/** Planet symbol wrapped in its metal-color span. data-planet allows test queries. */
function _pSym(planetName) {
const el = PLANET_ELEMENTS[planetName] || '';
const sym = PLANET_SYMBOLS[planetName] || planetName[0];
return el
? `<span class="tt-title--${el}" data-planet="${planetName}">${sym}</span>`
: `<span data-planet="${planetName}">${sym}</span>`;
}
/** Sign icon SVG wrapped in its element-color span. */
function _signIcon(signName) {
const sign = SIGNS.find(s => s.name === signName) || {};
const el = (sign.element || '').toLowerCase();
const svg = _signIconSvg(signName) || sign.symbol || '';
return el ? `<span class="tt-sign-icon-wrap--${el}">${svg}</span>` : svg;
}
/** Ordinal suffix HTML for a house number, e.g. 2 → "2<span class=tt-ord>nd</span>". */
function _houseOrdinal(n) {
const sfx = n === 1 ? 'st' : n === 2 ? 'nd' : n === 3 ? 'rd' : 'th';
return `${n}<span class="tt-ord">${sfx}</span>`;
}
/** <img> for an element-square badge (Ardor.svg etc.). */
function _elementSquareImg(elementKey) {
const info = ELEMENT_INFO[elementKey];
@@ -261,6 +317,10 @@ const NatusWheel = (() => {
num: i + 1,
label: HOUSE_LABELS[i + 1],
}));
_angleItems = [
{ name: 'ASC', deg: data.houses.asc },
{ name: 'MC', deg: data.houses.mc },
];
}
/** Clear all active-lock classes and reset cycle state. */
@@ -268,6 +328,9 @@ const NatusWheel = (() => {
if (_svg) {
_svg.selectAll('.nw-planet-group').classed('nw-planet--active', false);
_svg.selectAll('.nw-element-group').classed('nw-element--active', false);
_svg.selectAll('.nw-sign-group').classed('nw-sign--active', false);
_svg.selectAll('[data-house]').classed('nw-house--active', false);
_svg.selectAll('[data-angle]').classed('nw-angle--active', false);
}
_activeRing = null;
_activeIdx = null;
@@ -325,6 +388,97 @@ const NatusWheel = (() => {
}
}
/** Draw aspect lines from an angle (ASC/MC) to all planets in its aspect index. */
function _showAngleAspects(angleName) {
if (!_aspectGroup || !_currentData) return;
_clearAspectLines();
const angleDeg = angleName === 'ASC' ? _currentData.houses.asc : _currentData.houses.mc;
const asc = _currentData.houses.asc;
const a1 = _toAngle(angleDeg, asc);
const r1 = R.planetR;
(_aspectIndex[angleName] || []).forEach(({ partner, type, applying_planet }) => {
const pdata = _currentData.planets[partner];
if (!pdata) return;
const a2 = _toAngle(pdata.degree, asc);
const style = ASPECT_STYLES[type] || { dash: 'none', width: 0.8 };
const color = _aspectColor(applying_planet);
const line = _aspectGroup.append('line')
.attr('x1', _cx + r1 * Math.cos(a1))
.attr('y1', _cy + r1 * Math.sin(a1))
.attr('x2', _cx + R.planetR * Math.cos(a2))
.attr('y2', _cy + R.planetR * Math.sin(a2))
.attr('stroke', color)
.attr('stroke-width', style.width * 2)
.attr('stroke-opacity', 0.9);
if (style.dash !== 'none') line.attr('stroke-dasharray', style.dash);
});
}
/** Lock-activate an angle (ASC or MC) — show tooltip + draw aspect lines. */
function _activateAngle(angleName) {
if (_activeRing === 'angles' && _activeIdx === angleName) {
_closeTooltip();
return;
}
_clearActive();
_activeRing = 'angles';
_activeIdx = angleName;
if (_svg) _svg.select(`[data-angle="${angleName}"]`).classed('nw-angle--active', true);
const def = ANGLE_DEFS[angleName] || {};
const angleDeg = angleName === 'ASC' ? _currentData.houses.asc : _currentData.houses.mc;
const signIdx = Math.floor(angleDeg / 30) % 12;
const sign = SIGNS[signIdx] || {};
const inDeg = (angleDeg % 30).toFixed(1);
const icon = _signIconSvg(sign.name) || sign.symbol || '';
const elKey = (sign.element || '').toLowerCase();
let aspectHtml = '';
const myAspects = _aspectIndex[angleName] || [];
if (myAspects.length) {
aspectHtml = '<small class="tt-aspects">';
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
const ppdata = _currentData.planets[partner] || {};
const asym = ASPECT_SYMBOLS[type] || type;
const lineSvg = _aspectLineSvg(type, applying_planet);
aspectHtml +=
`<div class="tt-asp-row">` +
`${lineSvg} ${asym} ${_pSym(partner)} <span class="tt-asp-in">in</span> ${_signIcon(ppdata.sign)}` +
` <span class="tt-asp-orb">${APPLY_SYM} ${orb}°</span>` +
`</div>`;
});
aspectHtml += '</small>';
}
if (_ttBody) {
_ttBody.innerHTML =
`<div class="tt-planet-header">` +
`<span class="tt-title">${def.label || angleName}</span>` +
`<span class="tt-planet-sym tt-angle-sym">${def.sym || angleName}</span>` +
`</div>` +
`<div class="tt-planet-loc">` +
`<span>@${inDeg}° ${sign.name || ''}</span>` +
`<span class="tt-planet-sign-icon tt-planet-sign-icon--${elKey}">${icon}</span>` +
`</div>` +
`<div class="tt-angle-house">${_houseOrdinal(def.house)} House <span class="tt-dim">(${HOUSE_LABELS[def.house] || ''})</span></div>` +
aspectHtml;
}
_aspectsVisible = false;
_positionTooltipAtItem('angles', angleName);
if (_tooltipEl) {
_tooltipEl.style.display = 'block';
_tooltipEl.querySelector('.nw-asp-don')?.style.removeProperty('display');
_tooltipEl.querySelector('.nw-asp-doff')?.style.removeProperty('display');
}
_updateAspectToggleUI();
}
/**
* Position the tooltip in the vertical half of the wheel opposite to the
* clicked planet/element.
@@ -357,6 +511,9 @@ const NatusWheel = (() => {
} else if (ring === 'houses') {
const grp = svgNode.querySelector(`[data-house="${_houseItems[idx].num}"]`);
el = grp && (grp.querySelector('path') || grp);
} else if (ring === 'angles') {
const grp = svgNode.querySelector(`[data-angle="${idx}"]`);
el = grp && (grp.querySelector('text') || grp);
}
if (el) iRect = el.getBoundingClientRect();
}
@@ -404,16 +561,29 @@ const NatusWheel = (() => {
if (myAspects.length) {
aspectHtml = '<small class="tt-aspects">';
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
const psym = PLANET_SYMBOLS[partner] || partner[0];
const ppdata = _currentData.planets[partner] || {};
const sicon = _signIconSvg(ppdata.sign) || (SIGNS.find(s => s.name === ppdata.sign) || {}).symbol || '';
const isAngle = partner === 'ASC' || partner === 'MC';
const ppdata = isAngle ? {} : (_currentData.planets[partner] || {});
const asym = ASPECT_SYMBOLS[type] || type;
const dirsym = applying_planet === item.name ? APPLY_SYM : SEP_SYM;
const lineSvg = _aspectLineSvg(type, applying_planet);
const applying = applying_planet === item.name;
const lineSvg = _aspectLineSvg(type, applying_planet);
const partnerSym = isAngle
? `<span class="tt-title tt-angle-sym">${(ANGLE_DEFS[partner] || {}).sym || partner}</span>`
: _pSym(partner);
let signPart;
if (isAngle) {
const aDeg = partner === 'ASC' ? _currentData.houses.asc : _currentData.houses.mc;
const aSign = SIGNS[Math.floor(aDeg / 30) % 12];
signPart = aSign ? ` <span class="tt-asp-in">in</span> ${_signIcon(aSign.name)}` : '';
} else {
signPart = ` <span class="tt-asp-in">in</span> ${_signIcon(ppdata.sign)}`;
}
const orbHtml = applying
? `${APPLY_SYM} ${orb}°`
: `${orb}° ${SEP_SYM}`;
aspectHtml +=
`<div class="tt-asp-row">` +
`${lineSvg} ${asym} ${psym} <span class="tt-asp-in">in</span> ${sicon}` +
` <span class="tt-asp-orb">(${dirsym} ${orb}°)</span>` +
`${lineSvg} ${asym} ${partnerSym}${signPart}` +
` <span class="tt-asp-orb">${orbHtml}</span>` +
`</div>`;
});
aspectHtml += '</small>';
@@ -427,7 +597,7 @@ const NatusWheel = (() => {
`</div>` +
`<div class="tt-planet-loc">` +
`<span>@${inDeg}° ${pdata.sign}${rx}</span>` +
`<span class="tt-planet-sign-icon">${icon}</span>` +
`<span class="tt-planet-sign-icon tt-planet-sign-icon--${(signData.element || '').toLowerCase()}">${icon}</span>` +
`</div>` +
aspectHtml;
}
@@ -464,14 +634,13 @@ const NatusWheel = (() => {
if (CLASSIC_ELEMENTS.has(item.key)) {
const contribs = elData.contributors || [];
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
bodyHtml += `<div class="tt-sign-section-header">Planets</div>`;
if (contribs.length) {
bodyHtml += '<div class="tt-el-contribs">';
contribs.forEach(c => {
const psym = PLANET_SYMBOLS[c.planet] || c.planet[0];
const pdata = (_currentData.planets || {})[c.planet] || {};
const inDeg = pdata.degree !== undefined ? _inSignDeg(pdata.degree).toFixed(1) : '?';
const sicon = _signIconSvg(c.sign) || (SIGNS.find(s => s.name === c.sign) || {}).symbol || '';
bodyHtml += `<div class="tt-asp-row">${psym} @ ${inDeg}° ${sicon} +1</div>`;
bodyHtml += `<div class="tt-asp-row">${_pSym(c.planet)} @ ${inDeg}° ${_signIcon(c.sign)} +1</div>`;
});
bodyHtml += '</div>';
} else {
@@ -482,17 +651,21 @@ const NatusWheel = (() => {
const stellia = elData.stellia || [];
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
if (stellia.length) {
const isTie = stellia.length > 1;
bodyHtml += '<div class="tt-el-contribs">';
stellia.forEach(st => {
const bonus = st.planets.length - 1;
bodyHtml += `<div class="tt-el-formation-header">Stellium +${bonus}</div>`;
st.planets.forEach(p => {
const psym = PLANET_SYMBOLS[p.planet] || p.planet[0];
const pdata = (_currentData.planets || {})[p.planet] || {};
const inDeg = pdata.degree !== undefined ? _inSignDeg(pdata.degree).toFixed(1) : '?';
const sicon = _signIconSvg(p.sign) || (SIGNS.find(s => s.name === p.sign) || {}).symbol || '';
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${psym} @ ${inDeg}° ${sicon}</div>`;
});
bodyHtml += `<div class="tt-el-formation-header"><span class="tt-el-formation-label">Stellium</span> +${bonus}</div>`;
if (isTie) {
st.planets.forEach(p => {
const pdata = (_currentData.planets || {})[p.planet] || {};
const inDeg = pdata.degree !== undefined ? _inSignDeg(pdata.degree).toFixed(1) : '?';
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${_pSym(p.planet)} @ ${inDeg}° ${_signIcon(p.sign)}</div>`;
});
} else {
const psyms = st.planets.map(p => _pSym(p.planet)).join(' ');
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${_signIcon(st.sign)} : ${psyms}</div>`;
}
});
bodyHtml += '</div>';
} else {
@@ -506,8 +679,7 @@ const NatusWheel = (() => {
bodyHtml += '<div class="tt-el-contribs">';
parades.forEach(pd => {
const bonus = pd.signs.length - 1;
bodyHtml += `<div class="tt-el-formation-header">Parade +${bonus}</div>`;
// Group planets by sign, sorted by ecliptic degree (counterclockwise = ascending)
bodyHtml += `<div class="tt-el-formation-header"><span class="tt-el-formation-label">Parade</span> +${bonus}</div>`;
const bySign = {};
pd.planets.forEach(p => {
if (!bySign[p.sign]) bySign[p.sign] = [];
@@ -522,9 +694,8 @@ const NatusWheel = (() => {
});
pd.signs.forEach(sign => {
const planets = bySign[sign] || [];
const sicon = _signIconSvg(sign) || (SIGNS.find(s => s.name === sign) || {}).symbol || sign;
const psyms = planets.map(p => PLANET_SYMBOLS[p.planet] || p.planet[0]).join(' ');
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${sicon} (${psyms})</div>`;
const psyms = planets.map(p => _pSym(p.planet)).join(' ');
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${_signIcon(sign)} : ${psyms}</div>`;
});
});
bodyHtml += '</div>';
@@ -549,9 +720,11 @@ const NatusWheel = (() => {
}
function _activateSign(idx) {
_clearActive();
_activeRing = 'signs';
_activeIdx = idx;
const sign = _signItems[idx];
_svg.select(`[data-sign-name="${sign.name}"]`).classed('nw-sign--active', true);
const elKey = sign.element.toLowerCase();
const modality = SIGN_MODALITIES[idx];
const vecImg = _elementVectorImg(sign.element);
@@ -559,33 +732,51 @@ const NatusWheel = (() => {
`<span class="tt-sign-sym-fallback">${sign.symbol}</span>`;
let planetsHtml = '';
let cuspsHtml = '';
if (_currentData) {
const cusps = (_currentData.houses || {}).cusps || [];
const inSign = Object.entries(_currentData.planets || {})
.filter(([, p]) => p.sign === sign.name)
.sort((a, b) => a[1].degree - b[1].degree);
inSign.forEach(([pname, pdata]) => {
const psym = PLANET_SYMBOLS[pname] || pname[0];
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
const house = cusps.length ? _planetHouse(pdata.degree, cusps) : null;
const domain = house ? HOUSE_LABELS[house] : '';
const houseHtml = house
? ` ${_houseOrdinal(house)} House <span class="tt-dim">(${domain})</span>`
: '';
planetsHtml +=
`<div class="tt-asp-row">${psym} <span class="tt-asp-orb">@${inDeg}°</span>` +
(domain ? `, House of ${domain}` : '') + `</div>`;
`<div class="tt-house-planet-row">${_pSym(pname)} <span class="tt-dim">@${inDeg}°</span>${houseHtml}</div>`;
});
if (cusps.length) {
cusps.forEach((cuspDeg, i) => {
const norm = ((cuspDeg % 360) + 360) % 360;
if (Math.floor(norm / 30) === idx) {
const houseNum = i + 1;
const inDeg = _inSignDeg(cuspDeg).toFixed(1);
cuspsHtml +=
`<div class="tt-house-planet-row">${_houseOrdinal(houseNum)} House <span class="tt-dim">@${inDeg}°</span> ${_signIcon(sign.name)}</div>`;
}
});
}
}
if (_ttBody) {
_ttBody.innerHTML =
`<div class="tt-sign-header">` +
`<span class="tt-title tt-title--el-${elKey}">${sign.name}</span>` +
`<span class="tt-sign-icon-wrap">${iconSvg}</span>` +
`<span class="tt-title tt-title--sign-${elKey}">${sign.name}</span>` +
`<span class="tt-sign-icon-wrap tt-sign-icon-wrap--${elKey}">${iconSvg}</span>` +
`</div>` +
`<div class="tt-sign-meta">` +
`<span>${modality} ${sign.element}</span>` +
vecImg +
`</div>` +
(planetsHtml ? `<div class="tt-sign-planets">${planetsHtml}</div>` : '');
`<div class="tt-sign-section-header">Planets</div>` +
(planetsHtml ? `<div class="tt-sign-planets">${planetsHtml}</div>` : `<div class="tt-el-formation">—</div>`) +
`<br>` +
`<div class="tt-sign-section-header">Cusps</div>` +
(cuspsHtml ? `<div class="tt-sign-cusps">${cuspsHtml}</div>` : `<div class="tt-el-formation">—</div>`);
}
_positionTooltipAtItem('signs', idx);
if (_tooltipEl) {
@@ -595,9 +786,11 @@ const NatusWheel = (() => {
}
function _activateHouse(idx) {
_clearActive();
_activeRing = 'houses';
_activeIdx = idx;
const house = _houseItems[idx];
_svg.select(`[data-house="${house.num}"]`).classed('nw-house--active', true);
const cusps = (_currentData && (_currentData.houses || {}).cusps) || [];
let planetsHtml = '';
@@ -606,20 +799,26 @@ const NatusWheel = (() => {
.filter(([, p]) => _planetHouse(p.degree, cusps) === house.num)
.sort((a, b) => a[1].degree - b[1].degree);
inHouse.forEach(([pname, pdata]) => {
const psym = PLANET_SYMBOLS[pname] || pname[0];
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
const sicon = _signIconSvg(pdata.sign) || (SIGNS.find(s => s.name === pdata.sign) || {}).symbol || '';
planetsHtml += `<div class="tt-asp-row">${psym} <span class="tt-asp-orb">@${inDeg}°</span> ${sicon}</div>`;
planetsHtml += `<div class="tt-house-planet-row">${_pSym(pname)} <span class="tt-dim">@${inDeg}°</span> ${_signIcon(pdata.sign)}</div>`;
});
}
const quadrant = HOUSE_QUADRANT[house.num] || '';
const phase = HOUSE_PHASE[house.num] || '';
if (_ttBody) {
_ttBody.innerHTML =
`<div class="tt-house-header">` +
`<span class="tt-title"><span class="tt-house-of">House of</span><br>${house.label}</span>` +
`<span class="tt-title">` +
`<span class="tt-house-of">House of</span><br>` +
`${house.label}<br>` +
`<span class="tt-house-type">${quadrant} ${phase}</span>` +
`</span>` +
`<span class="tt-house-num">${house.num}</span>` +
`</div>` +
(planetsHtml ? `<div class="tt-house-planets">${planetsHtml}</div>` : '');
`<div class="tt-sign-section-header">Planets</div>` +
(planetsHtml ? `<div class="tt-house-planets">${planetsHtml}</div>` : `<div class="tt-el-formation">—</div>`);
}
_positionTooltipAtItem('houses', idx);
if (_tooltipEl) {
@@ -662,6 +861,8 @@ const NatusWheel = (() => {
if (_aspectsVisible) {
if (_activeRing === 'planets' && _activeIdx !== null) {
_showPlanetAspects(_planetItems[_activeIdx].name);
} else if (_activeRing === 'angles' && _activeIdx !== null) {
_showAngleAspects(_activeIdx);
}
} else {
_clearAspectLines();
@@ -691,9 +892,9 @@ const NatusWheel = (() => {
e.stopPropagation(); _stepCycle(1);
});
_tooltipEl.querySelector('.nw-asp-don')
.addEventListener('click', (e) => { e.stopPropagation(); _toggleAspects(); });
.addEventListener('click', (e) => { e.stopPropagation(); if (!e.currentTarget.classList.contains('btn-disabled')) _toggleAspects(); });
_tooltipEl.querySelector('.nw-asp-doff')
.addEventListener('click', (e) => { e.stopPropagation(); _toggleAspects(); });
.addEventListener('click', (e) => { e.stopPropagation(); if (!e.currentTarget.classList.contains('btn-disabled')) _toggleAspects(); });
// Sync button to current state in case of redraw mid-session.
_updateAspectToggleUI();
}
@@ -705,15 +906,6 @@ const NatusWheel = (() => {
document.addEventListener('click', (e) => {
if (_activeRing === null) return;
if (_tooltipEl && _tooltipEl.contains(e.target)) return;
// DON/DOFF have pointer-events:none when disabled — check bounding rect directly
if (_tooltipEl) {
for (const cls of ['.nw-asp-don', '.nw-asp-doff', '.nw-tt-prv', '.nw-tt-nxt']) {
const btn = _tooltipEl.querySelector(cls);
if (!btn) continue;
const r = btn.getBoundingClientRect();
if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) return;
}
}
_closeTooltip();
}, { signal: _outsideClickController.signal });
}
@@ -750,13 +942,13 @@ const NatusWheel = (() => {
const asc = data.houses.asc;
const mc = data.houses.mc;
const points = [
{ deg: asc, label: 'ASC' },
{ deg: asc + 180, label: 'DSC' },
{ deg: mc, label: 'MC' },
{ deg: mc + 180, label: 'IC' },
{ deg: asc, label: 'ASC', clickable: true },
{ deg: asc + 180, label: 'DSC', clickable: false },
{ deg: mc, label: 'MC', clickable: true },
{ deg: mc + 180, label: 'IC', clickable: false },
];
const axisGroup = g.append('g').attr('class', 'nw-axes');
points.forEach(({ deg, label }) => {
points.forEach(({ deg, label, clickable }) => {
const a = _toAngle(deg, asc);
const x1 = _cx + R.houseInner * Math.cos(a);
const y1 = _cy + R.houseInner * Math.sin(a);
@@ -766,7 +958,8 @@ const NatusWheel = (() => {
.attr('x1', x1).attr('y1', y1)
.attr('x2', x2).attr('y2', y2)
.attr('class', 'nw-axis-line');
axisGroup.append('text')
const textEl = axisGroup.append('text')
.attr('x', _cx + (R.ascMcR + 12) * Math.cos(a))
.attr('y', _cy + (R.ascMcR + 12) * Math.sin(a))
.attr('text-anchor', 'middle')
@@ -774,6 +967,37 @@ const NatusWheel = (() => {
.attr('font-size', `${_r * 0.055}px`)
.attr('class', 'nw-axis-label')
.text(label);
if (clickable) {
const hitR = _r * 0.055 + 6;
const hitGroup = axisGroup.append('g')
.attr('class', 'nw-angle-group')
.attr('data-angle', label)
.style('cursor', 'pointer');
// Invisible hit circle over the label
hitGroup.append('circle')
.attr('cx', _cx + (R.ascMcR + 12) * Math.cos(a))
.attr('cy', _cy + (R.ascMcR + 12) * Math.sin(a))
.attr('r', hitR)
.attr('fill', 'transparent');
// Move the text into the clickable group
textEl.remove();
hitGroup.append('text')
.attr('x', _cx + (R.ascMcR + 12) * Math.cos(a))
.attr('y', _cy + (R.ascMcR + 12) * Math.sin(a))
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', `${_r * 0.055}px`)
.attr('class', 'nw-axis-label')
.text(label);
hitGroup.on('click', function (event) {
event.stopPropagation();
_activateAngle(label);
});
}
});
}
@@ -822,7 +1046,7 @@ const NatusWheel = (() => {
.attr('cx', lx)
.attr('cy', ly)
.attr('r', cr)
.attr('class', `nw-sign-icon-bg--${sign.element.toLowerCase()}`);
.attr('class', `nw-sign-icon-bg nw-sign-icon-bg--${sign.element.toLowerCase()}`);
if (_signPaths[sign.name]) {
signSlice.append('path')
@@ -848,8 +1072,22 @@ const NatusWheel = (() => {
return { i, startA, sa, ea, midA: (sa + ea) / 2 };
});
houses.forEach(({ i, sa, ea }) => {
houseGroup.append('path')
houses.forEach(({ i, startA, sa, ea, midA }) => {
// Per-house group — wraps fill + number so CSS can target both on hover/active
const grp = houseGroup.append('g')
.attr('class', 'nw-house-group')
.attr('data-house', i + 1)
.on('click', function (event) {
event.stopPropagation();
const clickIdx = i;
if (_activeRing === 'houses' && _activeIdx === clickIdx) {
_closeTooltip();
} else {
_activateHouse(clickIdx);
}
});
grp.append('path')
.attr('transform', `translate(${_cx},${_cy})`)
.attr('d', arc({
innerRadius: R.houseInner,
@@ -857,30 +1095,9 @@ const NatusWheel = (() => {
startAngle: sa + Math.PI / 2,
endAngle: ea + Math.PI / 2,
}))
.attr('class', i % 2 === 0 ? 'nw-house-fill--even' : 'nw-house-fill--odd')
.attr('data-house', i + 1)
.on('click', function (event) {
event.stopPropagation();
const clickIdx = i; // _houseItems[i].num === i+1
if (_activeRing === 'houses' && _activeIdx === clickIdx) {
_closeTooltip();
} else {
_activateHouse(clickIdx);
}
});
});
.attr('class', i % 2 === 0 ? 'nw-house-fill--even' : 'nw-house-fill--odd');
houses.forEach(({ i, startA, midA }) => {
if (i % 3 === 0) {
houseGroup.append('line')
.attr('x1', _cx + R.houseInner * Math.cos(startA))
.attr('y1', _cy + R.houseInner * Math.sin(startA))
.attr('x2', _cx + R.signInner * Math.cos(startA))
.attr('y2', _cy + R.signInner * Math.sin(startA))
.attr('class', 'nw-house-cusp');
}
houseGroup.append('text')
grp.append('text')
.attr('x', _cx + R.houseNumR * Math.cos(midA))
.attr('y', _cy + R.houseNumR * Math.sin(midA))
.attr('text-anchor', 'middle')
@@ -889,6 +1106,16 @@ const NatusWheel = (() => {
.attr('class', 'nw-house-num')
.attr('pointer-events', 'none')
.text(i + 1);
// Cusp lines at quadrant boundaries stay in houseGroup (not clickable)
if (i % 3 === 0) {
houseGroup.append('line')
.attr('x1', _cx + R.houseInner * Math.cos(startA))
.attr('y1', _cy + R.houseInner * Math.sin(startA))
.attr('x2', _cx + R.signInner * Math.cos(startA))
.attr('y2', _cy + R.signInner * Math.sin(startA))
.attr('class', 'nw-house-cusp');
}
});
}
@@ -989,6 +1216,17 @@ const NatusWheel = (() => {
* Build the aspect index (planet → aspects list) and create the persistent
* nw-aspects group. Lines are drawn per-planet on click, not here.
*/
/** Compute aspect type + orb between an angle (deg) and a planet (deg), or null. */
function _angleAspectWith(angleDeg, planetDeg) {
let diff = Math.abs(planetDeg - angleDeg) % 360;
if (diff > 180) diff = 360 - diff;
for (const { type, angle } of MAJOR_ASPECTS) {
const orb = Math.abs(diff - angle);
if (orb <= ANGLE_ORB) return { type, orb: +orb.toFixed(1) };
}
return null;
}
function _drawAspects(g, data) {
_aspectIndex = {};
Object.entries(data.planets).forEach(([name]) => { _aspectIndex[name] = []; });
@@ -1000,6 +1238,19 @@ const NatusWheel = (() => {
_aspectIndex[planet2].push({ partner: planet1, ...shared });
});
// Compute planet-angle aspects client-side (planet always applies to angle).
['ASC', 'MC'].forEach(angleName => {
const angleDeg = angleName === 'ASC' ? data.houses.asc : data.houses.mc;
_aspectIndex[angleName] = [];
Object.entries(data.planets).forEach(([pname, pdata]) => {
const asp = _angleAspectWith(angleDeg, pdata.degree);
if (!asp) return;
const shared = { type: asp.type, orb: asp.orb, applying_planet: pname };
_aspectIndex[angleName].push({ partner: pname, ...shared });
_aspectIndex[pname].push({ partner: angleName, ...shared });
});
});
_aspectGroup = g.append('g').attr('class', 'nw-aspects');
}