/* * 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; using System.Collections; using System.Drawing; using System.Text; namespace NPlot { /// /// Provides functionality for drawing axes with a linear numeric scale. /// public class LinearAxis : Axis, ICloneable { /// /// 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. /// public float ApproxNumberLargeTicks = 3.0f; /// /// 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. /// public double[] Mantissas = {1.0, 2.0, 5.0}; /// /// If NumberOfSmallTicks isn't specified then .... /// If specified LargeTickStep manually, then no small ticks unless /// NumberOfSmallTicks specified. /// public int[] SmallTickCounts = {4, 1, 4}; /// /// If set !NaN, gives the distance between large ticks. /// private double largeTickStep_ = double.NaN; private double largeTickValue_ = double.NaN; private object numberSmallTicks_; private double offset_; private double scale_ = 1.0; /// /// Copy constructor /// /// The Axis to clone public LinearAxis(Axis a) : base(a) { Init(); } /// /// Default constructor. /// public LinearAxis() { Init(); } /// /// Construct a linear axis with the provided world min and max values. /// /// the world minimum value of the axis. /// the world maximum value of the axis. public LinearAxis(double worldMin, double worldMax) : base(worldMin, worldMax) { Init(); } /// /// The distance between large ticks. If this is set to NaN [default], /// this distance will be calculated automatically. /// public double LargeTickStep { set { largeTickStep_ = value; } get { return largeTickStep_; } } /// /// If set, a large tick will be placed at this position, and other large ticks will /// be placed relative to this position. /// public double LargeTickValue { set { largeTickValue_ = value; } get { return largeTickValue_; } } /// /// The number of small ticks between large ticks. /// public int NumberOfSmallTicks { set { numberSmallTicks_ = value; } get { // TODO: something better here. return (int) numberSmallTicks_; } } /// /// Scale to apply to world values when labelling axis: /// (labelWorld = world * scale + offset). This does not /// affect the "real" world range of the axis. /// public double Scale { get { return scale_; } set { scale_ = value; } } /// /// 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. /// public double Offset { get { return offset_; } set { offset_ = value; } } /// /// Deep copy of LinearAxis. /// /// A copy of the LinearAxis Class 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; } /// /// Helper method for Clone. /// 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}"; } /// /// Draws the large and small ticks [and tick labels] for this axis. /// /// The graphics surface on which to draw. /// The physical position corresponding to the world minimum of the axis. /// The physical position corresponding to the world maximum of the axis. /// out: smallest box that completely surrounds all ticks and associated labels for this axis. /// out: offset from the axis to draw the axis label. 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. } } /// /// Determines the positions, in world coordinates, of the small ticks /// if they have not already been generated. /// /// The physical position corresponding to the world minimum of the axis. /// The physical position corresponding to the world maximum of the axis. /// The positions of the large ticks. /// If null, small tick positions are returned via this parameter. Otherwise this function does nothing. 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); } } } } /// /// Adjusts a real world value to one that has been modified to /// reflect the Axis Scale and Offset properties. /// /// world value to adjust /// adjusted world value public double AdjustedWorldValue(double world) { return world*scale_ + offset_; } /// /// 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. /// /// The physical position corresponding to the world minimum of the axis. /// The physical position corresponding to the world maximum of the axis. /// ArrayList containing the positions of the large ticks. /// ArrayList containing the positions of the small ticks if calculated, null otherwise. 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; } } /// /// 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 /// /// physical length of the axis /// /// 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. /// /// Large tick spacing /// TODO: This can be optimised a bit. 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]; } /// /// Given the large tick step, determine the number of small ticks that should /// be placed in between. /// /// the large tick step. /// the number of small ticks to place between large ticks. 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; } } }