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 }