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