refactor
diff --git a/asterix-examples/src/main/resources/black-cherry/cherry.tpl b/asterix-examples/src/main/resources/black-cherry/cherry.tpl
index a3dc994..02016a5 100755
--- a/asterix-examples/src/main/resources/black-cherry/cherry.tpl
+++ b/asterix-examples/src/main/resources/black-cherry/cherry.tpl
@@ -9,16 +9,6 @@
     <!-- Bootstrap & jQuery Styles -->
     <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
     <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" type="text/css">
-
-    <!-- Bootstrap Javascript -->
-    <script src="http://maps.googleapis.com/maps/api/js?sensor=false&libraries=places" type="text/javascript"></script>
-        
-    <script src="http://code.jquery.com/jquery.min.js"></script>
-    <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
-    <script src="static/js/bootstrap.min.js"></script>
-    <script src="static/js/geostats.js"></script>
-    <script src="static/js/asterix-sdk-stable.js"></script>
-    <script src="static/js/cherry.js"></script>
     
     <style type="text/css">
         
@@ -26,17 +16,6 @@
             max-width: none;
         }
         
-        #legend-holder {
-            background: white;
-            padding: 10px;
-            margin: 10px;
-            text-align: center;
-        }
-        
-        #review-handles-dropdown {
-            padding: 0.5em;
-        }
-        
         .panel-primary {
           border-color: #273f93;
         }
@@ -135,9 +114,9 @@
                 
                 <!-- Submission Buttons -->
                 <li class="list-group-item">
-                  <button id="submit-button">Submit</button>
-                  <button id="clear-button">Clear</button>
-                  <button id="show-query-button" style="display: none;">Show Query</button><br/>
+                  <button class="btn btn-primary" type="button" id="submit-button">Submit</button>
+                  <button class="btn btn-primary" type="button" id="clear-button">Clear</button>
+                  <button class="btn btn-primary" type="button" id="show-query-button">Show Query</button><br/>
                   <input type="checkbox" value="Submit Asynchronously" name="async" id="asbox" />
                   Submit asynchronously?
                 </li>
@@ -185,9 +164,23 @@
         </div>
         
         <div class="col-md-7">
+          <!-- Map Container -->
           <div class="container well" id="right-col">
             <div id="map_canvas" style="max-width: 100%; height: auto;"></div>
           </div>
+          
+          <!-- Legend Container -->
+          <div id="rainbow-legend-container" class="container well">
+            <div id="legend-min" class="col-md-1"></div>
+            <div id="legend-gradient" class="col-md-6"></div>
+            <div id="legend-max" class="col-md-2"></div>
+            <div class="col-md-3">
+              <button id="windows-off-btn" class="btn btn-default">
+                Close Counts
+              </button>
+            </div>
+          </div>
+          
         </div>
       </div>
       
@@ -257,15 +250,50 @@
       <!-- About Row -->
       <div class="row" id="aboutr" style="display:none;">
         <div class="container">
-          <div id="welcome-message">
+        
+          <!-- Welcome Message -->
+          <div class="row">
+          
+            <!-- Welcome Message -->
             <p>Welcome to the top-level page of the mysteriously named Black Cherry Demo of AsterixDB.  The purpose of this demo is to illustrate how a "cool application" can be built using the JavaScript SDK of AsterixDB and to exercise all of the AsterixDB HTTP APIs.  If you are building an app of your own, reading the code for this app is a great way to get acquainted with what you'll need to know.</p>
 
-            <p>In this demo, which is based on spatial analysis of Tweets (it is 2013, afterall), you will see how to formulate aggregate queries and drill-down queries using the query door of the AsterixDB API.  You will see how to do this either synchronously or asynchronously (for larger queries whose results may take a while to cook).  You will also see how to create and drop datasets (to manage Tweetbooks, notebooks with user commentary on Tweets) and how to perform inserts and deletes (to add/remove Tweetbook entries). Enjoy!</p>
+            <p>In this demo, which is based on spatial analysis of Tweets (it is 2013, afterall), you will see how to formulate aggregate queries and drill-down queries using the query door of the AsterixDB API.  You will see how to do this either synchronously or asynchronously (for larger queries whose results may take a while to cook).  You will also see how to create and drop datasets (to manage Tweetbooks, notebooks with user commentary on Tweets) and how to perform inserts and deletes (to add/remove Tweetbook entries). Let's walk through the demo.</p>
+            
+            <hr/>
+            
+            <div style="margin-bottom: 2em; text-align: center;">
+                <img src="static/img/Tutorial1.png" style="max-width:100%;">
+            </div><hr/>
+            
+            <!-- Tutorial Part 2: Location Search -->
+            <div style="margin-bottom: 2em; text-align: center;">
+              <img src="static/img/Tutorial2.png" style="max-width:100%;">
+            </div><hr/>
+            
+            <!-- Tutorial Part 3: Search Results, Drilling Down, Saving Comments -->
+            <div style="margin-bottom: 2em; text-align: center;">
+              <!--<img src="static/img/Tutorial3.png" style="max-width:100%;">-->
+            </div><hr/>
+            
+            <!-- Tutorial Part 4: Review Mode -->
+            <div style="margin-bottom: 2em; text-align: center;">
+              <img src="static/img/Tutorial4.png" style="max-width:100%;">
+            </div>
           </div>
