Drill-Down Using The Ignite UI Map

Jordan Tsankov / Wednesday, November 28, 2012

Infragistics Ignite UI Map Drill-Down

This blog post will bring the last addition to my series on the Ignite UI Map – this time we’ll look into drill-down functionality.The feature is not something that comes out-of-the-box for the control , so I will present an alternative solution to achieving the same effect.

Drill-down is when a selection of a series item shows up additional series contained within that item , each one bearing distinct data related to it. In this example , we will be using a map of the United States , showing each state’s counties upon selection.

 

Let’s get on with it !

 

 

 

 

 

 

Implementation

First , as usual , you will need to reference the required JavaScript libraries – we’ll use the CDN-hosted versions of jQuery and Modernizr:

   1: <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js" type="text/javascript"></script>
   2: <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.17/jquery-ui.js" type="text/javascript"></script>
   3: <script src="http://www.modernizr.com/downloads/modernizr-latest.js" type="text/javascript"></script>

Of course , we also need to add the Ignite UI /js and /css folders to the project as well , and then add a reference to the Infragistics Loader.

Now , let’s define some bushes and outlines that we will use throughout the process – we’ll need a pair for when the counties series is invisible , a pair for when any of the series is visible and finally a pair for drawing the non-selected states ( when you select a state and view its counties , all the other states are visible yet non-selected )

   1: var outline_visible = "rgba(123,166,180, 1)";
   2: var brush_visible = "rgba(173,216,230, 0.5)";
   3:  
   4: var outline_nonselected = "rgba(100, 100, 200, 0.3)";
   5: var brush_nonselected = "rgba(200, 200, 200, 0.3)";
   6:  
   7: var outline_invisible = "rgba(255,255,255,0)";
   8: var brush_invisible = "rgba(255,255,255,0)";

Next , we will create a styleSelector object that will be used for the styling of the counties.  Notice how we use the bushes in direct conjunction with each shape style’s visibility.

   1: function CountiesByStateSelector() {            
   2:     var _selectedStateFIPS = "";
   3:     this.stateFIPS = function (state) {
   4:         if (arguments.length == 0) {
   5:             return _selectedStateFIPS;
   6:         }
   7:         else {
   8:             _selectedStateFIPS = state;
   9:         }
  10:     };
  11:  
  12:     this.selectStyle = function (shapeData, shapeProperties) {
  13:         // STATE_FIPS is the property in the .dbf files that holds the ID for each county/state
  14:         var shapeState = shapeData.fields.item("STATE_FIPS");
  15:         
  16:         if (shapeState === _selectedStateFIPS || _selectedStateFIPS === "") {
  17:             shapeProperties.visibility($.ig.Visibility.prototype.visible);
  18:             return this.visibleStyle;
  19:         }
  20:         else {
  21:             shapeProperties.visibility($.ig.Visibility.prototype.collapsed);
  22:             return this.invisibleStyle;
  23:         }
  24:     };
  25: }
  26:  
  27: CountiesByStateSelector.prototype.invisibleStyle = {
  28:     fill: brush_invisible,
  29:     stroke: outline_invisible
  30: };
  31:  
  32: CountiesByStateSelector.prototype.visibleStyle = {
  33:     fill: brush_visible,
  34:     stroke: outline_visible
  35: };

