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