/******************************************************************************* * 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; } }