4
\$\begingroup\$

I am creating an interactive quad(heat) map of USGS Quadrangles wherein each quad has 12 different attribute fields CSV attached to the JSON using papa.parse.

Currently, I am using two different radio buttons to select values that update an interactive web map. I plan to expand the values available to toggle (e.g., instead of Animals, I'm going to have more groups - Fish, Birds, Reptiles, etc. (i.e. each quad could have 40-50 attribute fields)). This increases the possible number of permutations (and I'll probably switch from radio buttons to some sort of dropdown selector). My code currently has 12 conditional statements. How can I simplify this code so that when I add more values and I'm not writing up another 30-40 if statements?

  (function () {
  L.mapbox.accessToken =
    'pk.eyJ1IjoiaWFuaG9ybiIsImEiOiJjamRocmdseWUxMjJ0MnlwNDdiMDd5ZXZzIn0.jBWD0r86k4pBSymNrCKfzw';
  var map = L.mapbox.map('map', 'mapbox.pirates', {
    zoomSnap: .1,
    center: [37.6438332, -85.6487252],
    zoom: 7,
    minZoom: 1,
    maxZoom: 20,
    fillOpacity: 1
    //maxBounds: L.latLngBounds([], [])
  });


  var attributeValue = "allRecords";
  // AJAX request for GeoJSON data
  $.getJSON('data/kyQuads.json', function (quads) {

    //AJAX Request for parsing of CSV data
    Papa.parse("data/eoUnion.csv", {
      download: true,
      header: true,
      complete: function (data) {
        // data is accessible to us here
       //console.log('data: ', data);

        processData(quads, data)
        console.log(quads, data)
      }
    });


  }); // end of $.getJSON()

  function processData(quads, data) {
    //console.log(quads);
    //console.log(data);

    // loop through all the quads
    for (var i = 0; i < quads.features.length; i++) {
      var props = quads.features[i].properties;


      for (var j = 0; j < data.data.length; j++) {
        //console.log(typeof data.object[j]);
        if (props.quad24name === data.data[j].QUAD_NAME) {
          // reassign the variables
          quads.features[i].properties = data.data[j]

          break;

        }
      }
    }

    drawMap(quads);

  }

  function drawMap(data) {
    // create Leaflet object with geometry data and add to map
    var dataLayer = L.geoJson(data, {
      style: function (feature) {
        return {
          color: 'blue',
          weight: 1,
          fillOpacity: 1,
          fillColor: '#1f78b4'
        };
      }
    }).addTo(map);
    //first set the zoom/center to the dataLayer's extent
    map.fitBounds(dataLayer.getBounds());

    // call the update function
    updateMap(dataLayer);
    addUi(dataLayer);
    addLegend();
  } // end function drawMap
  function updateMap(dataLayer) {

    // reference to get sample jquery radio button: 
    // http: //www.jquerybyexample.net/2012/02/get-radio-button-value-using-jquery.html
    //jquery to select input values
    var presenceValue = $('input[name=toggle]:checked').val()
    var taxonomyValue = $('input[name=toggle2]:checked').val()

    //console.log(taxonomyValue, presenceValue)
    if (presenceValue === "allRecords" && taxonomyValue === "allRecords") {
      attributeValue = "allRecords"
    }
    if (presenceValue === "allRecords" && taxonomyValue === "Animals") {
      attributeValue = "Animals"
    }
    if (presenceValue === "allRecords" && taxonomyValue === "Plants") {
      attributeValue = "Plants"
    }
    if (presenceValue === "allRecords" && taxonomyValue === "Communities") {
      attributeValue = "Communities"
    }
    if (presenceValue === "Extant" && taxonomyValue === "allRecords") {
      attributeValue = "Extant"
    }
    if (presenceValue === "Extant" && taxonomyValue === "Animals") {
      attributeValue = "eAnimals"
    }
    if (presenceValue === "Extant" && taxonomyValue === "Plants") {
      attributeValue = "ePlants"
    }
    if (presenceValue === "Extant" && taxonomyValue === "Communities") {
      attributeValue = "eCommunities"
    }
    if (presenceValue === "Historic/Extirpated" && taxonomyValue === "allRecords") {
      attributeValue = "Historic/Extirpated"
    }
    if (presenceValue === "Historic/Extirpated" && taxonomyValue === "Animals") {
      attributeValue = "hAnimals"
    }
    if (presenceValue === "Historic/Extirpated" && taxonomyValue === "Plants") {
      attributeValue = "hPlants"
    }
    if (presenceValue === "Historic/Extirpated" && taxonomyValue === "Communities") {
      attributeValue = "hCommunities"
    }

    //console.log(attributeValue)
    // get the class breaks for the current data attribute
    var breaks = getClassBreaks(dataLayer);
    updateLegend(breaks);
    // loop through each county layer to update the color and tooltip info
    dataLayer.eachLayer(function (layer) {
      var props = layer.feature.properties;
      // set the fill color of layer based on its normalized data value
      if (+props[attributeValue] > 0) {
        // set the fill color of layer based on its normalized data value
        layer.setStyle({
          fillColor: getColor(+props[attributeValue], breaks),
          opacity: 1,
          fillOpacity: 1
        });
      } else {
        layer.setStyle({
          opacity: 0,
          fillOpacity: 0
        })
      }

      // assemble string sequence of info for tooltip (end line break with + operator)
      var tooltipInfo = "QUAD: " + props["quad24name"] + "<br>" + "# of Records" + ": " +
        props[attributeValue] + " EOs";
      // bind a tooltip to layer with county-specific information
      layer.bindTooltip(tooltipInfo, {
        // sticky property so tooltip follows the mouse
        sticky: true,
        tooltipAnchor: [300, 200]
      });
    });
  }

  function getClassBreaks(dataLayer) {
    // create empty Array for storing values
    var values = [];
    // loop through all the quads
    dataLayer.eachLayer(function (layer) {
      var value = layer.feature.properties[attributeValue];
      // don't push zero values into the array
      if (value > 0) {
        values.push(+value); // push the value for each layer into the Array
      }
    });
    // determine similar clusters
    var clusters = ss.ckmeans(values, 5);
    // create an array of the lowest value within each cluster
    var breaks = clusters.map(function (cluster) {
      return [cluster[0], cluster.pop()];
    });

    //return array of arrays, e.g., [[0,5], [6, 10], etc]
    return breaks;
  }

  function getColor(d, breaks) {
    // function accepts a single  data attribute value
    // and uses a series of conditional statements to determine which
    // which color value to return to return to the function caller
    // https://carto.com/carto-colors/
    // #c4e6c3,#96d2a4,#6dbc90,#4da284,#36877a,#266b6e,#1d4f60
    if (d <= breaks[0][1]) {
      return '#c4e6c3';
    } else if (d <= breaks[1][1]) {
      return '#96d2a4';
    } else if (d <= breaks[2][1]) {
      return '#6dbc90';
    } else if (d <= breaks[3][1]) {
      return '#36877a'
    } else if (d <= breaks[4][1]) {
      return '#1d4f60'
    }
  }

  function addLegend(breaks) {
    // create a new Leaflet control object, and position it top left
    var legendControl = L.control({
      position: 'topleft'
    });
    // when the legend is added to the map
    legendControl.onAdd = function (map) {
      // select a div element with an id attribute of legend
      var legend = L.DomUtil.get('legend');
      // disable scroll and click/touch on map when on legend
      L.DomEvent.disableScrollPropagation(legend);
      L.DomEvent.disableClickPropagation(legend);
      // return the selection to the method
      return legend;
    };
    // add the empty legend div to the map
    legendControl.addTo(map);
  }

  function updateLegend(breaks) {
    // get the currently selected values 
    // var taxonomyValue = $("#taxonomy-selector option:selected").val()
    var taxonomyValue = $('input[name=toggle2]:checked').val()

    var presenceValue = $('input[name=toggle]:checked').val()

    //  if/else statement to change title

    if (taxonomyValue == "allRecords") {
      taxonomyValue = "EOs"
    }

    if (presenceValue == "allRecords") {
      presenceValue = "All"
    }

    //  console.log(taxonomyValue, presenceValue);

    // select the legend, add a title, begin an unordered list and assign to a variable
    var legend = $('#legend').html("<h5>" + presenceValue + " " + taxonomyValue + " </h5>");
    // loop through the Array of classification break values
    for (var i = 0; i <= breaks.length - 1; i++) {
      var color = getColor(breaks[i][0], breaks);
      legend.append('<span style="background:' + color + '"></span> ' +
        '<label>' + (breaks[i][0]) + ' &mdash; ' + (breaks[i][1]) +
        '</label>');
    }
  }

  function addUi(dataLayer) {
    // create the slider control
    var selectControl = L.control({
      position: 'topright'
    });
    // when control is added
    selectControl.onAdd = function (map) {
      // get the element with id attribute of ui-controls
      return L.DomUtil.get("ui-presence");
    }
    // add the control to the map
    selectControl.addTo(map);
    $('input[name="toggle"]').change(function () {
      // call updateMap function
      updateMap(dataLayer);
    });

    // create the slider control
    var selectControl = L.control({
      position: 'topright'
    });
    // when control is added
    selectControl.onAdd = function (map) {
      // get the element with id attribute of ui-controls
      return L.DomUtil.get("ui-controls");
    }
    // add the control to the map
    selectControl.addTo(map);
    $('input[name=toggle2]').change(function () {
      // call updateMap function
      updateMap(dataLayer);
    });

  }

})()

