Skip to main content

🧱 Einbindung in MODX 3

Einbindung in MODX 3 mit Bootstrap 5 – dwdWetter

Snippet: dwdWetterNowModx (Übersicht + Wetter jetzt)

<?php
/**
 * dwdWetterNowModx
 * V 2025-12-01
 *
 * Liest eine dwdWetter-JSON-Datei und gibt:
 *  - Kopfbereich / Übersicht
 *  - aktuellen Eintrag (erste forecast-Zeile) für "Wetter jetzt"
 *
 * Parameter:
 *  - stationID  : DWD-Station (z.B. K428)
 *  - jsonDir    : Dateisystempfad zum wetterdata-Ordner (optional)
 */

$stationID = $modx->getOption('stationID', $scriptProperties, 'K428');
$defaultJsonDir = MODX_BASE_PATH . 'wetterdata/';
$jsonDir  = $modx->getOption('jsonDir', $scriptProperties, $defaultJsonDir);

// Dateipfad zur JSON-Datei
$file = rtrim($jsonDir, '/\\') . DIRECTORY_SEPARATOR . 'wetter_' . $stationID . '.json';

// Optionales Logging (kannst du auch entfernen, wenn es stört)
$modx->log(modX::LOG_LEVEL_INFO, '[dwdWetterNowModx] JSON-Pfad: ' . $file);

if (!file_exists($file)) {
    return '<div class="alert alert-warning">Keine Wetterdaten für Station ' . htmlentities($stationID) . ' gefunden.</div>';
}

$json = file_get_contents($file);
$data = json_decode($json, true);

if (!is_array($data) || empty($data['forecast'])) {
    return '<div class="alert alert-warning">Wetterdaten für Station ' . htmlentities($stationID) . ' sind ungültig.</div>';
}

// Basis-Platzhalter (Station + Sonne)
$base = [
    'stationID'   => $data['stationID']   ?? '',
    'stationName' => $data['stationName'] ?? '',
    'pubDate'     => $data['pubDate']     ?? '',
    'sunrise'     => $data['sunrise']     ?? '',
    'sunset'      => $data['sunset']      ?? '',
];

// Tageslänge aus sunrise/sunset berechnen (nur auf Uhrzeitbasis)
$lenHours = '';
if (!empty($data['sunrise']) && !empty($data['sunset'])) {
    $sr = DateTime::createFromFormat('H:i', $data['sunrise']);
    $ss = DateTime::createFromFormat('H:i', $data['sunset']);
    if ($sr && $ss) {
        $diff = $sr->diff($ss);
        $len  = $diff->h + $diff->i / 60;
        $lenHours = number_format($len, 2, '.', '');
    }
}
$base['dayLength'] = $lenHours;

// Fertiger Text für "Tageslicht ..."
if (!empty($data['sunrise']) && !empty($data['sunset']) && $lenHours !== '') {
    $base['dayInfo'] = sprintf(
        'Tageslicht %s bis %s (Tageslänge %s h)',
        $data['sunrise'],
        $data['sunset'],
        $lenHours
    );
} else {
    $base['dayInfo'] = '';
}

// Luftdrucktendenz aus den ersten beiden Forecast-Einträgen ableiten
$pressureTrend = '';

if (
    isset($data['forecast'][0]['pressure'], $data['forecast'][1]['pressure'])
) {
    // Druckwerte in hPa (dwdWetter.php liefert sie bereits in hPa)
    $p1 = (int)$data['forecast'][0]['pressure'];
    $p2 = (int)$data['forecast'][1]['pressure'];

    $delta    = $p2 - $p1;        // Differenz in hPa
    $deltaAbs = abs($delta);

    // Zeitabstand in Stunden für Anzeige (z.B. +3 hPa/3h)
    $hoursDiff = 1;
    if (
        isset($data['forecast'][0]['date'], $data['forecast'][0]['time'],
              $data['forecast'][1]['date'], $data['forecast'][1]['time'])
    ) {
        $t1 = strtotime($data['forecast'][0]['date'].' '.$data['forecast'][0]['time']);
        $t2 = strtotime($data['forecast'][1]['date'].' '.$data['forecast'][1]['time']);
        if ($t1 && $t2 && $t2 !== $t1) {
            $hoursDiff = max(1, (int)round(($t2 - $t1) / 3600));
        }
    }

    // stabil, wenn |Δp| <= 4 hPa
    $threshold = 4;

    if ($deltaAbs > $threshold) {
        $trendWord   = ($p2 > $p1) ? 'steigend' : 'fallend';
        $deltaSigned = sprintf('%+d', $delta); // Vorzeichen +/-

        $pressureTrend = sprintf(
            'Luftdrucktendenz %s (%s hPa/%dh)',
            $trendWord,
            $deltaSigned,
            $hoursDiff
        );
    } else {
        $pressureTrend = 'Luftdrucktendenz ist stabil';
    }
}

