5
\$\begingroup\$

I created an interactive web map with d3. I am fairly new to web programming and am interested what you think about the code. The map is based on unemployment rates for each NUTS-2 region in europe. I created a sample JSON, so you maybe can use this for a working reproducible example. I hope the code provided is not too much.

EDIT: I checked my code with the provided sample JSON. I noticed that the function playButtonSlider() is moving the slider-handle way to fast. The JSON I use for my project is significantly larger and the slider-handle is moving reasonable slower. So something is definitely not coded well with the function playButtonSlider(). But I don't know a different approach to get the same effect.

sample JSON

{
"type": "FeatureCollection",
"crs": {
    "type": "name",
    "properties": {
        "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
    }
},
"features": [{
        "type": "Feature",
        "geometry": {
            "type": "Polygon",
            "coordinates": [[[10.00, 45.00], [10.00, 50.00], [15.00, 50.00], [15.00, 45.00], [10.00, 45.00]]]
        },
        "properties": {
            "2005": "7.8",
            "2006": "7.3",
            "2007": "4.1",
            "2008": "4.2",
            "2009": "7",
            "2010": "4",
            "2011": "1.8",
            "2012": "4.6",
            "2013": "4.3",
            "2014": "4.8",
            "2015": "7.2",
            "NUTS_ID": "AT11",
            "STAT_LEVL_": 2,
            "SHAPE_Leng": 4.87315,
            "SHAPE_Area": 0.467459,
            "Region": "Burgenland (AT)"
        },
        "id": 1453
    }, {
        "type": "Feature",
        "geometry": {
            "type": "Polygon",
            "coordinates": [[[5.00, 45.00], [5.00, 50.00], [10.00, 50.00], [10.00, 45.00], [10.00, 45.00]]]
        },
        "properties": {
            "2005": "12.7",
            "2006": "12.5",
            "2007": "12",
            "2008": "10.7",
            "2009": "12.7",
            "2010": "10.9",
            "2011": "12.5",
            "2012": "12.6",
            "2013": "15",
            "2014": "15.1",
            "2015": "15.2",
            "NUTS_ID": "AT12",
            "STAT_LEVL_": 2,
            "SHAPE_Leng": 8.788773,
            "SHAPE_Area": 2.321136,
            "Region": "Niederösterreich"
        },
        "id": 1454
    }
]
}

HTML

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title> Europe Unemployment (NUTS-2 Regions) </title>
    <script src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js'></script>
    <script src="//code.jquery.com/ui/1.12.0/jquery-ui.js"></script>
    <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.6/d3.min.js'></script>
    <link rel="stylesheet" href="//code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css">
    <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' type='text/css'>
    <link rel="stylesheet" href="css/customCSS" type="text/css">
</head>

<body>

     <h1 style="color:#ffffff;">Europe - Unemploymentrate</h1>

    <button id="resetButton" class="btn">Reset</button>

    <div class="wrapper">
        <div id="svgMap"></div>
        <div class="infobox-wrapper">
            <div class="infobox" id="regionName">
                Europe
            </div>
            <div class="infobox" id="infobox-content">
                <p>Year: <span class="infoBox" id="currentYear">2005</span></p>
                <p>Unemployment: <span class="infoBox" id="unemploymentRate">100</span> %</p>
                <p>Inhabitants: <span class="infoBox">default</span></p>
                <p>Country: <span class="infoBox">default</span></p>
            </div>
            <div class="infobox" id="infobox-plot">
                <div id="svgPlot"></div>
            </div>
        </div>
    </div>

    <div class="wrapper">
        <div id="playButtonSlider" class="btn glyphicon glyphicon-play"></div>
        <div id="slider"></div>
    </div>

    <script src="javascript/EUmap.js"></script>

</body>

</html>