+
           
         </div>
       </div><!-- /About -->
 
-    </div><!-- container -->      
+    </div><!-- container --> 
+    
+    <!-- Bootstrap Javascript -->
+    <script src="http://maps.googleapis.com/maps/api/js?sensor=false&libraries=places" type="text/javascript"></script>    
+    <script src="http://code.jquery.com/jquery.min.js"></script>
+    <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
+    <script src="static/js/bootstrap.min.js"></script>
+    <script src="static/js/asterix-sdk-stable.js"></script>
+    <script src="static/js/rainbowvis.js"></script>
+    <script src="static/js/cherry.js"></script>     
   </body>
 </html>
diff --git a/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial1.png b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial1.png
new file mode 100644
index 0000000..c59b133
--- /dev/null
+++ b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial1.png
Binary files differ
diff --git a/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial2.png b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial2.png
new file mode 100644
index 0000000..33a7ed7
--- /dev/null
+++ b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial2.png
Binary files differ
diff --git a/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial4.png b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial4.png
new file mode 100644
index 0000000..72a1bc7
--- /dev/null
+++ b/asterix-examples/src/main/resources/black-cherry/static/img/Tutorial4.png
Binary files differ
diff --git a/asterix-examples/src/main/resources/black-cherry/static/img/mobile.png b/asterix-examples/src/main/resources/black-cherry/static/img/mobile.png
deleted file mode 100755
index 4d59e09..0000000
--- a/asterix-examples/src/main/resources/black-cherry/static/img/mobile.png
+++ /dev/null
Binary files differ
diff --git a/asterix-examples/src/main/resources/black-cherry/static/img/mobile_green.png b/asterix-examples/src/main/resources/black-cherry/static/img/mobile_green.png
deleted file mode 100755
index 31e367a..0000000
--- a/asterix-examples/src/main/resources/black-cherry/static/img/mobile_green.png
+++ /dev/null
Binary files differ
diff --git a/asterix-examples/src/main/resources/black-cherry/static/js/cherry.js b/asterix-examples/src/main/resources/black-cherry/static/js/cherry.js
index 2d42251..84fc8c9 100755
--- a/asterix-examples/src/main/resources/black-cherry/static/js/cherry.js
+++ b/asterix-examples/src/main/resources/black-cherry/static/js/cherry.js
@@ -52,6 +52,13 @@
     map_tweet_markers = [];
     map_info_windows = {};
     
+    // Legend Container
+    // Create a rainbow from a pretty color scheme. 
+    // http://www.colourlovers.com/palette/292482/Terra
+    rainbow = new Rainbow();
+    rainbow.setSpectrum("#E8DDCB", "#CDB380", "#036564", "#033649", "#031634");
+    buildLegend();
+    
     // UI Elements - Modals & perspective tabs
     $('#drilldown_modal').modal('hide');
     $('#explore-mode').click( onLaunchExploreMode );
@@ -523,7 +530,7 @@
     // TODO these are all included in coordinates already...
     var coordinates = [];
     var weights = [];
-    var al = 1;
+    var maxWeight = 0;
     
     // Parse resulting JSON objects. Here is an example record:
     // { "cell": { rectangle: [{ point: [22.5, 64.5]}, { point: [24.5, 66.5]}]}, "count": { int64: 5 }}
@@ -544,11 +551,11 @@
             "weight"    : record.count.int64
         }
         
-        weights.push(coordinate["weight"]);
+        maxWeight = Math.max(coordinate["weight"], maxWeight);
         coordinates.push(coordinate);
     });
     