$base['pressureTrend'] = $pressureTrend;

// "Jetzt" = erster forecast-Eintrag
$now = $data['forecast'][0];
if (!is_array($now)) {
    return '<div class="alert alert-warning">Fehler beim Lesen der aktuellen Wetterdaten.</div>';
}

$pl = array_merge($base, $now);

// Chunk fürs Layout
return $modx->getChunk('dwdWetterNowTpl', $pl);

Info
Die Luftdrucktendenz wird anhand der ersten beiden Forecast-Druckwerte berechnet und gilt als stabil, wenn die Änderung höchstens 4 hPa beträgt. Übersteigt die Differenz diesen Wert, wird die Richtung als steigend oder fallend angegeben, inklusive des hPa-Delta pro Stunde.


Snippet: dwdWetterConditionDe (ww-Code - Hashs mit deutschen Konditionen)

<?php
/**
 * dwdWetterConditionDe
 *
 * Gibt für einen DWD-ww-Code (0–100) die deutsche Beschreibung zurück.
 *
 * Aufruf:
 *   [[!dwdWetterConditionDe? &code=`60`]]
 *     oder
 *   [[!dwdWetterConditionDe? &code=`[[+sigWeatherCode]]`]]
 *     oder
 *   [[!dwdWetterConditionDe? &code=`[[+mainSigWeatherCode]]`]]
 *
 * Parameter:
 *   - code     : ww-Code (Zahl oder String)
 *   - default  : (optional) Fallback-Text, falls Code unbekannt ist
 */

$code    = $modx->getOption('code', $scriptProperties, '');
$default = $modx->getOption('default', $scriptProperties, '');

// ww-Code in String-Schlüssel normieren (z.B. 60 → "60")
$code = trim((string)(is_numeric($code) ? (int)$code : $code));

if ($code === '') {
    return $default;
}

