1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.chart;
  27 
  28 import java.text.DecimalFormat;
  29 import java.text.ParseException;
  30 import java.util.ArrayList;
  31 import java.util.Collections;
  32 import java.util.List;
  33 
  34 import javafx.animation.KeyFrame;
  35 import javafx.animation.KeyValue;
  36 import javafx.beans.property.BooleanProperty;
  37 import javafx.beans.property.BooleanPropertyBase;
  38 import javafx.beans.property.DoubleProperty;
  39 import javafx.beans.property.SimpleStringProperty;
  40 import javafx.beans.property.StringProperty;
  41 import javafx.beans.value.ChangeListener;
  42 import javafx.beans.value.WritableValue;
  43 import javafx.geometry.Dimension2D;
  44 import javafx.geometry.Side;
  45 import javafx.util.Duration;
  46 import javafx.util.StringConverter;
  47 
  48 import com.sun.javafx.charts.ChartLayoutAnimator;
  49 
  50 import javafx.css.StyleableDoubleProperty;
  51 import javafx.css.CssMetaData;
  52 
  53 import com.sun.javafx.css.converters.SizeConverter;
  54 
  55 import javafx.css.Styleable;
  56 import javafx.css.StyleableProperty;
  57 
  58 /**
  59  * A axis class that plots a range of numbers with major tick marks every "tickUnit". You can use any Number type with
  60  * this axis, Long, Double, BigDecimal etc.
  61  * @since JavaFX 2.0
  62  */
  63 public final class NumberAxis extends ValueAxis<Number> {
  64 
  65     private Object currentAnimationID;
  66     private final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);
  67     private StringProperty currentFormatterProperty = new SimpleStringProperty(this, "", "");
  68     private DefaultFormatter defaultFormatter = new DefaultFormatter(this);
  69 
  70     // -------------- PUBLIC PROPERTIES --------------------------------------------------------------------------------
  71 
  72     /** When true zero is always included in the visible range. This only has effect if auto-ranging is on. */
  73     private BooleanProperty forceZeroInRange = new BooleanPropertyBase(true) {
  74         @Override protected void invalidated() {
  75             // This will effect layout if we are auto ranging
  76             if(isAutoRanging()) {
  77                 requestAxisLayout();
  78                 invalidateRange();
  79             }
  80         }
  81 
  82         @Override
  83         public Object getBean() {
  84             return NumberAxis.this;
  85         }
  86 
  87         @Override
  88         public String getName() {
  89             return "forceZeroInRange";
  90         }
  91     };
  92     public final boolean isForceZeroInRange() { return forceZeroInRange.getValue(); }
  93     public final void setForceZeroInRange(boolean value) { forceZeroInRange.setValue(value); }
  94     public final BooleanProperty forceZeroInRangeProperty() { return forceZeroInRange; }
  95 
  96     /**  The value between each major tick mark in data units. This is automatically set if we are auto-ranging. */
  97     private DoubleProperty tickUnit = new StyleableDoubleProperty(5) {
  98         @Override protected void invalidated() {
  99             if(!isAutoRanging()) {
 100                 invalidateRange();
 101                 requestAxisLayout();
 102             }
 103         }
 104         
 105         @Override
 106         public CssMetaData<NumberAxis,Number> getCssMetaData() {
 107             return StyleableProperties.TICK_UNIT;
 108         }
 109 
 110         @Override
 111         public Object getBean() {
 112             return NumberAxis.this;
 113         }
 114 
 115         @Override
 116         public String getName() {
 117             return "tickUnit";
 118         }
 119     };
 120     public final double getTickUnit() { return tickUnit.get(); }
 121     public final void setTickUnit(double value) { tickUnit.set(value); }
 122     public final DoubleProperty tickUnitProperty() { return tickUnit; }
 123 
 124     // -------------- CONSTRUCTORS -------------------------------------------------------------------------------------
 125 
 126     /**
 127      * Create a auto-ranging NumberAxis
 128      */
 129     public NumberAxis() {}
 130 
 131     /**
 132      * Create a non-auto-ranging NumberAxis with the given upper bound, lower bound and tick unit
 133      *
 134      * @param lowerBound The lower bound for this axis, ie min plottable value
 135      * @param upperBound The upper bound for this axis, ie max plottable value
 136      * @param tickUnit The tick unit, ie space between tickmarks
 137      */
 138     public NumberAxis(double lowerBound, double upperBound, double tickUnit) {
 139         super(lowerBound, upperBound);
 140         setTickUnit(tickUnit);
 141     }
 142 
 143     /**
 144      * Create a non-auto-ranging NumberAxis with the given upper bound, lower bound and tick unit
 145      *
 146      * @param axisLabel The name to display for this axis
 147      * @param lowerBound The lower bound for this axis, ie min plottable value
 148      * @param upperBound The upper bound for this axis, ie max plottable value
 149      * @param tickUnit The tick unit, ie space between tickmarks
 150      */
 151     public NumberAxis(String axisLabel, double lowerBound, double upperBound, double tickUnit) {
 152         super(lowerBound, upperBound);
 153         setTickUnit(tickUnit);
 154         setLabel(axisLabel);
 155     }
 156 
 157     // -------------- PROTECTED METHODS --------------------------------------------------------------------------------
 158 
 159     /**
 160      * Get the string label name for a tick mark with the given value
 161      *
 162      * @param value The value to format into a tick label string
 163      * @return A formatted string for the given value
 164      */
 165     @Override protected String getTickMarkLabel(Number value) {
 166         StringConverter<Number> formatter = getTickLabelFormatter();
 167         if (formatter == null) formatter = defaultFormatter;
 168         return formatter.toString(value);
 169     }
 170 
 171     /**
 172      * Called to get the current axis range.
 173      *
 174      * @return A range object that can be passed to setRange() and calculateTickValues()
 175      */
 176     @Override protected Object getRange() {
 177         return new Object[]{
 178             getLowerBound(),
 179             getUpperBound(),
 180             getTickUnit(),
 181             getScale(),
 182             currentFormatterProperty.get()
 183         };
 184     }
 185 
 186     /**
 187      * Called to set the current axis range to the given range. If isAnimating() is true then this method should
 188      * animate the range to the new range.
 189      *
 190      * @param range A range object returned from autoRange()
 191      * @param animate If true animate the change in range
 192      */
 193     @Override protected void setRange(Object range, boolean animate) {
 194         final Object[] rangeProps = (Object[]) range;
 195         final double lowerBound = (Double)rangeProps[0];
 196         final double upperBound = (Double)rangeProps[1];
 197         final double tickUnit = (Double)rangeProps[2];
 198         final double scale = (Double)rangeProps[3];
 199         final String formatter = (String)rangeProps[4];
 200         currentFormatterProperty.set(formatter);
 201         final double oldLowerBound = getLowerBound();
 202         setLowerBound(lowerBound);
 203         setUpperBound(upperBound);
 204         setTickUnit(tickUnit);
 205         if(animate) {
 206             animator.stop(currentAnimationID);
 207             currentAnimationID = animator.animate(
 208                 new KeyFrame(Duration.ZERO,
 209                         new KeyValue(currentLowerBound, oldLowerBound),
 210                         new KeyValue(scalePropertyImpl(), getScale())
 211                 ),
 212                 new KeyFrame(Duration.millis(700),
 213                         new KeyValue(currentLowerBound, lowerBound),
 214                         new KeyValue(scalePropertyImpl(), scale)
 215                 )
 216             );
 217         } else {
 218             currentLowerBound.set(lowerBound);
 219             setScale(scale);
 220         }
 221     }
 222 
 223     /**
 224      * Calculate a list of all the data values for each tick mark in range
 225      *
 226      * @param length The length of the axis in display units
 227      * @param range A range object returned from autoRange()
 228      * @return A list of tick marks that fit along the axis if it was the given length
 229      */
 230     @Override protected List<Number> calculateTickValues(double length, Object range) {
 231         final Object[] rangeProps = (Object[]) range;
 232         final double lowerBound = (Double)rangeProps[0];
 233         final double upperBound = (Double)rangeProps[1];
 234         final double tickUnit = (Double)rangeProps[2];
 235         List<Number> tickValues = new ArrayList<>();
 236         if (lowerBound == upperBound) {
 237             tickValues.add(lowerBound);
 238         } else if (tickUnit <= 0) {
 239             tickValues.add(lowerBound);
 240             tickValues.add(upperBound);
 241         } else if (tickUnit > 0) {
 242             tickValues.add(lowerBound);
 243             if (((upperBound - lowerBound) / tickUnit) > 2000) {
 244                 // This is a ridiculous amount of major tick marks, something has probably gone wrong
 245                 System.err.println("Warning we tried to create more than 2000 major tick marks on a NumberAxis. " +
 246                         "Lower Bound=" + lowerBound + ", Upper Bound=" + upperBound + ", Tick Unit=" + tickUnit);
 247             } else {
 248                 if (lowerBound + tickUnit < upperBound) {
 249                     // If tickUnit is integer, start with the nearest integer
 250                     double first = Math.rint(tickUnit) == tickUnit ? Math.ceil(lowerBound) : lowerBound + tickUnit;
 251                     for (double major = first; major < upperBound; major += tickUnit) {
 252                         if (!tickValues.contains(major)) {
 253                             tickValues.add(major);
 254                         }
 255                     }
 256                 }
 257             }
 258             tickValues.add(upperBound);
 259         }
 260         return tickValues;
 261     }
 262 
 263     /**
 264      * Calculate a list of the data values for every minor tick mark
 265      *
 266      * @return List of data values where to draw minor tick marks
 267      */
 268     protected List<Number> calculateMinorTickMarks() {
 269         final List<Number> minorTickMarks = new ArrayList<>();
 270         final double lowerBound = getLowerBound();
 271         final double upperBound = getUpperBound();
 272         final double tickUnit = getTickUnit();
 273         final double minorUnit = tickUnit/Math.max(1, getMinorTickCount());
 274         if (tickUnit > 0) {
 275             if(((upperBound - lowerBound) / minorUnit) > 10000) {
 276                 // This is a ridiculous amount of major tick marks, something has probably gone wrong
 277                 System.err.println("Warning we tried to create more than 10000 minor tick marks on a NumberAxis. " +
 278                         "Lower Bound=" + getLowerBound() + ", Upper Bound=" + getUpperBound() + ", Tick Unit=" + tickUnit);
 279                 return minorTickMarks;
 280             }
 281             final boolean tickUnitIsInteger = Math.rint(tickUnit) == tickUnit;
 282             if (tickUnitIsInteger) {
 283                 for (double minor = Math.floor(lowerBound) + minorUnit; minor < Math.ceil(lowerBound); minor += minorUnit) {
 284                     if (minor > lowerBound) {
 285                         minorTickMarks.add(minor);
 286                     }
 287                 }
 288             }
 289             double major = tickUnitIsInteger ? Math.ceil(lowerBound) : lowerBound;
 290             for (; major < upperBound; major += tickUnit)  {
 291                 final double next = Math.min(major + tickUnit, upperBound);
 292                 for (double minor = major + minorUnit; minor < next; minor += minorUnit) {
 293                     minorTickMarks.add(minor);
 294                 }
 295             }
 296         }
 297         return minorTickMarks;
 298     }
 299 
 300     /**
 301      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 302      *
 303      * @param value tick mark value
 304      * @param range range to use during calculations
 305      * @return size of tick mark label for given value
 306      */
 307     @Override protected Dimension2D measureTickMarkSize(Number value, Object range) {
 308         final Object[] rangeProps = (Object[]) range;
 309         final String formatter = (String)rangeProps[4];
 310         return measureTickMarkSize(value, getTickLabelRotation(), formatter);
 311     }
 312 
 313     /**
 314      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 315      *
 316      * @param value     tick mark value
 317      * @param rotation  The text rotation
 318      * @param numFormatter The number formatter
 319      * @return size of tick mark label for given value
 320      */
 321     private Dimension2D measureTickMarkSize(Number value, double rotation, String numFormatter) {
 322         String labelText;
 323         StringConverter<Number> formatter = getTickLabelFormatter();
 324         if (formatter == null) formatter = defaultFormatter;
 325         if(formatter instanceof DefaultFormatter) {
 326             labelText = ((DefaultFormatter)formatter).toString(value, numFormatter);
 327         } else {
 328             labelText = formatter.toString(value);
 329         }
 330         return measureTickMarkLabelSize(labelText, rotation);
 331     }
 332 
 333     /**
 334      * Called to set the upper and lower bound and anything else that needs to be auto-ranged
 335      *
 336      * @param minValue The min data value that needs to be plotted on this axis
 337      * @param maxValue The max data value that needs to be plotted on this axis
 338      * @param length The length of the axis in display coordinates
 339      * @param labelSize The approximate average size a label takes along the axis
 340      * @return The calculated range
 341      */
 342     @Override protected Object autoRange(double minValue, double maxValue, double length, double labelSize) {
 343         final Side side = getEffectiveSide();
 344         // check if we need to force zero into range
 345         if (isForceZeroInRange()) {
 346             if (maxValue < 0) {
 347                 maxValue = 0;
 348             } else if (minValue > 0) {
 349                 minValue = 0;
 350             }
 351         }
 352         final double range = maxValue-minValue;
 353         // pad min and max by 2%, checking if the range is zero
 354         final double paddedRange = (range==0) ? 2 : Math.abs(range)*1.02;
 355         final double padding = (paddedRange - range) / 2;
 356         // if min and max are not zero then add padding to them
 357         double paddedMin = minValue - padding;
 358         double paddedMax = maxValue + padding;
 359         // check padding has not pushed min or max over zero line
 360         if ((paddedMin < 0 && minValue >= 0) || (paddedMin > 0 && minValue <= 0)) {
 361             // padding pushed min above or below zero so clamp to 0
 362             paddedMin = 0;
 363         }
 364         if ((paddedMax < 0 && maxValue >= 0) || (paddedMax > 0 && maxValue <= 0)) {
 365             // padding pushed min above or below zero so clamp to 0
 366             paddedMax = 0;
 367         }
 368         // calculate the number of tick-marks we can fit in the given length
 369         int numOfTickMarks = (int)Math.floor(length/labelSize);
 370         // can never have less than 2 tick marks one for each end
 371         numOfTickMarks = Math.max(numOfTickMarks, 2);
 372         // calculate tick unit for the number of ticks can have in the given data range
 373         double tickUnit = paddedRange/(double)numOfTickMarks;
 374         // search for the best tick unit that fits
 375         double tickUnitRounded = 0;
 376         double minRounded = 0;
 377         double maxRounded = 0;
 378         int count = 0;
 379         double reqLength = Double.MAX_VALUE;
 380         String formatter = "0.00000000";
 381         // loop till we find a set of ticks that fit length and result in a total of less than 20 tick marks
 382         while (reqLength > length || count > 20) {
 383             int exp = (int)Math.floor(Math.log10(tickUnit));
 384             final double mant = tickUnit / Math.pow(10, exp);
 385             double ratio = mant;
 386             if (mant > 5d) {
 387                 exp++;
 388                 ratio = 1;
 389             } else if (mant > 1d) {
 390                 ratio = mant > 2.5 ? 5 : 2.5;
 391             }
 392             if (exp > 1) {
 393                 formatter = "#,##0";
 394             } else if (exp == 1) {
 395                 formatter = "0";
 396             } else {
 397                 final boolean ratioHasFrac = Math.rint(ratio) != ratio;
 398                 final StringBuilder formatterB = new StringBuilder("0");
 399                 int n = ratioHasFrac ? Math.abs(exp) + 1 : Math.abs(exp);
 400                 if (n > 0) formatterB.append(".");
 401                 for (int i = 0; i < n; ++i) {
 402                     formatterB.append("0");
 403                 }
 404                 formatter = formatterB.toString();
 405 
 406             }
 407             tickUnitRounded = ratio * Math.pow(10, exp);
 408             // move min and max to nearest tick mark
 409             minRounded = Math.floor(paddedMin / tickUnitRounded) * tickUnitRounded;
 410             maxRounded = Math.ceil(paddedMax / tickUnitRounded) * tickUnitRounded;
 411             // calculate the required length to display the chosen tick marks for real, this will handle if there are
 412             // huge numbers involved etc or special formatting of the tick mark label text
 413             double maxReqTickGap = 0;
 414             double last = 0;
 415             count = 0;
 416             for (double major = minRounded; major <= maxRounded; major += tickUnitRounded, count ++)  {
 417                 double size = side.isVertical() ? measureTickMarkSize(major, getTickLabelRotation(), formatter).getHeight() :
 418                                             measureTickMarkSize(major, getTickLabelRotation(), formatter).getWidth();
 419                 if (major == minRounded) { // first
 420                     last = size/2;
 421                 } else {
 422                     maxReqTickGap = Math.max(maxReqTickGap, last + 6 + (size/2) );
 423                 }
 424             }
 425             reqLength = (count-1) * maxReqTickGap;
 426             tickUnit = tickUnitRounded;
 427 
 428             // fix for RT-35600 where a massive tick unit was being selected
 429             // unnecessarily. There is probably a better solution, but this works
 430             // well enough for now.
 431             if (numOfTickMarks == 2 && reqLength > length) {
 432                 break;
 433             }
 434             if (reqLength > length || count > 20) tickUnit *= 2; // This is just for the while loop, if there are still too many ticks
 435         }
 436         // calculate new scale
 437         final double newScale = calculateNewScale(length, minRounded, maxRounded);
 438         // return new range
 439         return new Object[]{minRounded, maxRounded, tickUnitRounded, newScale, formatter};
 440     }
 441 
 442     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 443 
 444      /** @treatAsPrivate implementation detail */
 445     private static class StyleableProperties {
 446         private static final CssMetaData<NumberAxis,Number> TICK_UNIT =
 447             new CssMetaData<NumberAxis,Number>("-fx-tick-unit",
 448                 SizeConverter.getInstance(), 5.0) {
 449 
 450             @Override
 451             public boolean isSettable(NumberAxis n) {
 452                 return n.tickUnit == null || !n.tickUnit.isBound();
 453             }
 454 
 455             @Override
 456             public StyleableProperty<Number> getStyleableProperty(NumberAxis n) {
 457                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tickUnitProperty();
 458             }
 459         };
 460 
 461         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 462         static {
 463            final List<CssMetaData<? extends Styleable, ?>> styleables = 
 464                new ArrayList<CssMetaData<? extends Styleable, ?>>(ValueAxis.getClassCssMetaData());
 465            styleables.add(TICK_UNIT);
 466            STYLEABLES = Collections.unmodifiableList(styleables);
 467         }
 468     }
 469 
 470     /**
 471      * @return The CssMetaData associated with this class, which may include the
 472      * CssMetaData of its super classes.
 473      * @since JavaFX 8.0
 474      */
 475     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 476         return StyleableProperties.STYLEABLES;
 477     }
 478 
 479     /**
 480      * {@inheritDoc}
 481      * @since JavaFX 8.0
 482      */
 483     @Override
 484     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 485         return getClassCssMetaData();
 486     }
 487 
 488     // -------------- INNER CLASSES ------------------------------------------------------------------------------------
 489 
 490     /**
 491      * Default number formatter for NumberAxis, this stays in sync with auto-ranging and formats values appropriately.
 492      * You can wrap this formatter to add prefixes or suffixes;
 493      * @since JavaFX 2.0
 494      */
 495     public static class DefaultFormatter extends StringConverter<Number> {
 496         private DecimalFormat formatter;
 497         private String prefix = null;
 498         private String suffix = null;
 499 
 500         /**
 501          * Construct a DefaultFormatter for the given NumberAxis
 502          *
 503          * @param axis The axis to format tick marks for
 504          */
 505         public DefaultFormatter(final NumberAxis axis) {
 506             formatter = axis.isAutoRanging()? new DecimalFormat(axis.currentFormatterProperty.get()) : new DecimalFormat();
 507             final ChangeListener<Object> axisListener = (observable, oldValue, newValue) -> {
 508                 formatter = axis.isAutoRanging()? new DecimalFormat(axis.currentFormatterProperty.get()) : new DecimalFormat();
 509             };
 510             axis.currentFormatterProperty.addListener(axisListener);
 511             axis.autoRangingProperty().addListener(axisListener);
 512         }
 513 
 514         /**
 515          * Construct a DefaultFormatter for the given NumberAxis with a prefix and/or suffix.
 516          *
 517          * @param axis The axis to format tick marks for
 518          * @param prefix The prefix to append to the start of formatted number, can be null if not needed
 519          * @param suffix The suffix to append to the end of formatted number, can be null if not needed
 520          */
 521         public DefaultFormatter(NumberAxis axis, String prefix, String suffix) {
 522             this(axis);
 523             this.prefix = prefix;
 524             this.suffix = suffix;
 525         }
 526 
 527         /**
 528         * Converts the object provided into its string form.
 529         * Format of the returned string is defined by this converter.
 530         * @return a string representation of the object passed in.
 531         * @see StringConverter#toString
 532         */
 533         @Override public String toString(Number object) {
 534             return toString(object, formatter);
 535         }
 536 
 537         private String toString(Number object, String numFormatter) {
 538             return toString(object, new DecimalFormat(numFormatter));
 539         }
 540 
 541         private String toString(Number object, DecimalFormat formatter) {
 542             if (prefix != null && suffix != null) {
 543                 return prefix + formatter.format(object) + suffix;
 544             } else if (prefix != null) {
 545                 return prefix + formatter.format(object);
 546             } else if (suffix != null) {
 547                 return formatter.format(object) + suffix;
 548             } else {
 549                 return formatter.format(object);
 550             }
 551         }
 552 
 553         /**
 554         * Converts the string provided into a Number defined by the this converter.
 555         * Format of the string and type of the resulting object is defined by this converter.
 556         * @return a Number representation of the string passed in.
 557         * @see StringConverter#toString
 558         */
 559         @Override public Number fromString(String string) {
 560             try {
 561                 int prefixLength = (prefix == null)? 0: prefix.length();
 562                 int suffixLength = (suffix == null)? 0: suffix.length();
 563                 return formatter.parse(string.substring(prefixLength, string.length() - suffixLength));
 564             } catch (ParseException e) {
 565                 return null;
 566             }
 567         }
 568     }
 569 
 570 }
 571 
 572 /*
 573   // Code to generate tick unit defaults
 574 
 575   public static void main(String[] args) {
 576         List<BigDecimal> values = new ArrayList<BigDecimal>();
 577         List<String> formats = new ArrayList<String>();
 578         for(int power=-10; power <= 12; power ++) {
 579             BigDecimal val = new BigDecimal(10);
 580             val = val.pow(power, MathContext.DECIMAL32);
 581             BigDecimal val2 = val.multiply(new BigDecimal(2.5d));
 582             BigDecimal val5 = val.multiply(new BigDecimal(5d));
 583             values.add(val);
 584             values.add(val2);
 585             values.add(val5);
 586             System.out.print("["+power+"]  ");
 587             System.out.print(
 588                     val.doubleValue() + "d, " +
 589                             val2.doubleValue() + "d, " +
 590                             val5.doubleValue() + "d, "
 591             );
 592             DecimalFormat df = null;
 593             DecimalFormat dfTwoHalf = null;
 594             if (power < 0) {
 595                 String nf = "0.";
 596                 for (int i=0; i<Math.abs(power); i++) nf = nf+"0";
 597                 System.out.print("    ---   nf = " + nf);
 598                 String nf2 = "0.";
 599                 for (int i=0; i<=Math.abs(power); i++) nf2 = nf2+"0";
 600                 System.out.print("    ---   nf2 = " + nf2);
 601                 df = new DecimalFormat(nf);
 602                 dfTwoHalf = new DecimalFormat(nf2);
 603                 formats.add(nf);
 604                 formats.add(nf2);
 605                 formats.add(nf);
 606             } else if (power == 0) {
 607                 df = new DecimalFormat("0");
 608                 dfTwoHalf = new DecimalFormat("0.0");
 609                 formats.add("0");
 610                 formats.add("0.0");
 611                 formats.add("0");
 612             } else {
 613                 String nf = "0";
 614                 for (int i=0; i<Math.abs(power); i++) {
 615                     if((i % 3) == 2) {
 616                         nf = "#," + nf;
 617                     } else {
 618                         nf = "#" + nf;
 619                     }
 620                 }
 621                 System.out.print("    ---   nf = " + nf);
 622                 formats.add(nf);
 623                 formats.add(nf);
 624                 formats.add(nf);
 625                 dfTwoHalf = df = new DecimalFormat(nf);
 626             }
 627             System.out.println("        ---      "+
 628                     df.format(val.doubleValue())+", "+
 629                     dfTwoHalf.format(val2.doubleValue())+", "+
 630                     df.format(val5.doubleValue())+", "
 631             );
 632         }
 633         System.out.print("    private static final double[] TICK_UNIT_DEFAULTS = { ");
 634         for(BigDecimal val: values) System.out.print(val.doubleValue()+", ");
 635         System.out.println(" };");
 636         System.out.print("    private static final String[] TICK_UNIT_FORMATTER_DEFAULTS = { ");
 637         for(String format: formats) System.out.print("\""+format+"\", ");
 638         System.out.println(" };");
 639     }
 640 */