OpenData – Gasspeicher in Deutschland – Teil 2: 2026

2026 – Das Jahr mit wenig Gas. Wahrscheinlich.

Schlagzeilen wie „Gasspeicher so leer wie nie: Wird das Gas im Winter knapp? (Radio FFH)“ oder „Experte schlägt Alarm: Füllstand deutscher Gasspeicher auf „historischem Tiefstand“ (Berliner Morgenpost)“ konnte man im Spätsommer überall lesen. Wir aber brauchen keine Experten, wir haben die Daten selber.

Festzustellen, dass 2026 der niedrigste historische Füllstand ist, ist nicht schwierig. Man muss einfach in den Daten nachsehen, was der zweitniedrigste Wert war und man ist fertig:

import { readFile } from 'node:fs/promises';

async function main() {
const data = JSON.parse(await readFile(new URL('history.json', import.meta.url)));

// Nur Einträge behalten, deren Monat "08" (August) ist.
// gasDayStart hat das Format "YYYY-MM-DD", slice(5, 7) liefert "MM".
const augustEntries = data.filter((entry) => entry.gasDayStart.slice(5, 7) === '08');

// Summe und Anzahl der "full"-Werte pro Jahr sammeln, um später
// den Durchschnitt bilden zu können (Map: Jahr -> { sum, count }).
const sumsByYear = new Map();
for (const entry of augustEntries) {
// Erste 4 Zeichen von gasDayStart ("YYYY-MM-DD") sind das Jahr.
const year = entry.gasDayStart.slice(0, 4);
// Bestehenden Zwischenstand holen oder mit 0/0 initialisieren.
const stats = sumsByYear.get(year) ?? { sum: 0, count: 0 };
stats.sum += entry.full;
stats.count += 1;
sumsByYear.set(year, stats);
}

// Aus den gesammelten Summen den Durchschnitt pro Jahr berechnen
// und aufsteigend nach Durchschnitt sortieren (niedrigster zuerst).
const averagesByYear = [...sumsByYear.entries()]
.map(([year, { sum, count }]) => ({ year, average: sum / count }))
.sort((a, b) => a.average - b.average);

// Übersicht aller Jahre ausgeben, sortiert vom niedrigsten
// zum höchsten August-Durchschnitt.
for (const { year, average } of averagesByYear) {
console.log(`${year}: ${average.toFixed(2)} %`);
}

// Die drei niedrigsten Jahre stehen nach der Sortierung ganz vorne
// im Array, daher genügt Destrukturierung der ersten drei Elemente.
const [lowest, secondLowest, thirdLowest] = averagesByYear;

console.log(`\nNiedrigster:        ${lowest.year} mit ${lowest.average.toFixed(2)} %`);
console.log(`Zweitniedrigster:   ${secondLowest.year} mit ${secondLowest.average.toFixed(2)} %`);
console.log(`Drittniedrigster:   ${thirdLowest.year} mit ${thirdLowest.average.toFixed(2)} %`);

// Differenzen zwischen den drei niedrigsten Jahren berechnen,
// um zu zeigen, wie weit die Werte auseinanderliegen.
console.log(`Unterschied 1-2:    ${(secondLowest.average - lowest.average).toFixed(2)} %`);
console.log(`Unterschied 2-3:    ${(thirdLowest.average - secondLowest.average).toFixed(2)} %`);
console.log(`Unterschied 1-3:    ${(thirdLowest.average - lowest.average).toFixed(2)} %`);
}

main();

import { readFile } from 'node:fs/promises';

