 		public const showValue:LinkableBoolean = registerLinkableChild(this, new LinkableBoolean(false));
 		public const fontSize:LinkableNumber = registerLinkableChild(this, new LinkableNumber(11));
 		public const fontColor:LinkableNumber = registerLinkableChild(this, new LinkableNumber(0x000000));
-		
-		protected const filteredDataX:FilteredColumn = newDisposableChild(this, FilteredColumn);
-		protected const filteredDataY:FilteredColumn = newDisposableChild(this, FilteredColumn);
-		
-		public function get dataX():DynamicColumn
-		{
-			return filteredDataX.internalDynamicColumn;
-		}
-		public function get dataY():DynamicColumn
-		{
-			return filteredDataY.internalDynamicColumn;
-		}
-		
-		public const sourceProjection:LinkableString = newSpatialProperty(LinkableString);
-		public const destinationProjection:LinkableString = newSpatialProperty(LinkableString);
+		private var valueStats:IColumnStatistics;
 		
 		private const bitmapText:BitmapText = new BitmapText();
+		protected const tempPoint:Point = new Point();
 		protected const tempSourcePoint:Point = new Point();
-		protected const tempDestinationPoint:Point = new Point();
-		protected const tempGeometryPoint:Point = new Point();
-		private var _projector:IProjector;
-		private var _xCoordCache:Dictionary;
-		private var _yCoordCache:Dictionary;
 		
 		/**
-		 * This gets called whenever any of the following change: dataX, dataY, sourceProjection, destinationProjection
-		 */		
-		private function updateProjector():void
+		 * @param geomKey
+		 * @param output
+		 * @return true on success 
+		 */
+		protected function getGeomCoords(geomKey:IQualifiedKey, output:Point):Boolean
 		{
-			_xCoordCache = new Dictionary(true);
-			_yCoordCache = new Dictionary(true);
-			
-			var sourceSRS:String = sourceProjection.value;
-			var destinationSRS:String = destinationProjection.value;
-			
-			// if sourceSRS is missing and both X and Y projections are the same, use that.
-			if (!sourceSRS)
+			var geoms:Array = geometryColumn.getValueFromKey(geomKey) as Array;
+			var geom:GeneralizedGeometry;
+			if (geoms && geoms.length)
+				geom = geoms[0] as GeneralizedGeometry;
+			if (geom)
 			{
-				var projX:String = dataX.getMetadata(ColumnMetadata.PROJECTION);
-				var projY:String = dataY.getMetadata(ColumnMetadata.PROJECTION);
-				if (projX == projY)
-					sourceSRS = projX;
+				geom.bounds.getCenterPoint(output);
+				return true;
 			}
-			
-			if (sourceSRS && destinationSRS)
-				_projector = WeaveAPI.ProjectionManager.getProjector(sourceSRS, destinationSRS);
 			else
-				_projector = null;
-		}
-		
-		protected function getCoordsFromRecordKey(recordKey:IQualifiedKey, output:Point):void
-		{
-			if (detectLinkableObjectChange(updateProjector, dataX, dataY, sourceProjection, destinationProjection))
-				updateProjector();
-			
-			if (_xCoordCache[recordKey] !== undefined)
 			{
-				output.x = _xCoordCache[recordKey];
-				output.y = _yCoordCache[recordKey];
-				return;
-			}
-			
-			for (var i:int = 0; i < 2; i++)
-			{
-				var result:Number = NaN;
-				var dataCol:IAttributeColumn = i == 0 ? dataX : dataY;
-				if (dataCol.getMetadata(ColumnMetadata.DATA_TYPE) == DataTypes.GEOMETRY)
-				{
-					var geoms:Array = dataCol.getValueFromKey(recordKey) as Array;
-					var geom:GeneralizedGeometry;
-					if (geoms && geoms.length)
-						geom = geoms[0] as GeneralizedGeometry;
-					if (geom)
-					{
-						if (i == 0)
-							result = geom.bounds.getXCenter();
-						else
-							result = geom.bounds.getYCenter();
-					}
-				}
-				else
-				{
-					result = dataCol.getValueFromKey(recordKey, Number);
-				}
-				
-				if (i == 0)
-				{
-					output.x = result;
-					_xCoordCache[recordKey] = result;
-				}
-				else
-				{
-					output.y = result;
-					_yCoordCache[recordKey] = result;
-				}
-			}
-			if (_projector)
-			{
-				_projector.reproject(output);
-				_xCoordCache[recordKey] = output.x;
-				_yCoordCache[recordKey] = output.y;
+				output.x = output.y = NaN;
+				return false;
 			}
 		}
 		
