D3 TopoJSON U.S. map resizing in Angular

Solution 1:

Here is a complete solution that resizes the map upon window resize. Bonus implementation with reference from (https://codepen.io/TiannanZ/pen/rrEKoB). I hope you find this helpful.

Stackblitz Example

[app.component.html]

<div class="container-fluid">
  <div class="row">
    <div class="col g-0 col-xxl-8 col-xl-8 col-lg-8 col-md-8 col-sm-12 col-12">
      <div id="map">
        <svg
          width="100%"
          height="100%"
          stroke-linejoin="round"
          stroke-linecap="round"
        >
          <defs>
            <filter id="blur">
              <feGaussianBlur stdDeviation="5"></feGaussianBlur>
            </filter>
          </defs>
        </svg>
      </div>
    </div>
    <div class="col-4">
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia, magnam
      at dolore iure laborum minima doloribus voluptate sed harum impedit sit,
      quos in architecto adipisci minus quo ipsa debitis magni.
    </div>
  </div>
</div>

[app.component.scss]

#map {
  max-width: 1000px;
  margin: 2%;
  padding: 20px;
}

[app.component.ts]

import { Component, ElementRef, OnInit } from '@angular/core';

import * as d3 from 'd3';
import * as topojson from 'topojson-client';
import { GeometryCollection } from 'topojson-specification';

import { TopographyService } from './topography.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  svg: any;
  projection: any;
  topoFeatureStates: any;
  path: any;

  constructor(
    private topographyService: TopographyService,
    private el: ElementRef
  ) {}

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

  initialMap(): void {
    this.topographyService.getTopographyData().subscribe((topography: any) => {
      this.draw(topography);
    });
  }

  draw(topography): void {
    const { width, height } = this.getMapContainerWidthAndHeight();

    this.topoFeatureStates = topojson.feature(
      topography,
      topography.objects.states
    );
    this.projection = d3
      .geoIdentity()
      .fitSize([width, height], this.topoFeatureStates);

    this.path = d3.geoPath(this.projection);

    // render svg
    this.svg = d3
      .select('svg')
      .attr('width', width + 50)
      .attr('height', height);

    this.renderNationFeaturesWithShadow(topography);
    this.renderCountiesFeatures(topography);
    this.renderStateFeaures(topography);

    // resize event
    d3.select(window).on('resize', this.resizeMap);
  }

  renderNationFeaturesWithShadow(topography: any): void {
    const defs = this.svg.select('defs');
    defs
      .append('path')
      .datum(topojson.feature(topography, topography.objects.nation))
      .attr('id', 'nation')
      .attr('d', this.path);

    this.svg
      .append('use')
      .attr('xlink:href', '#nation')
      .attr('fill-opacity', 0.2)
      .attr('filter', 'url(#blur)');

    this.svg.append('use').attr('xlink:href', '#nation').attr('fill', '#fff');

    // extra touch (counties in grid)
    this.svg
      .append('path')
      .attr('fill', 'none')
      .attr('stroke', '#777')
      .attr('stroke-width', 0.35)
      .attr(
        'd',
        this.path(
          topojson.mesh(
            topography,
            topography.objects.counties,
            (a: any, b: any) => {
              // tslint:disable-next-line:no-bitwise
              return ((a.id / 1000) | 0) === ((b.id / 1000) | 0);
            }
          )
        )
      );
    // end extra touch
  }

  renderCountiesFeatures(topography: any): void {
    this.svg
      .append('g')
      .attr('class', 'county')
      .attr('fill', '#fff')
      .selectAll('path')
      .data(
        topojson.feature(
          topography,
          topography.objects.counties as GeometryCollection
        ).features
      )
      .join('path')
      .attr('id', (d: any) => {
        return d.id;
      })
      .attr('d', this.path);
  }

  renderStateFeaures(topography: any): void {
    this.svg
      .append('g')
      .attr('class', 'state')
      .attr('fill', 'none')
      .attr('stroke', '#BDBDBD')
      .attr('stroke-width', '0.7')
      .selectAll('path.state')
      .data(
        topojson.feature(
          topography,
          topography.objects.states as GeometryCollection
        ).features
      )
      .join('path')
      .attr('id', (d: any) => {
        return d.id;
      })
      .attr('d', this.path);
  }

  resizeMap = () => {
    const { width, height } = this.getMapContainerWidthAndHeight();

    this.svg.attr('width', width + 50).attr('height', height);

    // update projection
    this.projection.fitSize([width, height], this.topoFeatureStates);

    this.svg.selectAll('path').attr('d', this.path);
  };

  getMapContainerWidthAndHeight = (): { width: number; height: number } => {
    const mapContainerEl = this.el.nativeElement.querySelector(
      '#map'
    ) as HTMLDivElement;
    const width = mapContainerEl.clientWidth - 50;
    const height = (width / 960) * 600;

    return { width, height };
  };
}

[topography.service.ts]

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class TopographyService {
  constructor(private http: HttpClient) {}

  getTopographyData(): Observable<any> {
    const topoDataURL =
      'https://cdn.jsdelivr.net/npm/us-atlas@3/counties-albers-10m.json';

    return this.http.get(topoDataURL);
  }
}