Architecting Brennan Center's Interactive Map Building Tool

Peter Sax's picture

Managing Director, Technology and Product

Blog

Architecting Brennan Center's Interactive Map Building Tool

Our own devilishly handsome Project Manager and #1 Giants fan Tony Guzmán already wrote about the U.S. State Map building tool we created for the Brennan Center for Justice. Now I'm going to outline a bit of the underlying architecture, built on Drupal 7.

The basic idea for this tool was for Brennan Center staff to use Drupal's CMS to create a map of US states, wherein each state may be assigned a color, whose significance is described in a status key below the map. Each state may be assigned a unique text description to appear when the state is clicked. This is a great live exampleVox thought so too.

The Map

The US Map jQuery plugin was a perfect match for the Brennan Center's needs. We didn't need the zooming or panning that comes with a regular Google Maps style map, and having Alaska and Hawaii inset was crucial. The plugin has all the features we needed and is well documented.

The Data Model

In purest form, what we needed were map pages, each possessing multiple legend, or key, options (a combination of a chosen color and a brief description), each of which is related to one or more states and a text description.

Of course there are a lot of ways to skin a cat in Drupal, and there are a huge number of modules which interrelate entities. I chose a combination of Node Reference and Field Collection. If I was doing this today I might choose Entity Reference instead of Node Reference, and Inline Entity Form instead of Field Collection, but that's another discussion, and the architecture in place has been problem-free.

There are two node types, a field collection type, and a taxonomy vocabulary:

Map Page node type's fields:
node reference to Map Legend Item

Color taxonomy vocabulary's fields:
text field for color hex value
text field for rollover/hover color hex value

Map Legend Item node type's fields:
corresponding node reference to Map Page
term reference for Color taxonomy term field collection for states
text field for legend description

States field collection's fields:
list field of U.S. States
longtext field for description to show when state is clicked

So, to summarize, each Map Page is related to many Map Legend Items, each of which contains a color, legend description, and a group of states to which this color should be applied.

Why This Data Model?

Good question!

Obviously there are many ways this could be set up. It's all about the workflow for the client. First of all, before any Map Pages are made, the Colors must be entered. Once the colors exist, a Map Page is created. Next each Map Legend Item is created. They're assigned to the new Map Page via node reference, the color is chosen, the description is added for the legend. Field Collection items are added for each state that will be this color. The Item is saved and a new one is created for each group of states.

This strikes a nice balance between minimizing the amount of nodes the client has to create, and and simplicity of the interface. If this were done in one node, the interface would have to accommodate 2 layers of nested one-to-many entities. But many other methods would require a lot more entities to be created per map, which when iterated repeatedly, becomes pretty cumbersome.

Ok So How Does It Get Into The Map?

Another wonderful question! It's like you're reading my mind!

The jQuery us-map plugin accepts as a parameter of its instantiation stateSpecificStyles and stateSpecificHoverStyles in this format:

$('#map').usmap({
  stateSpecificStyles: {
    'MD': {fill: 'yellow'},
    'VA': {fill: 'teal'}
  } 
});

So we just need to pass some data to a JavaScript array which can be used to form those parameters. This is done using drupal_add_js and is described a bit later.

As for the click-triggered description, the us-map plugin accepts event handlers in this form:

$('#map').on('usmap<event>', function(event, data) {
  // Output the abbreviation of the state name to the console
  console.log(data.name);
});

Notice that the state name (abbreviation actually) is available to the click handler. We just need a JavaScript object containing the descriptions, keyed by state abbreviation.

Getting the Data

Let's take a look at the PHP that creates these 2 JavaScript variables. The goal is to create PHP arrays and pass them to js Drupal.settings via drupal_add_js. This can be done in most hooks, so I chose hook_node_view since the node object, which has most of what I need, is readily available.

