/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.utils.statistics; import java.util.ArrayList; /** * Some helper methods for calculating and rendering graphs. * @author Rinke * */ public class GraphHelper { /** * maximum number of points on the x-axis. */ private static final int MAX_X_RANGE_VALUE = 24; /** * minimal number of points on the x-axis. */ private static final int MIN_X_RANGE_VALUE = 8; /** * reasonable category widths for the getAxisCategory method. (we don't want x-axis labels like 0 - 2.17 - 4.34 - etc etc) */ private static final double FACTORS[] = { 1, 2, 5 }; /** * calculates the optimal division of a a range of number into a reasonable amount of axis categories.<br> * Example: if the x-axis of a graph should run from 0 to 1000, how to divide this into a reasonable number of ticks over the x-axis? Clearly not * all 1000 points can be drawn. The method takes the start and end value of the x-axis domain, and calculates what the x-axis ticks should show * to get not too many, and not too few points. Makes use of the FACTORS constant.<br> * Tested up to a range of a million wide, but should theoretically work up to infinity (so for any range). * @param start a Number indicating the start of the x-axis range. * @param end a Number indicating the end of the x-axis range. Swapping with start (so start is the highest and end is the lowest value) is no * problem. * @return a range of number to be put on the x-axis of the graph, in one array of numbers. */ public static Number[] getAxisCategories(final Number start, final Number end) { final double highest = Math.max(start.doubleValue(), end.doubleValue()); final double lowest = Math.min(start.doubleValue(), end.doubleValue()); final double difference = highest - lowest; double newDifference = difference; double factor = 1; if (difference > MAX_X_RANGE_VALUE) { // first determine the log scale, as steps of 10 are the most nice. final double logBase = 10.0; final double logScale = Math.floor(Math.log(difference) / Math.log(logBase)); factor = Math.pow(logBase, logScale); // now start repeating or multiplying until in range. newDifference = difference / factor; if (newDifference < MIN_X_RANGE_VALUE) { factor = factor / logBase; newDifference = difference / factor; } boolean success = false; int tenFactor = 0; do { // multiply the category width (= factor) by the next item in factors until we get a reasonable amount of categories for (final double element : FACTORS) { final double newfactor = factor * element * Math.pow(10.0, tenFactor); newDifference = difference / newfactor; if (newDifference <= MAX_X_RANGE_VALUE) { factor = newfactor; success = true; break; } } // if still no success, multiply the factors array by 10 (or 100, 1000, 10000, etc) until we are successful tenFactor++; } while (!success); } final int categoryNumber = (int) Math.round(newDifference); final Number[] resultArray = new Number[categoryNumber + 1]; final int direction = (start.doubleValue() < end.doubleValue()) ? 1 : -1; for (int i = 0; i < categoryNumber + 1; i++) { resultArray[i] = start.doubleValue() + (i * direction * factor); } return resultArray; } /** * Creates a nice range around a "midpoint", based on the optimum number of points along the x-axis. The created range will have a width 80% of * the maximum width as defined in * * <pre> * MAX_X_RANGE_VALUE * </pre> * * . * @param midPoint a Number, being the point around which the range is built. Note that this method only makes much sense when midPoint is an * integer. * @param percentile a double; the midPoint is on the * * <pre> * percentile * </pre> * * percentile of the range. Example: if percentile is 33, the "midPoint" is on 1/3 of the range, having 1/3 of the points smaller, and 2/3 of the * points larger than midPoint. * @param precision an int indicating the scale factor. A MidPoint of 0 and a percentile of 50 creates -9, -8 ... 0.. 8, 9 when scaling is 0; it * creates -90, -80... 80, 90 when scaling is 1. Scaling may be negative too. * @param rangeWidth a double indicating the width of the range, as a fraction of the maximum range width MAX_X_RANGE_VALUE. A value of 0.8 means * that the rangeWidth is 80% of the max range width. From this, it follows that values must be between 0 and 1, or else an * IllegalArgumenException is thrown. * @param lowerLimit a Double indicating the lowest value a point in the range can get. Any points in the range lower than this limit are just * skipped / left out. This parameter may be null, which means that it is just ignored. * @throws IllegalArgumentException if rangeWidth < 0 or > 1; and if percentile < 0 or > 100 */ public static Number[] getOptimalRangeAround(final Number midPoint, final double percentile, final int precision, final double rangeWidth, final Double lowerLimit) { if (percentile < 0 || percentile > 100) { throw new IllegalArgumentException("Percentile must be value between 0 and 100."); } double lowerPart = percentile * MAX_X_RANGE_VALUE * Math.pow(10.0, precision) / 100.0; double upperPart = (100.0 - percentile) * MAX_X_RANGE_VALUE * Math.pow(10.0, precision) / 100.0; // apply rangeWidth if (rangeWidth < 0 || rangeWidth > 1) { throw new IllegalArgumentException("Rangewidth must be value between 0 and 1."); } lowerPart = rangeWidth * lowerPart; upperPart = rangeWidth * upperPart; if (midPoint instanceof Integer) { final Number start = Math.round((midPoint.doubleValue() - lowerPart) / Math.pow(10.0, precision)) * Math.pow(10.0, precision); final Number end = Math.round((midPoint.doubleValue() + upperPart) / Math.pow(10.0, precision)) * Math.pow(10.0, precision); final double step = Math.pow(10.0, precision); final ArrayList<Number> result = new ArrayList<Number>(); double next = start.doubleValue(); while (next < end.doubleValue()) { if (lowerLimit == null || next >= lowerLimit) { result.add(next); } next += step; } final Number[] resultArray = new Number[result.size()]; return result.toArray(resultArray); } return getAxisCategories(midPoint.doubleValue() - lowerPart, midPoint.doubleValue() + upperPart); } }