2016-04-02 11:22:42 +02:00
/*
* NPlot - A charting library for .NET
*
* Axis.cs
* Copyright (C) 2003-2006 Matt Howlett and others.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of NPlot nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System ;
using System.Collections ;
using System.Drawing ;
using System.Drawing.Drawing2D ;
namespace NPlot
{
/// <summary>
/// Encapsulates functionality common to all axis classes. All specific
/// axis classes derive from Axis. Axis can be used as a concrete class
/// itself - it is an Axis without any embilishments [tick marks or tick
/// mark labels].<br></br><br></br>
/// This class encapsulates no physical information about where the axes
/// are drawn.
/// </summary>
public class Axis : ICloneable
{
private bool autoScaleText_ ;
private bool autoScaleTicks_ ;
private StringFormat drawFormat_ ;
private bool flipTicksLabel_ ;
private float fontScale_ ;
private bool hidden_ ;
private bool hideTickText_ ;
private Brush labelBrush_ ;
private Font labelFontScaled_ ;
private Font labelFont_ ;
private bool labelOffsetAbsolute_ ;
private bool labelOffsetScaled_ = true ;
private float labelOffset_ ;
private string label_ ;
private int largeTickSize_ ;
private Pen linePen_ ;
private int minPhysicalLargeTickStep_ = 30 ;
private string numberFormat_ ;
private bool reversed_ ;
private int smallTickSize_ ;
private Brush tickTextBrush_ ;
private Font tickTextFontScaled_ ;
private Font tickTextFont_ ;
private bool tickTextNextToAxis_ ;
private float ticksAngle_ = ( float ) Math . PI / 2.0f ;
private bool ticksCrossAxis_ ;
private bool ticksIndependentOfPhysicalExtent_ ;
private float ticksLabelAngle_ ;
private double worldMax_ ;
private double worldMin_ ;
/// <summary>
/// Default constructor
/// </summary>
public Axis ()
{
Init ();
}
/// <summary>
/// Constructor that takes only world min and max values.
/// </summary>
/// <param name="worldMin">The minimum world coordinate.</param>
/// <param name="worldMax">The maximum world coordinate.</param>
public Axis ( double worldMin , double worldMax )
{
Init ();
WorldMin = worldMin ;
WorldMax = worldMax ;
}
/// <summary>
/// Copy constructor.
/// </summary>
/// <param name="a">The Axis to clone.</param>
public Axis ( Axis a )
{
DoClone ( a , this );
}
/// <summary>
/// If true, tick marks will cross the axis, with their centre on the axis line.
/// If false, tick marks will be drawn as a line with origin starting on the axis line.
/// </summary>
public bool TicksCrossAxis
{
get { return ticksCrossAxis_ ; }
set { ticksCrossAxis_ = value ; }
}
/// <summary>
/// The maximum world extent of the axis. Note that it is sensical if
/// WorldMax is less than WorldMin - the axis would just be descending
/// not ascending. Currently Axes won't display properly if you do
/// this - use the Axis.Reversed property instead to achieve the same
/// result.
/// Setting this raises the WorldMinChanged event and the WorldExtentsChanged event.
/// </summary>
public virtual double WorldMax
{
get { return worldMax_ ; }
set
{
worldMax_ = value ;
/*
if (this.WorldExtentsChanged != null)
this.WorldExtentsChanged(this, new WorldValueChangedArgs(worldMax_, WorldValueChangedArgs.MinMaxType.Max));
if (this.WorldMaxChanged != null)
this.WorldMaxChanged(this, new WorldValueChangedArgs(worldMax_, WorldValueChangedArgs.MinMaxType.Max));
*/
}
}
/// <summary>
/// The minumum world extent of the axis. Note that it is sensical if
/// WorldMax is less than WorldMin - the axis would just be descending
/// not ascending. Currently Axes won't display properly if you do
/// this - use the Axis.Reversed property instead to achieve the same
/// result.
/// Setting this raises the WorldMinChanged event and the WorldExtentsChanged event.
/// </summary>
public virtual double WorldMin
{
get { return worldMin_ ; }
set
{
worldMin_ = value ;
/*
if (this.WorldExtentsChanged != null)
this.WorldExtentsChanged( this, new WorldValueChangedArgs( worldMin_, WorldValueChangedArgs.MinMaxType.Min) );
if (this.WorldMinChanged != null)
this.WorldMinChanged( this, new WorldValueChangedArgs(worldMin_, WorldValueChangedArgs.MinMaxType.Min) );
*/
}
}
/// <summary>
/// Length (in pixels) of a large tick. <b>Not</b> the distance
/// between large ticks. The length of the tick itself.
/// </summary>
public int LargeTickSize
{
get { return largeTickSize_ ; }
set { largeTickSize_ = value ; }
}
/// <summary>
/// Length (in pixels) of the small ticks.
/// </summary>
public int SmallTickSize
{
get { return smallTickSize_ ; }
set { smallTickSize_ = value ; }
}
/// <summary>
/// The Axis Label
/// </summary>
public string Label
{
get { return label_ ; }
set { label_ = value ; }
}
/// <summary>
/// If true, text associated with tick marks will be drawn on the other side of the
/// axis line [next to the axis]. If false, tick mark text will be drawn at the end
/// of the tick mark [on the same of the axis line as the tick].
/// </summary>
public bool TickTextNextToAxis
{
get { return tickTextNextToAxis_ ; }
set { tickTextNextToAxis_ = value ; }
}
/// <summary>
/// If set to true, the axis is hidden. That is, the axis line, ticks, tick
/// labels and axis label will not be drawn.
/// </summary>
public bool Hidden
{
get { return hidden_ ; }
set { hidden_ = value ; }
}
/// <summary>
/// If set true, the axis will behave as though the WorldMin and WorldMax values
/// have been swapped.
/// </summary>
public bool Reversed
{
get { return reversed_ ; }
set { reversed_ = value ; }
}
/// <summary>
/// If true, no text will be drawn next to any axis tick marks.
/// </summary>
public bool HideTickText
{
get { return hideTickText_ ; }
set { hideTickText_ = value ; }
}
/// <summary>
/// This font is used for the drawing of text next to the axis tick marks.
/// </summary>
public Font TickTextFont
{
get { return tickTextFont_ ; }
set
{
tickTextFont_ = value ;
UpdateScale ();
}
}
/// <summary>
/// This font is used to draw the axis label.
/// </summary>
public Font LabelFont
{
get { return labelFont_ ; }
set
{
labelFont_ = value ;
UpdateScale ();
}
}
/// <summary>
/// Specifies the format used for drawing tick labels. See
/// StringBuilder.AppendFormat for a description of this
/// string.
/// </summary>
public string NumberFormat
{
get { return numberFormat_ ; }
set { numberFormat_ = value ; }
}
/// <summary>
/// If LargeTickStep isn't specified, then this will be calculated
/// automatically. The calculated value will not be less than this
/// amount.
/// </summary>
public int MinPhysicalLargeTickStep
{
get { return minPhysicalLargeTickStep_ ; }
set { minPhysicalLargeTickStep_ = value ; }
}
/// <summary>
/// The color of the pen used to draw the ticks and the axis line.
/// </summary>
public Color AxisColor
{
get { return linePen_ . Color ; }
set { linePen_ = new Pen ( value ); }
}
/// <summary>
/// The pen used to draw the ticks and the axis line.
/// </summary>
public Pen AxisPen
{
get { return linePen_ ; }
set { linePen_ = value ; }
}
/// <summary>
/// If true, automated tick placement will be independent of the physical
/// extent of the axis. Tick placement will look good for charts of typical
/// size (say physical dimensions of 640x480). If you want to produce the
/// same chart on two graphics surfaces of different sizes [eg Windows.Forms
/// control and printer], then you will want to set this property to true.
/// If false [default], the number of ticks and their placement will be
/// optimally calculated to look the best for the given axis extent. This
/// is very useful if you are creating a cart with particularly small or
/// large physical dimensions.
/// </summary>
public bool TicksIndependentOfPhysicalExtent
{
get { return ticksIndependentOfPhysicalExtent_ ; }
set { ticksIndependentOfPhysicalExtent_ = value ; }
}
/// <summary>
/// If true label is flipped about the text center line parallel to the text.
/// </summary>
public bool FlipTicksLabel
{
get { return flipTicksLabel_ ; }
set { flipTicksLabel_ = value ; }
}
/// <summary>
/// Angle to draw ticks at (measured anti-clockwise from axis direction).
/// </summary>
public float TicksAngle
{
get { return ticksAngle_ ; }
set { ticksAngle_ = value ; }
}
/// <summary>
/// Angle to draw large tick labels at (clockwise from horizontal). Note:
/// this is currently only implemented well for the lower x-axis.
/// </summary>
public float TicksLabelAngle
{
get { return ticksLabelAngle_ ; }
set { ticksLabelAngle_ = value ; }
}
/// <summary>
/// The color of the brush used to draw the axis label.
/// </summary>
public Color LabelColor
{
set { labelBrush_ = new SolidBrush ( value ); }
}
/// <summary>
/// The brush used to draw the axis label.
/// </summary>
public Brush LabelBrush
{
get { return labelBrush_ ; }
set { labelBrush_ = value ; }
}
/// <summary>
/// The color of the brush used to draw the axis tick labels.
/// </summary>
public Color TickTextColor
{
set { tickTextBrush_ = new SolidBrush ( value ); }
}
/// <summary>
/// The brush used to draw the tick text.
/// </summary>
public Brush TickTextBrush
{
get { return tickTextBrush_ ; }
set { tickTextBrush_ = value ; }
}
/// <summary>
/// If true, label and tick text will be scaled to match size
/// of PlotSurface2D. If false, they won't be.
/// </summary>
/// <remarks>Could also be argued this belongs in PlotSurface2D</remarks>
public bool AutoScaleText
{
get { return autoScaleText_ ; }
set { autoScaleText_ = value ; }
}
/// <summary>
/// If true, tick lengths will be scaled to match size
/// of PlotSurface2D. If false, they won't be.
/// </summary>
/// <remarks>Could also be argued this belongs in PlotSurface2D</remarks>
public bool AutoScaleTicks
{
get { return autoScaleTicks_ ; }
set { autoScaleTicks_ = value ; }
}
/// <summary>
/// World extent of the axis.
/// </summary>
public double WorldLength
{
get { return Math . Abs ( worldMax_ - worldMin_ ); }
}
/// <summary>
/// Scale label and tick fonts by this factor. Set by PlotSurface2D
/// Draw method.
/// </summary>
internal float FontScale
{
get { return fontScale_ ; }
set
{
fontScale_ = value ;
UpdateScale ();
}
}
/// <summary>
/// Scale tick mark lengths by this factor. Set by PlotSurface2D
/// Draw method.
/// </summary>
internal float TickScale { get ; set ; }
/// <summary>
/// Get whether or not this axis is linear.
/// </summary>
public virtual bool IsLinear
{
get { return true ; }
}
/// <summary>
/// If LabelOffsetAbsolute is false (default) then this is the offset
/// added to default axis label position. If LabelOffsetAbsolute is
/// true, then this is the absolute offset of the label from the axis.
/// If positive, offset is further away from axis, if negative, towards
/// the axis.
/// </summary>
public float LabelOffset
{
get { return labelOffset_ ; }
set { labelOffset_ = value ; }
}
/// <summary>
/// If true, the value specified by LabelOffset is the absolute distance
/// away from the axis that the label is drawn. If false, the value
/// specified by LabelOffset is added to the pre-calculated value to
/// determine the axis label position.
/// </summary>
/// <value></value>
public bool LabelOffsetAbsolute
{
get { return labelOffsetAbsolute_ ; }
set { labelOffsetAbsolute_ = value ; }
}
/// <summary>
/// Whether or not the supplied LabelOffset should be scaled by
/// a factor as specified by FontScale.
/// </summary>
public bool LabelOffsetScaled
{
get { return labelOffsetScaled_ ; }
set { labelOffsetScaled_ = value ; }
}
/// <summary>
/// Set the Axis color (sets all of axis line color, Tick text color, and label color).
/// </summary>
public Color Color
{
set
{
AxisColor = value ;
TickTextColor = value ;
LabelColor = value ;
}
}
/// <summary>
/// Deep copy of Axis.
/// </summary>
/// <remarks>
/// This method includes a check that guards against derived classes forgetting
/// to implement their own Clone method. If Clone is called on a object derived
/// from Axis, and the Clone method hasn't been overridden by that object, then
/// the test this.GetType == typeof(Axis) will fail.
/// </remarks>
/// <returns>A copy of the Axis Class</returns>
public virtual object Clone ()
{
// ensure that this isn't being called on a derived type. If that is the case
// then the derived type didn't override this method as it should have.
if ( GetType () != typeof ( Axis ))
{
throw new NPlotException ( "Clone not defined in derived type." );
}
Axis a = new Axis ();
DoClone ( this , a );
return a ;
}
/// <summary>
/// Helper method for Clone. Does all the copying - can be called by derived
/// types so they don't need to implement this part of the copying themselves.
/// also useful in constructor of derived types that takes Axis class.
/// </summary>
protected static void DoClone ( Axis b , Axis a )
{
// value items
a . autoScaleText_ = b . autoScaleText_ ;
a . autoScaleTicks_ = b . autoScaleTicks_ ;
a . worldMax_ = b . worldMax_ ;
a . worldMin_ = b . worldMin_ ;
a . tickTextNextToAxis_ = b . tickTextNextToAxis_ ;
a . hidden_ = b . hidden_ ;
a . hideTickText_ = b . hideTickText_ ;
a . reversed_ = b . reversed_ ;
a . ticksAngle_ = b . ticksAngle_ ;
a . ticksLabelAngle_ = b . ticksLabelAngle_ ;
a . minPhysicalLargeTickStep_ = b . minPhysicalLargeTickStep_ ;
a . ticksIndependentOfPhysicalExtent_ = b . ticksIndependentOfPhysicalExtent_ ;
a . largeTickSize_ = b . largeTickSize_ ;
a . smallTickSize_ = b . smallTickSize_ ;
a . ticksCrossAxis_ = b . ticksCrossAxis_ ;
a . labelOffset_ = b . labelOffset_ ;
a . labelOffsetAbsolute_ = b . labelOffsetAbsolute_ ;
a . labelOffsetScaled_ = b . labelOffsetScaled_ ;
// reference items.
a . tickTextFont_ = ( Font ) b . tickTextFont_ . Clone ();
a . label_ = ( string ) b . label_ . Clone ();
if ( b . numberFormat_ != null )
{
a . numberFormat_ = ( string ) b . numberFormat_ . Clone ();
}
else
{
a . numberFormat_ = null ;
}
a . labelFont_ = ( Font ) b . labelFont_ . Clone ();
a . linePen_ = ( Pen ) b . linePen_ . Clone ();
a . tickTextBrush_ = ( Brush ) b . tickTextBrush_ . Clone ();
a . labelBrush_ = ( Brush ) b . labelBrush_ . Clone ();
a . FontScale = b . FontScale ;
a . TickScale = b . TickScale ;
}
/// <summary>
/// Helper function for constructors.
/// Do initialization here so that Clear() method is handled properly
/// </summary>
private void Init ()
{
worldMax_ = double . NaN ;
worldMin_ = double . NaN ;
Hidden = false ;
SmallTickSize = 2 ;
LargeTickSize = 6 ;
FontScale = 1.0f ;
TickScale = 1.0f ;
AutoScaleTicks = false ;
AutoScaleText = false ;
TickTextNextToAxis = true ;
HideTickText = false ;
TicksCrossAxis = false ;
LabelOffset = 0.0f ;
LabelOffsetAbsolute = false ;
LabelOffsetScaled = true ;
Label = "" ;
NumberFormat = null ;
Reversed = false ;
FontFamily fontFamily = new FontFamily ( "Arial" );
TickTextFont = new Font ( fontFamily , 10 , FontStyle . Regular , GraphicsUnit . Pixel );
LabelFont = new Font ( fontFamily , 12 , FontStyle . Regular , GraphicsUnit . Pixel );
LabelColor = Color . Black ;
TickTextColor = Color . Black ;
linePen_ = new Pen ( Color . Black );
linePen_ . Width = 1.0f ;
FontScale = 1.0f ;
// saves constructing these in draw method.
drawFormat_ = new StringFormat ();
drawFormat_ . Alignment = StringAlignment . Center ;
}
/// <summary>
/// Determines whether a world value is outside range WorldMin -> WorldMax
/// </summary>
/// <param name="coord">the world value to test</param>
/// <returns>true if outside limits, false otherwise</returns>
public bool OutOfRange ( double coord )
{
if ( double . IsNaN ( WorldMin ) || double . IsNaN ( WorldMax ))
{
throw new NPlotException ( "world min / max not set" );
}
if ( coord > WorldMax || coord < WorldMin )
{
return true ;
}
else
{
return false ;
}
}
/// <summary>
/// Sets the world extent of the current axis to be just large enough
/// to encompas the current world extent of the axis, and the world
/// extent of the passed in axis
/// </summary>
/// <param name="a">The other Axis instance.</param>
public void LUB ( Axis a )
{
if ( a == null )
{
return ;
}
// mins
if (! double . IsNaN ( a . worldMin_ ))
{
if ( double . IsNaN ( worldMin_ ))
{
WorldMin = a . WorldMin ;
}
else
{
if ( a . WorldMin < WorldMin )
{
WorldMin = a . WorldMin ;
}
}
}
// maxs.
if (! double . IsNaN ( a . worldMax_ ))
{
if ( double . IsNaN ( worldMax_ ))
{
WorldMax = a . WorldMax ;
}
else
{
if ( a . WorldMax > WorldMax )
{
WorldMax = a . WorldMax ;
}
}
}
}
/// <summary>
/// World to physical coordinate transform.
/// </summary>
/// <param name="coord">The coordinate value to transform.</param>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="clip">if false, then physical value may extend outside worldMin / worldMax. If true, the physical value returned will be clipped to physicalMin or physicalMax if it lies outside this range.</param>
/// <returns>The transformed coordinates.</returns>
/// <remarks>
/// Not sure how much time is spent in this often called function. If it's lots, then
/// worth optimizing (there is scope to do so).
/// </remarks>
public virtual PointF WorldToPhysical (
double coord ,
PointF physicalMin ,
PointF physicalMax ,
bool clip )
{
// (1) account for reversed axis. Could be tricky and move
// this out, but would be a little messy.
PointF _physicalMin ;
PointF _physicalMax ;
if ( Reversed )
{
_physicalMin = physicalMax ;
_physicalMax = physicalMin ;
}
else
{
_physicalMin = physicalMin ;
_physicalMax = physicalMax ;
}
// (2) if want clipped value, return extrema if outside range.
if ( clip )
{
if ( WorldMin < WorldMax )
{
if ( coord > WorldMax )
{
return _physicalMax ;
}
if ( coord < WorldMin )
{
return _physicalMin ;
}
}
else
{
if ( coord < WorldMax )
{
return _physicalMax ;
}
if ( coord > WorldMin )
{
return _physicalMin ;
}
}
}
// (3) we are inside range or don't want to clip.
double range = WorldMax - WorldMin ;
double prop = (( coord - WorldMin )/ range );
// Force clipping at bounding box largeClip times that of real bounding box
// anyway. This is effectively at infinity.
const double largeClip = 100.0 ;
if ( prop > largeClip && clip )
prop = largeClip ;
if ( prop < - largeClip && clip )
prop = - largeClip ;
if ( range == 0 )
{
if ( coord >= WorldMin )
prop = largeClip ;
if ( coord < WorldMin )
prop = - largeClip ;
}
// calculate the physical coordinate.
PointF offset = new PointF (
( float ) ( prop *( _physicalMax . X - _physicalMin . X )),
( float ) ( prop *( _physicalMax . Y - _physicalMin . Y )));
return new PointF (( _physicalMin . X + offset . X ), ( _physicalMin . Y + offset . Y ));
}
/// <summary>
/// Return the world coordinate of the projection of the point p onto
/// the axis.
/// </summary>
/// <param name="p">The point to project onto the axis</param>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="clip">If true, the world value will be clipped to WorldMin or WorldMax as appropriate if it lies outside this range.</param>
/// <returns>The world value corresponding to the projection of the point p onto the axis.</returns>
public virtual double PhysicalToWorld (
PointF p ,
PointF physicalMin ,
PointF physicalMax ,
bool clip )
{
// (1) account for reversed axis. Could be tricky and move
// this out, but would be a little messy.
PointF _physicalMin ;
PointF _physicalMax ;
if ( Reversed )
{
_physicalMin = physicalMax ;
_physicalMax = physicalMin ;
}
else
{
_physicalMin = physicalMin ;
_physicalMax = physicalMax ;
}
// normalised axis dir vector
float axis_X = _physicalMax . X - _physicalMin . X ;
float axis_Y = _physicalMax . Y - _physicalMin . Y ;
float len = ( float ) Math . Sqrt ( axis_X * axis_X + axis_Y * axis_Y );
axis_X /= len ;
axis_Y /= len ;
// point relative to axis physical minimum.
PointF posRel = new PointF ( p . X - _physicalMin . X , p . Y - _physicalMin . Y );
// dist of point projection on axis, normalised.
float prop = ( axis_X * posRel . X + axis_Y * posRel . Y )/ len ;
double world = prop *( WorldMax - WorldMin ) + WorldMin ;
// if want clipped value, return extrema if outside range.
if ( clip )
{
world = Math . Max ( world , WorldMin );
world = Math . Min ( world , WorldMax );
}
return world ;
}
/// <summary>
/// Draw the Axis Label
/// </summary>
/// <param name="g">The GDI+ drawing surface on which to draw.</param>
/// <param name="offset">offset from axis. Should be calculated so as to make sure axis label misses tick labels.</param>
/// <param name="axisPhysicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="axisPhysicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <returns>boxed Rectangle indicating bounding box of label. null if no label printed.</returns>
public object DrawLabel (
Graphics g ,
Point offset ,
Point axisPhysicalMin ,
Point axisPhysicalMax )
{
if ( Label != "" )
{
// first calculate any extra offset for axis label spacing.
float extraOffsetAmount = LabelOffset ;
extraOffsetAmount += 2.0f ; // empirically determed - text was too close to axis before this.
if ( AutoScaleText )
{
if ( LabelOffsetScaled )
{
extraOffsetAmount *= FontScale ;
}
}
// now extend offset.
float offsetLength = ( float ) Math . Sqrt ( offset . X * offset . X + offset . Y * offset . Y );
if ( offsetLength > 0.01 )
{
float x_component = offset . X / offsetLength ;
float y_component = offset . Y / offsetLength ;
x_component *= extraOffsetAmount ;
y_component *= extraOffsetAmount ;
if ( LabelOffsetAbsolute )
{
offset . X = ( int ) x_component ;
offset . Y = ( int ) y_component ;
}
else
{
offset . X += ( int ) x_component ;
offset . Y += ( int ) y_component ;
}
}
// determine angle of axis in degrees
double theta = Math . Atan2 (
axisPhysicalMax . Y - axisPhysicalMin . Y ,
axisPhysicalMax . X - axisPhysicalMin . X );
theta = theta * 180.0f / Math . PI ;
PointF average = new PointF (
( axisPhysicalMax . X + axisPhysicalMin . X )/ 2.0f ,
( axisPhysicalMax . Y + axisPhysicalMin . Y )/ 2.0f );
g . TranslateTransform ( offset . X , offset . Y ); // this is done last.
g . TranslateTransform ( average . X , average . Y );
g . RotateTransform (( float ) theta ); // this is done first.
SizeF labelSize = g . MeasureString ( Label , labelFontScaled_ );
//bounding box for label centered around zero.
RectangleF drawRect = new RectangleF (
- labelSize . Width / 2.0f ,
- labelSize . Height / 2.0f ,
labelSize . Width ,
labelSize . Height );
g . DrawString (
Label ,
labelFontScaled_ ,
labelBrush_ ,
drawRect ,
drawFormat_ );
// now work out physical bounds of label.
Matrix m = g . Transform ;
PointF [] recPoints = new PointF [ 2 ];
recPoints [ 0 ] = new PointF (- labelSize . Width / 2.0f , - labelSize . Height / 2.0f );
recPoints [ 1 ] = new PointF ( labelSize . Width / 2.0f , labelSize . Height / 2.0f );
m . TransformPoints ( recPoints );
int x1 = ( int ) Math . Min ( recPoints [ 0 ]. X , recPoints [ 1 ]. X );
int x2 = ( int ) Math . Max ( recPoints [ 0 ]. X , recPoints [ 1 ]. X );
int y1 = ( int ) Math . Min ( recPoints [ 0 ]. Y , recPoints [ 1 ]. Y );
int y2 = ( int ) Math . Max ( recPoints [ 0 ]. Y , recPoints [ 1 ]. Y );
g . ResetTransform ();
// and return label bounding box.
return new Rectangle ( x1 , y1 , ( x2 - x1 ), ( y2 - y1 ));
}
return null ;
}
/// <summary>
/// Draw a tick on the axis.
/// </summary>
/// <param name="g">The graphics surface on which to draw.</param>
/// <param name="w">The tick position in world coordinates.</param>
/// <param name="size">The size of the tick (in pixels)</param>
/// <param name="text">The text associated with the tick</param>
/// <param name="textOffset">The Offset to draw from the auto calculated position</param>
/// <param name="axisPhysMin">The minimum physical extent of the axis</param>
/// <param name="axisPhysMax">The maximum physical extent of the axis</param>
/// <param name="boundingBox">out: The bounding rectangle for the tick and tickLabel drawn</param>
/// <param name="labelOffset">out: offset from the axies required for axis label</param>
public virtual void DrawTick (
Graphics g ,
double w ,
float size ,
string text ,
Point textOffset ,
Point axisPhysMin ,
Point axisPhysMax ,
out Point labelOffset ,
out Rectangle boundingBox )
{
// determine physical location where tick touches axis.
PointF tickStart = WorldToPhysical ( w , axisPhysMin , axisPhysMax , true );
// determine offset from start point.
PointF axisDir = Utils . UnitVector ( axisPhysMin , axisPhysMax );
// rotate axis dir clockwise by angle radians to get tick direction.
float x1 = ( float ) ( Math . Cos (- TicksAngle )* axisDir . X + Math . Sin (- TicksAngle )* axisDir . Y );
float y1 = ( float ) (- Math . Sin (- TicksAngle )* axisDir . X + Math . Cos (- TicksAngle )* axisDir . Y );
// now get the scaled tick vector.
PointF tickVector = new PointF ( TickScale * size * x1 , TickScale * size * y1 );
if ( TicksCrossAxis )
{
tickStart = new PointF (
tickStart . X - tickVector . X / 2.0f ,
tickStart . Y - tickVector . Y / 2.0f );
}
// and the end point [point off axis] of tick mark.
PointF tickEnd = new PointF ( tickStart . X + tickVector . X , tickStart . Y + tickVector . Y );
// and draw it!
if ( g != null )
g . DrawLine ( linePen_ , ( int ) tickStart . X , ( int ) tickStart . Y , ( int ) tickEnd . X , ( int ) tickEnd . Y );
// note: casting to int for tick positions was necessary to ensure ticks drawn where we wanted
// them. Not sure of the reason.
// calculate bounds of tick.
int minX = ( int ) Math . Min ( tickStart . X , tickEnd . X );
int minY = ( int ) Math . Min ( tickStart . Y , tickEnd . Y );
int maxX = ( int ) Math . Max ( tickStart . X , tickEnd . X );
int maxY = ( int ) Math . Max ( tickStart . Y , tickEnd . Y );
boundingBox = new Rectangle ( minX , minY , maxX - minX , maxY - minY );
// by default, label offset from axis is 0. TODO: revise this.
labelOffset = new Point (
-( int ) tickVector . X ,
-( int ) tickVector . Y );
// ------------------------
// now draw associated text.
// **** TODO ****
// The following code needs revising. A few things are hard coded when
// they should not be. Also, angled tick text currently just works for
// the bottom x-axis. Also, it's a bit hacky.
if ( text != "" && ! HideTickText && g != null )
{
SizeF textSize = g . MeasureString ( text , tickTextFontScaled_ );
// determine the center point of the tick text.
float textCenterX ;
float textCenterY ;
// if text is at pointy end of tick.
if (! TickTextNextToAxis )
{
// offset due to tick.
textCenterX = tickStart . X + tickVector . X * 1.2f ;
textCenterY = tickStart . Y + tickVector . Y * 1.2f ;
// offset due to text box size.
textCenterX += 0.5f * x1 * textSize . Width ;
textCenterY += 0.5f * y1 * textSize . Height ;
}
// else it's next to the axis.
else
{
// start location.
textCenterX = tickStart . X ;
textCenterY = tickStart . Y ;
// offset due to text box size.
textCenterX -= 0.5f * x1 * textSize . Width ;
textCenterY -= 0.5f * y1 * textSize . Height ;
// bring text away from the axis a little bit.
textCenterX -= x1 *( 2.0f + FontScale );
textCenterY -= y1 *( 2.0f + FontScale );
}
// If tick text is angled..
if ( TicksLabelAngle != 0.0f )
{
// determine the point we want to rotate text about.
PointF textScaledTickVector = new PointF ( TickScale * x1 *( textSize . Height / 2.0f ), TickScale * y1 *( textSize . Height / 2.0f ));
PointF rotatePoint ;
if ( TickTextNextToAxis )
{
rotatePoint = new PointF ( tickStart . X - textScaledTickVector . X , tickStart . Y - textScaledTickVector . Y );
}
else
{
rotatePoint = new PointF ( tickEnd . X + textScaledTickVector . X , tickEnd . Y + textScaledTickVector . Y );
}
float actualAngle ;
if ( flipTicksLabel_ )
{
double radAngle = ( Math . PI / 180 )* TicksLabelAngle ;
rotatePoint . X += textSize . Width *( float ) Math . Cos ( radAngle );
rotatePoint . Y += textSize . Width *( float ) Math . Sin ( radAngle );
actualAngle = TicksLabelAngle + 180 ;
}
else
{
actualAngle = TicksLabelAngle ;
}
g . TranslateTransform ( rotatePoint . X , rotatePoint . Y );
g . RotateTransform ( actualAngle );
Matrix m = g . Transform ;
PointF [] recPoints = new PointF [ 2 ];
recPoints [ 0 ] = new PointF ( 0.0f , -( textSize . Height / 2 ));
recPoints [ 1 ] = new PointF ( textSize . Width , textSize . Height );
m . TransformPoints ( recPoints );
float t_x1 = Math . Min ( recPoints [ 0 ]. X , recPoints [ 1 ]. X );
float t_x2 = Math . Max ( recPoints [ 0 ]. X , recPoints [ 1 ]. X );
float t_y1 = Math . Min ( recPoints [ 0 ]. Y , recPoints [ 1 ]. Y );
float t_y2 = Math . Max ( recPoints [ 0 ]. Y , recPoints [ 1 ]. Y );
boundingBox = Rectangle . Union ( boundingBox , new Rectangle (( int ) t_x1 , ( int ) t_y1 , ( int ) ( t_x2 - t_x1 ), ( int ) ( t_y2 - t_y1 )));
RectangleF drawRect = new RectangleF ( 0.0f , -( textSize . Height / 2 ), textSize . Width , textSize . Height );
g . DrawString (
text ,
tickTextFontScaled_ ,
tickTextBrush_ ,
drawRect ,
drawFormat_ );
t_x2 -= tickStart . X ;
t_y2 -= tickStart . Y ;
t_x2 *= 1.25f ;
t_y2 *= 1.25f ;
labelOffset = new Point (( int ) t_x2 , ( int ) t_y2 );
g . ResetTransform ();
//g.DrawRectangle( new Pen(Color.Purple), boundingBox.X, boundingBox.Y, boundingBox.Width, boundingBox.Height );
}
else
{
float bx1 = ( textCenterX - textSize . Width / 2.0f );
float by1 = ( textCenterY - textSize . Height / 2.0f );
float bx2 = textSize . Width ;
float by2 = textSize . Height ;
RectangleF drawRect = new RectangleF ( bx1 , by1 , bx2 , by2 );
Rectangle drawRect_int = new Rectangle (( int ) bx1 , ( int ) by1 , ( int ) bx2 , ( int ) by2 );
// g.DrawRectangle( new Pen(Color.Green), bx1, by1, bx2, by2 );
boundingBox = Rectangle . Union ( boundingBox , drawRect_int );
// g.DrawRectangle( new Pen(Color.Purple), boundingBox.X, boundingBox.Y, boundingBox.Width, boundingBox.Height );
g . DrawString (
text ,
tickTextFontScaled_ ,
tickTextBrush_ ,
drawRect ,
drawFormat_ );
textCenterX -= tickStart . X ;
textCenterY -= tickStart . Y ;
textCenterX *= 2.3f ;
textCenterY *= 2.3f ;
labelOffset = new Point (( int ) textCenterX , ( int ) textCenterY );
}
}
}
/// <summary>
/// Draw the axis. This involves three steps:
/// (1) Draw the axis line.
/// (2) Draw the tick marks.
/// (3) Draw the label.
/// </summary>
/// <param name="g">The drawing surface on which to draw.</param>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="boundingBox">out The bounding rectangle of the axis including axis line, label, tick marks and tick mark labels</param>
public virtual void Draw (
Graphics g ,
Point physicalMin ,
Point physicalMax ,
out Rectangle boundingBox )
{
// calculate the bounds of the axis line only.
int x1 = Math . Min ( physicalMin . X , physicalMax . X );
int x2 = Math . Max ( physicalMin . X , physicalMax . X );
int y1 = Math . Min ( physicalMin . Y , physicalMax . Y );
int y2 = Math . Max ( physicalMin . Y , physicalMax . Y );
Rectangle bounds = new Rectangle ( x1 , y1 , x2 - x1 , y2 - y1 );
if (! Hidden )
{
// (1) Draw the axis line.
g . DrawLine ( linePen_ , physicalMin . X , physicalMin . Y , physicalMax . X , physicalMax . Y );
// (2) draw tick marks (subclass responsibility).
object labelOffset ;
object tickBounds ;
DrawTicks ( g , physicalMin , physicalMax , out labelOffset , out tickBounds );
// (3) draw the axis label
object labelBounds = null ;
if (! HideTickText )
{
labelBounds = DrawLabel ( g , ( Point ) labelOffset , physicalMin , physicalMax );
}
// (4) merge bounds and return.
if ( labelBounds != null )
bounds = Rectangle . Union ( bounds , ( Rectangle ) labelBounds );
if ( tickBounds != null )
bounds = Rectangle . Union ( bounds , ( Rectangle ) tickBounds );
}
boundingBox = bounds ;
}
/// <summary>
/// Update the bounding box and label offset associated with an axis
/// to encompass the additionally specified mergeBoundingBox and
/// mergeLabelOffset respectively.
/// </summary>
/// <param name="labelOffset">Current axis label offset.</param>
/// <param name="boundingBox">Current axis bounding box.</param>
/// <param name="mergeLabelOffset">the label offset to merge. The current label offset will be replaced by this if it's norm is larger.</param>
/// <param name="mergeBoundingBox">the bounding box to merge. The current bounding box will be replaced by this if null, or by the least upper bound of bother bounding boxes otherwise.</param>
protected static void UpdateOffsetAndBounds (
ref object labelOffset , ref object boundingBox ,
Point mergeLabelOffset , Rectangle mergeBoundingBox )
{
// determining largest label offset and use it.
Point lo = ( Point ) labelOffset ;
double norm1 = Math . Sqrt ( lo . X * lo . X + lo . Y * lo . Y );
double norm2 = Math . Sqrt ( mergeLabelOffset . X * mergeLabelOffset . X + mergeLabelOffset . Y * mergeLabelOffset . Y );
if ( norm1 < norm2 )
{
labelOffset = mergeLabelOffset ;
}
// determining bounding box.
Rectangle b = mergeBoundingBox ;
if ( boundingBox == null )
{
boundingBox = b ;
}
else
{
boundingBox = Rectangle . Union (( Rectangle ) boundingBox , b );
}
}
/// <summary>
/// DrawTicks method. In base axis class this does nothing.
/// </summary>
/// <param name="g">The graphics surface on which to draw</param>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="labelOffset">is set to a suitable offset from the axis to draw the axis label. In this base method, set to null.</param>
/// <param name="boundingBox">is set to the smallest box that bounds the ticks and the tick text. In this base method, set to null.</param>
protected virtual void DrawTicks (
Graphics g ,
Point physicalMin ,
Point physicalMax ,
out object labelOffset ,
out object boundingBox )
{
labelOffset = null ;
boundingBox = null ;
// do nothing. This class is not abstract because a subclass may
// want to override the Axis.Draw method to one that doesn't
// require DrawTicks.
}
/// <summary>
/// Determines the positions, in world coordinates, of the large ticks.
/// When the physical extent of the axis is small, some of the positions
/// that were generated in this pass may be converted to small tick
/// positions and returned as well.
/// This default implementation returns empty large ticks list and null
/// small tick list.
/// </summary>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
/// <param name="smallTickPositions">ArrayList containing the positions of the small ticks if calculated, null otherwise.</param>
internal virtual void WorldTickPositions_FirstPass (
Point physicalMin ,
Point physicalMax ,
out ArrayList largeTickPositions ,
out ArrayList smallTickPositions
)
{
largeTickPositions = new ArrayList ();
smallTickPositions = null ;
}
/// <summary>
/// Determines the positions, in world coordinates, of the small ticks
/// if they have not already been generated.
/// This default implementation creates an empty smallTickPositions list
/// if it doesn't already exist.
/// </summary>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="largeTickPositions">The positions of the large ticks.</param>
/// <param name="smallTickPositions">If null, small tick positions are returned via this parameter. Otherwise this function does nothing.</param>
internal virtual void WorldTickPositions_SecondPass (
Point physicalMin ,
Point physicalMax ,
ArrayList largeTickPositions ,
ref ArrayList smallTickPositions )
{
if ( smallTickPositions == null )
smallTickPositions = new ArrayList ();
}
/// <summary>
/// Determines the positions of all Large and Small ticks.
/// </summary>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
/// <param name="smallTickPositions">ArrayList containing the positions of the small ticks.</param>
public void WorldTickPositions (
Point physicalMin ,
Point physicalMax ,
out ArrayList largeTickPositions ,
out ArrayList smallTickPositions
)
{
WorldTickPositions_FirstPass ( physicalMin , physicalMax , out largeTickPositions , out smallTickPositions );
WorldTickPositions_SecondPass ( physicalMin , physicalMax , largeTickPositions , ref smallTickPositions );
}
/// <summary>
/// Moves the world min and max values so that the world axis
/// length is [percent] bigger. If the current world
/// max and min values are the same, they are moved appart
/// an arbitrary amount. This arbitrary amount is currently
/// 0.01, and will probably be configurable in the future.
/// </summary>
/// <param name="percent">Percentage to increase world length by.</param>
/// <remarks>Works for the case WorldMax is less than WorldMin.</remarks>
public void IncreaseRange ( double percent )
{
double range = WorldMax - WorldMin ;
if (! Utils . DoubleEqual ( range , 0.0 ))
{
range *= percent ;
}
else
{
// arbitrary number.
// TODO make this configurable.
range = 0.01 ;
}
WorldMax += range ;
WorldMin -= range ;
}
private void UpdateScale ()
{
if ( labelFont_ != null )
labelFontScaled_ = Utils . ScaleFont ( labelFont_ , FontScale );
if ( tickTextFont_ != null )
tickTextFontScaled_ = Utils . ScaleFont ( tickTextFont_ , FontScale );
}
/// <summary>
/// returns a suitable offset for the axis label in the case that there are no
/// ticks or tick text in the way.
/// </summary>
/// <param name="physicalMin">physical point corresponding to the axis world maximum.</param>
/// <param name="physicalMax">physical point corresponding to the axis world minimum.</param>
/// <returns>axis label offset</returns>
protected Point getDefaultLabelOffset ( Point physicalMin , Point physicalMax )
{
Rectangle tBoundingBox ;
Point tLabelOffset ;
DrawTick ( null , WorldMax , LargeTickSize ,
"" ,
new Point ( 0 , 0 ),
physicalMin , physicalMax ,
out tLabelOffset , out tBoundingBox );
return tLabelOffset ;
}
/*
// finish implementation of this at some point.
public class WorldValueChangedArgs
{
public WorldValueChangedArgs( double value, MinMaxType minOrMax )
{
Value = value;
MinOrMax = minOrMax;
}
public double Value;
public enum MinMaxType
{
Min = 0,
Max = 1
}
public MinMaxType MinOrMax;
}
public delegate void WorldValueChangedHandler( object sender, WorldValueChangedArgs e );
public event WorldValueChangedHandler WorldMinChanged;
public event WorldValueChangedHandler WorldMaxChanged;
public event WorldValueChangedHandler WorldExtentsChanged;
*/
}
2016-03-05 15:25:01 +01:00
}