/******************************************************************************* * Copyright (c) 2008-2009 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 ) ? 0 : (int)min; max = ( 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; } }