# ww-Code - Hashs mit deutschen Konditionen (Code/Description), Quellen:
# https://wetterkanal.kachelmannwetter.com/was-ist-der-ww-code-in-der-meteorologie/
# https://www.dwd.de/DE/leistungen/opendata/help/schluessel_datenformate/kml/mosmix_element_weather_xls.html
# es werden nicht alle benötigt, aber wer weiss... ;-)
$strConditions_de = array(
    // Bewölkung
    '0'  => 'Effektive Wolkendecke weniger als 2/8',
    '1'  => 'Effektive Wolkendecke zwischen 2/8 und 5/8',
    '2'  => 'Effektive Wolkendecke zwischen 5/8 und 6/8',
    '3'  => 'Effektive Wolkendecke mindestens 6/8',
    // Dunst, Rauch, Staub oder Sand
    '4'  => 'Sicht durch Rauch oder Asche vermindert',
    '5'  => 'trockener Dunst (relative Feuchte < 80 %)',
    '6'  => 'verbreiteter Schwebstaub, nicht vom Wind herangeführt',
    '7'  => 'Staub oder Sand bzw. Gischt, vom Wind herangeführt',
    '8'  => 'gut entwickelte Staub- oder Sandwirbel',
    '9'  => 'Staub- oder Sandsturm im Gesichtskreis, aber nicht an der Station',
    // Trockenereignisse
    '10' => 'feuchter Dunst (relative Feuchte > 80 %)',
    '11' => 'Schwaden von Bodennebel',
    '12' => 'durchgehender Bodennebel',
    '13' => 'Wetterleuchten sichtbar, kein Donner gehört',
    '14' => 'Niederschlag im Gesichtskreis, nicht den Boden erreichend',
    '15' => 'Niederschlag in der Ferne (> 5 km), aber nicht an der Station',
    '16' => 'Niederschlag in der Nähe (< 5 km), aber nicht an der Station',
    '17' => 'Gewitter (Donner hörbar), aber kein Niederschlag an der Station',
    '18' => 'Markante Böen im Gesichtskreis, aber kein Niederschlag an der Station',
    '19' => 'Tromben (trichterförmige Wolkenschläuche) im Gesichtskreis',
    // Ereignisse der letzten Stunde, aber nicht zur Beobachtungszeit
    '20' => 'nach Sprühregen oder Schneegriesel',
    '21' => 'nach Regen',
    '22' => 'nach Schneefall',
    '23' => 'nach Schneeregen oder Eiskörnern',
    '24' => 'nach gefrierendem Regen',
    '25' => 'nach Regenschauer',
    '26' => 'nach Schneeschauer',
    '27' => 'nach Graupel- oder Hagelschauer',
    '28' => 'nach Nebel',
    '29' => 'nach Gewitter',
    // Staubsturm, Sandsturm, Schneefegen oder -treiben
    '30' => 'leichter oder mäßiger Sandsturm, an Intensität abnehmend',
    '31' => 'leichter oder mäßiger Sandsturm, unveränderte Intensität',
    '32' => 'leichter oder mäßiger Sandsturm, an Intensität zunehmend',
    '33' => 'schwerer Sandsturm, an Intensität abnehmen',
    '34' => 'schwerer Sandsturm, unveränderte Intensität',
    '35' => 'schwerer Sandsturm, an Intensität zunehmend',
    '36' => 'leichtes oder mäßiges Schneefegen, unter Augenhöhe',
    '37' => 'starkes Schneefegen, unter Augenhöhe',
    '38' => 'leichtes oder mäßiges Schneetreiben, über Augenhöhe',
    '39' => 'starkes Schneetreiben, über Augenhöhe',
    // Nebel oder Eisnebel
    '40' => 'Nebel in einiger Entfernung',
    '41' => 'Nebel in Schwaden oder Bänken',
    '42' => 'Nebel, Himmel erkennbar, dünner werdend',
    '43' => 'Nebel, Himmel nicht erkennbar, dünner werdend',
    '44' => 'Nebel, Himmel erkennbar, unverändert',
    '45' => 'Nebel, Himmel nicht erkennbar, unverändert',
    '46' => 'Nebel, Himmel erkennbar, dichter werdend',
    '47' => 'Nebel, Himmel nicht erkennbar, dichter werdend',
    '48' => 'Nebel mit Reifansatz, Himmel erkennbar',
    '49' => 'Nebel mit Reifansatz, Himmel nicht erkennbar',
    // Sprühregen
    '50' => 'unterbrochener leichter Sprühregen',
    '51' => 'durchgehend leichter Sprühregen',
    '52' => 'unterbrochener mäßiger Sprühregen',
    '53' => 'durchgehend mäßiger Sprühregen',
    '54' => 'unterbrochener starker Sprühregen',
    '55' => 'durchgehend starker Sprühregen',
    '56' => 'leichter gefrierender Sprühregen',
    '57' => 'mäßiger oder starker gefrierender Sprühregen',
    '58' => 'leichter Sprühregen mit Regen',
    '59' => 'mäßiger oder starker Sprühregen mit Regen',
    // Regen
    '60' => 'unterbrochener leichter Regen oder einzelne Regentropfen',
    '61' => 'durchgehend leichter Regen',
    '62' => 'unterbrochener mäßiger Regen',
    '63' => 'durchgehend mäßiger Regen',
    '64' => 'unterbrochener starker Regen',
    '65' => 'durchgehend starker Regen',
    '66' => 'leichter gefrierender Regen',
    '67' => 'mäßiger oder starker gefrierender Regen',
    '68' => 'leichter Schneeregen',
    '69' => 'mäßiger oder starker Schneeregen',
    // Schnee
    '70' => 'unterbrochener leichter Schneefall oder einzelne Schneeflocken',
    '71' => 'durchgehend leichter Schneefall',
    '72' => 'unterbrochener mäßiger Schneefall',
    '73' => 'durchgehend mäßiger Schneefall',
    '74' => 'unterbrochener starker Schneefall',
    '75' => 'durchgehend starker Schneefall',
    '76' => 'Eisnadeln (Polarschnee)',
    '77' => 'Schneegriesel',
    '78' => 'Schneekristalle',
    '79' => 'Eiskörner (gefrorene Regentropfen)',
    // Schauer
    '80' => 'leichter Regenschauer',
    '81' => 'mäßiger oder starker Regenschauer',
    '82' => 'äußerst heftiger Regenschauer',
    '83' => 'leichter Schneeregenschauer',
    '84' => 'mäßiger oder starker Schneeregenschauer',
    '85' => 'leichter Schneeschauer',
    '86' => 'mäßiger oder starker Schneeschauer',
    '87' => 'leichter Graupelschauer',
    '88' => 'mäßiger oder starker Graupelschauer',
    '89' => 'leichter Hagelschauer',
    '90' => 'mäßiger oder starker Hagelschauer',
    // Gewitter
    '91' => 'Gewitter in der letzten Stunde, zurzeit leichter Regen',
    '92' => 'Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Regen',
    '93' => 'Gewitter in der letzten Stunde, zurzeit leichter Schneefall/Schneeregen/Graupel/Hagel',
    '94' => 'Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Schneefall/Schneeregen/Graupel/Hagel',
    '95' => 'leichtes oder mäßiges Gewitter mit Regen oder Schnee',
    '96' => 'leichtes oder mäßiges Gewitter mit Graupel oder Hagel',
    '97' => 'starkes Gewitter mit Regen oder Schnee',
    '98' => 'starkes Gewitter mit Sandsturm',
    '99' => 'starkes Gewitter mit Graupel oder Hagel',
    '100' => 'not available',
);