async function main() {
const data = JSON.parse(await readFile(new URL('history.json', import.meta.url)));

// Nur Einträge behalten, deren Monat "08" (August) ist.
// gasDayStart hat das Format "YYYY-MM-DD", slice(5, 7) liefert "MM".
const augustEntries = data.filter((entry) => entry.gasDayStart.slice(5, 7) === '08');

// Summe und Anzahl der "full"-Werte pro Jahr sammeln, um später
// den Durchschnitt bilden zu können (Map: Jahr -> { sum, count }).
const sumsByYear = new Map();
for (const entry of augustEntries) {
// Erste 4 Zeichen von gasDayStart ("YYYY-MM-DD") sind das Jahr.
const year = entry.gasDayStart.slice(0, 4);
// Bestehenden Zwischenstand holen oder mit 0/0 initialisieren.
const stats = sumsByYear.get(year) ?? { sum: 0, count: 0 };
stats.sum += entry.full;
stats.count += 1;
sumsByYear.set(year, stats);
}

// Aus den gesammelten Summen den Durchschnitt pro Jahr berechnen
// und aufsteigend nach Durchschnitt sortieren (niedrigster zuerst).
const averagesByYear = [...sumsByYear.entries()]
.map(([year, { sum, count }]) => ({ year, average: sum / count }))
.sort((a, b) => a.average - b.average);

// Übersicht aller Jahre ausgeben, sortiert vom niedrigsten
// zum höchsten August-Durchschnitt.
for (const { year, average } of averagesByYear) {
console.log(`${year}: ${average.toFixed(2)} %`);
}

// Die drei niedrigsten Jahre stehen nach der Sortierung ganz vorne
// im Array, daher genügt Destrukturierung der ersten drei Elemente.
const [lowest, secondLowest, thirdLowest] = averagesByYear;

console.log(`\nNiedrigster:        ${lowest.year} mit ${lowest.average.toFixed(2)} %`);
console.log(`Zweitniedrigster:   ${secondLowest.year} mit ${secondLowest.average.toFixed(2)} %`);
console.log(`Drittniedrigster:   ${thirdLowest.year} mit ${thirdLowest.average.toFixed(2)} %`);

// Differenzen zwischen den drei niedrigsten Jahren berechnen,
// um zu zeigen, wie weit die Werte auseinanderliegen.
console.log(`Unterschied 1-2:    ${(secondLowest.average - lowest.average).toFixed(2)} %`);
console.log(`Unterschied 2-3:    ${(thirdLowest.average - secondLowest.average).toFixed(2)} %`);
console.log(`Unterschied 1-3:    ${(thirdLowest.average - lowest.average).toFixed(2)} %`);
}

main();

Ausgabe:

Niedrigster: 2026 mit 48.55 %
Zweitniedrigster: 2021 mit 54.79 %
Drittniedrigster: 2015 mit 65.67 %
Unterschied 1-2: 6.24 %
Unterschied 2-3: 10.88 %
Unterschied 1-3: 17.12 %

Niedrigster: 2026 mit 48.55 %
Zweitniedrigster: 2021 mit 54.79 %
Drittniedrigster: 2015 mit 65.67 %
Unterschied 1-2: 6.24 %
Unterschied 2-3: 10.88 %
Unterschied 1-3: 17.12 %

Das war einfach. Weitergeben an die Redaktion und Feierabend… oder doch nicht.

2026 im historischen Kontext

Wie in Teil 1 beschrieben, ist dies aber nur ein Datenpunkt aus vielen Jahren. Wie war das in den vorherigen Jahren. Kann man aus den Graphen etwas ableiten?

Hinweis: Für die folgenden Diagramme benutzen wir nur die vorher geladenen historischen Daten. Die kontinuierliche Aktualisierung wird in Teil 4 beschrieben.

1. Der Tech-Stack

Wir nutzen ein klassisches Backend-/Frontend-System, bestehend aus PHP und JavaScript, das auch auf günstigen Managed Servern funktioniert.
PHP ist dabei für die Auswertung der Daten zuständig, JavaScript für die Darstellung der Diagramme.
Die Diagramme werden mit dem Apache-Projekt ECharts dargestellt. ECharts mag zunächst als Overkill erscheinen, immerhin 1MB Programmcode, aber wenn man später komplexere oder interaktive Diagramme erstellen möchte, muss man nicht die Bibliothek wechseln.

2. Eine einfache Darstellung

Die einfachste Gegenüberstellung der historischen Daten ist die Mittelwertbildung je Monat aller Jahre vor 2026 und die Mittelwerte der Monate im Jahr 2026 und die Darstellung als Liniendiagramm mit zwei Graphen.

Zusätzlich gibt eine Diagramm Auskunft über den Unterschied der Werte der beiden Graphen:

Oiginal-Diagramm – Code bei GitHub

Hinweis: Hier werden nur die wichtigsten Programmteile beschrieben. Daher kann es sein, das Variablen und Funktionen „magisch“ auftauchen, die vorher nicht beschrieben wurden.

2.1 Einlesen der Daten

$raw = file_get_contents($dataFile); // Rohinhalt der JSON-Datei einlesen
$entries = json_decode($raw, true); // JSON in assoziatives Array dekodieren

$raw = file_get_contents($dataFile); // Rohinhalt der JSON-Datei einlesen
$entries = json_decode($raw, true); // JSON in assoziatives Array dekodieren

Mit file_get_contents($dataFile) wird der komplette Inhalt der JSON-Datei als String eingelesen, anschließend dekodiert json_decode($raw, true) diesen String in ein PHP-Array. Der zweite Parameter true sorgt dafür, dass JSON-Objekte als assoziative Arrays statt als stdClass-Objekte dekodiert werden – das erlaubt den direkten Zugriff über eckige Klammern wie $entry['full'], ohne Objekt-Syntax ($entry->full) zu benötigen.

