Files
ntpa/NPlot/LinearAxis.cs
T
2016-03-05 14:06:38 +01:00

631 lines
18 KiB
C#
Executable File

/*
* NPlot - A charting library for .NET
*
* LinearAxis.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.Drawing;
using System.Collections;
using System;
using System.Text;
using System.Diagnostics;
namespace NPlot
{
/// <summary>
/// Provides functionality for drawing axes with a linear numeric scale.
/// </summary>
public class LinearAxis : Axis, System.ICloneable
{
/// <summary>
/// Deep copy of LinearAxis.
/// </summary>
/// <returns>A copy of the LinearAxis Class</returns>
public override object Clone()
{
LinearAxis a = new LinearAxis();
// ensure that this isn't being called on a derived type. If it is, then oh no!
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.
/// </summary>
protected void DoClone( LinearAxis b, LinearAxis a )
{
Axis.DoClone( b, a );
a.numberSmallTicks_ = b.numberSmallTicks_;
a.largeTickValue_ = b.largeTickValue_;
a.largeTickStep_ = b.largeTickStep_;
a.offset_ = b.offset_;
a.scale_ = b.scale_;
}
/// <summary>
/// Copy constructor
/// </summary>
/// <param name="a">The Axis to clone</param>
public LinearAxis( Axis a )
: base( a )
{
Init();
}
/// <summary>
/// Default constructor.
/// </summary>
public LinearAxis()
: base()
{
Init();
}
/// <summary>
/// Construct a linear axis with the provided world min and max values.
/// </summary>
/// <param name="worldMin">the world minimum value of the axis.</param>
/// <param name="worldMax">the world maximum value of the axis.</param>
public LinearAxis( double worldMin, double worldMax )
: base( worldMin, worldMax )
{
Init();
}
private void Init()
{
this.NumberFormat = "{0:g5}";
}
/// <summary>
/// Draws the large and small ticks [and tick labels] for this axis.
/// </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="boundingBox">out: smallest box that completely surrounds all ticks and associated labels for this axis.</param>
/// <param name="labelOffset">out: offset from the axis to draw the axis label.</param>
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 );
labelOffset = new Point( 0, 0 );
boundingBox = null;
if (largeTickPositions.Count > 0)
{
for (int i = 0; i < largeTickPositions.Count; ++i)
{
double labelNumber = (double)largeTickPositions[i];
// TODO: Find out why zero is sometimes significantly not zero [seen as high as 10^-16].
if (Math.Abs(labelNumber) < 0.000000000000001)
{
labelNumber = 0.0;
}
StringBuilder label = new StringBuilder();
label.AppendFormat(this.NumberFormat, labelNumber);
this.DrawTick( g, ((double)largeTickPositions[i]/this.scale_-this.offset_),
this.LargeTickSize, label.ToString(),
new Point(0,0), physicalMin, physicalMax,
out tLabelOffset, out tBoundingBox );
Axis.UpdateOffsetAndBounds( ref labelOffset, ref boundingBox,
tLabelOffset, tBoundingBox );
}
}
for (int i = 0; i<smallTickPositions.Count; ++i)
{
this.DrawTick( g, ((double)smallTickPositions[i]/this.scale_-this.offset_),
this.SmallTickSize, "",
new Point(0, 0), physicalMin, physicalMax,
out tLabelOffset, out tBoundingBox );
// assume bounding box and label offset 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.</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 )
{
// return if already generated.
if (smallTickPositions != null)
return;
int physicalAxisLength = Utils.Distance( physicalMin, physicalMax );
double adjustedMax = this.AdjustedWorldValue( WorldMax );
double adjustedMin = this.AdjustedWorldValue( WorldMin );
smallTickPositions = new ArrayList();
// TODO: Can optimize this now.
bool shouldCullMiddle;
double bigTickSpacing = this.DetermineLargeTickStep( physicalAxisLength, out shouldCullMiddle );
int nSmall = this.DetermineNumberSmallTicks( bigTickSpacing );
double smallTickSpacing = bigTickSpacing / (double)nSmall;
// if there is at least one big tick
if (largeTickPositions.Count > 0)
{
double pos1 = (double)largeTickPositions[0] - smallTickSpacing;
while (pos1 > adjustedMin)
{
smallTickPositions.Add( pos1 );
pos1 -= smallTickSpacing;
}
}
for (int i = 0; i < largeTickPositions.Count; ++i )
{
for (int j = 1; j < nSmall; ++j )
{
double pos = (double)largeTickPositions[i] + ((double)j) * smallTickSpacing;
if (pos <= adjustedMax)
{
smallTickPositions.Add( pos );
}
}
}
}
/// <summary>
/// Adjusts a real world value to one that has been modified to
/// reflect the Axis Scale and Offset properties.
/// </summary>
/// <param name="world">world value to adjust</param>
/// <returns>adjusted world value</returns>
public double AdjustedWorldValue( double world )
{
return world * this.scale_ + this.offset_;
}
/// <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.
///
/// If the LargeTickStep isn't set then this is calculated automatically and
/// depends on the physical extent of the axis.
/// </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 override void WorldTickPositions_FirstPass(
Point physicalMin,
Point physicalMax,
out ArrayList largeTickPositions,
out ArrayList smallTickPositions
)
{
// (1) error check
if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
{
throw new NPlotException( "world extent of axis not set." );
}
double adjustedMax = this.AdjustedWorldValue( WorldMax );
double adjustedMin = this.AdjustedWorldValue( WorldMin );
// (2) determine distance between large ticks.
bool shouldCullMiddle;
double tickDist = this.DetermineLargeTickStep(
Utils.Distance(physicalMin, physicalMax),
out shouldCullMiddle );
// (3) determine starting position.
double first = 0.0f;
if (!double.IsNaN(largeTickValue_))
{
// this works for both case when largTickValue_ lt or gt adjustedMin.
first = largeTickValue_ + (Math.Ceiling((adjustedMin-largeTickValue_)/tickDist))*tickDist;
}
else
{
if( adjustedMin > 0.0 )
{
double nToFirst = Math.Floor(adjustedMin / tickDist) + 1.0f;
first = nToFirst * tickDist;
}
else
{
double nToFirst = Math.Floor(-adjustedMin/tickDist) - 1.0f;
first = -nToFirst * tickDist;
}
// could miss one, if first is just below zero.
if ((first - tickDist) >= adjustedMin)
{
first -= tickDist;
}
}
// (4) now make list of large tick positions.
largeTickPositions = new ArrayList();
if (tickDist < 0.0) // some sanity checking. TODO: remove this.
throw new NPlotException( "Tick dist is negative" );
double position = first;
int safetyCount = 0;
while (
(position <= adjustedMax) &&
(++safetyCount < 5000) )
{
largeTickPositions.Add( position );
position += tickDist;
}
// (5) if the physical extent is too small, and the middle
// ticks should be turned into small ticks, then do this now.
smallTickPositions = null;
if (shouldCullMiddle)
{
smallTickPositions = new ArrayList();
if (largeTickPositions.Count > 2)
{
for (int i=1; i<largeTickPositions.Count-1; ++i)
{
smallTickPositions.Add( largeTickPositions[i] );
}
}
ArrayList culledPositions = new ArrayList();
culledPositions.Add( largeTickPositions[0] );
culledPositions.Add( largeTickPositions[largeTickPositions.Count-1] );
largeTickPositions = culledPositions;
}
}
/// <summary>
/// Calculates the world spacing between large ticks, based on the physical
/// axis length (parameter), world axis length, Mantissa values and
/// MinPhysicalLargeTickStep. A value such that at least two
/// </summary>
/// <param name="physicalLength">physical length of the axis</param>
/// <param name="shouldCullMiddle">Returns true if we were forced to make spacing of
/// large ticks too small in order to ensure that there are at least two of
/// them. The draw ticks method should not draw more than two large ticks if this
/// returns true.</param>
/// <returns>Large tick spacing</returns>
/// <remarks>TODO: This can be optimised a bit.</remarks>
private double DetermineLargeTickStep( float physicalLength, out bool shouldCullMiddle )
{
shouldCullMiddle = false;
if ( double.IsNaN(WorldMin) || double.IsNaN(WorldMax) )
{
throw new NPlotException( "world extent of axis not set." );
}
// if the large tick has been explicitly set, then return this.
if ( !double.IsNaN(largeTickStep_) )
{
if ( largeTickStep_ <= 0.0f )
{
throw new NPlotException(
"can't have negative or zero tick step - reverse WorldMin WorldMax instead."
);
}
return largeTickStep_;
}
// otherwise we need to calculate the large tick step ourselves.
// adjust world max and min for offset and scale properties of axis.
double adjustedMax = this.AdjustedWorldValue( WorldMax );
double adjustedMin = this.AdjustedWorldValue( WorldMin );
double range = adjustedMax - adjustedMin;
// if axis has zero world length, then return arbitrary number.
if ( Utils.DoubleEqual( adjustedMax, adjustedMin ) )
{
return 1.0f;
}
double approxTickStep;
if (TicksIndependentOfPhysicalExtent)
{
approxTickStep = range / 6.0f;
}
else
{
approxTickStep = (MinPhysicalLargeTickStep / physicalLength) * range;
}
double exponent = Math.Floor( Math.Log10( approxTickStep ) );
double mantissa = Math.Pow( 10.0, Math.Log10( approxTickStep ) - exponent );
// determine next whole mantissa below the approx one.
int mantissaIndex = Mantissas.Length-1;
for (int i=1; i<Mantissas.Length; ++i)
{
if (mantissa < Mantissas[i])
{
mantissaIndex = i-1;
break;
}
}
// then choose next largest spacing.
mantissaIndex += 1;
if (mantissaIndex == Mantissas.Length)
{
mantissaIndex = 0;
exponent += 1.0;
}
if (!TicksIndependentOfPhysicalExtent)
{
// now make sure that the returned value is such that at least two
// large tick marks will be displayed.
double tickStep = Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
float physicalStep = (float)((tickStep / range) * physicalLength);
while (physicalStep > physicalLength/2)
{
shouldCullMiddle = true;
mantissaIndex -= 1;
if (mantissaIndex == -1)
{
mantissaIndex = Mantissas.Length-1;
exponent -= 1.0;
}
tickStep = Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
physicalStep = (float)((tickStep / range) * physicalLength);
}
}
// and we're done.
return Math.Pow( 10.0, exponent ) * Mantissas[mantissaIndex];
}
/// <summary>
/// Given the large tick step, determine the number of small ticks that should
/// be placed in between.
/// </summary>
/// <param name="bigTickDist">the large tick step.</param>
/// <returns>the number of small ticks to place between large ticks.</returns>
private int DetermineNumberSmallTicks( double bigTickDist )
{
if (this.numberSmallTicks_ != null)
{
return (int)this.numberSmallTicks_+1;
}
if (this.SmallTickCounts.Length != this.Mantissas.Length)
{
throw new NPlotException( "Mantissa.Length != SmallTickCounts.Length" );
}
if (bigTickDist > 0.0f)
{
double exponent = Math.Floor( Math.Log10( bigTickDist ) );
double mantissa = Math.Pow( 10.0, Math.Log10( bigTickDist ) - exponent );
for (int i=0; i<Mantissas.Length; ++i)
{
if ( Math.Abs(mantissa-Mantissas[i]) < 0.001 )
{
return SmallTickCounts[i]+1;
}
}
}
return 0;
}
/// <summary>
/// The distance between large ticks. If this is set to NaN [default],
/// this distance will be calculated automatically.
/// </summary>
public double LargeTickStep
{
set
{
largeTickStep_ = value;
}
get
{
return largeTickStep_;
}
}
/// <summary>
/// If set !NaN, gives the distance between large ticks.
/// </summary>
private double largeTickStep_ = double.NaN;
/// <summary>
/// If set, a large tick will be placed at this position, and other large ticks will
/// be placed relative to this position.
/// </summary>
public double LargeTickValue
{
set
{
largeTickValue_ = value;
}
get
{
return largeTickValue_;
}
}
private double largeTickValue_ = double.NaN;
/// <summary>
/// The number of small ticks between large ticks.
/// </summary>
public int NumberOfSmallTicks
{
set
{
numberSmallTicks_ = value;
}
get
{
// TODO: something better here.
return (int)numberSmallTicks_;
}
}
private object numberSmallTicks_ = null;
/// <summary>
/// Scale to apply to world values when labelling axis:
/// (labelWorld = world * scale + offset). This does not
/// affect the "real" world range of the axis.
/// </summary>
public double Scale
{
get
{
return scale_;
}
set
{
scale_ = value;
}
}
/// <summary>
/// Offset to apply to world values when labelling the axis:
/// (labelWorld = axisWorld * scale + offset). This does not
/// affect the "real" world range of the axis.
/// </summary>
public double Offset
{
get
{
return offset_;
}
set
{
offset_ = value;
}
}
/// <summary>
/// If LargeTickStep isn't specified, then a suitable value is
/// calculated automatically. To determine the tick spacing, the
/// world axis length is divided by ApproximateNumberLargeTicks
/// and the next lowest distance m*10^e for some m in the Mantissas
/// set and some integer e is used as the large tick spacing.
/// </summary>
public float ApproxNumberLargeTicks = 3.0f;
/// <summary>
/// If LargeTickStep isn't specified, then a suitable value is
/// calculated automatically. The value will be of the form
/// m*10^e for some m in this set.
/// </summary>
public double[] Mantissas = {1.0, 2.0, 5.0};
/// <summary>
/// If NumberOfSmallTicks isn't specified then ....
/// If specified LargeTickStep manually, then no small ticks unless
/// NumberOfSmallTicks specified.
/// </summary>
public int[] SmallTickCounts = {4, 1, 4};
private double offset_ = 0.0;
private double scale_ = 1.0;
}
}