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;

    }