PICK SKY: natal wheel planet tooltips + FT modernisation

- natus-wheel.js: per-planet <g> group with data-planet/sign/degree/retrograde
  attrs; mouseover/mouseout on group (pointer-events:none on child text/℞ so
  the whole apparatus triggers hover); tooltip uses .tt-title/.tt-description;
  in-sign degree via _inSignDeg() (ecliptic % 30); D3 switched from CDN to
  local d3.min.js
- _natus.scss: .nw-planet--hover glow; #id_natus_tooltip position:fixed z-200
- _natus_overlay.html: tooltip div uses .tt; local d3.min.js script tag
- T3/T4/T5 converted from Selenium execute_script to Jasmine unit tests
  (NatusWheelSpec.js) — NatusWheel was never defined in headless GeckoDriver;
  SpecRunner.html updated to load D3 + natus-wheel.js
- test_pick_sky.py: NatusWheelTooltipTest removed (replaced by Jasmine)
- test_component_cards_tarot / test_trinket_carte_blanche: equip assertions
  updated from legacy .equip-deck-btn/.equip-trinket-btn mini-tooltip pattern
  to current DON|DOFF (.btn-equip in main portal); mini-portal text assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-16 01:57:02 -04:00
parent db9ac9cb24
commit 2910012b67
12 changed files with 370 additions and 59 deletions

View File

@@ -1030,6 +1030,9 @@ def natus_preview(request, room_id):
return HttpResponse(status=502)
data = resp.json()
# PySwiss uses "Earth"; the wheel and SCSS use "Stone".
if 'elements' in data and 'Earth' in data['elements']:
data['elements']['Stone'] = data['elements'].pop('Earth')
data['distinctions'] = _compute_distinctions(data['planets'], data['houses'])
data['timezone'] = tz_str
return JsonResponse(data)

File diff suppressed because one or more lines are too long

View File

@@ -98,6 +98,11 @@ const NatusWheel = (() => {
return v || fallback;
}
/** Ecliptic longitude → degrees within the sign (029.999…). */
function _inSignDeg(ecliptic) {
return ((ecliptic % 360) + 360) % 360 % 30;
}
function _layout(svgEl) {
const rect = svgEl.getBoundingClientRect();
const size = Math.min(rect.width || 400, rect.height || 400);
@@ -264,16 +269,48 @@ const NatusWheel = (() => {
const finalA = _toAngle(pdata.degree, asc);
const el = PLANET_ELEMENTS[name] || '';
// Per-planet group — data attrs + hover events live here so the
// symbol text and ℞ indicator don't block mouse events on the circle.
const planetEl = planetGroup.append('g')
.attr('class', 'nw-planet-group')
.attr('data-planet', name)
.attr('data-sign', pdata.sign)
.attr('data-degree', pdata.degree.toFixed(1))
.attr('data-retrograde', pdata.retrograde ? 'true' : 'false')
.on('mouseover', function (event) {
d3.select(this).classed('nw-planet--hover', true);
const tooltip = document.getElementById('id_natus_tooltip');
if (!tooltip) return;
const sym = PLANET_SYMBOLS[name] || name[0];
const signData = SIGNS.find(s => s.name === pdata.sign) || {};
const signSym = signData.symbol || '';
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
const rx = pdata.retrograde ? ' ℞' : '';
tooltip.innerHTML =
`<div class="tt-title">${name} (${sym})</div>` +
`<div class="tt-description">${inDeg}° ${pdata.sign} ${signSym}${rx}</div>`;
tooltip.style.left = (event.clientX + 14) + 'px';
tooltip.style.top = (event.clientY - 10) + 'px';
tooltip.style.display = 'block';
})
.on('mouseout', function (event) {
// Ignore mouseout when moving between children of this group
if (planetEl.node().contains(event.relatedTarget)) return;
d3.select(this).classed('nw-planet--hover', false);
const tooltip = document.getElementById('id_natus_tooltip');
if (tooltip) tooltip.style.display = 'none';
});
// Circle behind symbol
const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle';
const circle = planetGroup.append('circle')
const circle = planetEl.append('circle')
.attr('cx', _cx + R.planetR * Math.cos(ascAngle))
.attr('cy', _cy + R.planetR * Math.sin(ascAngle))
.attr('r', _r * 0.05)
.attr('class', el ? `${circleBase} nw-planet--${el}` : circleBase);
// Symbol
const label = planetGroup.append('text')
// Symbol — pointer-events:none so hover is handled by the group
const label = planetEl.append('text')
.attr('x', _cx + R.planetR * Math.cos(ascAngle))
.attr('y', _cy + R.planetR * Math.sin(ascAngle))
.attr('text-anchor', 'middle')
@@ -281,17 +318,20 @@ const NatusWheel = (() => {
.attr('dy', '0.1em')
.attr('font-size', `${_r * 0.09}px`)
.attr('class', el ? `nw-planet-label nw-planet-label--${el}` : 'nw-planet-label')
.attr('pointer-events', 'none')
.text(PLANET_SYMBOLS[name] || name[0]);
// Retrograde indicator
// Retrograde indicator — also pointer-events:none
let rxLabel = null;
if (pdata.retrograde) {
planetGroup.append('text')
rxLabel = planetEl.append('text')
.attr('x', _cx + (R.planetR + _r * 0.055) * Math.cos(ascAngle))
.attr('y', _cy + (R.planetR + _r * 0.055) * Math.sin(ascAngle))
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', `${_r * 0.040}px`)
.attr('class', 'nw-rx')
.attr('pointer-events', 'none')
.text('℞');
}
@@ -311,10 +351,8 @@ const NatusWheel = (() => {
.attrTween('x', () => t => _cx + R.planetR * Math.cos(interpAngle(t)))
.attrTween('y', () => t => _cy + R.planetR * Math.sin(interpAngle(t)));
// Retrograde ℞ — move together with planet
if (pdata.retrograde) {
planetGroup.select('.nw-rx:last-child')
.transition(transition())
if (rxLabel) {
rxLabel.transition(transition())
.attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t)))
.attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t)));
}