And finally we’re ready to instantiate the map itself , we do it in a standard way – no beating around the bush with this one , it’s straightforward and you’re probably used to it already. Bear in mind you will need to .shp and .dbf files – one for the state series and one for the county series. Here’s the code snippet:

   1: $.ig.loader({
   2:     scriptPath: "./js/",
   3:     cssPath: "./css/"
   4: });
   5:  
   6: var shapeDataStates;
   7: var shapeCounties;
   8: $.ig.loader("igMap", function () {
   9:     shapeDataStates = new $.ig.ShapeDataSource({
  10:         shapefileSource: 'usa_states.shp',
  11:         databaseSource: 'usa_states.dbf'
  12:     });
  13:  
  14:     shapeCounties = new $.ig.ShapeDataSource({
  15:         shapefileSource: 'usa_counties.shp',
  16:         databaseSource: 'usa_counties.dbf'
  17:     });
  18:     shapeDataStates.dataBind();
  19:     shapeCounties.dataBind();
  20:  
  21:     $("#map").igMap({
  22:         width: "500px",
  23:         horizontalZoomable: true,
  24:         verticalZoomable: true,
  25:         overviewPlusDetailPaneVisibility: "visible",
  26:         series: [{
  27:             type: 'geographicShape',
  28:             name: 'usaStates',
  29:             shapeDataSource: shapeDataStates,
  30:             markerType: 'none',
  31:             outline: outline_visible,
  32:             brush: brush_visible
  33:         }, {
  34:             type: 'geographicShape',
  35:             name: 'usaCounties',
  36:             shapeDataSource: shapeCounties,
  37:             markerType: 'none',
  38:             outline: outline_invisible,
  39:             brush: brush_invisible
  40:         }],
  41:         seriesMouseLeftButtonUp: countyInfo,
  42:         windowResponse: "deferred",
  43:         windowRect: {
  44:             left: 0.26,
  45:             top: 0.33,
  46:             width: 0.1,
  47:             height: 0.12
  48:         }
  49:     });
  50: });

Now , some of you might have noticed that we’ve also defined an event handler for one of the series events – seriesMouseLeftButtonUp. This event is what will trigger the drill-down behavior , displaying the corresponding counties for a selected state. Here’s the code for the handler:

   1: function countyInfo(evt, ui) {
   2:     clearInfoPanel();
   3:     if (!first_run && ui.item.fields.item("COUNTY") != undefined) {
   4:         $("#info").append("<p class='infoMap'>State: " + ui.item.fields.item("STATE") + "</p>");
   5:         $("#info").append("<p class='infoMap'>County: " + ui.item.fields.item("COUNTY") + "</p>");
   6:         $("#info").append("<p class='infoMap'>Square Miles: " + ui.item.fields.item("SQUARE_MIL") + "</p>");
   7:     }
   8:     if (selectedCountiesSelector.stateFIPS() != ui.item.fields.item("STATE_FIPS")) {
   9:         //  Update the map only if another state is selected
  10:         selectedCountiesSelector.stateFIPS(ui.item.fields.item("STATE_FIPS"));
  11:         first_run = false;
  12:         
  13:         $("#info").append("<p class='infoMap'>State: " + ui.item.fields.item("STATE") + "</p>");
  14:         var stateShapes = getStateShapes(selectedCountiesSelector.stateFIPS());
  15:         var stateBounds = findShapeArrayBounds(stateShapes);
  16:         var mapWindow = calculateMapWindow(stateBounds, 1);
  17:  
  18:         $("#map").igMap("option", "windowRect", mapWindow);
  19:  
  20:         $("#map").igMap("option", "series", [{
  21:             name: "usaStates",
  22:             outline: outline_nonselected,
  23:             brush: brush_nonselected
  24:         }]);
  25:  
  26:         //  Initiate repainting of the counties series
  27:         $("#map").igMap("option", "series", [{
  28:             name: "usaCounties",
  29:             shapeStyleSelector: selectedCountiesSelector
  30:         }]);
  31:     }
  32: }

What happens here is that , first , we update a small information frame with data from the currently selected item.  The crucial bits are lines 8 & 10 , and then from 14 to 30.

On lines 8 & 10 , we check if the newly selected state is different than the last one and if it is , we save its ID ( in this case called STATE_FIPS ).

Then , on line 14 we use this ID to look up all the shapes for the selected state. We have a function written just for this case.

On line 15 we use these shapes to get the bounds of all the shapes contained within the state.

And on line 16 we reposition the view rectangle ( the map window ) to zoom in on the state that we have selected.

