source: trunk/packages/vizservers/nanovis/Axis.cpp @ 2808

Last change on this file since 2808 was 2808, checked in by ldelgass, 8 years ago

Fix log scale axis ticks: arguments to Ticks::SetValues? were out of order.

  • Property svn:eol-style set to native
File size: 14.2 KB
Line 
1/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2#include <stdlib.h>
3#include <stdio.h>
4#include <math.h>
5#include <float.h>
6#include <string.h>
7#include "Axis.h"
8
9NaN _NaN;
10
11inline bool DEFINED(double x) {
12    return !isnan(x);
13}
14
15inline double EXP10(double x) {
16    return pow(10.0, x);
17}
18
19inline int ROUND(double x) {
20    return (int)round(x);
21}
22
23inline double UROUND(double x, double u) {
24    return (ROUND((x)/(u)))*u;
25}
26
27inline double UCEIL(double x, double u) {
28    return (ceil((x)/(u)))*u;
29}
30
31inline double UFLOOR(double x, double u) {
32    return (floor((x)/(u)))*u;
33}
34
35/*
36 * ----------------------------------------------------------------------
37 *
38 * NiceNum --
39 *
40 *      Reference: Paul Heckbert, "Nice Numbers for Graph Labels",
41 *                 Graphics Gems, pp 61-63. 
42 *
43 *      Finds a "nice" number approximately equal to x.
44 *
45 * ----------------------------------------------------------------------
46 */
47static double
48NiceNum(
49    double x,
50    int round)                  /* If non-zero, round. Otherwise take ceiling
51                                 * of value. */
52{
53    double expt;                /* Exponent of x */
54    double frac;                /* Fractional part of x */
55    double nice;                /* Nice, rounded fraction */
56
57    expt = floor(log10(x));
58    frac = x / EXP10(expt);     /* between 1 and 10 */
59    if (round) {
60        if (frac < 1.5) {
61            nice = 1.0;
62        } else if (frac < 3.0) {
63            nice = 2.0;
64        } else if (frac < 7.0) {
65            nice = 5.0;
66        } else {
67            nice = 10.0;
68        }
69    } else {
70        if (frac <= 1.0) {
71            nice = 1.0;
72        } else if (frac <= 2.0) {
73            nice = 2.0;
74        } else if (frac <= 5.0) {
75            nice = 5.0;
76        } else {
77            nice = 10.0;
78        }
79    }
80    return nice * EXP10(expt);
81}
82
83void 
84Ticks::SetTicks(void)
85{
86    numTicks_ = 0;
87    ticks_ = new float[nSteps_];
88    if (step_ == 0.0) { 
89        /* Hack: A zero step indicates to use log values. */
90        unsigned int i;
91        /* Precomputed log10 values [1..10] */
92        static double logTable[] = {
93            0.0, 
94            0.301029995663981, 
95            0.477121254719662, 
96            0.602059991327962, 
97            0.698970004336019, 
98            0.778151250383644, 
99            0.845098040014257,
100            0.903089986991944, 
101            0.954242509439325, 
102            1.0
103        };
104        for (i = 0; i < nSteps_; i++) {
105            ticks_[i] = logTable[i];
106        }
107    } else {
108        double value;
109        unsigned int i;
110   
111        value = initial_;       /* Start from smallest axis tick */
112        for (i = 0; i < nSteps_; i++) {
113            value = initial_ + (step_ * i);
114            ticks_[i] = UROUND(value, step_);
115        }
116    }
117    numTicks_ = nSteps_;
118}
119
120Axis::Axis(const char *axisName) :
121    major_(5), minor_(2) 
122{
123    name_ = NULL;
124    name(axisName);
125    units_ = NULL;
126    title_ = NULL;
127    valueMin_ = DBL_MAX, valueMax_ = -DBL_MAX;
128    min_ = DBL_MAX, max_ = -DBL_MAX;
129    reqMin_ = reqMax_ = _NaN;
130    range_ = 0.0, scale_ = 0.0;
131    reqStep_ = 0.0;
132    flags_ = AUTOSCALE;
133}
134
135/*
136 * ----------------------------------------------------------------------
137 *
138 * InRange --
139 *
140 *      Determines if a value lies within a given range.
141 *
142 *      The value is normalized by the current axis range. If the normalized
143 *      value is between [0.0..1.0] then it's in range.  The value is compared
144 *      to 0 and 1., where 0.0 is the minimum and 1.0 is the maximum.
145 *      DBL_EPSILON is the smallest number that can be represented on the host
146 *      machine, such that (1.0 + epsilon) != 1.0.
147 *
148 *      Please note, *max* can't equal *min*.
149 *
150 * Results:
151 *      If the value is within the interval [min..max], 1 is returned; 0
152 *      otherwise.
153 *
154 * ----------------------------------------------------------------------
155 */
156bool
157Axis::InRange(double x)
158{
159    if (range_ < DBL_EPSILON) {
160        return (fabs(max_ - x) >= DBL_EPSILON);
161    } else {
162        x = (x - min_) * scale_;
163        return ((x >= -DBL_EPSILON) && ((x - 1.0) < DBL_EPSILON));
164    }
165}
166
167void
168Axis::FixRange(double min, double max)
169{
170    if (min == DBL_MAX) {
171        if (DEFINED(reqMin_)) {
172            min = reqMin_;
173        } else {
174            min = (flags_ & LOGSCALE) ? 0.001 : 0.0;
175        }
176    }
177    if (max == -DBL_MAX) {
178        if (DEFINED(reqMax_)) {
179            max = reqMax_;
180        } else {
181            max = 1.0;
182        }
183    }
184    if (min >= max) {
185        /*
186         * There is no range of data (i.e. min is not less than max), so
187         * manufacture one.
188         */
189        if (min == 0.0) {
190            min = 0.0, max = 1.0;
191        } else {
192            max = min + (fabs(min) * 0.1);
193        }
194    }
195
196    /*   
197     * The axis limits are either the current data range or overridden by the
198     * values selected by the user with the -min or -max options.
199     */
200    valueMin_ = (DEFINED(reqMin_)) ? reqMin_ : min;
201    valueMax_ = (DEFINED(reqMax_)) ? reqMax_ : max;
202    if (valueMax_ < valueMin_) {
203        /*   
204         * If the limits still don't make sense, it's because one limit
205         * configuration option (-min or -max) was set and the other default
206         * (based upon the data) is too small or large.  Remedy this by making
207         * up a new min or max from the user-defined limit.
208         */
209        if (!DEFINED(reqMin_)) {
210            valueMin_ = valueMax_ - (fabs(valueMax_) * 0.1);
211        }
212        if (!DEFINED(reqMax_)) {
213            valueMax_ = valueMin_ + (fabs(valueMax_) * 0.1);
214        }
215    }
216}
217
218/*
219 * ----------------------------------------------------------------------
220 *
221 * LogScale --
222 *
223 *      Determine the range and units of a log scaled axis.
224 *
225 *      Unless the axis limits are specified, the axis is scaled
226 *      automatically, where the smallest and largest major ticks encompass
227 *      the range of actual data values.  When an axis limit is specified,
228 *      that value represents the smallest(min)/largest(max) value in the
229 *      displayed range of values.
230 *
231 *      Both manual and automatic scaling are affected by the step used.  By
232 *      default, the step is the largest power of ten to divide the range in
233 *      more than one piece.
234 *
235 *      Automatic scaling:
236 *      Find the smallest number of units which contain the range of values.
237 *      The minimum and maximum major tick values will be represent the
238 *      range of values for the axis. This greatest number of major ticks
239 *      possible is 10.
240 *
241 *      Manual scaling:
242 *      Make the minimum and maximum data values the represent the range of
243 *      the values for the axis.  The minimum and maximum major ticks will be
244 *      inclusive of this range.  This provides the largest area for plotting
245 *      and the expected results when the axis min and max values have be set
246 *      by the user (.e.g zooming).  The maximum number of major ticks is 20.
247 *
248 *      For log scale, there's the possibility that the minimum and
249 *      maximum data values are the same magnitude.  To represent the
250 *      points properly, at least one full decade should be shown.
251 *      However, if you zoom a log scale plot, the results should be
252 *      predictable. Therefore, in that case, show only minor ticks.
253 *      Lastly, there should be an appropriate way to handle numbers
254 *      <=0.
255 *
256 *          maxY
257 *            |    units = magnitude (of least significant digit)
258 *            |    high  = largest unit tick < max axis value
259 *      high _|    low   = smallest unit tick > min axis value
260 *            |
261 *            |    range = high - low
262 *            |    # ticks = greatest factor of range/units
263 *           _|
264 *        U   |
265 *        n   |
266 *        i   |
267 *        t  _|
268 *            |
269 *            |
270 *            |
271 *       low _|
272 *            |
273 *            |_minX________________maxX__
274 *            |   |       |      |       |
275 *     minY  low                        high
276 *           minY
277 *
278 *
279 *      numTicks = Number of ticks
280 *      min = Minimum value of axis
281 *      max = Maximum value of axis
282 *      range    = Range of values (max - min)
283 *
284 *      If the number of decades is greater than ten, it is assumed
285 *      that the full set of log-style ticks can't be drawn properly.
286 *
287 * Results:
288 *      None
289 *
290 * ---------------------------------------------------------------------- */
291void
292Axis::LogScale()
293{
294    double range;
295    double tickMin, tickMax;
296    double majorStep, minorStep;
297    int nMajor, nMinor;
298    double min, max;
299
300    nMajor = nMinor = 0;
301    /* Suppress compiler warnings. */
302    majorStep = minorStep = 0.0;
303    tickMin = tickMax = _NaN;
304    min = valueMin_, max = valueMax_;
305    if (min < max) {
306        min = (min != 0.0) ? log10(fabs(min)) : 0.0;
307        max = (max != 0.0) ? log10(fabs(max)) : 1.0;
308
309        tickMin = floor(min);
310        tickMax = ceil(max);
311        range = tickMax - tickMin;
312       
313        if (range > 10) {
314            /* There are too many decades to display a major tick at every
315             * decade.  Instead, treat the axis as a linear scale.  */
316            range = NiceNum(range, 0);
317            majorStep = NiceNum(range / major_.reqNumTicks, 1);
318            tickMin = UFLOOR(tickMin, majorStep);
319            tickMax = UCEIL(tickMax, majorStep);
320            nMajor = (int)((tickMax - tickMin) / majorStep) + 1;
321            minorStep = EXP10(floor(log10(majorStep)));
322            if (minorStep == majorStep) {
323                nMinor = 4, minorStep = 0.2;
324            } else {
325                nMinor = ROUND(majorStep / minorStep) - 1;
326            }
327        } else {
328            if (tickMin == tickMax) {
329                tickMax++;
330            }
331            majorStep = 1.0;
332            nMajor = (int)(tickMax - tickMin + 1); /* FIXME: Check this. */
333           
334            minorStep = 0.0;    /* This is a special hack to pass
335                                 * information to the SetTicks
336                                 * method. An interval of 0.0 indicates
337                                 *      1) this is a minor sweep and
338                                 *      2) the axis is log scale. 
339                                 */
340            nMinor = 10;
341        }
342        if ((flags_ & TIGHT_MIN) || (DEFINED(reqMin_))) {
343            tickMin = min;
344            nMajor++;
345        }
346        if ((flags_ & TIGHT_MAX) || (DEFINED(reqMax_))) {
347            tickMax = max;
348        }
349    }
350    major_.SetValues(floor(tickMin), majorStep, nMajor);
351    minor_.SetValues(minorStep, minorStep, nMinor);
352    min_ = tickMin;
353    max_ = tickMax;
354    range_ = max_ - min_;
355    scale_ = 1.0 / range_;
356}
357
358/*
359 * ----------------------------------------------------------------------
360 *
361 * Axis::LinearScale --
362 *
363 *      Determine the units of a linear scaled axis.
364 *
365 *      The axis limits are either the range of the data values mapped
366 *      to the axis (autoscaled), or the values specified by the -min
367 *      and -max options (manual).
368 *
369 *      If autoscaled, the smallest and largest major ticks will
370 *      encompass the range of data values.  If the -loose option is
371 *      selected, the next outer ticks are choosen.  If tight, the
372 *      ticks are at or inside of the data limits are used.
373 *
374 *      If manually set, the ticks are at or inside the data limits
375 *      are used.  This makes sense for zooming.  You want the
376 *      selected range to represent the next limit, not something a
377 *      bit bigger.
378 *
379 *      Note: I added an "always" value to the -loose option to force
380 *            the manually selected axes to be loose. It's probably
381 *            not a good idea.
382 *
383 *          maxY
384 *            |    units = magnitude (of least significant digit)
385 *            |    high  = largest unit tick < max axis value
386 *      high _|    low   = smallest unit tick > min axis value
387 *            |
388 *            |    range = high - low
389 *            |    # ticks = greatest factor of range/units
390 *           _|
391 *        U   |
392 *        n   |
393 *        i   |
394 *        t  _|
395 *            |
396 *            |
397 *            |
398 *       low _|
399 *            |
400 *            |_minX________________maxX__
401 *            |   |       |      |       |
402 *     minY  low                        high
403 *           minY
404 *
405 *      numTicks = Number of ticks
406 *      min = Minimum value of axis
407 *      max = Maximum value of axis
408 *      range    = Range of values (max - min)
409 *
410 * Results:
411 *      None.
412 *
413 * Side Effects:
414 *      The axis tick information is set.  The actual tick values will
415 *      be generated later.
416 *
417 * ----------------------------------------------------------------------
418 */
419void
420Axis::LinearScale()
421{
422    double step;
423    double tickMin, tickMax;
424    unsigned int nTicks;
425
426    nTicks = 0;
427    step = 1.0;
428    /* Suppress compiler warning. */
429    tickMin = tickMax = 0.0;
430    if (valueMin_ < valueMax_) {
431        double range;
432
433        range = valueMax_ - valueMin_;
434        /* Calculate the major tick stepping. */
435        if (reqStep_ > 0.0) {
436            /* An interval was designated by the user.  Keep scaling it until
437             * it fits comfortably within the current range of the axis.  */
438            step = reqStep_;
439            while ((2 * step) >= range) {
440                step *= 0.5;
441            }
442        } else {
443            range = NiceNum(range, 0);
444            step = NiceNum(range / major_.reqNumTicks, 1);
445        }
446       
447        /* Find the outer tick values. Add 0.0 to prevent getting -0.0. */
448        tickMin = floor(valueMin_ / step) * step + 0.0;
449        tickMax = ceil(valueMax_ / step) * step + 0.0;
450       
451        nTicks = ROUND((tickMax - tickMin) / step) + 1;
452    } 
453    major_.SetValues(tickMin, step, nTicks);
454
455    /*
456     * The limits of the axis are either the range of the data ("tight") or at
457     * the next outer tick interval ("loose").  The looseness or tightness has
458     * to do with how the axis fits the range of data values.  This option is
459     * overridden when the user sets an axis limit (by either -min or -max
460     * option).  The axis limit is always at the selected limit (otherwise we
461     * assume that user would have picked a different number).
462     */
463    min_ = ((flags_ & TIGHT_MIN)||(DEFINED(reqMin_))) ? valueMin_ : tickMin;
464    max_ = ((flags_ & TIGHT_MAX)||(DEFINED(reqMax_))) ? valueMax_ : tickMax;
465    range_ = max_ - min_;
466    scale_ = 1.0 / range_;
467
468    /* Now calculate the minor tick step and number. */
469
470    if ((minor_.reqNumTicks > 0) && (minor_.autoscale())) {
471        nTicks = minor_.reqNumTicks - 1;
472        step = 1.0 / (nTicks + 1);
473    } else {
474        nTicks = 0;             /* No minor ticks. */
475        step = 0.5;             /* Don't set the minor tick interval to
476                                 * 0.0. It makes the GenerateTicks routine
477                                 * create minor log-scale tick marks.  */
478    }
479    minor_.SetValues(step, step, nTicks);
480}
481
482
483void
484Axis::SetScale(double min, double max)
485{
486    FixRange(min, max);
487    if (flags_ & LOGSCALE) {
488        LogScale();
489    } else {
490        LinearScale();
491    }
492    major_.SweepTicks();
493    minor_.SweepTicks();
494    MakeTicks();
495}
496
497void
498Axis::MakeTicks(void)
499{
500    major_.Reset();
501    minor_.Reset();
502    int i;
503    for (i = 0; i < major_.numTicks(); i++) {
504        double t1, t2;
505        int j;
506       
507        t1 = major_.tick(i);
508        /* Minor ticks */
509        for (j = 0; j < minor_.numTicks(); j++) {
510            t2 = t1 + (major_.step() * minor_.tick(j));
511            if (!InRange(t2)) {
512                continue;
513            }
514            if (t1 == t2) {
515                continue;       // Don't add duplicate minor ticks.
516            }
517            minor_.Append(t2);
518        }
519        if (!InRange(t1)) {
520            continue;
521        }
522        major_.Append(t1);
523    }
524}
525
526double
527Axis::Map(double x)
528{
529    if ((flags_ & LOGSCALE) && (x != 0.0)) {
530        x = log10(fabs(x));
531    }
532    /* Map graph coordinate to normalized coordinates [0..1] */
533    x = (x - min_) * scale_;
534    if (flags_ & DESCENDING) {
535        x = 1.0 - x;
536    }
537    return x;
538}
539
540double
541Axis::InvMap(double x)
542{
543    if (flags_ & DESCENDING) {
544        x = 1.0 - x;
545    }
546    x = (x * range_) + min_;
547    if (flags_ & LOGSCALE) {
548        x = EXP10(x);
549    }
550    return x;
551}
Note: See TracBrowser for help on using the repository browser.