refactor
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.');
+		}
+	}
+}
+