Here are the functions used:

   1: function getStateShapes(stateFIPS) {
   2:     //  Finds all shapes in the current data source that have
   3:     //  a specific ID (the field is called STATE_FIPS in this case)
   4:     var shapeEnumerator = shapeDataStates.converter().getEnumerator();
   5:     var shapesArray = [];
   6:     while (shapeEnumerator.moveNext()) {
   7:         var currentItem = shapeEnumerator.current();
   8:         if (currentItem.fields.item("STATE_FIPS") === stateFIPS) {
   9:             shapesArray.push(currentItem);
  10:         }
  11:     }
  12:     return shapesArray;
  13: }
  14:  
  15: function findShapeArrayBounds(shapeArray) {
  16:     //  Store to improve performance and readability
  17:     var sCount = shapeArray.length;
  18:  
  19:     if (sCount > 0) {
  20:         var left, top, right, bottom;
  21:  
  22:         //  Enumerate shapes
  23:         for (var s = 0; s < sCount; s++) {
  24:             var currentShapeBounds = findShapeBounds(shapeArray[s]);
  25:  
  26:             if (currentShapeBounds.left < left || !left) left = currentShapeBounds.left;
  27:             if (currentShapeBounds.right > right || !right) right = currentShapeBounds.right;
  28:  
  29:             if (currentShapeBounds.top > top || !top) top = currentShapeBounds.top;
  30:             if (currentShapeBounds.bottom < bottom || !bottom) bottom = currentShapeBounds.bottom;
  31:         }
  32:  
  33:         return {
  34:             left: left,
  35:             right: right,
  36:             top: top,
  37:             bottom: bottom
  38:         };
  39:     }
  40: }
  41:  
  42: function findShapeBounds(shape) {
  43:     var left, top, right, bottom;
  44:     var points = shape.points.item(0);
  45:     var pCount = points.count();
  46:     //  Enumerate shape points
  47:     if (pCount > 0) {
  48:         //  Find bounds of the state
  49:         for (var i = 0; i < pCount; i++) {
  50:             currentPoint = points.item(i);
  51:  
  52:             if (currentPoint.__x < left || !left) left = currentPoint.__x;
  53:             if (currentPoint.__x > right || !right) right = currentPoint.__x;
  54:  
  55:             if (currentPoint.__y > top || !top) top = currentPoint.__y;
  56:             if (currentPoint.__y < bottom || !bottom) bottom = currentPoint.__y;
  57:         }
  58:  
  59:         return {
  60:             left: left,
  61:             right: right,
  62:             top: top,
  63:             bottom: bottom
  64:         };
  65:     }
  66: }
  67:  
  68: function calculateMapWindow(minViewWindow, zoomRatio) {
  69:     if (!zoomRatio) {
  70:         zoomRatio = 1;
  71:     }
  72:     //  Calculate central point and required radius
  73:     var width = minViewWindow.right - minViewWindow.left;
  74:     var height = minViewWindow.top - minViewWindow.bottom;
  75:     var centered = {
  76:         longitude: minViewWindow.right - width / 2,
  77:         latitude: minViewWindow.top - height / 2,
  78:         radius: (width > height) ? width / 2 * zoomRatio : height / 2 * zoomRatio
  79:     };
  80:     //  Calculate map window in relative units
  81:     var zoomRect = $("#map").igMap("getZoomFromGeographic", geographicFromCentered(centered));
  82:     return zoomRect;
  83: }
  84:  
  85: //  Calculates the geographic coordinates of a square around a central point and radius
  86: function geographicFromCentered(centered) {
  87:     var geographic =
  88:     {
  89:         left: centered.longitude - centered.radius,
  90:         top: centered.latitude - centered.radius,
  91:         width: centered.radius * 2,
  92:         height: centered.radius * 2
  93:     };
  94:     return geographic;
  95: }

And of course , you will need to have a DOM element to render the map into , let’s just use a div. While we’re at it , let’s also make one for the info panel.

   1: <div id="map"></div>
   2: <div id="info" style="border: 1px solid black; width: 200px; height: 200px;">Information<br /></div>

 

And that’s it , you’ve got yourself a drill-down map of the United States !

Download this sample by clicking on this link and play around it it. You’ll see all the code in action.

Additionally , you can view more samples on this page.