2.2 Aggregation je Monat

$sums = array_fill(1, 12, 0.0);
$counts = array_fill(1, 12, 0);

foreach ($entries as $entry) {
$month = (int) substr($entry['gasDayStart'], 5, 2);
$sums[$month] += $entry['full'];
$counts[$month]++;
} // Ende foreach

$sums = array_fill(1, 12, 0.0);
$counts = array_fill(1, 12, 0);

foreach ($entries as $entry) {
$month = (int) substr($entry['gasDayStart'], 5, 2);
$sums[$month] += $entry['full'];
$counts[$month]++;
} // Ende foreach

Initialisierung: Zwei Arrays ($sums für Füllstände mit 0.0, $counts für Anzahl mit 0) werden via array_fill mit den Schlüsseln 1 bis 12 vorbelegt, damit jeder Monat garantiert existiert.

Aggregation: Für gültige Datensätze wird der Monat extrahiert, der Füllstand (full) auf $sums aufaddiert und der Monatscounter in $counts um 1 erhöht.

2.3 Mittelwertbildung

$averages = []; // Ergebnis-Array initialisieren
for ($month = 1; $month <= 12; $month++) {
$averages[$month] = $counts[$month] > 0
? round($sums[$month] / $counts[$month], 4) : null;
}

$averages = []; // Ergebnis-Array initialisieren
for ($month = 1; $month <= 12; $month++) {
$averages[$month] = $counts[$month] > 0
? round($sums[$month] / $counts[$month], 4) : null;
}

Mittelwertberechnung: Eine for-Schleife dividiert für jeden Monat die Summe durch die Anzahl ($sums / $counts) und rundet das Ergebnis auf 4 Nachkommastellen.

Umgang mit fehlenden Daten: Monate ohne Datensätze erhalten den Wert null statt 0.

Visualisierung: Der Wert null sorgt in ECharts für eine saubere Datenlücke im Liniendiagramm, während eine 0 fälschlicherweise als leerer Speicherstand dargestellt würde.

2.4 Berechnung der für das Diagramm notwenigen Felder

$dataFile = __DIR__ . 'history.json';
$monthlyAverages = getCalendarMonthFullAverages($dataFile);
$monthlyAverages2026 = getCalendarMonthFullAverages($dataFile, 2026);

$dataFile = __DIR__ . 'history.json';
$monthlyAverages = getCalendarMonthFullAverages($dataFile);
$monthlyAverages2026 = getCalendarMonthFullAverages($dataFile, 2026);

Die zuvor beschriebenen Funktionen werden in einer Funktion getCalendarMonthFullAverages zusammengefasst, denn wir müssen sie einmal für die historischen Daten und einmal für das Jahr 2026 aufrufen.

$months = array();
$values = array();
$values2026 = array();
$diffValues = array();

foreach ($monthlyAverages as $monthNumber => $averageAllYears) {
array_push($months, $monthNames[$monthNumber]);
array_push($values, $averageAllYears);
$val2026 = isset($monthlyAverages2026[$monthNumber]) ?
monthlyAverages2026[$monthNumber] : null;
array_push($values2026, $val2026);
if ($val2026 !== null && $averageAllYears !== null) {
array_push($diffValues, round($val2026 - $averageAllYears, 2)); }
else {
array_push($diffValues, null);
}
}

$months = array();
$values = array();
$values2026 = array();
$diffValues = array();

foreach ($monthlyAverages as $monthNumber => $averageAllYears) {
array_push($months, $monthNames[$monthNumber]);
array_push($values, $averageAllYears);
$val2026 = isset($monthlyAverages2026[$monthNumber]) ?
monthlyAverages2026[$monthNumber] : null;
array_push($values2026, $val2026);
if ($val2026 !== null && $averageAllYears !== null) {
array_push($diffValues, round($val2026 - $averageAllYears, 2)); }
else {
array_push($diffValues, null);
}
}

Schleifendurchlauf: Eine foreach-Schleife iteriert über alle 12 Monate der Eingabedaten.

Befüllung der Reihen: Per array_push werden der Monatsname, der Gesamtdurchschnitt ($values) sowie der 2026er-Mittelwert ($values2026) in die jeweiligen Arrays übertragen.

Differenzberechnung: Die Differenz zwischen 2026 und dem Gesamtdurchschnitt wird nur berechnet und auf zwei Nachkommastellen gerundet, wenn für beide Werte Daten vorliegen – andernfalls wird null als Platzhalter für Datenlücken eingesetzt.

