CombinedDomainPlot with Category and XY subplots

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
astenwick
Posts: 13
Joined: Tue Apr 13, 2004 8:03 pm
Location: Seattle

CombinedDomainPlot with Category and XY subplots

Post by astenwick » Fri May 14, 2004 1:24 am

I've created a class that can accept both XYPlots and CategoryPlots as subplots as long as they both share a common ValueAxis (domain for the XYPlot and range for the CategoryPlot). I chose to extend the XYPlot class to use as my starting point, but you probably could've used the CategoryPlot as the starting point as well, or make a completely new type. I think that this may be a very useful addition to JFreeChart.

Code: Select all

public class CombinedDomainPlot extends XYPlot 
		implements Cloneable, PublicCloneable, Serializable, PlotChangeListener 
{

	/** Storage for the subplot references. */
	private List subplots;
	
	/** Subtitles for all the subplots. */
	private List subtitles;
	
	/** Weights for all the subplots */
	private List weights;

	/** Total weight of all charts. */
	private double totalWeight = 0.0;

	/** The gap between subplots. */
	private double gap = 0.0;

	/** Temporary storage for the subplot areas. */
	private transient Rectangle2D[] subplotAreas;

	/**
	 * Creates a new combined plot that shares a domain axis among multiple subplots.
	 *
	 * @param domainAxis  the shared axis.
	 */
	public CombinedDomainPlot(ValueAxis domainAxis) {

		super(	null,        // no data in the parent plot
				domainAxis,
				null,        // no range axis
				null         // no rendereer
		);  

		this.subplots = new java.util.ArrayList();
		this.subtitles = new ArrayList();
		this.weights = new ArrayList();
	}

	/**
	 * Returns a string describing the type of plot.
	 *
	 * @return The type of plot.
	 */
	public String getPlotType() {
		return "CombinedDomainPlot";
	}

	/**
	 * Sets the orientation for the plot (also changes the orientation for all the subplots
	 * to match).
	 * 
	 * @param orientation  the orientation.
	 */
	public void setOrientation(PlotOrientation orientation) {

		super.setOrientation(orientation);

		// only reset those charts that didn't set their own
		Iterator iterator = subplots.iterator();
		while (iterator.hasNext()) {
			Plot plot = (Plot)iterator.next();
			if(plot instanceof XYPlot)
			{	
				XYPlot xy = (XYPlot)plot;
				if(xy.getOrientation() == null)
					xy.setOrientation(orientation);
			}else {
				CategoryPlot cat = (CategoryPlot)plot;
				if(cat.getOrientation() == null)
					cat.setOrientation(orientation);
			}
			
		}

	}

	/**
	 * Returns the range for the axis.  This is the combined range of all the subplots.
	 *
	 * @param axis  the axis.
	 *
	 * @return The range.
	 */
	public Range getDataRange(ValueAxis axis) {

		Range result = null;

		if (subplots != null) {
			Iterator iterator = subplots.iterator();
			while (iterator.hasNext()) {
				Plot subplot = (Plot) iterator.next();
				if(subplot instanceof XYPlot)
					result = Range.combine(result, ((XYPlot)subplot).getDataRange(axis));
				else
					result = Range.combine(result, ((CategoryPlot)subplot).getDataRange(axis));
			}
		}

		return result;

	}

	/**
	 * Returns the space between subplots.
	 *
	 * @return the gap
	 */
	public double getGap() {
		return gap;
	}

	/**
	 * Sets the amount of space between subplots.
	 *
	 * @param gap  the gap between subplots
	 */
	public void setGap(double gap) {
		this.gap = gap;
	}

	/**
	 * Adds a subplot, with a default 'weight' of 1.
	 * <P>
	 * The subplot should have a null domain axis.
	 *
	 * @param subplot  the subplot.
	 */
	public void add(Plot subplot, Title title) {
		add(subplot, title, 1);
	}

	/**
	 * Adds a subplot with a particular weight (greater than or equal to one).  The weight
	 * determines how much space is allocated to the subplot relative to all the other subplots.
	 * <P>
	 * The domain axis for the subplot will be set to <code>null</code>.
	 *
	 * @param subplot  the subplot.
	 * @param weight  the weight (must be 1 or greater).
	 */
	public void add(Plot subplot, Title title, double weight) {

		// verify valid weight
		if (weight <= 0) {
			String msg = "SharedDomainXYPlot.add(...): weight must be positive.";
			throw new IllegalArgumentException(msg);
		}

		if(title != null)
			this.subtitles.add(title);
		
		// store the plot and its weight
		subplot.setParent(this);
		weights.add(new Double(weight));
//		if(subplot instanceof XYPlot)
//			((XYPlot)subplot).setWeight(weight);
//		else
//			((CategoryPlot)subplot).setWeight(weight);
		subplot.setInsets(new Insets(0, 0, 0, 0));
		if(subplot instanceof XYPlot)
			((XYPlot)subplot).setDomainAxis(null);
		else
			((CategoryPlot)subplot).setRangeAxis(null);
		subplot.addChangeListener(this);
		this.subplots.add(subplot);

		// keep track of total weights
		this.totalWeight += weight;

		ValueAxis axis = getDomainAxis();
		if (axis != null) {
			axis.configure();
		}
		
		notifyListeners(new PlotChangeEvent(this));

	}

	/**
	 * Removes a subplot from the combined chart.
	 *
	 * @param subplot  the subplot.
	 */
	public void remove(Plot subplot) {

		if(subplots.contains(subplot))
		{
			int index = subplots.indexOf(subplot);
			subplots.remove(index);
			subplot.setParent(null);
			subplot.removeChangeListener(this);
			this.totalWeight -= ((Double)weights.get(index)).doubleValue();
		}
		ValueAxis domain = getDomainAxis();
		if (domain != null) {
			domain.configure();
		}

		notifyListeners(new PlotChangeEvent(this));

	}

	/**
	 * Returns the list of subplots.
	 *
	 * @return An unmodifiable list of subplots.
	 */
	public List getSubplots() {
		return Collections.unmodifiableList(this.subplots);
	}



	/**
	 * Draws the plot on a Java 2D graphics device (such as the screen or a printer).
	 * Will perform all the placement calculations for each sub-plots and then tell these to draw
	 * themselves.
	 *
	 * @param g2  the graphics device.
	 * @param area  the area within which the plot (including axis labels) should be drawn.
	 * @param parentState  the parent state.
	 * @param info  collects information about the drawing (null permitted).
	 */
	public void draw(Graphics2D g2, 
			Rectangle2D area, 
			PlotState parentState,
			PlotRenderingInfo info) 
	{
		draw(g2, area, null, parentState, info);                          
	}
	
	/**
	 * Draws the plot within the specified area on a graphics device.
	 * 
	 * @param g2  the graphics device.
	 * @param area  the plot area (in Java2D space).
	 * @param anchor  an anchor point in Java2D space (<code>null</code> permitted).
	 * @param parentState  the state from the parent plot, if there is one (<code>null</code> 
	 *                     permitted).
	 * @param info  collects chart drawing information (<code>null</code> permitted).
	 */
	public void draw(Graphics2D g2,
			Rectangle2D area,
			Point2D anchor,
			PlotState parentState,
			PlotRenderingInfo info) {
		
		// set up info collection...
		if(info!=null)
			info.setPlotArea(area);
		
		// adjust the drawing area for plot insets (if any)...
		Insets insets = getInsets();
		if (insets != null) {
			area.setRect(area.getX() + insets.left,
					area.getY() + insets.top,
					area.getWidth() - insets.left - insets.right,
					area.getHeight() - insets.top - insets.bottom);
		}

		AxisSpace space = calculateAxisSpace(g2, area);
		Rectangle2D dataArea = space.shrink(area, null);
		//this.axisOffset.trim(dataArea);

		// set the width and height of non-shared axis of all sub-plots
		setFixedRangeAxisSpaceForSubplots(space);

		// draw the shared axis
		ValueAxis axis = getDomainAxis();
		RectangleEdge edge = getDomainAxisEdge();
		double cursor = RectangleEdge.coordinate(dataArea, edge);
		AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
		if (parentState == null) {
			parentState = new PlotState();
		}
		parentState.getSharedAxisStates().put(axis, axisState);
		
		// draw all the subplots
		for (int i = 0; i < this.subplots.size(); i++) {
			Plot plot = (Plot) subplots.get(i);
			PlotRenderingInfo subplotInfo = null;
			if (info != null) {
				subplotInfo = new PlotRenderingInfo(info.getOwner());
				info.addSubplotInfo(subplotInfo);
			}
			TextTitle currentTitle = getSubtitle(i);
			drawTitle(currentTitle, g2, this.subplotAreas[i]);
			plot.draw(g2, this.subplotAreas[i], anchor, parentState, subplotInfo);
		}

		if (info != null) {
			info.setDataArea(dataArea);
		}
		
	}
	
	/**
	 * Gets the subtitle for the specified plot
	 * 
	 * @param index	subplot index
	 * @return		the subtitle
	 */
	public TextTitle getSubtitle(int index)
	{
		return (TextTitle)subtitles.get(index);
	}
		
	/**
	 * Draws a title.  The title should be drawn at the top, bottom, left or right of the 
	 * specified area, and the area should be updated to reflect the amount of space used by 
	 * the title.
	 *
	 * @param title  the title.
	 * @param g2  the graphics device.
	 * @param area  the area that should contain the title.
	 */
	public void drawTitle(TextTitle title, Graphics2D g2, Rectangle2D area) {

		Rectangle2D titleArea = new Rectangle2D.Double();
		double availableHeight = 0.0;
		double availableWidth = 0.0;
		
		availableWidth = area.getWidth();
		availableHeight = Math.min(title.getPreferredHeight(g2, (float) availableWidth), area.getHeight());
		titleArea.setRect(area.getX(), area.getY(), availableWidth, availableHeight);
		title.setBackgroundPaint(Color.white);
		title.draw(g2, titleArea);
		area.setRect(area.getX(), Math.min(area.getY() + availableHeight, area.getMaxY()),
					availableWidth, Math.max(area.getHeight() - availableHeight, 0));
		
	}

	/**
	 * Returns a collection of legend items for the plot.
	 *
	 * @return the legend items.
	 */
	public LegendItemCollection getLegendItems() {

		LegendItemCollection result = new LegendItemCollection();

		boolean ccqSubplotExists = false;
		boolean atLeastOne = false;
		if (subplots != null) {
			Iterator iterator = subplots.iterator();
			while (iterator.hasNext()) {
				Plot plot = (Plot) iterator.next();
				if(plot instanceof CCQSubplot)
				{	
					if(!ccqSubplotExists) {
						LegendItemCollection more = plot.getLegendItems();
						if(more!=null) {
							if(atLeastOne)
							{
								// add line break here
								result.add(new LegendItem("", "", new Line2D.Float(), 
										Color.white, Color.white, new BasicStroke()));
							}
							result.addAll(more);
							atLeastOne = true;
						}
					}
					ccqSubplotExists = true;
				}else
				{
					LegendItemCollection more = plot.getLegendItems();
					if(more!=null) {
						if(atLeastOne)
						{
							// add line break here
							result.add(new LegendItem("", "", new Line2D.Float(), 
									Color.white, Color.white, new BasicStroke()));
						}							
						result.addAll(more);
						atLeastOne = true;
					}
				}

			}
		}

		return result;

	}

	/**
	 * A zoom method that (currently) does nothing.
	 *
	 * @param percent  the zoom percentage.
	 */
	public void zoom(double percent) {
		// need to decide how to handle zooming...
	}

	/**
	 * Sets the size (width or height, depending on the orientation of the plot) for the domain
	 * axis of each subplot.
	 *
	 * @param space  the space.
	 */
	protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {

		Iterator iterator = subplots.iterator();
		while (iterator.hasNext()) {
			Plot plot = (Plot) iterator.next();
			if(plot instanceof XYPlot)
				((XYPlot)plot).setFixedRangeAxisSpace(space);
			else
				((CategoryPlot)plot).setFixedDomainAxisSpace(space);
			
		}

	}

	/**
	 * Handles a 'click' on the plot by updating the anchor values...
	 *
	 * @param x  x-coordinate, where the click occured.
	 * @param y  y-coordinate, where the click occured.
	 * @param info  object containing information about the plot dimensions.
	 */
	public void handleClick(int x, int y, PlotRenderingInfo info) {

		Rectangle2D dataArea = info.getDataArea();
		if (dataArea.contains(x, y)) {
			for (int i = 0; i < this.subplots.size(); i++) {
				Plot subplot = (Plot) this.subplots.get(i);
				PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
				subplot.handleClick(x, y, subplotInfo);
			}
		}

	}
	
	/**
	 * Receives a {@link PlotChangeEvent} and responds by notifying all listeners.
	 * 
	 * @param event  the event.
	 */
	public void plotChanged(PlotChangeEvent event) {
		notifyListeners(event);
	}

	/**
	 * Tests this plot for equality with another object.
	 *
	 * @param obj  the other object.
	 *
	 * @return <code>true</code> or <code>false</code>.
	 */
	public boolean equals(Object obj) {

		if (obj == null) {
			return false;
		}

		if (obj == this) {
			return true;
		}

		if (obj instanceof CombinedDomainPlot) {
			CombinedDomainPlot p = (CombinedDomainPlot) obj;
			if (super.equals(obj)) {
				boolean b0 = ObjectUtils.equal(this.subplots, p.subplots);
				boolean b1 = (this.totalWeight == p.totalWeight);
				boolean b2 = (this.gap == p.gap);
				return b0 && b1 && b2;
			}
		}

		return false;
	}
	
	/**
	 * Returns a clone of the annotation.
	 * 
	 * @return A clone.
	 * 
	 * @throws CloneNotSupportedException  this class will not throw this exception, but subclasses
	 *         (if any) might.
	 */
	public Object clone() throws CloneNotSupportedException {
		
		CombinedDomainPlot result = (CombinedDomainPlot) super.clone(); 
		result.subplots = ObjectUtils.clone(subplots);
		for (Iterator it = result.subplots.iterator(); it.hasNext();) {
			Plot child = (Plot) it.next();
			child.setParent(result);
		}
		
		// after setting up all the subplots, the shared domain axis may need reconfiguring
		ValueAxis domainAxis = result.getDomainAxis();
		if (domainAxis != null) {
			domainAxis.configure();
		}
		
		return result;
		
	}

	/**
	 * Calculates the axis space required.
	 * 
	 * @param g2  the graphics device.
	 * @param plotArea  the plot area.
	 * 
	 * @return The space.
	 */
	public AxisSpace calculateAxisSpace(Graphics2D g2, Rectangle2D plotArea) {
		
		AxisSpace space = new AxisSpace();
		PlotOrientation orientation = getOrientation();
		
		// work out the space required by the domain axis...
		AxisSpace fixed = getFixedDomainAxisSpace();
		if (fixed != null) {
			if (orientation == PlotOrientation.HORIZONTAL) {
				space.setLeft(fixed.getLeft());
				space.setRight(fixed.getRight());
			}
			else if (orientation == PlotOrientation.VERTICAL) {
				space.setTop(fixed.getTop());
				space.setBottom(fixed.getBottom());                
			}
		}
		else {
			ValueAxis xAxis = getDomainAxis();
			RectangleEdge xEdge = Plot.resolveDomainAxisLocation(getDomainAxisLocation(), 
					orientation);
			if (xAxis != null) {
				space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space);
			}
		}
		
		Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
		
		// work out the maximum height or width of the non-shared axes...
		int n = subplots.size();
		this.subplotAreas = new Rectangle2D[n];
		double x = adjustedPlotArea.getX();
		double y = adjustedPlotArea.getY();
		double usableSize = 0.0;
		if (orientation == PlotOrientation.HORIZONTAL) {
			usableSize = adjustedPlotArea.getWidth() - gap * (n - 1);
		}
		else if (orientation == PlotOrientation.VERTICAL) {
			usableSize = adjustedPlotArea.getHeight() - gap * (n - 1);
		}

		for (int i = 0; i < n; i++) {
			Plot plot = (Plot)subplots.get(i);

			// calculate sub-plot area
			if (orientation == PlotOrientation.HORIZONTAL) {
				double w = usableSize * ((Double)weights.get(i)).doubleValue() / totalWeight;
				this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 
						adjustedPlotArea.getHeight());
				x = x + w + gap;
			}
			else if (orientation == PlotOrientation.VERTICAL) {
				double h = usableSize * ((Double)weights.get(i)).doubleValue() / totalWeight;
				this.subplotAreas[i] = new Rectangle2D.Double(x, y, adjustedPlotArea.getWidth(), h);
				y = y + h + gap;
			}
			
			AxisSpace subSpace;
			if(plot instanceof XYPlot)
				subSpace = calculateRangeAxisSpace((XYPlot)plot, g2, this.subplotAreas[i], null);
			else
				subSpace = calculateDomainAxisSpace((CategoryPlot)plot, g2, this.subplotAreas[i], null);
			
			space.ensureAtLeast(subSpace);

		}

		return space;
	}
	
	/**
	 * Calculates the space required for the range axis/axes.
	 *
	 * @param g2  the graphics device.
	 * @param plotArea  the plot area.
	 * @param space  a carrier for the result (<code>null</code> permitted).
	 *
	 * @return  The required space.
	 */
	protected AxisSpace calculateRangeAxisSpace(XYPlot plot, Graphics2D g2, Rectangle2D plotArea,
			AxisSpace space) {

		if (space == null) {
			space = new AxisSpace();
		}

		// reserve some space for the range axis...
		if (plot.getFixedRangeAxisSpace() != null) {
			if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
				space.ensureAtLeast(plot.getFixedRangeAxisSpace().getTop(), RectangleEdge.TOP);
				space.ensureAtLeast(plot.getFixedRangeAxisSpace().getBottom(), RectangleEdge.BOTTOM);
			}
			else if (plot.getOrientation() == PlotOrientation.VERTICAL) {
				space.ensureAtLeast(plot.getFixedRangeAxisSpace().getLeft(), RectangleEdge.LEFT);
				space.ensureAtLeast(plot.getFixedRangeAxisSpace().getRight(), RectangleEdge.RIGHT);
			}
		}
		else {
			Axis rangeAxis1 = plot.getRangeAxis();
			if (rangeAxis1 != null) {
				space = rangeAxis1.reserveSpace(g2, plot, plotArea, plot.getRangeAxisEdge(), space);
			}

			// reserve space for the secondary range axes (if any)...
			for (int i = 0; i < plot.getSecondaryRangeAxisCount(); i++) {
				Axis secondaryRangeAxis = plot.getSecondaryRangeAxis(i);
				if (secondaryRangeAxis != null) {
					RectangleEdge edge = plot.getSecondaryRangeAxisEdge(i);
					space = secondaryRangeAxis.reserveSpace(g2, plot, plotArea, edge, space);
				}
			}
		}
		return space;

	}
	
	/**
	 * Calculates the space required for the domain axis/axes.
	 * 
	 * @param g2  the graphics device.
	 * @param plotArea  the plot area.
	 * @param space  a carrier for the result (<code>null</code> permitted).
	 * 
	 * @return  The required space.
	 */
	protected AxisSpace calculateDomainAxisSpace(CategoryPlot plot, Graphics2D g2, Rectangle2D plotArea, 
			AxisSpace space) {
		
		if (space == null) {
			space = new AxisSpace();
		}
		
		// reserve some space for the domain axis...
		if (plot.getFixedDomainAxisSpace()!= null) {
			if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
				space.ensureAtLeast(plot.getFixedDomainAxisSpace().getLeft(), RectangleEdge.LEFT);
				space.ensureAtLeast(plot.getFixedDomainAxisSpace().getRight(), RectangleEdge.RIGHT);
			}
			else if (plot.getOrientation() == PlotOrientation.VERTICAL) {
				space.ensureAtLeast(plot.getFixedDomainAxisSpace().getTop(), RectangleEdge.TOP);
				space.ensureAtLeast(plot.getFixedDomainAxisSpace().getBottom(), RectangleEdge.BOTTOM);
			}
		}
		else {
			// reserve space for the primary domain axis...
			RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(
					plot.getDomainAxisLocation(), plot.getOrientation()
			);
			if (plot.getDomainAxis() != null) {
				space = plot.getDomainAxis().reserveSpace(g2, this, plotArea, domainEdge, space);
			}
			else {
//				if (plot.getDrawSharedDomainAxis()) {
					space = getDomainAxis().reserveSpace(g2, this, plotArea, domainEdge, space);
//				}
			}
			
		}

		return space;
		
	}
	
	/** 
	 * Gets the total weight of all the subplots.  (Used in rendering the legend)
	 * 
	 * @return the weight
	 */
	public double getTotalWeight()
	{
		return this.totalWeight;
	}
	
	/**
	 * Gets the weight of the subplot.
	 * 
	 * @param index	subplot index
	 * @return		the subplot weight
	 */
	public double getWeight(int index)
	{
		return ((Double)weights.get(index)).doubleValue();
	}

}

dandante

Post by dandante » Wed Sep 22, 2004 8:51 pm

I had no luck with this code. First I had to add imports, which the code lacks for some reason.

Then there were some errors--
I had two of these:
The method getSecondaryRangeAxisCount() is undefined for the type XYPlot
and one of these:
The method getSecondaryRangeAxisEdge(int) is undefined for the type XYPlot

Those methods don't occur in this class or its superclass.

Then I got one of these:

CCQSubplot cannot be resolved or is not a type

There is no such class anywhere.

Could it be that this worked with an earlier version of the API but not 0.9.20 which I am using? THat would make sense since I am getting some deprecation warnings as well.

Locked