// Ergebnis zurückgeben
if (array_key_exists($code, $strConditions_de)) {
    return $strConditions_de[$code];
}

return $default;

Dieses Snippet wandelt den DWD-ww-Code (z.B. 61) in einen lesbaren deutschen Text um.

Eingaben:

  • Parameter &code= → numerischer oder stringbasierter ww-Code
  • Parameter &default= → Fallback-Text, falls Code unbekannt

Interne Logik (Beschreibung):

  • code wird als String normalisiert (z.B. 61)

  • eine interne Map ordnet jedem Code eine Beschreibung zu, z.B.:

    • 60 → „unterbrochener leichter Regen …“
    • 61 → „durchgehend leichter Regen“
    • 63 → „durchgehend mäßiger Regen“
    • 80 → „leichter Regenschauer“
    • 95 → „leichtes oder mäßiges Gewitter mit Regen oder Schnee“
    • usw. (0–100)
  • wenn der Code in der Map existiert → Text zurückgeben

  • sonst → default zurückgeben


Chunk: dwdWetterNowTpl (Übersicht + „Wetter jetzt“ Karte, BS5)

<div class="col-12 col-md-6 col-lg-4 mb-4">
  <div class="card h-100 text-center shadow-sm">

    <div class="card-body">

      <!-- Datum / Uhrzeit / Text-->
      <div class="small text-muted mb-2">
        <strong>Stand: [[+weekday]] [[+date]] [[+time]]</strong>
      </div>
      <div class="small text-muted mb-2">
        [[!dwdWetterConditionDe? &code=`[[+sigWeatherCode]]`]]
      </div>

      <!-- Icon -->
      <div class="my-2">
        <img src="/assets/dwdWetter/dwd_img/[[+icon]]"
             alt="Wettericon"
             class="img-fluid"
             style="max-width:80px;">
      </div>

      <!-- Temperatur -->
      <div class="display-6 mb-3">
        [[+temp2m]] °C
      </div>

      <!-- Zusatzinfos -->
      <div class="small text-start mx-auto" style="max-width:220px;">
        <div><strong>Wind:</strong> [[+windSpeedKmh]] km/h ([[+windDirection]])</div>
        <div><strong>Regen (6h):</strong> [[+rain6h]] mm</div>
        <div><strong>Wolken:</strong> [[+cloud]] %</div>
        <div><strong>Sicht:</strong> [[+visibilityKm]] km</div>
      </div>

    </div>

    <div class="card-footer small text-muted">

      <!-- Tageslicht -->
      <div class="mb-1">
        [[+dayInfo]]
      </div>

      <!-- Luftdruck -->
      <div class="mb-1">
        Luftdruck: [[+pressure]] hPa
      </div>

      <!-- Luftdrucktendenz -->
      <div class="mb-2">
        [[+pressureTrend]]
      </div>

      <div>
        <strong>[[+stationName]] ([[+stationID]])</strong><br>
        Stand: [[+pubDate]]
      </div>

    </div>

  </div>
</div>

Snippet:dwdWetterDailyModx (10-Tages-Übersicht aus daily)

