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 pro rozhodnutí, 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 pro vývojáře oříšek. 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 výkonná, plně reaktivní a nezpůsobuje zbytečné překreslování DOMu. Moderní Angular s podporou zoneless režimu a signálů byl pro tento účel ideální volbou.
Čtyři série v jednom grafu bez vizuálního chaosu
Při stavbě složitého grafu počasí je kombinace Angularu a knihovny Chart.js osvědčený základ 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 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é svislé osy (osy Y). 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 drží tyto preference reaktivní signály (Signal); změna jednotek nebo motivu spustí efekt, který konfiguraci Chart.js postaví znovu s přepočtenými hodnotami.
Samotná komponenta grafu je navržena jako bezstavová (stateless standalone component). Data do ní proudí výhradně skrze vstupy (@Input). Komponenta si sama data nestahuje, což usnadňuje její testování a znovupoužitelnost; jediný odchozí požadavek vzniká, když si přihlášený uživatel přepnutím série uloží nastavení.
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í hodinové údaje, ale uživatel rychle ztrácí přehled o tom, kde končí úterý a začíná středa.
Řešením je implementovat do Chart.js vlastní plugin, který na pozadí grafu vykreslí svislé oddělovací čáry přesně v místech, kde začíná nový den (tedy o půlnoci).
Plugin se registruje přímo v konfiguraci grafu a využívá metodu beforeDraw nebo afterDraw k přístupu k 2D plátnu (canvas). Pomocí interních metod Chart.js pro přepočet datových bodů na pixely dokážeme přesně určit 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á čára „teď“
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.
Aktivitu karty v prohlížeči navíc sleduje Page Visibility API. Pokud uživatel odejde na jinou kartu a po hodině se vrátí, graf díky změně stavu viditelnosti překreslí pozici aktuální čáry okamžitě, místo aby čekal na další tik minutového časovače.
Tento přístup zajišťuje, že informace o aktuálním čase v grafu zůstávají vždy přesné, a zároveň šetří baterii mobilních zařízení, na kterých lidé předpověď počasí sledují nejčastěji.
Tooltip, který dává smysl
Výchozí tooltipy v Chart.js se vykreslují přímo do canvasu. To s sebou přináší značná omezení ve stylování, responzivity a přístupnosti. Pokud chceme zobrazit čtyři různé veličiny s jejich specifickými jednotkami a ikonkami, standardní textové pole rychle naráží na hranice svých možností.
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á ve chvíli, kdy myš najede na bod grafu předá souřadnice a surová data.
Naše komponenta v Angularu tyto souřadnice zachytí a umístí HTML element s absolutní pozicí 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 dotykových 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ů; dva nejdelší (72 a 168 hodin) jsou součástí placeného tarifu, takže v bezplatné verzi data končí dřív. Díky tomu, že jsou karty počasí plně bezstavové, stačí při změně rozsahu pouze vyfiltrovat datové pole, které do komponenty vstupuje. Komponenta dostane celé pole hodin a vybrané okno si z něj ukrojí sama; rodič drží jen zvolený rozsah.
Jak taková data získat bez zbytečného přetěžování serveru i poskytovatele, se dočtete v článku o tom, jak se data dostanou do grafu levně.
Díky reaktivitě postavené na signálech zaregistruje Angular okamžitě změnu vstupního pole, přepočítá konfiguraci Chart.js a graf překreslí křivky. Přepnutí série v legendě přitom instanci Chart zachová — jen se změní viditelnost datové řady; změna rozsahu, jednotek nebo motivu ji postaví znovu.
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 sleduje pouze 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 platformě Weather InTouch se proto stav viditelnosti jednotlivých datových řad ukládá.
Když uživatel skryje vítr, tato preference se okamžitě uloží do localStorage prohlížeče. U přihlášených uživatelů se nastavení synchronizuje přes API také 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.

