Angular D3 TopoJSON: Zoom Functionality

Published

I have been struggling with making a functional choropleth map in D3 on Angular, and thus far have managed to piece together a tooltip and zoom-to-county-on-click functionality. I am currently trying to figure out:

  1. How to get the zoom to reset upon second click, but most examples online seem to use a different zoom functionality entirely.
  2. How to enable panning, which may affect generic zoom as mentioned in:
  3. How to enable a zoom button to conduct a generic zoom in the center of the current view

I have commented my TypeScript below with questions related to this.

map.component.html

<div id="map">
  <div id="button-row">
    <button id="zoom-in">+</button>
    <button id="zoom-out">-</button>
    <button id="zoom-reset">$</button>
  </div>
</div>

map.component.ts

import { Component, OnInit } from '@angular/core';
import {FetchTopoService} from '../fetch-topo.service';  // Service that simply returns the following topo json: 'https://cdn.jsdelivr.net/npm/[email protected]/counties-albers-10m.json'
import * as d3 from 'd3';
import * as topojson from 'topojson-client';  // Is this the current best approach?
import { GeometryObject, GeometryCollection, Topology, Objects } from 'topojson-specification';  // Is this different from the topojson-client? How?


@Component({
  selector: 'app-map',
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit {
  // Note: ideally the map initialization would be reusable (in a directive?) and I would not have to specify any features related to it within the component.
  topo: any = null
  svg: any = null
  path: any = d3.geoPath()
  nation: any = null
  states: any = null
  counties: any = null
  tooltip: any = null
  zoom: any = null
  zoomIn: any = null

  constructor( private topoService: FetchTopoService ) { }

  async ngOnInit(): Promise<void> {
    this.topo = await this.topoService.getData()
    this.initMap("#map"). // Initializing to the #map id element.
  }

  initMap(divId:any) {

    // Instantiate tooltip.
    this.tooltip = d3.select(divId)
      .append('div')
      .attr('class', 'map-tooltip')
      .style('visibility', 'hidden')
      .style('background-color', 'white')
      .style('padding', '5px')
      .style('position', 'absolute')
      .style('opacity', '0.5')
      .on('mouseover', (event) => {
        this.tooltip.style('visibility', 'hidden');
      });

    // Instantiate svg.
    this.svg = d3.select(divId).append('svg')
      .attr("preserveAspectRatio", "xMidYMid meet")
      .attr("viewBox", "0 0 960 600")
      .attr("height", "98%")
      .attr("width", "100%")

    // Draw U.S. nation object.
    this.nation = this.svg.append("path")
      .datum(topojson.feature(this.topo, this.topo["objects"]["nation"]))
      .attr("fill", "#f5f5f5")
      .attr("class", "nation")
      .attr("d", this.path)

    // Draw county objects.
    this.counties = this.svg.selectAll("path.county")
      .data(topojson.feature(this.topo, this.topo["objects"]["counties"] as GeometryCollection)["features"])
        .join("path")
        .attr("id", function(d:any) {return d["id"]})
      .attr("class", "county")
      .attr("fill", "#E7E7E8")
      .attr("stroke", "#ffffff")
      .attr("stroke-linejoin", "round")
      .attr("stroke-width", "0.25")
      .attr("d", this.path)

      // Show tooltip on mouseover.
      .on("mouseover", (event:any, d:any) => {
        this.tooltip.style('visibility', 'visible')
        d3.select(`[id="${d['id']}"]`)
          .attr("stroke-width", "2.0")
      })

      // Hide tooltip on mouse leave.
      .on("mouseleave", (event:any, d:any) => {
        this.tooltip.style('visibility', 'hidden')
        d3.select(`[id="${d['id']}"]`)
          .attr("stroke-width", "0.25")
      })

      // Have tooltip follow mouse movement.
      .on('mousemove', (event:any, d:any) => {
        this.tooltip
          .html(d["id"] + '<br>' + d["properties"]["name"])
          .style('left', (event.x + 10) + 'px')
          .style('top', (event.y + 10) + 'px')
          .style('z-index', '2')
      })

      // On county click, zoom to county.
      .on('click', (event:any, d:any) => {
        this.clicked(d)
      })

    // Overlay state objects (no fill) to get thicker borders — this causes issues in clicked.
    this.states = this.svg.selectAll("path.state")
      .data(topojson.feature(this.topo, this.topo["objects"]["states"]  as GeometryCollection)["features"])
      .join("path")
        .attr("id", function(d:any) {return d["id"]})
      .attr("class", "state")
      .attr("fill", "none")
      .attr("stroke", "#ffffff")
      .attr("stroke-linejoin", "round")
      .attr("stroke-width", "1.25")
      .attr("d", this.path)

    // This does not work — ideally zooms on click zoom button.
    this.zoomIn = d3.select('#zoom-in').on('click', (event:any, d:any) => {
      d3.zoom()
        .scaleExtent([1, 8])
        .scaleBy(this.svg.transition().duration(750), 1.3)
    })

  // On click county, remove states, then translate — there must be an easier way to do this.
  clicked(d:any) {
    this.states
      .attr("stroke", "none")
        let bounds = this.path.bounds(d)
        let dx = bounds[1][0] - bounds[0][0]
        let dy = bounds[1][1] - bounds[0][1]
        let x = (bounds[0][0] + bounds[1][0]) / 2
        let y = (bounds[0][1] + bounds[1][1]) / 2
        let scale = .9 / Math.max(dx / 960, dy / 600)
        let translate = [960 / 2 - scale * x, 600 / 2 - scale * y]
        this.counties.transition()
          .duration(1000)
          .style("stroke-width", 1.5 / scale + "px")
          .attr("transform", "translate(" + translate + ")scale(" + scale + ")")
          d3.select(`[id="${d['id']}"]`)
            .attr("stroke-width", "2.0")
  }

}

Source: Angular Questions

Published
Categorized as angular, d3.js, html, topojson, typescript Tagged , , , ,

Answers

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Still Have Questions?


Our dedicated development team is here for you!

We can help you find answers to your question for as low as 5$.

Contact Us
faq