<?php
/**
 * dwdWetterDailyModx
 *
 * Gibt Tageskarten aus dem daily-Array der dwdWetter-JSON aus.
 *
 * Parameter:
 *  - stationID  : DWD-Station (z.B. K428)
 *  - jsonDir    : Dateisystempfad zum wetterdata-Ordner (optional)
 *  - days       : Anzahl Tage (Standard 10)
 */

$stationID = $modx->getOption('stationID', $scriptProperties, 'K428');
$days      = (int)$modx->getOption('days', $scriptProperties, 10);

$defaultJsonDir = MODX_BASE_PATH . 'wetterdata/';
$jsonDir  = $modx->getOption('jsonDir', $scriptProperties, $defaultJsonDir);

$file = rtrim($jsonDir, '/\\') . DIRECTORY_SEPARATOR . 'wetter_' . $stationID . '.json';

if (!file_exists($file)) {
    return '<div class="alert alert-warning">Keine Tagesdaten für Station ' . htmlentities($stationID) . ' gefunden.</div>';
}

$json = file_get_contents($file);
$data = json_decode($json, true);

if (!is_array($data) || empty($data['daily'])) {
    return '<div class="alert alert-warning">Tagesdaten für Station ' . htmlentities($stationID) . ' sind ungültig.</div>';
}

$daily = array_slice($data['daily'], 0, $days);

// Basis-Platzhalter
$base = [
    'stationID'   => $data['stationID']   ?? '',
    'stationName' => $data['stationName'] ?? '',
];

$out = '';

foreach ($daily as $row) {
    if (!is_array($row)) {
        continue;
    }

    // mainIcon kommt jetzt direkt aus der JSON (vom PHP-Hauptscript erzeugt).
    // Fallback, falls es aus irgendeinem Grund fehlt oder leer ist.
    if (empty($row['mainIcon'])) {
        $row['mainIcon'] = 'unknown.png';
    }

    $pl = array_merge($base, $row);

    $out .= $modx->getChunk('dwdWetterDailyCardTpl', $pl);
}

return $out;

Chunk: dwdWetterDailyCardTpl (10-Tage-Vorschau, BS5)

<div class="col-6 col-md-3 col-lg-2 mb-3">
  <div class="card h-100 text-center">
    <div class="card-body p-2">

      <div class="small text-muted mb-1">
        [[+weekday]] [[+date]]
      </div>
      <div class="small text-muted mb-1">
        [[!dwdWetterConditionDe? &code=`[[+mainSigWeatherCode]]`]]
      </div>

      <div class="my-1">
        <img src="/assets/dwdWetter/dwd_img/[[+mainIcon]]"
             alt="Tagesicon"
             style="max-width:48px; height:auto;">
      </div>

      <div class="fw-bold mb-1">
        [[+tempMin]] – [[+tempMax]] °C
      </div>

      <div class="small">
        mittlere Temp.: [[+tempMean]] °C<br>
        Niederschlag gesamt: [[+rain6hSum]] mm<br>
        max. Böe: [[+windGustMax]] km/h
      </div>

    </div>
  </div>
</div>

Einbindung in eine MODX-Seite (Beispiel)

[[$fcCalendar]]

<div class="mt-4 ms-3">
  <p class="mb-1"><em>Farblegende:</em></p>

  <p class="small">
    <span class="badge bg-primary me-1">&nbsp;</span>
    Termine aus dem Dorfkalender (TV-Einträge)<br>

    <span class="badge bg-success me-1">&nbsp;</span>
    Abfuhrtermine A.R.T.<br>

    <small class="ms-4 d-block">
      Quelle:
      <a href="https://www.art-trier.de/abfuhrtermin" target="_blank" rel="noopener noreferrer">
        www.art-trier.de/abfuhrtermin
      </a>
    </small>

    <span class="badge me-1" style="background-color: #7e2555;">&nbsp;</span>
    Schulferien Rheinland-Pfalz<br>

    <span class="badge me-1" style="background-color: #7e6025;">&nbsp;</span>
    Feiertage und besondere kirchliche Tage<br>

    <span class="badge me-1" style="background-color: #6b7d3f;">&nbsp;</span>
    Berichte und Ereignisse (teilweise mit Link)<br>

    <span class="badge me-1" style="background-color: #138496;">&nbsp;</span>
    Zeitumstellung Sommer-/Winterzeit
  </p>

  <p class="small text-muted mb-0">
    <em>Alle Angaben ohne Gewähr. Keine Garantie für Korrektheit der Daten.</em>
  </p>
</div>

Einbindung Wetter Chart-Snippet dwdWetterDailyChartModx

Benötigt: chart.js von https://cdn.jsdelivr.net/npm/chart.js und wird nur geladen, wenn das Snippet verwendet wird, und dank Placeholder nur einmal pro Seite.

dwdWetterChart.jpg

<?php
/**
 * dwdWetterDailyChartModx
 *
 * Benötigt: chart.js von https://cdn.jsdelivr.net/npm/chart.js
 *
 * 10-Tage-Chart (Temperatur min/max + Niederschlag) aus dem daily-Array.
 *
 * Parameter:
 *  - stationID : DWD-Station (z.B. K428)
 *  - jsonDir   : Dateisystempfad zum JSON-Ordner (optional)
 *  - days      : Anzahl Tage (Standard 10)
 */

$stationID = $modx->getOption('stationID', $scriptProperties, 'K428');
$days      = (int)$modx->getOption('days', $scriptProperties, 10);

$defaultJsonDir = MODX_BASE_PATH . 'wetterdata/';
$jsonDir        = $modx->getOption('jsonDir', $scriptProperties, $defaultJsonDir);

// Chart.js nur einmal einbinden
if (!$modx->getPlaceholder('dwdwetter_chartjs_loaded')) {

      // assets_url aus MODX-Config holen
      $assetsUrl = $modx->getOption('assets_url', null, '/assets/');

      // Script registrieren (relative URL wird automatisch korrekt ausgegeben)
      // Lokation prüfen und anpassen!
      $modx->regClientScript($assetsUrl . 'bs5/chart.js');

      // Merker setzen, damit Chart.js nur einmal geladen wird
      $modx->setPlaceholder('dwdwetter_chartjs_loaded', 1);
}

$file = rtrim($jsonDir, '/\\') . DIRECTORY_SEPARATOR . 'wetter_' . $stationID . '.json';

if (!file_exists($file)) {
    return '<div class="alert alert-warning">Keine Tagesdaten für Station ' .
        htmlentities($stationID) . ' gefunden.</div>';
}

$json = file_get_contents($file);
$data = json_decode($json, true);

if (!is_array($data) || empty($data['daily'])) {
    return '<div class="alert alert-warning">Tagesdaten für Station ' .
        htmlentities($stationID) . ' sind ungültig.</div>';
}

// Tagesdaten begrenzen
$daily = array_slice($data['daily'], 0, $days);

// Datenarrays für Chart.js
$labels      = [];
$tempMinArr  = [];
$tempMaxArr  = [];
$rainArr     = [];

foreach ($daily as $row) {
    if (!is_array($row)) {
        continue;
    }

    // Label im Format "Mo. 2025-11-24"
    $labels[]     = ($row['weekday'] ?? '') . ' ' . ($row['date'] ?? '');
    $tempMinArr[] = isset($row['tempMin'])   ? (float)$row['tempMin']   : null;
    $tempMaxArr[] = isset($row['tempMax'])   ? (float)$row['tempMax']   : null;
    $rainArr[]    = isset($row['rain6hSum']) ? (float)$row['rain6hSum'] : 0.0;
}

if (empty($labels)) {
    return '<div class="alert alert-warning">Keine anzeigbaren Tagesdaten für Station ' .
        htmlentities($stationID) . ' gefunden.</div>';
}

// Einzigartige Canvas-ID
$canvasId = 'wxDailyChart_' .
    preg_replace('/[^A-Za-z0-9_]/', '', $stationID) . '_' .
    substr(uniqid('', true), -6);

// Platzhalter für den Chunk
$placeholders = [
    'canvasId'    => $canvasId,
    'stationID'   => $data['stationID']   ?? $stationID,
    'stationName' => $data['stationName'] ?? $stationID,
    'pubDate'     => $data['pubDate']     ?? '',
    'labelsJson'  => json_encode($labels,     JSON_UNESCAPED_UNICODE),
    'tempMinJson' => json_encode($tempMinArr),
    'tempMaxJson' => json_encode($tempMaxArr),
    'rainJson'    => json_encode($rainArr),
];

// Design + JS über Chunk
return $modx->getChunk('dwdWetterDailyChartTpl', $placeholders);