CSS

  body {
      background-color: #18121E;
      margin: 50px;
  }

  #svgMap {
      width: 65%;
      height: 100%;
      background-color: #233237;
      border: 2px solid grey;
      border-radius: 10px;
      float: left;
  }

  .infobox-wrapper {
    width: 35%;
    height: 100%;
    padding: 5px 0px 0px 15px;
    float: right;
  }

  .infobox {
    margin-top: 15px;
    padding: 5px 5px 5px 10px;
    border: 2px solid grey;
    border-radius: 10px;
    background-color:#233237;
    color:#ffffff;
  }

  /* RESET BUTTON */

  .btn {
    -webkit-border-radius: 15;
    -moz-border-radius: 15;
    border-radius: 15px;
    background: #7c100a;
    border: solid #ffffff 1px;
    text-decoration: none;

  }

  .btn:hover {
    background: #7d5f5e;
    text-decoration: none;
  }

  #resetButton {
    margin: 15px 0 0 10px;
    position: absolute;
    color: #ffffff;
    font-size:0.8em;
  }

  /* SLIDER + SLIDER PLAY BUTTON */
  #playButtonSlider {
    margin: 10px 0 0 15px;
    float: left;
    font-size:0.7em;
  }

  .glyphicon {
    color: #ffffff;
    padding: 3px 5px 3px 5px;
  }

  #slider {
      margin: 15px 0 0 25px;
      width: 50%;
      background-color: #233237;
      float: left;
  }

  #slider label {
      position: absolute;
      width: 20px;
      margin-top: 20px;
      margin-left: -20px;
      text-align: center;
      font-size: 0.8em;
      font-family: "Lucida Console", Monaco, monospace;
      color: grey;
  }

  .ui-slider-tick-mark {
      display: inline-block;
      width: 2px;
      background: grey;
      height: 10px;
      position: absolute;
  }

  .ui-widget-content .ui-corner-all {
      background: #7c100a;
      border-color: #ffffff;
  }

  .ui-widget-content .ui-corner-all:hover {
      background: #7d5f5e;
      border-color: #ffffff;
  }

  /* PLOT */

  .euPlot {
      fill: steelblue;
      stroke: white;
  }

  .yAxis path{
    stroke: white;
    color:white;
  }
  .yAxis line{
    stroke: white;
    color:white;
  }
  .yAxis text{
    fill: white;
  }

  .xAxis path{
    stroke: white;
  }
  .xAxis line{
    stroke: white;
  }
  .xAxis text{
    fill: white;
  }

