Automatic Legends for Proportional Symbol Maps – Code
This Java code for computing candidate values for inclusion in the legend, and for filtering the candidate values. Candidate values are at "round" numbers between a minimum and a maximum value. The filtered values are a subset of the candidate values, such that no lines are too close to each other when rendered.
A Java Netbeans project with all source code of the applet is available (Netbeans 6.5 project).
/**
* Computes the values for drawing the legend. The new values are returned
* in an array of doubles. Values are ordered in decreasing order.
* @return An array with the potential values.
*/
public double[] computePotentialValues() {
// make sure the minimum is larger than 0
double min = Math.max(1e-10, Math.abs(minimum));
double max = Math.abs(maximum);
if (min == max) {
return new double[]{max};
}
// an index into bases array
int baseID = 0;
// array that will hold the potential values
ArrayList values = new ArrayList();
// add the maximum value to the array
values.add(new Double(max));
/* compute the scale factor to guarantee that minimum > 1
This is possibly more stable than:
while (min < 1.) {
. min *= 10.;
. max *= 10.;
. scale /= 10;
}*/
int scaleExp = (int)Math.ceil(-log10(min));
double scale = pow10(scaleExp);
double min_s = min * scale;
double max_s = max * scale;
// compute the number of digits of the scaled maximum value
int ndigits = (int)Math.floor(log10(max_s));
// find the index into the bases array for the first v smaller than max_s
final double firstLimit = max_s / pow10(ndigits);
for (int i = 0; i < bases.length; ++i) {
if (firstLimit >= bases[i]) {
baseID = i;
break;
}
}
// compute the values following the maximum
while (true) {
// compute the value
double v = min_s;
if (baseID < bases.length) {
v = bases[baseID] * pow10(ndigits);
}
// if the value is smaller than min_s, add the minimum and stop the loop
if (v <= min_s) {
// add minimum value
values.add(new Double(min));
break;
}
// otherwise divide v by the scale. This could be done with:
// v /= scale;
// However, this is numerically less precise and sometimes leads
// to rounding errors due to the limited precision of floating point
// values. The following alternative is numerically more stable:
v = bases[baseID] * pow10(ndigits - scaleExp);
// store the scaled value in the values array
values.add(new Double(v));
// switch to the next base
baseID++;
if (baseID >= bases.length) {
baseID = 0;
--ndigits;
}
}
return ArrayUtils.arrayListToDoubleArray(values);
}
/**
* Filters the passed candidate values, making sure values are not too close
* to each other when rendered.
* @param values All unfiltered values.
* @param dmin The minimum size difference between two symbols.
* @return An array with the filtered values.
*/
public double[] filterValues(double[] values, double dmin) {
// array that will hold the filtered values
double[] filteredValues = new double[values.length];
// add the maximum value
int filteredValuesCount = 0;
filteredValues[filteredValuesCount++] = values[0];
// find the height and value of the smallest acceptable symbol
double lastHeight = 0;
int lastValueID = values.length - 1; // start with the minimum value
while (lastValueID >= 0) {
lastHeight = valueToSymbolHeight(values[lastValueID]);
if (lastHeight > dmin) {
break;
}
--lastValueID;
}
// remember the height of the previously added value
double previousHeight = valueToSymbolHeight(values[0]);
// loop over all values that are large enough
for (int limitID = 1; limitID <= lastValueID; limitID++) {
double v = values[limitID];
// compute the height of the symbol
double h = valueToSymbolHeight(v);
// do not draw the symbol if it is too close to the smallest symbol
// (but is not the smallest symbol itself)
if ((h - lastHeight) < dmin && limitID != lastValueID) {
continue;
}
// do not draw the symbol if it is too close to the previously
// drawn symbol
if ((previousHeight - h) < dmin) {
continue;
}
filteredValues[filteredValuesCount++] = values[limitID];
// remember the height of the last drawn symbol
previousHeight = h;
}
return ArrayUtils.trimLength(filteredValues, filteredValuesCount);
}
/**
* Converts a value to a symbol height. For circles this is twice the radius.
*/
protected double valueToSymbolHeight(double val) {
return Math.sqrt(val * valueScaleFactor) * 2;
}
