/******************************************************************************* * 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; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; 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.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.swtchart.Chart; import org.swtchart.Constants; import org.swtchart.IBarSeries; import org.swtchart.ILegend; import org.swtchart.ILineSeries; import org.swtchart.ISeries; import org.swtchart.internal.series.LineSeries; import org.swtchart.internal.series.Series; /** * A legend for chart. */ public class Legend extends Composite implements ILegend, PaintListener { /** the plot chart */ private Chart chart; /** the state indicating the legend visibility */ private boolean visible; /** the position of legend */ private int position; /** the margin */ private static final int MARGIN = 5; /** the width of area to draw symbol */ private static final int SYMBOL_WIDTH = 20; /** the line width */ private static final int LINE_WIDTH = 2; /** the default foreground */ private static final Color DEFAULT_FOREGROUND = Display.getDefault ().getSystemColor ( SWT.COLOR_BLACK ); /** the default background */ private static final Color DEFAULT_BACKGROUND = Display.getDefault ().getSystemColor ( SWT.COLOR_WHITE ); /** the default font */ private Font defaultFont; /** the default font size */ private static final int DEFAULT_FONT_SIZE = Constants.SMALL_FONT_SIZE; /** the default position */ private static final int DEFAULT_POSITION = SWT.RIGHT; /** the map between series id and cell bounds */ private Map<String, Rectangle> cellBounds; /** * Constructor. * * @param chart * the chart * @param style * the style */ public Legend ( Chart chart, int style ) { super ( chart, style ); this.chart = chart; visible = true; position = DEFAULT_POSITION; cellBounds = new HashMap<String, Rectangle> (); defaultFont = new Font ( Display.getDefault (), "Tahoma", DEFAULT_FONT_SIZE, SWT.NORMAL ); setFont ( defaultFont ); setForeground ( DEFAULT_FOREGROUND ); setBackground ( DEFAULT_BACKGROUND ); addPaintListener ( this ); } /* * @see Control#setVisible(boolean) */ @Override public void setVisible ( boolean visible ) { if ( this.visible == visible ) { return; } this.visible = visible; chart.updateLayout (); } /* * @see Control#isVisible() */ @Override public boolean isVisible () { return visible; } /* * @see Canvas#setFont(Font) */ @Override public void setFont ( Font font ) { if ( font == null ) { super.setFont ( defaultFont ); } else { super.setFont ( font ); } chart.updateLayout (); } /* * @see Control#setForeground(Color) */ @Override public void setForeground ( Color color ) { if ( color == null ) { super.setForeground ( DEFAULT_FOREGROUND ); } else { super.setForeground ( color ); } } /* * @see Control#setBackground(Color) */ @Override public void setBackground ( Color color ) { if ( color == null ) { super.setBackground ( DEFAULT_BACKGROUND ); } else { super.setBackground ( color ); } } /* * @see ILegend#getPosition() */ public int getPosition () { return position; } /* * @see ILegend#setPosition(int) */ public void setPosition ( int value ) { if ( value == SWT.LEFT || value == SWT.RIGHT || value == SWT.TOP || value == SWT.BOTTOM ) { position = value; } else { position = DEFAULT_POSITION; } chart.updateLayout (); } /* * @see ILegend#getBounds(String) */ public Rectangle getBounds ( String seriesId ) { return cellBounds.get ( seriesId ); } /* * @see Widget#dispose() */ @Override public void dispose () { super.dispose (); if ( !defaultFont.isDisposed () ) { defaultFont.dispose (); } } /** * Sorts the given series array. For instance, if there are two stack series * in horizontal orientation, the top of stack series should appear at top * of legend. * <p> * If there are multiple x axes, the given series array will be sorted with * x axis first. And then, the series in each x axis will be sorted with * {@link Legend#sort(List, boolean, boolean)}. * * @param seriesArray * the series array * @return the sorted series array */ private ISeries[] sort ( ISeries[] seriesArray ) { // create a map between axis id and series list Map<Integer, List<ISeries>> map = new HashMap<Integer, List<ISeries>> (); for ( ISeries series : seriesArray ) { int axisId = series.getXAxisId (); List<ISeries> list = map.get ( axisId ); if ( list == null ) { list = new ArrayList<ISeries> (); } list.add ( series ); map.put ( axisId, list ); } // sort an each series list List<ISeries> sortedArray = new ArrayList<ISeries> (); boolean isVertical = chart.getOrientation () == SWT.VERTICAL; for ( Entry<Integer, List<ISeries>> entry : map.entrySet () ) { boolean isCategoryEnabled = chart.getAxisSet ().getXAxis ( entry.getKey () ).isCategoryEnabled (); sortedArray.addAll ( sort ( entry.getValue (), isCategoryEnabled, isVertical ) ); } return sortedArray.toArray ( new ISeries[0] ); } /** * Sorts the given series list which belongs to a certain x axis. * <ul> * <li>The stacked series will be gathered, and the order of stack series * will be reversed.</li> <li>In the case of vertical orientation, the order * of whole series will be reversed.</li> * </ul> * * @param seriesList * the series list which belongs to a certain x axis * @param isCategoryEnabled * true if category is enabled * @param isVertical * true in the case of vertical orientation * @return the sorted series array */ private List<ISeries> sort ( List<ISeries> seriesList, boolean isCategoryEnabled, boolean isVertical ) { List<ISeries> sortedArray = new ArrayList<ISeries> (); // gather the stacked series reversing the order of stack series int insertIndex = -1; for ( int i = 0; i < seriesList.size (); i++ ) { if ( isCategoryEnabled && ( (Series)seriesList.get ( i ) ).isValidStackSeries () ) { if ( insertIndex == -1 ) { insertIndex = i; } else { sortedArray.add ( insertIndex, seriesList.get ( i ) ); continue; } } sortedArray.add ( seriesList.get ( i ) ); } // reverse the order of whole series in the case of vertical orientation if ( isVertical ) { Collections.reverse ( sortedArray ); } return sortedArray; } /** * Update the layout data. */ public void updateLayoutData () { if ( !visible ) { setLayoutData ( new ChartLayoutData ( 0, 0 ) ); return; } int width = 0; int height = 0; ISeries[] seriesArray = sort ( chart.getSeriesSet ().getSeries () ); Rectangle r = chart.getClientArea (); int titleHeight = ( (Composite)chart.getTitle () ).getSize ().y; int cellHeight = Util.getExtentInGC ( getFont (), "dummy" ).y; if ( position == SWT.RIGHT || position == SWT.LEFT ) { int columns = 1; int yPosition = MARGIN; int maxCellWidth = 0; for ( ISeries series : seriesArray ) { int textWidth = Util.getExtentInGC ( getFont (), series.getId () ).x; int cellWidth = textWidth + SYMBOL_WIDTH + MARGIN * 3; maxCellWidth = Math.max ( maxCellWidth, cellWidth ); if ( yPosition + cellHeight < r.height - titleHeight || yPosition == MARGIN ) { yPosition += cellHeight + MARGIN; } else { columns++; yPosition = cellHeight + MARGIN * 2; } cellBounds .put ( series.getId (), new Rectangle ( maxCellWidth * ( columns - 1 ), yPosition - cellHeight - MARGIN, cellWidth, cellHeight ) ); height = Math.max ( yPosition, height ); } width = maxCellWidth * columns; } else if ( position == SWT.TOP || position == SWT.BOTTOM ) { int rows = 1; int xPosition = 0; for ( ISeries series : seriesArray ) { int textWidth = Util.getExtentInGC ( getFont (), series.getId () ).x; int cellWidth = textWidth + SYMBOL_WIDTH + MARGIN * 3; if ( xPosition + cellWidth < r.width || xPosition == 0 ) { xPosition += cellWidth; } else { rows++; xPosition = cellWidth; } cellBounds .put ( series.getId (), new Rectangle ( xPosition - cellWidth, ( cellHeight + MARGIN ) * ( rows - 1 ) + MARGIN, cellWidth, cellHeight ) ); width = Math.max ( xPosition, width ); } height = ( cellHeight + MARGIN ) * rows + MARGIN; } setLayoutData ( new ChartLayoutData ( width, height ) ); } /** * Draws the symbol of series. * * @param gc * the graphics context * @param series * the series * @param r * the rectangle to draw the symbol of series */ protected void drawSymbol ( GC gc, Series series, Rectangle r ) { if ( !visible ) { return; } if ( series instanceof ILineSeries ) { // draw plot line gc.setForeground ( ( (ILineSeries)series ).getLineColor () ); gc.setLineWidth ( LINE_WIDTH ); int lineStyle = Util.getIndexDefinedInSWT ( ( (ILineSeries)series ).getLineStyle () ); int x = r.x; int y = r.y + r.height / 2; if ( lineStyle != SWT.NONE ) { gc.setLineStyle ( lineStyle ); gc.drawLine ( x, y, x + SYMBOL_WIDTH, y ); } // draw series symbol Color color = ( (ILineSeries)series ).getSymbolColor (); Color[] colors = ( (ILineSeries)series ).getSymbolColors (); if ( colors != null && colors.length > 0 ) { color = colors[0]; } ( (LineSeries)series ).drawSeriesSymbol ( gc, x + SYMBOL_WIDTH / 2, y, color ); } else if ( series instanceof IBarSeries ) { // draw riser gc.setBackground ( ( (IBarSeries)series ).getBarColor () ); int size = SYMBOL_WIDTH / 2; int x = r.x + size / 2; int y = (int) ( r.y - size / 2d + r.height / 2d ); gc.fillRectangle ( x, y, size, size ); } } /* * @see PaintListener#paintControl(PaintEvent) */ public void paintControl ( PaintEvent e ) { if ( !visible ) { return; } GC gc = e.gc; gc.setFont ( getFont () ); ISeries[] seriesArray = chart.getSeriesSet ().getSeries (); if ( seriesArray.length == 0 ) { return; } // draw frame gc.setLineStyle ( SWT.LINE_SOLID ); gc.setLineWidth ( 1 ); gc.setForeground ( Display.getDefault ().getSystemColor ( SWT.COLOR_GRAY ) ); gc.drawRectangle ( 0, 0, getSize ().x - 1, getSize ().y - 1 ); // draw content for ( int i = 0; i < seriesArray.length; i++ ) { // draw plot line, symbol etc String id = seriesArray[i].getId (); Rectangle r = cellBounds.get ( id ); drawSymbol ( gc, (Series)seriesArray[i], new Rectangle ( r.x + MARGIN, r.y + MARGIN, SYMBOL_WIDTH, r.height - MARGIN * 2 ) ); // draw plot id gc.setBackground ( getBackground () ); gc.setForeground ( getForeground () ); gc.drawText ( id, r.x + SYMBOL_WIDTH + MARGIN * 2, r.y, true ); } } }