![JAR search and dependency download from the Maven repository](/logo.png)
org.gillius.jfxutils.chart.StableTicksAxis Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jfxutils Show documentation
Show all versions of jfxutils Show documentation
Zoom and Pan Charts and Pane Scaling
/*
* Copyright 2013 Jason Winnebeck
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gillius.jfxutils.chart;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.WritableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Dimension2D;
import javafx.scene.chart.ValueAxis;
import javafx.util.Duration;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
/**
* StableTicksAxis is not ready to be used.
*
* @author Jason Winnebeck
*/
public class StableTicksAxis extends ValueAxis {
/**
* Possible tick spacing at the 10^1 level. These numbers must be >= 1 and < 10.
*/
private static final double[] dividers = new double[] { 1.0, 2.5, 5.0 };
private static final int numMinorTicks = 3;
private final Timeline animationTimeline = new Timeline();
private final WritableValue scaleValue = new WritableValue() {
@Override
public Double getValue() {
return getScale();
}
@Override
public void setValue( Double value ) {
setScale( value );
}
};
private final NumberFormat normalFormat = NumberFormat.getNumberInstance();
private final NumberFormat engFormat = NumberFormat.getNumberInstance();
private NumberFormat currFormat = normalFormat;
private List minorTicks;
/**
* Amount of padding to add on the each end of the axis when auto ranging.
*/
private DoubleProperty autoRangePadding = new SimpleDoubleProperty( 0.1 );
/**
* If true, when auto-ranging, force 0 to be the min or max end of the range.
*/
private BooleanProperty forceZeroInRange = new SimpleBooleanProperty( true );
public StableTicksAxis() {
}
public StableTicksAxis( double lowerBound, double upperBound ) {
super( lowerBound, upperBound );
}
/**
* Amount of padding to add on the each end of the axis when auto ranging.
*/
public double getAutoRangePadding() {
return autoRangePadding.get();
}
/**
* Amount of padding to add on the each end of the axis when auto ranging.
*/
public DoubleProperty autoRangePaddingProperty() {
return autoRangePadding;
}
/**
* Amount of padding to add on the each end of the axis when auto ranging.
*/
public void setAutoRangePadding( double autoRangePadding ) {
this.autoRangePadding.set( autoRangePadding );
}
/**
* If true, when auto-ranging, force 0 to be the min or max end of the range.
*/
public boolean isForceZeroInRange() {
return forceZeroInRange.get();
}
/**
* If true, when auto-ranging, force 0 to be the min or max end of the range.
*/
public BooleanProperty forceZeroInRangeProperty() {
return forceZeroInRange;
}
/**
* If true, when auto-ranging, force 0 to be the min or max end of the range.
*/
public void setForceZeroInRange( boolean forceZeroInRange ) {
this.forceZeroInRange.set( forceZeroInRange );
}
@Override
protected Range autoRange( double minValue, double maxValue, double length, double labelSize ) {
// System.out.printf( "autoRange(%f, %f, %f, %f)",
// minValue, maxValue, length, labelSize );
//noinspection FloatingPointEquality
if ( minValue == maxValue ) {
//Normally this is the case for all points with the same value
minValue = minValue - 1;
maxValue = maxValue + 1;
} else {
//Add padding
double delta = maxValue - minValue;
double paddedMin = minValue - delta * autoRangePadding.get();
//If we've crossed the 0 line, clamp to 0.
//noinspection FloatingPointEquality
if ( Math.signum( paddedMin ) != Math.signum( minValue ) )
paddedMin = 0.0;
double paddedMax = maxValue + delta * autoRangePadding.get();
//If we've crossed the 0 line, clamp to 0.
//noinspection FloatingPointEquality
if ( Math.signum( paddedMax ) != Math.signum( maxValue ) )
paddedMax = 0.0;
minValue = paddedMin;
maxValue = paddedMax;
}
//Handle forcing zero into the range
if ( forceZeroInRange.get() ) {
if ( minValue < 0 && maxValue < 0 ) {
maxValue = 0;
minValue -= -minValue * autoRangePadding.get();
} else if ( minValue > 0 && maxValue > 0 ) {
minValue = 0;
maxValue += maxValue * autoRangePadding.get();
}
}
Range ret = getRange( minValue, maxValue );
// System.out.printf( " = %s%n", ret );
return ret;
}
private Range getRange( double minValue, double maxValue ) {
double length = getLength();
double delta = maxValue - minValue;
double scale = calculateNewScale( length, minValue, maxValue );
int maxTicks = Math.max( 1, (int) ( length / getLabelSize() ) );
Range ret;
ret = new Range( minValue, maxValue, calculateTickSpacing( delta, maxTicks ), scale );
return ret;
}
public static double calculateTickSpacing( double delta, int maxTicks ) {
if ( delta == 0.0 )
return 0.0;
if ( delta <= 0.0 )
throw new IllegalArgumentException( "delta must be positive" );
if ( maxTicks < 1 )
throw new IllegalArgumentException( "must be at least one tick" );
//The factor will be close to the log10, this just optimizes the search
int factor = (int) Math.log10( delta );
int divider = 0;
double numTicks = delta / ( dividers[divider] * Math.pow( 10, factor ) );
//We don't have enough ticks, so increase ticks until we're over the limit, then back off once.
if ( numTicks < maxTicks ) {
while ( numTicks < maxTicks ) {
//Move up
--divider;
if ( divider < 0 ) {
--factor;
divider = dividers.length - 1;
}
numTicks = delta / ( dividers[divider] * Math.pow( 10, factor ) );
}
//Now back off once unless we hit exactly
//noinspection FloatingPointEquality
if ( numTicks != maxTicks ) {
++divider;
if ( divider >= dividers.length ) {
++factor;
divider = 0;
}
}
} else {
//We have too many ticks or exactly max, so decrease until we're just under (or at) the limit.
while ( numTicks > maxTicks ) {
++divider;
if ( divider >= dividers.length ) {
++factor;
divider = 0;
}
numTicks = delta / ( dividers[divider] * Math.pow( 10, factor ) );
}
}
// System.out.printf( "calculateTickSpacing( %f, %d ) = %f%n",
// delta, maxTicks, dividers[divider] * Math.pow( 10, factor ) );
return dividers[divider] * Math.pow( 10, factor );
}
@Override
protected List calculateMinorTickMarks() {
// System.out.println( "StableTicksAxis.calculateMinorTickMarks" );
return minorTicks;
}
@Override
protected void setRange( Object range, boolean animate ) {
Range rangeVal = (Range) range;
// System.out.format( "StableTicksAxis.setRange (%s, %s)%n",
// range, animate );
if ( animate ) {
animationTimeline.stop();
ObservableList keyFrames = animationTimeline.getKeyFrames();
keyFrames.setAll(
new KeyFrame( Duration.ZERO,
new KeyValue( currentLowerBound, getLowerBound() ),
new KeyValue( scaleValue, getScale() ) ),
new KeyFrame( Duration.millis( 750 ),
new KeyValue( currentLowerBound, rangeVal.low ),
new KeyValue( scaleValue, rangeVal.scale ) ) );
animationTimeline.play();
} else {
currentLowerBound.set( rangeVal.low );
setScale( rangeVal.scale );
}
setLowerBound( rangeVal.low );
setUpperBound( rangeVal.high );
//Set the number format. Pick a "normal" format unless the numbers are quite large or small.
currFormat = normalFormat;
double log10 = Math.log10( rangeVal.low );
if ( log10 < -4.0 || log10 > 5.0 ) {
currFormat = engFormat;
} else {
log10 = Math.log10( rangeVal.high );
if ( log10 < -4.0 || log10 > 5.0 ) {
currFormat = engFormat;
}
}
}
@Override
protected Range getRange() {
Range ret = getRange( getLowerBound(), getUpperBound() );
// System.out.println( "StableTicksAxis.getRange = " + ret );
return ret;
}
@Override
protected List calculateTickValues( double length, Object range ) {
Range rangeVal = (Range) range;
// System.out.format( "StableTicksAxis.calculateTickValues (length=%f, range=%s)",
// length, rangeVal );
//Use floor so we start generating ticks before the axis starts -- this is really only relevant
//because of the minor ticks before the first visible major tick. We'll generate a first
//invisible major tick but the ValueAxis seems to filter it out.
double firstTick = Math.floor( rangeVal.low / rangeVal.tickSpacing ) * rangeVal.tickSpacing;
//Generate one more tick than we expect, for "overlap" to get minor ticks on both sides of the
//first and last major tick.
int numTicks = (int) (rangeVal.getDelta() / rangeVal.tickSpacing) + 1;
List ret = new ArrayList( numTicks + 1 );
minorTicks = new ArrayList( ( numTicks + 2 ) * numMinorTicks );
double minorTickSpacing = rangeVal.tickSpacing / ( numMinorTicks + 1 );
for ( int i = 0; i <= numTicks; ++i ) {
double majorTick = firstTick + rangeVal.tickSpacing * i;
ret.add( majorTick );
for ( int j = 1; j <= numMinorTicks; ++j ) {
minorTicks.add( majorTick + minorTickSpacing * j );
}
}
// System.out.printf( " = %s%n", ret );
return ret;
}
@Override
protected String getTickMarkLabel( Number number ) {
return currFormat.format( number );
}
private double getLength() {
if ( getSide().isHorizontal() )
return getWidth();
else
return getHeight();
}
private double getLabelSize() {
Dimension2D dim = measureTickMarkLabelSize( "-888.88E-88", getTickLabelRotation() );
if ( getSide().isHorizontal() ) {
return dim.getWidth();
} else {
return dim.getHeight();
}
}
private static class Range {
public final double low;
public final double high;
public final double tickSpacing;
public final double scale;
private Range( double low, double high, double tickSpacing, double scale ) {
this.low = low;
this.high = high;
this.tickSpacing = tickSpacing;
this.scale = scale;
}
public double getDelta() {
return high - low;
}
@Override
public String toString() {
return "Range{" +
"low=" + low +
", high=" + high +
", tickSpacing=" + tickSpacing +
", scale=" + scale +
'}';
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy