PICK SKY overlay: D3 natal wheel, Character model, PySwiss aspects+tz
PySwiss: - calculate_aspects() in calc.py (conjunction/sextile/square/trine/opposition with orbs) - /api/tz/ endpoint (timezonefinder lat/lon → IANA timezone) - aspects included in /api/chart/ response - timezonefinder==8.2.2 added to requirements - 14 new unit tests (test_calc.py) + 12 new integration tests (TimezoneApiTest, aspect fields) Main app: - Sign, Planet, AspectType, HouseLabel reference models + seeded migrations (0032–0033) - Character model with birth_dt/lat/lon/place, house_system, chart_data, celtic_cross, confirmed_at/retired_at lifecycle (migration 0034) - natus_preview proxy view: calls PySwiss /api/chart/ + optional /api/tz/ auto-resolution, computes planet-in-house distinctions, returns enriched JSON - natus_save view: find-or-create draft Character, confirmed_at on action='confirm' - natus-wheel.js: D3 v7 SVG natal wheel (elements pie, signs, houses, planets, aspects, ASC/MC axes); NatusWheel.draw() / redraw() / clear() - _natus_overlay.html: Nominatim place autocomplete (debounced 400ms), geolocation button with reverse-geocode city name, live chart preview (debounced 300ms), tz auto-fill, NVM / SAVE SKY footer; html.natus-open class toggle pattern - _natus.scss: Gaussian backdrop+modal, two-column form|wheel layout, suggestion dropdown, portrait collapse at 600px, landscape sidebar z-index sink - room.html: include overlay when table_status == SKY_SELECT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,14 @@ SIGN_ELEMENT = {
|
||||
'Cancer': 'Water', 'Scorpio': 'Water', 'Pisces': 'Water',
|
||||
}
|
||||
|
||||
ASPECTS = [
|
||||
('Conjunction', 0, 8.0),
|
||||
('Sextile', 60, 6.0),
|
||||
('Square', 90, 8.0),
|
||||
('Trine', 120, 8.0),
|
||||
('Opposition', 180, 10.0),
|
||||
]
|
||||
|
||||
PLANET_CODES = {
|
||||
'Sun': swe.SUN,
|
||||
'Moon': swe.MOON,
|
||||
@@ -90,3 +98,33 @@ def get_element_counts(planets):
|
||||
counts['Space'] = max_seq - 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def calculate_aspects(planets):
|
||||
"""Return a list of aspects between all planet pairs.
|
||||
|
||||
Each entry: {planet1, planet2, type, angle (actual, rounded), orb (rounded)}.
|
||||
Only the first matching aspect type is reported per pair (aspects are
|
||||
well-separated enough that at most one can apply with standard orbs).
|
||||
"""
|
||||
names = list(planets.keys())
|
||||
aspects = []
|
||||
for i, name1 in enumerate(names):
|
||||
for name2 in names[i + 1:]:
|
||||
deg1 = planets[name1]['degree']
|
||||
deg2 = planets[name2]['degree']
|
||||
angle = abs(deg1 - deg2)
|
||||
if angle > 180:
|
||||
angle = 360 - angle
|
||||
for aspect_name, target, max_orb in ASPECTS:
|
||||
orb = abs(angle - target)
|
||||
if orb <= max_orb:
|
||||
aspects.append({
|
||||
'planet1': name1,
|
||||
'planet2': name2,
|
||||
'type': aspect_name,
|
||||
'angle': round(angle, 2),
|
||||
'orb': round(orb, 2),
|
||||
})
|
||||
break
|
||||
return aspects
|
||||
|
||||
Reference in New Issue
Block a user