Kapitel 4: Was nur der Browser kann
Die vorigen Kapitel ließen sich mit Bildern erzählen. Dieses hier nicht — es geht um Dinge, die du tun musst, um sie zu verstehen.
Zoom und Pan
Ein Bereich, den du selbst einstellen kannst, ist grundlegend etwas anderes als einer, den jemand für dich ausgewählt hat. Ziehen zum Hereinzoomen, Doppelklick zum Zurücksetzen, Mausrad für stufenlosen Zoom.
Viele Punkte
30.000 Messwerte sind kein Randfall, sondern ein Jahr in Minutenauflösung. Die Bibliothek zeichnet nicht jeden Punkt, sondern das, was bei der aktuellen Auflösung sichtbar wäre — und zwar so, dass Ausreißer erhalten bleiben. Naives Ausdünnen würde genau die Spitzen verschlucken, nach denen du suchst.
Mehrere Achsen
Temperatur und Luftfeuchtigkeit in einem Bild, auf getrennten Skalen. Die zweite Achse ist praktisch und verführerisch: Zwei Kurven in einem Rahmen suggerieren einen Zusammenhang, den die Wahl der Skalen erzeugt hat, nicht die Daten. Nutze sie dort, wo der Zusammenhang feststeht, nicht um einen zu unterstellen.
Ab drei Achsen — oder wenn du selbst bestimmen willst, welche links und welche
rechts steht — nimmst du statt axes.left und axes.right die Array-Form
axes.y und ordnest jede Reihe über yAxisIndex zu. Jede Achse trägt dann
ihren eigenen Titel, in ihrer eigenen Farbe, neben ihrer eigenen Linie:
axes: {
y: [
{ position: 'left', label: 'Temperatur (°C)', axis: { color: '#ef4444' } },
{ position: 'left', label: 'Taupunkt (°C)', axis: { color: '#f59e0b' } },
{ position: 'right', label: 'Feuchte (%)', axis: { color: '#3b82f6' } },
{ position: 'right', label: 'Druck (hPa)', axis: { color: '#10b981' } },
],
},
series: [{ name: 'Temperatur', data, yAxisIndex: 0 }, /* … */],axes.y, je zwei pro Seite — vom Go-Renderer gezeichnet.Wo der Titel steht, sucht sich die Bibliothek selbst: neben den
Tick-Beschriftungen seiner Achse, und zwar in der ersten Lücke, die breit genug
ist. Was nicht mehr hineinpasst, rückt nach außen an den Rand. Damit ein Titel
zwischen zwei Achsen Platz findet, braucht er rund 14 px zusätzlich zu den
Beschriftungen — bei Werten wie 1030 hPa also etwa 70 px Achsenabstand statt
der voreingestellten 50:
{ position: 'left', offset: 75, label: 'Taupunkt (°C)' }Ohne den Platz stehen die Titel beieinander am Rand statt je an ihrer Achse. Übereinander stehen sie nie: jeder belegte Streifen ist für den nächsten Titel gesperrt.
Soll ein Titel an einer bestimmten Stelle stehen, setzt labelOffset sie: den Abstand von der
Achsenlinie bis zur Mitte des Titels. Für diesen Titel entfällt die Suche, die anderen weichen
ihm aus. Das gilt auch in der Form mit left/right, wo der Titel sonst 14 px vom Bildrand
steht:
{ position: 'left', label: 'Taupunkt (°C)', labelOffset: 34 }Welche Reihen sich eine Achse teilen, kann man die Daten fragen. axisGroups
nimmt je Reihe Einheit und Messwerte und schlägt vor: gleiche Einheit und
benachbarte Bereiche teilen sich eine; eine Lücke größer als der größere Bereich
bekommt eine eigene. Die Reihenfolge bleibt, die Achsen springen also nicht,
wenn ein Fühler dazukommt:
import { axisGroups } from 'ml-time-graph';
const { indices, axes } = axisGroups(fuehler.map((f) => ({ unit: f.unit, data: f.data })));
new MLTimeGraph({
series: fuehler.map((f, i) => ({ ...f, yAxisIndex: indices[i] })),
axes: { y: axes },
});Angewendet wird nichts — es ist ein Vorschlag, den du noch überstimmen kannst.
Eine Legende zum Anklicken
Bei fünf Messreihen in einem Bild ist das Ausblenden von vieren der schnellste Weg, die fünfte anzusehen. Bis 1.12.0 hieß das: die Legende selbst zeichnen. Die eingebaute war eine Beschriftung und sonst nichts — auf einen Klick passierte nichts, und an einen einzelnen Eintrag kam man gar nicht heran.
Jetzt steht jeder Eintrag in einer eigenen Gruppe und trägt denselben
Schlüssel wie die Serie, die er benennt: #legend-temperature hier,
#series-temperature dort. Von einem zum anderen führt ein Ersetzen, kein
Nachschlagen über den Klarnamen. itemClass hängt eine eigene Klasse an, und
mehr bietet die Bibliothek nicht an — sie bringt nichts zum Klicken mit, sie
bringt etwas zum Klicken hin:
legend: { show: true, itemClass: 'clickmetotoggle' }container.addEventListener('click', (e) => {
const item = e.target.closest('.clickmetotoggle');
if (!item) return;
container.querySelector('#' + item.id.replace('legend-', 'series-'))
.classList.toggle('hidden'); // .hidden { display: none }
});Ein Handler für das ganze Diagramm. Jede Demo auf dieser Seite benutzt genau diesen Code, einmal in der Demo-Maschinerie eingehängt — probier es an einem beliebigen Diagramm mit Legende weiter unten aus.
Dass das überhaupt trägt, hängt an einer Zusage aus 1.3.0: der Schlüssel ist
stabil. Er kommt aus der id der Serie, sonst aus ihrem Namen, und er ist in
TypeScript und Go derselbe. Vorher trug er einen Zeitstempel und änderte sich bei
jedem Rendern — ein Stilblatt oder ein Handler dagegen zeigte eine Sekunde später
ins Leere.
Zwei Diagramme auf derselben Seite, die beide eine Reihe Temperatur zeigen,
schrieben #series-temperature zweimal — ungültiges HTML, und getElementById
findet das falsche Diagramm. Seit 1.17 nimmt jedes Diagramm einen idPrefix:
new MLTimeGraph({ idPrefix: 'keller', /* … */ }); // #keller-series-temperatureDer Handler oben läuft unverändert weiter: aus keller-legend-temperature wird
keller-series-temperature. Die Klassen .legend-item--temperature und
.series--temperature tragen keinen Präfix — sie sind für Stilblätter, und die
sollen alle Diagramme zugleich treffen.
Das umgekehrte Problem kommt aus der Seite um das Diagramm herum. Die Klassen sind schlichte
Wörter, series, time-axis, chart-grid, und eine Regel wie .series { display: none } im
Stilblatt der Seite träfe sie. classPrefix setzt einen Präfix vor jede Klasse, die das
Diagramm schreibt. Anders als idPrefix ist er für alle Diagramme einer Anwendung derselbe:
new MLTimeGraph({ classPrefix: 'mltg', /* … */ }); // class="mltg-series mltg-series--temperature"Die eigene legend.itemClass bleibt, wie du sie geschrieben hast, denn dein Handler hängt
daran.
Selbst zeichnen
Mit position: 'separate' zeichnet die Bibliothek keine —
chart.legendItems() gibt dir, was sie gezeichnet hätte, und du entscheidest, wo
sie steht und was sie tut. Das bleibt der Weg, wenn die Legende als HTML unter
das Diagramm gehört, in eine Seitenleiste, oder wenn sie mehr können muss als
einen Klick.
Jeder Eintrag trägt mehr als Name und Farbe — die Linienart seiner Reihe und, wenn
das Diagramm sie zeichnet, ihr Symbol. Das zählt, sobald das Bild den Bildschirm
verlässt. Auf Papier ist jedes farbige Kästchen dasselbe Grau, und wem Rot und
Grün gleich aussehen, dem hat die Farbe ohnehin nie geholfen; Strichmuster und
Symbol überstehen beides. Zeichne das Kästchen mit drawMarker und dem Renderer,
dann kann es nicht von der Kurve abweichen, die es benennt.
Ein Symbol wird nur gemeldet, wenn es auch gezeichnet wird: Eine Reihe, die ihre Marker oberhalb einiger hundert Punkte ausblendet, bekommt auch in der Legende keines. Ein Symbol, das im Bild fehlt, behauptet etwas Falsches — und auf Papier kann das niemand prüfen.
Ein eigener Tooltip
Der eingebaute Tooltip ist HTML, das die Bibliothek schreibt. Sobald er aussehen soll wie der Rest deiner Anwendung, willst du stattdessen die Daten und schreibst das HTML selbst. Drei Teile reichen dafür:
import { MLTimeGraph, autoMargin, zeigerImSvg } from 'ml-time-graph';
const chart = new MLTimeGraph({ ...optionen, margin: autoMargin(optionen) });
svg.addEventListener('pointermove', (e) => {
const { x, y } = zeigerImSvg(svg, e.clientX, e.clientY); // bei jeder Skalierung
const p = chart.plotRect; // die Zeichenfläche
if (x < p.x || x > p.x + p.width) return verbergen();
zeigen(chart.pointsAt(x, y, { overlays: true })); // eine Zeile je Reihe
});pointsAt liefert den Wert, den die gezeichnete Linie am Zeiger zeigt — in
fester Reihenfolge, nicht nach Abstand sortiert. Eine Treppe hält ihren letzten
Wert; ein Fühler, der nur bei Änderung meldet, bekommt seinen letzten Messwert,
markiert mit held und eigener time; eine Enum-Reihe bringt neben der Zahl
ihr label ("offen") mit. plotRect ist die Fläche innerhalb der Ränder, ein
Fadenkreuz braucht also keine Randrechnung, und autoMargin errechnet die
Ränder aus dem, was wirklich gezeichnet wird — Achsenbeschriftungen, Titel,
Grenzwert-Beschriftungen außerhalb.
Ein ganzes Dashboard
Mehrere Diagramme, ein gemeinsamer Zeitbereich, synchronisierter Zoom.