2.5 Übergabe der Daten an JavaScript

Dieser Teil ist sehr PHP-spezifisch, sollte aber trotzdem an dieser Stelle beschrieben werden, um das ganze Beispiel zu verstehen.



window.gasData = <?php echo json_encode([ 'months' => $months, 'values' => $values, 'values2026' => $values2026, 'diffValues' => $diffValues, ]); ?>;




window.gasData = <?php echo json_encode([ 'months' => $months, 'values' => $values, 'values2026' => $values2026, 'diffValues' => $diffValues, ]); ?>;


2.5.1 Auf dem Server
PHP verarbeitet die JSON-Datei, berechnet die Arrays und stößt auf die Zeile echo json_encode(...). json_encode wandelt das PHP-Array in ein valides JavaScript-Objekt um.

PHP wird auf dem Server ausgeführt und wenn das Skript durchgelaufen ist, wird fertiger HTML-Code an den Browser geliefert. Aus dem PHP-Code

window.gasData = <?php echo json_encode([ 'months' => ['Januar', 'Februar'], 'values' => [45.2, 50.1] ]);

window.gasData = <?php echo json_encode([ 'months' => ['Januar', 'Februar'], 'values' => [45.2, 50.1] ]);

generiert der PHP-Interpreter den folgenden Text für die HTML-Ausgabe

window.gasData = {"months":["Januar","Februar"],"values":[45.2,50.1]};

window.gasData = {"months":["Januar","Februar"],"values":[45.2,50.1]};

2.5.2 Die Übertragung
Der Server schickt das fertige HTML-/JS-Dokument an den Browser. Der PHP-Code ist darin vollständig verschwunden.

2.5.3 Im Browser
Wenn der Browser die Seite von oben nach unten liest und ausführt:

Erstellt er die globale Variable window.gasData und füllt sie mit dem von PHP geschriebenen Objekt.

Lädt er direkt danach die script.js.

Da script.js erst nach diesem Block geladen wird, kann es problemlos auf window.gasData zugreifen und die Werte an ECharts übergeben:

console.log(window.gasData.months);

console.log(window.gasData.months);

3. Die Darstellung als Diagramm

3.1 Basis-Darstellung

Zunächst werden die Daten des übergebenen Objekts dekonstruiert:

const { months, values, values2026, diffValues } = window.gasData;

const { months, values, values2026, diffValues } = window.gasData;

Dann wird das DOM-Element, am dem das Diagram dargestellt wird, ermittelt und das ECharts-Objekt dort initialisiert:

const chartDom = document.getElementById('chart');
const chart = echarts.init(chartDom);

const chartDom = document.getElementById('chart');
const chart = echarts.init(chartDom);

Anschließend werden die Optionen des Diagramms gesetzt:

chart.setOption({ // Konfiguration für erstes Diagramm setzen
backgroundColor: '#fff', // weißer Diagrammhintergrund
legend: { // Legenden-Konfiguration
data: ['Ø full (alle Jahre)', 'Ø full (2026)']
},

xAxis: { // X-Achsen-Konfiguration
type: 'category', // Kategorie-Achse verwenden
data: months // Monatsnamen als Achsenbeschriftung
},

yAxis: {
type: 'value', // Werteachse verwenden
name: '%' // Achsenbeschriftung Prozent
},

series: [ // Datenreihen des Diagramms
{ // erste Datenreihe
name: 'Ø full (alle Jahre)', // Name der Reihe
type: 'line', // Liniendiagramm
smooth: true, // Linie geglättet darstellen
data: values, // Werte der Reihe
itemStyle: { color: '#9bca3c' }, // Punktfarbe
lineStyle: { color: '#9bca3c' } // Linienfarbe
}, // Ende erste Datenreihe

{ // zweite Datenreihe
name: 'Ø full (2026)', // Name der Reihe
type: 'line', // Liniendiagramm
smooth: true, // Linie geglättet darstellen
data: values2026, // Werte der Reihe
itemStyle: { color: '#5182e4' }, // Punktfarbe
lineStyle: { color: '#5182e4' } // Linienfarbe
}, // Ende zweite Datenreihe

] // Ende series
});

