javascript – d3.js onclick and touch works erratically on topojson map-ThrowExceptions

Exception or error:

I’ve been playing with d3.js for sometime now and I’ve been trying to create a d3 map wherein clicking on different districts/counties on a state map displays details of the districts/counties next to the map.

I was initially using mouseover and mouseout to display the same, but it wasn’t mobile friendly. So now I’m trying to do the same with onclick but its not working in the same way.

The district should change colour on click (it worked with mouseover). However it changes colour only after several repeated random clicks inside the district.

This is what I have done.

    var width = 345,
    height = 450;
  var projection = d3.geoMercator()
    .center([88.36, 27.58])
    .translate([width / 2, height / 2])
    .scale(6000);
  var path = d3.geoPath()
    .projection(projection);

  var svg = d3.select('#Sk_Map').append('svg')
    .attr('width', width)
    .attr('height', height);

  var g = svg.append('g');

  d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
    .then(state => {

      g.append('path')
        .datum(topojson.merge(state, state.objects.Sikkim.geometries))
        .attr('class', 'land')
        .attr('d', path);

      g.append('path')
        .datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
        .attr('class', 'boundary')
        .attr('d', path);

      g.append("g")
        .selectAll("path")
        .data(topojson.feature(state, state.objects.Sikkim).features)
        .enter()
        .append("path")
        .attr("d", path)
        .attr("class","boundary")

        //.on("mouseover", function(d)){

        .on("click", function(d) {
          var prop = d.properties;

          var string = "<p><strong>District Name</strong>: " + prop.Dist_Name;

          d3.select("#Place_Details")
            .html("")
            .append("text")
            .html(string);
       d3.select(this).attr("class","boundary hover");

        })

            //.on("mouseout"), function(d){

        .on("click", function(d) {
        d3.select("h2").text("");
        d3.select(this).attr("class","boundary")
              .attr("fill", "#ff1a75");
        });
    });
.columns {
    float: left;
    width: 50%;
}

/* Clear floats after the columns */
    .mapcontainer:after {
    content: "";
    display: table;
    clear: both;
}

svg {
      background: #ffffff;
    }

    .land {
      fill: #ff1a75;
    }

    .boundary {
      fill: none;
      stroke: #00ffff;
      stroke-linejoin: round;
      stroke-linecap: round;
      stroke-width: 1px;
      vector-effect: non-scaling-stroke;
    }

h2 {
        top: 50px;
        font-size: 1.6em;
    }
    .hover {
        fill: yellow;
        }
 <script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
    <script src="https://unpkg.com/topojson@3" charset="utf-8"></script>
    <div id="Sk_Map" style="width: 300px; float:left; height:450px; margin:5px"></div>
    <div id="Place_Details" style="width: 400px; float:right; height:450px; overflow: auto; margin:5px"></div>
  

How do I optimize this code? I wish to add zoom functionality to the map but for now I want to display the name of the district/county.

How to solve:

There are a few issues in your code.

Currently you assign two event listeners to the same selection:

  g.append("g")
    .selectAll("path")
    .data(topojson.feature(state, state.objects.Sikkim).features)
    .enter()
    .append("path")
    ...
    .on("click", function(d) {
       /* on click code here */
    })
    .on("click", function(d) {
      /* on click code here too */
    });

When assigning event listeners like this, the second one overwrites the first. So the only event listener in your snippet that is used is the second one:

    .on("click", function(d) {
       d3.select("h2").text("");
       d3.select(this).attr("class","boundary")
         .attr("fill", "#ff1a75");
    });

As you don’t have an h2 element (in the snippet at least), nothing happens.

If we drop the second event listener and use only the first, we still don’t get much of an on click event. In the below, I remove the other features (the ones without click events, as well I remove unrelated css, resize for snippet, and change the feature stroke color). It should be clear why a click event doesn’t work very well, the features have no fill. Click only triggers an event on the boundary:

var width = 345,
    height = 300;
var projection = d3.geoMercator()
  .center([88.36, 27.58])
  .translate([width / 2, height / 2])
  .scale(7000);
var path = d3.geoPath()
  .projection(projection);

var svg = d3.select('#Sk_Map').append('svg')
  .attr('width', width)
  .attr('height', height);

var g = svg.append('g');

d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
    .then(state => {

      g.append("g")
        .selectAll("path")
        .data(topojson.feature(state, state.objects.Sikkim).features)
        .enter()
        .append("path")
        .attr("d", path)
        .attr("class","boundary")
        .on("click", function(d) {
          alert("click!");
        })
});
    .boundary {
      fill: none;
      stroke: black;
      stroke-linejoin: round;
      stroke-linecap: round;
      stroke-width: 1px;
      vector-effect: non-scaling-stroke;
    }
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
    <script src="https://unpkg.com/topojson@3" charset="utf-8"></script>
    <div id="Place_Details"></div>
    <div id="Sk_Map" style="width: 300px; float:left; height:200px; margin:5px"></div>

The solution would be to give the features a fill. This brings us to an optimization: we don’t need the first feature to be drawn:

  g.append('path')
    .datum(topojson.merge(state, state.objects.Sikkim.geometries))
    .attr('class', 'land')
    .attr('d', path);

Because it will entirely be covered by clickable features.

Also, if we want the boundaries to be not clickable, we should draw this feature:

  g.append('path')
    .datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
    .attr('class', 'boundary')
    .attr('d', path);

After we draw the clickable features, so that it is drawn on top. If we don’t care if the borders are clickable, we could skip drawing this feature as we could just apply a stroke to the clickable features. Though internal borders might be somewhat thicker/darker.

Here’s the above modifications:

var width = 345,
    height = 300;
var projection = d3.geoMercator()
  .center([88.36, 27.58])
  .translate([width / 2, height / 2])
  .scale(7000);
var path = d3.geoPath()
  .projection(projection);

var svg = d3.select('#Sk_Map').append('svg')
  .attr('width', width)
  .attr('height', height);

var g = svg.append('g');

d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
    .then(state => {

      g.append("g")
        .selectAll("path")
        .data(topojson.feature(state, state.objects.Sikkim).features)
        .enter()
        .append("path")
        .attr("d", path)
        .attr("class","feature")
        .on("click", function(d) {
             var prop = d.properties;
             var string = "<p><strong>District Name</strong>: " + prop.Dist_Name;
             d3.select("#Place_Details")
               .html(string)
        })
        
      g.append('path')
        .datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
        .attr('class', 'boundary')
        .attr('d', path);        
        
});
.boundary {
   fill: none;
   stroke: #00ffff;
   stroke-linejoin: round;
   stroke-linecap: round;
   stroke-width: 1px;
   vector-effect: non-scaling-stroke;
}

.feature {
   fill: steelblue;
}


.hover {
   fill: yellow;
}
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
    <script src="https://unpkg.com/topojson@3" charset="utf-8"></script>
    <div id="Place_Details"></div>
    <div id="Sk_Map" style="width: 300px; float:left; height:200px; margin:5px"></div>

If you have a question about how to manage two click events, or alternating click events on a feature, that should be a new question.

Leave a Reply

Your email address will not be published. Required fields are marked *