root/plot/Pybrary.Plot/NumericAxis.cs

Revision 755, 27.0 kB (checked in by mfenniak, 2 years ago)

Add Pybrary.Plot.Demo code

Line 
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4 using System.Drawing;
5 using System.Drawing.Drawing2D;
6
7 namespace Pybrary.Plot
8 {
9     public abstract class NumericAxis : Axis
10     {
11         // axis scale order - zoomed, user, software, data, unscaled
12         protected string title = null;
13         protected FontDescription titleFont = new FontDescription("Arial", 12f, FontStyle.Bold);
14         protected double? zoomedMinimum = null;
15         protected double? zoomedMaximum = null;
16         private double? userMaximum = null;
17         private double? userMinimum = null;
18         private double? softwareMaximum = null;
19         private double? softwareMinimum = null;
20         private double unscaledMaximum = 10;
21         private double unscaledMinimum = 0;
22         private double unscaledLogMinimum = 0.1;
23         private bool visible = true;
24         protected bool logAxis = false;
25         private bool autoscaleIncludesZero = true;
26         private bool integerAxis = false;
27
28         protected NumericAxis()
29         {
30         }
31
32         protected abstract bool AxisReverseOfCoordinateArea();
33         protected abstract float AxisLength(AdvancedRect dataArea);
34         protected abstract float AxisStart(AdvancedRect dataArea);
35         protected abstract double? CalculateDataMinimum();
36         protected abstract double? CalculateDataMaximum();
37         protected abstract double? CalculateDataMinimumGtZero();
38
39         public float DataToCoordinate(double v, AdvancedRect rect)
40         {
41             if (ScaleMinimum == ScaleMaximum)
42                 return rect.BottomRight.Y;
43             double v2 = (double)v;
44             if (!logAxis)
45             {
46                 double r = (v2 - ScaleMinimum) / (ScaleMaximum - ScaleMinimum);
47                 if (AxisReverseOfCoordinateArea())
48                     r = (1.0 - r);
49                 return (float)((AxisLength(rect) * r) + AxisStart(rect));
50             }
51             else
52             {
53                 double r = (Math.Log10(v2) - Math.Log10(ScaleMinimum)) / (Math.Log10(ScaleMaximum) - Math.Log10(ScaleMinimum));
54                 if (AxisReverseOfCoordinateArea())
55                     r = (1.0 - r);
56                 return (float)((AxisLength(rect) * r) + AxisStart(rect));
57             }
58         }
59
60         public double CoordinateToData(float x, AdvancedRect rect)
61         {
62             if (!logAxis)
63             {
64                 double r = ((x - AxisStart(rect)) / AxisLength(rect));
65                 if (AxisReverseOfCoordinateArea())
66                     r = (1.0 - r);
67                 return (ScaleMaximum - ScaleMinimum) * r + ScaleMinimum;
68             }
69             else
70             {
71                 double r = ((x - AxisStart(rect)) / AxisLength(rect));
72                 if (AxisReverseOfCoordinateArea())
73                     r = (1.0 - r);
74                 return Math.Pow(10, Math.Log10(ScaleMinimum) + (r * (Math.Log10(ScaleMaximum) - Math.Log10(ScaleMinimum))));
75             }
76         }
77
78         public String FormatLabel(double v)
79         {
80             return String.Format("{0}", v);
81         }
82
83         public double ScaleMaximum
84         {
85             get
86             {
87                 if (zoomedMaximum.HasValue)
88                     return zoomedMaximum.Value;
89                 if (userMaximum.HasValue)
90                     return userMaximum.Value;
91                 if (softwareMaximum.HasValue)
92                     return softwareMaximum.Value;
93                 double? max = CalculateDataMaximum();
94                 double? min = CalculateDataMinimum();
95                 if (max.HasValue && min.HasValue)
96                 {
97                     if (logAxis)
98                         return Math.Pow(10, Math.Ceiling(Math.Log10(max.Value)));
99                     else
100                     {
101                         // add 5% of delta for extra white space
102                         double realMax = CalculateRoundedMax(max.Value, min.Value);
103                         if (realMax < 0 && autoscaleIncludesZero)
104                             return 0;
105                         else if (realMax > 0 && (min.Value <= 0 && max.Value <= 0) && autoscaleIncludesZero)
106                             return 0;
107                         return realMax;
108                     }
109                 }
110                 return unscaledMaximum;
111             }
112         }
113
114         public double ScaleMinimum
115         {
116             get
117             {
118                 if (zoomedMinimum.HasValue)
119                     return zoomedMinimum.Value;
120                 if (userMinimum.HasValue)
121                     return userMinimum.Value;
122                 if (softwareMinimum.HasValue)
123                     return softwareMinimum.Value;
124                 double? max = CalculateDataMaximum();
125                 double? min = CalculateDataMinimum();
126                 if (max.HasValue && min.HasValue)
127                 {
128                     if (logAxis)
129                     {
130                         double? min_gt_zero = CalculateDataMinimumGtZero();
131                         if (min_gt_zero.HasValue)
132                             return Math.Pow(10, Math.Floor(Math.Log10(min_gt_zero.Value)));
133                         return unscaledLogMinimum;
134                     }
135                     else
136                     {
137                         // add 5% of delta for extra white space
138                         double realMin = CalculateRoundedMin(max.Value, min.Value);
139                         if (realMin > 0 && autoscaleIncludesZero)
140                             return 0;
141                         else if (realMin < 0 && (min.Value >= 0 && max.Value >= 0) && autoscaleIncludesZero)
142                             return 0;
143                         return realMin;
144                     }
145                 }
146                 return unscaledMinimum;
147             }
148         }
149
150         public string Title
151         {
152             get
153             {
154                 return title;
155             }
156             set
157             {
158                 title = value;
159                 raiseEvent();
160             }
161         }
162
163         public double? SoftwareMinimum
164         {
165             get
166             {
167                 return softwareMinimum;
168             }
169             set
170             {
171                 softwareMinimum = (double?)value;
172                 raiseEvent();
173             }
174         }
175
176         public double? SoftwareMaximum
177         {
178             get
179             {
180                 return softwareMaximum;
181             }
182             set
183             {
184                 softwareMaximum = (double?)value;
185                 raiseEvent();
186             }
187         }
188
189         public double? UserMinimum
190         {
191             get
192             {
193                 return userMinimum;
194             }
195             set
196             {
197                 userMinimum = (double?)value;
198                 raiseEvent();
199             }
200         }
201
202         public double? UserMaximum
203         {
204             get
205             {
206                 return userMaximum;
207             }
208             set
209             {
210                 userMaximum = (double?)value;
211                 raiseEvent();
212             }
213         }
214
215         public bool AutoscaleIncludesZero
216         {
217             get
218             {
219                 return autoscaleIncludesZero;
220             }
221             set
222             {
223                 autoscaleIncludesZero = value;
224                 raiseEvent();
225             }
226         }
227
228         public bool Visible
229         {
230             get
231             {
232                 return visible;
233             }
234             set
235             {
236                 visible = value;
237                 raiseEvent();
238             }
239         }
240
241         public bool LogAxis
242         {
243             get
244             {
245                 return logAxis;
246             }
247             set
248             {
249                 using (SuspendEvents())
250                 {
251                     logAxis = value;
252                     if (logAxis)
253                     {
254                         // If the user zoomed into the graph, or a min value was
255                         // set by the software before, and it cannot be drawn on
256                         // a log axis - remove the restriction and let data scaling
257                         // occur.
258                         if (ZoomedMinimum <= 0)
259                             ZoomedMinimum = null;
260                         if (UserMinimum <= 0)
261                             UserMinimum = null;
262                         if (ZoomedMaximum <= 0)
263                             ZoomedMaximum = null;
264                         if (UserMaximum <= 0)
265                             UserMaximum = null;
266                     }
267                     raiseEvent();
268                 }
269             }
270         }
271
272         public double? ZoomedMaximum
273         {
274             get
275             {
276                 return zoomedMaximum;
277             }
278             set
279             {
280                 zoomedMaximum = value;
281                 raiseEvent();
282             }
283         }
284
285         public double? ZoomedMinimum
286         {
287             get
288             {
289                 return zoomedMinimum;
290             }
291             set
292             {
293                 zoomedMinimum = value;
294                 raiseEvent();
295             }
296         }
297
298         public bool IntegerAxis
299         {
300             get
301             {
302                 return integerAxis;
303             }
304             set
305             {
306                 integerAxis = value;
307                 raiseEvent();
308             }
309         }
310
311         public IEnumerable<double> GenerateTickLocations(int maxIntervals)
312         {
313             int intervals;
314             double delta = CalculateInterval(ScaleMinimum, ScaleMaximum, maxIntervals, out intervals);
315
316             double start;
317             if ((ScaleMinimum % delta) == 0)
318                 start = ScaleMinimum;
319             else if (ScaleMinimum < 0)
320                 start = ScaleMinimum - (ScaleMinimum % delta);
321             else
322                 start = ScaleMinimum + (delta - (ScaleMinimum % delta));
323
324             for (int i = 0; i < intervals; i++)
325             {
326                 double v = start + (delta * i);
327                 yield return v;
328             }
329         }
330
331         public static double CalculateRoundedMax(double max, double min)
332         {
333             return max + ((max - min) * 0.05);
334         }
335
336         public static double CalculateRoundedMin(double max, double min)
337         {
338             return min - ((max - min) * 0.05);
339         }
340
341         public static bool ToleranceCheck(double a, double b, double tolerance)
342         {
343             return Math.Abs(a - b) < tolerance;
344         }
345
346         private double[] intervalSizes = new double[] {
347             5e8, 2.5e8, 2e8, 1e8,
348             5e7, 2.5e7, 2e7, 1e7, 5e6, 2.5e6, 2e6, 1e6,
349             5e5, 2.5e5, 2e5, 1e5, 5e4, 2.5e4, 2e4, 1e4,
350             5e3, 2.5e3, 2e3, 1e3, 5e2, 2.5e2, 2e2, 1e2,
351             50, 25, 20, 10, 5, 2, 1, 0.5, 0.1, 0.05, 0.01
352         };
353
354         public double CalculateInterval(double min, double max, int maxIntervals, out int intervals)
355         {
356             double delta = (max - min);
357             int i = 0;
358             for (; i < intervalSizes.Length; i++)
359             {
360                 double n = intervalSizes[i];
361                 if (IntegerAxis && n < 1)
362                     break;
363                 double ni = Math.Floor(delta / n) + 1.0;
364                 if (ni > maxIntervals)
365                     break;
366             }
367
368             if (i == 0)
369                 i = 1;
370
371             // let's return i - 1
372             intervals = (int)(Math.Floor(delta / intervalSizes[i - 1])) + 1;
373             return intervalSizes[i - 1];
374
375             /*
376             // Worst case scenario - no intervals
377             intervals = 2;
378             return delta;
379             */
380         }
381     }
382
383     public class NumericYAxis : NumericAxis, YAxis
384     {
385         private string name = null;
386         private SeriesCollection series;
387         private bool rightSide;
388         private PenDescription borderPen;
389
390         public NumericYAxis(string name, SeriesCollection series, bool rightSide, PenDescription borderPen)
391         {
392             this.name = name;
393             this.series = series;
394             this.rightSide = rightSide;
395             this.borderPen = borderPen;
396         }
397
398         protected override float AxisStart(AdvancedRect dataArea)
399         {
400             return dataArea.TopLeft.Y;
401         }
402
403         protected override float AxisLength(AdvancedRect dataArea)
404         {
405             return dataArea.Height;
406         }
407
408         protected override bool AxisReverseOfCoordinateArea()
409         {
410             return true;
411         }
412
413         protected override double? CalculateDataMinimum()
414         {
415             return series.GetMinYForAxis(this.Name);
416         }
417
418         protected override double? CalculateDataMaximum()
419         {
420             return series.GetMaxYForAxis(this.Name);
421         }
422
423         protected override double? CalculateDataMinimumGtZero()
424         {
425             return series.GetMinYGtZeroForAxis(this.Name);
426         }
427
428         public float CalculateWidth(Graphics g)
429         {
430             float width = 0;
431
432             if (title != null)
433             {
434                 // title
435                 using (Font f = titleFont.CreateFont())
436                     width += g.MeasureString(title, f).Height;
437
438                 // Some padding between title and labels
439                 width += 0.02f;
440             }
441
442             // label widths
443             if (logAxis)
444             {
445                 // Longest labels will always be min or max.
446                 using (Font f = labelFont.CreateFont())
447                 {
448                     double min = Math.Pow(10, Math.Ceiling(Math.Log10(ScaleMinimum)));
449                     double max = Math.Pow(10, Math.Floor(Math.Log10(ScaleMaximum)));
450                     SizeF sz1 = g.MeasureString(FormatLabel(min), f);
451                     SizeF sz2 = g.MeasureString(FormatLabel(max), f);
452                     width += Math.Max(sz1.Width, sz2.Width);
453                 }
454             }
455             else
456             {
457                 int intervals;
458                 double delta = CalculateInterval(ScaleMinimum, ScaleMaximum, 10, out intervals);
459                 using (Font f = labelFont.CreateFont())
460                 {
461                     SizeF sz1 = g.MeasureString(FormatLabel(delta), f);
462                     SizeF sz2 = g.MeasureString(FormatLabel(delta * (intervals - 1)), f);
463                     width += Math.Max(sz1.Width, sz2.Width);
464                 }
465             }
466
467             // tick marks
468             width += tickLength;
469
470             return width;
471         }
472
473         public void DrawY(Graphics g, AdvancedRect area, AdvancedRect plotArea)
474         {
475             drawArea = area;
476
477             GraphicsState _s = g.Save();
478
479             //using (Brush br = new SolidBrush(Color.Purple))
480             //    g.FillRectangle(br, area.Rect);
481
482             if (title != null)
483             {
484                 using (Brush br = titleFont.CreateBrush())
485                 using (Font f = titleFont.CreateFont())
486                 {
487                     SizeF titleSize = g.MeasureString(title, f);
488
489                     GraphicsState s = g.Save();
490                     if (rightSide)
491                     {
492                         g.TranslateTransform(area.BottomRight.X, area.Center.Y - (titleSize.Width / 2));
493                         g.RotateTransform(90);
494                     }
495                     else
496                     {
497                         g.TranslateTransform(area.TopLeft.X, area.Center.Y + (titleSize.Width / 2));
498                         g.RotateTransform(-90);
499                     }
500                     StringFormat format = new StringFormat();
501                     format.Alignment = StringAlignment.Center;
502                     format.LineAlignment = StringAlignment.Center;
503                     g.DrawString(title, f, br, new PointF(0, 0));
504                     g.Restore(s);
505                 }
506             }
507
508             if (!logAxis)
509             {
510                 int maxIntervals = (int)Math.Ceiling(area.Height * 1.25);
511
512                 // calculate label and tick mark intervals
513                 using (Brush br = labelFont.CreateBrush())
514                 using (Font f = labelFont.CreateFont())
515                 using (Pen p = tickPen.CreatePen())
516                 {
517                     foreach (double v in GenerateTickLocations(maxIntervals))
518                     {
519                         string txt = FormatLabel(v);
520                         float yCoord = DataToCoordinate(v, area);
521
522                         if (yCoord < area.TopLeft.Y || yCoord > area.BottomRight.Y)
523                             continue;
524
525                         SizeF sz = g.MeasureString(txt, f);
526                         if (rightSide)
527                         {
528                             g.DrawLine(p, area.TopLeft.X, yCoord, area.TopLeft.X + tickLength, yCoord);
529                             g.DrawString(txt, f, br, area.TopLeft.X + tickLength, yCoord - (sz.Height / 2));
530                         }
531                         else
532                         {
533                             g.DrawLine(p, area.BottomRight.X, yCoord, area.BottomRight.X - tickLength, yCoord);
534                             g.DrawString(txt, f, br, area.BottomRight.X - sz.Width - tickLength, yCoord - (sz.Height / 2));
535                         }
536                     }
537                 }
538
539                 if (gridlinesEnabled)
540                 {
541                     using (Pen p = gridlinePen.CreatePen())
542                     {
543                         foreach (double v in GenerateTickLocations(maxIntervals))
544                         {
545                             float yCoord = DataToCoordinate(v, area);
546                             if (yCoord < area.TopLeft.Y || yCoord > area.BottomRight.Y)
547                                 continue;
548                             g.DrawLine(p, plotArea.TopLeft.X, yCoord, plotArea.BottomRight.X, yCoord);
549                         }
550                     }
551                 }
552             }
553             else
554             {
555                 // log axis drawing
556                 int start = (int)Math.Floor(Math.Log10(ScaleMinimum));
557                 int end = (int)Math.Ceiling(Math.Log10(ScaleMaximum));
558
559                 using (Brush br = labelFont.CreateBrush())
560                 using (Font f = labelFont.CreateFont())
561                 using (Pen p = tickPen.CreatePen())
562                 using (Pen p2 = gridlinePen.CreatePen())
563                 {
564                     for (int i = start; i <= end; i++)
565                     {
566                         double v = Math.Pow(10, i);
567                         float yCoord = DataToCoordinate(v, area);
568
569                         // ticks and labels
570                         string txt = FormatLabel(v);
571                         SizeF sz = g.MeasureString(txt, f);
572                         if (yCoord >= area.TopLeft.Y && yCoord <= area.BottomRight.Y)
573                         {
574                             if (rightSide)
575                             {
576                                 g.DrawLine(p, area.TopLeft.X, yCoord, area.TopLeft.X + tickLength, yCoord);
577                                 g.DrawString(txt, f, br, area.TopLeft.X + tickLength, yCoord - (sz.Height / 2));
578                             }
579                             else
580                             {
581                                 g.DrawLine(p, area.BottomRight.X, yCoord, area.BottomRight.X - tickLength, yCoord);
582                                 g.DrawString(txt, f, br, area.BottomRight.X - sz.Width - tickLength, yCoord - (sz.Height / 2));
583                             }
584                         }
585
586                         if (gridlinesEnabled && i != end)
587                         {
588                             double v2 = Math.Pow(10, i + 1);
589                             double delta = (v2 - v) / 9;
590                             for (int j = 1; j < 10; j++)
591                             {
592                                 double v3 = v + (delta * j);
593                                 yCoord = DataToCoordinate(v3, area);
594                                 if (yCoord > area.TopLeft.Y && yCoord < area.BottomRight.Y)
595                                     g.DrawLine(p2, plotArea.TopLeft.X, yCoord, plotArea.BottomRight.X, yCoord);
596                             }
597                         }
598                     }
599                 }
600             }
601
602             // Because there can be multiple Y axises that are not always next to the data area,
603             // we draw our own border.  The plot might stroke over it again later, but that's
604             // not such a big deal.
605             using (Pen p = borderPen.CreatePen())
606             {
607                 if (rightSide)
608                     g.DrawLine(p, area.TopLeft, new PointF(area.TopLeft.X, area.BottomRight.Y));
609                 else
610                     g.DrawLine(p, area.BottomRight, new PointF(area.BottomRight.X, area.TopLeft.Y));
611             }
612            
613             g.Restore(_s);
614         }
615
616         public string Name
617         {
618             get
619             {
620                 return name;
621             }
622         }
623     }
624
625     public class NumericXAxis : NumericAxis, XAxis
626     {
627         private Plot parent;
628
629         public NumericXAxis(Plot parent)
630         {
631             this.parent = parent;
632         }
633
634         protected override float AxisStart(AdvancedRect dataArea)
635         {
636             return dataArea.TopLeft.X;
637         }
638
639         protected override float AxisLength(AdvancedRect dataArea)
640         {
641             return dataArea.Width;
642         }
643
644         protected override bool AxisReverseOfCoordinateArea()
645         {
646             return false;
647         }
648
649         protected override double? CalculateDataMinimum()
650         {
651             return parent.Series.MinX;
652         }
653
654         protected override double? CalculateDataMaximum()
655         {
656             return parent.Series.MaxX;
657         }
658
659         protected override double? CalculateDataMinimumGtZero()
660         {
661             return parent.Series.MinX_gt_Zero;
662         }
663
664         public float CalculateHeight(Graphics g, float maximumWidth)
665         {
666             float height = 0;
667
668             if (title != null)
669             {
670                 // title
671                 using (Font f = titleFont.CreateFont())
672                     height += g.MeasureString(title, f).Height;
673
674                 // Some padding between title and labels
675                 height += 0.02f;
676             }
677
678             // label height
679             using (Font f = labelFont.CreateFont())
680             {
681                 double max = Math.Pow(10, Math.Floor(Math.Log10(ScaleMaximum)));
682                 height += g.MeasureString(FormatLabel(max), f).Height;
683             }
684
685             // tick marks
686             height += tickLength;
687
688             return height;
689         }
690
691         public void DrawX(Graphics g, AdvancedRect area, AdvancedRect plotArea)
692         {
693             drawArea = area;
694
695             GraphicsState _s = g.Save();
696
697             //using (Brush br = new SolidBrush(Color.Purple))
698             //    g.FillRectangle(br, area.Rect);
699
700             if (title != null)
701             {
702                 using (Brush br = titleFont.CreateBrush())
703                 using (Font f = titleFont.CreateFont())
704                 {
705                     SizeF titleSize = g.MeasureString(title, f);
706                     g.DrawString(title, f, br, area.Center.X - (titleSize.Width / 2), area.BottomRight.Y - titleSize.Height);
707                 }
708             }
709
710             if (!logAxis)
711             {
712                 int maxIntervals = (int)Math.Ceiling(area.Width * 1.25);
713
714                 // calculate label and tick mark intervals
715                 using (Brush br = labelFont.CreateBrush())
716                 using (Font f = labelFont.CreateFont())
717                 using (Pen p = tickPen.CreatePen())
718                 {
719                     foreach (double v in GenerateTickLocations(maxIntervals))
720                     {
721                         string txt = FormatLabel(v);
722                         float xCoord = DataToCoordinate(v, area);
723                         SizeF sz = g.MeasureString(txt, f);
724
725                         if (xCoord < area.TopLeft.X || xCoord > area.BottomRight.X)
726                             continue;
727
728                         g.DrawLine(p, xCoord, area.TopLeft.Y, xCoord, area.TopLeft.Y + tickLength);
729                         g.DrawString(txt, f, br, xCoord - (sz.Width / 2), area.TopLeft.Y + tickLength);
730                     }
731                 }
732
733                 if (gridlinesEnabled)
734                 {
735                     using (Pen p = gridlinePen.CreatePen())
736                     {
737                         foreach (double v in GenerateTickLocations(maxIntervals))
738                         {
739                             float xCoord = DataToCoordinate(v, area);
740                             if (xCoord < area.TopLeft.X || xCoord > area.BottomRight.X)
741                                 continue;
742                             g.DrawLine(p, xCoord, plotArea.TopLeft.Y, xCoord, plotArea.BottomRight.Y);
743                         }
744                     }
745                 }
746             }
747             else
748             {
749                 // log axis drawing
750                 int start = (int)Math.Floor(Math.Log10(ScaleMinimum));
751                 int end = (int)Math.Ceiling(Math.Log10(ScaleMaximum));
752
753                 using (Brush br = labelFont.CreateBrush())
754                 using (Font f = labelFont.CreateFont())
755                 using (Pen p = tickPen.CreatePen())
756                 using (Pen p2 = gridlinePen.CreatePen())
757                 {
758                     for (int i = start; i <= end; i++)
759                     {
760                         double v = Math.Pow(10, i);
761                         float xCoord = DataToCoordinate(v, area);
762
763                         // ticks and labels
764                         string txt = FormatLabel(v);
765                         SizeF sz = g.MeasureString(txt, f);
766
767                         if (xCoord >= area.TopLeft.X && xCoord <= area.BottomRight.X)
768                         {
769                             g.DrawLine(p, xCoord, area.TopLeft.Y, xCoord, area.TopLeft.Y + tickLength);
770                             g.DrawString(txt, f, br, xCoord - (sz.Width / 2), area.TopLeft.Y + tickLength);
771                         }
772
773                         if (gridlinesEnabled && i != end)
774                         {
775                             double v2 = Math.Pow(10, i + 1);
776                             double delta = (v2 - v) / 9;
777                             for (int j = 1; j < 10; j++)
778                             {
779                                 double v3 = v + (delta * j);
780                                 xCoord = DataToCoordinate(v3, area);
781                                 if (xCoord > area.TopLeft.X && xCoord < area.BottomRight.X)
782                                     g.DrawLine(p2, xCoord, plotArea.TopLeft.Y, xCoord, plotArea.BottomRight.Y);
783                             }
784                         }
785                     }
786                 }
787             }
788
789             g.Restore(_s);
790         }
791
792     }
793 }
Note: See TracBrowser for help on using the browser.