-		override public function getDataBoundsFromRecordKey(recordKey:IQualifiedKey, output:Array):void
+		public var includeDestPointsInDataBounds:Boolean = false; // for testing
+		
+		override public function getDataBoundsFromRecordKey(geomKey:IQualifiedKey, output:Array):void
 		{
-			getCoordsFromRecordKey(recordKey, tempGeometryPoint);
-			var bounds:IBounds2D = initBoundsArray(output);
-			bounds.setCenteredRectangle(tempGeometryPoint.x, tempGeometryPoint.y, 0, 0);
-			if (isNaN(tempGeometryPoint.x))
-				bounds.setXRange(-Infinity, Infinity);
-			if (isNaN(tempSourcePoint.y))
-				bounds.setYRange(-Infinity, Infinity);
+			getGeomCoords(geomKey, tempPoint);
+			
+			if (includeDestPointsInDataBounds)
+			{
+				var rowKeys:Array = EquationColumnLib.getAssociatedKeys(sourceKeyColumn, geomKey);
+				var n:int = rowKeys ? rowKeys.length : 0;
+				initBoundsArray(output, n + 1).includePoint(tempPoint);
+				for (var i:int = 0; i < n; i++)
+				{
+					getGeomCoords(destinationKeyColumn.getValueFromKey(rowKeys[i], IQualifiedKey), tempPoint);
+					(output[i + 1] as IBounds2D).includePoint(tempPoint);
+				}
+			}
+			else
+			{
+				initBoundsArray(output).includePoint(tempPoint);
+			}
 		}
 		
 		override public function drawPlotAsyncIteration(task:IPlotTask):Number
 		{
-			var i:int;
 			// Make sure all four column are populated
-			if (sourceKeyColumn.keys.length == 0 || destinationKeyColumn.keys.length == 0 || valueColumn.keys.length == 0 || geometryColumn.keys.length == 0) return 1;
+			if (task.iteration == 0 && (
+					sourceKeyColumn.keys.length == 0
+					|| destinationKeyColumn.keys.length == 0
+					|| valueColumn.keys.length == 0
+					|| geometryColumn.keys.length == 0))
+				return 1;
 			
 			// this template from AbstractPlotter will draw one record per iteration
 			if (task.iteration < task.recordKeys.length)
 			{
+				
 				//------------------------
 				// draw one record
 				var geoKey:IQualifiedKey = task.recordKeys[task.iteration] as IQualifiedKey;
 				tempShape.graphics.clear();
 
-				getCoordsFromRecordKey(geoKey, tempSourcePoint); // Get source coordinate
+				if (!getGeomCoords(geoKey, tempSourcePoint))
+					return task.iteration / task.recordKeys.length;
+				
 				task.dataBounds.projectPointTo(tempSourcePoint, task.screenBounds);
 
-				// Loop over the data table to find all the row keys with this source key value
-				var tempRowKeys:Array = new Array();
-				for (i = 0; i < sourceKeyColumn.keys.length; i++)
-				{
-					if (sourceKeyColumn.getValueFromKey(sourceKeyColumn.keys[i], IQualifiedKey) == geoKey)
-						tempRowKeys.push(sourceKeyColumn.keys[i]);
-				}
+				var rowKeys:Array = EquationColumnLib.getAssociatedKeys(sourceKeyColumn, geoKey);
+				var rowKey:IQualifiedKey;
+				var destKey:IQualifiedKey;
+				var value:Number;
 				
 				// Draw lines from source to destinations
-				var max:Number; // Absoulte max used for normalization
-				if (WeaveAPI.StatisticsCache.getColumnStatistics(valueColumn).getMax() > -WeaveAPI.StatisticsCache.getColumnStatistics(valueColumn).getMin())
-					max = WeaveAPI.StatisticsCache.getColumnStatistics(valueColumn).getMax();
-				else
-					max = WeaveAPI.StatisticsCache.getColumnStatistics(valueColumn).getMin();
+				var absMax:Number = Math.max(Math.abs(valueStats.getMin()), Math.abs(valueStats.getMax()));
 				
 				// Value normalization
-				for (i = 0; i < tempRowKeys.length; i++)
+				for each (rowKey in rowKeys)
 				{
-					if (valueColumn.getValueFromKey(tempRowKeys[i], Number) > 0)
-					{
-						tempShape.graphics.lineStyle(Math.round((valueColumn.getValueFromKey(tempRowKeys[i], Number) / max) * lineWidth.value), posLineColor.value);
-					}
-					else
-					{
-						tempShape.graphics.lineStyle(-Math.round((valueColumn.getValueFromKey(tempRowKeys[i], Number) / max) * lineWidth.value), negLineColor.value);
-					}
+					destKey = destinationKeyColumn.getValueFromKey(rowKey, IQualifiedKey);
+					value = valueColumn.getValueFromKey(rowKey, Number);
 					
+					tempShape.graphics.lineStyle(
+						Math.round(Math.abs(value / absMax) * lineWidth.value),
+						value < 0 ? negLineColor.value : posLineColor.value
+					);
 					tempShape.graphics.moveTo(tempSourcePoint.x, tempSourcePoint.y);
-					getCoordsFromRecordKey(destinationKeyColumn.getValueFromKey(tempRowKeys[i], IQualifiedKey), tempDestinationPoint); // Get destionation coordinate
-					task.dataBounds.projectPointTo(tempDestinationPoint, task.screenBounds);
-					tempShape.graphics.lineTo(tempDestinationPoint.x, tempDestinationPoint.y);
+					if (!getGeomCoords(destKey, tempPoint))
+						continue;
+					task.dataBounds.projectPointTo(tempPoint, task.screenBounds);
+					tempShape.graphics.lineTo(tempPoint.x, tempPoint.y);
 				}
 								
 				task.buffer.draw(tempShape);
 				
 				if (showValue.value)
 				{
-					for (i = 0; i < tempRowKeys.length; i++)
+					for each (rowKey in rowKeys)
 					{
-						getCoordsFromRecordKey(destinationKeyColumn.getValueFromKey(tempRowKeys[i], IQualifiedKey), tempDestinationPoint); // Get destionation coordinate
-						task.dataBounds.projectPointTo(tempDestinationPoint, task.screenBounds);
+						destKey = destinationKeyColumn.getValueFromKey(rowKey, IQualifiedKey);
+						if (!getGeomCoords(destKey, tempPoint))
+							continue;
+						task.dataBounds.projectPointTo(tempPoint, task.screenBounds);
 						
-						bitmapText.x = Math.round((tempSourcePoint.x + tempDestinationPoint.x) / 2);
-						bitmapText.y = Math.round((tempSourcePoint.y + tempDestinationPoint.y) / 2);
-						bitmapText.text = valueColumn.getValueFromKey(tempRowKeys[i], Number);
+						bitmapText.x = Math.round((tempSourcePoint.x + tempPoint.x) / 2);
+						bitmapText.y = Math.round((tempSourcePoint.y + tempPoint.y) / 2);
+						bitmapText.text = valueColumn.getValueFromKey(rowKey, String);
 						bitmapText.verticalAlign = BitmapText.VERTICAL_ALIGN_MIDDLE;
 						bitmapText.horizontalAlign = BitmapText.HORIZONTAL_ALIGN_CENTER;
 						

		public function GeometryRelationPlotter()
		{
			registerSpatialProperty(geometryColumn);
			
			// set up x,y columns to be derived from the geometry column
			linkSessionState(geometryColumn, dataX.requestLocalObject(ReprojectedGeometryColumn, true));
			linkSessionState(geometryColumn, dataY.requestLocalObject(ReprojectedGeometryColumn, true));
			setColumnKeySources([geometryColumn]);
		}
		// Need to set columns dataType in AdminConsole
		public const geometryColumn:ReprojectedGeometryColumn = newSpatialProperty(ReprojectedGeometryColumn);
		public const sourceKeyColumn:DynamicColumn = newSpatialProperty(DynamicColumn);
		public const destinationKeyColumn:DynamicColumn = newSpatialProperty(DynamicColumn);

/*
	Weave (Web-based Analysis and Visualization Environment)
	Copyright (C) 2008-2011 University of Massachusetts Lowell
	
	This file is a part of Weave.
	
	Weave is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License, Version 3,
	as published by the Free Software Foundation.
	
	Weave is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with Weave.  If not, see <http://www.gnu.org/licenses/>.
*/

package weave.visualization.plotters
{
	import flash.geom.Point;
	import flash.text.TextFormat;
	
	import weave.api.WeaveAPI;
	import weave.api.data.IColumnStatistics;
	import weave.api.data.IQualifiedKey;
	import weave.api.newLinkableChild;
	import weave.api.primitives.IBounds2D;
	import weave.api.registerLinkableChild;
