Legend Alignment

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
ThePerchik
Posts: 2
Joined: Mon Jun 06, 2005 2:15 pm
Contact:

Legend Alignment

Post by ThePerchik » Fri Jul 15, 2005 2:30 pm

Hello and thanks in advnace for your time.

I have a legend. I set the legend to be at the top of the chart by...

Code: Select all

LegendTitle legend = chart.getLegend();
legend.setPosition(RectangleEdge.TOP);
I then set the legend to be centered by....

Code: Select all

LegendTitle legend = chart.getLegend();
legend.setHorizontalAlignment(HorizontalAlignment.CENTER);
The legend does get to the top of the chart but it does not center. And the legend streches to the entire width of the chart.

Image

Any ideas anyone?

Robert.Bune
Posts: 4
Joined: Mon Jul 18, 2005 2:01 pm
Location: UK

Post by Robert.Bune » Tue Jul 19, 2005 9:59 am

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;

	}

}
You can use this class as follows:

Code: Select all

// Create a 2 column legend
MultiColumnLegend legend = new MultiColumnLegend(2);
legend.setAnchor(DefaultOldLegend.NORTH);
chart.setOldLegend(legend);
David Gilbert, any chance of adding multi-column support to the standard legend?

p.s. I am not sure how to achieve the same effect using the new legend implementation.

John Matthews
Posts: 513
Joined: Wed Sep 12, 2007 3:18 pm

Re: Legend Alignment

Post by John Matthews » Sat Nov 10, 2012 4:21 pm

An alternative is to render LegendItem instances in a separate JPanel having the desired layout. This minimal example uses a JLabel with Icon in a GridLayout.

Image

Locked