package com.iwobanas.charts
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.geom.Point;
import flash.utils.Timer;
import mx.charts.chartClasses.CartesianChart;
import mx.charts.chartClasses.IAxis;
import mx.charts.chartClasses.IAxisRenderer;
import mx.charts.chartClasses.Series;
import mx.events.FlexEvent;
/**
* Dispatched when the user scrolls or zooms the chart.
*
* @eventType flash.events.Event.CHANGE
*/
[Event(name="change", type="flash.events.Event")]
/**
* Dispatched specified time after user finished zooming or scrolling the chart.
* This event can be used to initiate retrieve data from server because
* it is not dispatched as frequently as <code>change</code> event.
*
* @eventType mx.events.FlexEvent.VALUE_COMMIT
*/
[Event(name="valueCommit", type="mx.events.FlexEvent")]
/**
* The ChartScroller class adds scrolling and zooming functionality to CartesianChart.
*
* <p>To scroll the chart user have to drag data series or axis renderer and move mouse.
* To zoom data (change scale) user should use mouse wheel over series or axis renderer.
* If <code>chartMouseEnabled</code> is set user can drag or use mouse wheel
* over any part of the chart.</p>
*
* <p>Both scrolling and zooming are done by modifying <code>minimum</code>
* and <code>maximum</code> properties of chart axes.
* Currently LinearAxis and DateTimeAxis are supported.</p>
*
*/
public class ChartScroller extends EventDispatcher
{
/**
* Constructor.
*/
public function ChartScroller()
{
super();
commitTimer.addEventListener(TimerEvent.TIMER_COMPLETE, commitTimerCompleteHandler);
addEventListener(Event.DEACTIVATE, deactivateHandler);
}
/**
* Chart which content should be scrolled/zoomed.
*/
public function get chart():CartesianChart
{
return _chart;
}
/**
* @private
*/
public function set chart(value:CartesianChart):void
{
if (_chart)
{
_chart.removeEventListener(MouseEvent.MOUSE_DOWN, chartMouseDownHandler);
_chart.removeEventListener(MouseEvent.MOUSE_WHEEL, chartMouseWheelHandler);
}
_chart = value;
if (_chart)
{
_chart.addEventListener(MouseEvent.MOUSE_DOWN, chartMouseDownHandler);
_chart.addEventListener(MouseEvent.MOUSE_WHEEL, chartMouseWheelHandler);
}
}
/**
* @private
* Storage variable for <code>chart<chart> property.
*/
private var _chart:CartesianChart;
[Bindable]
/**
* Specifies whether chart should be scrolled horizontally.
*/
public var horizontalScrollEnabled:Boolean = true;
[Bindable]
/**
* Specifies whether chart should be scrolled vertically.
*/
public var verticalScrollEnabled:Boolean = false;
[Bindable]
/**
* Specifies whether chart should be zoomed along horizontal axis when mouse wheel is supn.
*/
public var horizontalZoomEnabled:Boolean = true;
[Bindable]
/**
* Specifies whether chart should be zoomed along vertical axis when mouse wheel is supn.
*/
public var verticalZoomEnabled:Boolean = false;
[Bindable]
/**
* Speed of zooming the chart.
*
* If <code>zoomSpeed</code> is negative zooming in/out occurs when spinning mouse wheel in different direction.
*/
public var zoomSpeed:Number = 1;
[Bindable]
/**
* If <code>true</code> whole area of the char can be dragged to scroll the chart along default axes
* and spinning mouse wheel over any part of the char causes zooming along default axes.
* If <code>false</code> only data series and axes support scrolling/zooming.
*/
public var chartMouseEnabled:Boolean = true;
[Bindable]
[Inspectable(format="Time", type="Number", defaultValue="1000")]
/**
* Time after which valueCommit event is dispatched when users stops scrolling/zooming the chart.
* If no scrolling/zooming occurs within <code>valueCommitDeley</code>
* we assume that user finished his/her action and dispatch commitValue event.
*/
public function get valueCommitDeley():Number
{
return commitTimer.delay;
}
/**
* @private
*/
public function set valueCommitDeley(value:Number):void
{
commitTimer.delay = value;
}
/**
* Chart data series used to transform mouse position to data coordinates.
* This property is set by initialization methods when users start zooming/scrolling the chart.
*
* Although zooming/scrolling affects all series using modified axis
* one series is used for coordinates transformation.
*/
protected var series:Series;
/**
* Currently zoomed/scrolled horizontal axis.
*/
protected var hAxis:IAxis;
/**
* Currently zoomed/scrolled vertical axis.
*/
protected var vAxis:IAxis;
/**
* Point in data coordinates at which scrolling (drag) is started.
*/
protected var dragStartDataPoint:Point;
/**
* Indicate if user is dragging the chart.
*/
protected var isDragging:Boolean = false;
/**
* Timer responsible for deleying valueCommit event.
*/
protected var commitTimer:Timer = new Timer(1000, 1);
/**
* Initialize zooming/scrolling based on mouse event.
*
* This method assigns values to <code>series</code>, <code>hAxis</code>
* and <code>vAxis</code> based on passed mouse event.
*/
protected function initializeForMouseEvent(event:MouseEvent):void
{
if (event.target is Series)
{
initializeForSeries(event.target as Series);
}
else if (event.target is IAxisRenderer)
{
initializeForAxisRenderer(event.target as IAxisRenderer);
}
else if (chartMouseEnabled)
{
initializeForChartAxes();
}
else
{
series = null;
hAxis = null;
vAxis = null;
}
}
/**
* Initialize zooming/scrolling for a given series.
*
* This method assigns values to <code>series</code>, <code>hAxis</code>
* and <code>vAxis</code> based on passed series.
*/
protected function initializeForSeries(s:Series):void
{
hAxis = s.getAxis('h');
vAxis = s.getAxis('v');
series = s;
}
/**
* Initialize zooming/scrolling for a given axis renderer.
*
* This method assigns values to <code>series</code>, <code>hAxis</code>
* and <code>vAxis</code> based on passed axis renderer.
*/
protected function initializeForAxisRenderer(renderer:IAxisRenderer):void
{
if (renderer.horizontal)
{
vAxis = null;
hAxis = renderer.axis;
}
else
{
vAxis = renderer.axis;
hAxis = null;
}
series = getSeriesForAxes(hAxis, vAxis);
}
/**
* Initialize zooming/scrolling for a default axes of the chart.
*
* This method assigns values to <code>series</code>, <code>hAxis</code>
* and <code>vAxis</code> based on <code>horizontalAxis</code>
* and <code>verticalAxis</code> chart properties.
*/
protected function initializeForChartAxes():void
{
hAxis = chart.horizontalAxis;
vAxis = chart.verticalAxis;
series = getSeriesForAxes(hAxis, vAxis);
}
/**
* Indicate if ChartScroller is initialized i.e. has series and at least one axis set.
*/
protected function get isInitialized():Boolean
{
return (series && (hAxis || vAxis));
}
/**
* Get chart data series which is using specified horizontal and vertical axis.
* If passed axis is <code>null</code> it is ignored i.e. returned series can use any axis for this direction.
*
* @param hAxis horizontal axis
* @param vAxis vertical axis
* @return chart series using passed horizontal and vertical axes
*/
protected function getSeriesForAxes(hAxis:IAxis, vAxis:IAxis):Series
{
for each (var s:Object in chart.series)
{
if (s is Series )
{
if ((!hAxis || (hAxis == s.getAxis('h'))) && (!vAxis || (vAxis == s.getAxis('v'))))
{
return s as Series;
}
}
}
return null;
}
/**
* Convert point in global (stage) coordinates to data coordinates.
* @param point point in global (stage) coordinates
* @return point in data coordinates
*/
protected function globalToData(point:Point):Point
{
var dataPoint:Point;
var dataArray:Array;
if (series && point)
{
dataArray = series.localToData(series.globalToLocal(point));
dataPoint = new Point(dataArray[0], dataArray[1]);
}
return dataPoint;
}
/**
* Scroll chart content by specified horizontal and vertical distance.
* Scrolling is done by modifying axis minimum and maximum values.
* <p>Note that call to this function takes effect only if ChartScroller
* is initialized (i.e. <code>series</code> and at least one of <code>hAxis</code>
* and <code>vAxis</code> are set) and scrolling is enabled
* (horizontalScrollEnabled/verticalScrollEnabled properties are set).</p>
*
* @param dx distance to scroll horizontally in data coordinates
* @param dy distance to scroll vertically in data coordinates
*/
protected function scrollChart(dx:Number, dy:Number):void
{
var changed:Boolean = false;
if (hAxis && horizontalScrollEnabled)
{
NumericAxisHelper.setMin(hAxis, NumericAxisHelper.getMin(hAxis) + dx);
NumericAxisHelper.setMax(hAxis, NumericAxisHelper.getMax(hAxis) + dx);
changed = true;
}
if (vAxis && verticalScrollEnabled)
{
NumericAxisHelper.setMin(vAxis, NumericAxisHelper.getMin(vAxis) + dy);
NumericAxisHelper.setMax(vAxis, NumericAxisHelper.getMax(vAxis) + dy);
changed = true;
}
if (changed)
{
processChange();
}
}
/**
* Zoom chart content relative to a given point in data coordinates.
* <p>Note that call to this function takes effect only if ChartScroller
* is initialized (i.e. <code>series</code> and at least one of <code>hAxis</code>
* and <code>vAxis</code> are set) and zooming is enabled
* (horizontalZoomEnabled/verticalZoomEnabled properties are set).</p>
*
* @param ralativeTo point in data coordinates
* @param zoom indicate how much the content should be zoomed,
* if this value is greater than 1 content is zoomed in otherways content is zoomed out.
*/
protected function zoomChart(ralativeTo:Point, zoom:Number):void
{
var dMin:Number; var dMax:Number; var changed:Boolean = false;
if (hAxis && horizontalZoomEnabled)
{
dMin = ralativeTo.x - NumericAxisHelper.getMin(hAxis);
dMax = NumericAxisHelper.getMax(hAxis) - ralativeTo.x;
NumericAxisHelper.setMin(hAxis, ralativeTo.x - dMin/zoom);
NumericAxisHelper.setMax(hAxis, ralativeTo.x + dMax/zoom);
changed = true;
}
if (vAxis && verticalZoomEnabled)
{
dMin = ralativeTo.y - NumericAxisHelper.getMin(vAxis);
dMax = NumericAxisHelper.getMax(vAxis) - ralativeTo.y;
NumericAxisHelper.setMin(vAxis, ralativeTo.y - dMin/zoom);
NumericAxisHelper.setMax(vAxis, ralativeTo.y + dMax/zoom);
changed = true;
}
if (changed)
{
processChange();
}
}
/**
* Dispatch change event and restart commitTimer.
* This function should be called after modifying axis minimum/maximum.
*/
protected function processChange():void
{
dispatchEvent(new Event(Event.CHANGE));
commitTimer.reset();
commitTimer.start();
}
/**
* End chart scrolling using mouse by romoving mouse event listeners.
*/
protected function endDrag():void
{
chart.systemManager.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
chart.systemManager.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
isDragging = false;
}
/**
* Mouse down on the chart event hadler.
* Initialize scrolling using mouse.
*/
protected function chartMouseDownHandler(event:MouseEvent):void
{
initializeForMouseEvent(event);
if (isInitialized)
{
dragStartDataPoint = globalToData(new Point(event.stageX, event.stageY));
isDragging = true;
chart.systemManager.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
chart.systemManager.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
}
}
/**
* Mouse move event handler.
*
* Calculate distance in chart data coordinates between the current mouse position
* and drag start position and scroll (move) chart by this distance.
*
* This event listener is registered on mouse down and unregistered
* on mouse up or deactivate.
*/
protected function mouseMoveHandler(event:MouseEvent):void
{
var currDataPoint:Point = globalToData(new Point(event.stageX, event.stageY));
var dx:Number = dragStartDataPoint.x - currDataPoint.x;
var dy:Number = dragStartDataPoint.y - currDataPoint.y;
scrollChart(dx, dy);
}
/**
* Mouse up event handler.
* Stop scrolling the chart.
*/
protected function mouseUpHandler(event:MouseEvent):void
{
endDrag();
}
/**
* Deactivate event handler.
* Stop scrolling the chart.
*/
protected function deactivateHandler(event:Event):void
{
endDrag();
}
/**
* Mouse wheel over chart event handler.
* Initialize ChartScroller if needed and zoom chart depending on event.delta
*/
protected function chartMouseWheelHandler(event:MouseEvent):void
{
if (!isDragging) {
initializeForMouseEvent(event);
}
if (isInitialized)
{
var relativeTo:Point = globalToData(new Point(event.stageX, event.stageY));
var zoom:Number = 1 / (1 - (event.delta / 100 * zoomSpeed));
zoom = Math.max(zoom, 0.5);
zoom = Math.min(zoom, 2);
zoomChart(relativeTo, zoom);
}
}
/**
* Commit timer complete event handler.
* Dispatch <code>valueCommit</code> event.
*/
protected function commitTimerCompleteHandler(event:TimerEvent):void
{
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
}
}