source: nanovis/trunk/Axis.cpp @ 4880

Last change on this file since 4880 was 3611, checked in by ldelgass, 11 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.