I have an Angular app in which an HTML page contains a div > row > col
with a D3 TopoJSON map.
I have seen various solutions for resizing maps to parent containers within regular JS frameworks, but these do not seem to translate smoothly to Angular. Ideally I'd also like to add a drop shadow around the nation object, and examples of this also don't seem to function in Angular (I think the main issue is my ineptitude at interacting with the DOM from TypeScript).
I have a page set up as follows:
HTML
<div class="container-fluid">
<div class="row">
<div id="map-col" class="col g-0 col-xxl-8 col-xl-8 col-lg-8 col-md-8 col-sm-12 col-12">
<div class="map"></div>
</div>
<div class="col-4"></div>
</div>
CSS
.container-fluid {
height: 70%;
width: 100%;
padding: 1%;
}
.row {
width: 100%;
height: 100%;
padding: 0 0 0 0;
margin: 0 0 0 0;
}
.map {
height: 100%;
}
TypeScript
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as d3 from 'd3';
import * as topojson from 'topojson-client';
import { GeometryCollection } from 'topojson-specification';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private http: HttpClient) { }
// mapInit
path: any = d3.geoPath()
topography: any = Object
svg: any = null
g: any = null
nation: any = null
states: any = null
counties: any = null
async ngOnInit(): Promise<void> {
await this.mapInit()
}
async mapInit() {
this.topography = await this.http.get(`https://cdn.jsdelivr.net/npm/us-atlas@3/counties-albers-10m.json`).toPromise()
this.svg = d3.select(".map").append("svg")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("height", "100%")
.attr("width", "100%")
this.g = this.svg.append("g")
this.nation = this.g.append('g')
.attr("class", "nation")
.attr("fill", "none")
.selectAll('path')
.data(topojson.feature(this.topography, this.topography["objects"]["nation"] as GeometryCollection)["features"])
.join("path")
.attr('d', this.path)
this.counties = this.g.append("g")
.attr("class", "county")
.attr("fill", "#E7E7E8")
.attr("stroke", "#ffffff")
.attr("stroke-linejoin", "round")
.attr("stroke-width", "0.25")
.selectAll('path')
.data(topojson.feature(this.topography, this.topography["objects"]["counties"] as GeometryCollection)["features"])
.join("path")
.attr("id", function(d:any) {return d["id"]})
.attr('d', this.path)
this.states = this.g.append('g')
.attr("class", "state")
.attr("fill", "none")
.attr("stroke", "#ffffff")
.attr("stroke-linejoin", "round")
.attr("stroke-width", "0.5")
.selectAll("path.state")
.data(topojson.feature(this.topography, this.topography["objects"]["states"] as GeometryCollection)["features"])
.join("path")
.attr("id", function(d:any) {return d["id"]})
.attr("d", this.path)
}
But this results in a map that overflows its parent (see screenshot).
Is there a way to get the map to continuously identify the dimensions of its parent, adjust its own to maintain its aspect ratio but fit inside, and resize? Bonus points if you're aware of a way to cleanly translate this shadowing script: https://codepen.io/TiannanZ/pen/rrEKoB !
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.
[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);
// resize the map
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);
}
}
Live example: https://stackblitz.com/edit/angular-d3-map-topojson
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.