📄 aspectratiobanker.as
字号:
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
* <code>OperatorList</code> <b>before</b> the <code>AxisLayout</code>
* operator, and set the <code>dataField</code> 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 <code>bankYAxis</code>
* property to <code>false</code>.
*/
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 <code>averageAbsoluteAngle</code>. */
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.
* <pre>
* 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
* </pre>
* @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<N; ++i) {
var slope:Number = Math.abs(a[i+1] - a[i]) / Ry;
if (slope > 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<N; ++i) {
x = c[i] * alpha;
f += Math.atan(x);
fprime += c[i] / (1 + x*x);
}
f /= N;
fprime /= N;
f -= Math.PI/4;
// apply the Newton-Raphson increment
alpha = alpha_p - f/fprime;
// finish iteration when update difference drops beneath tolerance
} while (Math.abs(alpha - alpha_p) > 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<a.length; ++i) {
var slope:Number = Math.abs(a[i] - a[i-1]);
if (slope/yRange > 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
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -