mirror of https://github.com/mhowlett/nplot.git
1382 lines
52 KiB
C#
1382 lines
52 KiB
C#
/*
|
|
* 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.
|
|
*
|
|
* 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;
|
|
|
|
*/
|
|
}
|
|
} |