/******************************************************************************* * 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.math.BigDecimal; import java.text.DecimalFormat; import java.text.Format; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.eclipse.swt.SWT; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Display; import org.swtchart.Chart; import org.swtchart.internal.ChartLayoutData; import org.swtchart.internal.Util; /** * Axis tick labels. */ public class AxisTickLabels implements PaintListener { /** the chart */ private Chart chart; /** the axis */ private Axis axis; /** the foreground color */ private Color foreground; /** the width hint of tick labels area */ private int widthHint; /** the height hint of tick labels area */ private int heightHint; /** the bounds of tick labels area */ private Rectangle bounds; /** the array of tick label vales */ private ArrayList<Double> tickLabelValues; /** the array of tick label */ private ArrayList<String> tickLabels; /** the array of tick label position in pixels */ private ArrayList<Integer> tickLabelPositions; /** the array of visibility state of tick label */ private ArrayList<Boolean> tickVisibilities; /** the maximum length of tick labels */ private int tickLabelMaxLength; /** the format for tick labels */ private Format format; /** the default foreground */ private static final int DEFAULT_FOREGROUND = SWT.COLOR_BLUE; /** the default font */ private static final Font DEFAULT_FONT = Display.getDefault ().getSystemFont (); /** the default label format */ private static final String DEFAULT_DECIMAL_FORMAT = "#.###########"; /** the possible tick steps */ private Map<Integer, Integer[]> possibleTickSteps; /** the time unit for tick step */ private int timeUnit; /** the font */ private Font font; /** * Constructor. * * @param chart * the chart * @param axis * the axis */ protected AxisTickLabels ( Chart chart, Axis axis ) { this.chart = chart; this.axis = axis; tickLabelValues = new ArrayList<Double> (); tickLabels = new ArrayList<String> (); tickLabelPositions = new ArrayList<Integer> (); tickVisibilities = new ArrayList<Boolean> (); initializePossibleTickSteps (); font = DEFAULT_FONT; foreground = Display.getDefault ().getSystemColor ( DEFAULT_FOREGROUND ); chart.addPaintListener ( this ); } /** * Initialized the possible tick steps. */ private void initializePossibleTickSteps () { final Integer[] milliseconds = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 999 }; final Integer[] seconds = { 1, 2, 5, 10, 15, 20, 30, 59 }; final Integer[] minutes = { 1, 2, 3, 5, 10, 15, 20, 30, 59 }; final Integer[] hours = { 1, 2, 3, 4, 6, 12, 22 }; final Integer[] dates = { 1, 7, 14, 28 }; final Integer[] months = { 1, 2, 3, 4, 6, 11 }; final Integer[] years = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 }; possibleTickSteps = new HashMap<Integer, Integer[]> (); possibleTickSteps.put ( Calendar.MILLISECOND, milliseconds ); possibleTickSteps.put ( Calendar.SECOND, seconds ); possibleTickSteps.put ( Calendar.MINUTE, minutes ); possibleTickSteps.put ( Calendar.HOUR_OF_DAY, hours ); possibleTickSteps.put ( Calendar.DATE, dates ); possibleTickSteps.put ( Calendar.MONTH, months ); possibleTickSteps.put ( Calendar.YEAR, years ); } /** * Sets the foreground color. * * @param color * the foreground color */ public void setForeground ( Color color ) { if ( color == null ) { foreground = Display.getDefault ().getSystemColor ( DEFAULT_FOREGROUND ); } else { foreground = color; } } /** * Gets the foreground color. * * @return the foreground color */ protected Color getForeground () { if ( foreground.isDisposed () ) { foreground = Display.getDefault ().getSystemColor ( DEFAULT_FOREGROUND ); } return foreground; } /** * Updates the tick labels. * * @param length * the axis length */ protected void update ( int length ) { tickLabelValues.clear (); tickLabels.clear (); tickLabelPositions.clear (); if ( axis.isValidCategoryAxis () ) { updateTickLabelForCategoryAxis ( length ); } else if ( axis.isLogScaleEnabled () ) { updateTickLabelForLogScale ( length ); } else if ( axis.isDateEnabled () ) { updateTickLabelForDateAxis ( length ); } else { updateTickLabelForLinearScale ( length ); } updateTickVisibility (); updateTickLabelMaxLength (); } /** * Updates tick label for date axis. * * @param length * the length of axis */ private void updateTickLabelForDateAxis ( int length ) { double min = axis.getRange ().lower; double max = axis.getRange ().upper; double gridStepHint = Math.abs ( max - min ) / length * axis.getTick ().getTickMarkStepHint (); timeUnit = getTimeUnit ( gridStepHint ); if ( timeUnit == Calendar.MILLISECOND || timeUnit == Calendar.SECOND || timeUnit == Calendar.MINUTE || timeUnit == Calendar.HOUR_OF_DAY || timeUnit == Calendar.DATE ) { Integer[] steps = possibleTickSteps.get ( timeUnit ); for ( int i = 0; i < steps.length - 1; i++ ) { if ( gridStepHint < ( getPeriodInMillis ( timeUnit, steps[i] ) + getPeriodInMillis ( timeUnit, steps[i + 1] ) ) / 2d ) { BigDecimal gridStep = new BigDecimal ( Long .valueOf ( getPeriodInMillis ( timeUnit, steps[i] ) ) .toString () ); updateTickLabelForLinearScale ( length, gridStep ); break; } } } else if ( timeUnit == Calendar.MONTH || timeUnit == Calendar.YEAR ) { updateTickLabelForMonthOrYear ( length, gridStepHint, timeUnit ); } } /** * Updates the tick label for month or year. The month and year are handled * differently from other units of time, since 1 month and 1 year can be * different depending on which time to start counting. * * @param length * the length of axis * @param gridStepHint * the grid step hint * @param tickStepUnit * the tick step unit of time */ private void updateTickLabelForMonthOrYear ( int length, double gridStepHint, int tickStepUnit ) { double min = axis.getRange ().lower; double max = axis.getRange ().upper; // get initial position Calendar cal = Calendar.getInstance (); cal.setTime ( new Date ( (long)min ) ); int month = cal.get ( Calendar.MONTH ); int year = cal.get ( Calendar.YEAR ); if ( tickStepUnit == Calendar.MONTH ) { if ( month == Calendar.DECEMBER ) { year++; month = Calendar.JANUARY; } else { month++; } } else if ( tickStepUnit == Calendar.YEAR ) { month = Calendar.JANUARY; year++; } // get tick step Integer[] steps = possibleTickSteps.get ( tickStepUnit ); int step = steps[steps.length - 1]; for ( int i = 0; i < steps.length - 1; i++ ) { if ( gridStepHint < ( getPeriodInMillis ( tickStepUnit, steps[i] ) + getPeriodInMillis ( tickStepUnit, steps[i + 1] ) ) / 2d ) { step = steps[i]; break; } } // set tick labels cal.clear (); cal.set ( year, month, 1 ); while ( cal.getTimeInMillis () < max ) { tickLabelValues.add ( Double.valueOf ( cal.getTimeInMillis () ) ); tickLabels.add ( format ( cal.getTimeInMillis () ) ); int tickLabelPosition = (int) ( ( cal.getTimeInMillis () - min ) / ( max - min ) * length ); tickLabelPositions.add ( tickLabelPosition ); if ( tickStepUnit == Calendar.MONTH ) { month += step; if ( month + step > Calendar.DECEMBER ) { year++; month -= Calendar.DECEMBER + 1; } } else if ( tickStepUnit == Calendar.YEAR ) { year += step; } cal.clear (); cal.set ( year, month, 1 ); } } /** * Updates tick label for category axis. * * @param length * the length of axis */ private void updateTickLabelForCategoryAxis ( int length ) { String[] series = axis.getCategorySeries (); if ( series == null ) { return; } int min = (int)axis.getRange ().lower; int max = (int)axis.getRange ().upper; int sizeOfTickLabels = ( series.length < max - min + 1 ) ? series.length : max - min + 1; int initialIndex = ( min < 0 ) ? 0 : min; for ( int i = 0; i < sizeOfTickLabels; i++ ) { tickLabels.add ( series[i + initialIndex] ); int tickLabelPosition = (int) ( length * ( i + 0.5 ) / sizeOfTickLabels ); tickLabelPositions.add ( tickLabelPosition ); } } /** * Updates tick label for log scale. * * @param length * the length of axis */ private void updateTickLabelForLogScale ( int length ) { double min = axis.getRange ().lower; double max = axis.getRange ().upper; int digitMin = (int)Math.ceil ( Math.log10 ( min ) ); int digitMax = (int)Math.ceil ( Math.log10 ( max ) ); final BigDecimal MIN = new BigDecimal ( new Double ( min ).toString () ); BigDecimal tickStep = pow ( 10, digitMin - 1 ); BigDecimal firstPosition; if ( MIN.remainder ( tickStep ).doubleValue () <= 0 ) { firstPosition = MIN.subtract ( MIN.remainder ( tickStep ) ); } else { firstPosition = MIN.subtract ( MIN.remainder ( tickStep ) ).add ( tickStep ); } for ( int i = digitMin; i <= digitMax; i++ ) { for ( BigDecimal j = firstPosition; j.doubleValue () <= pow ( 10, i ).doubleValue (); j = j.add ( tickStep ) ) { if ( j.doubleValue () > max ) { break; } if ( axis.isDateEnabled () ) { Date date = new Date ( (long)j.doubleValue () ); tickLabels.add ( format ( date ) ); } else { tickLabels.add ( format ( j.doubleValue () ) ); } tickLabelValues.add ( j.doubleValue () ); int tickLabelPosition = (int) ( ( Math.log10 ( j.doubleValue () ) - Math.log10 ( min ) ) / ( Math .log10 ( max ) - Math.log10 ( min ) ) * length ); tickLabelPositions.add ( tickLabelPosition ); } tickStep = tickStep.multiply ( pow ( 10, 1 ) ); firstPosition = tickStep.add ( pow ( 10, i ) ); } } /** * Updates tick label for normal scale. * * @param length * axis length (>0) */ private void updateTickLabelForLinearScale ( int length ) { double min = axis.getRange ().lower; double max = axis.getRange ().upper; updateTickLabelForLinearScale ( length, getGridStep ( length, min, max ) ); } /** * Updates tick label for normal scale. * * @param length * axis length (>0) * @param tickStep * the tick step */ private void updateTickLabelForLinearScale ( int length, BigDecimal tickStep ) { double min = axis.getRange ().lower; double max = axis.getRange ().upper; final BigDecimal MIN = new BigDecimal ( new Double ( min ).toString () ); BigDecimal firstPosition; /* if (min % tickStep <= 0) */ if ( MIN.remainder ( tickStep ).doubleValue () <= 0 ) { /* firstPosition = min - min % tickStep */ firstPosition = MIN.subtract ( MIN.remainder ( tickStep ) ); } else { /* firstPosition = min - min % tickStep + tickStep */ firstPosition = MIN.subtract ( MIN.remainder ( tickStep ) ).add ( tickStep ); } // the unit time starts from 1:00 if ( axis.isDateEnabled () ) { BigDecimal zeroOclock = firstPosition.subtract ( new BigDecimal ( new Double ( 3600000 ).toString () ) ); if ( MIN.compareTo ( zeroOclock ) == -1 ) { firstPosition = zeroOclock; } } for ( BigDecimal b = firstPosition; b.doubleValue () <= max; b = b.add ( tickStep ) ) { if ( axis.isDateEnabled () ) { Date date = new Date ( (long)b.doubleValue () ); tickLabels.add ( format ( date ) ); } else { tickLabels.add ( format ( b.doubleValue () ) ); } tickLabelValues.add ( b.doubleValue () ); int tickLabelPosition = (int) ( ( b.doubleValue () - min ) / ( max - min ) * length ); tickLabelPositions.add ( tickLabelPosition ); } } /** * Updates the visibility of tick labels. */ private void updateTickVisibility () { // initialize the array of tick label visibility state tickVisibilities.clear (); for ( int i = 0; i < tickLabelPositions.size (); i++ ) { tickVisibilities.add ( Boolean.TRUE ); } if ( tickLabelPositions.size () == 0 ) { return; } // set the tick label visibility int previousPosition = 0; for ( int i = 0; i < tickLabelPositions.size (); i++ ) { // check if there is enough space to draw tick label boolean hasSpaceToDraw = true; if ( i != 0 ) { hasSpaceToDraw = hasSpaceToDraw ( previousPosition, tickLabelPositions.get ( i ), tickLabels.get ( i ) ); } // check if the tick label value is major boolean isMajorTick = true; if ( !axis.isValidCategoryAxis () && axis.isLogScaleEnabled () ) { isMajorTick = isMajorTick ( tickLabelValues.get ( i ) ); } // check if the same tick label is repeated String currentLabel = tickLabels.get ( i ); try { double value = Double.parseDouble ( currentLabel ); if ( value != tickLabelValues.get ( i ) ) { isMajorTick = false; } } catch ( NumberFormatException e ) { // label is not decimal value but string } if ( hasSpaceToDraw && isMajorTick ) { previousPosition = tickLabelPositions.get ( i ); } else { tickVisibilities.set ( i, Boolean.FALSE ); } } } /** * Gets the tick step unit. * * @param gridStepHint * the grid step hint * @return the tick step unit. */ private int getTimeUnit ( double gridStepHint ) { final Integer[] units = { Calendar.MILLISECOND, Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DATE, Calendar.MONTH, Calendar.YEAR }; for ( Integer unit : units ) { Integer[] steps = possibleTickSteps.get ( unit ); if ( gridStepHint < ( getPeriodInMillis ( unit, steps[steps.length - 2] ) + getPeriodInMillis ( unit, steps[steps.length - 1] ) ) / 2d ) { return unit; } } return Calendar.YEAR; } /** * Gets the period in milliseconds of given unit of date and amount. The * period is calculated based on UTC of January 1, 1970. * * @param unit * the unit of time like <tt>Calendar.YEAR<tt>. * @param amount * the amount of period. * @return the period in milliseconds */ private long getPeriodInMillis ( int unit, int amount ) { Calendar cal = Calendar.getInstance (); cal.setTimeInMillis ( 0 ); cal.roll ( unit, amount ); return cal.getTimeInMillis (); } /** * Formats the given object. * * @param obj * the object * @return the formatted string */ private String format ( Object obj ) { if ( format == null ) { if ( axis.isDateEnabled () ) { String dateFormat = "yyyyy.MMMMM.dd"; if ( timeUnit == Calendar.MILLISECOND ) { dateFormat = "HH:mm:ss.SSS"; } else if ( timeUnit == Calendar.SECOND ) { dateFormat = "HH:mm:ss"; } else if ( timeUnit == Calendar.MINUTE ) { dateFormat = "HH:mm"; } else if ( timeUnit == Calendar.HOUR_OF_DAY ) { dateFormat = "dd HH:mm"; } else if ( timeUnit == Calendar.DATE ) { dateFormat = "MMMMM d"; } else if ( timeUnit == Calendar.MONTH ) { dateFormat = "yyyy MMMMM"; } else if ( timeUnit == Calendar.YEAR ) { dateFormat = "yyyy"; } return new SimpleDateFormat ( dateFormat ).format ( obj ); } return new DecimalFormat ( DEFAULT_DECIMAL_FORMAT ).format ( obj ); } return format.format ( obj ); } /** * Checks if the tick label is major (...,0.01,0.1,1,10,100,...). * * @param tickValue * the tick label value * @return true if the tick label is major */ private boolean isMajorTick ( double tickValue ) { if ( !axis.isLogScaleEnabled () ) { return true; } if ( Math.log10 ( tickValue ) % 1 == 0 ) { return true; } return false; } /** * Returns the state indicating if there is a space to draw tick label. * * @param previousPosition * the previously drawn tick label position. * @param tickLabelPosition * the tick label position. * @param tickLabel * the tick label text * @return true if there is a space to draw tick label */ private boolean hasSpaceToDraw ( int previousPosition, int tickLabelPosition, String tickLabel ) { Point p = Util.getExtentInGC ( axis.getTick ().getFont (), tickLabel ); int interval = tickLabelPosition - previousPosition; int textLength = axis.isHorizontalAxis () ? p.x : p.y; return interval > textLength; } /** * Gets max length of tick label. */ private void updateTickLabelMaxLength () { int maxLength = 0; for ( int i = 0; i < tickLabels.size (); i++ ) { if ( tickVisibilities.size () > i && tickVisibilities.get ( i ) == true ) { Point p = Util.getExtentInGC ( axis.getTick ().getFont (), tickLabels.get ( i ) ); if ( p.x > maxLength ) { maxLength = p.x; } } } tickLabelMaxLength = maxLength; } /** * Calculates the value of the first argument raised to the power of the * second argument. * * @param base * the base * @param expornent * the exponent * @return the value <tt>a<sup>b</sup></tt> in <tt>BigDecimal</tt> */ private BigDecimal pow ( double base, int expornent ) { BigDecimal value; if ( expornent > 0 ) { value = new BigDecimal ( new Double ( base ).toString () ).pow ( expornent ); } else { value = BigDecimal.ONE.divide ( new BigDecimal ( new Double ( base ).toString () ).pow ( -expornent ) ); } return value; } /** * Gets the grid step. * * @param lengthInPixels * axis length in pixels * @param min * minimum value * @param max * maximum value * @return rounded value. */ private BigDecimal getGridStep ( int lengthInPixels, double min, double max ) { if ( lengthInPixels <= 0 ) { throw new IllegalArgumentException ( "lengthInPixels must be positive value." ); } if ( min >= max ) { throw new IllegalArgumentException ( "min must be less than max." ); } double length = Math.abs ( max - min ); double gridStepHint = length / lengthInPixels * axis.getTick ().getTickMarkStepHint (); // gridStepHint --> mantissa * 10 ** exponent // e.g. 724.1 --> 7.241 * 10 ** 2 double mantissa = gridStepHint; int exponent = 0; if ( mantissa < 1 ) { while ( mantissa < 1 ) { mantissa *= 10.0; exponent--; } } else { while ( mantissa >= 10 ) { mantissa /= 10.0; exponent++; } } // calculate the grid step with hint. BigDecimal gridStep; if ( mantissa > 7.5 ) { // gridStep = 10.0 * 10 ** exponent gridStep = BigDecimal.TEN.multiply ( pow ( 10, exponent ) ); } else if ( mantissa > 3.5 ) { // gridStep = 5.0 * 10 ** exponent gridStep = new BigDecimal ( new Double ( 5 ).toString () ).multiply ( pow ( 10, exponent ) ); } else if ( mantissa > 1.5 ) { // gridStep = 2.0 * 10 ** exponent gridStep = new BigDecimal ( new Double ( 2 ).toString () ).multiply ( pow ( 10, exponent ) ); } else { // gridStep = 1.0 * 10 ** exponent gridStep = pow ( 10, exponent ); } return gridStep; } /** * Gets the tick label positions. * * @return the tick label positions */ public ArrayList<Integer> getTickLabelPositions () { return tickLabelPositions; } /** * Sets the font. * * @param font * the font */ protected void setFont ( Font font ) { if ( font == null ) { this.font = DEFAULT_FONT; } else { this.font = font; } } /** * Gets the font. * * @return the font */ protected Font getFont () { if ( font.isDisposed () ) { font = DEFAULT_FONT; } return font; } /** * Gets the layout data. * * @return the layout data */ public ChartLayoutData getLayoutData () { return new ChartLayoutData ( widthHint, heightHint ); } /** * Sets the bounds on chart panel. * * @param x * the x coordinate * @param y * the y coordinate * @param width * the width * @param height * the height */ public void setBounds ( int x, int y, int width, int height ) { bounds = new Rectangle ( x, y, width, height ); } /** * Gets the bounds on chart panel. * * @return the bounds on chart panel */ protected Rectangle getBounds () { return bounds; } /** * Disposes the resources. */ protected void dispose () { if ( !chart.isDisposed () ) { chart.removePaintListener ( this ); } } /** * Updates title layout. */ protected void updateLayoutData () { widthHint = SWT.DEFAULT; heightHint = SWT.DEFAULT; if ( !axis.getTick ().isVisible () ) { widthHint = 0; heightHint = 0; } else { if ( axis.isHorizontalAxis () ) { heightHint = Axis.MARGIN + Util.getExtentInGC ( getFont (), "dummy" ).y; } else { widthHint = tickLabelMaxLength + Axis.MARGIN; } } } /* * @see PaintListener#paintControl(PaintEvent) */ public void paintControl ( PaintEvent e ) { if ( !axis.getTick ().isVisible () ) { return; } e.gc.setBackground ( chart.getBackground () ); e.gc.setForeground ( getForeground () ); if ( axis.isHorizontalAxis () ) { drawXTick ( e.gc ); } else { drawYTick ( e.gc ); } } /** * Draw the X tick. * * @param gc * the graphics context */ private void drawXTick ( GC gc ) { int offset = axis.getTick ().getAxisTickMarks ().getBounds ().x; // draw tick labels gc.setFont ( axis.getTick ().getFont () ); for ( int i = 0; i < tickLabelPositions.size (); i++ ) { if ( axis.isValidCategoryAxis () || tickVisibilities.get ( i ) == true ) { String text = tickLabels.get ( i ); int fontWidth = gc.textExtent ( text ).x; int x = (int) ( tickLabelPositions.get ( i ) - fontWidth / 2.0 + offset ); gc.drawText ( text, bounds.x + x, bounds.y ); } } } /** * Draw the Y tick. * * @param gc * the graphics context */ private void drawYTick ( GC gc ) { int margin = Axis.MARGIN + AxisTickMarks.TICK_LENGTH; // draw tick labels gc.setFont ( axis.getTick ().getFont () ); int figureHeight = gc.textExtent ( "dummy" ).y; for ( int i = 0; i < tickLabelPositions.size (); i++ ) { if ( tickVisibilities.size () == 0 || tickLabels.size () == 0 ) { break; } if ( tickVisibilities.get ( i ) == true ) { String text = tickLabels.get ( i ); int x = 0; if ( tickLabels.get ( 0 ).startsWith ( "-" ) && !text.startsWith ( "-" ) ) { x += gc.textExtent ( "-" ).x; } int y = (int) ( bounds.height - 1 - tickLabelPositions.get ( i ) - figureHeight / 2.0 - margin ); gc.drawText ( text, bounds.x + x, bounds.y + y ); } } } /** * Sets the format for axis tick label. <tt>DecimalFormat</tt> and * <tt>DateFormat</tt> should be used for <tt>double[]</tt> series and * <tt>Data[]</tt> series respectively. * <p> * If <tt>null</tt> is set, default format will be used. * * @param format * the format */ protected void setFormat ( Format format ) { this.format = format; } /** * Gets the format for axis tick label. * * @return the format */ protected Format getFormat () { return format; } }