sky wheel: element contributor display; sign + house tooltips — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

sky_save now re-fetches from PySwiss server-side on save so stored
chart_data always carries enriched element format (contributors/stellia/
parades). New sky/data endpoint serves fresh PySwiss data to the My Sky
applet on load, replacing the stale inline json_script approach.

natus-wheel.js: sign ring slices (data-sign-name) and house ring slices
(data-house) now have click handlers with _activateSign/_activateHouse;
em-dash fallback added for classic elements with empty contributor lists.
Action URLs sky/preview, sky/save, sky/data lose trailing slashes.

Jasmine: T12 sign tooltip, T13 house tooltip, T14 enriched element
contributor display (symbols, Stellium/Parade formations, em-dash fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-21 20:07:40 -04:00
parent 02975d79d3
commit b8ac004fb6
10 changed files with 868 additions and 95 deletions

View File

@@ -119,10 +119,12 @@ const NatusWheel = (() => {
// ── Cycle state ────────────────────────────────────────────────────────────
let _activeRing = null; // 'planets' | 'elements' | null
let _activeRing = null; // 'planets' | 'elements' | 'signs' | 'houses' | 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
// Tooltip DOM refs — set by _injectTooltipControls() on each draw().
let _tooltipEl = null;
@@ -233,6 +235,11 @@ const NatusWheel = (() => {
.map(([name, p]) => ({ name, degree: p.degree }))
.sort((a, b) => b.degree - a.degree); // descending = clockwise on wheel
_elementItems = ELEMENT_ORDER.map(key => ({ key }));
_signItems = SIGNS.map(s => ({ name: s.name, symbol: s.symbol, element: s.element }));
_houseItems = Array.from({ length: 12 }, (_, i) => ({
num: i + 1,
label: HOUSE_LABELS[i + 1],
}));
}
/** Clear all active-lock classes and reset cycle state. */
@@ -320,9 +327,15 @@ const NatusWheel = (() => {
if (ring === 'planets') {
const grp = svgNode.querySelector(`[data-planet="${_planetItems[idx].name}"]`);
el = grp && (grp.querySelector('circle') || grp);
} else {
} else if (ring === 'elements') {
const grp = svgNode.querySelector(`[data-element="${_elementItems[idx].key}"]`);
el = grp && (grp.querySelector('path') || grp);
} else if (ring === 'signs') {
const grp = svgNode.querySelector(`[data-sign-name="${_signItems[idx].name}"]`);
el = grp && (grp.querySelector('path') || grp);
} else if (ring === 'houses') {
const grp = svgNode.querySelector(`[data-house="${_houseItems[idx].num}"]`);
el = grp && (grp.querySelector('path') || grp);
}
if (el) iRect = el.getBoundingClientRect();
}
@@ -434,6 +447,8 @@ const NatusWheel = (() => {
bodyHtml += `<div class="tt-asp-row">${psym} @ ${inDeg}° ${sicon} +1</div>`;
});
bodyHtml += '</div>';
} else {
bodyHtml += `<div class="tt-el-formation">—</div>`;
}
} else if (item.key === 'Time') {
@@ -506,6 +521,43 @@ const NatusWheel = (() => {
}
}
function _activateSign(idx) {
_activeRing = 'signs';
_activeIdx = idx;
const sign = _signItems[idx];
if (_ttBody) {
_ttBody.innerHTML =
`<div class="tt-sign-header">` +
`<span class="tt-sign-symbol">${sign.symbol}</span>` +
`<span class="tt-title">${sign.name}</span>` +
`<span class="tt-sign-element"> · ${sign.element}</span>` +
`</div>`;
}
_positionTooltipAtItem('signs', idx);
if (_tooltipEl) {
_tooltipEl.querySelector('.nw-asp-don')?.style.setProperty('display', 'none');
_tooltipEl.querySelector('.nw-asp-doff')?.style.setProperty('display', 'none');
}
}
function _activateHouse(idx) {
_activeRing = 'houses';
_activeIdx = idx;
const house = _houseItems[idx];
if (_ttBody) {
_ttBody.innerHTML =
`<div class="tt-house-header">` +
`<span class="tt-title">${house.num}</span>` +
`<span class="tt-house-label"> · ${house.label}</span>` +
`</div>`;
}
_positionTooltipAtItem('houses', idx);
if (_tooltipEl) {
_tooltipEl.querySelector('.nw-asp-don')?.style.setProperty('display', 'none');
_tooltipEl.querySelector('.nw-asp-doff')?.style.setProperty('display', 'none');
}
}
/** Advance the active ring by +1 (NXT) or -1 (PRV). */
function _stepCycle(dir) {
if (_activeRing === 'planets') {
@@ -514,6 +566,12 @@ const NatusWheel = (() => {
} else if (_activeRing === 'elements') {
_activeIdx = (_activeIdx + dir + _elementItems.length) % _elementItems.length;
_activateElement(_activeIdx);
} else if (_activeRing === 'signs') {
_activeIdx = (_activeIdx + dir + _signItems.length) % _signItems.length;
_activateSign(_activeIdx);
} else if (_activeRing === 'houses') {
_activeIdx = (_activeIdx + dir + _houseItems.length) % _houseItems.length;
_activateHouse(_activeIdx);
}
}
@@ -661,7 +719,20 @@ const NatusWheel = (() => {
const endA = _toAngle(endDeg, asc);
const [sa, ea] = startA > endA ? [endA, startA] : [startA, endA];
sigGroup.append('path')
const signSlice = sigGroup.append('g')
.attr('class', `nw-sign-group`)
.attr('data-sign-name', sign.name)
.on('click', function (event) {
event.stopPropagation();
const clickIdx = _signItems.findIndex(s => s.name === sign.name);
if (_activeRing === 'signs' && _activeIdx === clickIdx) {
_closeTooltip();
} else {
_activateSign(clickIdx);
}
});
signSlice.append('path')
.attr('transform', `translate(${_cx},${_cy})`)
.attr('d', arc({
innerRadius: R.signInner,
@@ -677,14 +748,14 @@ const NatusWheel = (() => {
const cr = _r * 0.065;
const sf = (cr * 2 * 0.85) / 640;
sigGroup.append('circle')
signSlice.append('circle')
.attr('cx', lx)
.attr('cy', ly)
.attr('r', cr)
.attr('class', `nw-sign-icon-bg--${sign.element.toLowerCase()}`);
if (_signPaths[sign.name]) {
sigGroup.append('path')
signSlice.append('path')
.attr('d', _signPaths[sign.name])
.attr('transform',
`translate(${lx},${ly}) scale(${sf}) translate(-320,-320)`)
@@ -716,7 +787,17 @@ 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('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 }) => {