New module to perform decimal floating point arithmetic for charts.
[pspp] / src / math / chart-geometry.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2004, 2015 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18 #include <math.h>
19 #include <float.h>
20 #include <assert.h>
21
22 #include "chart-geometry.h"
23 #include "decimal.h"
24 #include <stdlib.h>
25
26 static const double standard_tick[] = {1, 2, 5, 10};
27
28 /* Adjust tick to be a sensible value
29    ie:  ... 0.1,0.2,0.5,   1,2,5,  10,20,50 ... */
30 void
31 chart_rounded_tick (double tick, struct decimal *result)
32 {
33   int i;
34
35   struct decimal ddif = {1, 1000};
36
37   /* Avoid arithmetic problems with very small values */
38   if (fabs (tick) < DBL_EPSILON)
39     {
40       result->ordinate = 0;
41       result->mantissa = 0;
42       return;
43     }
44
45   struct decimal dt;
46   decimal_from_double (&dt, tick);
47   
48   double expd = dec_log10 (&dt) - 1;
49
50   for (i = 0  ; i < 4 ; ++i)
51     {
52       struct decimal candidate;
53       struct decimal delta;
54
55       decimal_init (&candidate, standard_tick[i], expd);
56       
57       delta = dt;
58       decimal_subtract (&delta, &candidate);
59       delta.ordinate = llabs (delta.ordinate);
60
61       if (decimal_cmp (&delta, &ddif) < 0)
62         {
63           ddif = delta;
64           *result = candidate;
65         }
66     }
67 }
68
69 /* 
70    Find a set {LOWER, INTERVAL, N_TICKS} such that:
71
72    LOWER <= LOWDBL,
73    LOWER + INTERVAL > LOWDBL,
74    
75    LOWER + N_TICKS * INTERVAL < HIGHDBL
76    LOWER + (N_TICKS + 1) * INTERVAL >= HIGHDBL
77
78    INTERVAL = X * 10^N
79     where: N is integer 
80     and    X is an element of {1, 2, 5}
81
82    In other words:
83
84          INTERVAL
85          >      <
86      |....+....+....+.      .+....|
87    LOWER  1    2    3     N_TICKS
88         ^LOWDBL                 ^HIGHDBL
89 */
90 void
91 chart_get_scale (double highdbl, double lowdbl,
92                  struct decimal *lower, 
93                  struct decimal *interval,
94                  int *n_ticks)
95 {
96   int i;
97   double fitness = DBL_MAX;
98   *n_ticks = 0;
99   struct decimal high;
100   struct decimal low;
101
102   assert (highdbl >= lowdbl);
103
104   decimal_from_double (&high, highdbl);
105   decimal_from_double (&low, lowdbl);
106   
107   struct decimal diff = high;
108   decimal_subtract (&diff, &low);
109
110   double expd = dec_log10 (&diff) - 2;
111
112   /* Find the most pleasing interval */
113   for (i = 1; i < 4; ++i)
114     {
115       struct decimal clbound = low;
116       struct decimal cubound = high;
117       struct decimal candidate;
118       decimal_init (&candidate, standard_tick[i], expd);
119
120       decimal_divide (&clbound, &candidate);
121       int fl = decimal_floor (&clbound);
122       decimal_int_multiply (&candidate, fl);
123       clbound = candidate;
124
125
126       decimal_init (&candidate, standard_tick[i], expd);
127       decimal_divide (&cubound, &candidate);
128       int fu = decimal_ceil (&cubound);
129       decimal_int_multiply (&candidate, fu);
130
131       cubound = candidate;
132
133       decimal_init (&candidate, standard_tick[i], expd);
134       decimal_subtract (&cubound, &clbound);
135       decimal_divide (&cubound, &candidate);
136
137
138       ord_t n_int = decimal_floor (&cubound);
139
140       /* We prefer to have between 5 and 10 tick marks on a scale */
141       double f = 1 - exp (-0.2 *  fabs (n_int - 7.5) / 7.5);
142
143       if (f < fitness)
144         {
145           fitness = f;
146           *lower = clbound;
147           *interval = candidate;
148           *n_ticks = n_int;
149         }
150     }
151 }