function bcj_state_map_node_view($node, $view_mode, $langcode){
  //do all this this only on map_page nodes
  if ($node->type == 'map_page'){
    //this states array is here for ease of retrieving the full state name from the abbreviation -- used toward the bottom
    $statenames = array('AL' => 'Alabama','AK' => 'Alaska','AZ' => 'Arizona','AR' => 'Arkansas','CA' => 'California','CO' => 'Colorado','CT' => 'Connecticut','DE' => 'Delaware','DC' => 'District Of Columbia','FL' => 'Florida','GA' => 'Georgia','HI' => 'Hawaii','ID' => 'Idaho','IL' => 'Illinois','IN' => 'Indiana','IA' => 'Iowa','KS' => 'Kansas','KY' => 'Kentucky','LA' => 'Louisiana','ME' => 'Maine','MD' => 'Maryland','MA' => 'Massachusetts','MI' => 'Michigan','MN' => 'Minnesota','MS' => 'Mississippi','MO' => 'Missouri','MT' => 'Montana','NE' => 'Nebraska','NV' => 'Nevada','NH' => 'New Hampshire','NJ' => 'New Jersey','NM' => 'New Mexico','NY' => 'New York','NC' => 'North Carolina','ND' => 'North Dakota','OH' => 'Ohio','OK' => 'Oklahoma','OR' => 'Oregon','PA' => 'Pennsylvania','RI' => 'Rhode Island','SC' => 'South Carolina','SD' => 'South Dakota','TN' => 'Tennessee','TX' => 'Texas','UT' => 'Utah','VT' => 'Vermont','VA' => 'Virginia','WA' => 'Washington','WV' => 'West Virginia','WI' => 'Wisconsin','WY' => 'Wyoming');
    //initialize the two arrays we will pass to js
    $states = $descriptions = array();
    if (!empty($node->field_map_legend_items)){
      //iterate the map_legend_items from the node reference field -- each of these is a group of states sharing color and hover color, and a legend description
      foreach($node->field_map_legend_items[LANGUAGE_NONE] as $k => $item){
        $tid = $item['node']->field_color[LANGUAGE_NONE][0]['tid'];
        //load the color term to fetch the 2 hex values
        $term = taxonomy_term_load($tid);
        $color = $term->field_hex_value[LANGUAGE_NONE][0]['safe_value'];
        $rollover_color = $term->field_rollover_hex_value[LANGUAGE_NONE][0]['safe_value'];
        if(!empty($item['node']->field_state_assignments[LANGUAGE_NONE])){
          //iterate the state field collection items attached to this map legend item
          foreach($item['node']->field_state_assignments[LANGUAGE_NONE] as $idx => $fc){
            //load the field collection item to fetch the state and description
            $fc_ent = field_collection_item_load($fc['value']);
            $state = $fc_ent->field_us_state[LANGUAGE_NONE][0]['value'];
            $description = $fc_ent->field_state_popup_text[LANGUAGE_NONE][0]['safe_value'];
            //stuff those arrays
            $states[] = array(
            'postal' => $state,
            'color' => $color,
            'rolloverColor' => $rollover_color);
            $descriptions[$state]['description'] = $description;
            $descriptions[$state]['fullname'] = $statenames[$state];
          }
        }
      }
    }
    //pass the variables to js
    drupal_add_js(array('states' => $states),'setting');
    drupal_add_js(array('descriptions' => $descriptions), 'setting');
  }
}

Finally, the JavaScript

First, the stateSpecificHoverStyles. Notice that Drupal.settings.states is a JavaScript array with numerical keys, for ease of iteration.

      var states = Drupal.settings.states;
      styles = {};
      hoverStyles = {};
      clickHandlers = {};
      for (index=0; index<states.length; index++){
        styles[states[index].postal] = {};
        styles[states[index].postal].fill = '#' + states[index].color;
        hoverStyles[states[index].postal] = {};
        hoverStyles[states[index].postal].fill = '#' + states[index].rolloverColor;  
      }
 
      $('#map').usmap({
        //there are a lot of other settings to play with, but we'll focus on what's coming out of Drupal.
        stateSpecificStyles: styles,
        stateSpecificHoverStyles: hoverStyles
 
      });

And the click handler. In this case Drupal.settings.descriptions is an object, so that the state data can be looked up by state abbreviation:

    var descriptions = Drupal.settings.descriptions;
    $('#map').bind('usmapclick', function(click, data) {
      $('#state-text').show();
      $('#state-text').html('<h3>' + descriptions[data.name].fullname + '</h3>' + descriptions[data.name].description);
    });

And that's the gist of it! It's really pretty amazing how easy it can be to get things done in Drupal.

I see you like to read printed material. You should check out Nicco's book The End of Big: http://endofbig.com