source: nanovis/trunk/Axis.cpp @ 5303

Last change on this file since 5303 was 3611, checked in by ldelgass, 7 years ago

Use nv namespace for classes in nanovis rather than prefixing class names with
Nv (still need to convert shader classes).

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