The value assigned to attributeValue refers to the attribute table attached to my JSON data.

enter image description here

As I toggle between the different radio buttons, the attributeValue changes to the corresponding value in the table.

\$\endgroup\$
  • \$\begingroup\$ @IanHorn which browsers are you aiming to support with this page? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Apr 7 '18 at 9:38
  • \$\begingroup\$ Any browser, but I've noticed that if I add to Sharepoint webpart, the javascript doesn't work in IE. \$\endgroup\$ – Ian Horn Apr 8 '18 at 16:03
1
\$\begingroup\$

Option 1

Offered only for future reference

// Don't repeat the same check
// Chain them if they are mutually exclusive
if (presenceValue === "allRecords") {
  if (taxonomyValue === "allRecords") {
    attributeValue = "allRecords"
  } else if (taxonomyValue === "Animals") {
    attributeValue = "Animals"
  } else if (taxonomyValue === "Plants") {
    attributeValue = "Plants"
  } else if (taxonomyValue === "Communities") {
    attributeValue = "Communities"
  }
} else if (presenceValue === "Extant") {
  if (taxonomyValue === "allRecords") {
    attributeValue = "Extant"
  } else if (taxonomyValue === "Animals") {
    attributeValue = "eAnimals"
  } else if (taxonomyValue === "Plants") {
    attributeValue = "ePlants"
  } else if (taxonomyValue === "Communities") {
    attributeValue = "eCommunities"
  }
} else if (presenceValue === "Historic/Extirpated") {
  if (taxonomyValue === "allRecords") {
    attributeValue = "Historic/Extirpated"
  } else if (taxonomyValue === "Animals") {
    attributeValue = "hAnimals"
  } else if (taxonomyValue === "Plants") {
    attributeValue = "hPlants"
  } else if (taxonomyValue === "Communities") {
    attributeValue = "hCommunities"
  }
}

