//-------------------------------------------------------------
//
// Copyright © Microsoft Corporation. All Rights Reserved.
//
//-------------------------------------------------------------
// @owner=alexgor, deliant
//=================================================================
// File: ChartAreaCursor.cs
//
// Namespace: System.Web.UI.WebControls[Windows.Forms].Charting
//
// Classes: Cursor, CursorEventArgs
//
// Purpose: A cursor is a horizontal or vertical line that
// defines a position along an axis. A range selection
// is a range along an axis that is defined by a beginning
// and end position, and is displayed using a semi-transparent
// color.
//
// Both cursors and range selections are implemented by the
// Cursor class, which is exposed as the CursorX and CursorY
// properties of the ChartArea object. The CursorX object is
// for the X axis of a chart area, and the CursorY object is
// for the Y axis. The AxisType property of these objects
// determines if the associated axis is primary or secondary.
//
// Cursors and range selections can be set via end-user
// interaction and programmatically.
//
// NOTE: ASP.NET version of the chart uses this class only
// for appearance and position properties. Drawing of the
// selection and cursor is implemented through client side
// java script.
//
// Reviewed: AG - August 8, 2002
// AG - March 16, 2007
//
//===================================================================
#if WINFORMS_CONTROL
#region Used Namespaces
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.ComponentModel;
using System.Collections;
using System.Windows.Forms.DataVisualization.Charting;
using System.Windows.Forms.DataVisualization.Charting.Data;
using System.Windows.Forms.DataVisualization.Charting.ChartTypes;
using System.Windows.Forms.DataVisualization.Charting.Utilities;
using System.Windows.Forms.DataVisualization.Charting.Borders3D;
using System.Drawing.Design;
using System.Collections.Generic;
#endregion
namespace System.Windows.Forms.DataVisualization.Charting
{
///
/// The Cursor class is responsible for chart axes cursor and selection
/// functionality. It contains properties which define visual appearance,
/// position and behavior settings. It also contains methods for
/// drawing cursor and selection in the plotting area.
///
[
DefaultProperty("Enabled"),
SRDescription("DescriptionAttributeCursor_Cursor"),
]
public class Cursor : IDisposable
{
#region Cursor constructors and initialization
///
/// Public constructor
///
public Cursor()
{
}
///
/// Initialize cursor class.
///
/// Chart area the cursor belongs to.
/// Indicates which axes should be used X or Y.
internal void Initialize(ChartArea chartArea, AxisName attachedToXAxis)
{
// Set chart are reference
this._chartArea = chartArea;
// Attach cursor to specified axis
this._attachedToXAxis = attachedToXAxis;
}
#endregion
#region Cursor fields
// Reference to the chart area object the cursor belongs to
private ChartArea _chartArea = null;
// Defines which axis the cursor attached to X or Y
private AxisName _attachedToXAxis = AxisName.X;
// Enables/Disables chart area cursor.
private bool _isUserEnabled = false;
// Enables/Disables chart area selection.
private bool _isUserSelectionEnabled = false;
// Indicates that cursor will automatically scroll the area scaleView if necessary.
private bool _autoScroll = true;
// Cursor line color
private Color _lineColor = Color.Red;
// Cursor line width
private int _lineWidth = 1;
// Cursor line style
private ChartDashStyle _lineDashStyle = ChartDashStyle.Solid;
// Chart area selection color
private Color _selectionColor = Color.LightGray;
// AxisName of the axes (primary/secondary) the cursor is attached to
private AxisType _axisType = AxisType.Primary;
// Cursor position
private double _position = Double.NaN;
// Range selection start position.
private double _selectionStart = Double.NaN;
// Range selection end position.
private double _selectionEnd = Double.NaN;
// Cursor movement interval current & original values
private double _interval = 1;
// Cursor movement interval type
private DateTimeIntervalType _intervalType = DateTimeIntervalType.Auto;
// Cursor movement interval offset current & original values
private double _intervalOffset = 0;
// Cursor movement interval offset type
private DateTimeIntervalType _intervalOffsetType = DateTimeIntervalType.Auto;
// Reference to the axis obhect
private Axis _axis = null;
// User selection start point
private PointF _userSelectionStart = PointF.Empty;
// Indicates that selection must be drawn
private bool _drawSelection = true;
// Indicates that events must be fired when position/selection is changed
private bool _fireUserChangingEvent = false;
// Indicates that XXXChanged events must be fired when position/selection is changed
private bool _fireUserChangedEvent = false;
// Scroll size and direction when AutoScroll is set
private MouseEventArgs _mouseMoveArguments = null;
// Timer used to scroll the data while selecting
private System.Windows.Forms.Timer _scrollTimer = new System.Windows.Forms.Timer();
// Indicates that axis data scaleView was scrolled as a result of the mouse move event
private bool _viewScrolledOnMouseMove = false;
#endregion
#region Cursor "Behavior" public properties.
///
/// Gets or sets the position of a cursor.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(Double.NaN),
SRDescription("DescriptionAttributeCursor_Position"),
ParenthesizePropertyNameAttribute(true),
TypeConverter(typeof(DoubleDateNanValueConverter)),
]
public double Position
{
get
{
return _position;
}
set
{
if(_position != value)
{
_position = value;
// Align cursor in connected areas
if(this._chartArea != null && this._chartArea.Common != null && this._chartArea.Common.ChartPicture != null)
{
if(!this._chartArea.alignmentInProcess)
{
AreaAlignmentOrientations orientation = (this._attachedToXAxis == AxisName.X || this._attachedToXAxis == AxisName.X2) ?
AreaAlignmentOrientations.Vertical : AreaAlignmentOrientations.Horizontal;
this._chartArea.Common.ChartPicture.AlignChartAreasCursor(this._chartArea, orientation, false);
}
}
if(this._chartArea != null && !this._chartArea.alignmentInProcess)
{
this.Invalidate(false);
}
}
}
}
///
/// Gets or sets the starting position of a cursor's selected range.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(Double.NaN),
SRDescription("DescriptionAttributeCursor_SelectionStart"),
TypeConverter(typeof(DoubleDateNanValueConverter)),
]
public double SelectionStart
{
get
{
return _selectionStart;
}
set
{
if(_selectionStart != value)
{
_selectionStart = value;
// Align cursor in connected areas
if(this._chartArea != null && this._chartArea.Common != null && this._chartArea.Common.ChartPicture != null)
{
if(!this._chartArea.alignmentInProcess)
{
AreaAlignmentOrientations orientation = (this._attachedToXAxis == AxisName.X || this._attachedToXAxis == AxisName.X2) ?
AreaAlignmentOrientations.Vertical : AreaAlignmentOrientations.Horizontal;
this._chartArea.Common.ChartPicture.AlignChartAreasCursor(this._chartArea, orientation, false);
}
}
if(this._chartArea != null && !this._chartArea.alignmentInProcess)
{
this.Invalidate(false);
}
}
}
}
///
/// Gets or sets the ending position of a range selection.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(Double.NaN),
SRDescription("DescriptionAttributeCursor_SelectionEnd"),
TypeConverter(typeof(DoubleDateNanValueConverter)),
]
public double SelectionEnd
{
get
{
return _selectionEnd;
}
set
{
if(_selectionEnd != value)
{
_selectionEnd = value;
// Align cursor in connected areas
if(this._chartArea != null && this._chartArea.Common != null && this._chartArea.Common.ChartPicture != null)
{
if(!this._chartArea.alignmentInProcess)
{
AreaAlignmentOrientations orientation = (this._attachedToXAxis == AxisName.X || this._attachedToXAxis == AxisName.X2) ?
AreaAlignmentOrientations.Vertical : AreaAlignmentOrientations.Horizontal;
this._chartArea.Common.ChartPicture.AlignChartAreasCursor(this._chartArea, orientation, false);
}
}
if(this._chartArea != null && !this._chartArea.alignmentInProcess)
{
this.Invalidate(false);
}
}
}
}
///
/// Gets or sets a property that enables or disables the cursor interface.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(false),
SRDescription("DescriptionAttributeCursor_UserEnabled"),
]
public bool IsUserEnabled
{
get
{
return _isUserEnabled;
}
set
{
_isUserEnabled = value;
}
}
///
/// Gets or sets a property that enables or disables the range selection interface.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(false),
SRDescription("DescriptionAttributeCursor_UserSelection"),
]
public bool IsUserSelectionEnabled
{
get
{
return _isUserSelectionEnabled;
}
set
{
_isUserSelectionEnabled = value;
}
}
///
/// Determines if scrolling will occur if a range selection operation
/// extends beyond a boundary of the chart area.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(true),
SRDescription("DescriptionAttributeCursor_AutoScroll"),
]
public bool AutoScroll
{
get
{
return _autoScroll;
}
set
{
_autoScroll = value;
}
}
///
/// Gets or sets the type of axis that the cursor is attached to.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
SRDescription("DescriptionAttributeCursor_AxisType"),
DefaultValue(AxisType.Primary)
]
public AxisType AxisType
{
get
{
return _axisType;
}
set
{
_axisType = value;
// Reset reference to the axis object
_axis = null;
this.Invalidate(true);
}
}
///
/// Gets or sets the cursor movement interval.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(1.0),
SRDescription("DescriptionAttributeCursor_Interval"),
]
public double Interval
{
get
{
return _interval;
}
set
{
_interval = value;
}
}
///
/// Gets or sets the unit of measurement of the Interval property.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(DateTimeIntervalType.Auto),
SRDescription("DescriptionAttributeCursor_IntervalType")
]
public DateTimeIntervalType IntervalType
{
get
{
return _intervalType;
}
set
{
_intervalType = (value != DateTimeIntervalType.NotSet) ? value : DateTimeIntervalType.Auto;
}
}
///
/// Gets or sets the interval offset, which determines
/// where to draw the cursor and range selection.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(0.0),
SRDescription("DescriptionAttributeCursor_IntervalOffset"),
]
public double IntervalOffset
{
get
{
return _intervalOffset;
}
set
{
// Validation
if( value < 0.0 )
{
throw (new ArgumentException(SR.ExceptionCursorIntervalOffsetIsNegative, "value"));
}
_intervalOffset = value;
}
}
///
/// Gets or sets the unit of measurement of the IntervalOffset property.
///
[
SRCategory("CategoryAttributeBehavior"),
Bindable(true),
DefaultValue(DateTimeIntervalType.Auto),
SRDescription("DescriptionAttributeCursor_IntervalOffsetType"),
]
public DateTimeIntervalType IntervalOffsetType
{
get
{
return _intervalOffsetType;
}
set
{
_intervalOffsetType = (value != DateTimeIntervalType.NotSet) ? value : DateTimeIntervalType.Auto;
}
}
#endregion
#region Cursor "Appearance" public properties
///
/// Gets or sets the color the cursor line.
///
[
SRCategory("CategoryAttributeAppearance"),
Bindable(true),
DefaultValue(typeof(Color), "Red"),
SRDescription("DescriptionAttributeLineColor"),
TypeConverter(typeof(ColorConverter)),
Editor(Editors.ChartColorEditor.Editor, Editors.ChartColorEditor.Base),
]
public Color LineColor
{
get
{
return _lineColor;
}
set
{
_lineColor = value;
this.Invalidate(false);
}
}
///
/// Gets or sets the style of the cursor line.
///
[
SRCategory("CategoryAttributeAppearance"),
Bindable(true),
DefaultValue(ChartDashStyle.Solid),
SRDescription("DescriptionAttributeLineDashStyle"),
]
public ChartDashStyle LineDashStyle
{
get
{
return _lineDashStyle;
}
set
{
_lineDashStyle = value;
this.Invalidate(false);
}
}
///
/// Gets or sets the width of the cursor line.
///
[
SRCategory("CategoryAttributeAppearance"),
Bindable(true),
DefaultValue(1),
SRDescription("DescriptionAttributeLineWidth"),
]
public int LineWidth
{
get
{
return _lineWidth;
}
set
{
if(value < 0)
{
throw (new ArgumentOutOfRangeException("value", SR.ExceptionCursorLineWidthIsNegative));
}
_lineWidth = value;
this.Invalidate(true);
}
}
///
/// Gets or sets a semi-transparent color that highlights a range of data.
///
[
SRCategory("CategoryAttributeAppearance"),
Bindable(true),
DefaultValue(typeof(Color), "LightGray"),
SRDescription("DescriptionAttributeCursor_SelectionColor"),
TypeConverter(typeof(ColorConverter)),
Editor(Editors.ChartColorEditor.Editor, Editors.ChartColorEditor.Base),
]
public Color SelectionColor
{
get
{
return _selectionColor;
}
set
{
_selectionColor = value;
this.Invalidate(false);
}
}
#endregion
#region Cursor painting methods
///
/// Draws chart area cursor and selection.
///
/// Reference to the ChartGraphics object.
internal void Paint( ChartGraphics graph )
{
//***************************************************
//** Prepare for drawing
//***************************************************
// Do not proceed with painting if cursor is not attached to the axis
if(this.GetAxis() == null ||
this._chartArea == null ||
this._chartArea.Common == null ||
this._chartArea.Common.ChartPicture == null ||
this._chartArea.Common.ChartPicture.isPrinting)
{
return;
}
// Get plot area position
RectangleF plotAreaPosition = this._chartArea.PlotAreaPosition.ToRectangleF();
// Detect if cursor is horizontal or vertical
bool horizontal = true;
if(this.GetAxis().AxisPosition == AxisPosition.Bottom || this.GetAxis().AxisPosition == AxisPosition.Top)
{
horizontal = false;
}
//***************************************************
//** Draw selection
//***************************************************
// Check if selection need to be drawn
if(this._drawSelection &&
!double.IsNaN(this.SelectionStart) &&
!double.IsNaN(this.SelectionEnd) &&
this.SelectionColor != Color.Empty)
{
// Calculate selection rectangle
RectangleF rectSelection = GetSelectionRect(plotAreaPosition);
rectSelection.Intersect(plotAreaPosition);
// Get opposite axis selection rectangle
RectangleF rectOppositeSelection = GetOppositeSelectionRect(plotAreaPosition);
// Draw selection if rectangle is not empty
if(!rectSelection.IsEmpty && rectSelection.Width > 0 && rectSelection.Height > 0)
{
// Limit selection rectangle to the area of the opposite selection
if(!rectOppositeSelection.IsEmpty && rectOppositeSelection.Width > 0 && rectOppositeSelection.Height > 0)
{
rectSelection.Intersect(rectOppositeSelection);
// We do not need to draw selection in the opposite axis
Cursor oppositeCursor =
(_attachedToXAxis == AxisName.X || _attachedToXAxis == AxisName.X2) ?
_chartArea.CursorY : _chartArea.CursorX;
oppositeCursor._drawSelection = false;
}
// Make sure selection is inside plotting area
rectSelection.Intersect(plotAreaPosition);
// If selection rectangle is not empty
if(rectSelection.Width > 0 && rectSelection.Height > 0)
{
// Add transparency to solid colors
Color rangeSelectionColor = this.SelectionColor;
if(rangeSelectionColor.A == 255)
{
rangeSelectionColor = Color.FromArgb(120, rangeSelectionColor);
}
// Draw selection
graph.FillRectangleRel( rectSelection,
rangeSelectionColor,
ChartHatchStyle.None,
"",
ChartImageWrapMode.Tile,
Color.Empty,
ChartImageAlignmentStyle.Center,
GradientStyle.None,
Color.Empty,
Color.Empty,
0,
ChartDashStyle.NotSet,
Color.Empty,
0,
PenAlignment.Inset );
}
}
}
//***************************************************
//** Draw cursor
//***************************************************
// Check if cursor need to be drawn
if(!double.IsNaN(this.Position) &&
this.LineColor != Color.Empty &&
this.LineWidth > 0 &&
this.LineDashStyle != ChartDashStyle.NotSet)
{
// Calculate line position
bool insideArea = false;
PointF point1 = PointF.Empty;
PointF point2 = PointF.Empty;
if(horizontal)
{
// Set cursor coordinates
point1.X = plotAreaPosition.X;
point1.Y = (float)this.GetAxis().GetLinearPosition(this.Position);
point2.X = plotAreaPosition.Right;
point2.Y = point1.Y;
// Check if cursor is inside plotting rect
if(point1.Y >= plotAreaPosition.Y && point1.Y <= plotAreaPosition.Bottom)
{
insideArea = true;
}
}
else
{
// Set cursor coordinates
point1.X = (float)this.GetAxis().GetLinearPosition(this.Position);
point1.Y = plotAreaPosition.Y;
point2.X = point1.X;
point2.Y = plotAreaPosition.Bottom;
// Check if cursor is inside plotting rect
if(point1.X >= plotAreaPosition.X && point1.X <= plotAreaPosition.Right)
{
insideArea = true;
}
}
// Draw cursor if it's inside the chart area plotting rectangle
if(insideArea)
{
graph.DrawLineRel(this.LineColor, this.LineWidth, this.LineDashStyle, point1, point2);
}
}
// Reset draw selection flag
this._drawSelection = true;
}
#endregion
#region Cursor position setting methods
///
/// This method sets the position of a cursor within a chart area at a given axis value.
///
/// The new position of the cursor. Measured as a value along the relevant axis.
public void SetCursorPosition(double newPosition)
{
// Check if we are setting different value
if(this.Position != newPosition)
{
double newRoundedPosition = RoundPosition(newPosition);
// Send PositionChanging event
if(_fireUserChangingEvent && GetChartObject() != null)
{
CursorEventArgs arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), newRoundedPosition);
GetChartObject().OnCursorPositionChanging(arguments);
// Check if position values were changed in the event
newRoundedPosition = arguments.NewPosition;
}
// Change position
this.Position = newRoundedPosition;
// Send PositionChanged event
if(_fireUserChangedEvent && GetChartObject() != null)
{
CursorEventArgs arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), this.Position);
GetChartObject().OnCursorPositionChanged(arguments);
}
}
}
///
/// This method displays a cursor at the specified position. Measured in pixels.
///
/// A PointF structure that specifies where the cursor will be drawn.
/// If true, the cursor will be drawn along the nearest chart area boundary
/// when the specified position does not fall within a ChartArea object.
public void SetCursorPixelPosition(PointF point, bool roundToBoundary)
{
if(this._chartArea != null && this._chartArea.Common != null && this.GetAxis() != null)
{
PointF relativeCoord = GetPositionInPlotArea(point, roundToBoundary);
if(!relativeCoord.IsEmpty)
{
// Get new cursor position
double newCursorPosition = PositionToCursorPosition(relativeCoord);
// Set new cursor & selection position
this.SetCursorPosition(newCursorPosition);
}
}
}
///
/// This method sets the position of a selected range within a chart area at given axis values.
///
/// The new starting position of the range selection. Measured as a value along the relevant axis..
/// The new ending position of the range selection. Measured as a value along the relevant axis.
public void SetSelectionPosition(double newStart, double newEnd)
{
// Check if we are setting different value
if(this.SelectionStart != newStart || this.SelectionEnd != newEnd)
{
// Send PositionChanging event
double newRoundedSelectionStart = RoundPosition(newStart);
double newRoundedSelectionEnd = RoundPosition(newEnd);
// Send SelectionRangeChanging event
if(_fireUserChangingEvent && GetChartObject() != null)
{
CursorEventArgs arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), newRoundedSelectionStart, newRoundedSelectionEnd);
GetChartObject().OnSelectionRangeChanging(arguments);
// Check if position values were changed in the event
newRoundedSelectionStart = arguments.NewSelectionStart;
newRoundedSelectionEnd = arguments.NewSelectionEnd;
}
// Change selection position
this._selectionStart = newRoundedSelectionStart;
this._selectionEnd = newRoundedSelectionEnd;
// Align cursor in connected areas
if(this._chartArea != null && this._chartArea.Common != null && this._chartArea.Common.ChartPicture != null)
{
if(!this._chartArea.alignmentInProcess)
{
AreaAlignmentOrientations orientation = (this._attachedToXAxis == AxisName.X || this._attachedToXAxis == AxisName.X2) ?
AreaAlignmentOrientations.Vertical : AreaAlignmentOrientations.Horizontal;
this._chartArea.Common.ChartPicture.AlignChartAreasCursor(this._chartArea, orientation, true);
}
}
if(this._chartArea != null && !this._chartArea.alignmentInProcess)
{
this.Invalidate(false);
}
// Send SelectionRangeChanged event
if(_fireUserChangedEvent && GetChartObject() != null)
{
CursorEventArgs arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), this.SelectionStart, this.SelectionEnd);
GetChartObject().OnSelectionRangeChanged(arguments);
}
}
}
///
/// This method sets the starting and ending positions of a range selection.
///
/// A PointF structure that specifies where the range selection begins.
/// A PointF structure that specifies where the range selection ends
/// If true, the starting and ending points will be rounded to the nearest chart area boundary
/// when the specified positions do not fall within a ChartArea object.
public void SetSelectionPixelPosition(PointF startPoint, PointF endPoint, bool roundToBoundary)
{
if(this._chartArea != null && this._chartArea.Common != null && this.GetAxis() != null)
{
// Calculating the start position
double newStart = this.SelectionStart;
if(!startPoint.IsEmpty)
{
PointF relativeCoord = GetPositionInPlotArea(startPoint, roundToBoundary);
if(!relativeCoord.IsEmpty)
{
// Get new selection start position
newStart = PositionToCursorPosition(relativeCoord);
}
}
// Setting the end position
double newEnd = newStart;
if(!endPoint.IsEmpty)
{
PointF relativeCoord = GetPositionInPlotArea(endPoint, roundToBoundary);
if(!relativeCoord.IsEmpty)
{
// Get new selection position
newEnd = PositionToCursorPosition(relativeCoord);
}
}
// Set new selection start & end position
this.SetSelectionPosition(newStart, newEnd);
}
}
#endregion
#region Position rounding methods
///
/// Rounds new position of the cursor or range selection
///
///
///
internal double RoundPosition(double cursorPosition)
{
double roundedPosition = cursorPosition;
if(!double.IsNaN(roundedPosition))
{
// Check if position rounding is required
if(this.GetAxis() != null &&
this.Interval != 0 &&
!double.IsNaN(this.Interval))
{
// Get first series attached to this axis
Series axisSeries = null;
if(_axis.axisType == AxisName.X || _axis.axisType == AxisName.X2)
{
List seriesArray = _axis.ChartArea.GetXAxesSeries((_axis.axisType == AxisName.X) ? AxisType.Primary : AxisType.Secondary, _axis.SubAxisName);
if(seriesArray.Count > 0)
{
string seriesName = seriesArray[0] as string;
axisSeries = _axis.Common.DataManager.Series[seriesName];
if(axisSeries != null && !axisSeries.IsXValueIndexed)
{
axisSeries = null;
}
}
}
// If interval type is not set - use number
DateTimeIntervalType intervalType =
(this.IntervalType == DateTimeIntervalType.Auto) ?
DateTimeIntervalType.Number : this.IntervalType;
// If interval offset type is not set - use interval type
DateTimeIntervalType offsetType =
(this.IntervalOffsetType == DateTimeIntervalType.Auto) ?
intervalType : this.IntervalOffsetType;
// Round numbers
if(intervalType == DateTimeIntervalType.Number)
{
double newRoundedPosition = Math.Round(roundedPosition / this.Interval) * this.Interval;
// Add offset number
if(this.IntervalOffset != 0 &&
!double.IsNaN(IntervalOffset) &&
offsetType != DateTimeIntervalType.Auto)
{
if(this.IntervalOffset > 0)
{
newRoundedPosition += ChartHelper.GetIntervalSize(newRoundedPosition, this.IntervalOffset, offsetType);
}
else
{
newRoundedPosition -= ChartHelper.GetIntervalSize(newRoundedPosition, this.IntervalOffset, offsetType);
}
}
// Find rounded position after/before the current
double nextPosition = newRoundedPosition;
if(newRoundedPosition <= cursorPosition)
{
nextPosition += ChartHelper.GetIntervalSize(newRoundedPosition, this.Interval, intervalType, axisSeries, 0, DateTimeIntervalType.Number, true);
}
else
{
nextPosition -= ChartHelper.GetIntervalSize(newRoundedPosition, this.Interval, intervalType, axisSeries, 0, DateTimeIntervalType.Number, true);
}
// Choose closest rounded position
if(Math.Abs(nextPosition - cursorPosition) > Math.Abs(cursorPosition - newRoundedPosition))
{
roundedPosition = newRoundedPosition;
}
else
{
roundedPosition = nextPosition;
}
}
// Round date/time
else
{
// Find one rounded position prior and one after current position
// Adjust start position depending on the interval and type
double prevPosition = ChartHelper.AlignIntervalStart(cursorPosition, this.Interval, intervalType, axisSeries);
// Adjust start position depending on the interval offset and offset type
if( IntervalOffset != 0 && axisSeries == null)
{
if(this.IntervalOffset > 0)
{
prevPosition += ChartHelper.GetIntervalSize(
prevPosition,
this.IntervalOffset,
offsetType,
axisSeries,
0,
DateTimeIntervalType.Number,
true);
}
else
{
prevPosition += ChartHelper.GetIntervalSize(
prevPosition,
-this.IntervalOffset,
offsetType,
axisSeries,
0,
DateTimeIntervalType.Number,
true);
}
}
// Find rounded position after/before the current
double nextPosition = prevPosition;
if(prevPosition <= cursorPosition)
{
nextPosition += ChartHelper.GetIntervalSize(prevPosition, this.Interval, intervalType, axisSeries, 0, DateTimeIntervalType.Number, true);
}
else
{
nextPosition -= ChartHelper.GetIntervalSize(prevPosition, this.Interval, intervalType, axisSeries, 0, DateTimeIntervalType.Number, true);
}
// Choose closest rounded position
if(Math.Abs(nextPosition - cursorPosition) > Math.Abs(cursorPosition - prevPosition))
{
roundedPosition = prevPosition;
}
else
{
roundedPosition = nextPosition;
}
}
}
}
return roundedPosition;
}
#endregion
#region Mouse events handling for the Cursor
///
/// Mouse down event handler.
///
internal void Cursor_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
// Set flag to fire position changing events
_fireUserChangingEvent = true;
_fireUserChangedEvent = false;
// Check if left mouse button was clicked in chart area
if(e.Button == MouseButtons.Left && !GetPositionInPlotArea(new PointF(e.X, e.Y), false).IsEmpty)
{
// Change cursor position and selection start position when mouse down
if(this.IsUserEnabled)
{
SetCursorPixelPosition(new PointF(e.X, e.Y), false);
}
if(this.IsUserSelectionEnabled)
{
this._userSelectionStart = new PointF(e.X, e.Y);
SetSelectionPixelPosition(this._userSelectionStart, PointF.Empty, false);
}
}
// Clear flag to fire position changing events
_fireUserChangingEvent = false;
_fireUserChangedEvent = false;
}
///
/// Mouse up event handler.
///
internal void Cursor_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
// If in range selection mode
if(!this._userSelectionStart.IsEmpty)
{
// Stop timer
_scrollTimer.Stop();
_mouseMoveArguments = null;
// Check if axis data scaleView zooming UI is enabled
if(this._axis != null &&
this._axis.ScaleView.Zoomable &&
!double.IsNaN(this.SelectionStart) &&
!double.IsNaN(this.SelectionEnd) &&
this.SelectionStart != this.SelectionEnd)
{
// Zoom data scaleView
double start = Math.Min(this.SelectionStart, this.SelectionEnd);
double size = (double)Math.Max(this.SelectionStart, this.SelectionEnd) - start;
bool zoomed = this._axis.ScaleView.Zoom(start, size, DateTimeIntervalType.Number, true, true);
// Clear image buffer
if(this._chartArea.areaBufferBitmap != null && zoomed)
{
this._chartArea.areaBufferBitmap.Dispose();
this._chartArea.areaBufferBitmap = null;
}
// Clear range selection
this.SelectionStart = double.NaN;
this.SelectionEnd = double.NaN;
// NOTE: Fixes issue #6823
// Clear cursor position after the zoom in operation
this.Position = double.NaN;
// Align cursor in connected areas
if(this._chartArea != null && this._chartArea.Common != null && this._chartArea.Common.ChartPicture != null)
{
if(!this._chartArea.alignmentInProcess)
{
AreaAlignmentOrientations orientation = (this._attachedToXAxis == AxisName.X || this._attachedToXAxis == AxisName.X2) ?
AreaAlignmentOrientations.Vertical : AreaAlignmentOrientations.Horizontal;
this._chartArea.Common.ChartPicture.AlignChartAreasZoomed(this._chartArea, orientation, zoomed);
}
}
}
// Fire XXXChanged events
if(GetChartObject() != null)
{
CursorEventArgs arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), this.SelectionStart, this.SelectionEnd);
GetChartObject().OnSelectionRangeChanged(arguments);
arguments = new CursorEventArgs(this._chartArea, this.GetAxis(), this.Position);
GetChartObject().OnCursorPositionChanged(arguments);
}
// Stop range selection mode
this._userSelectionStart = PointF.Empty;
}
}
///
/// Mouse move event handler.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Mobility", "CA1601:DoNotUseTimersThatPreventPowerStateChanges", Justification = "The timer is used for simulating scrolling behavior")]
internal void Cursor_MouseMove(System.Windows.Forms.MouseEventArgs e, ref bool handled)
{
// Process range selection
if(this._userSelectionStart != PointF.Empty)
{
// Mouse move event should not be handled by any other chart elements
handled = true;
// Set flag to fire position changing events
_fireUserChangingEvent = true;
_fireUserChangedEvent = false;
// Check if mouse position is outside of the chart area and if not - try scrolling
if(this.AutoScroll)
{
if(this._chartArea != null && this._chartArea.Common != null && this.GetAxis()!= null)
{
// Check if axis data scaleView is enabled
if(!double.IsNaN(this._axis.ScaleView.Position) && !double.IsNaN(this._axis.ScaleView.Size))
{
ScrollType scrollType = ScrollType.SmallIncrement;
bool insideChartArea = true;
double offsetFromBoundary = 0.0;
// Translate mouse pixel coordinates into the relative chart area coordinates
float mouseX = e.X * 100F / ((float)(this._chartArea.Common.Width - 1));
float mouseY = e.Y * 100F / ((float)(this._chartArea.Common.Height - 1));
// Check if coordinate is inside chart plotting area
if(this._axis.AxisPosition == AxisPosition.Bottom || this._axis.AxisPosition == AxisPosition.Top)
{
if(mouseX < this._chartArea.PlotAreaPosition.X)
{
scrollType = ScrollType.SmallDecrement;
insideChartArea = false;
offsetFromBoundary = this._chartArea.PlotAreaPosition.X - mouseX;
}
else if(mouseX > this._chartArea.PlotAreaPosition.Right)
{
scrollType = ScrollType.SmallIncrement;
insideChartArea = false;
offsetFromBoundary = mouseX - this._chartArea.PlotAreaPosition.Right;
}
}
else
{
if(mouseY < this._chartArea.PlotAreaPosition.Y)
{
scrollType = ScrollType.SmallIncrement;
insideChartArea = false;
offsetFromBoundary = this._chartArea.PlotAreaPosition.Y - mouseY;
}
else if(mouseY > this._chartArea.PlotAreaPosition.Bottom)
{
scrollType = ScrollType.SmallDecrement;
insideChartArea = false;
offsetFromBoundary = mouseY - this._chartArea.PlotAreaPosition.Bottom;
}
}
// Try scrolling scaleView position
if(!insideChartArea)
{
// Set flag that data scaleView was scrolled
_viewScrolledOnMouseMove = true;
// Get minimum scroll interval
double scrollInterval = ChartHelper.GetIntervalSize(
this._axis.ScaleView.Position,
this._axis.ScaleView.GetScrollingLineSize(),
this._axis.ScaleView.GetScrollingLineSizeType());
offsetFromBoundary *= 2;
if(offsetFromBoundary > scrollInterval)
{
scrollInterval = ((int)(offsetFromBoundary / scrollInterval)) * scrollInterval;
}
// Scroll axis data scaleView
double newDataViewPosition = this._axis.ScaleView.Position;
if(scrollType == ScrollType.SmallIncrement)
{
newDataViewPosition += scrollInterval;
}
else
{
newDataViewPosition -= scrollInterval;
}
// Scroll axis data scaleView
this._axis.ScaleView.Scroll(newDataViewPosition);
// Save last mouse move arguments
_mouseMoveArguments = new MouseEventArgs(e.Button, e.Clicks, e.X, e.Y, e.Delta);
// Start selection scrolling timer
if(!_scrollTimer.Enabled)
{
// Start timer
_scrollTimer.Tick += new EventHandler(SelectionScrollingTimerEventProcessor);
_scrollTimer.Interval = 200;
_scrollTimer.Start();
}
}
else
{
// Stop timer
_scrollTimer.Stop();
_mouseMoveArguments = null;
}
}
}
}
// Change cursor position and selection end position when mouse moving
if(this.IsUserEnabled)
{
SetCursorPixelPosition(new PointF(e.X, e.Y), true);
}
if(this.IsUserSelectionEnabled)
{
// Set selection
SetSelectionPixelPosition(PointF.Empty, new PointF(e.X, e.Y), true);
}
// Clear flag to fire position changing events
_fireUserChangingEvent = false;
_fireUserChangedEvent = false;
// Clear flag that data scaleView was scrolled
_viewScrolledOnMouseMove = false;
}
}
///
/// This is the method to run when the timer is raised.
/// Used to scroll axis data scaleView while mouse is outside of the chart area.
///
///
///
private void SelectionScrollingTimerEventProcessor(Object myObject, EventArgs myEventArgs)
{
// Simulate mouse move events
if(_mouseMoveArguments != null)
{
bool handled = false;
this.Cursor_MouseMove(_mouseMoveArguments, ref handled);
}
}
#endregion
#region Cursor helper methods
///
/// Helper function which returns a reference to the chart object
///
/// Chart object reference.
private Chart GetChartObject()
{
if(this._chartArea != null )
{
return this._chartArea.Chart;
}
return null;
}
///
/// Get rectangle of the axis range selection.
///
/// Selection rectangle.
/// Plot area rectangle.
///
private RectangleF GetSelectionRect(RectangleF plotAreaPosition)
{
RectangleF rect = RectangleF.Empty;
if(this._axis != null &&
this.SelectionStart != this.SelectionEnd)
{
double start = (float)this._axis.GetLinearPosition(this.SelectionStart);
double end = (float)this._axis.GetLinearPosition(this.SelectionEnd);
// Detect if cursor is horizontal or vertical
bool horizontal = true;
if(this.GetAxis().AxisPosition == AxisPosition.Bottom || this.GetAxis().AxisPosition == AxisPosition.Top)
{
horizontal = false;
}
if(horizontal)
{
rect.X = plotAreaPosition.X;
rect.Width = plotAreaPosition.Width;
rect.Y = (float)Math.Min(start, end);
rect.Height = (float)Math.Max(start, end) - rect.Y;
}
else
{
rect.Y = plotAreaPosition.Y;
rect.Height = plotAreaPosition.Height;
rect.X = (float)Math.Min(start, end);
rect.Width = (float)Math.Max(start, end) - rect.X;
}
}
return rect;
}
///
/// Get rectangle of the opposite axis selection
///
/// Plot area rectangle.
/// Opposite selection rectangle.
private RectangleF GetOppositeSelectionRect(RectangleF plotAreaPosition)
{
if(_chartArea != null)
{
// Get opposite cursor
Cursor oppositeCursor =
(_attachedToXAxis == AxisName.X || _attachedToXAxis == AxisName.X2) ?
_chartArea.CursorY : _chartArea.CursorX;
return oppositeCursor.GetSelectionRect(plotAreaPosition);
}
return RectangleF.Empty;
}
///
/// Converts X or Y position value to the cursor axis value
///
/// Position in relative coordinates.
/// Cursor position as axis value.
private double PositionToCursorPosition(PointF position)
{
// Detect if cursor is horizontal or vertical
bool horizontal = true;
if(this.GetAxis().AxisPosition == AxisPosition.Bottom || this.GetAxis().AxisPosition == AxisPosition.Top)
{
horizontal = false;
}
// Convert relative coordinates into axis values
double newCursorPosition = double.NaN;
if(horizontal)
{
newCursorPosition = this.GetAxis().PositionToValue(position.Y);
}
else
{
newCursorPosition = this.GetAxis().PositionToValue(position.X);
}
// Round new position using Step & StepType properties
newCursorPosition = RoundPosition(newCursorPosition);
return newCursorPosition;
}
///
/// Checks if specified point is located inside the plotting area.
/// Converts pixel coordinates to relative.
///
/// Point coordinates to test.
/// Indicates that coordinates must be rounded to area boundary.
/// PointF.IsEmpty or relative coordinates in plotting area.
private PointF GetPositionInPlotArea(PointF point, bool roundToBoundary)
{
PointF result = PointF.Empty;
if(this._chartArea != null && this._chartArea.Common != null && this.GetAxis()!= null)
{
// Translate mouse pixel coordinates into the relative chart area coordinates
result.X = point.X * 100F / ((float)(this._chartArea.Common.Width - 1));
result.Y = point.Y * 100F / ((float)(this._chartArea.Common.Height - 1));
// Round coordinate if it' outside chart plotting area
RectangleF plotAreaPosition = this._chartArea.PlotAreaPosition.ToRectangleF();
if(roundToBoundary)
{
if(result.X < plotAreaPosition.X)
{
result.X = plotAreaPosition.X;
}
if(result.X > plotAreaPosition.Right)
{
result.X = plotAreaPosition.Right;
}
if(result.Y < plotAreaPosition.Y)
{
result.Y = plotAreaPosition.Y;
}
if(result.Y > plotAreaPosition.Bottom)
{
result.Y = plotAreaPosition.Bottom;
}
}
else
{
// Check if coordinate is inside chart plotting area
if(result.X < plotAreaPosition.X ||
result.X > plotAreaPosition.Right ||
result.Y < plotAreaPosition.Y ||
result.Y > plotAreaPosition.Bottom)
{
result = PointF.Empty;
}
}
}
return result;
}
///
/// Invalidate chart are with the cursor.
///
/// Chart area must be invalidated.
private void Invalidate(bool invalidateArea)
{
if(this.GetChartObject() != null && this._chartArea != null && !this.GetChartObject().disableInvalidates)
{
// If data scaleView was scrolled - just invalidate the chart area
if(_viewScrolledOnMouseMove || invalidateArea || this.GetChartObject().dirtyFlag)
{
this._chartArea.Invalidate();
}
// If only cursor/selection position was changed - use optimized drawing algorithm
else
{
// Set flag to redraw cursor/selection only
this.GetChartObject().paintTopLevelElementOnly = true;
// Invalidate and update the chart
this._chartArea.Invalidate();
this.GetChartObject().Update();
// Clear flag to redraw cursor/selection only
this.GetChartObject().paintTopLevelElementOnly = false;
}
}
}
///
/// Gets axis objects the cursor is attached to.
///
/// Axis object.
internal Axis GetAxis()
{
if(_axis == null && _chartArea != null)
{
if(_attachedToXAxis == AxisName.X)
{
_axis = (_axisType == AxisType.Primary) ? _chartArea.AxisX : _chartArea.AxisX2;
}
else
{
_axis = (_axisType == AxisType.Primary) ? _chartArea.AxisY : _chartArea.AxisY2;
}
}
return _axis;
}
#endregion
#region IDisposable Members
///
/// Releases unmanaged and - optionally - managed resources
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Dispose managed resources
if (this._scrollTimer != null)
{
this._scrollTimer.Dispose();
this._scrollTimer = null;
}
}
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
///
/// The CursorEventArgs class stores the event arguments for cursor and selection events.
///
public class CursorEventArgs : EventArgs
{
#region Private fields
// Private fields for properties values storage
private ChartArea _chartArea = null;
private Axis _axis = null;
private double _newPosition = double.NaN;
private double _newSelectionStart = double.NaN;
private double _newSelectionEnd = double.NaN;
#endregion
#region Constructors
///
/// CursorEventArgs constructor.
///
/// ChartArea of the cursor.
/// Axis of the cursor.
/// New cursor position.
public CursorEventArgs(ChartArea chartArea, Axis axis, double newPosition)
{
this._chartArea = chartArea;
this._axis = axis;
this._newPosition = newPosition;
this._newSelectionStart = double.NaN;
this._newSelectionEnd = double.NaN;
}
///
/// CursorEventArgs constructor.
///
/// ChartArea of the cursor.
/// Axis of the cursor.
/// New range selection starting position.
/// New range selection ending position.
public CursorEventArgs(ChartArea chartArea, Axis axis, double newSelectionStart, double newSelectionEnd)
{
this._chartArea = chartArea;
this._axis = axis;
this._newPosition = double.NaN;
this._newSelectionStart = newSelectionStart;
this._newSelectionEnd = newSelectionEnd;
}
#endregion
#region Properties
///
/// ChartArea of the event.
///
[
SRDescription("DescriptionAttributeChartArea"),
]
public ChartArea ChartArea
{
get
{
return _chartArea;
}
}
///
/// Axis of the event.
///
[
SRDescription("DescriptionAttributeAxis"),
]
public Axis Axis
{
get
{
return _axis;
}
}
///
/// New cursor position.
///
[
SRDescription("DescriptionAttributeCursorEventArgs_NewPosition"),
]
public double NewPosition
{
get
{
return _newPosition;
}
set
{
_newPosition = value;
}
}
///
/// New range selection starting position.
///
[
SRDescription("DescriptionAttributeCursorEventArgs_NewSelectionStart"),
]
public double NewSelectionStart
{
get
{
return _newSelectionStart;
}
set
{
_newSelectionStart = value;
}
}
///
/// New range selection ending position.
///
[
SRDescription("DescriptionAttributeCursorEventArgs_NewSelectionEnd"),
]
public double NewSelectionEnd
{
get
{
return _newSelectionEnd;
}
set
{
_newSelectionEnd = value;
}
}
#endregion
}
}
#endif // #if WINFORMS_CONTROL