Angular weather-intouch 2026-06-08 · 7 min read

A Readable Hourly Weather Chart: Chart.js, 4 Series, 7 Days

An icon says "rain today." A chart says "rain 2–4pm, then wind picks up." How to fit 4 series and 168 hours into one readable Chart.js graph.

Why a chart beats an icon for planning

While a sun-and-cloud icon in a weather forecast looks nice, its informational value for detailed planning is minimal. It only tells you that there will be occasional showers today. A chart, on the other hand, shows you that it will rain from 2:00 PM to 4:00 PM, followed by a sharp drop in temperature and a rising north wind. This is the fundamental difference between deciding whether to just grab a jacket or to reschedule outdoor work to the morning.

However, displaying a large amount of meteorological data clearly is a developer challenge. How do you render four different variables over up to seven days on an hourly basis into a single chart that remains readable even on a mobile screen? The solution is a combination of the modern Angular framework, the Chart.js library, and several custom extensions that allow users to interactively filter out noise and focus on what matters.

When designing the client-side part used by the entire Weather InTouch project, it became clear that standard chart rendering options were not enough. It was necessary to build a component that is highly performant, fully reactive, and does not cause unnecessary DOM re-renders. Modern Angular with support for zoneless mode and signals proved to be the ideal choice for this purpose.

Four series in one chart without visual chaos

When building a complex weather chart, the combination of Angular and the Chart.js library proved ideal for defining a clean data layer and its subsequent visualization. To provide the user with a comprehensive overview, we need to display four key variables in a single time frame:

  • Air temperature (solid line)
  • Dew point (dashed line indicating humidity)
  • Precipitation (bar chart in the background)
  • Wind speed and gusts (line with a distinct style and its own axis)

To avoid visual chaos, the chart uses three independent vertical axes (Y-axes). Temperature and dew point share the left axis expressed in degrees. Precipitation has the right axis marked in millimeters, and wind has its own hidden or secondary axis so that its curve does not interfere with temperature extremes.

An important aspect is unit localization. Users in different parts of the world require different formats (°C versus °F, millimeters versus inches, meters per second versus kilometers per hour or knots). In Angular, we use reactive signals (Signal) for this purpose, which automatically recalculate raw data from the API based on user preferences before it is passed to the Chart.js configuration.

The chart component itself is designed as a stateless standalone component. Data flows into it exclusively through inputs (input signals). The component itself does not make any network requests, which makes it easier to test and reuse in different parts of the application.

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);
    }
  }
}

Custom plugin for day separators

When displaying a long time span, such as 168 hours (7 days), the horizontal X-axis becomes extremely cluttered. While standard Chart.js labels can intelligently hide duplicate hour labels, the user quickly loses track of where Tuesday ends and Wednesday begins.

To solve this problem, you need to implement a custom plugin in Chart.js that draws vertical separator lines in the background of the chart exactly where the transition to a new day occurs (i.e., at midnight).

The plugin is registered directly in the chart configuration and uses the beforeDraw or afterDraw method to access the 2D canvas. Using internal Chart.js methods to convert data points to pixels, we can precisely locate the indexes corresponding to midnight and draw a subtle vertical line with a text label of the day near the top edge of the chart.

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);
        
        // Draw vertical line
        ctx.beginPath();
        ctx.moveTo(xPos, top);
        ctx.lineTo(xPos, bottom);
        ctx.stroke();

        // Draw day name
        const dayName = date.toLocaleDateString('cs-CZ', { weekday: 'short' });
        ctx.fillText(dayName, xPos + 5, top + 15);
      }
    });
    ctx.restore();
  }
};

Moving "now" line

A user looking at the chart most often needs to orient themselves relative to the current moment. What is happening right now, and what lies ahead in the coming hours? A moving vertical line labeled "now" serves this purpose.

This line must move dynamically as time passes. In an application running on Angular 22 without zone-based change detection (zoneless), we can optimize this process so that it does not strain the CPU with constant re-rendering. The update interval is set to approximately one minute.

Furthermore, browser tab activity detection is implemented using the Page Visibility API. If the user switches to another tab and returns after an hour, the chart immediately detects the visibility state change and recalculates the position of the current line without needing an unnecessary background timer to run.

This approach ensures that the current time information in the chart is always accurate while saving the battery of mobile devices, where users consume weather forecasts most frequently.

Tooltips that make sense

Default tooltips in Chart.js are rendered directly onto the canvas. This brings significant limitations in terms of styling, responsiveness, and accessibility. If we want to display four different variables with their specific units and icons, the standard text box quickly reaches its limits.

A custom HTML tooltip solves this problem. Chart.js allows you to completely disable the native tooltip and instead provide an external callback function, which passes coordinates and raw data when hovering over a chart point.

Our Angular component captures these coordinates and positions an absolutely placed HTML element over the canvas itself. The benefits are obvious:

  • Ability to use standard CSS (Grid, Flexbox) for perfect alignment.
  • Display of color indicators, units, and additional text in a readable format.
  • Smooth CSS transitions when moving the cursor between individual hours.
  • Easier support for touch devices, where the tooltip behaves as a floating card below the chart.

Ranges from 24 to 168 hours

Every situation requires a different level of detail. If you are planning an afternoon bike trip, you are interested in a detailed breakdown of the next 24 or 48 hours. If you are planning a weekend or a work week, you need to see a 7-day outlook (168 hours).

The chart supports switching between these ranges. Because the weather cards are completely stateless, changing the range is done by simply filtering the data array that enters the component. Once the user clicks a range button, the parent component slices the corresponding segment of data and passes it to the chart input.

To read about how to efficiently retrieve such detailed data from meteorological models without overloading the server or your wallet, check out the article on how data gets into the chart on a budget.

Thanks to signal-based reactivity, Angular immediately registers changes to the input field, recalculates the Chart.js configuration, and the chart smoothly redraws the curves without needing to completely destroy and recreate the Chart object instance, unless absolutely necessary due to changes in the axis structure.

What the user enables, stays enabled

When displaying four variables at once, the chart might feel too cluttered for some users. Someone might only care about temperature and precipitation, with no interest in wind or dew point. By default, Chart.js allows users to hide individual series by clicking on the legend.

In a typical web application, however, this setting is lost upon page refresh. Within the Weather InTouch platform, this behavior is taken to the next level. The visibility state of individual data series is persisted.

If a user hides the wind display, this preference is immediately saved to the browser's localStorage. If the user is logged into their account, this setting is synchronized via an API to the database (built on a NestJS + MongoDB + Redis architecture).

Upon logging in from any other device (such as a mobile phone), the chart automatically renders in the configuration the user selected. The result is a personalized interface that respects the needs of the individual and avoids annoying them with repeated filter setups on every visit.

You can find more detailed information about the overall architecture and other features of this meteorological platform on the page describing the entire Weather InTouch project.

Related articles

Contact Info

Feel free to reach out for project inquiries, consulting, or longer-term collaboration. I'm open to discussing practical product work and frontend engineering opportunities.

Get in Touch

© 2026 Martin Hubalek. All rights reserved.