package jas.plot; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; /** * Implements a simple numeric axis, with either linear or logarithmic scale. Users * may set a flag that determines whether the axis may round to close values for simplicity * of labels or whether the axis should keep to the given range. If, for example, the minimum * is set to 4.3 and the maximum is set to 95.9, this class may be configured to round the * extrema so that the labels are 0, 10, 20, ..., 100. Use setUseSuggestedRange(boolean) * to set whether a pretty range is automatically used. * @see #setUseSuggestedRange(boolean) * @author Jonas Gifford */ public final class DoubleAxis extends AxisType implements DoubleCoordinateTransformation { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin constructors */ /** Creates linear axis. */ public DoubleAxis() { this(false); } /** * Creates a new numeric axis. * @param logarithmic whether the axis uses logarithmic (as opposed to liear) scale */ public DoubleAxis(final boolean logarithmic) { this.logarithmic = logarithmic; } /* * end constructors * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin public interface */ public void setLogarithmic(final boolean logarithmic) { if (this.logarithmic != logarithmic) { labelsValid = false; // old labels are useless now, so on next layout new ones will be created this.logarithmic = logarithmic; if (axis != null) axis.revalidate(); } } public boolean isLogarithmic() { return logarithmic; } public void setUseSuggestedRange(final boolean useSuggestedRange) { if (this.useSuggestedRange != useSuggestedRange) { labelsValid = false; // old labels are useless now, so on next layout new ones will be created this.useSuggestedRange = useSuggestedRange; if (axis != null) axis.revalidate(); } } public boolean getUseSuggestedRange() { return useSuggestedRange; } public void setMin(final double data_min) { if (this.data_min != data_min) { labelsValid = false; // old labels are useless now, so on next layout new ones will be created this.data_min = data_min; this.plot_min = data_min; if (axis != null) axis.revalidate(); } } public void setMax(final double data_max) { if (this.data_max != data_max) { labelsValid = false; // old labels are useless now, so on next layout new ones will be created this.data_max = data_max; this.plot_max = data_max; if (axis != null) axis.revalidate(); } } /** Returns the minimum of the axis range. */ public double getPlotMin() { return plot_min; } /** Returns the maximum of the axis range. */ public double getPlotMax() { return plot_max; } /** * Returns the minimum of the data on the axis, as given * to the method setMin(). * @see #setMin(double) */ public double getDataMin() { return data_min; } /** * Returns the maximum of the data on the axis, as given * to the method setMax(). * @see #setMax(double) */ public double getDataMax() { return data_max; } /* * end public interface * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin private methods */ private int stringWidth(final FontMetrics fm, final String s) { if (s.startsWith("e")) return fm.stringWidth(s) + fm.stringWidth("10") - fm.charWidth('e'); else return fm.stringWidth(s); } 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 un 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 we 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); } } private boolean areEqual(double d1, double d2) { // returns whether they are close enough to be considered equal return d1 == d2 || Math.abs(d2 - d1) < Math.abs(d1 != 0.0 ? 0.0001 * d1 : 0.000001); } 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 void createNewLabels(final int maxNumberOfDivisions) { 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. 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; final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power); // the formatter uses scale_power when creating labels if (logarithmic) { // log_min is the logarithm of the min value of the data double log_min = data_min == 0d ? 0d : Math.log(Math.abs(data_min)) / log10; int min_int = round(log_min, useSuggestedRange); // min_int is the order of magnitude of the smallest major tick // If min is a multiple of 10, min_int will be the order of // magnitude of data_min. Otherwise, it will be the next highest // order of magnitude. However, if we are using the // suggested range, we want the next lowest, not the next highest. int max_int = round(log_max, !useSuggestedRange); // max_int is the order of magnitude of the largest major tick. // If we are using the suggested range, we want the next highest, // not the next lowest final int nDecades = max_int - min_int + 1; // nDecades is the number of major ticks we have on the plot if (nDecades < (useSuggestedRange ? 3 : 2)) // if we can't get at least two decade marks in the range we put // some ugly (but correct) labels on the axis spaced approximately evenly // we require at least 3 if the suggested range is used because it // rounds such that there are at least two by default and we want at least // one more { tryForNewLabelsOnExpansion = true; // when we resize, we may want new labels minorTickPositions = null; // no minor ticks final int pow = (int) Math.floor(Math.log(data_max - data_min) / log10) - (maxNumberOfDivisions > 40 ? 2 : 1); // we will divide numbers by this many factors of 10, round using // Math.floor(), and multiply again by this many factors of ten, thus // getting a series of numbers truncated to a fixed point if (scale_power > 0 || pow < 0) format.setFractionDigits(scale_power - pow); else // we don't need any fraction digits in out result format.setFractionDigits(0); final double mag = Math.pow(10.0, pow); // mag is the scaling factor (this is just to keep from having to recalculate each time) nDivisions = Math.max(maxNumberOfDivisions, 2); if (useSuggestedRange) { plot_min = Math.floor(data_min / mag) * mag; // we set a value for the plot_min which is rounded down from our data_min at the precision specified by mag plot_max = Math.ceil(data_max / mag) * mag; // we set a value for the plot_max which is rounded up from our data_max at the precision specified by mag // We want log_min and log_max to reflect extrema on the axis range, not for the data. We have to update // these values to reflect a new range log_min = Math.log(plot_min) / log10; log_max = Math.log(plot_max) / log10; // create a new label array labels = new AxisLabel[nDivisions]; } else { // because we're not using the suggested range, plot_min and plot_max are the same as data_min and data_max // (note that log_min and log_max are already correct) plot_min = data_min; plot_max = data_max; // lastLabel is a potential label value that we will include if it fits final double lastLabel = Math.ceil(Math.pow(10.0, log_max) / mag) * mag; if (lastLabel < data_max || areEqual(lastLabel, data_max)) // if the last label is safely within the data range, we create an array // that will include that label labels = new AxisLabel[nDivisions]; else // we create an array that will not include that last label labels = new AxisLabel[nDivisions - 1]; } for (int i = 0; i < labels.length; i++) { double labelValue = Math.ceil(Math.pow(10.0, i * (log_max - log_min) / (nDivisions - 1) + log_min) / mag) * mag; // we multiply the number by mag, floor, then divide by mag labels[i] = new AxisLabel(); labels[i].text = format.format(labelValue); labels[i].position = (Math.log(labelValue) / log10 - log_min) / (log_max - log_min); } } else { scale_power = 0; // reset any settings because we don't scale logs int nLabels = nDecades; // we will initially have exactly one label per decade // (we may decrease this if skip, defined below, increases) int skip = 1; // skip is the number of orders of magnitude between tick marks if (useSuggestedRange) { // max_int and min_int are the powers of ten for the axis range, so once cast to double, they // will do just fine as log_min and log_max, which are supposed to be the logs of the axis boundaries log_min = min_int; log_max = max_int; // the plot range is different from the data range: plot_min = Math.pow(10.0, min_int); plot_max = Math.pow(10.0, max_int); } else { // the plot range is equal to the data range plot_min = data_min; plot_max = data_max; } if (nDecades > maxNumberOfDivisions && maxNumberOfDivisions > 1) // we have a problem: we have more decades than we have room for ticks // time to set a good value for skip { // if the axis is extended, we may want new labels (i.e., to reflect a smaller skip) so... tryForNewLabelsOnExpansion = true; while (skip + 1 < nDecades && nDecades / skip >= maxNumberOfDivisions - 1) // we just increement skip until we find something that works skip++; if (skip != 1) { // we now have fewer labels nLabels = nDecades / skip; if (useSuggestedRange) // we have to round things to make the labels land exactly on the ends { // mod represents how much the axis min currently goes past the currently highest multiple of skip final int mod = (max_int - min_int) % skip; if (mod != 0) // we need to move the max up so that it is a multiple of skip { max_int += skip - mod; // now plot_max and log_max must be increased to reflect rounding up the max plot_max = Math.pow(10.0, max_int); log_max = max_int; } // by using the suggested range, we guarantee that there will be a label exactly on either end // this involved having (nDecades + 1) labels, so... nLabels++; } // For various reasons, we might not have enough labels, so... if (min_int + nLabels * skip <= max_int) // if we don't have enough, we add another nLabels++; } } else // We have no skip, and therefore no way of inserting new labels between the ones we have, so there is // no point in calculating new labels if we resize. Therefore, ... tryForNewLabelsOnExpansion = false; nDivisions = nLabels; labels = new AxisLabel[nLabels]; if (charsReq(min_int) > maxCharsPerLabel || charsReq(max_int) > maxCharsPerLabel) // if we can't fit the full label in, we use scientific notation { int power = min_int; for (int i = 0; i < nLabels; i++) { labels[i] = new AxisLabel(); // the label text for 10^6 is "e6" // we therefore concatenate the power as a string to the string "e" labels[i].text = "e".concat(String.valueOf(power)); labels[i].position = (power - log_min) / (log_max - log_min); power += skip; } } else // we are going to display the whole labels { if (min_int >= 0) // all our numbers are greater than or equal to 1, so all // we have to do is add zeros to one string buffer { // empty the buffer b.setLength(0); b.append('1'); for (int j = 0; j < min_int; j++) b.append('0'); // we now have a buffer initialized for the first label for (int i = 0; i < nLabels; i++) { labels[i] = new AxisLabel(); labels[i].text = b.toString(); labels[i].position = (i * skip + min_int - log_min) / (log_max - log_min); // now, we set it up for the next label for (int j = 0; j < skip; j++) b.append('0'); } } else if (max_int < 0) // all of our labels are less than or equal to 0.1, so we can just insert '0' characters into a single buffer { // empty the buffer b.setLength(0); // initialize the buffer b.append("0.1"); for (int i = max_int; i < -1; i++) b.insert(2, '0'); // for each order less than -1 we insert a '0' after the '.' for (int i = nLabels - 1; i >= 0; i--) { labels[i] = new AxisLabel(); labels[i].text = b.toString(); labels[i].position = (i * skip + min_int - log_min) / (log_max - log_min); // set up for the next label for (int j = 0; j < skip; j++) b.insert(2, '0'); // insert another '0' for each skip } } else // this isn't so simple because we can't just use one string // buffer for all of the labels { int power = min_int; // this is the order of magnitude of the current label for (int i = 0; i < nLabels; i++) { b.setLength(0); if (power < 0) // we have to create a decimal number { b.append("0."); for (int j = -1; j > power; j--) b.append('0'); // for every order of magnitude less than -1 we // add a zero b.append('1'); } else // our number is 1 or greater { b.append('1'); for (int j = 0; j < power; j++) b.append('0'); // for every order of magnitude we add a zero } labels[i] = new AxisLabel(); labels[i].text = b.toString(); labels[i].position = (power - log_min) / (log_max - log_min); // increment power for the next label power += skip; } } } // create minor ticks if (skip == 1) // our task is to create 8 minor ticks between each decade mark { double minorTickBase = areEqual(min_int, log_min) ? plot_min : Math.pow(10.0, min_int - 1); // tickMarkBase is the multiple of ten that the minor ticks themselves are multiples of. // Between two decades marks, the eight minor ticks are at: // 2 * tickMarkBase, 3 * tickMarkBase, 4 * tickMarkBase, 5 * tickMarkBase, // 6 * tickMarkBase, 7 * tickMarkBase, 8 * tickMarkBase, 9 * tickMarkBase // and tickMarkBase is the value of the smaller of the surrounding decade marks. // Therefore, between the marks 100 and 1000, we use the eight multiples above times // the smaller decade (100) to get the following minor tick values: // 200, 300, 400, 500, 600, 700, 800, 900 // We calculate the initial value of minorTickBase by taking 10 to the power of (min_int - 1) // if the min_int is not equal to the log_min (i.e., if the axis minimum is not a multiple of 10) // and plot_min is the axis minimum is a multiple of 10. When we go to the next decade to put the // marks there, we will just multiply minorTickBase by 10 to get the next value. final double initialIndex = plot_min / minorTickBase; final double initialIndex_ceil = Math.ceil(initialIndex); int index = initialIndex_ceil > initialIndex ? (int) initialIndex_ceil : (int) initialIndex_ceil + 1; // The code above merely calculates the initial value for index. index is the multiple of minorTick // base for a given minor tick. It rolls within the inclusive range 2:9. int nMinorTicks = (max_int - min_int) * 8; // so far we have 8 minor ticks for each decade interval if (! useSuggestedRange) // we may have extras because there is space after the last decade and befer the first // where maybe we will need minor ticks { // increase nMinorTicks by the number that are needed below the first decade mark nMinorTicks += (int) ((Math.pow(10.0, min_int) - data_min) / Math.pow(10.0, min_int - 1)); // increase nMinorTicks by the number that are needed after the last decade mark nMinorTicks += (int) ((data_max - Math.pow(10.0, max_int)) / Math.pow(10.0, max_int)); } minorTickPositions = new double[nMinorTicks]; for (int i = 0; i < nMinorTicks; i++) { if (index > 9) // we have come to the end of one decade interval, so... { // we restart index at 2 and... index = 2; // increase minorTickBase for the new decade minorTickBase *= 10.0; } // we assign the position of this minor tick minorTickPositions[i] = (Math.log(index * minorTickBase) / log10 - log_min) / (log_max - log_min); // we increment index for the next tick index++; } } else // We have a skip other than 1, so we will try to put minor ticks on decade evenly spaced // between the labels (major ticks). { minorTickPositions = null; // initially null; we'll see if we can get a nice pattern for (int nMinorDivisions = Math.min(6 /* max */, skip); nMinorDivisions > 1; nMinorDivisions--) { if (skip % nMinorDivisions != 0) // this number won't work, so we... continue; // By this point we know we have a number that will work. We assign an array to // minorTickPositions (which was previously null) and we break from the loop. // minorSkip is the number of decades between minor ticks final int minorSkip = skip / nMinorDivisions; // nMinorTicks is the number of minor ticks int nMinorTicks = (nMinorDivisions - 1) * (max_int - min_int) / skip; // tickPower is the power of ten for the current tick // we have set it here to the value for the first tick int tickPower = min_int + minorSkip; minorTickPositions = new double[nMinorTicks]; for (int i = 0; i < nMinorTicks; i++) { if ((tickPower - min_int) % skip == 0) // we have actually landed on a major tick, so we skip over to the next minorTick value tickPower += minorSkip; minorTickPositions[i] = (tickPower - log_min) / (log_max - log_min); tickPower += minorSkip; } break; } } } } else { /* * 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). */ tryForNewLabelsOnExpansion = true; minorTickPositions = null; // no minor ticks 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, useSuggestedRange); int intMax = round(data_max / conversion, !useSuggestedRange); // we now have intMin and intMax: the integer pair with a two-digit difference if (useSuggestedRange) { plot_min = intMin * conversion; plot_max = intMax * conversion; } else { plot_min = data_min; plot_max = data_max; } final int naturalNumberOfDivisions = intMax - intMin; // this number has precisely two digits int nDivisions; 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) { if (useSuggestedRange) { nDivisions = naturalNumberOfDivisions * nUnits; } else { 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 (useSuggestedRange) { // changed is a flag that indicates if the number of divisions has changed boolean changed = false; if (intMin % nUnits != 0) // intMin is not a multiple of nUnits, so we... { // decrease it so that it is a multiple, intMin -= intMin % nUnits; // adjust plot_min accordingly, and plot_min = intMin * conversion; // set the flag that the range has changed changed = true; } if (intMax % nUnits != 0) // intMax is not a multiple of nUnits, so we... { // increase it so that it is a multiple, intMax += nUnits - intMax % nUnits; // adjust plot_max accordingly, and plot_max = intMax * conversion; // set the flag that the range has changed changed = true; } if (changed) // we need to recalculate the number of divisions { nDivisions = (intMax - intMin) / nUnits; } } 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; } if (nDivisions < 0) nDivisions = 0; if (nDivisions > maxNumberOfDivisions) nDivisions = maxNumberOfDivisions; labels = new AxisLabel[nDivisions + 1]; this.nDivisions = nDivisions; format.setFractionDigits(fractDigits); for (int j = 0; j < labels.length; j++) { final double labelValue = minLabelValue + j * inc; // this method of finding labelValue is more expensive than the method in the // previous version (i.e., the last version checked in to source safe) but this method // avoids some miniscule rounding problems we had in the previous version labels[j] = new AxisLabel(); labels[j].text = format.format(labelValue); labels[j].position = (labelValue - plot_min) / (plot_max - plot_min); } } } private int calculateMaxNDivisions(final FontMetrics fm, final int axisLength) { int result; if (axis.getAxisOrientation() == Axis.HORIZONTAL) result = axisLength / (fm.charWidth('5') * maxCharsPerLabel + minSpaceBetweenLabels); // assume '5' has typical width else result = axisLength / (fm.getHeight() + minSpaceBetweenLabels); return Math.max(minNumberOfDivisions, result); } private int getPowerOffset(final FontMetrics fm) // returns how far the term indicating power is below the regular labels (horizontal axis only) { // kind of an arbitrary return value (we just want some number that relates to font size) return fm.getAscent() / 2; } /* * end private methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin AxisType methods */ void assumeAxisLength(final int axisLength) { Font font = axis.getFont(); final FontMetrics fm = axis.getToolkit().getFontMetrics(font); final int maxNumberOfDivisions = calculateMaxNDivisions(fm, axisLength); if (!labelsValid || labels == null || nDivisions < 2 || nDivisions > maxNumberOfDivisions || tryForNewLabelsOnExpansion && nDivisions < maxNumberOfDivisions / 2) { createNewLabels(maxNumberOfDivisions); } // we are going to indicate that the minor ticks should be hidden if there are minor ticks and // the number of divisions is high enough that the ticks would be crammed too close together hideMinorTicks = minorTickPositions == null || nDivisions > maxNumberOfDivisions / 3; final int lastLabelLocation = (int) ((1.0 - labels[labels.length - 1].position) * axisLength); if (axis.getAxisOrientation() == Axis.VERTICAL) { spaceRequirements.width = longestStringLength(fm,labels) + Axis.padFromAxis; spaceRequirements.flowPastEnd = fm.getMaxAscent() - fm.getAscent() / 2 - lastLabelLocation; if (logarithmic && labels[0].text.startsWith("e")) { spaceRequirements.width += fm.stringWidth("10") - fm.charWidth('e'); spaceRequirements.flowPastEnd += fm.getHeight() / 2; } if (scale_power != 0) { spaceRequirements.width = Math.max(fm.stringWidth("x10") + fm.stringWidth(String.valueOf(scale_power)), spaceRequirements.width); spaceRequirements.flowPastEnd += minSpaceBetweenLabels; if (spaceRequirements.flowPastEnd < 0) spaceRequirements.flowPastEnd = 0; spaceRequirements.flowPastEnd += fm.getAscent(); } else if (spaceRequirements.flowPastEnd < 0) spaceRequirements.flowPastEnd = 0; final int lineHeight = isLogarithmic() && labels[0].text.startsWith("e") ? fm.getHeight() / 2 + fm.getAscent() : fm.getAscent(); spaceRequirements.height = Math.max(lineHeight / 2 - (int) (labels[0].position * axisLength), 0); } else { spaceRequirements.width = Math.max(stringWidth(fm, labels[0].text) / 2 - (int) (labels[0].position * axisLength), 0); spaceRequirements.flowPastEnd = stringWidth(fm, labels[labels.length - 1].text) / 2 - lastLabelLocation; final int lineHeight = isLogarithmic() && labels[0].text.startsWith("e") ? fm.getHeight() / 2 + fm.getMaxAscent() + fm.getDescent() : fm.getMaxAscent() + fm.getDescent(); spaceRequirements.height = lineHeight + Axis.padFromAxis; if (scale_power != 0) { spaceRequirements.height = Math.max(fm.getHeight() / 2 + fm.getAscent() + getPowerOffset(fm), spaceRequirements.height); spaceRequirements.flowPastEnd += minSpaceBetweenLabels; if (spaceRequirements.flowPastEnd < 0) spaceRequirements.flowPastEnd = 0; spaceRequirements.flowPastEnd += fm.stringWidth("x10") + fm.stringWidth(String.valueOf(scale_power)); } else if (spaceRequirements.flowPastEnd < 0) spaceRequirements.flowPastEnd = 0; } } /** * Returns an instance of DoubleCoordinateTransformation. * @see DoubleCoordinateTransformation */ public CoordinateTransformation getCoordinateTransformation() { return this; } void paintAxis(final PlotGraphics g, final double originX, final double originY, final double length, final Color textColor, final Color majorTickColor, final Color minorTickColor) { final boolean isHorizontal = axis.getAxisOrientation() == Axis.HORIZONTAL; final boolean onLeft = (!isHorizontal) && axis.onLeftSide; final double minL = isHorizontal ? originX : originY; final double maxL = isHorizontal ? originX + length : originY - length; final FontMetrics fm = g.getFontMetrics(); final int lineHeight = fm.getHeight(); final int ascent = fm.getAscent(); final int maxAscent = fm.getMaxAscent(); double lastLabel = 0; if (labels == null) return; if (logarithmic) { final boolean isInExp = labels[0].text.startsWith("e"); for (int i = 0; i < labels.length; i++) { String text = labels[i].text; String pow = null; double textLength; if (isInExp) { pow = text.substring(1); text = "10"; textLength = fm.stringWidth(text) + fm.stringWidth(pow); } else { textLength = fm.stringWidth(text); } double pos = minL + (int) (labels[i].position * (maxL - minL)); if (isHorizontal) { g.setColor(majorTickColor); g.drawLine(pos, originY + majorTickLength, pos, originY - majorTickLength); double y = originY + maxAscent + Axis.padFromAxis; pos -= textLength / 2; g.setColor(textColor); if (isInExp) { g.drawString(pow, pos + fm.stringWidth(text), y); y += lineHeight / 2; } g.drawString(text, pos, y); } else { g.setColor(majorTickColor); g.drawLine(originX - majorTickLength, pos, originX + majorTickLength, pos); final double x = onLeft ? originX - textLength - Axis.padFromAxis : originX + Axis.padFromAxis; pos += ascent / 2; g.setColor(textColor); if (isInExp) { // the "10" is centered in the tick mark // the exponent is higher by lineHeight / 2 g.drawString(pow, x + fm.stringWidth(text), pos - lineHeight / 2); } g.drawString(text, x, pos); } } } else { for (int i = 0; i < labels.length; i++) { final String text = labels[i].text; final double pos = minL + labels[i].position * (maxL - minL); if (isHorizontal) { g.setColor(majorTickColor); g.drawLine(pos, originY + majorTickLength, pos, originY - majorTickLength); final double y = originY + maxAscent + Axis.padFromAxis; g.setColor(textColor); g.drawString(text, pos - fm.stringWidth(text) / 2, y); } else { g.setColor(majorTickColor); g.drawLine(originX - majorTickLength, pos, originX + majorTickLength, pos); final double x = onLeft ? originX - fm.stringWidth(text) - Axis.padFromAxis : originX + Axis.padFromAxis; g.setColor(textColor); g.drawString(text, x, pos + ascent/2); } } lastLabel = minL + labels[labels.length-1].position*(maxL-minL); } if (scale_power != 0) { g.setColor(textColor); final String s = String.valueOf(scale_power); if (isHorizontal) { final double y = originY + ascent + getPowerOffset(fm) + 2; final double x = axis.getSize().width - Axis.padAroundEdge - fm.stringWidth(s) + spaceRequirements.flowPastEnd; g.drawString("x10", x - fm.stringWidth("x10"), y + lineHeight / 2); g.drawString(s, x, y + lineHeight / 5); // g.drawString(s, x, y); } else { // final double y = ascent + Axis.padAroundEdge; // final double y1 = originY - length - lineHeight; final double y = lineHeight/2; final double x = onLeft ? originX - fm.stringWidth(s) : originX; g.drawString("x10", onLeft ? x - fm.stringWidth("x10") : x, y + lineHeight / 2); g.drawString(s, onLeft ? x : x + fm.stringWidth("x10"), y + lineHeight / 5); // g.drawString(s, onLeft ? x : x + fm.stringWidth("x10"), y); } } if (!hideMinorTicks && minorTickPositions != null && minorTickPositions.length != 0) { g.setColor(minorTickColor); if (isHorizontal) { for (int i = 0; i < minorTickPositions.length; i++) { final double x = minL + (int) (minorTickPositions[i] * (maxL - minL)); g.drawLine(x, originY + minorTickLength, x, originY - minorTickLength); } } else { for (int i = 0; i < minorTickPositions.length; i++) { final double y = minL + (int) (minorTickPositions[i] * (maxL - minL)); g.drawLine(originX - minorTickLength, y, originX + minorTickLength,y); } } } } int getMajorTickMarkLength() { return majorTickLength; } /* * end AxisType methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin coordinate transformation methods */ // Convert between axis coordinate (double) and drawing coordinates (pixels); public double convert(double d) { final int minL = axis.getMinLocation(); final int maxL = axis.getMaxLocation(); if (logarithmic) { final double min = Math.log(plot_min) / log10; final double max = Math.log(plot_max) / log10; if(d > 0) d = Math.log(d) / log10; else d = min; final double f = (d - min) / (max - min); return minL + f*(maxL - minL); } else { final double f = (d - plot_min) / (plot_max - plot_min); return minL + f*(maxL - minL); } } public double unConvert(double d) { final int minL = axis.getMinLocation(); final int maxL = axis.getMaxLocation(); final double f = (d - minL) / (maxL - minL); if (logarithmic) { final double min = Math.log(plot_min) / log10; final double max = Math.log(plot_max) / log10; return plot_min + f*(max - min); } else { return plot_min + f*(plot_max - plot_min); } } public double getIntersection() { return plot_min; } /* * end coordinate transformation methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin externalization methods */ public void writeExternal(final ObjectOutput out) throws IOException { out.writeBoolean(logarithmic); out.writeBoolean(useSuggestedRange); } public void readExternal(final ObjectInput in) throws IOException { logarithmic = in.readBoolean(); useSuggestedRange = in.readBoolean(); } /* * end externalization methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * begin private member fields */ /* CONSTANTS */ private final StringBuffer b = new StringBuffer(); private final double log10 = Math.log(10.0); private final int majorTickLength = 5; private final int minorTickLength = 3; private final int maxCharsPerLabel = 5; private final int minSpaceBetweenLabels = 3; private final int minNumberOfDivisions = 1; /* VARIABLES */ private int nDivisions = 0; 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 double[] minorTickPositions; private boolean logarithmic; private int scale_power; private boolean useSuggestedRange = false; private boolean tryForNewLabelsOnExpansion; // This will only be false if we have a logarithmic axis where all the decades are showing. // Otherwise, such an axis will recalculate new labels each time it is expanded. private boolean hideMinorTicks = false; /* * end private member fields * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ }