/*
* 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
{
///
/// 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].
/// This class encapsulates no physical information about where the axes
/// are drawn.
///
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_;
///
/// Default constructor
///
public Axis()
{
Init();
}
///
/// Constructor that takes only world min and max values.
///
/// The minimum world coordinate.
/// The maximum world coordinate.
public Axis(double worldMin, double worldMax)
{
Init();
WorldMin = worldMin;
WorldMax = worldMax;
}
///
/// Copy constructor.
///
/// The Axis to clone.
public Axis(Axis a)
{
DoClone(a, this);
}
///
/// 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.
///
public bool TicksCrossAxis
{
get { return ticksCrossAxis_; }
set { ticksCrossAxis_ = value; }
}
///
/// 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.
///
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));
*/
}
}
///
/// 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.
///
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) );
*/
}
}
///
/// Length (in pixels) of a large tick. Not the distance
/// between large ticks. The length of the tick itself.
///
public int LargeTickSize
{
get { return largeTickSize_; }
set { largeTickSize_ = value; }
}
///
/// Length (in pixels) of the small ticks.
///
public int SmallTickSize
{
get { return smallTickSize_; }
set { smallTickSize_ = value; }
}
///
/// The Axis Label
///
public string Label
{
get { return label_; }
set { label_ = value; }
}
///
/// 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].
///
public bool TickTextNextToAxis
{
get { return tickTextNextToAxis_; }
set { tickTextNextToAxis_ = value; }
}
///
/// If set to true, the axis is hidden. That is, the axis line, ticks, tick
/// labels and axis label will not be drawn.
///
public bool Hidden
{
get { return hidden_; }
set { hidden_ = value; }
}
///
/// If set true, the axis will behave as though the WorldMin and WorldMax values
/// have been swapped.
///
public bool Reversed
{
get { return reversed_; }
set { reversed_ = value; }
}
///
/// If true, no text will be drawn next to any axis tick marks.
///
public bool HideTickText
{
get { return hideTickText_; }
set { hideTickText_ = value; }
}
///
/// This font is used for the drawing of text next to the axis tick marks.
///
public Font TickTextFont
{
get { return tickTextFont_; }
set
{
tickTextFont_ = value;
UpdateScale();
}
}
///
/// This font is used to draw the axis label.
///
public Font LabelFont
{
get { return labelFont_; }
set
{
labelFont_ = value;
UpdateScale();
}
}
///
/// Specifies the format used for drawing tick labels. See
/// StringBuilder.AppendFormat for a description of this
/// string.
///
public string NumberFormat
{
get { return numberFormat_; }
set { numberFormat_ = value; }
}
///
/// If LargeTickStep isn't specified, then this will be calculated
/// automatically. The calculated value will not be less than this
/// amount.
///
public int MinPhysicalLargeTickStep
{
get { return minPhysicalLargeTickStep_; }
set { minPhysicalLargeTickStep_ = value; }
}
///
/// The color of the pen used to draw the ticks and the axis line.
///
public Color AxisColor
{
get { return linePen_.Color; }
set { linePen_ = new Pen(value); }
}
///
/// The pen used to draw the ticks and the axis line.
///
public Pen AxisPen
{
get { return linePen_; }
set { linePen_ = value; }
}
///
/// 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.
///
public bool TicksIndependentOfPhysicalExtent
{
get { return ticksIndependentOfPhysicalExtent_; }
set { ticksIndependentOfPhysicalExtent_ = value; }
}
///
/// If true label is flipped about the text center line parallel to the text.
///
public bool FlipTicksLabel
{
get { return flipTicksLabel_; }
set { flipTicksLabel_ = value; }
}
///
/// Angle to draw ticks at (measured anti-clockwise from axis direction).
///
public float TicksAngle
{
get { return ticksAngle_; }
set { ticksAngle_ = value; }
}
///
/// Angle to draw large tick labels at (clockwise from horizontal). Note:
/// this is currently only implemented well for the lower x-axis.
///
public float TicksLabelAngle
{
get { return ticksLabelAngle_; }
set { ticksLabelAngle_ = value; }
}
///
/// The color of the brush used to draw the axis label.
///
public Color LabelColor
{
set { labelBrush_ = new SolidBrush(value); }
}
///
/// The brush used to draw the axis label.
///
public Brush LabelBrush
{
get { return labelBrush_; }
set { labelBrush_ = value; }
}
///
/// The color of the brush used to draw the axis tick labels.
///
public Color TickTextColor
{
set { tickTextBrush_ = new SolidBrush(value); }
}
///
/// The brush used to draw the tick text.
///
public Brush TickTextBrush
{
get { return tickTextBrush_; }
set { tickTextBrush_ = value; }
}
///
/// If true, label and tick text will be scaled to match size
/// of PlotSurface2D. If false, they won't be.
///
/// Could also be argued this belongs in PlotSurface2D
public bool AutoScaleText
{
get { return autoScaleText_; }
set { autoScaleText_ = value; }
}
///
/// If true, tick lengths will be scaled to match size
/// of PlotSurface2D. If false, they won't be.
///
/// Could also be argued this belongs in PlotSurface2D
public bool AutoScaleTicks
{
get { return autoScaleTicks_; }
set { autoScaleTicks_ = value; }
}
///
/// World extent of the axis.
///
public double WorldLength
{
get { return Math.Abs(worldMax_ - worldMin_); }
}
///
/// Scale label and tick fonts by this factor. Set by PlotSurface2D
/// Draw method.
///
internal float FontScale
{
get { return fontScale_; }
set
{
fontScale_ = value;
UpdateScale();
}
}
///
/// Scale tick mark lengths by this factor. Set by PlotSurface2D
/// Draw method.
///
internal float TickScale { get; set; }
///
/// Get whether or not this axis is linear.
///
public virtual bool IsLinear
{
get { return true; }
}
///
/// 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.
///
public float LabelOffset
{
get { return labelOffset_; }
set { labelOffset_ = value; }
}
///
/// 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.
///
///
public bool LabelOffsetAbsolute
{
get { return labelOffsetAbsolute_; }
set { labelOffsetAbsolute_ = value; }
}
///
/// Whether or not the supplied LabelOffset should be scaled by
/// a factor as specified by FontScale.
///
public bool LabelOffsetScaled
{
get { return labelOffsetScaled_; }
set { labelOffsetScaled_ = value; }
}
///
/// Set the Axis color (sets all of axis line color, Tick text color, and label color).
///
public Color Color
{
set
{
AxisColor = value;
TickTextColor = value;
LabelColor = value;
}
}
///
/// Deep copy of Axis.
///
///
/// 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.
///
/// A copy of the Axis Class
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;
}
///
/// 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.
///
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;
}
///
/// Helper function for constructors.
/// Do initialization here so that Clear() method is handled properly
///
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;
}
///
/// Determines whether a world value is outside range WorldMin -> WorldMax
///
/// the world value to test
/// true if outside limits, false otherwise
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;
}
}
///
/// 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
///
/// The other Axis instance.
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;
}
}
}
}
///
/// World to physical coordinate transform.
///
/// The coordinate value to transform.
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// 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.
/// The transformed coordinates.
///
/// 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).
///
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));
}
///
/// Return the world coordinate of the projection of the point p onto
/// the axis.
///
/// The point to project onto the axis
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// If true, the world value will be clipped to WorldMin or WorldMax as appropriate if it lies outside this range.
/// The world value corresponding to the projection of the point p onto the axis.
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;
}
///
/// Draw the Axis Label
///
/// The GDI+ drawing surface on which to draw.
/// offset from axis. Should be calculated so as to make sure axis label misses tick labels.
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// boxed Rectangle indicating bounding box of label. null if no label printed.
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;
}
///
/// Draw a tick on the axis.
///
/// The graphics surface on which to draw.
/// The tick position in world coordinates.
/// The size of the tick (in pixels)
/// The text associated with the tick
/// The Offset to draw from the auto calculated position
/// The minimum physical extent of the axis
/// The maximum physical extent of the axis
/// out: The bounding rectangle for the tick and tickLabel drawn
/// out: offset from the axies required for axis label
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);
}
}
}
///
/// Draw the axis. This involves three steps:
/// (1) Draw the axis line.
/// (2) Draw the tick marks.
/// (3) Draw the label.
///
/// The drawing surface on which to draw.
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// out The bounding rectangle of the axis including axis line, label, tick marks and tick mark labels
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;
}
///
/// Update the bounding box and label offset associated with an axis
/// to encompass the additionally specified mergeBoundingBox and
/// mergeLabelOffset respectively.
///
/// Current axis label offset.
/// Current axis bounding box.
/// the label offset to merge. The current label offset will be replaced by this if it's norm is larger.
/// 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.
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);
}
}
///
/// DrawTicks method. In base axis class this does nothing.
///
/// The graphics surface on which to draw
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// is set to a suitable offset from the axis to draw the axis label. In this base method, set to null.
/// is set to the smallest box that bounds the ticks and the tick text. In this base method, set to null.
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.
}
///
/// 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.
///
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// ArrayList containing the positions of the large ticks.
/// ArrayList containing the positions of the small ticks if calculated, null otherwise.
internal virtual void WorldTickPositions_FirstPass(
Point physicalMin,
Point physicalMax,
out ArrayList largeTickPositions,
out ArrayList smallTickPositions
)
{
largeTickPositions = new ArrayList();
smallTickPositions = null;
}
///
/// 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.
///
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// The positions of the large ticks.
/// If null, small tick positions are returned via this parameter. Otherwise this function does nothing.
internal virtual void WorldTickPositions_SecondPass(
Point physicalMin,
Point physicalMax,
ArrayList largeTickPositions,
ref ArrayList smallTickPositions)
{
if (smallTickPositions == null)
smallTickPositions = new ArrayList();
}
///
/// Determines the positions of all Large and Small ticks.
///
/// The physical position corresponding to the world minimum of the axis.
/// The physical position corresponding to the world maximum of the axis.
/// ArrayList containing the positions of the large ticks.
/// ArrayList containing the positions of the small ticks.
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);
}
///
/// 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.
///
/// Percentage to increase world length by.
/// Works for the case WorldMax is less than WorldMin.
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);
}
///
/// returns a suitable offset for the axis label in the case that there are no
/// ticks or tick text in the way.
///
/// physical point corresponding to the axis world maximum.
/// physical point corresponding to the axis world minimum.
/// axis label offset
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;
*/
}
}