I had a similar problem which I solved by creating a MultiColumnLegend class which extends the DefaultOldLegend. Much of the code was ripped out of the DefaultOldLegend.java source to gain access to the required parts of the code.
Code: Select all
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.List;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.DefaultOldLegend;
import org.jfree.chart.DrawableLegendItem;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.LegendItemEntity;
import org.jfree.text.TextUtilities;
import org.jfree.ui.TextAnchor;
import org.jfree.util.Log;
import org.jfree.util.LogContext;
public class MultiColumnLegend extends DefaultOldLegend {
private static final long serialVersionUID = 1L;
/** Access to logging facilities. */
private static final LogContext LOGGER = Log
.createContext(MultiColumnLegend.class);
/** Reported when illegal legend is unexpectedly found. */
private static final String UNEXPECTED_LEGEND_ANCHOR = "Unexpected legend anchor";
private int columns = 2;
public MultiColumnLegend()
{
this(1);
}
public MultiColumnLegend(int columns)
{
super();
setColumns(columns);
}
public int getColumns() {
return columns;
}
public void setColumns(int columns) {
this.columns = columns;
}
/**
* Draws the legend.
*
* @param g2
* the graphics device.
* @param available
* the area available for drawing the chart.
* @param horizontal
* a flag indicating whether the legend items are laid out
* horizontally.
* @param inverted
* ???
* @param info
* collects rendering info (optional).
*
* @return The remaining available drawing area.
*/
protected Rectangle2D draw(Graphics2D g2, Rectangle2D available,
boolean horizontal, boolean inverted, ChartRenderingInfo info) {
LegendItemCollection legendItems = getChart().getPlot()
.getLegendItems();
if (legendItems == null || legendItems.getItemCount() == 0) {
return available;
}
// else...
DrawableLegendItem legendTitle = null;
LegendItem titleItem = null;
if (getTitle() != null && !getTitle().equals("")) {
titleItem = new LegendItem(getTitle(), getTitle(), null, null, null, Color.black);
}
RectangularShape legendArea;
double availableWidth = available.getWidth() / 2;
// the translation point for the origin of the drawing system
Point2D translation;
// Create buffer for individual items within the legend
List items = new ArrayList();
// Compute individual rectangles in the legend, translation point as
// well
// as the bounding box for the legend.
double maxColumnWidth = 0;
double maxRowHeight = 0;
g2.setFont(getItemFont());
for (int i = 0; i < legendItems.getItemCount(); i++) {
DrawableLegendItem item = createDrawableLegendItem(g2, legendItems
.get(i), 0, 0);
maxColumnWidth = Math.max(maxColumnWidth, item.getWidth());
maxRowHeight = Math.max(maxRowHeight, item.getHeight());
}
int colIndex = 1;
double xoffset = 0;
double totalHeight = 0;
if (titleItem != null) {
g2.setFont(getTitleFont());
legendTitle = createDrawableLegendItem(
g2, titleItem, xoffset, totalHeight
);
totalHeight = legendTitle.getHeight();
}
for (int i = 0; i < legendItems.getItemCount(); i++) {
items.add(createDrawableLegendItem(g2, legendItems.get(i), xoffset,
totalHeight));
colIndex++;
if (colIndex > columns) {
totalHeight += maxRowHeight;
xoffset = 0;
colIndex = 1;
} else {
xoffset += maxColumnWidth;
}
}
if (colIndex != 1) {
totalHeight += maxRowHeight;
}
// Create the bounding box
legendArea = new RoundRectangle2D.Double(0, 0,
(maxColumnWidth * columns), totalHeight,
getBoundingBoxArcWidth(), getBoundingBoxArcHeight());
if (horizontal) {
translation = createTranslationPointForHorizontalDraw(available,
inverted, (maxColumnWidth * columns), totalHeight);
} else {
translation = createTranslationPointForVerticalDraw(available,
inverted, totalHeight, (maxColumnWidth * columns));
}
// Move the origin of the drawing to the appropriate location
g2.translate(translation.getX(), translation.getY());
LOGGER.debug("legendArea = " + legendArea.getWidth() + ", "
+ legendArea.getHeight());
drawLegendBox(g2, legendArea);
drawLegendTitle(g2, legendTitle);
drawSeriesElements(g2, items, translation, info);
// translate the origin back to what it was prior to drawing the legend
g2.translate(-translation.getX(), -translation.getY());
return calcRemainingDrawingArea(available, horizontal, inverted,
legendArea);
}
/**
* ???
*
* @param available
* the available area.
* @param inverted
* inverted?
* @param maxRowWidth
* the maximum row width.
* @param totalHeight
* the total height.
*
* @return The translation point.
*/
private Point2D createTranslationPointForHorizontalDraw(
Rectangle2D available, boolean inverted, double maxRowWidth,
double totalHeight) {
// The yloc point is the variable part of the translation point
// for horizontal legends xloc can be: left, center or right.
double yloc = (inverted) ? available.getMaxY() - totalHeight
- getMargin().calculateBottomOutset(available.getHeight())
: available.getY()
+ getMargin().calculateTopOutset(available.getHeight());
double xloc;
if (isAnchoredToLeft()) {
xloc = available.getX()
+ getMargin().calculateLeftOutset(available.getWidth());
} else if (isAnchoredToCenter()) {
xloc = available.getX() + available.getWidth() / 2 - maxRowWidth
/ 2;
} else if (isAnchoredToRight()) {
xloc = available.getX() + available.getWidth() - maxRowWidth
- getChart().getPlot().getInsets().getLeft();
} else {
throw new IllegalStateException(UNEXPECTED_LEGEND_ANCHOR);
}
// Create the translation point
return new Point2D.Double(xloc, yloc);
}
/**
* ???
*
* @param available the available area.
* @param inverted inverted?
* @param totalHeight the total height.
* @param maxWidth the maximum width.
*
* @return The translation point.
*/
private Point2D createTranslationPointForVerticalDraw(Rectangle2D available,
boolean inverted, double totalHeight, double maxWidth) {
// The xloc point is the variable part of the translation point
// for vertical legends yloc can be: top, middle or bottom.
double xloc = (inverted)
? available.getMaxX() - maxWidth
- getMargin().calculateRightOutset(available.getWidth())
: available.getX()
+ getMargin().calculateLeftOutset(available.getWidth());
double yloc;
if (isAnchoredToTop()) {
yloc = available.getY() + getChart().getPlot().getInsets().getTop();
}
else if (isAnchoredToMiddle()) {
yloc = available.getY() + (available.getHeight() / 2) - (totalHeight / 2);
}
else if (isAnchoredToBottom()) {
yloc = available.getY() + available.getHeight()
- getChart().getPlot().getInsets().getBottom() - totalHeight;
}
else {
throw new IllegalStateException(UNEXPECTED_LEGEND_ANCHOR);
}
// Create the translation point
return new Point2D.Double(xloc, yloc);
}
/**
* Draws the bounding box for the legend.
*
* @param g2
* the graphics device.
* @param legendArea
* the legend area.
*/
private void drawLegendBox(Graphics2D g2, RectangularShape legendArea) {
// Draw the legend's bounding box
g2.setPaint(getBackgroundPaint());
g2.fill(legendArea);
g2.setPaint(getOutlinePaint());
g2.setStroke(getOutlineStroke());
g2.draw(legendArea);
}
/**
* Draws the legend title.
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param legendTitle
* the title (<code>null</code> permitted, in which case the
* method does nothing).
*/
private void drawLegendTitle(Graphics2D g2, DrawableLegendItem legendTitle) {
if (legendTitle != null) {
// XXX dsm - make title bold?
g2.setPaint(legendTitle.getItem().getFillPaint());
g2.setPaint(getItemPaint());
g2.setFont(getTitleFont());
TextUtilities.drawAlignedString(legendTitle.getItem().getLabel(),
g2, (float) legendTitle.getLabelPosition().getX(),
(float) legendTitle.getLabelPosition().getY(),
TextAnchor.CENTER_LEFT);
LOGGER.debug("Title x = " + legendTitle.getLabelPosition().getX());
LOGGER.debug("Title y = " + legendTitle.getLabelPosition().getY());
}
}
/**
* Draws the series elements.
*
* @param g2
* the graphics device.
* @param items
* the items.
* @param translation
* the translation point.
* @param info
* optional carrier for rendering info.
*/
private void drawSeriesElements(Graphics2D g2, List items,
Point2D translation, ChartRenderingInfo info) {
EntityCollection entities = null;
if (info != null) {
entities = info.getEntityCollection();
}
// Draw individual series elements
for (int i = 0; i < items.size(); i++) {
DrawableLegendItem item = (DrawableLegendItem) items.get(i);
g2.setPaint(item.getItem().getFillPaint());
Shape keyBox = item.getMarker();
if (item.getItem().isLineVisible()) {
g2.setStroke(item.getItem().getLineStroke());
g2.draw(item.getLine());
if (item.getItem().isShapeVisible()) {
if (item.getItem().isShapeFilled()) {
g2.fill(keyBox);
} else {
g2.draw(keyBox);
}
}
} else {
if (item.getItem().isShapeFilled()) {
g2.fill(keyBox);
}
if (item.getItem().isShapeOutlineVisible()) {
g2.setPaint(item.getItem().getOutlinePaint());
g2.setStroke(item.getItem().getOutlineStroke());
g2.draw(keyBox);
}
}
g2.setPaint(getItemPaint());
g2.setFont(getItemFont());
TextUtilities.drawAlignedString(item.getItem().getLabel(), g2,
(float) item.getLabelPosition().getX(), (float) item
.getLabelPosition().getY(), TextAnchor.CENTER_LEFT);
LOGGER.debug("Item x = " + item.getLabelPosition().getX());
LOGGER.debug("Item y = " + item.getLabelPosition().getY());
if (entities != null) {
Rectangle2D area = new Rectangle2D.Double(translation.getX()
+ item.getX(), translation.getY() + item.getY(), item
.getWidth(), item.getHeight());
LegendItemEntity entity = new LegendItemEntity(area);
entity.setSeriesIndex(i);
entities.add(entity);
}
}
}
/**
* Calculates the remaining drawing area.
*
* @param available the available area.
* @param horizontal horizontal?
* @param inverted inverted?
* @param legendArea the legend area.
*
* @return The remaining drawing area.
*/
private Rectangle2D calcRemainingDrawingArea(Rectangle2D available,
boolean horizontal, boolean inverted, RectangularShape legendArea) {
Rectangle2D drawingArea;
if (horizontal) {
// The remaining drawing area bounding box will have the same
// x origin, width and height independent of the anchor's
// location. The variable is the y coordinate. If the anchor is
// SOUTH, the y coordinate is simply the original y coordinate
// of the available area. If it is NORTH, we adjust original y
// by the total height of the legend and the initial gap.
double yy = available.getY();
double yloc = (inverted) ? yy
: yy + legendArea.getHeight()
+ getMargin().calculateBottomOutset(available.getHeight());
// return the remaining available drawing area
drawingArea = new Rectangle2D.Double(
available.getX(), yloc, available.getWidth(),
available.getHeight() - legendArea.getHeight()
- getMargin().calculateTopOutset(available.getHeight())
- getMargin().calculateBottomOutset(available.getHeight())
);
}
else {
// The remaining drawing area bounding box will have the same
// y origin, width and height independent of the anchor's
// location. The variable is the x coordinate. If the anchor is
// EAST, the x coordinate is simply the original x coordinate
// of the available area. If it is WEST, we adjust original x
// by the total width of the legend and the initial gap.
double xloc = (inverted) ? available.getX()
: available.getX()
+ legendArea.getWidth()
+ getMargin().calculateLeftOutset(available.getWidth())
+ getMargin().calculateRightOutset(available.getWidth());
// return the remaining available drawing area
drawingArea = new Rectangle2D.Double(
xloc, available.getY(),
available.getWidth() - legendArea.getWidth()
- getMargin().calculateLeftOutset(available.getWidth())
- getMargin().calculateRightOutset(available.getWidth()),
available.getHeight()
);
}
return drawingArea;
}
/**
* Creates a drawable legend item.
* <P>
* The marker box for each entry will be positioned next to the name of the
* specified series within the legend area. The marker box will be square
* and 70% of the height of current font.
*
* @param graphics
* the graphics context (supplies font metrics etc.).
* @param legendItem
* the legend item.
* @param x
* the upper left x coordinate for the bounding box.
* @param y
* the upper left y coordinate for the bounding box.
*
* @return A legend item encapsulating all necessary info for drawing.
*/
private DrawableLegendItem createDrawableLegendItem(Graphics2D graphics,
LegendItem legendItem, double x, double y) {
LOGGER.debug("In createDrawableLegendItem(x = " + x + ", y = " + y);
int insideGap = 2;
FontMetrics fm = graphics.getFontMetrics();
LineMetrics lm = fm.getLineMetrics(legendItem.getLabel(), graphics);
float textAscent = lm.getAscent();
float lineHeight = textAscent + lm.getDescent() + lm.getLeading();
DrawableLegendItem item = new DrawableLegendItem(legendItem);
float xLabelLoc = (float) (x + insideGap + 1.15f * lineHeight);
float yLabelLoc = (float) (y + insideGap + 0.5f * lineHeight);
item.setLabelPosition(new Point2D.Float(xLabelLoc, yLabelLoc));
float width = (float) (item.getLabelPosition().getX() - x
+ fm.stringWidth(legendItem.getLabel()) + 0.5 * textAscent);
float height = (2 * insideGap + lineHeight);
item.setBounds(x, y, width, height);
float boxDim = lineHeight * 0.70f;
float xloc = (float) (x + insideGap + 0.15f * lineHeight);
float yloc = (float) (y + insideGap + 0.15f * lineHeight);
if (legendItem.isLineVisible()) {
Line2D line = new Line2D.Float(xloc, yloc + boxDim / 2, xloc
+ boxDim * 3, yloc + boxDim / 2);
item.setLine(line);
// lengthen the bounds to accomodate the longer item
item.setBounds(item.getX(), item.getY(), item.getWidth() + boxDim
* 2, item.getHeight());
item.setLabelPosition(new Point2D.Float(xLabelLoc + boxDim * 2,
yLabelLoc));
if (item.getItem().isShapeVisible()) {
Shape marker = legendItem.getShape();
AffineTransform t1 = AffineTransform.getScaleInstance(
getShapeScaleX(), getShapeScaleY());
Shape s1 = t1.createTransformedShape(marker);
AffineTransform transformer = AffineTransform
.getTranslateInstance(xloc + (boxDim * 1.5), yloc
+ boxDim / 2);
Shape s2 = transformer.createTransformedShape(s1);
item.setMarker(s2);
}
} else {
if (item.getItem().isShapeVisible()) {
Shape marker = legendItem.getShape();
AffineTransform t1 = AffineTransform.getScaleInstance(
getShapeScaleX(), getShapeScaleY());
Shape s1 = t1.createTransformedShape(marker);
AffineTransform transformer = AffineTransform
.getTranslateInstance(xloc + boxDim / 2, yloc + boxDim
/ 2);
Shape s2 = transformer.createTransformedShape(s1);
item.setMarker(s2);
} else {
item
.setMarker(new Rectangle2D.Float(xloc, yloc, boxDim,
boxDim));
}
}
return item;
}
}