mirror of https://github.com/mhowlett/nplot.git
572 lines
21 KiB
C#
572 lines
21 KiB
C#
/*
|
|
* 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.
|
|
*
|
|
* 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>
|
|
/// Provides functionality for drawing axes with a linear numeric scale.
|
|
/// </summary>
|
|
public class LinearAxis : Axis, ICloneable
|
|
{
|
|
/// <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};
|
|
|
|
/// <summary>
|
|
/// If set !NaN, gives the distance between large ticks.
|
|
/// </summary>
|
|
private double largeTickStep_ = double.NaN;
|
|
|
|
private double largeTickValue_ = double.NaN;
|
|
private object numberSmallTicks_;
|
|
private double offset_;
|
|
|
|
private double scale_ = 1.0;
|
|
|
|
/// <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()
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <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, 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_; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of small ticks between large ticks.
|
|
/// </summary>
|
|
public int NumberOfSmallTicks
|
|
{
|
|
set { numberSmallTicks_ = value; }
|
|
get
|
|
{
|
|
// TODO: something better here.
|
|
return (int) numberSmallTicks_;
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// 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 (GetType() != a.GetType())
|
|
{
|
|
throw new NPlotException("Clone not defined in derived type. Help!");
|
|
}
|
|
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_;
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
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 = getDefaultLabelOffset(physicalMin, physicalMax);
|
|
boundingBox = null;
|
|
|
|
ArrayList largeTickPositions;
|
|
ArrayList smallTickPositions;
|
|
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(NumberFormat, labelNumber);
|
|
|
|
DrawTick(g, ((double) largeTickPositions[i]/scale_ - offset_),
|
|
LargeTickSize, label.ToString(),
|
|
new Point(0, 0), physicalMin, physicalMax,
|
|
out tLabelOffset, out tBoundingBox);
|
|
|
|
UpdateOffsetAndBounds(ref labelOffset, ref boundingBox,
|
|
tLabelOffset, tBoundingBox);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < smallTickPositions.Count; ++i)
|
|
{
|
|
DrawTick(g, ((double) smallTickPositions[i]/scale_ - offset_),
|
|
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 = AdjustedWorldValue(WorldMax);
|
|
double adjustedMin = AdjustedWorldValue(WorldMin);
|
|
|
|
smallTickPositions = new ArrayList();
|
|
|
|
// TODO: Can optimize this now.
|
|
bool shouldCullMiddle;
|
|
double bigTickSpacing = DetermineLargeTickStep(physicalAxisLength, out shouldCullMiddle);
|
|
|
|
int nSmall = DetermineNumberSmallTicks(bigTickSpacing);
|
|
double smallTickSpacing = bigTickSpacing/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] + (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*scale_ + 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 = AdjustedWorldValue(WorldMax);
|
|
double adjustedMin = AdjustedWorldValue(WorldMin);
|
|
|
|
// (2) determine distance between large ticks.
|
|
bool shouldCullMiddle;
|
|
double tickDist = 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 = AdjustedWorldValue(WorldMax);
|
|
double adjustedMin = 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 (numberSmallTicks_ != null)
|
|
{
|
|
return (int) numberSmallTicks_ + 1;
|
|
}
|
|
|
|
if (SmallTickCounts.Length != 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;
|
|
}
|
|
}
|
|
} |