Hodinový graf počasí, který se dá číst: Chart.js, 4 veličiny, 7 dní
Ikonka řekne „bude pršet". Graf řekne „prší od 14 do 16, pak jde vítr nahoru". Jak narvat 4 veličiny a 168 hodin do jednoho čitelného grafu v Chart.js.
Proč graf poráží ikonku při plánování
Ikonka mráčku se sluníčkem v předpovědi počasí sice vypadá pěkně, ale její informační hodnota je pro detailní plánování minimální. Řekne vám pouze, že dnes budou občasné přeháňky. Graf vám naproti tomu ukáže, že pršet bude od 14:00 do 16:00, následně prudce klesne teplota a zvedne se severní vítr. To je zásadní rozdíl mezi rozhodnutím, zda si pouze vzít bundu, nebo raději přesunout venkovní práci na dopoledne.
Zobrazit velké množství meteorologických dat přehledně je však vývojářská výzva. Jak vykreslit čtyři různé veličiny v průběhu až sedmi dnů po hodinách do jednoho grafu, který zůstane čitelný i na displeji mobilního telefonu? Řešením je kombinace moderního frameworku Angular, knihovny Chart.js a několika vlastních rozšíření, která uživateli umožní interaktivně odfiltrovat šum a soustředit se na to podstatné.
Při návrhu klientské části, kterou používá celý projekt Weather InTouch, se ukázalo, že standardní možnosti vykreslování grafů nestačí. Bylo nutné postavit komponentu, která je maximálně výkonná, plně reaktivní a nezpůsobuje zbytečné překreslování DOMu. Moderní Angular s podporou zoneless režimu a signálů se pro tento účel ukázal jako ideální volba.
Čtyři série v jednom grafu bez vizuálního chaosu
Při stavbě komplexního grafu počasí se kombinace Angularu a knihovny Chart.js ukázala jako ideální pro definici čisté datové vrstvy a její následnou vizualizaci. Abychom uživateli poskytli ucelený přehled, musíme v jednom časovém úseku zobrazit čtyři klíčové veličiny:
- Teplotu vzduchu (spojitá čára)
- Rosný bod (přerušovaná čára indikující vlhkost)
- Srážky (sloupcový graf na pozadí)
- Rychlost a nárazy větru (čára s odlišným stylem a vlastní osou)
Aby nedošlo k vizuálnímu chaosu, graf využívá tři nezávislé vertikální osy (Y-osy). Teplota a rosný bod sdílejí levou osu vyjádřenou ve stupních. Srážky mají pravou osu vyznačenou v milimetrech a vítr má svou vlastní skrytou nebo sekundární osu, aby jeho křivka nezasahovala do teplotních extrémů.
Důležitým aspektem je lokalizace jednotek. Uživatelé v různých částech světa vyžadují odlišné formáty (°C versus °F, milimetry versus palce, metry za sekundu versus kilometry za hodinu či uzly). V Angularu k tomuto účelu využíváme reaktivní signály (Signal), které automaticky přepočítávají surová data z API na základě uživatelských preferencí ještě předtím, než jsou předána do konfigurace Chart.js.
Samotná komponenta grafu je navržena jako bezstavová (stateless standalone component). Data do ní proudí výhradně skrze vstupy (input signály). Komponenta sama neprovádí žádné síťové požadavky, což usnadňuje její testování a znovupoužitelnost v různých částech aplikace.
import { Component, input, computed, effect, ElementRef, viewChild } from '@angular/core';
import { Chart, ChartConfiguration } from 'chart.js/auto';
@Component({
selector: 'app-weather-chart',
standalone: true,
template: `<canvas #chartCanvas></canvas>`,
styleUrls: ['./weather-chart.component.css']
})
export class WeatherChartComponent {
readonly weatherData = input.required<WeatherData[]>();
readonly units = input.required<UserUnits>();
private canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
private chartInstance?: Chart;
private chartConfig = computed<ChartConfiguration>(() => {
const data = this.weatherData();
const currentUnits = this.units();
return {
type: 'line',
data: {
labels: data.map(d => d.timestamp),
datasets: [
{
label: 'Teplota',
data: data.map(d => convertTemp(d.temp, currentUnits.temp)),
yAxisID: 'yTemp',
borderColor: '#ff5722',
tension: 0.4
},
{
label: 'Srážky',
type: 'bar',
data: data.map(d => convertPrecip(d.precip, currentUnits.precip)),
yAxisID: 'yPrecip',
backgroundColor: 'rgba(33, 150, 243, 0.3)'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
yTemp: { type: 'linear', position: 'left' },
yPrecip: { type: 'linear', position: 'right', grid: { drawOnChartArea: false } }
}
}
};
});
constructor() {
effect(() => {
const config = this.chartConfig();
this.updateChart(config);
});
}
private updateChart(config: ChartConfiguration) {
if (this.chartInstance) {
this.chartInstance.destroy();
}
const ctx = this.canvas().nativeElement.getContext('2d');
if (ctx) {
this.chartInstance = new Chart(ctx, config);
}
}
}
Vlastní plugin pro oddělovače dní
Při zobrazení dlouhého časového úseku, například 168 hodin (7 dní), se horizontální osa X stává extrémně nepřehlednou. Standardní popisky Chart.js sice dokážou inteligentně skrývat duplicitní popisky hodin, ale uživatel rychle ztrácí přehled o tom, kde končí úterý a začíná středa.
Chcete-li tento problém vyřešit, musíte do Chart.js implementovat vlastní plugin, který do pozadí grafu vykreslí svislé oddělovací čáry přesně v místech, kde dochází k přechodu na nový den (tedy o půlnoci).
Plugin se registruje přímo v konfiguraci grafu a využívá metodu beforeDraw nebo afterDraw k přístupu na 2D plátno (canvas). Pomocí interních metod Chart.js pro přepočet datových bodů na pixely dokážeme přesně lokalizovat indexy odpovídající půlnoci a vykreslit jemnou vertikální linku s textovým popiskem dne u horního okraje grafu.
const daySeparatorPlugin = {
id: 'daySeparator',
afterDraw: (chart: Chart) => {
const { ctx, chartArea: { top, bottom }, scales: { x } } = chart;
const ticks = x.ticks;
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.font = '11px sans-serif';
ticks.forEach((tick, index) => {
const date = new Date(chart.data.labels[index] as string);
if (date.getHours() === 0) {
const xPos = x.getPixelForValue(index);
// Vykreslení svislé čáry
ctx.beginPath();
ctx.moveTo(xPos, top);
ctx.lineTo(xPos, bottom);
ctx.stroke();
// Vykreslení názvu dne
const dayName = date.toLocaleDateString('cs-CZ', { weekday: 'short' });
ctx.fillText(dayName, xPos + 5, top + 15);
}
});
ctx.restore();
}
};
Pohyblivá „teď" čára
Uživatel, který se dívá na graf, se nejčastěji potřebuje zorientovat vůči aktuálnímu okamžiku. Co se děje právě teď a co mě čeká v nejbližších hodinách? K tomu slouží pohyblivá vertikální čára s označením „teď".
Tato čára se musí dynamicky posouvat s plynoucím časem. V aplikaci běžící na Angularu 22 bez zónové detekce změn (zoneless) můžeme tento proces optimalizovat tak, aby nezatěžoval procesor neustálým překreslováním. Interval aktualizace je nastaven přibližně na jednu minutu.
Dále je implementována detekce aktivity karty prohlížeče pomocí Page Visibility API. Pokud uživatel odejde na jinou záložku a po hodině se vrátí, graf okamžitě detekuje změnu stavu viditelnosti a přepočítá pozici aktuální čáry, aniž by musel na pozadí běžet zbytečný časovač.
Tento přístup zajišťuje, že informace o aktuálním čase v grafu jsou vždy přesné a zároveň šetří baterii mobilních zařízení, na kterých uživatelé předpovědi počasí konzumují nejčastěji.
Tooltip, který dává smysl
Výchozí tooltipy v Chart.js jsou vykreslovány přímo do plátna canvas. To s sebou přináší značná omezení v oblasti stylingu, responzivity a přístupnosti. Pokud chceme zobrazit čtyři různé veličiny s jejich specifickými jednotkami a ikonkami, standardní textový box rychle narazí na své limity.
Vlastní HTML tooltip tento problém řeší. Chart.js umožňuje zcela vypnout nativní tooltip a namísto něj poskytnout callback funkci external, která v momentě najetí myši na bod grafu předá koordináty a surová data.
Naše Angular komponenta tyto souřadnice zachytí a napozicionuje absolutně umístěný HTML element nad samotný canvas. Výhody jsou zřejmé:
- Možnost použít standardní CSS (Grid, Flexbox) pro dokonalé zarovnání.
- Zobrazení barevných indikátorů, jednotek a doplňujících textů v čitelném formátu.
- Plynulé CSS přechody (transitions) při pohybu kurzoru mezi jednotlivými hodinami.
- Snazší podpora pro dotyková zařízení, kde se tooltip chová jako plovoucí karta pod grafem.
Rozsahy 24 až 168 hodin
Každá situace vyžaduje jiný detail pohledu. Pokud plánujete odpolední cyklovýlet, zajímá vás detailní rozlišení na příštích 24 nebo 48 hodin. Pokud plánujete víkend nebo pracovní týden, potřebujete vidět výhled na 7 dní (168 hodin).
Graf podporuje přepínání těchto rozsahů. Díky tomu, že jsou karty počasí plně bezstavové, se změna rozsahu provádí pouhou filtrací datového pole, které do komponenty vstupuje. Jakmile uživatel klikne na tlačítko rozsahu, nadřazená komponenta vyřízne odpovídající úsek dat a předá jej do vstupu grafu.
O tom, jak efektivně získat takto detailní data z meteorologických modelů bez zbytečného přetěžování serveru a peněženky, se dočtete v článku o tom, jak se data dostanou do grafu levně.
Díky reaktivitě postavené na signálech Angular okamžitě zaregistruje změnu vstupního pole, přepočítá konfiguraci Chart.js a graf plynule překreslí křivky bez nutnosti kompletního zničení a znovuvytvoření instance objektu Chart, pokud to není nezbytně nutné z důvodu změny struktury os.
Co si uživatel zapne, to mu zůstane
Při zobrazení čtyř veličin najednou se může stát, že pro konkrétního uživatele je graf příliš přeplněný. Někdo se zajímá pouze o teplotu a srážky, vítr ani rosný bod ho nezajímají. Chart.js standardně umožňuje kliknutím na legendu jednotlivé série skrýt.
V běžné webové aplikaci se však toto nastavení po obnovení stránky ztratí. V rámci platformy Weather InTouch je chování povýšeno na vyšší úroveň. Stav viditelnosti jednotlivých datových řad se ukládá.
Pokud uživatel skryje zobrazení větru, tato preference se okamžitě uloží do localStorage prohlížeče. Pokud je uživatel přihlášen ke svému účtu, synchronizuje se toto nastavení skrze API do databáze (postavené na architektuře NestJS + MongoDB + Redis).
Při dalším přihlášení z jakéhokoli jiného zařízení (například z mobilního telefonu) se graf automaticky vykreslí v konfiguraci, kterou si uživatel sám navolil. Výsledkem je personalizované rozhraní, které respektuje potřeby konkrétního člověka a neobtěžuje ho opakovaným nastavováním filtrů při každé návštěvě.
Podrobnější informace o celkové architektuře a dalších funkcích této meteorologické platformy naleznete na stránce popisující celý projekt Weather InTouch.

