简体   繁体   中英

D3 TopoJSON U.S. map resizing in Angular

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM