nplot-gtk/lib/LogAxis.cs

679 строки
20 KiB
C#

/*
NPlot - A charting library for .NET
LogAxis.cs
Copyright (C) 2003
Paolo Pierini, Matt Howlett
Redistribution and use of NPlot or parts there-of in source and
binary forms, with or without modification, are permitted provided
that the following conditions are met:
1. Re-distributions in source form must retain at the head of each
source file the above copyright notice, this list of conditions
and the following disclaimer.
2. Any product ("the product") that makes use NPlot or parts
there-of must either:
(a) allow any user of the product to obtain a complete machine-
readable copy of the corresponding source code for the
product and the version of NPlot used for a charge no more
than your cost of physically performing source distribution,
on a medium customarily used for software interchange, or:
(b) reproduce the following text in the documentation, about
box or other materials intended to be read by human users
of the product that is provided to every human user of the
product:
"This product includes software developed as
part of the NPlot library project available
from: http://www.nplot.com/"
The words "This product" may optionally be replace with
the actual name of the product.
------------------------------------------------------------------------
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.Drawing;
using System.Collections;
using System.Text;
namespace NPlot
{
/// <summary>
/// The class implementing logarithmic axes.
/// </summary>
public class LogAxis : Axis
{
/// <summary>
/// Deep Copy of the LogAxis.
/// </summary>
/// <returns>A Copy of the LogAxis Class.</returns>
public override object Clone()
{
LogAxis a = new LogAxis();
if (this.GetType() != a.GetType())
{
throw new NPlotException("Clone not defined in derived type. Help!");
}
this.DoClone( this, a );
return a;
}
/// <summary>
/// Helper method for Clone (actual implementation)
/// </summary>
/// <param name="a">The original object to clone.</param>
/// <param name="b">The cloned object.</param>
protected void DoClone(LogAxis b, LogAxis a)
{
Axis.DoClone(b,a);
// add specific elemtents of the class for the deep copy of the object
a.numberSmallTicks_ = b.numberSmallTicks_;
a.largeTickValue_ = b.largeTickValue_;
a.largeTickStep_ = b.largeTickStep_;
}
/// <summary>
/// Default constructor.
/// </summary>
public LogAxis()
: base()
{
Init();
}
/// <summary>
/// Copy Constructor
/// </summary>
/// <param name="a">The Axis to clone.</param>
public LogAxis(Axis a)
: base(a)
{
Init();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="worldMin">Minimum World value for the axis.</param>
/// <param name="worldMax">Maximum World value for the axis.</param>
public LogAxis(double worldMin, double worldMax)
: base( worldMin, worldMax )
{
Init();
}
private void Init()
{
this.NumberFormat = "{0:g5}";
}
/// <summary>
/// Draw the ticks.
/// </summary>
/// <param name="g">The drawing surface on which to draw.</param>
/// <param name="physicalMin">The minimum physical extent of the axis.</param>
/// <param name="physicalMax">The maximum physical extent of the axis.</param>
/// <param name="boundingBox">out: smallest box that completely encompasses all of the ticks and tick labels.</param>
/// <param name="labelOffset">out: a suitable offset from the axis to draw the axis label.</param>
/// <returns> An ArrayList containing the offset from the axis required for an axis label
/// to miss this tick, followed by a bounding rectangle for the tick and tickLabel drawn.</returns>
protected override void DrawTicks(
Graphics g,
Point physicalMin,
Point physicalMax,
out object labelOffset,
out object boundingBox )
{
Point tLabelOffset;
Rectangle tBoundingBox;
labelOffset = this.getDefaultLabelOffset( physicalMin, physicalMax );
boundingBox = null;
ArrayList largeTickPositions;
ArrayList smallTickPositions;
this.WorldTickPositions( physicalMin, physicalMax, out largeTickPositions, out smallTickPositions );
Point offset = new Point( 0, 0 );
object bb = null;
// Missed this protection
if (largeTickPositions.Count > 0)
{
for (int i=0; i<largeTickPositions.Count; ++i)
{
StringBuilder label = new StringBuilder();
// do google search for "format specifier writeline" for help on this.
label.AppendFormat(this.NumberFormat, (double)largeTickPositions[i]);
this.DrawTick( g, (double)largeTickPositions[i], this.LargeTickSize, label.ToString(),
new Point(0,0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );
Axis.UpdateOffsetAndBounds( ref labelOffset, ref boundingBox, tLabelOffset, tBoundingBox );
}
}
else
{
// just get the axis bounding box)
PointF dir = Utils.UnitVector(physicalMin,physicalMax);
Rectangle rr = new Rectangle( physicalMin.X,
(int)((physicalMax.X-physicalMin.X)*dir.X),
physicalMin.Y,
(int)((physicalMax.Y-physicalMin.Y)*dir.Y) );
bb = rr;
}
// missed protection for zero ticks
if (smallTickPositions.Count > 0)
{
for (int i=0; i<smallTickPositions.Count; ++i)
{
this.DrawTick( g, (double)smallTickPositions[i], this.SmallTickSize,
"", new Point(0,0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );
// ignore r for now - assume bb unchanged by small tick bounds.
}
}
}
/// <summary>
/// Determines the positions, in world coordinates, of the small ticks
/// if they have not already been generated.
/// </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, unchanged</param>
/// <param name="smallTickPositions">If null, small tick positions are returned via this parameter. Otherwise this function does nothing.</param>
internal override void WorldTickPositions_SecondPass(
Point physicalMin,
Point physicalMax,
ArrayList largeTickPositions,
ref ArrayList smallTickPositions )
{
if (smallTickPositions != null)
{
throw new NPlotException( "not expecting smallTickPositions to be set already." );
}
smallTickPositions = new ArrayList();
// retrieve the spacing of the big ticks. Remember this is decades!
double bigTickSpacing = this.DetermineTickSpacing();
int nSmall = this.DetermineNumberSmallTicks( bigTickSpacing );
// now we have to set the ticks
// let us start with the easy case where the major tick distance
// is larger than a decade
if ( bigTickSpacing > 1.0f )
{
if (largeTickPositions.Count > 0)
{
// deal with the smallticks preceding the
// first big tick
double pos1 = (double)largeTickPositions[0];
while (pos1 > this.WorldMin)
{
pos1 = pos1 / 10.0f;
smallTickPositions.Add( pos1 );
}
// now go on for all other Major ticks
for (int i=0; i<largeTickPositions.Count; ++i )
{
double pos = (double)largeTickPositions[i];
for (int j=1; j<=nSmall; ++j )
{
pos=pos*10.0F;
// check to see if we are still in the range
if (pos < WorldMax)
{
smallTickPositions.Add( pos );
}
}
}
}
}
else
{
// guess what...
double [] m = { 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f };
// Then we deal with the other ticks
if (largeTickPositions.Count > 0)
{
// first deal with the smallticks preceding the first big tick
// positioning before the first tick
double pos1=(double)largeTickPositions[0]/10.0f;
for (int i=0; i<m.Length; i++)
{
double pos=pos1*m[i];
if (pos>this.WorldMin)
{
smallTickPositions.Add(pos);
}
}
// now go on for all other Major ticks
for (int i=0; i<largeTickPositions.Count; ++i )
{
pos1=(double)largeTickPositions[i];
for (int j=0; j<m.Length; ++j )
{
double pos=pos1*m[j];
// check to see if we are still in the range
if (pos < WorldMax)
{
smallTickPositions.Add( pos );
}
}
}
}
else
{
// probably a minor tick would anyway fall in the range
// find the decade preceding the minimum
double dec=Math.Floor(Math.Log10(WorldMin));
double pos1=Math.Pow(10.0,dec);
for (int i=0; i<m.Length; i++)
{
double pos=pos1*m[i];
if (pos>this.WorldMin && pos< this.WorldMax )
{
smallTickPositions.Add(pos);
}
}
}
}
}
private static double m_d5Log = -Math.Log10(0.5); // .30103
private static double m_d5RegionPos = Math.Abs(m_d5Log + ((1 - m_d5Log) / 2)); // ' .6505
private static double m_d5RegionNeg = Math.Abs(m_d5Log / 2); // '.1505
private void CalcGrids( double dLenAxis, int nNumDivisions, ref double dDivisionInterval)
{
double dMyInterval = dLenAxis / nNumDivisions;
double dPower = Math.Log10(dMyInterval);
dDivisionInterval = 10 ^ (int)dPower;
double dFixPower = dPower - (int)dPower;
double d5Region = Math.Abs(dPower - dFixPower);
double dMyMult;
if (dPower < 0)
{
d5Region = -(dPower - dFixPower);
dMyMult = 0.5;
}
else
{
d5Region = 1 - (dPower - dFixPower);
dMyMult = 5;
}
if ((d5Region >= m_d5RegionNeg) && (d5Region <= m_d5RegionPos))
{
dDivisionInterval = dDivisionInterval * dMyMult;
}
}
/// <summary>
/// Determines the positions, in world coordinates, of the log spaced large 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">null</param>
internal override void WorldTickPositions_FirstPass(
Point physicalMin,
Point physicalMax,
out ArrayList largeTickPositions,
out ArrayList smallTickPositions
)
{
smallTickPositions = null;
largeTickPositions = new ArrayList();
if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
{
throw new NPlotException( "world extent of axis not set." );
}
double roundTickDist = this.DetermineTickSpacing( );
// now determine first tick position.
double first = 0.0f;
// if the user hasn't specified a large tick position.
if (double.IsNaN(largeTickValue_))
{
if( WorldMin > 0.0 )
{
double nToFirst = Math.Floor(Math.Log10(WorldMin) / roundTickDist)+1.0f;
first = nToFirst * roundTickDist;
}
// could miss one, if first is just below zero.
if (first-roundTickDist >= Math.Log10(WorldMin))
{
first -= roundTickDist;
}
}
// the user has specified one place they would like a large tick placed.
else
{
first = Math.Log10( this.LargeTickValue );
// TODO: check here not too much different.
// could result in long loop.
while (first < Math.Log10(WorldMin))
{
first += roundTickDist;
}
while (first > Math.Log10(WorldMin)+roundTickDist)
{
first -= roundTickDist;
}
}
double mark = first;
while (mark <= Math.Log10(WorldMax))
{
// up to here only logs are dealt with, but I want to return
// a real value in the arraylist
double val;
val = Math.Pow( 10.0, mark );
largeTickPositions.Add( val );
mark += roundTickDist;
}
}
/// <summary>
/// Determines the tick spacing.
/// </summary>
/// <returns>The tick spacing (in decades!)</returns>
private double DetermineTickSpacing( )
{
if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
{
throw new NPlotException( "world extent of axis is not set." );
}
// if largeTickStep has been set, it is used
if ( !double.IsNaN( this.largeTickStep_) )
{
if ( this.largeTickStep_ <= 0.0f )
{
throw new NPlotException( "can't have negative tick step - reverse WorldMin WorldMax instead." );
}
return this.largeTickStep_;
}
double MagRange = (double)(Math.Floor(Math.Log10(WorldMax)) - Math.Floor(Math.Log10(WorldMin))+1.0);
if ( MagRange > 0.0 )
{
// for now, a simple logic
// start with a major tick every order of magnitude, and
// increment if in order not to have more than 10 ticks in
// the plot.
double roundTickDist=1.0F;
int nticks=(int)(MagRange/roundTickDist);
while (nticks > 10)
{
roundTickDist++;
nticks=(int)(MagRange/roundTickDist);
}
return roundTickDist;
}
else
{
return 0.0f;
}
}
/// <summary>
/// Determines the number of small ticks between two large ticks.
/// </summary>
/// <param name="bigTickDist">The distance between two large ticks.</param>
/// <returns>The number of small ticks.</returns>
private int DetermineNumberSmallTicks( double bigTickDist )
{
// if the big ticks is more than one decade, the
// small ticks are every decade, I don't let the user set it.
if (this.numberSmallTicks_ != null && bigTickDist == 1.0f)
{
return (int)this.numberSmallTicks_+1;
}
// if we are plotting every decade, we have to
// put the log ticks. As a start, I put every
// small tick (.2,.3,.4,.5,.6,.7,.8,.9)
if (bigTickDist == 1.0f)
{
return 8;
}
// easy, put a tick every missed decade
else if (bigTickDist > 1.0f)
{
return (int)bigTickDist - 1;
}
else
{
throw new NPlotException("Wrong Major tick distance setting");
}
}
/// <summary>
/// The step between large ticks, expressed in decades for the Log scale.
/// </summary>
public double LargeTickStep
{
set
{
largeTickStep_ = value;
}
get
{
return largeTickStep_;
}
}
/// <summary>
/// Position of one of the large ticks [other positions will be calculated relative to this one].
/// </summary>
public double LargeTickValue
{
set
{
largeTickValue_ = value;
}
get
{
return largeTickValue_;
}
}
/// <summary>
/// The number of small ticks between large ticks.
/// </summary>
public int NumberSmallTicks
{
set
{
numberSmallTicks_ = value;
}
}
// Private members
private object numberSmallTicks_;
private double largeTickValue_ = double.NaN;
private double largeTickStep_ = double.NaN;
/// <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>TODO: make Reversed property work for this.</remarks>
public override PointF WorldToPhysical(
double coord,
PointF physicalMin,
PointF physicalMax,
bool clip )
{
// if want clipped value, return extrema if outside range.
if (clip)
{
if (coord > WorldMax)
{
return physicalMax;
}
if (coord < WorldMin)
{
return physicalMin;
}
}
if (coord < 0.0f)
{
throw new NPlotException( "Cannot have negative values for data using Log Axis" );
}
// inside range or don't want to clip.
double lrange = (double)(Math.Log10(WorldMax) - Math.Log10(WorldMin));
double prop = (double)((Math.Log10(coord) - Math.Log10(WorldMin)) / lrange);
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 override double PhysicalToWorld( PointF p, PointF physicalMin, PointF physicalMax, bool clip )
{
// use the base method to do the projection on the axis.
double t = base.PhysicalToWorld( p, physicalMin, physicalMax, clip );
// now reconstruct phys dist prop along this assuming linear scale as base method did.
double v = (t - this.WorldMin) / (this.WorldMax - this.WorldMin);
double ret = WorldMin*Math.Pow( WorldMax / WorldMin, v );
// if want clipped value, return extrema if outside range.
if (clip)
{
ret = Math.Max( ret, WorldMin );
ret = Math.Min( ret, WorldMax );
}
return ret;
}
/// <summary>
/// The minimum world extent of the axis. Must be greater than zero.
/// </summary>
public override double WorldMin
{
get
{
return (double)base.WorldMin;
}
set
{
if (value > 0.0f)
{
base.WorldMin = value;
}
else
{
throw new NPlotException("Cannot have negative values in Log Axis");
}
}
}
/// <summary>
/// The maximum world extent of the axis. Must be greater than zero.
/// </summary>
public override double WorldMax
{
get
{
return (double)base.WorldMax;
}
set
{
if (value > 0.0F)
{
base.WorldMax = value;
}
else
{
throw new NPlotException("Cannot have negative values in Log Axis");
}
}
}
/// <summary>
/// Get whether or not this axis is linear. It is not.
/// </summary>
public override bool IsLinear
{
get
{
return false;
}
}
}
}