/*******************************************************************************
* Copyright (c) 2008-2011 SWTChart project. All rights reserved.
*
* This code is distributed under the terms of the Eclipse Public License v1.0
* which is available at http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.swtchart.internal.axis;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Event;
import org.swtchart.Chart;
import org.swtchart.IAxis;
import org.swtchart.IDisposeListener;
import org.swtchart.IGrid;
import org.swtchart.ISeries;
import org.swtchart.ITitle;
import org.swtchart.Range;
import org.swtchart.internal.Grid;
import org.swtchart.internal.series.Series;
import org.swtchart.internal.series.SeriesSet;
/**
* An axis.
*/
public class Axis implements IAxis {
/** the margin in pixels */
public final static int MARGIN = 5;
/** the default minimum value of range */
public final static double DEFAULT_MIN = 0d;
/** the default maximum value of range */
public final static double DEFAULT_MAX = 1d;
/** the default minimum value of log scale range */
public final static double DEFAULT_LOG_SCALE_MIN = 0.1d;
/** the default maximum value of log scale range */
public final static double DEFAULT_LOG_SCALE_MAX = 1d;
/** the ratio to be zoomed */
private static final double ZOOM_RATIO = 0.2;
/** the ratio to be scrolled */
private static final double SCROLL_RATIO = 0.1;
/** the maximum resolution with digits */
private static final double MAX_RESOLUTION = 13;
/** the axis id */
private int id;
/** the axis direction */
private Direction direction;
/** the axis position */
private Position position;
/** the minimum value of axis range */
private double min;
/** the maximum value of axis range */
private double max;
/** the axis title */
private AxisTitle title;
/** the axis tick */
private AxisTick tick;
/** the grid */
private Grid grid;
/** the plot chart */
private Chart chart;
/** the state if the axis scale is log scale */
private boolean logScaleEnabled;
/** the state indicating if axis type is category */
private boolean categoryAxisEnabled;
/** the category series */
private String[] categorySeries;
/** the number of riser per category */
private int numRisers;
/** the state indicating if the axis is horizontal */
private boolean isHorizontalAxis;
/** the plot area width */
private int width;
/** the plot area height */
private int height;
/** the list of dispose listeners */
private List<IDisposeListener> listeners;
/**
* Constructor.
*
* @param id
* the axis index
* @param direction
* the axis direction (X or Y)
* @param chart
* the chart
*/
public Axis(int id, Direction direction, Chart chart) {
this.id = id;
this.direction = direction;
this.chart = chart;
grid = new Grid(this);
title = new AxisTitle(chart, SWT.NONE, this, direction);
tick = new AxisTick(chart, this);
listeners = new ArrayList<IDisposeListener>();
// sets initial default values
position = Position.Primary;
min = DEFAULT_MIN;
max = DEFAULT_MAX;
if (direction == Direction.X) {
title.setText("X axis");
} else if (direction == Direction.Y) {
title.setText("Y axis");
}
logScaleEnabled = false;
categoryAxisEnabled = false;
}
/*
* @see IAxis#getId()
*/
public int getId() {
return id;
}
/*
* @see IAxis#getDirection()
*/
public Direction getDirection() {
return direction;
}
/*
* @see IAxis#getPosition()
*/
public Position getPosition() {
return position;
}
/*
* @see IAxis#setPosition(Position)
*/
public void setPosition(Position position) {
if (position == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
}
if (this.position == position) {
return;
}
this.position = position;
chart.updateLayout();
}
/*
* @see IAxis#setRange(Range)
*/
public void setRange(Range range) {
setRange(range, true);
}
/**
* Sets the axis range.
*
* @param range
* the axis range
* @param update
* true if updating the chart layout
*/
public void setRange(Range range, boolean update) {
if (range == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
return; // to suppress warnings...
}
if (Double.isNaN(range.lower) || Double.isNaN(range.upper)
|| range.lower > range.upper) {
throw new IllegalArgumentException("Illegal range: " + range);
}
if (min == range.lower && max == range.upper) {
return;
}
if (isValidCategoryAxis()) {
min = (int) range.lower;
max = (int) range.upper;
if (min < 0) {
min = 0;
}
if (max > categorySeries.length - 1) {
max = categorySeries.length - 1;
}
} else {
if (range.lower == range.upper) {
throw new IllegalArgumentException("Given range is invalid");
}
if (logScaleEnabled && range.lower <= 0) {
range.lower = min;
}
if (Math.abs(range.lower / (range.upper - range.lower)) > Math.pow(
10, MAX_RESOLUTION)) {
return;
}
min = range.lower;
max = range.upper;
}
if (update) {
chart.updateLayout();
}
}
/*
* @see IAxis#getRange()
*/
public Range getRange() {
return new Range(min, max);
}
/*
* @see IAxis#getTitle()
*/
public ITitle getTitle() {
return title;
}
/*
* @see IAxis#getTick()
*/
public AxisTick getTick() {
return tick;
}
/*
* @see IAxis#enableLogScale(boolean)
*/
public void enableLogScale(boolean enabled) throws IllegalStateException {
if (logScaleEnabled == enabled) {
return;
}
if (enabled) {
if (chart.getSeriesSet().getSeries().length == 0) {
if (min <= 0) {
min = DEFAULT_LOG_SCALE_MIN;
}
if (max < min) {
max = DEFAULT_LOG_SCALE_MAX;
}
} else {
// check if series contain negative value
double minSeriesValue = getMinSeriesValue();
if (enabled && minSeriesValue <= 0) {
throw new IllegalStateException(
"Series contain negative value.");
}
// adjust the range in order not to have negative value
if (min <= 0) {
min = minSeriesValue;
}
}
// disable category axis
categoryAxisEnabled = false;
}
logScaleEnabled = enabled;
chart.updateLayout();
((SeriesSet) chart.getSeriesSet()).compressAllSeries();
}
/**
* Gets the minimum value of series belonging to this axis.
*
* @return the minimum value of series belonging to this axis
*/
private double getMinSeriesValue() {
double minimum = Double.NaN;
for (ISeries series : chart.getSeriesSet().getSeries()) {
double lower;
if (direction == Direction.X && series.getXAxisId() == getId()) {
lower = ((Series) series).getXRange().lower;
} else if (direction == Direction.Y
&& series.getYAxisId() == getId()) {
lower = ((Series) series).getYRange().lower;
} else {
continue;
}
if (Double.isNaN(minimum) || lower < minimum) {
minimum = lower;
}
}
return minimum;
}
/*
* @see IAxis#isLogScaleEnabled()
*/
public boolean isLogScaleEnabled() {
return logScaleEnabled;
}
/*
* @see IAxis#getGrid()
*/
public IGrid getGrid() {
return grid;
}
/*
* @see IAxis#adjustRange()
*/
public void adjustRange() {
adjustRange(true);
}
/**
* Adjusts the axis range to the series belonging to the axis.
*
* @param update
* true if updating chart layout
*/
public void adjustRange(boolean update) {
if (isValidCategoryAxis()) {
setRange(new Range(0, categorySeries.length - 1));
return;
}
double minimum = Double.NaN;
double maximum = Double.NaN;
for (ISeries series : chart.getSeriesSet().getSeries()) {
int axisId = direction == Direction.X ? series.getXAxisId()
: series.getYAxisId();
if (!series.isVisible() || getId() != axisId) {
continue;
}
// get axis length
int length;
if (isHorizontalAxis) {
length = chart.getPlotArea().getSize().x;
} else {
length = chart.getPlotArea().getSize().y;
}
// get min and max value of series
Range range = ((Series) series).getAdjustedRange(this, length);
if (Double.isNaN(minimum) || range.lower < minimum) {
minimum = range.lower;
}
if (Double.isNaN(maximum) || range.upper > maximum) {
maximum = range.upper;
}
}
// set adjusted range
if (!Double.isNaN(minimum) && !Double.isNaN(maximum)) {
if (minimum == maximum) {
double margin = (minimum == 0)? 1d : Math.abs(minimum / 2d);
minimum -= margin;
maximum += margin;
}
setRange(new Range(minimum, maximum), update);
}
}
/*
* @see IAxis#zoomIn()
*/
public void zoomIn() {
zoomIn((max + min) / 2d);
}
/*
* @see IAxis#zoomIn(double)
*/
public void zoomIn(double coordinate) {
double lower = min;
double upper = max;
if (isValidCategoryAxis()) {
if (lower != upper) {
if ((min + max) / 2d < coordinate) {
lower = min + 1;
} else if (coordinate < (min + max) / 2d) {
upper = max - 1;
} else {
lower = min + 1;
upper = max - 1;
}
}
} else if (isLogScaleEnabled()) {
double digitMin = Math.log10(min);
double digitMax = Math.log10(max);
double digitCoordinate = Math.log10(coordinate);
lower = Math.pow(10, digitMin + 2 * SCROLL_RATIO
* (digitCoordinate - digitMin));
upper = Math.pow(10, digitMax + 2 * SCROLL_RATIO
* (digitCoordinate - digitMax));
} else {
lower = min + 2 * ZOOM_RATIO * (coordinate - min);
upper = max + 2 * ZOOM_RATIO * (coordinate - max);
}
setRange(new Range(lower, upper));
}
/*
* @see IAxis#zoomOut()
*/
public void zoomOut() {
zoomOut((min + max) / 2d);
}
/*
* @see IAxis#zoomOut(double)
*/
public void zoomOut(double coordinate) {
double lower = min;
double upper = max;
if (isValidCategoryAxis()) {
if ((min + max) / 2d < coordinate && min != 0) {
lower = min - 1;
} else if (coordinate < (min + max) / 2d
&& max != categorySeries.length - 1) {
upper = max + 1;
} else {
lower = min - 1;
upper = max + 1;
}
} else if (isLogScaleEnabled()) {
double digitMin = Math.log10(min);
double digitMax = Math.log10(max);
double digitCoordinate = Math.log10(coordinate);
lower = Math.pow(10, (digitMin - ZOOM_RATIO * digitCoordinate)
/ (1 - ZOOM_RATIO));
upper = Math.pow(10, (digitMax - ZOOM_RATIO * digitCoordinate)
/ (1 - ZOOM_RATIO));
} else {
lower = (min - 2 * ZOOM_RATIO * coordinate) / (1 - 2 * ZOOM_RATIO);
upper = (max - 2 * ZOOM_RATIO * coordinate) / (1 - 2 * ZOOM_RATIO);
}
setRange(new Range(lower, upper));
}
/*
* @see IAxis#scrollUp()
*/
public void scrollUp() {
double lower = min;
double upper = max;
if (isValidCategoryAxis()) {
if (upper < categorySeries.length - 1) {
lower = min + 1;
upper = max + 1;
}
} else if (isLogScaleEnabled()) {
double digitMax = Math.log10(upper);
double digitMin = Math.log10(lower);
upper = Math.pow(10, digitMax + (digitMax - digitMin)
* SCROLL_RATIO);
lower = Math.pow(10, digitMin + (digitMax - digitMin)
* SCROLL_RATIO);
} else {
lower = min + (max - min) * SCROLL_RATIO;
upper = max + (max - min) * SCROLL_RATIO;
}
setRange(new Range(lower, upper));
}
/*
* @see IAxis#scrollDown()
*/
public void scrollDown() {
double lower = min;
double upper = max;
if (isValidCategoryAxis()) {
if (lower >= 1) {
lower = min - 1;
upper = max - 1;
}
} else if (isLogScaleEnabled()) {
double digitMax = Math.log10(upper);
double digitMin = Math.log10(lower);
upper = Math.pow(10, digitMax - (digitMax - digitMin)
* SCROLL_RATIO);
lower = Math.pow(10, digitMin - (digitMax - digitMin)
* SCROLL_RATIO);
} else {
lower = min - (max - min) * SCROLL_RATIO;
upper = max - (max - min) * SCROLL_RATIO;
}
setRange(new Range(lower, upper));
}
/*
* @see IAxis#isCategoryEnabled()
*/
public boolean isCategoryEnabled() {
return categoryAxisEnabled;
}
/**
* Gets the state indicating if the axis is valid category axis.
*
* @return true if the axis is valid category axis
*/
public boolean isValidCategoryAxis() {
return categoryAxisEnabled && categorySeries != null
&& categorySeries.length != 0;
}
/*
* @see IAxis#enableCategory(boolean)
*/
public void enableCategory(boolean enabled) {
if (categoryAxisEnabled == enabled) {
return;
}
if (enabled) {
if (direction == Direction.Y) {
throw new IllegalStateException(
"Y axis cannot be category axis.");
}
if (categorySeries != null && categorySeries.length != 0) {
min = (min < 0 || min >= categorySeries.length) ? 0 : (int) min;
max = (max < 0 || max >= categorySeries.length) ? max = categorySeries.length - 1
: (int) max;
}
logScaleEnabled = false;
}
categoryAxisEnabled = enabled;
chart.updateLayout();
((SeriesSet) chart.getSeriesSet()).updateCompressor(this);
((SeriesSet) chart.getSeriesSet()).updateStackAndRiserData();
}
/*
* @see IAxis#setCategorySeries(String[])
*/
public void setCategorySeries(String[] series) {
if (series == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
return; // to suppress warnings...
}
if (direction == Direction.Y) {
throw new IllegalStateException("Y axis cannot be category axis.");
}
String[] copiedSeries = new String[series.length];
System.arraycopy(series, 0, copiedSeries, 0, series.length);
categorySeries = copiedSeries;
if (isValidCategoryAxis()) {
min = (min < 0) ? 0 : (int) min;
max = (max >= categorySeries.length) ? max = categorySeries.length - 1
: (int) max;
}
chart.updateLayout();
((SeriesSet) chart.getSeriesSet()).updateCompressor(this);
((SeriesSet) chart.getSeriesSet()).updateStackAndRiserData();
}
/*
* @see IAxis#getCategorySeries()
*/
public String[] getCategorySeries() {
String[] copiedCategorySeries = null;
if (categorySeries != null) {
copiedCategorySeries = new String[categorySeries.length];
System.arraycopy(categorySeries, 0, copiedCategorySeries, 0,
categorySeries.length);
}
return copiedCategorySeries;
}
/*
* @see IAxis#getPixelCoordinate(double)
*/
public int getPixelCoordinate(double dataCoordinate) {
return getPixelCoordinate(dataCoordinate, min, max);
}
/**
* Gets the pixel coordinate corresponding to the given data coordinate.
*
* @param dataCoordinate
* the data coordinate
* @param lower
* the min value of range
* @param upper
* the max value of range
* @return the pixel coordinate on plot area
*/
public int getPixelCoordinate(double dataCoordinate, double lower,
double upper) {
int pixelCoordinate;
if (isHorizontalAxis) {
if (logScaleEnabled) {
pixelCoordinate = (int) ((Math.log10(dataCoordinate) - Math
.log10(lower))
/ (Math.log10(upper) - Math.log10(lower)) * width);
} else if (categoryAxisEnabled) {
pixelCoordinate = (int) ((dataCoordinate + 0.5 - lower)
/ (upper + 1 - lower) * width);
} else {
pixelCoordinate = (int) ((dataCoordinate - lower)
/ (upper - lower) * width);
}
} else {
if (logScaleEnabled) {
pixelCoordinate = (int) ((Math.log10(upper) - Math
.log10(dataCoordinate))
/ (Math.log10(upper) - Math.log10(lower)) * height);
} else if (categoryAxisEnabled) {
pixelCoordinate = (int) ((upper - dataCoordinate + 0.5)
/ (upper + 1 - lower) * height);
} else {
pixelCoordinate = (int) ((upper - dataCoordinate)
/ (upper - lower) * height);
}
}
return pixelCoordinate;
}
/*
* @see IAxis#getDataCoordinate(int)
*/
public double getDataCoordinate(int pixelCoordinate) {
return getDataCoordinate(pixelCoordinate, min, max);
}
/**
* Gets the data coordinate corresponding to the given pixel coordinate on
* plot area.
*
* @param pixelCoordinate
* the pixel coordinate on plot area
* @param lower
* the min value of range
* @param upper
* the max value of range
* @return the data coordinate
*/
public double getDataCoordinate(int pixelCoordinate, double lower,
double upper) {
double dataCoordinate;
if (isHorizontalAxis) {
if (logScaleEnabled) {
dataCoordinate = Math.pow(10, pixelCoordinate / (double) width
* (Math.log10(upper) - Math.log10(lower))
+ Math.log10(lower));
} else if (categoryAxisEnabled) {
dataCoordinate = Math.floor(pixelCoordinate / (double) width
* (upper + 1 - lower) + lower);
} else {
dataCoordinate = pixelCoordinate / (double) width
* (upper - lower) + lower;
}
} else {
if (logScaleEnabled) {
dataCoordinate = Math.pow(10, Math.log10(upper)
- pixelCoordinate / (double) height
* (Math.log10(upper) - Math.log10(lower)));
} else if (categoryAxisEnabled) {
dataCoordinate = Math.floor(upper + 1 - pixelCoordinate
/ (double) height * (upper + 1 - lower));
} else {
dataCoordinate = (height - pixelCoordinate) / (double) height
* (upper - lower) + lower;
}
}
return dataCoordinate;
}
/**
* Sets the number of risers per category.
*
* @param numRisers
* the number of risers per category
*/
public void setNumRisers(int numRisers) {
this.numRisers = numRisers;
}
/**
* Gets the number of risers per category.
*
* @return number of riser per category
*/
public int getNumRisers() {
return numRisers;
}
/**
* Checks if the axis is horizontal. X axis is not always horizontal. Y axis
* can be horizontal with <tt>Chart.setOrientation(SWT.VERTICAL)</tt>.
*
* @return true if the axis is horizontal
*/
public boolean isHorizontalAxis() {
int orientation = chart.getOrientation();
return (direction == Direction.X && orientation == SWT.HORIZONTAL)
|| (direction == Direction.Y && orientation == SWT.VERTICAL);
}
/**
* Disposes the resources.
*/
protected void dispose() {
tick.getAxisTickLabels().dispose();
tick.getAxisTickMarks().dispose();
title.dispose();
for (IDisposeListener listener : listeners) {
listener.disposed(new Event());
}
}
/*
* @see IAxis#addDisposeListener(IDisposeListener)
*/
public void addDisposeListener(IDisposeListener listener) {
listeners.add(listener);
}
/**
* Updates the layout data.
*/
public void updateLayoutData() {
title.updateLayoutData();
tick.updateLayoutData();
}
/**
* Refreshes the cache.
*/
public void refresh() {
int orientation = chart.getOrientation();
isHorizontalAxis = (direction == Direction.X && orientation == SWT.HORIZONTAL)
|| (direction == Direction.Y && orientation == SWT.VERTICAL);
width = chart.getPlotArea().getBounds().width;
height = chart.getPlotArea().getBounds().height;
}
/**
* Gets the state indicating if date is enabled.
*
* @return true if date is enabled
*/
public boolean isDateEnabled() {
if (!isHorizontalAxis) {
return false;
}
for (ISeries series : chart.getSeriesSet().getSeries()) {
if (series.getXAxisId() != id) {
continue;
}
if (((Series) series).isDateSeries() && series.isVisible()) {
return true;
}
}
return false;
}
}