1 /*
2  * NPlot - A charting library for .NET
3  *
4  * LogAxis.cs
5  * Copyright (C) 2003-2006 Matt Howlett and others.
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without modification,
9  * are permitted provided that the following conditions are met:
10  *
11  * 1. Redistributions of source code must retain the above copyright notice, this
12  *    list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright notice,
14  *    this list of conditions and the following disclaimer in the documentation
15  *    and/or other materials provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20  * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
21  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
22  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
25  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
26  * OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 using System;
30 using System.Collections;
31 using System.Drawing;
32 using System.Text;
33 
34 namespace NPlot
35 {
36     /// <summary>
37     /// The class implementing logarithmic axes.
38     /// </summary>
39     public class LogAxis : Axis
40     {
41         private static readonly double m_d5Log = -Math.Log10(0.5); // .30103
42         private static readonly double m_d5RegionPos = Math.Abs(m_d5Log + ((1 - m_d5Log)/2)); //	   ' .6505
43         private static readonly double m_d5RegionNeg = Math.Abs(m_d5Log/2); //	   '.1505
44         private double largeTickStep_ = double.NaN;
45         private double largeTickValue_ = double.NaN;
46         private object numberSmallTicks_;
47 
48         /// <summary>
49         /// Default constructor.
50         /// </summary>
LogAxis()51         public LogAxis()
52         {
53             Init();
54         }
55 
56         /// <summary>
57         /// Copy Constructor
58         /// </summary>
59         /// <param name="a">The Axis to clone.</param>
LogAxis(Axis a)60         public LogAxis(Axis a)
61             : base(a)
62         {
63             Init();
64         }
65 
66         /// <summary>
67         /// Constructor
68         /// </summary>
69         /// <param name="worldMin">Minimum World value for the axis.</param>
70         /// <param name="worldMax">Maximum World value for the axis.</param>
LogAxis(double worldMin, double worldMax)71         public LogAxis(double worldMin, double worldMax)
72             : base(worldMin, worldMax)
73         {
74             Init();
75         }
76 
77         /// <summary>
78         /// The step between large ticks, expressed in decades for the Log scale.
79         /// </summary>
80         public double LargeTickStep
81         {
82             set { largeTickStep_ = value; }
83             get { return largeTickStep_; }
84         }
85 
86         /// <summary>
87         /// Position of one of the large ticks [other positions will be calculated relative to this one].
88         /// </summary>
89         public double LargeTickValue
90         {
91             set { largeTickValue_ = value; }
92             get { return largeTickValue_; }
93         }
94 
95         /// <summary>
96         /// The number of small ticks between large ticks.
97         /// </summary>
98         public int NumberSmallTicks
99         {
100             set { numberSmallTicks_ = value; }
101         }
102 
103         /// <summary>
104         /// The minimum world extent of the axis. Must be greater than zero.
105         /// </summary>
106         public override double WorldMin
107         {
108             get { return base.WorldMin; }
109             set
110             {
111                 if (value > 0.0f)
112                 {
113                     base.WorldMin = value;
114                 }
115                 else
116                 {
117                     throw new NPlotException("Cannot have negative values in Log Axis");
118                 }
119             }
120         }
121 
122         /// <summary>
123         /// The maximum world extent of the axis. Must be greater than zero.
124         /// </summary>
125         public override double WorldMax
126         {
127             get { return base.WorldMax; }
128             set
129             {
130                 if (value > 0.0F)
131                 {
132                     base.WorldMax = value;
133                 }
134                 else
135                 {
136                     throw new NPlotException("Cannot have negative values in Log Axis");
137                 }
138             }
139         }
140 
141         /// <summary>
142         /// Get whether or not this axis is linear. It is not.
143         /// </summary>
144         public override bool IsLinear
145         {
146             get { return false; }
147         }
148 
149         /// <summary>
150         /// Deep Copy of the LogAxis.
151         /// </summary>
152         /// <returns>A Copy of the LogAxis Class.</returns>
Clone()153         public override object Clone()
154         {
155             LogAxis a = new LogAxis();
156             if (GetType() != a.GetType())
157             {
158                 throw new NPlotException("Clone not defined in derived type. Help!");
159             }
160             DoClone(this, a);
161             return a;
162         }
163 
164         /// <summary>
165         /// Helper method for Clone (actual implementation)
166         /// </summary>
167         /// <param name="a">The original object to clone.</param>
168         /// <param name="b">The cloned object.</param>
DoClone(LogAxis b, LogAxis a)169         protected void DoClone(LogAxis b, LogAxis a)
170         {
171             Axis.DoClone(b, a);
172             // add specific elemtents of the class for the deep copy of the object
173             a.numberSmallTicks_ = b.numberSmallTicks_;
174             a.largeTickValue_ = b.largeTickValue_;
175             a.largeTickStep_ = b.largeTickStep_;
176         }
177 
Init()178         private void Init()
179         {
180             NumberFormat = "{0:g5}";
181         }
182 
183         /// <summary>
184         /// Draw the ticks.
185         /// </summary>
186         /// <param name="g">The drawing surface on which to draw.</param>
187         /// <param name="physicalMin">The minimum physical extent of the axis.</param>
188         /// <param name="physicalMax">The maximum physical extent of the axis.</param>
189         /// <param name="boundingBox">out: smallest box that completely encompasses all of the ticks and tick labels.</param>
190         /// <param name="labelOffset">out: a suitable offset from the axis to draw the axis label.</param>
191         /// <returns>
192         /// An ArrayList containing the offset from the axis required for an axis label
193         /// to miss this tick, followed by a bounding rectangle for the tick and tickLabel drawn.
194         /// </returns>
DrawTicks( Graphics g, Point physicalMin, Point physicalMax, out object labelOffset, out object boundingBox)195         protected override void DrawTicks(
196             Graphics g,
197             Point physicalMin,
198             Point physicalMax,
199             out object labelOffset,
200             out object boundingBox)
201         {
202             Point tLabelOffset;
203             Rectangle tBoundingBox;
204 
205             labelOffset = getDefaultLabelOffset(physicalMin, physicalMax);
206             boundingBox = null;
207 
208             ArrayList largeTickPositions;
209             ArrayList smallTickPositions;
210             WorldTickPositions(physicalMin, physicalMax, out largeTickPositions, out smallTickPositions);
211 
212             //Point offset = new Point(0, 0);
213             //object bb = null;
214             // Missed this protection
215             if (largeTickPositions.Count > 0)
216             {
217                 for (int i = 0; i < largeTickPositions.Count; ++i)
218                 {
219                     StringBuilder label = new StringBuilder();
220                     // do google search for "format specifier writeline" for help on this.
221                     label.AppendFormat(NumberFormat, (double) largeTickPositions[i]);
222                     DrawTick(g, (double) largeTickPositions[i], LargeTickSize, label.ToString(),
223                              new Point(0, 0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox);
224 
225                     UpdateOffsetAndBounds(ref labelOffset, ref boundingBox, tLabelOffset, tBoundingBox);
226                 }
227             }
228             else
229             {
230                 // just get the axis bounding box)
231                 //PointF dir = Utils.UnitVector(physicalMin, physicalMax);
232                 //Rectangle rr = new Rectangle(physicalMin.X,
233                 //                             (int) ((physicalMax.X - physicalMin.X)*dir.X),
234                 //                             physicalMin.Y,
235                 //                             (int) ((physicalMax.Y - physicalMin.Y)*dir.Y));
236                 //bb = rr;
237             }
238 
239             // missed protection for zero ticks
240             if (smallTickPositions.Count > 0)
241             {
242                 for (int i = 0; i < smallTickPositions.Count; ++i)
243                 {
244                     DrawTick(g, (double) smallTickPositions[i], SmallTickSize,
245                              "", new Point(0, 0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox);
246                     // ignore r for now - assume bb unchanged by small tick bounds.
247                 }
248             }
249         }
250 
251         /// <summary>
252         /// Determines the positions, in world coordinates, of the small ticks
253         /// if they have not already been generated.
254         /// </summary>
255         /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
256         /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
257         /// <param name="largeTickPositions">The positions of the large ticks, unchanged</param>
258         /// <param name="smallTickPositions">If null, small tick positions are returned via this parameter. Otherwise this function does nothing.</param>
WorldTickPositions_SecondPass( Point physicalMin, Point physicalMax, ArrayList largeTickPositions, ref ArrayList smallTickPositions)259         internal override void WorldTickPositions_SecondPass(
260             Point physicalMin,
261             Point physicalMax,
262             ArrayList largeTickPositions,
263             ref ArrayList smallTickPositions)
264         {
265             if (smallTickPositions != null)
266             {
267                 throw new NPlotException("not expecting smallTickPositions to be set already.");
268             }
269 
270             smallTickPositions = new ArrayList();
271 
272             // retrieve the spacing of the big ticks. Remember this is decades!
273             double bigTickSpacing = DetermineTickSpacing();
274             int nSmall = DetermineNumberSmallTicks(bigTickSpacing);
275 
276             // now we have to set the ticks
277             // let us start with the easy case where the major tick distance
278             // is larger than a decade
279             if (bigTickSpacing > 1.0f)
280             {
281                 if (largeTickPositions.Count > 0)
282                 {
283                     // deal with the smallticks preceding the
284                     // first big tick
285                     double pos1 = (double) largeTickPositions[0];
286                     while (pos1 > WorldMin)
287                     {
288                         pos1 = pos1/10.0f;
289                         smallTickPositions.Add(pos1);
290                     }
291                     // now go on for all other Major ticks
292                     for (int i = 0; i < largeTickPositions.Count; ++i)
293                     {
294                         double pos = (double) largeTickPositions[i];
295                         for (int j = 1; j <= nSmall; ++j)
296                         {
297                             pos = pos*10.0F;
298                             // check to see if we are still in the range
299                             if (pos < WorldMax)
300                             {
301                                 smallTickPositions.Add(pos);
302                             }
303                         }
304                     }
305                 }
306             }
307             else
308             {
309                 // guess what...
310                 double[] m = {2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f};
311                 // Then we deal with the other ticks
312                 if (largeTickPositions.Count > 0)
313                 {
314                     // first deal with the smallticks preceding the first big tick
315                     // positioning before the first tick
316                     double pos1 = (double) largeTickPositions[0]/10.0f;
317                     for (int i = 0; i < m.Length; i++)
318                     {
319                         double pos = pos1*m[i];
320                         if (pos > WorldMin)
321                         {
322                             smallTickPositions.Add(pos);
323                         }
324                     }
325                     // now go on for all other Major ticks
326                     for (int i = 0; i < largeTickPositions.Count; ++i)
327                     {
328                         pos1 = (double) largeTickPositions[i];
329                         for (int j = 0; j < m.Length; ++j)
330                         {
331                             double pos = pos1*m[j];
332                             // check to see if we are still in the range
333                             if (pos < WorldMax)
334                             {
335                                 smallTickPositions.Add(pos);
336                             }
337                         }
338                     }
339                 }
340                 else
341                 {
342                     // probably a minor tick would anyway fall in the range
343                     // find the decade preceding the minimum
344                     double dec = Math.Floor(Math.Log10(WorldMin));
345                     double pos1 = Math.Pow(10.0, dec);
346                     for (int i = 0; i < m.Length; i++)
347                     {
348                         double pos = pos1*m[i];
349                         if (pos > WorldMin && pos < WorldMax)
350                         {
351                             smallTickPositions.Add(pos);
352                         }
353                     }
354                 }
355             }
356         }
357 
CalcGrids(double dLenAxis, int nNumDivisions, ref double dDivisionInterval)358         private void CalcGrids(double dLenAxis, int nNumDivisions, ref double dDivisionInterval)
359         {
360             double dMyInterval = dLenAxis/nNumDivisions;
361             double dPower = Math.Log10(dMyInterval);
362             dDivisionInterval = 10 ^ (int) dPower;
363             double dFixPower = dPower - (int) dPower;
364             double d5Region = Math.Abs(dPower - dFixPower);
365             double dMyMult;
366             if (dPower < 0)
367             {
368                 d5Region = -(dPower - dFixPower);
369                 dMyMult = 0.5;
370             }
371             else
372             {
373                 d5Region = 1 - (dPower - dFixPower);
374                 dMyMult = 5;
375             }
376             if ((d5Region >= m_d5RegionNeg) && (d5Region <= m_d5RegionPos))
377             {
378                 dDivisionInterval = dDivisionInterval*dMyMult;
379             }
380         }
381 
382         /// <summary>
383         /// Determines the positions, in world coordinates, of the log spaced large ticks.
384         /// </summary>
385         /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
386         /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
387         /// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
388         /// <param name="smallTickPositions">null</param>
WorldTickPositions_FirstPass( Point physicalMin, Point physicalMax, out ArrayList largeTickPositions, out ArrayList smallTickPositions )389         internal override void WorldTickPositions_FirstPass(
390             Point physicalMin,
391             Point physicalMax,
392             out ArrayList largeTickPositions,
393             out ArrayList smallTickPositions
394             )
395         {
396             smallTickPositions = null;
397             largeTickPositions = new ArrayList();
398 
399             if (double.IsNaN(WorldMin) || double.IsNaN(WorldMax))
400             {
401                 throw new NPlotException("world extent of axis not set.");
402             }
403 
404             double roundTickDist = DetermineTickSpacing();
405 
406             // now determine first tick position.
407             double first = 0.0f;
408 
409             // if the user hasn't specified a large tick position.
410             if (double.IsNaN(largeTickValue_))
411             {
412                 if (WorldMin > 0.0)
413                 {
414                     double nToFirst = Math.Floor(Math.Log10(WorldMin)/roundTickDist) + 1.0f;
415                     first = nToFirst*roundTickDist;
416                 }
417 
418                 // could miss one, if first is just below zero.
419                 if (first - roundTickDist >= Math.Log10(WorldMin))
420                 {
421                     first -= roundTickDist;
422                 }
423             }
424 
425                 // the user has specified one place they would like a large tick placed.
426             else
427             {
428                 first = Math.Log10(LargeTickValue);
429 
430                 // TODO: check here not too much different.
431                 // could result in long loop.
432                 while (first < Math.Log10(WorldMin))
433                 {
434                     first += roundTickDist;
435                 }
436 
437                 while (first > Math.Log10(WorldMin) + roundTickDist)
438                 {
439                     first -= roundTickDist;
440                 }
441             }
442 
443             double mark = first;
444             while (mark <= Math.Log10(WorldMax))
445             {
446                 // up to here only logs are dealt with, but I want to return
447                 // a real value in the arraylist
448                 double val;
449                 val = Math.Pow(10.0, mark);
450                 largeTickPositions.Add(val);
451                 mark += roundTickDist;
452             }
453         }
454 
455         /// <summary>
456         /// Determines the tick spacing.
457         /// </summary>
458         /// <returns>The tick spacing (in decades!)</returns>
DetermineTickSpacing()459         private double DetermineTickSpacing()
460         {
461             if (double.IsNaN(WorldMin) || double.IsNaN(WorldMax))
462             {
463                 throw new NPlotException("world extent of axis is not set.");
464             }
465 
466             // if largeTickStep has been set, it is used
467             if (!double.IsNaN(largeTickStep_))
468             {
469                 if (largeTickStep_ <= 0.0f)
470                 {
471                     throw new NPlotException("can't have negative tick step - reverse WorldMin WorldMax instead.");
472                 }
473 
474                 return largeTickStep_;
475             }
476 
477             double MagRange = (Math.Floor(Math.Log10(WorldMax)) - Math.Floor(Math.Log10(WorldMin)) + 1.0);
478 
479             if (MagRange > 0.0)
480             {
481                 // for now, a simple logic
482                 // start with a major tick every order of magnitude, and
483                 // increment if in order not to have more than 10 ticks in
484                 // the plot.
485                 double roundTickDist = 1.0F;
486                 int nticks = (int) (MagRange/roundTickDist);
487                 while (nticks > 10)
488                 {
489                     roundTickDist++;
490                     nticks = (int) (MagRange/roundTickDist);
491                 }
492                 return roundTickDist;
493             }
494             else
495             {
496                 return 0.0f;
497             }
498         }
499 
500         /// <summary>
501         /// Determines the number of small ticks between two large ticks.
502         /// </summary>
503         /// <param name="bigTickDist">The distance between two large ticks.</param>
504         /// <returns>The number of small ticks.</returns>
DetermineNumberSmallTicks(double bigTickDist)505         private int DetermineNumberSmallTicks(double bigTickDist)
506         {
507             // if the big ticks is more than one decade, the
508             // small ticks are every decade, I don't let the user set it.
509             if (numberSmallTicks_ != null && bigTickDist == 1.0f)
510             {
511                 return (int) numberSmallTicks_ + 1;
512             }
513 
514             // if we are plotting every decade, we have to
515             // put the log ticks. As a start, I put every
516             // small tick (.2,.3,.4,.5,.6,.7,.8,.9)
517             if (bigTickDist == 1.0f)
518             {
519                 return 8;
520             }
521                 // easy, put a tick every missed decade
522             else if (bigTickDist > 1.0f)
523             {
524                 return (int) bigTickDist - 1;
525             }
526             else
527             {
528                 throw new NPlotException("Wrong Major tick distance setting");
529             }
530         }
531 
532         /// <summary>
533         /// World to physical coordinate transform.
534         /// </summary>
535         /// <param name="coord">The coordinate value to transform.</param>
536         /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
537         /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
538         /// <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>
539         /// <returns>The transformed coordinates.</returns>
540         /// <remarks>TODO: make Reversed property work for this.</remarks>
WorldToPhysical( double coord, PointF physicalMin, PointF physicalMax, bool clip)541         public override PointF WorldToPhysical(
542             double coord,
543             PointF physicalMin,
544             PointF physicalMax,
545             bool clip)
546         {
547             // if want clipped value, return extrema if outside range.
548             if (clip)
549             {
550                 if (coord > WorldMax)
551                 {
552                     return physicalMax;
553                 }
554                 if (coord < WorldMin)
555                 {
556                     return physicalMin;
557                 }
558             }
559 
560             if (coord < 0.0f)
561             {
562                 throw new NPlotException("Cannot have negative values for data using Log Axis");
563             }
564 
565             // inside range or don't want to clip.
566             double lrange = (Math.Log10(WorldMax) - Math.Log10(WorldMin));
567             double prop = ((Math.Log10(coord) - Math.Log10(WorldMin))/lrange);
568             PointF offset = new PointF((float) (prop*(physicalMax.X - physicalMin.X)),
569                                        (float) (prop*(physicalMax.Y - physicalMin.Y)));
570 
571             return new PointF(physicalMin.X + offset.X, physicalMin.Y + offset.Y);
572         }
573 
574         /// <summary>
575         /// Return the world coordinate of the projection of the point p onto
576         /// the axis.
577         /// </summary>
578         /// <param name="p">The point to project onto the axis</param>
579         /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
580         /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
581         /// <param name="clip">If true, the world value will be clipped to WorldMin or WorldMax as appropriate if it lies outside this range.</param>
582         /// <returns>The world value corresponding to the projection of the point p onto the axis.</returns>
PhysicalToWorld(PointF p, PointF physicalMin, PointF physicalMax, bool clip)583         public override double PhysicalToWorld(PointF p, PointF physicalMin, PointF physicalMax, bool clip)
584         {
585             // use the base method to do the projection on the axis.
586             double t = base.PhysicalToWorld(p, physicalMin, physicalMax, clip);
587 
588             // now reconstruct phys dist prop along this assuming linear scale as base method did.
589             double v = (t - WorldMin)/(WorldMax - WorldMin);
590 
591             double ret = WorldMin*Math.Pow(WorldMax/WorldMin, v);
592 
593             // if want clipped value, return extrema if outside range.
594             if (clip)
595             {
596                 ret = Math.Max(ret, WorldMin);
597                 ret = Math.Min(ret, WorldMax);
598             }
599 
600             return ret;
601         }
602     }
603 }