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:
@@ -300,11 +300,24 @@ def _sky_natus_preview(request):
|
|||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def sky_view(request):
|
def sky_view(request):
|
||||||
|
chart_data = request.user.sky_chart_data
|
||||||
|
birth_dt = request.user.sky_birth_dt
|
||||||
|
saved_birth_date = ''
|
||||||
|
saved_birth_time = ''
|
||||||
|
if birth_dt:
|
||||||
|
if request.user.sky_birth_tz:
|
||||||
|
try:
|
||||||
|
birth_dt = birth_dt.astimezone(zoneinfo.ZoneInfo(request.user.sky_birth_tz))
|
||||||
|
except (zoneinfo.ZoneInfoNotFoundError, KeyError):
|
||||||
|
pass
|
||||||
|
saved_birth_date = birth_dt.strftime('%Y-%m-%d')
|
||||||
|
saved_birth_time = birth_dt.strftime('%H:%M')
|
||||||
return render(request, "apps/dashboard/sky.html", {
|
return render(request, "apps/dashboard/sky.html", {
|
||||||
"preview_url": request.build_absolute_uri("/dashboard/sky/preview"),
|
"preview_url": request.build_absolute_uri("/dashboard/sky/preview"),
|
||||||
"save_url": request.build_absolute_uri("/dashboard/sky/save"),
|
"save_url": request.build_absolute_uri("/dashboard/sky/save"),
|
||||||
"saved_sky": request.user.sky_chart_data,
|
"saved_sky_json": json.dumps(chart_data) if chart_data else 'null',
|
||||||
"saved_birth_dt": request.user.sky_birth_dt,
|
"saved_birth_date": saved_birth_date,
|
||||||
|
"saved_birth_time": saved_birth_time,
|
||||||
"saved_birth_place": request.user.sky_birth_place,
|
"saved_birth_place": request.user.sky_birth_place,
|
||||||
"saved_birth_lat": request.user.sky_birth_lat,
|
"saved_birth_lat": request.user.sky_birth_lat,
|
||||||
"saved_birth_lon": request.user.sky_birth_lon,
|
"saved_birth_lon": request.user.sky_birth_lon,
|
||||||
@@ -329,13 +342,23 @@ def sky_save(request):
|
|||||||
return HttpResponse(status=400)
|
return HttpResponse(status=400)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
|
birth_tz_str = body.get('birth_tz', '').strip()
|
||||||
birth_dt_str = body.get('birth_dt', '')
|
birth_dt_str = body.get('birth_dt', '')
|
||||||
if birth_dt_str:
|
if birth_dt_str:
|
||||||
try:
|
try:
|
||||||
naive = datetime.fromisoformat(birth_dt_str.replace('Z', '+00:00'))
|
naive = datetime.fromisoformat(birth_dt_str.replace('Z', '').replace('+00:00', ''))
|
||||||
user.sky_birth_dt = naive if naive.tzinfo else naive.replace(
|
if naive.tzinfo is None and birth_tz_str:
|
||||||
tzinfo=zoneinfo.ZoneInfo('UTC')
|
try:
|
||||||
)
|
local_tz = zoneinfo.ZoneInfo(birth_tz_str)
|
||||||
|
user.sky_birth_dt = naive.replace(tzinfo=local_tz).astimezone(
|
||||||
|
zoneinfo.ZoneInfo('UTC')
|
||||||
|
)
|
||||||
|
except (zoneinfo.ZoneInfoNotFoundError, KeyError):
|
||||||
|
user.sky_birth_dt = naive.replace(tzinfo=zoneinfo.ZoneInfo('UTC'))
|
||||||
|
elif naive.tzinfo is None:
|
||||||
|
user.sky_birth_dt = naive.replace(tzinfo=zoneinfo.ZoneInfo('UTC'))
|
||||||
|
else:
|
||||||
|
user.sky_birth_dt = naive.astimezone(zoneinfo.ZoneInfo('UTC'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
user.sky_birth_dt = None
|
user.sky_birth_dt = None
|
||||||
user.sky_birth_lat = body.get('birth_lat')
|
user.sky_birth_lat = body.get('birth_lat')
|
||||||
|
|||||||
@@ -103,12 +103,44 @@ const NatusWheel = (() => {
|
|||||||
'Cooperation', 'Regeneration', 'Enterprise', 'Career', 'Reward', 'Reprisal',
|
'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
|
// Cardinal / Fixed / Mutable — parallel index to SIGNS
|
||||||
const SIGN_MODALITIES = [
|
const SIGN_MODALITIES = [
|
||||||
'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
|
'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
|
||||||
'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 1–3: Juvenescence; 4–6: Adolescence; 7–9: Maturescence; 10–12: Senescence
|
||||||
|
const HOUSE_PHASE = [
|
||||||
|
'', 'Juvenescence', 'Juvenescence', 'Juvenescence',
|
||||||
|
'Adolescence', 'Adolescence', 'Adolescence',
|
||||||
|
'Maturescence', 'Maturescence', 'Maturescence',
|
||||||
|
'Senescence', 'Senescence', 'Senescence',
|
||||||
|
];
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let _svg = null;
|
let _svg = null;
|
||||||
@@ -125,12 +157,13 @@ const NatusWheel = (() => {
|
|||||||
|
|
||||||
// ── Cycle state ────────────────────────────────────────────────────────────
|
// ── 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 _activeIdx = null; // index within the active ring's sorted list
|
||||||
let _planetItems = []; // [{name, degree}] sorted by ecliptic degree ascending
|
let _planetItems = []; // [{name, degree}] sorted by ecliptic degree ascending
|
||||||
let _elementItems = []; // [{key}] in ELEMENT_ORDER
|
let _elementItems = []; // [{key}] in ELEMENT_ORDER
|
||||||
let _signItems = []; // [{name, symbol, element}] in SIGNS order
|
let _signItems = []; // [{name, symbol, element}] in SIGNS order
|
||||||
let _houseItems = []; // [{num, label}] houses 1–12
|
let _houseItems = []; // [{num, label}] houses 1–12
|
||||||
|
let _angleItems = []; // [{name, label, house}] — ASC and MC
|
||||||
|
|
||||||
// Tooltip DOM refs — set by _injectTooltipControls() on each draw().
|
// Tooltip DOM refs — set by _injectTooltipControls() on each draw().
|
||||||
let _tooltipEl = null;
|
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>`;
|
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.). */
|
/** <img> for an element-square badge (Ardor.svg etc.). */
|
||||||
function _elementSquareImg(elementKey) {
|
function _elementSquareImg(elementKey) {
|
||||||
const info = ELEMENT_INFO[elementKey];
|
const info = ELEMENT_INFO[elementKey];
|
||||||
@@ -261,6 +317,10 @@ const NatusWheel = (() => {
|
|||||||
num: i + 1,
|
num: i + 1,
|
||||||
label: HOUSE_LABELS[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. */
|
/** Clear all active-lock classes and reset cycle state. */
|
||||||
@@ -268,6 +328,9 @@ const NatusWheel = (() => {
|
|||||||
if (_svg) {
|
if (_svg) {
|
||||||
_svg.selectAll('.nw-planet-group').classed('nw-planet--active', false);
|
_svg.selectAll('.nw-planet-group').classed('nw-planet--active', false);
|
||||||
_svg.selectAll('.nw-element-group').classed('nw-element--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;
|
_activeRing = null;
|
||||||
_activeIdx = 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
|
* Position the tooltip in the vertical half of the wheel opposite to the
|
||||||
* clicked planet/element.
|
* clicked planet/element.
|
||||||
@@ -357,6 +511,9 @@ const NatusWheel = (() => {
|
|||||||
} else if (ring === 'houses') {
|
} else if (ring === 'houses') {
|
||||||
const grp = svgNode.querySelector(`[data-house="${_houseItems[idx].num}"]`);
|
const grp = svgNode.querySelector(`[data-house="${_houseItems[idx].num}"]`);
|
||||||
el = grp && (grp.querySelector('path') || grp);
|
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();
|
if (el) iRect = el.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
@@ -404,16 +561,29 @@ const NatusWheel = (() => {
|
|||||||
if (myAspects.length) {
|
if (myAspects.length) {
|
||||||
aspectHtml = '<small class="tt-aspects">';
|
aspectHtml = '<small class="tt-aspects">';
|
||||||
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
myAspects.forEach(({ partner, type, orb, applying_planet }) => {
|
||||||
const psym = PLANET_SYMBOLS[partner] || partner[0];
|
const isAngle = partner === 'ASC' || partner === 'MC';
|
||||||
const ppdata = _currentData.planets[partner] || {};
|
const ppdata = isAngle ? {} : (_currentData.planets[partner] || {});
|
||||||
const sicon = _signIconSvg(ppdata.sign) || (SIGNS.find(s => s.name === ppdata.sign) || {}).symbol || '';
|
|
||||||
const asym = ASPECT_SYMBOLS[type] || type;
|
const asym = ASPECT_SYMBOLS[type] || type;
|
||||||
const dirsym = applying_planet === item.name ? APPLY_SYM : SEP_SYM;
|
const applying = applying_planet === item.name;
|
||||||
const lineSvg = _aspectLineSvg(type, applying_planet);
|
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 +=
|
aspectHtml +=
|
||||||
`<div class="tt-asp-row">` +
|
`<div class="tt-asp-row">` +
|
||||||
`${lineSvg} ${asym} ${psym} <span class="tt-asp-in">in</span> ${sicon}` +
|
`${lineSvg} ${asym} ${partnerSym}${signPart}` +
|
||||||
` <span class="tt-asp-orb">(${dirsym} ${orb}°)</span>` +
|
` <span class="tt-asp-orb">${orbHtml}</span>` +
|
||||||
`</div>`;
|
`</div>`;
|
||||||
});
|
});
|
||||||
aspectHtml += '</small>';
|
aspectHtml += '</small>';
|
||||||
@@ -427,7 +597,7 @@ const NatusWheel = (() => {
|
|||||||
`</div>` +
|
`</div>` +
|
||||||
`<div class="tt-planet-loc">` +
|
`<div class="tt-planet-loc">` +
|
||||||
`<span>@${inDeg}° ${pdata.sign}${rx}</span>` +
|
`<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>` +
|
`</div>` +
|
||||||
aspectHtml;
|
aspectHtml;
|
||||||
}
|
}
|
||||||
@@ -464,14 +634,13 @@ const NatusWheel = (() => {
|
|||||||
if (CLASSIC_ELEMENTS.has(item.key)) {
|
if (CLASSIC_ELEMENTS.has(item.key)) {
|
||||||
const contribs = elData.contributors || [];
|
const contribs = elData.contributors || [];
|
||||||
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
|
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
|
||||||
|
bodyHtml += `<div class="tt-sign-section-header">Planets</div>`;
|
||||||
if (contribs.length) {
|
if (contribs.length) {
|
||||||
bodyHtml += '<div class="tt-el-contribs">';
|
bodyHtml += '<div class="tt-el-contribs">';
|
||||||
contribs.forEach(c => {
|
contribs.forEach(c => {
|
||||||
const psym = PLANET_SYMBOLS[c.planet] || c.planet[0];
|
|
||||||
const pdata = (_currentData.planets || {})[c.planet] || {};
|
const pdata = (_currentData.planets || {})[c.planet] || {};
|
||||||
const inDeg = pdata.degree !== undefined ? _inSignDeg(pdata.degree).toFixed(1) : '?';
|
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(c.planet)} @ ${inDeg}° ${_signIcon(c.sign)} +1</div>`;
|
||||||
bodyHtml += `<div class="tt-asp-row">${psym} @ ${inDeg}° ${sicon} +1</div>`;
|
|
||||||
});
|
});
|
||||||
bodyHtml += '</div>';
|
bodyHtml += '</div>';
|
||||||
} else {
|
} else {
|
||||||
@@ -482,17 +651,21 @@ const NatusWheel = (() => {
|
|||||||
const stellia = elData.stellia || [];
|
const stellia = elData.stellia || [];
|
||||||
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
|
bodyHtml = `<div class="tt-el-body-line">${vecImg} +${count} (${pct}%)</div>`;
|
||||||
if (stellia.length) {
|
if (stellia.length) {
|
||||||
|
const isTie = stellia.length > 1;
|
||||||
bodyHtml += '<div class="tt-el-contribs">';
|
bodyHtml += '<div class="tt-el-contribs">';
|
||||||
stellia.forEach(st => {
|
stellia.forEach(st => {
|
||||||
const bonus = st.planets.length - 1;
|
const bonus = st.planets.length - 1;
|
||||||
bodyHtml += `<div class="tt-el-formation-header">Stellium +${bonus}</div>`;
|
bodyHtml += `<div class="tt-el-formation-header"><span class="tt-el-formation-label">Stellium</span> +${bonus}</div>`;
|
||||||
st.planets.forEach(p => {
|
if (isTie) {
|
||||||
const psym = PLANET_SYMBOLS[p.planet] || p.planet[0];
|
st.planets.forEach(p => {
|
||||||
const pdata = (_currentData.planets || {})[p.planet] || {};
|
const pdata = (_currentData.planets || {})[p.planet] || {};
|
||||||
const inDeg = pdata.degree !== undefined ? _inSignDeg(pdata.degree).toFixed(1) : '?';
|
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(p.planet)} @ ${inDeg}° ${_signIcon(p.sign)}</div>`;
|
||||||
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${psym} @ ${inDeg}° ${sicon}</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>';
|
bodyHtml += '</div>';
|
||||||
} else {
|
} else {
|
||||||
@@ -506,8 +679,7 @@ const NatusWheel = (() => {
|
|||||||
bodyHtml += '<div class="tt-el-contribs">';
|
bodyHtml += '<div class="tt-el-contribs">';
|
||||||
parades.forEach(pd => {
|
parades.forEach(pd => {
|
||||||
const bonus = pd.signs.length - 1;
|
const bonus = pd.signs.length - 1;
|
||||||
bodyHtml += `<div class="tt-el-formation-header">Parade +${bonus}</div>`;
|
bodyHtml += `<div class="tt-el-formation-header"><span class="tt-el-formation-label">Parade</span> +${bonus}</div>`;
|
||||||
// Group planets by sign, sorted by ecliptic degree (counterclockwise = ascending)
|
|
||||||
const bySign = {};
|
const bySign = {};
|
||||||
pd.planets.forEach(p => {
|
pd.planets.forEach(p => {
|
||||||
if (!bySign[p.sign]) bySign[p.sign] = [];
|
if (!bySign[p.sign]) bySign[p.sign] = [];
|
||||||
@@ -522,9 +694,8 @@ const NatusWheel = (() => {
|
|||||||
});
|
});
|
||||||
pd.signs.forEach(sign => {
|
pd.signs.forEach(sign => {
|
||||||
const planets = bySign[sign] || [];
|
const planets = bySign[sign] || [];
|
||||||
const sicon = _signIconSvg(sign) || (SIGNS.find(s => s.name === sign) || {}).symbol || sign;
|
const psyms = planets.map(p => _pSym(p.planet)).join(' ');
|
||||||
const psyms = planets.map(p => PLANET_SYMBOLS[p.planet] || p.planet[0]).join(' ');
|
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${_signIcon(sign)} : ${psyms}</div>`;
|
||||||
bodyHtml += `<div class="tt-asp-row tt-el-planet-row">${sicon} (${psyms})</div>`;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
bodyHtml += '</div>';
|
bodyHtml += '</div>';
|
||||||
@@ -549,9 +720,11 @@ const NatusWheel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _activateSign(idx) {
|
function _activateSign(idx) {
|
||||||
|
_clearActive();
|
||||||
_activeRing = 'signs';
|
_activeRing = 'signs';
|
||||||
_activeIdx = idx;
|
_activeIdx = idx;
|
||||||
const sign = _signItems[idx];
|
const sign = _signItems[idx];
|
||||||
|
_svg.select(`[data-sign-name="${sign.name}"]`).classed('nw-sign--active', true);
|
||||||
const elKey = sign.element.toLowerCase();
|
const elKey = sign.element.toLowerCase();
|
||||||
const modality = SIGN_MODALITIES[idx];
|
const modality = SIGN_MODALITIES[idx];
|
||||||
const vecImg = _elementVectorImg(sign.element);
|
const vecImg = _elementVectorImg(sign.element);
|
||||||
@@ -559,33 +732,51 @@ const NatusWheel = (() => {
|
|||||||
`<span class="tt-sign-sym-fallback">${sign.symbol}</span>`;
|
`<span class="tt-sign-sym-fallback">${sign.symbol}</span>`;
|
||||||
|
|
||||||
let planetsHtml = '';
|
let planetsHtml = '';
|
||||||
|
let cuspsHtml = '';
|
||||||
if (_currentData) {
|
if (_currentData) {
|
||||||
const cusps = (_currentData.houses || {}).cusps || [];
|
const cusps = (_currentData.houses || {}).cusps || [];
|
||||||
const inSign = Object.entries(_currentData.planets || {})
|
const inSign = Object.entries(_currentData.planets || {})
|
||||||
.filter(([, p]) => p.sign === sign.name)
|
.filter(([, p]) => p.sign === sign.name)
|
||||||
.sort((a, b) => a[1].degree - b[1].degree);
|
.sort((a, b) => a[1].degree - b[1].degree);
|
||||||
inSign.forEach(([pname, pdata]) => {
|
inSign.forEach(([pname, pdata]) => {
|
||||||
const psym = PLANET_SYMBOLS[pname] || pname[0];
|
|
||||||
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
|
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
|
||||||
const house = cusps.length ? _planetHouse(pdata.degree, cusps) : null;
|
const house = cusps.length ? _planetHouse(pdata.degree, cusps) : null;
|
||||||
const domain = house ? HOUSE_LABELS[house] : '';
|
const domain = house ? HOUSE_LABELS[house] : '';
|
||||||
|
const houseHtml = house
|
||||||
|
? ` ${_houseOrdinal(house)} House <span class="tt-dim">(${domain})</span>`
|
||||||
|
: '';
|
||||||
planetsHtml +=
|
planetsHtml +=
|
||||||
`<div class="tt-asp-row">${psym} <span class="tt-asp-orb">@${inDeg}°</span>` +
|
`<div class="tt-house-planet-row">${_pSym(pname)} <span class="tt-dim">@${inDeg}°</span>${houseHtml}</div>`;
|
||||||
(domain ? `, House of ${domain}` : '') + `</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) {
|
if (_ttBody) {
|
||||||
_ttBody.innerHTML =
|
_ttBody.innerHTML =
|
||||||
`<div class="tt-sign-header">` +
|
`<div class="tt-sign-header">` +
|
||||||
`<span class="tt-title tt-title--el-${elKey}">${sign.name}</span>` +
|
`<span class="tt-title tt-title--sign-${elKey}">${sign.name}</span>` +
|
||||||
`<span class="tt-sign-icon-wrap">${iconSvg}</span>` +
|
`<span class="tt-sign-icon-wrap tt-sign-icon-wrap--${elKey}">${iconSvg}</span>` +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
`<div class="tt-sign-meta">` +
|
`<div class="tt-sign-meta">` +
|
||||||
`<span>${modality} ${sign.element}</span>` +
|
`<span>${modality} ${sign.element}</span>` +
|
||||||
vecImg +
|
vecImg +
|
||||||
`</div>` +
|
`</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);
|
_positionTooltipAtItem('signs', idx);
|
||||||
if (_tooltipEl) {
|
if (_tooltipEl) {
|
||||||
@@ -595,9 +786,11 @@ const NatusWheel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _activateHouse(idx) {
|
function _activateHouse(idx) {
|
||||||
|
_clearActive();
|
||||||
_activeRing = 'houses';
|
_activeRing = 'houses';
|
||||||
_activeIdx = idx;
|
_activeIdx = idx;
|
||||||
const house = _houseItems[idx];
|
const house = _houseItems[idx];
|
||||||
|
_svg.select(`[data-house="${house.num}"]`).classed('nw-house--active', true);
|
||||||
const cusps = (_currentData && (_currentData.houses || {}).cusps) || [];
|
const cusps = (_currentData && (_currentData.houses || {}).cusps) || [];
|
||||||
|
|
||||||
let planetsHtml = '';
|
let planetsHtml = '';
|
||||||
@@ -606,20 +799,26 @@ const NatusWheel = (() => {
|
|||||||
.filter(([, p]) => _planetHouse(p.degree, cusps) === house.num)
|
.filter(([, p]) => _planetHouse(p.degree, cusps) === house.num)
|
||||||
.sort((a, b) => a[1].degree - b[1].degree);
|
.sort((a, b) => a[1].degree - b[1].degree);
|
||||||
inHouse.forEach(([pname, pdata]) => {
|
inHouse.forEach(([pname, pdata]) => {
|
||||||
const psym = PLANET_SYMBOLS[pname] || pname[0];
|
|
||||||
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
|
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
|
||||||
const sicon = _signIconSvg(pdata.sign) || (SIGNS.find(s => s.name === pdata.sign) || {}).symbol || '';
|
planetsHtml += `<div class="tt-house-planet-row">${_pSym(pname)} <span class="tt-dim">@${inDeg}°</span> ${_signIcon(pdata.sign)}</div>`;
|
||||||
planetsHtml += `<div class="tt-asp-row">${psym} <span class="tt-asp-orb">@${inDeg}°</span> ${sicon}</div>`;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quadrant = HOUSE_QUADRANT[house.num] || '';
|
||||||
|
const phase = HOUSE_PHASE[house.num] || '';
|
||||||
|
|
||||||
if (_ttBody) {
|
if (_ttBody) {
|
||||||
_ttBody.innerHTML =
|
_ttBody.innerHTML =
|
||||||
`<div class="tt-house-header">` +
|
`<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>` +
|
`<span class="tt-house-num">${house.num}</span>` +
|
||||||
`</div>` +
|
`</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);
|
_positionTooltipAtItem('houses', idx);
|
||||||
if (_tooltipEl) {
|
if (_tooltipEl) {
|
||||||
@@ -662,6 +861,8 @@ const NatusWheel = (() => {
|
|||||||
if (_aspectsVisible) {
|
if (_aspectsVisible) {
|
||||||
if (_activeRing === 'planets' && _activeIdx !== null) {
|
if (_activeRing === 'planets' && _activeIdx !== null) {
|
||||||
_showPlanetAspects(_planetItems[_activeIdx].name);
|
_showPlanetAspects(_planetItems[_activeIdx].name);
|
||||||
|
} else if (_activeRing === 'angles' && _activeIdx !== null) {
|
||||||
|
_showAngleAspects(_activeIdx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_clearAspectLines();
|
_clearAspectLines();
|
||||||
@@ -691,9 +892,9 @@ const NatusWheel = (() => {
|
|||||||
e.stopPropagation(); _stepCycle(1);
|
e.stopPropagation(); _stepCycle(1);
|
||||||
});
|
});
|
||||||
_tooltipEl.querySelector('.nw-asp-don')
|
_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')
|
_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.
|
// Sync button to current state in case of redraw mid-session.
|
||||||
_updateAspectToggleUI();
|
_updateAspectToggleUI();
|
||||||
}
|
}
|
||||||
@@ -705,15 +906,6 @@ const NatusWheel = (() => {
|
|||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (_activeRing === null) return;
|
if (_activeRing === null) return;
|
||||||
if (_tooltipEl && _tooltipEl.contains(e.target)) 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();
|
_closeTooltip();
|
||||||
}, { signal: _outsideClickController.signal });
|
}, { signal: _outsideClickController.signal });
|
||||||
}
|
}
|
||||||
@@ -750,13 +942,13 @@ const NatusWheel = (() => {
|
|||||||
const asc = data.houses.asc;
|
const asc = data.houses.asc;
|
||||||
const mc = data.houses.mc;
|
const mc = data.houses.mc;
|
||||||
const points = [
|
const points = [
|
||||||
{ deg: asc, label: 'ASC' },
|
{ deg: asc, label: 'ASC', clickable: true },
|
||||||
{ deg: asc + 180, label: 'DSC' },
|
{ deg: asc + 180, label: 'DSC', clickable: false },
|
||||||
{ deg: mc, label: 'MC' },
|
{ deg: mc, label: 'MC', clickable: true },
|
||||||
{ deg: mc + 180, label: 'IC' },
|
{ deg: mc + 180, label: 'IC', clickable: false },
|
||||||
];
|
];
|
||||||
const axisGroup = g.append('g').attr('class', 'nw-axes');
|
const axisGroup = g.append('g').attr('class', 'nw-axes');
|
||||||
points.forEach(({ deg, label }) => {
|
points.forEach(({ deg, label, clickable }) => {
|
||||||
const a = _toAngle(deg, asc);
|
const a = _toAngle(deg, asc);
|
||||||
const x1 = _cx + R.houseInner * Math.cos(a);
|
const x1 = _cx + R.houseInner * Math.cos(a);
|
||||||
const y1 = _cy + R.houseInner * Math.sin(a);
|
const y1 = _cy + R.houseInner * Math.sin(a);
|
||||||
@@ -766,7 +958,8 @@ const NatusWheel = (() => {
|
|||||||
.attr('x1', x1).attr('y1', y1)
|
.attr('x1', x1).attr('y1', y1)
|
||||||
.attr('x2', x2).attr('y2', y2)
|
.attr('x2', x2).attr('y2', y2)
|
||||||
.attr('class', 'nw-axis-line');
|
.attr('class', 'nw-axis-line');
|
||||||
axisGroup.append('text')
|
|
||||||
|
const textEl = axisGroup.append('text')
|
||||||
.attr('x', _cx + (R.ascMcR + 12) * Math.cos(a))
|
.attr('x', _cx + (R.ascMcR + 12) * Math.cos(a))
|
||||||
.attr('y', _cy + (R.ascMcR + 12) * Math.sin(a))
|
.attr('y', _cy + (R.ascMcR + 12) * Math.sin(a))
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
@@ -774,6 +967,37 @@ const NatusWheel = (() => {
|
|||||||
.attr('font-size', `${_r * 0.055}px`)
|
.attr('font-size', `${_r * 0.055}px`)
|
||||||
.attr('class', 'nw-axis-label')
|
.attr('class', 'nw-axis-label')
|
||||||
.text(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('cx', lx)
|
||||||
.attr('cy', ly)
|
.attr('cy', ly)
|
||||||
.attr('r', cr)
|
.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]) {
|
if (_signPaths[sign.name]) {
|
||||||
signSlice.append('path')
|
signSlice.append('path')
|
||||||
@@ -848,8 +1072,22 @@ const NatusWheel = (() => {
|
|||||||
return { i, startA, sa, ea, midA: (sa + ea) / 2 };
|
return { i, startA, sa, ea, midA: (sa + ea) / 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
houses.forEach(({ i, sa, ea }) => {
|
houses.forEach(({ i, startA, sa, ea, midA }) => {
|
||||||
houseGroup.append('path')
|
// 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('transform', `translate(${_cx},${_cy})`)
|
||||||
.attr('d', arc({
|
.attr('d', arc({
|
||||||
innerRadius: R.houseInner,
|
innerRadius: R.houseInner,
|
||||||
@@ -857,30 +1095,9 @@ const NatusWheel = (() => {
|
|||||||
startAngle: sa + Math.PI / 2,
|
startAngle: sa + Math.PI / 2,
|
||||||
endAngle: ea + Math.PI / 2,
|
endAngle: ea + Math.PI / 2,
|
||||||
}))
|
}))
|
||||||
.attr('class', i % 2 === 0 ? 'nw-house-fill--even' : 'nw-house-fill--odd')
|
.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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
houses.forEach(({ i, startA, midA }) => {
|
grp.append('text')
|
||||||
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')
|
|
||||||
.attr('x', _cx + R.houseNumR * Math.cos(midA))
|
.attr('x', _cx + R.houseNumR * Math.cos(midA))
|
||||||
.attr('y', _cy + R.houseNumR * Math.sin(midA))
|
.attr('y', _cy + R.houseNumR * Math.sin(midA))
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
@@ -889,6 +1106,16 @@ const NatusWheel = (() => {
|
|||||||
.attr('class', 'nw-house-num')
|
.attr('class', 'nw-house-num')
|
||||||
.attr('pointer-events', 'none')
|
.attr('pointer-events', 'none')
|
||||||
.text(i + 1);
|
.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
|
* Build the aspect index (planet → aspects list) and create the persistent
|
||||||
* nw-aspects group. Lines are drawn per-planet on click, not here.
|
* 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) {
|
function _drawAspects(g, data) {
|
||||||
_aspectIndex = {};
|
_aspectIndex = {};
|
||||||
Object.entries(data.planets).forEach(([name]) => { _aspectIndex[name] = []; });
|
Object.entries(data.planets).forEach(([name]) => { _aspectIndex[name] = []; });
|
||||||
@@ -1000,6 +1238,19 @@ const NatusWheel = (() => {
|
|||||||
_aspectIndex[planet2].push({ partner: planet1, ...shared });
|
_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');
|
_aspectGroup = g.append('g').attr('class', 'nw-aspects');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -819,3 +819,137 @@ describe("NatusWheel — house ring click tooltips", () => {
|
|||||||
expect(tooltipEl.style.display).toBe("none");
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── T15 — ASC / MC angle click tooltips + aspect lines ────────────────────────
|
||||||
|
//
|
||||||
|
// ASC and MC labels are clickable groups ([data-angle='ASC'] / [data-angle='MC']).
|
||||||
|
// Clicking shows a tooltip similar to a planet tooltip:
|
||||||
|
// Title: "Ascendant" (ASC) or "Midheaven" (MC)
|
||||||
|
// Degree in sign + sign name, plus the house number the angle defines.
|
||||||
|
// Aspect list for planets within 10° orb of the angle (client-side computed).
|
||||||
|
// Aspect lines drawn to those planets.
|
||||||
|
// Clicking same angle again closes the tooltip.
|
||||||
|
// Planet tooltips include angle aspects in their own aspect lists.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("NatusWheel — angle (ASC/MC) click tooltips", () => {
|
||||||
|
|
||||||
|
// ASC=0°(Aries): Sun@8° → Conjunction orb 8° ✓; Mars@188° → Opposition orb 8° ✓
|
||||||
|
// MC=90°(Cancer): Moon@97° → Conjunction orb 7° ✓
|
||||||
|
const ANGLE_CHART = {
|
||||||
|
planets: {
|
||||||
|
Sun: { sign: "Aries", degree: 8.0, retrograde: false },
|
||||||
|
Moon: { sign: "Cancer", degree: 97.0, retrograde: false },
|
||||||
|
Mars: { sign: "Libra", degree: 188.0, retrograde: false },
|
||||||
|
},
|
||||||
|
houses: {
|
||||||
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
||||||
|
asc: 0.0,
|
||||||
|
mc: 90.0,
|
||||||
|
},
|
||||||
|
elements: { Fire: 2, Stone: 0, Air: 1, Water: 1, Time: 0, Space: 0 },
|
||||||
|
aspects: [],
|
||||||
|
distinctions: {
|
||||||
|
"1":1,"2":0,"3":0,"4":1,"5":0,"6":0,
|
||||||
|
"7":1,"8":0,"9":0,"10":1,"11":0,"12":0,
|
||||||
|
},
|
||||||
|
house_system: "O",
|
||||||
|
};
|
||||||
|
|
||||||
|
let svgEl, tooltipEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svgEl.setAttribute("id", "id_natus_svg");
|
||||||
|
svgEl.setAttribute("width", "400");
|
||||||
|
svgEl.setAttribute("height", "400");
|
||||||
|
svgEl.style.width = "400px";
|
||||||
|
svgEl.style.height = "400px";
|
||||||
|
document.body.appendChild(svgEl);
|
||||||
|
|
||||||
|
tooltipEl = document.createElement("div");
|
||||||
|
tooltipEl.id = "id_natus_tooltip";
|
||||||
|
tooltipEl.className = "tt";
|
||||||
|
tooltipEl.style.display = "none";
|
||||||
|
document.body.appendChild(tooltipEl);
|
||||||
|
|
||||||
|
NatusWheel.draw(svgEl, ANGLE_CHART);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
NatusWheel.clear();
|
||||||
|
svgEl.remove();
|
||||||
|
tooltipEl.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15a — ASC label is a clickable group with data-angle='ASC'
|
||||||
|
it("T15a: clicking [data-angle='ASC'] shows tooltip with 'Ascendant' title and house 1", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
expect(ascGroup).not.toBeNull("expected [data-angle='ASC'] to exist in the SVG");
|
||||||
|
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Ascendant");
|
||||||
|
// ASC=0° is the cusp of House 1
|
||||||
|
expect(text).toContain("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15b — MC label is a clickable group with data-angle='MC'
|
||||||
|
it("T15b: clicking [data-angle='MC'] shows tooltip with 'Midheaven' title and house 10", () => {
|
||||||
|
const mcGroup = svgEl.querySelector("[data-angle='MC']");
|
||||||
|
expect(mcGroup).not.toBeNull("expected [data-angle='MC'] to exist in the SVG");
|
||||||
|
|
||||||
|
mcGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Midheaven");
|
||||||
|
expect(text).toContain("10");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15c — ASC tooltip shows degree-in-sign and sign name
|
||||||
|
it("T15c: ASC tooltip shows the in-sign degree and sign of the Ascendant", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
// ASC=0° → 0° Aries
|
||||||
|
expect(text).toContain("Aries");
|
||||||
|
expect(text).toContain("0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15d — clicking same angle a second time hides the tooltip
|
||||||
|
it("T15d: clicking the same angle again hides the tooltip", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15e — ASC tooltip lists planets within 10° orb (client-side computed)
|
||||||
|
it("T15e: ASC tooltip includes aspect rows for planets within 10° orb", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
||||||
|
// Sun at 8° → Conjunction (orb 8°) ✓ — _pSym emits data-planet attr
|
||||||
|
expect(bodyHtml).toContain('data-planet="Sun"');
|
||||||
|
// Mars at 188° → Opposition to ASC (0°), angular distance 172° → orb 8° ✓
|
||||||
|
expect(bodyHtml).toContain('data-planet="Mars"');
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15f — planet tooltip includes ASC in its aspect list when within orb
|
||||||
|
it("T15f: planet tooltip for Sun lists ASC as an aspect partner", () => {
|
||||||
|
const sunGroup = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sunGroup).not.toBeNull();
|
||||||
|
|
||||||
|
sunGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
||||||
|
expect(bodyHtml).toContain("ASC");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -373,33 +373,33 @@ html.natus-open .natus-modal-wrap {
|
|||||||
.nw-axis-line { stroke: rgba(var(--secUser), 1); stroke-width: 1.5px; }
|
.nw-axis-line { stroke: rgba(var(--secUser), 1); stroke-width: 1.5px; }
|
||||||
.nw-axis-label { fill: rgba(var(--secUser), 1); }
|
.nw-axis-label { fill: rgba(var(--secUser), 1); }
|
||||||
|
|
||||||
// Sign ring — uniform --priMe bg at half opacity
|
// Sign ring — uniform --priYl bg at half opacity
|
||||||
.nw-sign--fire,
|
.nw-sign--fire,
|
||||||
.nw-sign--stone,
|
.nw-sign--stone,
|
||||||
.nw-sign--air,
|
.nw-sign--air,
|
||||||
.nw-sign--water {
|
.nw-sign--water {
|
||||||
fill: rgba(var(--priMe), 0.25);
|
fill: rgba(var(--priYl), 0.25);
|
||||||
stroke: rgba(var(--terUser), 1);
|
stroke: rgba(var(--terUser), 1);
|
||||||
stroke-width: 0.75px;
|
stroke-width: 0.75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon bg circles — element fill + matching border
|
// Icon bg circles — element fill + matching border
|
||||||
.nw-sign-icon-bg--fire { fill: rgba(var(--terRd), 0.92); stroke: rgba(var(--priOr), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--fire { fill: rgba(var(--quaRd), 0.92); stroke: rgba(var(--priOr), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--stone { fill: rgba(var(--priYl), 0.92); stroke: rgba(var(--priLm), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--stone { fill: rgba(var(--quaFs), 0.92); stroke: rgba(var(--priMe), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--air { fill: rgba(var(--terGn), 0.92); stroke: rgba(var(--priTk), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--air { fill: rgba(var(--quiCy), 0.92); stroke: rgba(var(--priBl), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--water { fill: rgba(var(--priCy), 0.92); stroke: rgba(var(--priBl), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--water { fill: rgba(var(--sixId), 0.92); stroke: rgba(var(--priVt), 1); stroke-width: 1px; }
|
||||||
|
|
||||||
// Inline SVG path icons — per-element colors
|
// Inline SVG path icons — per-element colors
|
||||||
.nw-sign-icon--fire { fill: rgba(var(--priOr), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
.nw-sign-icon--fire { fill: rgba(var(--priOr), 1); }
|
||||||
.nw-sign-icon--stone { fill: rgba(var(--terLm), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
.nw-sign-icon--stone { fill: rgba(var(--priMe), 1); }
|
||||||
.nw-sign-icon--air { fill: rgba(var(--priTk), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
.nw-sign-icon--air { fill: rgba(var(--priBl), 1); }
|
||||||
.nw-sign-icon--water { fill: rgba(var(--terBl), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
.nw-sign-icon--water { fill: rgba(var(--priVt), 1); }
|
||||||
|
|
||||||
// House ring — uniform --priFs bg
|
// House ring — uniform --priGn bg
|
||||||
.nw-house-cusp { stroke: rgba(var(--terUser), 1); stroke-width: 1.2px; }
|
.nw-house-cusp { stroke: rgba(var(--terUser), 1); stroke-width: 1.2px; }
|
||||||
.nw-house-num { fill: rgba(var(--ninUser), 1); }
|
.nw-house-num { fill: rgba(var(--ninUser), 1); }
|
||||||
.nw-house-fill--even { fill: rgba(var(--priFs), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
|
.nw-house-fill--even { fill: rgba(var(--secGn), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
|
||||||
.nw-house-fill--odd { fill: rgba(var(--priFs), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
|
.nw-house-fill--odd { fill: rgba(var(--quiGn), 0.25); stroke: rgba(var(--terUser), 1); stroke-width: 0.75px; }
|
||||||
|
|
||||||
// Planets — base geometry
|
// Planets — base geometry
|
||||||
.nw-planet-circle,
|
.nw-planet-circle,
|
||||||
@@ -431,17 +431,34 @@ html.natus-open .natus-modal-wrap {
|
|||||||
.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
|
.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
|
||||||
.nw-rx { fill: rgba(var(--terUser), 1); }
|
.nw-rx { fill: rgba(var(--terUser), 1); }
|
||||||
|
|
||||||
// Hover and active-lock glow — planet groups and element slice groups
|
// Hover and active-lock glow — planet, element, sign, house groups
|
||||||
.nw-planet-group,
|
.nw-planet-group,
|
||||||
.nw-element-group { cursor: pointer; }
|
.nw-element-group,
|
||||||
|
.nw-sign-group,
|
||||||
|
.nw-house-group { cursor: pointer; }
|
||||||
|
|
||||||
.nw-planet-group:hover,
|
.nw-planet-group:hover,
|
||||||
.nw-planet-group.nw-planet--active,
|
.nw-planet-group.nw-planet--active,
|
||||||
|
.nw-planet-group.nw-planet--asp-active,
|
||||||
.nw-element-group:hover,
|
.nw-element-group:hover,
|
||||||
.nw-element-group.nw-element--active {
|
.nw-element-group.nw-element--active,
|
||||||
|
.nw-sign-group:hover,
|
||||||
|
.nw-sign-group.nw-sign--active,
|
||||||
|
.nw-house-group:hover,
|
||||||
|
.nw-house-group.nw-house--active {
|
||||||
filter: drop-shadow(0 0 5px rgba(var(--ninUser), 0.9));
|
filter: drop-shadow(0 0 5px rgba(var(--ninUser), 0.9));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zodiac icon circles — muted by default, full opacity on hover/active
|
||||||
|
.nw-sign-icon-bg { opacity: 0.5; }
|
||||||
|
.nw-sign-group:hover .nw-sign-icon-bg,
|
||||||
|
.nw-sign-group.nw-sign--active .nw-sign-icon-bg { opacity: 1; }
|
||||||
|
|
||||||
|
// House numbers — muted by default, full opacity on hover/active
|
||||||
|
.nw-house-num { opacity: 0.75; }
|
||||||
|
.nw-house-group:hover .nw-house-num,
|
||||||
|
.nw-house-group.nw-house--active .nw-house-num { opacity: 1; }
|
||||||
|
|
||||||
// ── Planet tick lines — hidden until parent group is active ──────────────────
|
// ── Planet tick lines — hidden until parent group is active ──────────────────
|
||||||
.nw-planet-tick {
|
.nw-planet-tick {
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -521,6 +538,14 @@ body[class*="-light"] {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
.tt-angle-sym {
|
||||||
|
font-variant-caps: all-small-caps;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.tt-angle-house .tt-ord {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
.tt-planet-loc {
|
.tt-planet-loc {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -560,6 +585,19 @@ body[class*="-light"] {
|
|||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
.tt-sign-section-header {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.55;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
.tt-sign-cusps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
// House tooltip — "House of X" | number; planets in house
|
// House tooltip — "House of X" | number; planets in house
|
||||||
.tt-house-header {
|
.tt-house-header {
|
||||||
@@ -581,12 +619,24 @@ body[class*="-light"] {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.tt-house-type {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.6em;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 0.1em;
|
||||||
|
}
|
||||||
.tt-house-planets {
|
.tt-house-planets {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
.tt-house-planet-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
// DON|DOFF aspect line toggle — stacked at top-left outside the tooltip box,
|
// DON|DOFF aspect line toggle — stacked at top-left outside the tooltip box,
|
||||||
// matching the PRV/NXT pattern at the bottom corners.
|
// matching the PRV/NXT pattern at the bottom corners.
|
||||||
@@ -595,6 +645,7 @@ body[class*="-light"] {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: -1rem;
|
left: -1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
pointer-events: auto; // override btn-disabled; click must land here, not pass through to SVG
|
||||||
}
|
}
|
||||||
.nw-asp-don { top: -1rem; }
|
.nw-asp-don { top: -1rem; }
|
||||||
.nw-asp-doff { top: 1.2rem; }
|
.nw-asp-doff { top: 1.2rem; }
|
||||||
@@ -627,12 +678,30 @@ body[class*="-light"] {
|
|||||||
|
|
||||||
.tt-asp-line { flex-shrink: 0; vertical-align: middle; }
|
.tt-asp-line { flex-shrink: 0; vertical-align: middle; }
|
||||||
|
|
||||||
.tt-asp-orb,
|
.tt-asp-orb {
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.tt-asp-in {
|
.tt-asp-in {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.tt-dim {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
.tt-ord {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
vertical-align: 0.25rem;
|
||||||
|
line-height: 0;
|
||||||
|
opacity: 1;
|
||||||
|
margin-left: -0.25rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Element tooltip — title + square badge
|
// Element tooltip — title + square badge
|
||||||
.tt-el-header {
|
.tt-el-header {
|
||||||
@@ -672,9 +741,14 @@ body[class*="-light"] {
|
|||||||
|
|
||||||
.tt-el-formation-header {
|
.tt-el-formation-header {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-decoration: underline;
|
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.35rem;
|
||||||
|
.tt-el-formation-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.55;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-el-formation {
|
.tt-el-formation {
|
||||||
@@ -708,6 +782,24 @@ body[class*="-light"] {
|
|||||||
.tt-title--el-water { color: rgba(var(--priId), 1); }
|
.tt-title--el-water { color: rgba(var(--priId), 1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign tooltip title + sign icon SVG — element border colors (Stone/Air/Fire/Water schema)
|
||||||
|
#id_natus_tooltip,
|
||||||
|
#id_natus_tooltip_2 {
|
||||||
|
.tt-title--sign-fire { color: rgba(var(--priOr), 1); }
|
||||||
|
.tt-title--sign-stone { color: rgba(var(--priMe), 1); }
|
||||||
|
.tt-title--sign-air { color: rgba(var(--priBl), 1); }
|
||||||
|
.tt-title--sign-water { color: rgba(var(--priVt), 1); }
|
||||||
|
|
||||||
|
.tt-sign-icon-wrap--fire .tt-sign-icon,
|
||||||
|
.tt-planet-sign-icon--fire .tt-sign-icon { fill: rgba(var(--priOr), 1); }
|
||||||
|
.tt-sign-icon-wrap--stone .tt-sign-icon,
|
||||||
|
.tt-planet-sign-icon--stone .tt-sign-icon { fill: rgba(var(--priMe), 1); }
|
||||||
|
.tt-sign-icon-wrap--air .tt-sign-icon,
|
||||||
|
.tt-planet-sign-icon--air .tt-sign-icon { fill: rgba(var(--priBl), 1); }
|
||||||
|
.tt-sign-icon-wrap--water .tt-sign-icon,
|
||||||
|
.tt-planet-sign-icon--water .tt-sign-icon { fill: rgba(var(--priVt), 1); }
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
body[class*="-light"] #id_natus_tooltip_2 {
|
||||||
|
|||||||
@@ -819,3 +819,137 @@ describe("NatusWheel — house ring click tooltips", () => {
|
|||||||
expect(tooltipEl.style.display).toBe("none");
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── T15 — ASC / MC angle click tooltips + aspect lines ────────────────────────
|
||||||
|
//
|
||||||
|
// ASC and MC labels are clickable groups ([data-angle='ASC'] / [data-angle='MC']).
|
||||||
|
// Clicking shows a tooltip similar to a planet tooltip:
|
||||||
|
// Title: "Ascendant" (ASC) or "Midheaven" (MC)
|
||||||
|
// Degree in sign + sign name, plus the house number the angle defines.
|
||||||
|
// Aspect list for planets within 10° orb of the angle (client-side computed).
|
||||||
|
// Aspect lines drawn to those planets.
|
||||||
|
// Clicking same angle again closes the tooltip.
|
||||||
|
// Planet tooltips include angle aspects in their own aspect lists.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("NatusWheel — angle (ASC/MC) click tooltips", () => {
|
||||||
|
|
||||||
|
// ASC=0°(Aries): Sun@8° → Conjunction orb 8° ✓; Mars@188° → Opposition orb 8° ✓
|
||||||
|
// MC=90°(Cancer): Moon@97° → Conjunction orb 7° ✓
|
||||||
|
const ANGLE_CHART = {
|
||||||
|
planets: {
|
||||||
|
Sun: { sign: "Aries", degree: 8.0, retrograde: false },
|
||||||
|
Moon: { sign: "Cancer", degree: 97.0, retrograde: false },
|
||||||
|
Mars: { sign: "Libra", degree: 188.0, retrograde: false },
|
||||||
|
},
|
||||||
|
houses: {
|
||||||
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
||||||
|
asc: 0.0,
|
||||||
|
mc: 90.0,
|
||||||
|
},
|
||||||
|
elements: { Fire: 2, Stone: 0, Air: 1, Water: 1, Time: 0, Space: 0 },
|
||||||
|
aspects: [],
|
||||||
|
distinctions: {
|
||||||
|
"1":1,"2":0,"3":0,"4":1,"5":0,"6":0,
|
||||||
|
"7":1,"8":0,"9":0,"10":1,"11":0,"12":0,
|
||||||
|
},
|
||||||
|
house_system: "O",
|
||||||
|
};
|
||||||
|
|
||||||
|
let svgEl, tooltipEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svgEl.setAttribute("id", "id_natus_svg");
|
||||||
|
svgEl.setAttribute("width", "400");
|
||||||
|
svgEl.setAttribute("height", "400");
|
||||||
|
svgEl.style.width = "400px";
|
||||||
|
svgEl.style.height = "400px";
|
||||||
|
document.body.appendChild(svgEl);
|
||||||
|
|
||||||
|
tooltipEl = document.createElement("div");
|
||||||
|
tooltipEl.id = "id_natus_tooltip";
|
||||||
|
tooltipEl.className = "tt";
|
||||||
|
tooltipEl.style.display = "none";
|
||||||
|
document.body.appendChild(tooltipEl);
|
||||||
|
|
||||||
|
NatusWheel.draw(svgEl, ANGLE_CHART);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
NatusWheel.clear();
|
||||||
|
svgEl.remove();
|
||||||
|
tooltipEl.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15a — ASC label is a clickable group with data-angle='ASC'
|
||||||
|
it("T15a: clicking [data-angle='ASC'] shows tooltip with 'Ascendant' title and house 1", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
expect(ascGroup).not.toBeNull("expected [data-angle='ASC'] to exist in the SVG");
|
||||||
|
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Ascendant");
|
||||||
|
// ASC=0° is the cusp of House 1
|
||||||
|
expect(text).toContain("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15b — MC label is a clickable group with data-angle='MC'
|
||||||
|
it("T15b: clicking [data-angle='MC'] shows tooltip with 'Midheaven' title and house 10", () => {
|
||||||
|
const mcGroup = svgEl.querySelector("[data-angle='MC']");
|
||||||
|
expect(mcGroup).not.toBeNull("expected [data-angle='MC'] to exist in the SVG");
|
||||||
|
|
||||||
|
mcGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Midheaven");
|
||||||
|
expect(text).toContain("10");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15c — ASC tooltip shows degree-in-sign and sign name
|
||||||
|
it("T15c: ASC tooltip shows the in-sign degree and sign of the Ascendant", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
// ASC=0° → 0° Aries
|
||||||
|
expect(text).toContain("Aries");
|
||||||
|
expect(text).toContain("0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15d — clicking same angle a second time hides the tooltip
|
||||||
|
it("T15d: clicking the same angle again hides the tooltip", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15e — ASC tooltip lists planets within 10° orb (client-side computed)
|
||||||
|
it("T15e: ASC tooltip includes aspect rows for planets within 10° orb", () => {
|
||||||
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
||||||
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
||||||
|
// Sun at 8° → Conjunction (orb 8°) ✓ — _pSym emits data-planet attr
|
||||||
|
expect(bodyHtml).toContain('data-planet="Sun"');
|
||||||
|
// Mars at 188° → Opposition to ASC (0°), angular distance 172° → orb 8° ✓
|
||||||
|
expect(bodyHtml).toContain('data-planet="Mars"');
|
||||||
|
});
|
||||||
|
|
||||||
|
// T15f — planet tooltip includes ASC in its aspect list when within orb
|
||||||
|
it("T15f: planet tooltip for Sun lists ASC as an aspect partner", () => {
|
||||||
|
const sunGroup = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sunGroup).not.toBeNull();
|
||||||
|
|
||||||
|
sunGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
|
||||||
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
||||||
|
expect(bodyHtml).toContain("ASC");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
<div class="natus-field">
|
<div class="natus-field">
|
||||||
<label for="id_nf_date">Birth date</label>
|
<label for="id_nf_date">Birth date</label>
|
||||||
<input id="id_nf_date" name="date" type="date" required
|
<input id="id_nf_date" name="date" type="date" required
|
||||||
{% if saved_birth_dt %}value="{{ saved_birth_dt|date:'Y-m-d' }}"{% endif %}>
|
{% if saved_birth_date %}value="{{ saved_birth_date }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="natus-field">
|
<div class="natus-field">
|
||||||
<label for="id_nf_time">Birth time</label>
|
<label for="id_nf_time">Birth time</label>
|
||||||
<input id="id_nf_time" name="time" type="time"
|
<input id="id_nf_time" name="time" type="time"
|
||||||
value="{% if saved_birth_dt %}{{ saved_birth_dt|time:'H:i' }}{% else %}12:00{% endif %}">
|
value="{{ saved_birth_time|default:'12:00' }}">
|
||||||
<small>Local time at birth place. Use 12:00 if unknown.</small>
|
<small>Local time at birth place. Use 12:00 if unknown.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
const PLACE_DELAY = 400;
|
const PLACE_DELAY = 400;
|
||||||
const CHART_DELAY = 300;
|
const CHART_DELAY = 300;
|
||||||
|
|
||||||
NatusWheel.preload();
|
const _preloadReady = NatusWheel.preload();
|
||||||
|
|
||||||
// ── Status helper ───────────────────────────────────────────────────────
|
// ── Status helper ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -313,9 +313,18 @@
|
|||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-preview on load if form is pre-filled from saved sky ───────────
|
// ── Draw saved sky on load; only call PySwiss if no saved chart yet ─────
|
||||||
|
|
||||||
if (_formReady()) schedulePreview();
|
const _savedSky = {{ saved_sky_json|safe }};
|
||||||
|
_preloadReady.then(() => {
|
||||||
|
if (_savedSky) {
|
||||||
|
_lastChartData = _savedSky;
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
NatusWheel.draw(svgEl, _savedSky);
|
||||||
|
} else if (_formReady()) {
|
||||||
|
schedulePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user