Javascript

      // Slider
  $("#slider").slider({
          value: 2005,
          min: 2005,
          max: 2015,
          step: 0.01,
          create: function(event, ui) {
              setSliderTicks(event.target);
          },
      })
      .each(function() {
          var opt = $(this).data().uiSlider.options;
          var vals = opt.max - opt.min;
          for (var i = 0; i <= vals; i++) {
              var el = $('<label>' + (i + opt.min) + '</label>').css('left', (i / vals * 100) + '%');
              $("#slider").append(el);
          }
      });

  function setSliderTicks(el) {
      var $slider = $(el);
      var max = $slider.slider("option", "max");
      var min = $slider.slider("option", "min");
      var spacing = 100 / (max - min);

      $slider.find('.ui-slider-tick-mark').remove();
      for (var i = 1; i < max - min; i++) {
          $('<span class="ui-slider-tick-mark"></span>').css('left', (spacing * i) + '%').appendTo($slider);
      }
  }
  // Responsive Design
  var windowWidth = window.innerWidth;
  var windowHeight = window.innerHeight;

  var wrapperWidth = 0.9 * windowWidth;
  var wrapperHeight;

  if (windowWidth < windowHeight) {
    wrapperHeight = 0.5 * windowHeight;
  } else {
    wrapperHeight = 0.75 * windowHeight;
  }

  $(".wrapper").css("width", wrapperWidth);
  $(".wrapper").css("height", wrapperHeight);

  // Map EU
  var svgMapWidth =  (0.65 * wrapperWidth)  - 4;
  var svgMapHeight = wrapperHeight - 4;

  var myProjection = d3.geoMercator()
      .center([8.00, 57.00])
      .scale(500)
      .translate([svgMapWidth / 2, svgMapHeight / 2]);

  var geoPath = d3.geoPath().projection(myProjection);

  var color = d3.scaleThreshold()
      .domain([0, 2, 4, 6, 8, 10, 15, 20, 25, 50])
      .range(["#006837", "#1a9850", "#66bd63", "#a6d96a", "#d9ef8b", "#fee08b", "#fdae61", "#f46d43", "#d73027", "#a50026", "#808080"]);

  var svgEurope = d3.select("#svgMap").append("svg")
      .attr("width", svgMapWidth)
      .attr("height", svgMapHeight);

  var g = svgEurope.append("g");

  var zoom = d3.zoom()
      .scaleExtent([0.25, 8])
      .on("zoom", zoomed);

  svgEurope.call(zoom);

  d3.queue()
    .defer(d3.json, "data/sampleData.json")
    .await(initializeData);

  function initializeData(error, euRegionsJSON) {
    if (error) throw error;
    $('#slider').slider().bind('slidechange',e => renderMap(e,euRegionsJSON, sliderValue)); //same as $('#slider').slider().bind('slidechange',function e() {return renderMap(e,euRegionsJSON, sliderValue)});
    $('#slider').trigger('slidechange');
  };

  var sliderValue;

  $('#slider').slider().bind('slidechange', function(event, ui) {
      sliderValue = $('#slider').slider("value");
      sliderValue = Math.floor(sliderValue);
      $("#currentYear").text(sliderValue);
  });

  $("#playButtonSlider").on("click",playButtonSlider);
  function playButtonSlider() {
    function startTimer(counter) {
      if(counter <= 10) {
        setTimeout(function(){
          sliderValue = $("#slider").slider('value',2005+counter);
          counter += 0.01;
          startTimer(counter);
        },10);
      }
    }
    startTimer(0);
  }

  function renderMap(error, euRegionsJSON, sliderValue) {
    g.selectAll(".euMap").remove();
    g.selectAll("path")
        .data(euRegionsJSON.features)
        .enter().append("path")
        .attr("d", geoPath)
        .attr("class", "euMap")
        .attr("fill", function(d) {return color(d.properties[sliderValue]);})
        .style("stroke", "black")
        .style("stroke-width", "0.4px")
        .on("click",function(d) {
            highlight(this);
            setInfoBoxContent(d);
            createPlot(d);
        });
  }

  function highlight(mapClickedPath){
    d3.selectAll('path').style('fill',null);
    d3.select(mapClickedPath).style("fill", "#3333ff");
  }

  function setInfoBoxContent(mapClickedFeature) {
    $('#regionName').text(mapClickedFeature.properties.Region);
    $('#unemploymentRate').text(mapClickedFeature.properties[sliderValue]);
  }

  function zoomed() {
    g.style("stroke-width", 1.5 / d3.event.transform.k + "px");
    g.attr("transform", d3.event.transform);
  }

  $("#resetButton").on("click",reset);
  function reset(){
    svgEurope.transition().call(zoom.transform, d3.zoomIdentity);
    d3.selectAll('path').style('fill',null);
    $('#regionName').text("Europe");
    $('#unemploymentRate').text("100");
  }

  // Barplot
  var margin = {top: 10, right: 20, bottom: 35, left: 20};

  var svgPlotWidth = (0.33 * wrapperWidth) - margin.left - margin.right;
  var svgPlotHeight = 250 - margin.top - margin.bottom;

  var svgEuropeDiagramm = d3.select("#svgPlot").append("svg")
      .attr("width", svgPlotWidth + margin.left + margin.right)
      .attr("height", svgPlotHeight + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var x = d3.scaleBand()
            .rangeRound([0, svgPlotWidth])
            .padding(0.25);
  var y = d3.scaleLinear()
            .rangeRound([svgPlotHeight, 0]);

  function createPlot(mapClickedFeature) {

    var getFeatureEntries = d3.entries(mapClickedFeature.properties)
    var featureEntries = [];

    for (var i=0;i<=10;i++){
      if (getFeatureEntries[i].value == "NA") {
        getFeatureEntries[i].value = 0;
        featureEntries.push(getFeatureEntries[i])
      } else {
        featureEntries.push(getFeatureEntries[i])
      }
    }

    x.domain(featureEntries.map(function(d) { return d.key; }));
    y.domain([0, 50]);

    svgEuropeDiagramm.selectAll(".euPlot").remove();
    svgEuropeDiagramm.selectAll(".euPlot")
        .data(featureEntries)
        .enter().append("rect")
        .attr("class", "euPlot")
        .attr("x", function(d) { return x(d.key); })
        .attr("width", x.bandwidth())
        .attr("y", function(d) { return y(d.value); })
        .attr("height", function(d) { return svgPlotHeight - y(d.value); });

    svgEuropeDiagramm.selectAll(".xAxis").remove();
    svgEuropeDiagramm.append("g")
        .attr("transform", "translate(0," + svgPlotHeight + ")")
        .attr("class","xAxis")
        .call(d3.axisBottom(x))
      .selectAll("text")
        .attr("y", 0)
        .attr("x", 9)
        .attr("dy", ".35em")
        .attr("transform", "rotate(90)")
        .style("text-anchor", "start");

    svgEuropeDiagramm.selectAll(".yAxis").remove();
    svgEuropeDiagramm.append("g")
        .attr("class","yAxis")
        .call(d3.axisLeft(y));
  }
\$\endgroup\$
  • \$\begingroup\$ Is there a reason you hardcode the value 2015 4 times? \$\endgroup\$ – Mast Jul 29 '17 at 15:00
  • \$\begingroup\$ Hm, I don't see where I hardcoded the value 2015. Could you be more specific? What I see is that I don't have to give var slidervalue; an initial value (but which is 2005). \$\endgroup\$ – obrob Jul 29 '17 at 17:28
2
\$\begingroup\$

Before addressing your problem with the playButtonSlider function, I'd like to advise you: do not mix D3 and jQuery.

The problem with mixing D3 and jQuery is that not only this mix is most of the times (with rare exceptions) unnecessary, but also it can make things fail silently, which can be a nightmare to debug. For instance, if you select an element using jQuery ($) and try to set a on() D3 method on it, it will simply not work. Other examples of problems abound.

Back to your code.

My suggestion here is replacing your native setTimeout with the D3 timeout method. It's a better method when compared to setTimeout, and it will (hopefully) avoid this difference in the speed of the slider when dealing with a huge JSON.

According to D3 API:

d3-timer provides an efficient queue capable of managing thousands of concurrent animations, while guaranteeing consistent, synchronized timing with concurrent or staged animations. Internally, it uses requestAnimationFrame for fluid animation (if available), switching to setTimeout for delays longer than 24ms.

Also, I created a function to stop the slider when you click the "play" button again, which you don't have right now (you can turn the play button in a stop button — a square — when the slider is playing, which gives a better user experience).

This is my suggestion:

  var player;
  var playing = false;
  var counter = 0;

  d3.select("#playButtonSlider").on("click", function() {

      if (!playing) {
          playing = true;
          playButtonSlider()
      } else {
          player.stop()
          playing = false
      }

      function playButtonSlider() {
          if (counter > 10) {
              return
          }
          sliderValue = $("#slider").slider('value', 2005 + counter);
          counter += 1;
          player = d3.timeout(playButtonSlider, 1000)
      }

  });

Explanation:

First, using a boolean named playing, se verify if the slider is already moving or not. If not, we call playButtonSlider and toggle the boolean:

if (!playing) {
    playing = true;
    playButtonSlider()
}

Inside playButtonSlider comes the important part. This...

player = d3.timeout(playButtonSlider, 1000)

... calls the function again, using d3.timeout, every 1000ms, and increasing counter by 1 unit. Of course, you can tweak those values according to your needs.

It's important giving a name to the d3.timeout, so we can stop it when clicking the play button:

player.stop()
playing = false

Here is a Plunkr with those changes: https://plnkr.co/edit/aj45HA6TLshzNZe56gxu?p=preview

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.