Option 2

Use an alghoritm/formula to construct the name of attributeValue from the presenceValue & taxonomyValue.

var presenceValueOptions = {
  "allRecords": "all",
  "Extant": "ext",
  "Historic/Extirpated": "his"
}
var taxonomyValueOptions = {
  "allRecords": "all",
  "Animals": "Animals",
  "Plants": "Plants",
  "Communities": "Comunities"

}
var presenceValue = $("input[name=toggle]:checked").val()
var taxonomyValue = $("input[name=toggle2]:checked").val()

attributeValue = presenceValueOptions[presenceValue] + taxonomyValueOptions[taxonomyValue]

This outputs:

"allRecords" && "allRecords" -> attributeValue = "allall"
"allRecords" && "Animals" -> attributeValue = "allAnimals"
...
"Extant" && "Animals" -> attributeValue = "extAnimals"
"Extant" && "Plants" -> attributeValue = "extPlants"

You can add a taxonomy simply by adding a property to taxonomyValueOptions.

Answer to your comment regarding fields rename

Inside presenceValueOptions & taxonomyValueOptions:

propertyName must match your radio button value;

propertyValue can be anything you want, as long as, at the end, it matches your fields.

You could set presenceValueOptions.allRecords to "allP" and taxonomyValueOptions.allRecords to "allT" which is more expressive.

