Create Heat map using d3.js with Angular.

Checkout the previous post to get started with d3.js in Angular application.

In this post we will talk about creating a heat map chart using the capabilities of d3.js.

Just like any chart or graph, heat map is also a charting technique to visualize data in two dimensions w.r.t magnitude of an occurrence of an event represented with a color schema, typically the color schema varies from low to high depending on the magnitude.

Heat map

Create a new component.

import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import * as d3 from 'd3v6';
import * as _ from 'lodash';
import {
    HeatMap_ColorRange,
    HeatMap_DomainRange,
    HeatMap_Groups,
    HeatMap_Variables,
} from '../commom/constants';
import { HeatMapDataItemInterface } from '../commom/interfaces/heat-map-data-item.interface';
import { ChartsDataService } from '../commom/services/charts-data.service';

@Component({
    selector: 'app-heat-map',
    templateUrl: './heat-map.component.html',
    styleUrls: ['./heat-map.component.scss'],
})
export class HeatMapComponent implements OnInit {
    @ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
    @Input() chartId: string = 'heatmap';
    @Input() width: number = 600;
    @Input() height: number = 450;
    _chartData: HeatMapDataItemInterface[];

    private margin = { top: 40, right: 20, bottom: 20, left: 40 };
    private _chart = {
        svg: null,
        mainContainer: null,
        data: [],
        daysCount: 30,
        datesStrAsPerRange: '',
    };

    element!: HTMLElement;

    constructor(private service: ChartsDataService) {
        this._chart.data = service.getHeatMapData();
    }

    ngOnInit(): void {
        this.createHeatmap();
    }

    createHeatmap() {
        // set the dimensions and margins of the graph
        this.width =
            this.chartContainer.nativeElement.getBoundingClientRect().width;
        const widgetWidth =
            (this.chartContainer.nativeElement.getBoundingClientRect().width *
                60) /
            100;
        let heatMapHeight = Math.round(widgetWidth / 3.167);
        this.width = widgetWidth - this.margin.left - this.margin.right;
        this.height = heatMapHeight - this.margin.top - this.margin.bottom;
        const legendWidth = 80;

        /**
         * Remove all the elements in d3 charts if already exist
         */
        d3.select('#' + this.chartId)
            .selectAll('*')
            .remove();
        d3.select('#' + this.chartId + '_legend')
            .selectAll('*')
            .remove();

        /**
         * Mouse over function handler for the treemap nodes
         * Here we highlight the circle with the box shadow css
         */
        const mouseover = function () {
            d3.select(this).style('stroke', 'black');
        };

        /**
         * Mouse out handler
         */
        const mouseout = function () {
            d3.select(this).style('stroke', 'none');
        };

        // append the svg object to the body of the page
        const svg = d3
            .select('#' + this.chartId)
            .append('svg')
            .attr('width', this.width + this.margin.left + this.margin.right)
            .attr('height', this.height + this.margin.top + this.margin.bottom)
            .append('g')
            .attr(
                'transform',
                'translate(' + this.margin.left + ',' + this.margin.top + ')'
            );

        let groupNames = Object.values(HeatMap_Groups);
        let variableNames = Object.values(HeatMap_Variables);

        // Build X scales and axis:
        const x = d3
            .scaleBand()
            .range([0, this.width])
            .domain(groupNames)
            .padding(0.05);
        svg.append('g')
            .style('font-size', 12)
            .style('color', '#ADADAD')
            .attr('transform', 'translate(0, -5)')
            .call(d3.axisTop(x).tickSize(0))
            .select('.domain')
            .remove();

        // Build Y scales and axis:
        const y = d3
            .scaleBand()
            .range([this.height, 0])
            .domain(variableNames)
            .padding(0.05);
        svg.append('g')
            .style('font-size', 12)
            .style('color', '#ADADAD')
            .attr('transform', 'translate(-5, 0)')
            .call(d3.axisLeft(y).tickSize(0))
            .select('.domain')
            .remove();

        const myColor = d3
            .scaleLinear<string>()
            .range(HeatMap_ColorRange)
            .domain(HeatMap_DomainRange);

        // add the squares
        const cards = svg
            .selectAll()
            .data(this._chart.data, function (d) {
                return d.group + ':' + d.variable;
            })
            .enter()
            .append('rect')
            .attr('x', function (d) {
                return x(d.group);
            })
            .attr('y', function (d) {
                return y(d.variable);
            })
            .attr('rx', 4)
            .attr('ry', 4)
            .attr('value', function (d) {
                return d.value;
            })
            .attr('width', x.bandwidth())
            .attr('height', y.bandwidth())
            .attr('fill', 'white')
            .style('fill', function (d) {
                if (d.value === null || isNaN(d.value)) {
                    return '#C3C3C3';
                }
                return myColor(d.value);
            })
            .style('stroke-width', 2)
            .style('stroke', 'none')
            .style('opacity', 1)
            .on('mouseover', mouseover)
            .on('mouseout', mouseout)
            .on('mouseleave', mouseout);

        // append the legend svg object to the body of the page
        const legend = d3
            .select('#' + this.chartId + '_legend')
            .append('svg')
            .attr(
                'transform',
                'translate(' +
                    +(
                        this.width -
                        HeatMap_ColorRange.length * legendWidth +
                        this.margin.left
                    ) +
                    ',0)'
            )
            .attr('width', legendWidth * HeatMap_ColorRange.length)
            .attr('height', 35);

        const domainRange: string[] = HeatMap_DomainRange.map(
            (item, index, items) => {
                const domain = '' + item;
                if (index === 0) {
                    return '>' + domain;
                } else if (index === items.length - 1) {
                    return '<' + domain;
                }
                return '';
            }
        );
        legend
            .selectAll()
            .data(domainRange, function (d) {
                return d;
            })
            .enter()
            .append('g')
            .attr('class', 'g-legend')
            .append('rect')
            .attr('x', function (d, i) {
                return legendWidth * i;
            })
            .attr('y', 20)
            .attr('rx', 0)
            .attr('ry', 0)
            .attr('width', legendWidth)
            .style('width', '' + legendWidth + 'px')
            .attr('height', 10)
            .style('height', '10px')
            .style('fill', function (d, i) {
                return '' + HeatMap_ColorRange[i];
            });

        legend
            .selectAll('.g-legend')
            .append('text')
            .attr('class', 'g-legend-text')
            .text(function (d) {
                return '' + d;
            })
            .attr('x', function (d, i) {
                if (i === 0) {
                    return 0;
                }
                return legendWidth * i;
            })
            .attr('y', 10)
            .style('font-size', 11)
            .style('fill', '#ADADAD');

        legend.exit().remove();
    }
}

Code is pretty much self explanatory, there are two scales and axis (x, y) and the data is represented with groups, variables and domain range (color schema).

Check out the complete Angular project code on GitHub repo.

Hope you like the post!.

In the next post I will be trying transitions (animations), responsiveness and tooltips for the bar chart, so be sure to check it out.

Published by Kumar Gandhi K

Hi! I’m Kumar and I live in Bangalore (IN) with my family. By profession I’m a Web Developer experienced in a vast variety of frameworks and technologies, like, HTML, CSS, JavaScript, Angular, Bootstrap… Using these i have built or sometimes maintained mid market and enterprise level applications. I worked for few software companies over the years and on few domains like finance, field-service and storage. I also did consulting job at one point. I am loyal and hard (or for better word smart) working individual. In my free time I read books, in fact I buy a lot of books hoping that some day I might find all the time in the world to read them and I also enjoy watching TV.

Leave a comment