/* * AxisScalePolicyAutomaticBestFit.java of project jchart2d, <enterpurposehere>. * Copyright (C) 2002 - 2011, Achim Westermann, created on Apr 22, 2011 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * If you modify or optimize the code in a useful way please let me know. * Achim.Westermann@gmx.de * * * File : $Source: /cvsroot/jchart2d/jchart2d/codetemplates.xml,v $ * Date : $Date: 2009/02/24 16:45:41 $ * Version: $Revision: 1.2 $ */ package info.monitorenter.gui.chart.axis.scalepolicy; import info.monitorenter.gui.chart.IAxis; import info.monitorenter.gui.chart.IAxisScalePolicy; import info.monitorenter.gui.chart.LabeledValue; import info.monitorenter.gui.chart.axis.AAxis; import info.monitorenter.util.Range; import info.monitorenter.util.math.MathUtil; import java.awt.Graphics; import java.util.LinkedList; import java.util.List; /** * Very basic and fast scale policy implementation that ensures the following: * <ul> * <li>Every scale tick is a minor or major tick of the corresponding axis.</li> * <li>If a scale tick was found that matches a major and a minor tick it is judged as major tick.</li> * <li>Every major tick is a multiple of minor ticks: It is not possible for the sum minor ticks to "skip" a major tick.</li> * <li>There is no guarantee that the labels of ticks will overwrite each others.</li> * <li>There is no guarantee that the major and minor ticks of the axis are chosen in a reasonable manner: You could get no labels at all if the values are too high or thousands of labels with a weird output.</li> * </ul> * <p> * * * * @author <a href="mailto:Achim.Westermann@gmx.de">Achim Westermann </a> * */ public class AxisScalePolicyAutomaticBestFit implements IAxisScalePolicy { /** * Internally used for rounding to ticks, calculated once per paint iteration. */ protected double m_power; public List<LabeledValue> getScaleValues(final Graphics g2d, final IAxis<?> axis) { final double labelspacepx = axis.getAccessor().getMinimumValueDistanceForLabels(g2d); final double formattingspace = axis.getFormatter().getMinimumValueShiftForChange(); final double max = Math.max(labelspacepx, formattingspace); return this.getLabels(max, axis); } /** * @see info.monitorenter.gui.chart.IAxisScalePolicy#initPaintIteration(info.monitorenter.gui.chart.IAxis) */ public void initPaintIteration(IAxis<?> axis) { // get the powers of ten of the range, a minor Tick of 1.0 has to be // able to be 100 times in a range of 100 (match 1,2,3,... instead of // 10,20,30,.... final double range = axis.getMax() - axis.getMin(); double computeRange = range; if ((range == 0) || !MathUtil.isDouble(range)) { computeRange = 1; } double tmpPower = 0; if (computeRange > 1) { while (computeRange > 50) { computeRange /= 10; tmpPower++; } tmpPower = Math.pow(10, tmpPower - 1); } else { while (computeRange < 5) { computeRange *= 10; tmpPower++; } tmpPower = 1 / Math.pow(10, tmpPower); } this.m_power = tmpPower; } /** * Returns the labels for this axis. * <p> * The labels will have at least the given argument <code>resolution</code> as * distance in the value domain of the chart. * <p> * * @param resolution * the distance in the value domain of the chart that has to be at * least between to labels. * * @return the labels for the axis. */ protected List<LabeledValue> getLabels(final double resolution, final IAxis<?> axis) { final List<LabeledValue> collect = new LinkedList<LabeledValue>(); if (resolution > 0) { final Range domain = axis.getRange(); final double min = domain.getMin(); final double max = domain.getMax(); String oldLabelName = ""; LabeledValue label; final double range = max - min; final double tickResolution = this.roundToTicks(resolution, false, false, axis).getValue(); double value = Math.ceil(min / tickResolution) * tickResolution; // This was value before the patch that prevents the labels from jumping: // It's benefit was that the first label was not this // far from the start of data (in case startMajorTicks of axis is true): // double value = min; String labelName = "start"; int loopStop = 0; boolean firstMajorFound = false; // first tick, manual init while ((value <= max) && (loopStop < 100)) { if (loopStop == 99) { if (AAxis.DEBUG) { System.out.println(axis.getAccessor().toString() + " axis: loop to high"); } } if (oldLabelName.equals(labelName)) { if (AAxis.DEBUG) { System.out.println("constant Label " + labelName); } } label = this.roundToTicks(value, false, !firstMajorFound && axis.isStartMajorTick(), axis); oldLabelName = labelName; labelName = label.getLabel(); value = label.getValue(); loopStop++; if (firstMajorFound || !axis.isStartMajorTick() || label.isMajorTick()) { firstMajorFound = true; if ((value <= max) && (value >= min)) { collect.add(label); } else if (value > max) { if (AAxis.DEBUG) { System.out.println("Dropping label (too high) : (" + label + ")[max: " + max + "]"); } } else if (value < min) { if (AAxis.DEBUG) { System.out.println("Dropping label (too low) : (" + label + ")[min: " + min + "]"); } } } value += resolution; } final int stop = collect.size(); for (int i = 0; i < stop; i++) { label = collect.get(i); label.setValue((label.getValue() - min) / range); } } return collect; } /** * Internal rounding routine. * <p> * Arguments are not chosen to be "understandable" or "usable" but optimized * for performance. * <p> * The <code> findMajorTick</code> argument may be used e.g. to force labels * to start from a major tick. * <p> * * @param value * the value to round. * @param floor * if true, rounding goes to floor else to ceiling. * @param findMajorTick * if true the returned value will be a major tick (which might be * fare more away from the given value than the next major tick). * @return the value rounded to minor or major ticks. */ protected LabeledValue roundToTicks(final double value, final boolean floor, final boolean findMajorTick, final IAxis<?> axis) { final LabeledValue ret = new LabeledValue(); final double minorTick = axis.getMinorTickSpacing() * this.m_power; final double majorTick = axis.getMajorTickSpacing() * this.m_power; double majorRound; if (floor) { majorRound = Math.floor(value / majorTick); } else { majorRound = Math.ceil(value / majorTick); } final boolean majorZeroHit = (majorRound == 0) && (value != 0); majorRound *= majorTick; double minorRound; if (floor) { minorRound = Math.floor(value / minorTick); } else { minorRound = Math.ceil(value / minorTick); } final boolean minorZeroHit = (minorRound == 0) && (value != 0); minorRound *= minorTick; if (majorZeroHit || minorZeroHit) { if (AAxis.DEBUG) { System.out.println("zeroHit"); } } final double minorDistance = Math.abs(value - minorRound); final double majorDistance = Math.abs(value - majorRound); double majorMinorRelation = minorDistance / majorDistance; if (Double.isNaN(majorMinorRelation)) { majorMinorRelation = 1.0; } if ((majorDistance <= minorDistance) || findMajorTick) { ret.setValue(majorRound); ret.setMajorTick(true); } else { ret.setValue(minorRound); ret.setMajorTick(false); } // format label string. ret.setLabel(axis.getFormatter().format(ret.getValue())); // as formatting rounds too, reparse value so that it is exactly at the // point the label string describes. ret.setValue(axis.getFormatter().parse(ret.getLabel()).doubleValue()); return ret; } }