chart.setOption({ // Konfiguration für erstes Diagramm setzen
backgroundColor: '#fff', // weißer Diagrammhintergrund
legend: { // Legenden-Konfiguration
data: ['Ø full (alle Jahre)', 'Ø full (2026)']
},

xAxis: { // X-Achsen-Konfiguration
type: 'category', // Kategorie-Achse verwenden
data: months // Monatsnamen als Achsenbeschriftung
},

yAxis: {
type: 'value', // Werteachse verwenden
name: '%' // Achsenbeschriftung Prozent
},

series: [ // Datenreihen des Diagramms
{ // erste Datenreihe
name: 'Ø full (alle Jahre)', // Name der Reihe
type: 'line', // Liniendiagramm
smooth: true, // Linie geglättet darstellen
data: values, // Werte der Reihe
itemStyle: { color: '#9bca3c' }, // Punktfarbe
lineStyle: { color: '#9bca3c' } // Linienfarbe
}, // Ende erste Datenreihe

{ // zweite Datenreihe
name: 'Ø full (2026)', // Name der Reihe
type: 'line', // Liniendiagramm
smooth: true, // Linie geglättet darstellen
data: values2026, // Werte der Reihe
itemStyle: { color: '#5182e4' }, // Punktfarbe
lineStyle: { color: '#5182e4' } // Linienfarbe
}, // Ende zweite Datenreihe

] // Ende series
});

Das zweite Diagramm wird auf die gleiche Weise initialisiert, nur die Werte stammen aus diffValues.

3.2 Erweiterung der Darstellung

Man erkennt, dass die Werte des Jahres 2026 ca. 25 bis 30 Prozent unter denen des historischen Mittels liegt. Das ist aber nur grob abgeschätzt. Man kann zur genaueren Bestimmung der Unterschiede die Differenzen direkt in das Diagramm einzeichnen:

Original-Diagramm – Programmcode bei GitHub

Die zugefügten Linien nenn sich „range lines“. Sie werden in das Feld series in den Chart-Optionen eingefügt (ausgelassener Code ist mit ‚…‘ gekennzeichnet):

rangeChart.setOption({ // Konfiguration für Differenzlinien-Diagramm setzen
...
series: [ // Datenreihen des Diagramms
...
{ // erste Datenreihe: Ø alle Jahre
...
markLine: { // Verbindungslinien mit Differenzbeschriftung
silent: true, // keine eigene Interaktion/Tooltip für die Linien
symbol: 'none', // keine Pfeilspitzen an den Enden
lineStyle: { color: '#999', type: 'dashed' }, // dezente, gestrichelte Linie
label: { // Beschriftung mit dem Differenzwert
formatter: '{c}', // zeigt den 'value' des Startpunkts
position: 'middle', // Beschriftung mittig auf der Linie
color: '#333', // Textfarbe
fontSize: 11 // Textgröße
}, // Ende label
data: months // je Monat eine Linie zwischen den beiden Punkten
.map((_, i) => [ // Start- und Endpunkt der Linie
{ coord: [i, values[i]], value: diffValues[i] }, // Startpunkt mit Differenzwert
{ coord: [i, values2026[i]] } // Endpunkt
])
.filter((_, i) => diffValues[i] !== null) // Monate ohne 2026-Wert auslassen
} // Ende markLine
}, // Ende erste Datenreihe
...
] // Ende series
}); // Ende rangeChart.setOption

rangeChart.setOption({ // Konfiguration für Differenzlinien-Diagramm setzen
...
series: [ // Datenreihen des Diagramms
...
{ // erste Datenreihe: Ø alle Jahre
...
markLine: { // Verbindungslinien mit Differenzbeschriftung
silent: true, // keine eigene Interaktion/Tooltip für die Linien
symbol: 'none', // keine Pfeilspitzen an den Enden
lineStyle: { color: '#999', type: 'dashed' }, // dezente, gestrichelte Linie
label: { // Beschriftung mit dem Differenzwert
formatter: '{c}', // zeigt den 'value' des Startpunkts
position: 'middle', // Beschriftung mittig auf der Linie
color: '#333', // Textfarbe
fontSize: 11 // Textgröße
}, // Ende label
data: months // je Monat eine Linie zwischen den beiden Punkten
.map((_, i) => [ // Start- und Endpunkt der Linie
{ coord: [i, values[i]], value: diffValues[i] }, // Startpunkt mit Differenzwert
{ coord: [i, values2026[i]] } // Endpunkt
])
.filter((_, i) => diffValues[i] !== null) // Monate ohne 2026-Wert auslassen
} // Ende markLine
}, // Ende erste Datenreihe
...
] // Ende series
}); // Ende rangeChart.setOption

… und nun?

Aus diesen reinen Datendarstellungen Gründe abzuleiten, warum die Füllstände so niedrig sind, ist nicht möglich. Dafür müssen weitere Daten einbezogen und der Zusammenhang mit Wirtschaft, Gesellschaft und Politik hergestellt werden.

Wie das geht sehen wir in Teil 3: Daten und Politik.