-    triggerUIUpdate(coordinates, weights);
+    triggerUIUpdate(coordinates, maxWeight);
 }
 
 /**
@@ -556,66 +563,54 @@
 * @param    [Array]     mapPlotData, an array of coordinate and weight objects
 * @param    [Array]     plotWeights, a list of weights of the spatial cells - e.g., number of tweets
 */
-function triggerUIUpdate(mapPlotData, plotWeights) {
+function triggerUIUpdate(mapPlotData, maxWeight) {
     /** Clear anything currently on the map **/
     mapWidgetClearMap();
     
-    // Compute data point spread
-    var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
-    
+    // Initialize info windows.
     map_info_windows = {};
     
-    $.each(mapPlotData, function (m, val) {
-    
-        // Only map points in data range of top 4 natural breaks
-        if (mapPlotData[m].weight > dataBreakpoints[0]) {
-        
-            // Get color value of legend 
-            var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
-            var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
-            var point_opacity = 1.0;
-           
-            var point_center = new google.maps.LatLng(
-                (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0, 
-                (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
-            
-            // Create and plot marker
-            var map_circle_options = {
-                center: point_center,
-                anchorPoint: point_center,
-                radius: markerRadius,
-                map: map,
-                fillOpacity: point_opacity,
-                fillColor: mapColor,
-                clickable: true
-            };
-            var map_circle = new google.maps.Circle(map_circle_options);
-            map_circle.val = mapPlotData[m];
-            
-            map_info_windows[m] = new google.maps.InfoWindow({
-                content: mapPlotData[m].weight + " tweets",
-                position: point_center
-            });
+    $.each(mapPlotData, function (m) {
+   
+        var point_center = new google.maps.LatLng(
+            (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0, 
+            (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
 
-            // Clicking on a circle drills down map to that value, hovering over it displays a count
-            // of tweets at that location.
-            google.maps.event.addListener(map_circle, 'click', function (event) {
-                $.each(map_info_windows, function(i) {
-                    map_info_windows[i].close();
-                });
-                onMapPointDrillDown(map_circle.val);
-            });
+        var map_circle_options = {
+            center: point_center,
+            anchorPoint: point_center,
+            radius: mapWidgetComputeCircleRadius(mapPlotData[m], maxWeight),
+            map: map,
+            fillOpacity: 0.85,
+            fillColor: rainbow.colourAt(Math.ceil(100 * (mapPlotData[m].weight / maxWeight))),
+            clickable: true
+        };
+        var map_circle = new google.maps.Circle(map_circle_options);
+        map_circle.val = mapPlotData[m];
             
-            google.maps.event.addListener(map_circle, 'mouseover', function(event) {
-                if (!map_info_windows[m].getMap()) {
-                    map_info_windows[m].setPosition(map_circle.center);
-                    map_info_windows[m].open(map);
-                }
+        map_info_windows[m] = new google.maps.InfoWindow({
+            content: mapPlotData[m].weight + " tweets",
+            position: point_center
+        });
+
+        // Clicking on a circle drills down map to that value, hovering over it displays a count
+        // of tweets at that location.
+        google.maps.event.addListener(map_circle, 'click', function (event) {
+            $.each(map_info_windows, function(i) {
+                map_info_windows[i].close();
             });
+            onMapPointDrillDown(map_circle.val);
+        });
             
-            // Add this marker to global marker cells
-            map_cells.push(map_circle);
-        }    
+        google.maps.event.addListener(map_circle, 'mouseover', function(event) {
+            if (!map_info_windows[m].getMap()) {
+                map_info_windows[m].setPosition(map_circle.center);
+                map_info_windows[m].open(map);
+            }
+        });
+            
+        // Add this marker to global marker cells
+        map_cells.push(map_circle);   
     });
 }
 
@@ -1137,6 +1132,11 @@
     }
     map_cells = [];
     
+    $.each(map_info_windows, function(i) {
+        map_info_windows[i].close();
+    });
+    map_info_windows = {};
+    
     for (m in map_tweet_markers) {
         map_tweet_markers[m].setMap(null);
     }
@@ -1146,93 +1146,46 @@
 }
 
 /**
-* Uses jenks algorithm in geostats library to find natural breaks in numeric data
-* @param    {number Array} weights of points to plot
-* @returns  {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
-*/ 
-function mapWidgetLegendComputeNaturalBreaks(weights) {
-
-    if (weights.length < 10) {
-        return [0]; 
-    }
-
-    var plotDataWeights = new geostats(weights.sort());
-    return plotDataWeights.getJenks(6).slice(2,7);
-}
-
-/**
-* Computes values for map legend given a value and an array of jenks breakpoints
-* @param    {number}        weight of point to plot on map
-* @param    {number Array}  breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
-* @returns  {String}        an RGB value corresponding to a subset of data
+* buildLegend
+* 
+* no params
+*
+* Generates gradient, button action for legend bar
 */
-function mapWidgetLegendGetHeatValue(weight, breakpoints) {
-
-    // Determine into which range the weight falls
-    var weightColor = 0;
+function buildLegend() {
     
-    if (breakpoints.length == 1) {
-        weightColor = 2;
-    } else {
-        if (weight >= breakpoints[3]) {
-            weightColor = 3;
-        } else if (weight >= breakpoints[2]) {
-            weightColor = 2;
-        } else if (weight >= breakpoints[1]) {
-            weightColor = 1;
-        }
+    // Fill in legend area with colors
+    var gradientColor;
+    
+    for (i = 0; i=100; i++) {
+        $("#rainbow-legend-container").append("" + rainbow.colourAt(i));
     }
-
-    // Get default map color palette
-    var colorValues = mapWidgetGetColorPalette();
-    return colorValues[weightColor];
-}
-
-/**
-* Returns an array containing a 4-color palette, lightest to darkest
-* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
-* @returns  {Array}    [colors]
-*/
-function mapWidgetGetColorPalette() {
-    return [ 
-        "rgb(115,189,158)", 
-        "rgb(74,142,145)", 
-        "rgb(19,93,96)", 
-        "rgb(7,51,46)"
-    ];  
-}
+    
+    // Window clear button closes all info count windows
+    $("#windows-off-btn").on("click", function(e) {
+        $.each(map_info_windows, function(i) {
+            map_info_windows[i].close();
+        });
+    });
+}   
 
 /**
 * Computes radius for a given data point from a spatial cell
 * @param    {Object}    keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
 * @returns  {number}    radius between 2 points in metres
 */
-function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
-    
-    var weight = spatialCell.weight;
-    
-    if (breakpoints.length == 1) {
-        var weightColor = 0.25;
-    } else {
-        // Compute weight color
-        var weightColor = 0.25;
-        if (weight >= breakpoints[3]) {
-            weightColor = 1.0;
-        } else if (weight >= breakpoints[2]) {
-            weightColor = 0.75;
-        } else if (weight >= breakpoints[1]) {
-            weightColor = 0.5;
-        }
-    }
+function mapWidgetComputeCircleRadius(spatialCell, wLimit) {
 
     // Define Boundary Points
     var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
     var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
     var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
     
-    // TODO not actually a weight color :)
-    //return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
-    return 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
+    // Circle scale modifier = 
+    var scale = 500 + 500*(spatialCell.weight / wLimit);
+    
+    // Return proportionate value so that circles mostly line up.
+    return scale * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
 }
 
 /** External Utility Methods **/
diff --git a/asterix-examples/src/main/resources/black-cherry/static/js/rainbowvis.js b/asterix-examples/src/main/resources/black-cherry/static/js/rainbowvis.js
new file mode 100644
index 0000000..1f444dc
--- /dev/null
+++ b/asterix-examples/src/main/resources/black-cherry/static/js/rainbowvis.js
@@ -0,0 +1,178 @@
+/*
+RainbowVis-JS 
+Released under MIT License
+
+Source: https://github.com/anomal/RainbowVis-JS
+*/
+
+function Rainbow()
+{
+	var gradients = null;
+	var minNum = 0;
+	var maxNum = 100;
+	var colours = ['ff0000', 'ffff00', '00ff00', '0000ff']; 
+	setColours(colours);
+	
+	function setColours (spectrum) 
+	{
+		if (spectrum.length < 2) {
+			throw new Error('Rainbow must have two or more colours.');
+		} else {
+			var increment = (maxNum - minNum)/(spectrum.length - 1);
+			var firstGradient = new ColourGradient();
+			firstGradient.setGradient(spectrum[0], spectrum[1]);
+			firstGradient.setNumberRange(minNum, minNum + increment);
+			gradients = [ firstGradient ];
+			
+			for (var i = 1; i < spectrum.length - 1; i++) {
+				var colourGradient = new ColourGradient();
+				colourGradient.setGradient(spectrum[i], spectrum[i + 1]);
+				colourGradient.setNumberRange(minNum + increment * i, minNum + increment * (i + 1)); 
+				gradients[i] = colourGradient; 
+			}
+
+			colours = spectrum;
+			return this;
+		}
+	}
+
+	this.setColors = this.setColours;
+
+	this.setSpectrum = function () 
+	{
+		setColours(arguments);
+		return this;
+	}
+
+	this.setSpectrumByArray = function (array)
+	{
+		setColours(array);
+        return this;
+	}
+
+	this.colourAt = function (number)
+	{
+		if (isNaN(number)) {
+			throw new TypeError(number + ' is not a number');
+		} else if (gradients.length === 1) {
+			return gradients[0].colourAt(number);
+		} else {
+			var segment = (maxNum - minNum)/(gradients.length);
+			var index = Math.min(Math.floor((Math.max(number, minNum) - minNum)/segment), gradients.length - 1);
+			return gradients[index].colourAt(number);
+		}
+	}
+
+	this.colorAt = this.colourAt;
+
+	this.setNumberRange = function (minNumber, maxNumber)
+	{
+		if (maxNumber > minNumber) {
+			minNum = minNumber;
+			maxNum = maxNumber;
+			setColours(colours);
+		} else {
+			throw new RangeError('maxNumber (' + maxNumber + ') is not greater than minNumber (' + minNumber + ')');
+		}
+		return this;
+	}
+}
+
+function ColourGradient() 
+{
+	var startColour = 'ff0000';
+	var endColour = '0000ff';
+	var minNum = 0;
+	var maxNum = 100;
+
+	this.setGradient = function (colourStart, colourEnd)
+	{
+		startColour = getHexColour(colourStart);
+		endColour = getHexColour(colourEnd);
+	}
+
+	this.setNumberRange = function (minNumber, maxNumber)
+	{
+		if (maxNumber > minNumber) {
+			minNum = minNumber;
+			maxNum = maxNumber;
+		} else {
+			throw new RangeError('maxNumber (' + maxNumber + ') is not greater than minNumber (' + minNumber + ')');
+		}
+	}
+
+	this.colourAt = function (number)
+	{
+		return calcHex(number, startColour.substring(0,2), endColour.substring(0,2)) 
+			+ calcHex(number, startColour.substring(2,4), endColour.substring(2,4)) 
+			+ calcHex(number, startColour.substring(4,6), endColour.substring(4,6));
+	}
+	
+	function calcHex(number, channelStart_Base16, channelEnd_Base16)
+	{
+		var num = number;
+		if (num < minNum) {
+			num = minNum;
+		}
+		if (num > maxNum) {
+			num = maxNum;
+		} 
+		var numRange = maxNum - minNum;
+		var cStart_Base10 = parseInt(channelStart_Base16, 16);
+		var cEnd_Base10 = parseInt(channelEnd_Base16, 16); 
+		var cPerUnit = (cEnd_Base10 - cStart_Base10)/numRange;
+		var c_Base10 = Math.round(cPerUnit * (num - minNum) + cStart_Base10);
+		return formatHex(c_Base10.toString(16));
+	}
+
+	formatHex = function (hex) 
+	{
+		if (hex.length === 1) {
+			return '0' + hex;
+		} else {
+			return hex;
+		}
+	} 
+	
+	function isHexColour(string)
+	{
+		var regex = /^#?[0-9a-fA-F]{6}$/i;
+		return regex.test(string);
+	}
+
+	function getHexColour(string)
+	{
+		if (isHexColour(string)) {
+			return string.substring(string.length - 6, string.length);
+		} else {
+			var colourNames =
+			[
+				['red', 'ff0000'],
+				['lime', '00ff00'],
+				['blue', '0000ff'],
+				['yellow', 'ffff00'],
+				['orange', 'ff8000'],
+				['aqua', '00ffff'],
+				['fuchsia', 'ff00ff'],
+				['white', 'ffffff'],
+				['black', '000000'],
+				['gray', '808080'],
+				['grey', '808080'],
+				['silver', 'c0c0c0'],
+				['maroon', '800000'],
+				['olive', '808000'],
+				['green', '008000'],
+				['teal', '008080'],
+				['navy', '000080'],
+				['purple', '800080']
+			];
+			for (var i = 0; i < colourNames.length; i++) {
+				if (string.toLowerCase() === colourNames[i][0]) {
+					return colourNames[i][1];
+				}
+			}
+			throw new Error(string + ' is not a valid colour.');
+		}
+	}
+}
+