Pierre Vivet Pierre Vivet - 4 months ago 25
jQuery Question

D3.js selectors and functions on a jQuery object

I'm having troubles to use D3 selectors and functions on a previously created jQuery object.

In my case I got a map of french departments and I want to zoom to a specific one thanks to a select menu. It works fine when I create and populate the menu with D3.
But as I want a search box in the menu (to quickly access one specific department) I use the select2 jquery plugin which offers that option.

The problem is, when I create the menu with select2, and then populate it with D3 I lose the click handlers previously registered with D3: nothing happens when I click in the menu.

Here is a JSFiddle to show how it works when I fully use D3:
http://jsfiddle.net/c62uektb/

And here with the select2 object and the function that doesn't work anymore (only one line is modified):
http://jsfiddle.net/z5ygktnw/

The relevant part is between lines 43 and 50:

$("#zoomForm").select2();

d3.select('#zoomForm').selectAll("option")
.data(topojson.feature(data, data.objects.territoire).features)
.enter().append("option")
.text(function(d) { return "department n°" + d.properties.code; })
.on("click", clicked);


I looked on questions and explanations about interactions between D3 and jQuery but didn't find the answer to solve the problem.

Answer

D3 and jQuery do handle registering of event handler slightly different, which is why your select2 plugin's select options won't trigger event handlers registered via D3's selection.on() method. An easy way around this would be to use jQuery to register the clicked handler function to your select box:

$("#zoomForm").select2()
  .on("select2:select", clicked);  // Register the event handler

On the other hand, you can get rid of the registration when populating the select box with D3:

d3.select('#zoomForm').selectAll("option")
    .data(topojson.feature(data, data.objects.territoire).features)
    .enter().append("option")
    .text(function(d) { return "department n°" + d.properties.code; })
//  .on("click", clicked);   // Not needed, done using jQuery

Lastly, you need to adjust your clicked() handler function because it needs access to the data bound using D3. Since jQuery, in contrast to D3, won't pass the bound data to this handler but rather the event object, you need to retrieve the data yourself:

function clicked(evt) {
  // Get the datum bound to the element by D3
  var d = d3.select(evt.params.data.element).datum(); 

  // ...the rest remains untouched. 

Have a look at this working example:

var width = 600, height = 550, active = d3.select(null);

var path = d3.geo.path();

var projection = d3.geo.conicConformal() //focus on France
	.center([2.454071, 47.279229])
	.scale(3000)
	.translate([width / 2, height / 2]);

var zoom = d3.behavior.zoom()
    .translate([0, 0])
    .scale(1)
    .scaleExtent([1, 20])
    .on("zoom", zoomed);
	
path.projection(projection);

var svg = d3.select('#map').append("svg")
	.attr("id", "svg")
	.attr("width", width)
	.attr("height", height);
	
var departments = svg.append("g");

svg.call(zoom);

d3.json('https://gist.githubusercontent.com/PierreVivet/f46c2fe235ec7d7ab2db3dbaa163cc50/raw/f2f3fb092beb94f3a0582a9a82a040fa789028c1/departements.json', function(req, data) {
	data.objects.territoire.geometries.forEach(function (d) {
		d.properties = {};
		d.properties.code = d.code;
	});
	
	departments.selectAll("path")
		.data(topojson.feature(data, data.objects.territoire).features)
		.enter()
		.append("path")
		.attr("d", path)
		.attr('id', function(d) {return "d" + d.properties.code;})
		.style("fill", "white")
		.style("stroke", "black")
		.style("stroke-width", ".2px");
	
	$("#zoomForm").select2()
    .on("select2:select", clicked);  // Register the event handler
		
	d3.select('#zoomForm').selectAll("option")
		.data(topojson.feature(data, data.objects.territoire).features)
		.enter().append("option")
		.text(function(d) { return "department n°" + d.properties.code; })
//		.on("click", clicked);
});

function clicked(evt) {
  // Get the datum bound to the element by D3
  var d = d3.select(evt.params.data.element).datum();  

  var bounds = path.bounds(d),
      dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  svg.transition()
      .duration(750)
      .call(zoom.translate(translate).scale(scale).event);
}

function zoomed() {
  departments.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>

<script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.11.1/jquery-ui.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.full.min.js"></script>

<body>
  <div>
    <form id="form">
      <fieldset>
        <legend>Zoom on</legend>
          <select id="zoomForm" style="width: 28%">
          </select>
      </fieldset>
    </form>	
    <div id="map"></div>
  </div>
</body>

Comments