Einbindung Chart-Chunk dwdWetterDailyChartTpl

Das JS erkennt jeden Wechsel des data-bs-theme-Attributs (Hell/Dunkel/Auto) und aktualisiert sofort alle Chart.js-Farben wie Achsen, Grid, Legende und Beschriftungen, sodass der Wetter-Chart immer automatisch zum aktuellen Farbmodus passt.
Der gewählte Modus des Benutzers wird zusätzlich dauerhaft in localStorage gespeichert (modx-color-mode) und beim nächsten Besuch automatisch wiederhergestellt.

<div class="row justify-content-center my-4">
  <div class="col-12 col-md-10 col-lg-8">
    <div class="card h-100 shadow-sm">
      <div class="card-body">
        <h5 class="card-title mb-2">
          10-Tage-Trend – [[+stationName]]
        </h5>

        <div class="small text-muted mb-3">
          Datenquelle: Deutscher Wetterdienst (DWD)<br>
          Stand: [[+pubDate]]
        </div>

        <div class="chart-container" style="position:relative; height:180px;">
          <canvas id="[[+canvasId]]"></canvas>
        </div>
      </div>
    </div>
  </div>
</div>


<script>
document.addEventListener('DOMContentLoaded', function () {
      var ctx = document.getElementById('[[+canvasId]]');
      if (!ctx || typeof Chart === 'undefined') {
            console.warn('Chart.js ist nicht geladen oder Canvas fehlt ([[+canvasId]]).');
            return;
      }

      // Farben je nach Bootstrap-Theme (light / dark)
      function getChartColors() {
            var theme = document.documentElement.getAttribute('data-bs-theme') === 'dark'
                  ? 'dark'
                  : 'light';

            if (theme === 'dark') {
                  return {
                        axis: '#f8f9fa',                          // Achsentext, Legende
                        grid: 'rgba(255,255,255,0.15)',           // Grid im Chartbereich
                        gridSecondary: 'rgba(255,255,255,0.10)'   // z.B. rechte Y-Achse
                  };
            } else {
                  return {
                        axis: '#212529',
                        grid: 'rgba(0,0,0,0.08)',
                        gridSecondary: 'rgba(0,0,0,0.04)'
                  };
            }
      }

      var labels      = [[+labelsJson]];
      var tempMinData = [[+tempMinJson]];
      var tempMaxData = [[+tempMaxJson]];
      var rainData    = [[+rainJson]];

      var colors = getChartColors();

      var wxChart = new Chart(ctx, {
            data: {
                  labels: labels,
                  datasets: [
                        {
                              type: 'line',
                              label: 'Temperatur min.',
                              data: tempMinData,
                              yAxisID: 'yTemp',
                              borderWidth: 2,
                              tension: 0.4,
                              pointRadius: 3,
                              borderColor: '#003f8c',        // dunkelblau
                              backgroundColor: '#003f8c'
                        },
                        {
                              type: 'line',
                              label: 'Temperatur max.',
                              data: tempMaxData,
                              yAxisID: 'yTemp',
                              borderWidth: 2,
                              tension: 0.4,
                              pointRadius: 3,
                              borderColor: '#8c0000',        // dunkelrot
                              backgroundColor: '#8c0000'
                        },
                        {
                              type: 'bar',
                              label: 'Niederschlag (Tagessumme)',
                              data: rainData,
                              yAxisID: 'yRain',
                              borderWidth: 1,
                              backgroundColor: 'rgba(120, 160, 200, 0.45)',   // kühles Blau-Grau
                              borderColor: 'rgba(80, 120, 160, 0.9)'          // etwas dunklerer Rand
                        }
                  ]
            },
            options: {
                  responsive: true,
                  maintainAspectRatio: false,
                  interaction: {
                        mode: 'index',
                        intersect: false
                  },
                  scales: {
                        x: {
                              ticks: {
                                    color: colors.axis,
                                    maxRotation: 0,
                                    autoSkip: false,
                                    callback: function(value) {
                                          var label = this.getLabelForValue(value);
                                          if (!label) {
                                                return '';
                                          }
                                          var parts = label.split(' ');
                                          if (parts.length < 2) {
                                                return label;
                                          }
                                          var date = parts[1];
                                          var d    = date.split('-');
                                          if (d.length < 3) {
                                                return label;
                                          }
                                          return d[2] + '.' + d[1];
                                    }
                              },
                              grid: {
                                    color: colors.grid
                              }
                        },
                        yTemp: {
                              position: 'left',
                              title: {
                                    display: true,
                                    text: 'Temperatur (°C)',
                                    color: colors.axis
                              },
                              ticks: {
                                    color: colors.axis
                              },
                              grid: {
                                    color: colors.grid
                              }
                        },
                        yRain: {
                              position: 'right',
                              title: {
                                    display: true,
                                    text: 'Niederschlag (mm)',
                                    color: colors.axis
                              },
                              ticks: {
                                    color: colors.axis
                              },
                              grid: {
                                    drawOnChartArea: false,
                                    color: colors.gridSecondary
                              }
                        }
                  },
                  plugins: {
                        legend: {
                              position: 'top',
                              labels: {
                                    color: colors.axis
                              }
                        },
                        tooltip: {
                              callbacks: {
                                    label: function(context) {
                                          var label = context.dataset.label || '';
                                          if (label) label += ': ';
                                          if (context.parsed.y != null) {
                                                if (context.dataset.yAxisID === 'yTemp') {
                                                      label += context.parsed.y.toFixed(1) + ' °C';
                                                } else {
                                                      label += context.parsed.y.toFixed(1) + ' mm';
                                                }
                                          }
                                          return label;
                                    }
                              }
                        }
                  }
            }
      });

      // Auf Theme-Wechsel reagieren (Hell/Dunkel/Auto)
      var observer = new MutationObserver(function (mutations) {
            mutations.forEach(function (m) {
                  if (m.type === 'attributes' && m.attributeName === 'data-bs-theme') {
                        var c = getChartColors();

                        wxChart.options.scales.x.ticks.color = c.axis;
                        wxChart.options.scales.x.grid.color  = c.grid;

                        wxChart.options.scales.yTemp.title.color = c.axis;
                        wxChart.options.scales.yTemp.ticks.color = c.axis;
                        wxChart.options.scales.yTemp.grid.color  = c.grid;

                        wxChart.options.scales.yRain.title.color = c.axis;
                        wxChart.options.scales.yRain.ticks.color = c.axis;
                        wxChart.options.scales.yRain.grid.color  = c.gridSecondary;

                        wxChart.options.plugins.legend.labels.color = c.axis;

                        wxChart.update();
                  }
            });
      });

      observer.observe(document.documentElement, {
            attributes: true,
            attributeFilter: ['data-bs-theme']
      });
});
</script>

