diff --git a/public/css/style.css b/public/css/style.css index 30f8c68..2e142dc 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -897,6 +897,10 @@ footer a:hover { color: var(--gold); } width: 7rem; } +.sun-constellations-form select { + min-width: 12rem; +} + .sun-constellations-table-wrap { overflow-x: auto; } @@ -910,6 +914,89 @@ footer a:hover { color: var(--gold); } white-space: nowrap; } +.sun-constellations-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 0.75rem; + margin: 1.25rem 0; +} + +.sun-constellations-stats-grid div { + background: rgba(79, 195, 216, 0.06); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.9rem 1rem; +} + +.sun-constellations-stats-grid span { + display: block; + color: var(--text-dim); + font-size: 0.75rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.sun-constellations-stats-grid strong { + display: block; + color: var(--gold-pale); + margin-top: 0.3rem; +} + +.sun-constellations-year-details summary { + color: var(--cyan); + cursor: pointer; + margin-bottom: 1rem; +} + +.sun-constellations-chart-wrap { + background: rgba(3, 8, 18, 0.45); + border: 1px solid var(--border); + border-radius: var(--radius); + margin: 1.25rem 0; + padding: 0.8rem; +} + +.sun-constellations-chart { + display: block; + height: auto; + overflow: visible; + width: 100%; +} + +.sun-chart-grid { + stroke: rgba(180, 210, 235, 0.16); + stroke-width: 1; +} + +.sun-chart-line { + fill: none; + stroke: var(--cyan); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.5; +} + +.sun-chart-moving-average { + fill: none; + stroke: var(--gold); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 3; +} + +.sun-chart-marker { + fill: var(--gold-pale); + stroke: #07101d; + stroke-width: 1.5; +} + +.sun-chart-axis-label { + fill: var(--text-dim); + font-family: 'Space Mono', monospace; + font-size: 11px; +} + + @media (max-width: 560px) { .sun-constellations-form { align-items: stretch; diff --git a/public/py/api.py b/public/py/api.py index 36cfa07..7ed9358 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -2306,6 +2306,93 @@ def action_sun_constellation_changes_for_year(args: list[str]) -> dict: } +def action_sun_constellation_offset_statistics(args: list[str]) -> dict: + if len(args) != 3: + fail( + "Aktion sun_constellation_offset_statistics erwartet 3 Argumente: start_year constellation_symbol timezone", + extra={"argv": args}, + ) + + try: + start_year = int(args[0]) + except ValueError as exc: + fail("Startjahr ist ungueltig.", extra={"details": str(exc), "argv": args}) + + end_year = start_year + 99 + if start_year < 1600 or end_year > 2200: + fail("Der Zeitraum von 100 Jahren muss zwischen 1600 und 2200 liegen.", extra={"start_year": start_year}) + + constellation_symbol = args[1] + astrological_starts = { + "Cap": ("Steinbock", -1, 12, 22), + "Aqr": ("Wassermann", 0, 1, 20), + "Psc": ("Fische", 0, 2, 19), + "Ari": ("Widder", 0, 3, 21), + "Tau": ("Stier", 0, 4, 20), + "Gem": ("Zwillinge", 0, 5, 21), + "Cnc": ("Krebs", 0, 6, 21), + "Leo": ("Löwe", 0, 7, 23), + "Vir": ("Jungfrau", 0, 8, 23), + "Lib": ("Waage", 0, 9, 23), + "Sco": ("Skorpion", 0, 10, 23), + "Sgr": ("Schütze", 0, 11, 22), + } + if constellation_symbol not in astrological_starts: + fail("Sternbild ist fuer den astrologischen Vergleich nicht verfuegbar.", extra={"constellation_symbol": constellation_symbol}) + + timezone_name = args[2] + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + constellation_name, year_offset, month, day = astrological_starts[constellation_symbol] + yearly_values: list[dict] = [] + differences: list[float] = [] + + for year in range(start_year, end_year + 1): + yearly_data = action_sun_constellation_changes_for_year([str(year), timezone_name]) + event = next( + (item for item in yearly_data["events"] if item["to_constellation_symbol"] == constellation_symbol), + None, + ) + if event is None: + fail("Kein Sternbildwechsel im Berechnungszeitraum gefunden.", extra={"year": year, "constellation_symbol": constellation_symbol}) + + astronomical_change = datetime.fromisoformat(event["local_iso"]).astimezone(tz) + astrological_start = datetime(year + year_offset, month, day, 0, 0, 0, tzinfo=tz) + difference_days = (astronomical_change - astrological_start).total_seconds() / 86400.0 + differences.append(difference_days) + yearly_values.append({ + "year": year, + "astronomical_date": astronomical_change.strftime("%d.%m.%Y"), + "astrological_date": astrological_start.strftime("%d.%m.%Y"), + "difference_days": round(difference_days, 2), + }) + + mean_days = sum(differences) / len(differences) + variance = sum((value - mean_days) ** 2 for value in differences) / len(differences) + + return { + "ok": True, + "action": "sun_constellation_offset_statistics", + "selected": { + "constellation_symbol": constellation_symbol, + "constellation_name": constellation_name, + "start_year": start_year, + "end_year": end_year, + "timezone": timezone_name, + }, + "statistics": { + "min_days": round(min(differences), 2), + "max_days": round(max(differences), 2), + "standard_deviation_days": round(math.sqrt(variance), 2), + "change_over_period_days": round(differences[-1] - differences[0], 2), + }, + "years": yearly_values, + } + + def action_month_sky_context(args: list[str]) -> dict: if len(args) != 8: fail( @@ -6057,7 +6144,7 @@ def action_barycenter(args: list[str]) -> dict: def main() -> None: if len(sys.argv) < 2: - fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -6237,6 +6324,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "sun_constellation_offset_statistics": + result = action_sun_constellation_offset_statistics(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "moon_star_occultations_for_month": result = action_moon_star_occultations_for_month(args) print(json.dumps(result, ensure_ascii=True)) @@ -6252,7 +6344,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) if __name__ == "__main__": diff --git a/public/sonnensternbilder.php b/public/sonnensternbilder.php index cacaa8e..b290798 100644 --- a/public/sonnensternbilder.php +++ b/public/sonnensternbilder.php @@ -12,6 +12,11 @@ function h(string $value): string return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } +function formatSignedDays(float $value): string +{ + return ($value >= 0 ? '+' : '-') . number_format(abs($value), 2, ',', '.') . ' Tage'; +} + $timezoneName = 'Europe/Berlin'; $currentYear = (int) (new DateTimeImmutable('now', new DateTimeZone($timezoneName)))->format('Y'); $selectedYear = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT); @@ -49,6 +54,44 @@ $astrologicalStarts = [ 'Skorpion' => [$selectedYear, 10, 23], 'Schütze' => [$selectedYear, 11, 22], ]; +$astrologicalStartsBySymbol = [ + 'Cap' => [$selectedYear - 1, 12, 22], + 'Aqr' => [$selectedYear, 1, 20], + 'Psc' => [$selectedYear, 2, 19], + 'Ari' => [$selectedYear, 3, 21], + 'Tau' => [$selectedYear, 4, 20], + 'Gem' => [$selectedYear, 5, 21], + 'Cnc' => [$selectedYear, 6, 21], + 'Leo' => [$selectedYear, 7, 23], + 'Vir' => [$selectedYear, 8, 23], + 'Lib' => [$selectedYear, 9, 23], + 'Sco' => [$selectedYear, 10, 23], + 'Sgr' => [$selectedYear, 11, 22], +]; +$statConstellations = [ + 'Cap' => 'Steinbock', + 'Aqr' => 'Wassermann', + 'Psc' => 'Fische', + 'Ari' => 'Widder', + 'Tau' => 'Stier', + 'Gem' => 'Zwillinge', + 'Cnc' => 'Krebs', + 'Leo' => 'Löwe', + 'Vir' => 'Jungfrau', + 'Lib' => 'Waage', + 'Sco' => 'Skorpion', + 'Sgr' => 'Schütze', +]; +$selectedStatConstellation = trim((string) ($_GET['stat_constellation'] ?? 'Cap')); +if (!isset($statConstellations[$selectedStatConstellation])) { + $selectedStatConstellation = 'Cap'; +} +$statisticsResult = runPythonApi('sun_constellation_offset_statistics', [ + (string) $selectedYear, + $selectedStatConstellation, + $timezoneName, +]); +$statisticsData = $statisticsResult['ok'] ? $statisticsResult['data'] : null; ?>
@@ -92,8 +135,9 @@ $astrologicalStarts = [ $constellationName = (string) ($event['to_constellation_name'] ?? ''); $interval = $intervalsByStart[(string) ($event['local_iso'] ?? '')] ?? []; $astrologicalStart = null; - if (isset($astrologicalStarts[$constellationName])) { - [$astrologicalYear, $astrologicalMonth, $astrologicalDay] = $astrologicalStarts[$constellationName]; + $constellationSymbol = (string) ($event['to_constellation_symbol'] ?? ''); + if (isset($astrologicalStartsBySymbol[$constellationSymbol])) { + [$astrologicalYear, $astrologicalMonth, $astrologicalDay] = $astrologicalStartsBySymbol[$constellationSymbol]; $astrologicalStart = new DateTimeImmutable( sprintf('%04d-%02d-%02d 00:00:00', $astrologicalYear, $astrologicalMonth, $astrologicalDay), $displayTimezone @@ -120,6 +164,133 @@ $astrologicalStarts = [ + +
+

Versatz über 100 Jahre

+
+ + + + +
+

+ Zeitraum: bis . Der Versatz ist der astronomische Wechselzeitpunkt minus dem üblichen astrologischen Beginn. +

+ + +

+ + + + (float) ($yearValue['difference_days'] ?? 0), + $yearValues + ); + $chartMin = min($chartValues); + $chartMax = max($chartValues); + $chartPadding = max(0.25, ($chartMax - $chartMin) * 0.12); + $chartLow = $chartMin - $chartPadding; + $chartHigh = $chartMax + $chartPadding; + $chartRange = $chartHigh - $chartLow; + $chartPlotWidth = $chartWidth - $chartLeft - $chartRight; + $chartPlotHeight = $chartHeight - $chartTop - $chartBottom; + $chartPoints = []; + $chartMarkers = []; + foreach ($yearValues as $index => $yearValue) { + $value = (float) ($yearValue['difference_days'] ?? 0); + $x = $chartLeft + ($index / max(1, count($yearValues) - 1)) * $chartPlotWidth; + $y = $chartTop + (($chartHigh - $value) / $chartRange) * $chartPlotHeight; + $chartPoints[] = number_format($x, 2, '.', '') . ',' . number_format($y, 2, '.', ''); + if ($index % 10 === 0 || $index === count($yearValues) - 1) { + $chartMarkers[] = [ + 'x' => $x, + 'y' => $y, + 'year' => (string) ($yearValue['year'] ?? ''), + 'value' => $value, + ]; + } + } + $movingAverageWindow = 10; + $movingAverageValues = []; + $movingAveragePoints = []; + foreach ($chartValues as $index => $value) { + if ($index < $movingAverageWindow - 1) { + continue; + } + $movingAverage = array_sum(array_slice($chartValues, $index - $movingAverageWindow + 1, $movingAverageWindow)) / $movingAverageWindow; + $x = $chartLeft + ($index / max(1, count($yearValues) - 1)) * $chartPlotWidth; + $y = $chartTop + (($chartHigh - $movingAverage) / $chartRange) * $chartPlotHeight; + $movingAverageValues[] = $movingAverage; + $movingAveragePoints[] = number_format($x, 2, '.', '') . ',' . number_format($y, 2, '.', ''); + } + ?> +
+
10-Jahres-Trend
+
Kleinster Versatz
+
Größter Versatz
+
Streuung
+
Änderung im Zeitraum
+
+ +
+ + + + + + + + + + + + + + + + + +

Cyan: jährlicher Versatz. Gold: gleitender 10-Jahres-Durchschnitt.

+
+ +
+ Jahreswerte anzeigen ( Jahre) +
+ + + + + + + + + + + + + + + + + + + +
JahrAstronomischAstrologischVersatz
+
+
+ +