\$\endgroup\$
  • \$\begingroup\$ Thank you so much Valentin, that is very useful and makes sense. \$\endgroup\$ – Ian Horn Apr 3 '18 at 11:49
  • \$\begingroup\$ My CSV currently has all records for each distinct taxonomic value without a prefix. Should I change that in my source file so that attribute table has fields likeallPlants allAnimals allCommunities? I would just simply need to change my query view in Oracle - I'm going to anyway to expand to more taxonomic groups. \$\endgroup\$ – Ian Horn Apr 3 '18 at 13:11
  • \$\begingroup\$ You can set presenceValueOptions.allRecords = "". This will give you "allRecords" && "allRecords" -> attributeValue = "all","allRecords" && "Animals" -> attributeValue = "Animals" if you don't want to edit the fields. Editing the fields make's more sense to me as they communicate better what they refer to/contains. \$\endgroup\$ – Valentin Apr 3 '18 at 13:34
1
\$\begingroup\$

How can I simplify this code so that when I add more values and I'm not writing up another 30-40 if statements?

One could define a mapping (similar to the second option in Valentin's answer) that is similar to a 2-dimensional associative array. This can be achieved in JavaScript using objects- optionally using a Map object.

var mapping = { //format: presenceValue: { taxonomyValue: attributeValue }
    "allRecords": {
        "allRecords": "allRecords", 
        "Animals": "Animals",
        "Plants": "Plants",
        "Communities": "Communities"
    },
    "Extant": {
        "allRecords": "Extant", 
        "Animals": "eAnimals",
        "Plants": "ePlants",
        "Communities": "eCommunities"
    },
    "Historic/Extirpated": {
        "allRecords": "Historic/Extirpated", 
        "Animals": "hAnimals",
        "Plants": "hPlants",
        "Communities": "hCommunities"
    }
};

Then simply assign the value from that mapping using bracket notation of the mapping object:

var attributeValue = mapping[presenceValue][taxonomyValue];

If one wanted to ensure that attributeValue wouldn't get assign to undefined, you could check if the keys exist using Object.hasOwnProperty() :

if (mapping.hasOwnProperty(presenceValue) && mapping[presenceValue].hasOwnProperty(taxonomyValue)) {
    var attributeValue = mapping[presenceValue][taxonomyValue];
    //rest of code to utilize that value
}

or else the in operator

if (presenceValue in mapping && taxonomyValue in mapping[presenceValue]) {
    var attributeValue = mapping[presenceValue][taxonomyValue];
    //rest of code to utilize that value
}

That declaration of mapping could be stored outside of the updateMap() function, possibly in an external JSON file.

You could also consider using the Map instead of a plain object.

Other review points

Multiple jQuery libraries loaded

I noticed that the linked map page includes two versions of jQuery (i.e. 3.3.1 and 3.2.1). While those two likely won't have any conflicting code, including that library twice is pointless, and while most browser cache it, this leads to an additional 34.6 KB that the user must download.

You might consider whether you really need jQuery... check out youmightnotneedjquery.com/ for alternatives. The page already has 6 other scripts included...

I ran the page through the Yslow plugin - gave it a B rating:

1

Store DOM references in a variable (or constant)

The updateMap() function does a DOM lookup to get the value of the radio buttons on every call. Those DOM references should be stored in a variable (or a constant if the browsers that support const are sufficient). I know you mentioned you are considering switching to using select lists so I suggest simply storing references to those by id attribute value in a function when the DOM is ready (e.g. jQuery's .ready()).

Avoid extra anonymous function

I see a couple places where change handlers are set up like below:

$('input[name="toggle"]').change(function () {
  // call updateMap function
  updateMap(dataLayer);
});
//....
$('input[name=toggle2]').change(function () {
  // call updateMap function
  updateMap(dataLayer);
});

Those could be simplified using Function.bind() to create partially applied functions, which would allow for the removal of the extra wrapping function:

$('input[name="toggle"], input[name=toggle2]').change(updateMap.bind(null, dataLayer));

Notice that the CSS selectors were combined with a comma. A class name or other technique could be employed to select those elements.

Read more about removing extra anonymous functions in this article

\$\endgroup\$
  • \$\begingroup\$ Thank you for your comments and suggestions. I'll probably have more questions for you, but I'll get that second jquery library out of there and study this information you provided. Thanks for letting me know about Yslow. \$\endgroup\$ – Ian Horn Apr 3 '18 at 11:53

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.