package com.positive.charts.axis;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import com.positive.charts.axis.ticks.NumberTick;
import com.positive.charts.axis.ticks.NumberTickUnit;
import com.positive.charts.axis.ticks.TickUnit;
import com.positive.charts.axis.ticks.TickUnitSource;
import com.positive.charts.axis.ticks.TickUnits;
import com.positive.charts.block.RectangleInsets;
import com.positive.charts.common.RectangleEdge;
import com.positive.charts.data.Range;
import com.positive.charts.event.AxisChangeEvent;
import com.positive.charts.plot.Plot;
import com.positive.charts.plot.PlotRenderingInfo;
import com.positive.charts.plot.ValueAxisPlot;
import com.positive.charts.util.RectangleUtil;
import com.positive.charts.util.TextAnchor;
/**
* An axis for displaying numerical data.
* <P>
* If the axis is set up to automatically determine its range to fit the data,
* you can ensure that the range includes zero (statisticians usually prefer
* this) by setting the <code>autoRangeIncludesZero</code> flag to
* <code>true</code>.
* <P>
* The <code>NumberAxis</code> class has a mechanism for automatically selecting
* a tick unit that is appropriate for the current axis range. This mechanism is
* an adaptation of code suggested by Laurence Vanhelsuwe.
*/
public class NumberAxis extends ValueAxis {
public static class RangeType {
public static final RangeType FULL = new RangeType();
public static final RangeType POSITIVE = new RangeType();
public static final RangeType NEGATIVE = new RangeType();
}
/** For serialization. */
private static final long serialVersionUID = 2805933088476185789L;
/** The default value for the autoRangeIncludesZero flag. */
public static final boolean DEFAULT_AUTO_RANGE_INCLUDES_ZERO = true;
/** The default value for the autoRangeStickyZero flag. */
public static final boolean DEFAULT_AUTO_RANGE_STICKY_ZERO = true;
/** The default tick unit. */
public static final NumberTickUnit DEFAULT_TICK_UNIT = new NumberTickUnit(
1.0, new DecimalFormat("0"));
/** The default setting for the vertical tick labels flag. */
public static final boolean DEFAULT_VERTICAL_TICK_LABELS = false;
/**
* Returns a collection of tick units for integer values.
*
* @return A collection of tick units for integer values.
*/
public static TickUnitSource createIntegerTickUnits() {
final TickUnits units = new TickUnits();
final DecimalFormat df0 = new DecimalFormat("0");
final DecimalFormat df1 = new DecimalFormat("#,##0");
units.add(new NumberTickUnit(1, df0));
units.add(new NumberTickUnit(2, df0));
units.add(new NumberTickUnit(5, df0));
units.add(new NumberTickUnit(10, df0));
units.add(new NumberTickUnit(20, df0));
units.add(new NumberTickUnit(50, df0));
units.add(new NumberTickUnit(100, df0));
units.add(new NumberTickUnit(200, df0));
units.add(new NumberTickUnit(500, df0));
units.add(new NumberTickUnit(1000, df1));
units.add(new NumberTickUnit(2000, df1));
units.add(new NumberTickUnit(5000, df1));
units.add(new NumberTickUnit(10000, df1));
units.add(new NumberTickUnit(20000, df1));
units.add(new NumberTickUnit(50000, df1));
units.add(new NumberTickUnit(100000, df1));
units.add(new NumberTickUnit(200000, df1));
units.add(new NumberTickUnit(500000, df1));
units.add(new NumberTickUnit(1000000, df1));
units.add(new NumberTickUnit(2000000, df1));
units.add(new NumberTickUnit(5000000, df1));
units.add(new NumberTickUnit(10000000, df1));
units.add(new NumberTickUnit(20000000, df1));
units.add(new NumberTickUnit(50000000, df1));
units.add(new NumberTickUnit(100000000, df1));
units.add(new NumberTickUnit(200000000, df1));
units.add(new NumberTickUnit(500000000, df1));
units.add(new NumberTickUnit(1000000000, df1));
units.add(new NumberTickUnit(2000000000, df1));
units.add(new NumberTickUnit(5000000000.0, df1));
units.add(new NumberTickUnit(10000000000.0, df1));
return units;
}
/**
* Returns a collection of tick units for integer values. Uses a given
* Locale to create the DecimalFormats.
*
* @param locale
* the locale to use to represent Numbers.
*
* @return A collection of tick units for integer values.
*/
public static TickUnitSource createIntegerTickUnits(final Locale locale) {
final TickUnits units = new TickUnits();
final NumberFormat numberFormat = NumberFormat
.getNumberInstance(locale);
units.add(new NumberTickUnit(1, numberFormat));
units.add(new NumberTickUnit(2, numberFormat));
units.add(new NumberTickUnit(5, numberFormat));
units.add(new NumberTickUnit(10, numberFormat));
units.add(new NumberTickUnit(20, numberFormat));
units.add(new NumberTickUnit(50, numberFormat));
units.add(new NumberTickUnit(100, numberFormat));
units.add(new NumberTickUnit(200, numberFormat));
units.add(new NumberTickUnit(500, numberFormat));
units.add(new NumberTickUnit(1000, numberFormat));
units.add(new NumberTickUnit(2000, numberFormat));
units.add(new NumberTickUnit(5000, numberFormat));
units.add(new NumberTickUnit(10000, numberFormat));
units.add(new NumberTickUnit(20000, numberFormat));
units.add(new NumberTickUnit(50000, numberFormat));
units.add(new NumberTickUnit(100000, numberFormat));
units.add(new NumberTickUnit(200000, numberFormat));
units.add(new NumberTickUnit(500000, numberFormat));
units.add(new NumberTickUnit(1000000, numberFormat));
units.add(new NumberTickUnit(2000000, numberFormat));
units.add(new NumberTickUnit(5000000, numberFormat));
units.add(new NumberTickUnit(10000000, numberFormat));
units.add(new NumberTickUnit(20000000, numberFormat));
units.add(new NumberTickUnit(50000000, numberFormat));
units.add(new NumberTickUnit(100000000, numberFormat));
units.add(new NumberTickUnit(200000000, numberFormat));
units.add(new NumberTickUnit(500000000, numberFormat));
units.add(new NumberTickUnit(1000000000, numberFormat));
units.add(new NumberTickUnit(2000000000, numberFormat));
units.add(new NumberTickUnit(5000000000.0, numberFormat));
units.add(new NumberTickUnit(10000000000.0, numberFormat));
return units;
}
/**
* Creates the standard tick units.
* <P>
* If you don't like these defaults, create your own instance of TickUnits
* and then pass it to the setStandardTickUnits() method in the NumberAxis
* class.
*
* @return The standard tick units.
*/
public static TickUnitSource createStandardTickUnits() {
final TickUnits units = new TickUnits();
final DecimalFormat df0 = new DecimalFormat("0.00000000");
final DecimalFormat df1 = new DecimalFormat("0.0000000");
final DecimalFormat df2 = new DecimalFormat("0.000000");
final DecimalFormat df3 = new DecimalFormat("0.00000");
final DecimalFormat df4 = new DecimalFormat("0.0000");
final DecimalFormat df5 = new DecimalFormat("0.000");
final DecimalFormat df6 = new DecimalFormat("0.00");
final DecimalFormat df7 = new DecimalFormat("0.0");
final DecimalFormat df8 = new DecimalFormat("#,##0");
final DecimalFormat df9 = new DecimalFormat("#,###,##0");
final DecimalFormat df10 = new DecimalFormat("#,###,###,##0");
// we can add the units in any order, the TickUnits collection will
// sort them...
units.add(new NumberTickUnit(0.0000001, df1));
units.add(new NumberTickUnit(0.000001, df2));
units.add(new NumberTickUnit(0.00001, df3));
units.add(new NumberTickUnit(0.0001, df4));
units.add(new NumberTickUnit(0.001, df5));
units.add(new NumberTickUnit(0.01, df6));
units.add(new NumberTickUnit(0.1, df7));
units.add(new NumberTickUnit(1, df8));
units.add(new NumberTickUnit(10, df8));
units.add(new NumberTickUnit(100, df8));
units.add(new NumberTickUnit(1000, df8));
units.add(new NumberTickUnit(10000, df8));
units.add(new NumberTickUnit(100000, df8));
units.add(new NumberTickUnit(1000000, df9));
units.add(new NumberTickUnit(10000000, df9));
units.add(new NumberTickUnit(100000000, df9));
units.add(new NumberTickUnit(1000000000, df10));
units.add(new NumberTickUnit(10000000000.0, df10));
units.add(new NumberTickUnit(100000000000.0, df10));
units.add(new NumberTickUnit(0.00000025, df0));
units.add(new NumberTickUnit(0.0000025, df1));
units.add(new NumberTickUnit(0.000025, df2));
units.add(new NumberTickUnit(0.00025, df3));
units.add(new NumberTickUnit(0.0025, df4));
units.add(new NumberTickUnit(0.025, df5));
units.add(new NumberTickUnit(0.25, df6));
units.add(new NumberTickUnit(2.5, df7));
units.add(new NumberTickUnit(25, df8));
units.add(new NumberTickUnit(250, df8));
units.add(new NumberTickUnit(2500, df8));
units.add(new NumberTickUnit(25000, df8));
units.add(new NumberTickUnit(250000, df8));
units.add(new NumberTickUnit(2500000, df9));
units.add(new NumberTickUnit(25000000, df9));
units.add(new NumberTickUnit(250000000, df9));
units.add(new NumberTickUnit(2500000000.0, df10));
units.add(new NumberTickUnit(25000000000.0, df10));
units.add(new NumberTickUnit(250000000000.0, df10));
units.add(new NumberTickUnit(0.0000005, df1));
units.add(new NumberTickUnit(0.000005, df2));
units.add(new NumberTickUnit(0.00005, df3));
units.add(new NumberTickUnit(0.0005, df4));
units.add(new NumberTickUnit(0.005, df5));
units.add(new NumberTickUnit(0.05, df6));
units.add(new NumberTickUnit(0.5, df7));
units.add(new NumberTickUnit(5L, df8));
units.add(new NumberTickUnit(50L, df8));
units.add(new NumberTickUnit(500L, df8));
units.add(new NumberTickUnit(5000L, df8));
units.add(new NumberTickUnit(50000L, df8));
units.add(new NumberTickUnit(500000L, df8));
units.add(new NumberTickUnit(5000000L, df9));
units.add(new NumberTickUnit(50000000L, df9));
units.add(new NumberTickUnit(500000000L, df9));
units.add(new NumberTickUnit(5000000000L, df10));
units.add(new NumberTickUnit(50000000000L, df10));
units.add(new NumberTickUnit(500000000000L, df10));
return units;
}
/**
* Creates a collection of standard tick units. The supplied locale is used
* to create the number formatter (a localised instance of
* <code>NumberFormat</code>).
* <P>
* If you don't like these defaults, create your own instance of
* {@link TickUnits} and then pass it to the
* <code>setStandardTickUnits()</code> method.
*
* @param locale
* the locale.
*
* @return A tick unit collection.
*/
public static TickUnitSource createStandardTickUnits(final Locale locale) {
final TickUnits units = new TickUnits();
final NumberFormat numberFormat = NumberFormat
.getNumberInstance(locale);
// we can add the units in any order, the TickUnits collection will
// sort them...
units.add(new NumberTickUnit(0.0000001, numberFormat));
units.add(new NumberTickUnit(0.000001, numberFormat));
units.add(new NumberTickUnit(0.00001, numberFormat));
units.add(new NumberTickUnit(0.0001, numberFormat));
units.add(new NumberTickUnit(0.001, numberFormat));
units.add(new NumberTickUnit(0.01, numberFormat));
units.add(new NumberTickUnit(0.1, numberFormat));
units.add(new NumberTickUnit(1, numberFormat));
units.add(new NumberTickUnit(10, numberFormat));
units.add(new NumberTickUnit(100, numberFormat));
units.add(new NumberTickUnit(1000, numberFormat));
units.add(new NumberTickUnit(10000, numberFormat));
units.add(new NumberTickUnit(100000, numberFormat));
units.add(new NumberTickUnit(1000000, numberFormat));
units.add(new NumberTickUnit(10000000, numberFormat));
units.add(new NumberTickUnit(100000000, numberFormat));
units.add(new NumberTickUnit(1000000000, numberFormat));
units.add(new NumberTickUnit(10000000000.0, numberFormat));
units.add(new NumberTickUnit(0.00000025, numberFormat));
units.add(new NumberTickUnit(0.0000025, numberFormat));
units.add(new NumberTickUnit(0.000025, numberFormat));
units.add(new NumberTickUnit(0.00025, numberFormat));
units.add(new NumberTickUnit(0.0025, numberFormat));
units.add(new NumberTickUnit(0.025, numberFormat));
units.add(new NumberTickUnit(0.25, numberFormat));
units.add(new NumberTickUnit(2.5, numberFormat));
units.add(new NumberTickUnit(25, numberFormat));
units.add(new NumberTickUnit(250, numberFormat));
units.add(new NumberTickUnit(2500, numberFormat));
units.add(new NumberTickUnit(25000, numberFormat));
units.add(new NumberTickUnit(250000, numberFormat));
units.add(new NumberTickUnit(2500000, numberFormat));
units.add(new NumberTickUnit(25000000, numberFormat));
units.add(new NumberTickUnit(250000000, numberFormat));
units.add(new NumberTickUnit(2500000000.0, numberFormat));
units.add(new NumberTickUnit(25000000000.0, numberFormat));
units.add(new NumberTickUnit(0.0000005, numberFormat));
units.add(new NumberTickUnit(0.000005, numberFormat));
units.add(new NumberTickUnit(0.00005, numberFormat));
units.add(new NumberTickUnit(0.0005, numberFormat));
units.add(new NumberTickUnit(0.005, numberFormat));
units.add(new NumberTickUnit(0.05, numberFormat));
units.add(new NumberTickUnit(0.5, numberFormat));
units.add(new NumberTickUnit(5L, numberFormat));
units.add(new NumberTickUnit(50L, numberFormat));
units.add(new NumberTickUnit(500L, numberFormat));
units.add(new NumberTickUnit(5000L, numberFormat));
units.add(new NumberTickUnit(50000L, numberFormat));
units.add(new NumberTickUnit(500000L, numberFormat));
units.add(new NumberTickUnit(5000000L, numberFormat));
units.add(new NumberTickUnit(50000000L, numberFormat));
units.add(new NumberTickUnit(500000000L, numberFormat));
units.add(new NumberTickUnit(5000000000L, numberFormat));
units.add(new NumberTickUnit(50000000000L, numberFormat));
return units;
}
/**
* The range type (can be used to force the axis to display only positive
* values or only negative values.
*/
private RangeType rangeType;
/**
* A flag that affects the axis range when the range is determined
* automatically. If the auto range does NOT include zero and this flag is
* TRUE, then the range is changed to include zero.
*/
private boolean autoRangeIncludesZero;
/**
* A flag that affects the size of the margins added to the axis range when
* the range is determined automatically. If the value 0 falls within the
* margin and this flag is TRUE, then the margin is truncated at zero.
*/
private boolean autoRangeStickyZero;
/** The tick unit for the axis. */
private NumberTickUnit tickUnit;
/** The override number format. */
private NumberFormat numberFormatOverride;
/**
* Default constructor.
*/
public NumberAxis() {
this(null);
}
/**
* Constructs a number axis, using default values where necessary.
*
* @param label
* the axis label (<code>null</code> permitted).
*/
public NumberAxis(final String label) {
super(label, NumberAxis.createStandardTickUnits());
this.rangeType = RangeType.FULL;
this.autoRangeIncludesZero = DEFAULT_AUTO_RANGE_INCLUDES_ZERO;
this.autoRangeStickyZero = DEFAULT_AUTO_RANGE_STICKY_ZERO;
this.tickUnit = DEFAULT_TICK_UNIT;
this.numberFormatOverride = null;
}
/**
* Rescales the axis to ensure that all data is visible.
*/
protected void autoAdjustRange() {
final Plot plot = this.getPlot();
if (plot == null) {
return; // no plot, no data
}
if (plot instanceof ValueAxisPlot) {
final ValueAxisPlot vap = (ValueAxisPlot) plot;
Range r = vap.getDataRange(this);
if (r == null) {
r = new Range(DEFAULT_LOWER_BOUND, DEFAULT_UPPER_BOUND);
}
double upper = r.getUpperBound();
double lower = r.getLowerBound();
if (this.rangeType == RangeType.POSITIVE) {
lower = Math.max(0.0, lower);
upper = Math.max(0.0, upper);
} else if (this.rangeType == RangeType.NEGATIVE) {
lower = Math.min(0.0, lower);
upper = Math.min(0.0, upper);
}
if (this.getAutoRangeIncludesZero()) {
lower = Math.min(lower, 0.0);
upper = Math.max(upper, 0.0);
}
final double range = upper - lower;
// if fixed auto range, then derive lower bound...
final double fixedAutoRange = this.getFixedAutoRange();
if (fixedAutoRange > 0.0) {
lower = upper - fixedAutoRange;
} else {
// ensure the autorange is at least <minRange> in size...
final double minRange = this.getAutoRangeMinimumSize();
if (range < minRange) {
final double expand = (minRange - range) / 2;
upper = upper + expand;
lower = lower - expand;
if (lower == upper) { // see bug report 1549218
final double adjust = Math.abs(lower) / 10.0;
lower = lower - adjust;
upper = upper + adjust;
}
if (this.rangeType == RangeType.POSITIVE) {
if (lower < 0.0) {
upper = upper - lower;
lower = 0.0;
}
} else if (this.rangeType == RangeType.NEGATIVE) {
if (upper > 0.0) {
lower = lower - upper;
upper = 0.0;
}
}
}
if (this.getAutoRangeStickyZero()) {
if (upper <= 0.0) {
upper = Math.min(0.0, upper + this.getUpperMargin()
* range);
} else {
upper = upper + this.getUpperMargin() * range;
}
if (lower >= 0.0) {
lower = Math.max(0.0, lower - this.getLowerMargin()
* range);
} else {
lower = lower - this.getLowerMargin() * range;
}
} else {
upper = upper + this.getUpperMargin() * range;
lower = lower - this.getLowerMargin() * range;
}
}
this.setRange(new Range(lower, upper), false, false);
}
}
/**
* Calculates the value of the highest visible tick on the axis.
*
* @return The value of the highest visible tick on the axis.
*/
protected double calculateHighestVisibleTickValue() {
final double unit = this.getTickUnit().getSize();
final double index = Math.floor(this.getRange().getUpperBound() / unit);
return index * unit;
}
/**
* Calculates the value of the lowest visible tick on the axis.
*
* @return The value of the lowest visible tick on the axis.
*/
protected double calculateLowestVisibleTickValue() {
final double unit = this.getTickUnit().getSize();
final double index = Math.ceil(this.getRange().getLowerBound() / unit);
return index * unit;
}
/**
* Calculates the number of visible ticks.
*
* @return The number of visible ticks on the axis.
*/
protected int calculateVisibleTickCount() {
final double unit = this.getTickUnit().getSize();
final Range range = this.getRange();
return (int) (Math.floor(range.getUpperBound() / unit)
- Math.ceil(range.getLowerBound() / unit) + 1);
}
/**
* Configures the axis to work with the specified plot. If the axis has
* auto-scaling, then sets the maximum and minimum values.
*/
public void configure() {
if (this.isAutoRange()) {
this.autoAdjustRange();
}
}
/**
* Draws the axis on a Java 2D graphics device (such as the screen or a
* printer).
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param cursor
* the cursor location.
* @param plotArea
* the area within which the axes and data should be drawn (
* <code>null</code> not permitted).
* @param dataArea
* the area within which the data should be drawn (
* <code>null</code> not permitted).
* @param edge
* the location of the axis (<code>null</code> not permitted).
* @param plotState
* collects information about the plot (<code>null</code>
* permitted).
*
* @return The axis state (never <code>null</code>).
*/
public AxisState draw(final GC g2, final double cursor,
final Rectangle plotArea, final Rectangle dataArea,
final RectangleEdge edge, final PlotRenderingInfo plotState) {
AxisState state = null;
// if the axis is not visible, don't draw it...
if (!this.isVisible()) {
state = new AxisState(cursor);
// even though the axis is not visible, we need ticks for the
// gridlines...
final List ticks = this.refreshTicks(g2, state, dataArea, edge);
state.setTicks(ticks);
return state;
}
// draw the tick marks and labels...
state = this.drawTickMarksAndLabels(g2, cursor, plotArea, dataArea,
edge);
// // draw the marker band (if there is one)...
// if (getMarkerBand() != null) {
// if (edge == RectangleEdge.BOTTOM) {
// cursor = cursor - getMarkerBand().getHeight(g2);
// }
// getMarkerBand().draw(g2, plotArea, dataArea, 0, cursor);
// }
// draw the axis label...
state = this.drawLabel(this.getLabel(), g2, plotArea, dataArea, edge,
state);
return state;
}
/**
* Estimates the maximum tick label height.
*
* @param g2
* the graphics device.
*
* @return The maximum height.
*/
protected double estimateMaximumTickLabelHeight(final GC g2) {
final RectangleInsets tickLabelInsets = this.getTickLabelInsets();
double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
result += g2.getFontMetrics().getHeight();
return result;
}
/**
* Estimates the maximum width of the tick labels, assuming the specified
* tick unit is used.
* <P>
* Rather than computing the string bounds of every tick on the axis, we
* just look at two values: the lower bound and the upper bound for the
* axis. These two values will usually be representative.
*
* @param gc
* the graphics device.
* @param unit
* the tick unit to use for calculation.
*
* @return The estimated maximum width of the tick labels.
*/
protected double estimateMaximumTickLabelWidth(final GC gc,
final TickUnit unit) {
final RectangleInsets tickLabelInsets = this.getTickLabelInsets();
double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
if (this.isVerticalTickLabels()) {
// all tick labels have the same width (equal to the height of the
// font)...
result += gc.getFontMetrics().getHeight();
} else {
// look at lower and upper bounds...
final Range range = this.getRange();
final double lower = range.getLowerBound();
final double upper = range.getUpperBound();
final String lowerStr = unit.valueToString(lower);
final String upperStr = unit.valueToString(upper);
final double w1 = gc.textExtent(lowerStr).x;
final double w2 = gc.textExtent(upperStr).x;
result += Math.max(w1, w2);
}
return result;
}
/**
* Returns the flag that indicates whether or not the automatic axis range
* (if indeed it is determined automatically) is forced to include zero.
*
* @return The flag.
*/
public boolean getAutoRangeIncludesZero() {
return this.autoRangeIncludesZero;
}
/**
* Returns a flag that affects the auto-range when zero falls outside the
* data range but inside the margins defined for the axis.
*
* @return The flag.
*/
public boolean getAutoRangeStickyZero() {
return this.autoRangeStickyZero;
}
/**
* Returns the number format override. If this is non-null, then it will be
* used to format the numbers on the axis.
*
* @return The number formatter (possibly <code>null</code>).
*/
public NumberFormat getNumberFormatOverride() {
return this.numberFormatOverride;
}
/**
* Returns the axis range type.
*
* @return The axis range type (never <code>null</code>).
*/
public RangeType getRangeType() {
return this.rangeType;
}
/**
* Returns the tick unit for the axis.
* <p>
* Note: if the <code>autoTickUnitSelection</code> flag is <code>true</code>
* the tick unit may be changed while the axis is being drawn, so in that
* case the return value from this method may be irrelevant if the method is
* called before the axis has been drawn.
*
* @return The tick unit for the axis.
*
* @see #setTickUnit(NumberTickUnit)
* @see ValueAxis#isAutoTickUnitSelection()
*/
public NumberTickUnit getTickUnit() {
return this.tickUnit;
}
/**
* Converts a coordinate in Java2D space to the corresponding data value,
* assuming that the axis runs along one edge of the specified dataArea.
*
* @param java2DValue
* the coordinate in Java2D space.
* @param area
* the area in which the data is plotted.
* @param edge
* the location.
*
* @return The data value.
*/
public double java2DToValue(final double java2DValue, final Rectangle area,
final RectangleEdge edge) {
final Range range = this.getRange();
final double axisMin = range.getLowerBound();
final double axisMax = range.getUpperBound();
double min = 0.0;
double max = 0.0;
if (edge.isTopOrBottom()) {
min = area.x;
max = RectangleUtil.getMaxX(area);
} else if (edge.isLeftOrRight()) {
min = RectangleUtil.getMaxY(area);
max = area.y;
}
if (this.isInverted()) {
return axisMax - (java2DValue - min) / (max - min)
* (axisMax - axisMin);
} else {
return axisMin + (java2DValue - min) / (max - min)
* (axisMax - axisMin);
}
}
/**
* Calculates the positions of the tick labels for the axis, storing the
* results in the tick label list (ready for drawing).
*
* @param g2
* the graphics device.
* @param state
* the axis state.
* @param dataArea
* the area in which the plot should be drawn.
* @param edge
* the location of the axis.
*
* @return A list of ticks.
*
*/
public List refreshTicks(final GC g2, final AxisState state,
final Rectangle dataArea, final RectangleEdge edge) {
if (edge.isTopOrBottom()) {
return this.refreshTicksHorizontal(g2, dataArea, edge);
} else {
return this.refreshTicksVertical(g2, dataArea, edge);
}
}
/**
* Calculates the positions of the tick labels for the axis, storing the
* results in the tick label list (ready for drawing).
*
* @param g2
* the graphics device.
* @param dataArea
* the area in which the data should be drawn.
* @param edge
* the location of the axis.
*
* @return A list of ticks.
*/
protected List refreshTicksHorizontal(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
final List result = new ArrayList();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
if (this.isAutoTickUnitSelection()) {
this.selectAutoTickUnit(g2, dataArea, edge);
}
final double size = this.getTickUnit().getSize();
final int count = this.calculateVisibleTickCount();
final double lowestTickValue = this.calculateLowestVisibleTickValue();
if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
for (int i = 0; i < count; i++) {
final double currentTickValue = lowestTickValue + (i * size);
String tickLabel;
final NumberFormat formatter = this.getNumberFormatOverride();
if (formatter != null) {
tickLabel = formatter.format(currentTickValue);
} else {
tickLabel = this.getTickUnit().valueToString(
currentTickValue);
}
TextAnchor anchor = null;
TextAnchor rotationAnchor = null;
double angle = 0.0;
if (this.isVerticalTickLabels()) {
anchor = TextAnchor.CENTER_RIGHT;
rotationAnchor = TextAnchor.CENTER_RIGHT;
if (edge == RectangleEdge.TOP) {
angle = Math.PI / 2.0;
} else {
angle = -Math.PI / 2.0;
}
} else {
if (edge == RectangleEdge.TOP) {
anchor = TextAnchor.BOTTOM_CENTER;
rotationAnchor = TextAnchor.BOTTOM_CENTER;
} else {
anchor = TextAnchor.TOP_CENTER;
rotationAnchor = TextAnchor.TOP_CENTER;
}
}
final NumberTick tick = new NumberTick(new Double(
currentTickValue), tickLabel, anchor, rotationAnchor,
angle);
result.add(tick);
}
}
return result;
}
/**
* Calculates the positions of the tick labels for the axis, storing the
* results in the tick label list (ready for drawing).
*
* @param g2
* the graphics device.
* @param dataArea
* the area in which the plot should be drawn.
* @param edge
* the location of the axis.
*
* @return A list of ticks.
*
*/
protected List refreshTicksVertical(final GC g2, final Rectangle dataArea,
final RectangleEdge edge) {
final List result = new ArrayList();
result.clear();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
if (this.isAutoTickUnitSelection()) {
this.selectAutoTickUnit(g2, dataArea, edge);
}
final double size = this.getTickUnit().getSize();
final int count = this.calculateVisibleTickCount();
final double lowestTickValue = this.calculateLowestVisibleTickValue();
if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
for (int i = 0; i < count; i++) {
final double currentTickValue = lowestTickValue + (i * size);
String tickLabel;
final NumberFormat formatter = this.getNumberFormatOverride();
if (formatter != null) {
tickLabel = formatter.format(currentTickValue);
} else {
tickLabel = this.getTickUnit().valueToString(
currentTickValue);
}
TextAnchor anchor = null;
TextAnchor rotationAnchor = null;
double angle = 0.0;
if (this.isVerticalTickLabels()) {
if (edge == RectangleEdge.LEFT) {
anchor = TextAnchor.BOTTOM_CENTER;
rotationAnchor = TextAnchor.BOTTOM_CENTER;
angle = -Math.PI / 2.0;
} else {
anchor = TextAnchor.BOTTOM_CENTER;
rotationAnchor = TextAnchor.BOTTOM_CENTER;
angle = Math.PI / 2.0;
}
} else {
if (edge == RectangleEdge.LEFT) {
anchor = TextAnchor.CENTER_RIGHT;
rotationAnchor = TextAnchor.CENTER_RIGHT;
} else {
anchor = TextAnchor.CENTER_LEFT;
rotationAnchor = TextAnchor.CENTER_LEFT;
}
}
final NumberTick tick = new NumberTick(new Double(
currentTickValue), tickLabel, anchor, rotationAnchor,
angle);
result.add(tick);
}
}
return result;
}
/**
* Selects an appropriate tick value for the axis. The strategy is to
* display as many ticks as possible (selected from an array of 'standard'
* tick units) without the labels overlapping.
*
* @param g2
* the graphics device.
* @param dataArea
* the area defined by the axes.
* @param edge
* the axis location.
*/
protected void selectAutoTickUnit(final GC g2, final Rectangle dataArea,
final RectangleEdge edge) {
if (edge.isTopOrBottom()) {
this.selectHorizontalAutoTickUnit(g2, dataArea, edge);
} else if (edge.isLeftOrRight()) {
this.selectVerticalAutoTickUnit(g2, dataArea, edge);
}
}
/**
* Selects an appropriate tick value for the axis. The strategy is to
* display as many ticks as possible (selected from an array of 'standard'
* tick units) without the labels overlapping.
*
* @param g2
* the graphics device.
* @param dataArea
* the area defined by the axes.
* @param edge
* the axis location.
*/
protected void selectHorizontalAutoTickUnit(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
double tickLabelWidth = this.estimateMaximumTickLabelWidth(g2, this
.getTickUnit());
// start with the current tick unit...
final TickUnitSource tickUnits = this.getStandardTickUnits();
final TickUnit unit1 = tickUnits.getCeilingTickUnit(this.getTickUnit());
final double unit1Width = this.lengthToJava2D(unit1.getSize(),
dataArea, edge);
// then extrapolate...
final double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
NumberTickUnit unit2 = (NumberTickUnit) tickUnits
.getCeilingTickUnit(guess);
final double unit2Width = this.lengthToJava2D(unit2.getSize(),
dataArea, edge);
tickLabelWidth = this.estimateMaximumTickLabelWidth(g2, unit2);
if (tickLabelWidth > unit2Width) {
unit2 = (NumberTickUnit) tickUnits.getLargerTickUnit(unit2);
}
this.setTickUnit(unit2, false, false);
}
/**
* Selects an appropriate tick value for the axis. The strategy is to
* display as many ticks as possible (selected from an array of 'standard'
* tick units) without the labels overlapping.
*
* @param g2
* the graphics device.
* @param dataArea
* the area in which the plot should be drawn.
* @param edge
* the axis location.
*/
protected void selectVerticalAutoTickUnit(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
double tickLabelHeight = this.estimateMaximumTickLabelHeight(g2);
// start with the current tick unit...
final TickUnitSource tickUnits = this.getStandardTickUnits();
final TickUnit unit1 = tickUnits.getCeilingTickUnit(this.getTickUnit());
final double unitHeight = this.lengthToJava2D(unit1.getSize(),
dataArea, edge);
// then extrapolate...
final double guess = (tickLabelHeight / unitHeight) * unit1.getSize();
NumberTickUnit unit2 = (NumberTickUnit) tickUnits
.getCeilingTickUnit(guess);
final double unit2Height = this.lengthToJava2D(unit2.getSize(),
dataArea, edge);
tickLabelHeight = this.estimateMaximumTickLabelHeight(g2);
if (tickLabelHeight > unit2Height) {
unit2 = (NumberTickUnit) tickUnits.getLargerTickUnit(unit2);
}
this.setTickUnit(unit2, false, false);
}
/**
* Sets the flag that indicates whether or not the axis range, if
* automatically calculated, is forced to include zero.
* <p>
* If the flag is changed to <code>true</code>, the axis range is
* recalculated.
* <p>
* Any change to the flag will trigger an {@link AxisChangeEvent}.
*
* @param flag
* the new value of the flag.
*/
public void setAutoRangeIncludesZero(final boolean flag) {
if (this.autoRangeIncludesZero != flag) {
this.autoRangeIncludesZero = flag;
if (this.isAutoRange()) {
this.autoAdjustRange();
}
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets a flag that affects the auto-range when zero falls outside the data
* range but inside the margins defined for the axis.
*
* @param flag
* the new flag.
*/
public void setAutoRangeStickyZero(final boolean flag) {
if (this.autoRangeStickyZero != flag) {
this.autoRangeStickyZero = flag;
if (this.isAutoRange()) {
this.autoAdjustRange();
}
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the number format override. If this is non-null, then it will be
* used to format the numbers on the axis.
*
* @param formatter
* the number formatter (<code>null</code> permitted).
*/
public void setNumberFormatOverride(final NumberFormat formatter) {
this.numberFormatOverride = formatter;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the axis range type.
*
* @param rangeType
* the range type (<code>null</code> not permitted).
*/
public void setRangeType(final RangeType rangeType) {
if (rangeType == null) {
throw new IllegalArgumentException("Null 'rangeType' argument.");
}
this.rangeType = rangeType;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the tick unit for the axis and sends an {@link AxisChangeEvent} to
* all registered listeners. A side effect of calling this method is that
* the "auto-select" feature for tick units is switched off (you can restore
* it using the {@link ValueAxis#setAutoTickUnitSelection(boolean)} method).
*
* @param unit
* the new tick unit (<code>null</code> not permitted).
*
* @see #getTickUnit()
* @see #setTickUnit(NumberTickUnit, boolean, boolean)
*/
public void setTickUnit(final NumberTickUnit unit) {
// defer argument checking...
this.setTickUnit(unit, true, true);
}
/**
* Sets the tick unit for the axis and, if requested, sends an
* {@link AxisChangeEvent} to all registered listeners. In addition, an
* option is provided to turn off the "auto-select" feature for tick units
* (you can restore it using the
* {@link ValueAxis#setAutoTickUnitSelection(boolean)} method).
*
* @param unit
* the new tick unit (<code>null</code> not permitted).
* @param notify
* notify listeners?
* @param turnOffAutoSelect
* turn off the auto-tick selection?
*/
public void setTickUnit(final NumberTickUnit unit, final boolean notify,
final boolean turnOffAutoSelect) {
if (unit == null) {
throw new IllegalArgumentException("Null 'unit' argument.");
}
this.tickUnit = unit;
if (turnOffAutoSelect) {
this.setAutoTickUnitSelection(false, false);
}
if (notify) {
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Converts a data value to a coordinate in Java2D space, assuming that the
* axis runs along one edge of the specified dataArea.
* <p>
* Note that it is possible for the coordinate to fall outside the plotArea.
*
* @param value
* the data value.
* @param area
* the area for plotting the data.
* @param edge
* the axis location.
*
* @return The Java2D coordinate.
*/
public double valueToJava2D(final double value, final Rectangle area,
final RectangleEdge edge) {
final Range range = this.getRange();
final double axisMin = range.getLowerBound();
final double axisMax = range.getUpperBound();
double min = 0.0;
double max = 0.0;
if (edge.isTopOrBottom()) {
min = RectangleUtil.getMinX(area);
max = RectangleUtil.getMaxX(area);
} else if (edge.isLeftOrRight()) {
max = RectangleUtil.getMinY(area);
min = RectangleUtil.getMaxY(area);
}
if (this.isInverted()) {
return max - ((value - axisMin) / (axisMax - axisMin))
* (max - min);
} else {
return min + ((value - axisMin) / (axisMax - axisMin))
* (max - min);
}
}
}