-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAxisLabelCalculator.java
More file actions
395 lines (346 loc) · 16.9 KB
/
Copy pathAxisLabelCalculator.java
File metadata and controls
395 lines (346 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//package org.freehep.j3d.plot;
import java.io.*;
/**
* @author Joy Kyriakopulos (joyk@fnal.gov)
* @version $Id: AxisLabelCalculator.java 8584 2006-08-10 23:06:37Z duns $
*/
public class AxisLabelCalculator
{
/* VARIABLES */
private double data_min = 0d, data_max = 1d; // actual min/max for the data set
private double plot_min = 0d, plot_max = 1d; // min/max on the axis itself
private AxisLabel[] labels;
private int nDivisions = 0;
private boolean labelsValid;
public void createNewLabels(double min, double max)
{
data_min = min;
data_max = max;
int minNumberOfDivisions = 1;
// int maxNumberOfDivisions = 10; MH 141107
int maxNumberOfDivisions = 8;
double log10 = Math.log(10.0);
// int maxCharsPerLabel = 5; MH 141107
int maxCharsPerLabel = 7;
labelsValid = true;
// log_max is the logarithm of the max value of the data
double log_max = data_max == 0d ? 0d : Math.log(Math.abs(data_max)) / log10;
// int_log_max is the floored integer equivalent of log_max
final int int_log_max = (int) Math.floor(log_max);
// by default, scale_power is 0
// This number represents the amount by which we scale all labels. For example,
// if our range is from 2,000,000,000 to 5,000,000,000 we want scale_power to be
// 9 so that we get 2.0, 2.5, 3.0, ... or something like that on the labels.
int scale_power = 0;
if (int_log_max >= maxCharsPerLabel)
// we have an order of magnitude that can't be displayed in standard form, so we need to set
// a value for scale_power
scale_power = int_log_max;
else if (int_log_max <= -maxCharsPerLabel)
// we have an order of magnitude that can't be displayed in standard form, so we need to set
// a value for scale_power
scale_power = int_log_max;
// System.out.println();
// System.out.println("int_log_max = " + int_log_max + ", maxCharsPerLabel = " + maxCharsPerLabel + ", scale_power = " + scale_power);
final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power);
// the formatter uses scale_power when creating labels
/*
* Our strategy here is based on the observation that plotting the range
* 0.2 to 40 is very similar to the task of plotting the ranges 2 to 400,
* or 0.02 to 4. We scale the min and max by an order of ten such that when
* converted to integers (by a process not quite like trucation) the difference
* between those two integers is a number greater than or equal to 10 and less
* than (but not equal to) 100. In other words, there will be a difference of
* exactly two digits, which is a sensible precision to see in variation between
* axis labels. For example, the ranges listed above would all yield the integer
* pair 1 to 40 (because the difference between those integers has two digits).
* Given a pair of integers, the function proceeds to calculate appropriate labels.
* If we are using the suggested range, we just round the min down to the next nice
* label and we round the max up to the next nice label (unless the max and min are
* already on nice labels).
*/
final double difference = data_max - data_min;
final double pow = Math.floor(Math.log(difference) / log10) - 1.0;
// pow is the power of 10 used to get integers of the appropriate range
int fractDigits = 0;
if (scale_power > 0)
fractDigits = scale_power - (int) pow;
else if (pow < -0.5)
// we use -0.5 instead of 0.0 in case a Math.floor() returns something that should be 0.0
// but is really just barely under 0.0
fractDigits = scale_power - (int) pow;
final double conversion = Math.pow(10.0, pow);
// this is the actual conversion factor we used, stored once to keep from
// having to recalculate it
int intMin = round(data_min / conversion, false);
int intMax = round(data_max / conversion, true);
// we now have intMin and intMax: the integer pair with a two-digit difference
plot_min = data_min;
plot_max = data_max;
final int naturalNumberOfDivisions = intMax - intMin;
// this number has precisely two digits
int nDivisions = 0;
final float idealMinFraction = 0.5f;
// we will allow as few as this fraction of the maximum number of labels if it is convenient
int nUnits = 1;
// this number can represent two things:
// a) if naturalNumberOfDivisions < maxNumberOfDivisions
// it represents the number of divisions (units) between the natural divisions
// b) if naturalNumberOfDivisions > maxNumberOfDivisions
// it represents the number of units between divisions (the number to skip between divisions)
// 1 is the default value, but we will see if a different value might be better
if (naturalNumberOfDivisions < maxNumberOfDivisions)
// we might like to put in some new divisions
{
final float proximity = (float) naturalNumberOfDivisions / (float) maxNumberOfDivisions;
// this number measures how close the natural number is to the maximum
boolean niceDivisionFound = false;
if (proximity < idealMinFraction)
// we want to do something because the number we have is below the range we want
{
final int[] divisions = {2, 4, 5, 10, 20};
// These are the numbers of subdivisions we might want to place between natural divisions.
// The array only goes up to 20 because we would need a plot with at least 200 labels before
// needing to go any higher.
for (int i = 0; i < divisions.length; i++)
{
final int candidate = divisions[i];
if (proximity * candidate <= 1.0)
// this might work, because the number is small enough that we could fit
// this many divisions on the axis
{
niceDivisionFound = true;
nUnits = candidate;
// the next iteration might be even better, so we...
continue;
}
// if we didn't execute continue, it was because we can't fit this many
// divisions on the axis, and so there's no point in trying the next
// iteration either because it's even bigger, so we break out of the loop
break;
}
}
if (niceDivisionFound)
{
nDivisions = naturalNumberOfDivisions * nUnits + (int) ((plot_max / conversion - intMax) * nUnits);
/*
* The first term in the expression isn't tough: If nUnits is 2 then we need twice as many
* divisions on the axis. The second term isn't so obvious. Suppose our axis goes from 0
* to 12.7 and we decide to set nUnits to 2. We therefore get labels 0.0, 0.5, 1.0, ... , 12.0
* but we won't get 12.5 on there. There will instead be empty space where the 12.5 should go.
* The last term accounts for this, by taking the difference between the top label and the
* axis max (in this case 12.7 - 12.0 = 0.7), multiplying bn nUnits (to get 1.4 in this case)
* and truncating (to get 1 in this case). The result (1 in this case) is the number of labels
* extra we need.
*/
if (pow < 0.5 || scale_power > 0)
// we use 0.5 instead of 1.0 in case a Math.floor() returns something
// that should be 1.0 but is really just barely under 1.0
fractDigits++;
// we've gone down to one lover decimal level so we have to tell the formatter
if ((nUnits == 4 || nUnits == 20) && (pow < 1.5 || scale_power > 0)) // we've actually gone down two decimal levels, so...
// we use 1.5 instead of 2.0 in case a Math.floor() returns something
// that should be 2.0 but is really just barely under 2.0
fractDigits++; // we add another
}
else
// we give up and keep the natural number, even though it's smaller that what we'd like
{
nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
}
}
else if (naturalNumberOfDivisions > maxNumberOfDivisions)
// the natural number is larger than what we'd like, so we have to skip over some
// (typically this is the more common problem)
{
nDivisions = 1;
// we supply this initialization to make the compiler happy, but in the algorithm
// requires that this initial value change
final int[] skips = {2, 5, 10, 20, 25, 50};
// These are the numbers of natural divisions we're going to try skipping.
for (int i = 0; i < skips.length; i++)
{
final int nDivisionsThisTry = naturalNumberOfDivisions / skips[i];
if (nDivisionsThisTry > maxNumberOfDivisions)
{
// this many skips isn't big enough, so we'll try the next one
continue;
}
// we now assign to nUnits the number of natural divisions to skip over
nUnits = skips[i];
nDivisions = nDivisionsThisTry;
if (nUnits >= 10 && nUnits != 25 && fractDigits > 0)
// we're skipping at least an order of 10, and we're not in quarters, so...
fractDigits--; // we can get rid of one fraction digit
/*
* We may calculate a new value for the intMin. Consider, for example,
* the range 3 to 17. If we decide that our skip will be 2, we will get
* labels like 3, 5, 7, 9, etc. This will look dumb because we would much rather
* have the first label a nice multiple of our skip (i.e., we would rather
* have 2, 4, 6, 8, etc., or for multiples of 5 we would rather have 20,
* 25, 30, 35, etc. over 18, 23, 28, 33, etc.) Therefore, if the intMin is not
* a nice multiple of that skip then we increase the intMin, and we may have
* to decrement nDivisions because of a lost label.
*/
if (intMin % nUnits != 0)
// only true if we are not using the suggested range
{
// increase is the amount we will increase intMin by to make it a nice multiple of nUnits
final int increase = intMin > 0 ? nUnits - intMin % nUnits : -intMin % nUnits;
if (increase > intMax - (intMax - intMin) / nUnits * nUnits - intMin)
// we have put the last label over the limit, so...
nDivisions--; // we drop the last label
intMin += increase;
}
// We are happy with what we've got because it gives us an acceptable
// number of divisions. We don't want to go any higher because that
// will just make for fewer divisions, so we...
break;
}
}
else // hey! they're exactly equal
nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
double minLabelValue = intMin * conversion;
final double inc = naturalNumberOfDivisions < maxNumberOfDivisions ? conversion / nUnits : conversion * nUnits;
if (naturalNumberOfDivisions < maxNumberOfDivisions && minLabelValue - inc >= plot_min)
// this happens if we are dividing up divisions, and we get divisions below intMin * conversion
{
int nLost = (int) ((minLabelValue - inc) / inc);
minLabelValue -= nLost * inc;
nDivisions += nLost;
}
labels = new AxisLabel[nDivisions + 1];
this.nDivisions = nDivisions;
// System.out.println("fractDigits = " + fractDigits + ", minLabelValue = " + minLabelValue + ", inc = " + inc);
format.setFractionDigits(fractDigits);
for (int j = 0; j < labels.length; j++)
{
final double labelValue = minLabelValue + j * inc;
labels[j] = new AxisLabel();
labels[j].text = format.format(labelValue);
labels[j].position = (labelValue - plot_min) / (plot_max - plot_min);
// labels[j].logPosition = Math.log(labels[j].position + Math.E);
if (labels[j].position < 0.) labels[j].position = 0.;
}
}
private int charsReq(int pow)
/*
* Returns the number of characters required (not using scientific
* notation) to represent a decade of the given order of magnitude.
*/
{
if (pow < 0)
/*
* If the power is less than 0, we need one space for the
* leading zero, one for the decimal, and then -pow spaces
* for each of the following characters. For example, if
* pow = -2 then the result would be 4, because there are
* four characters in the string "0.01".
*/
return -pow + 2;
else
/*
* If the power is 0 or greater, we need one character for
* the character '1' and one '0' for each order of magnitude.
*/
return pow + 1;
}
private int round(final double d, final boolean down)
/*
* Determines an integer value from a double by rounding intelligently.
* In the logarithmic case, when determining the order of magnitude
* of the lowest tick, the "down" parameter is false because
* we want to round up from the minimum point in the axis (so that
* the tick shows up on the range of the axis) if we are not very close
* to an integer value. However, when we are determining the order
* of magnitude of the highest tick, we want to round down if we are
* not very close to an integer so that the tick appears within the
* range of the axis. Similarly in the case for the linear axis, we round
* up to get the smallest tick value and we round down to get the largest
* tick value. We do exactly the opposite if wew are using the suggested
* range. In that case, we round down to get the minimum and up to get
* the maximum.
*/
{
final double minProximity = 0.0001;
/*
* A parameter's proximity to the nearest integer must be this
* fraction of its size in order to be considered that value.
*/
final double round = Math.round(d); // the closest integer value
if (d == round || Math.abs(d - round) < (d != 0.0 ? minProximity * Math.abs(d) : 0.000001))
{
// we assume here that d is close enough to be an integer, so we round and return
return (int) round;
}
else
{
// d is not close enough to be an integer, so we return the
// floor if we were supposed to round down and ceil
// otherwise
return down ? (int) Math.floor(d) : (int) Math.ceil(d);
}
}
public String [] getLabels()
{
int len = labels.length;
String [] lab = new String [len];
for (int i = 0; i < len; ++i)
lab[i] = labels[i].text;
return lab;
}
public double [] getPositions()
{
int len = labels.length;
double [] pos = new double [len];
for (int i = 0; i < len; ++i)
pos[i] = labels[i].position;
return pos;
}
public void printLabels()
{
// System.out.println("data_min = " + data_min + ", data_max = " + data_max);
// System.out.println("plot_min = " + plot_min + ", plot_max = " + plot_max);
// System.out.println("nDivisions = " + nDivisions + ", labelsValid = " + labelsValid);
int i;
for (i = 0; i < labels.length; ++i)
{
System.out.println("label "+ (i+1) + ": " + labels[i].text + " position: " + labels[i].position);
}
}
// This class is used when storing information about
// the labels on the axis.
private class AxisLabel
{
String text;
double position;
// double logPosition;
}
/**
* Just for testing
*/
public static void main(String[] argv)
{
AxisLabelCalculator t = new AxisLabelCalculator();
t.createNewLabels(0., 1.);
t.printLabels();
t.createNewLabels(0., 100.);
t.printLabels();
t.createNewLabels(0.0001, 0.001);
t.printLabels();
t.createNewLabels(5., 10.);
t.printLabels();
t.createNewLabels(.001, .050);
t.printLabels();
t.createNewLabels(.00001, .00005);
t.printLabels();
t.createNewLabels(-.001, .00005);
t.printLabels();
t.createNewLabels(-.001, .005);
t.printLabels();
t.createNewLabels(-2., 5.);
t.printLabels();
t.createNewLabels(.05, 10.);
t.printLabels();
}
}