Chart Einbindung in eine MODX-Seite (Beispiel)

 [[!dwdWetterDailyChartModx?
    &stationID=`K428`
    &days=`10`
    &jsonDir=`[[++base_path]]assets/dwdWetter/json/`
 ]]

DWD Wetter Mini Icon für die Menüleiste

dwdWetter_mini.png

1) Snippet dwdWetterNowMini

<?php
$stationID = $modx->getOption('stationID',$scriptProperties,'K428');
$jsonDir   = $modx->getOption('jsonDir',$scriptProperties, MODX_BASE_PATH.'assets/dwdWetter/json/');

$file = rtrim($jsonDir,'/').'/wetter_'.$stationID.'.json';

if (!file_exists($file)) return '';

$data = json_decode(file_get_contents($file), true);

if (!is_array($data) || empty($data['forecast'][0])) return '';

$now = $data['forecast'][0];
$pl  = $now;
$pl['sigWeatherText'] = 'Wettercode '.$now['sigWeatherCode'];

return $modx->getChunk($scriptProperties['tpl'],$pl);

2) Chunk weatherMiniTpl

<div class="d-flex align-items-center ms-3"
     style="cursor:pointer;"
     onclick="location.href='[[~42]]'"
     title="[[+weekday]] [[+time]] • [[+temp2m]] °C • [[!dwdWetterConditionDe? &code=`[[+sigWeatherCode]]`]]">

    <img src="/assets/dwdWetter/dwd_img/[[+icon]]"
         alt="Wetter"
         class="me-2"
         style="width:32px; height:auto;">

    <span class="fw-semibold">[[+temp2m]] °C</span>
</div>

3) Setze dieses Snippet an eine geeignete Stelle in deinem Template

[[!dwdWetterNowMini?
    &stationID=`K428`
    &jsonDir=`[[++base_path]]assets/dwdWetter/json/`
    &tpl=`weatherMiniTpl`
]]