nplot/src/LogAxis.cs

603 lines
23 KiB
C#

/*
* NPlot - A charting library for .NET
*
* LogAxis.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.Text;
namespace NPlot
{
/// <summary>
/// The class implementing logarithmic axes.
/// </summary>
public class LogAxis : Axis
{
private static readonly double m_d5Log = -Math.Log10(0.5); // .30103
private static readonly double m_d5RegionPos = Math.Abs(m_d5Log + ((1 - m_d5Log)/2)); // ' .6505
private static readonly double m_d5RegionNeg = Math.Abs(m_d5Log/2); // '.1505
private double largeTickStep_ = double.NaN;
private double largeTickValue_ = double.NaN;
private object numberSmallTicks_;
/// <summary>
/// Default constructor.
/// </summary>
public LogAxis()
{
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();
}
/// <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; }
}
/// <summary>
/// The minimum world extent of the axis. Must be greater than zero.
/// </summary>
public override double WorldMin
{
get { return 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 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; }
}
/// <summary>
/// Deep Copy of the LogAxis.
/// </summary>
/// <returns>A Copy of the LogAxis Class.</returns>
public override object Clone()
{
LogAxis a = new LogAxis();
if (GetType() != a.GetType())
{
throw new NPlotException("Clone not defined in derived type. Help!");
}
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_;
}
private void Init()
{
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 = getDefaultLabelOffset(physicalMin, physicalMax);
boundingBox = null;
ArrayList largeTickPositions;
ArrayList smallTickPositions;
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(NumberFormat, (double) largeTickPositions[i]);
DrawTick(g, (double) largeTickPositions[i], LargeTickSize, label.ToString(),
new Point(0, 0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox);
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)
{
DrawTick(g, (double) smallTickPositions[i], 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 = DetermineTickSpacing();
int nSmall = 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 > 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 > 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 > WorldMin && pos < WorldMax)
{
smallTickPositions.Add(pos);
}
}
}
}
}
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 = 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(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(largeTickStep_))
{
if (largeTickStep_ <= 0.0f)
{
throw new NPlotException("can't have negative tick step - reverse WorldMin WorldMax instead.");
}
return largeTickStep_;
}
double MagRange = (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 (numberSmallTicks_ != null && bigTickDist == 1.0f)
{
return (int) 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>
/// 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 = (Math.Log10(WorldMax) - Math.Log10(WorldMin));
double prop = ((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 - WorldMin)/(WorldMax - 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;
}
}
}