package flare.analytics.optimization { import flare.animate.Transitioner; import flare.scale.Scale; import flare.util.Arrays; import flare.util.Property; import flare.vis.Visualization; import flare.vis.axis.CartesianAxes; import flare.vis.data.DataSprite; import flare.vis.operator.Operator; /** * Computes an optimized aspect ratio for drawing a line chart. * This operator will update the visualization's bounds to reflect the * optimized aspect ratio. Place this operator in an * OperatorList before the AxisLayout * operator, and set the dataField property to be the * same as the axis data field that should be banked. For example, in * a time series chart with time on the x-axis, the data field for this * operator should be the same as the data field used for the y-axis. * By default this class assumes that the data field is being laid out * on the y-axis. If this is not the case (e.g., you have a vertically * oriented line chart), be sure to set the bankYAxis * property to false. */ public class AspectRatioBanker extends Operator { private var _z:Property = null; /** The maximum width for the visualization bounds. */ public var maxWidth:Number = 500; /** The maximum height for the visualization bounds. */ public var maxHeight:Number = 500; /** Indicates if the data field is on the y-axis (default true). */ public var bankYAxis:Boolean = true; /** The banking function to use. This is a function that takes an * array of Numbers as input and returns an aspect ratio. It is * expected that this function will be one of the static functions of * this class. The default is averageAbsoluteAngle. */ public var banker:Function = averageAbsoluteAngle; /** The data field of the values to bank. */ public function get dataField():String { return _z.name; } public function set dataField(f:String):void { _z = Property.$(f); setup(); } /** * Creates a new AspectRatioBanker. * @param dataField the data field from which pull numeric values from * NodeSprites. These values are then used to determine the optimal * aspect ratio. */ public function AspectRatioBanker(dataField:String=null, bankYAxis:Boolean=true, maxWidth:Number=500, maxHeight:Number=500) { if (dataField) _z = Property.$(dataField); this.bankYAxis = bankYAxis; this.maxWidth = maxWidth; this.maxHeight = maxHeight; } // -------------------------------------------------------------------- /** @inheritDoc */ public override function operate(t:Transitioner=null):void { if (_z == null) return; // nothing to do // extract data var v:Array = []; visualization.data.nodes.visit(function(d:DataSprite):void { v.push(_z.getValue(d)); }); // compute the aspect ratio (= width/height) var ar:Number = banker(v); if (!bankYAxis) ar = 1/ar; ar = adjustToAxes(visualization, ar); // set visualization bounds and update axes visualization.setAspectRatio(ar, maxWidth, maxHeight); visualization.axes.update(t); } /** * Adjusts an aspect ratio for the "data rectangle" bounding the data * points to an new ratio that factors in the axis scale settings. * @param ar the desired aspect ratio of the data rectangle * @return the adjusted aspect ratio */ private static function adjustToAxes(vis:Visualization, ar:Number):Number { // get axis scales for each data field var axes:CartesianAxes = vis.xyAxes; var xsc:Scale = axes.xAxis.axisScale; var ysc:Scale = axes.yAxis.axisScale; // compute adjusted aspect ratio: this is the inverse aspect ratio // of the interpolated data rectangle in data space multipled by // the desired aspect ratio for the data rectangle in screen space var dy:Number, dx:Number; dy = ysc.interpolate(ysc.max) - ysc.interpolate(ysc.min); dx = xsc.interpolate(xsc.max) - xsc.interpolate(xsc.min); return ar * dy / dx; } // -------------------------------------------------------------------- /** * Bank the average absolute orientation to 45 degrees. * "Slopeless" lines are culled before the banking is computed. * Solved using Newton-Raphson iteration. *
	     * a     = aspect ratio (as height / width)
	     * ci    = normalized slope = N * abs(y_i+1 - y_i) / range(y)
	     * x     = a * ci
	     * f(a)  = sum(atan(x)) / N - pi/4
	     * f'(a) = sum(ci/(1 + x^2)) / N
	     * 
* @param a an array of data values to be banked. It is assumed that * values on the opposite axis are evenly spaced. * @return the optimized aspect ratio */ public static function averageAbsoluteAngle(a:Array):Number { var alpha:Number=0, alpha_p:Number, f:Number, fprime:Number; var x:Number, Ry:Number = Arrays.max(a) - Arrays.min(a); var N:int = a.length-1, iter:int = 0, i:int, j:int; // compute constants, perform culling var c:Array = []; for (i=0, j=0; i 1e-5) c.push(N * slope); } N = c.length; // Newton-Raphson iteration do { iter++; alpha_p = alpha; // compute function and function derivative f = fprime = 0; for (i=0; i 1e-5); return 1/alpha; } /** * Bank the median absolute slope to 45 degrees. * "Slopeless" lines are culled before the banking is computed. * @param a an array of data values to be banked. It is assumed that * values on the opposite axis are evenly spaced. * @return the optimized aspect ratio */ public static function medianAbsoluteSlope(a:Array):Number { var slopes:Array = [], i:int; var yRange:Number = Arrays.max(a) - Arrays.min(a); for (i=1; i 1e-5) { slopes.push(slope); } } slopes.sort(Array.NUMERIC); var median:Number = slopes[slopes.length>>1]; return (median*(a.length-1)) / yRange; } } // end of class AspectRatioBanker }