package com.positive.charting;
import java.awt.Paint;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import com.positive.charts.block.BlockParams;
import com.positive.charts.block.EntityBlockResult;
import com.positive.charts.block.LengthConstraintType;
import com.positive.charts.block.LineBorder;
import com.positive.charts.block.RectangleConstraint;
import com.positive.charts.block.RectangleInsets;
import com.positive.charts.common.RectangleEdge;
import com.positive.charts.data.Range;
import com.positive.charts.entity.EntityCollection;
import com.positive.charts.event.ChartChangeEvent;
import com.positive.charts.event.ChartChangeListener;
import com.positive.charts.event.PlotChangeEvent;
import com.positive.charts.event.PlotChangeListener;
import com.positive.charts.event.TitleChangeEvent;
import com.positive.charts.event.TitleChangeListener;
import com.positive.charts.plot.Plot;
import com.positive.charts.plot.PlotRenderingInfo;
import com.positive.charts.title.LegendTitle;
import com.positive.charts.title.TextTitle;
import com.positive.charts.title.Title;
import com.positive.charts.util.HorizontalAlignment;
import com.positive.charts.util.RectangleUtil;
import com.positive.charts.util.Size2D;
import com.positive.charts.util.Stroke;
import com.positive.charts.util.VerticalAlignment;
import com.positive.colorchecker.StaticColorChecker;
/**
* A chart class implemented using the SWT APIs.
*/
public class Chart implements PlotChangeListener, TitleChangeListener {
/** The chart title (optional). */
private TextTitle title;
/** Listeners to chart change events. */
private final ListenerList changeListeners = new ListenerList();
/** The padding between the chart border and the chart drawing area. */
private RectangleInsets padding;
private Color borderPaint;
/**
* The chart subtitles (zero, one or many). This field should never be
* <code>null</code>.
*/
private final List subtitles = new ArrayList();
/**
* A flag that can be used to enable/disable notification of chart change
* events.
*/
private boolean notify = true;
/** Draws the visual representation of the data. */
private final Plot plot;
private Color backgroundPaint;
private boolean borderVisible;
private Stroke borderStroke = new Stroke(1);
/**
* Creates a new instance of a chart drawer with a given plot.
*
* @param plot
*/
public Chart(final Plot plot, final boolean createLegend) {
this.plot = plot;
plot.addChangeListener(this);
// create a legend, if requested...
if (createLegend) {
final LegendTitle legend = new LegendTitle(this.plot);
// legend.setMargin(new RectangleInsets(0, 0, 0, 0));
// legend.setPadding(5, 1, 1,1);
final LineBorder frame = new LineBorder();
legend.setFrame(frame);
legend.setBackgroundPaint(StaticColorChecker.dublicateColor(SWT.COLOR_WHITE));// Display.getCurrent().getSystemColor(SWT.COLOR_WHITE));
legend.setPosition(RectangleEdge.BOTTOM);
this.subtitles.add(legend);
legend.addChangeListener(this);
}
}
/**
* Registers an object for notification of changes to the chart.
*
* @param listener
* the listener (<code>null</code> not permitted).
*/
public void addChangeListener(final ChartChangeListener listener) {
this.changeListeners.add(listener);
}
/**
* Adds a legend to the plot and sends a {@link ChartChangeEvent} to all
* registered listeners.
*
* @param legend
* the legend (<code>null</code> not permitted).
*
* @see #removeLegend()
*/
public void addLegend(final LegendTitle legend) {
this.addSubtitle(legend);
}
/**
* Adds a subtitle at a particular position in the subtitle list, and sends
* a {@link ChartChangeEvent} to all registered listeners.
*
* @param index
* the index (in the range 0 to {@link #getSubtitleCount()}).
* @param subtitle
* the subtitle to add (<code>null</code> not permitted).
*
* @since 1.0.6
*/
public void addSubtitle(final int index, final Title subtitle) {
if ((index < 0) || (index > this.getSubtitleCount())) {
throw new IllegalArgumentException(
"The 'index' argument is out of range.");
}
if (subtitle == null) {
throw new IllegalArgumentException("Null 'subtitle' argument.");
}
this.subtitles.add(index, subtitle);
subtitle.addChangeListener(this);
this.fireChartChanged();
}
/**
* Adds a chart subtitle, and notifies registered listeners that the chart
* has been modified.
*
* @param subtitle
* the subtitle (<code>null</code> not permitted).
*
* @see #getSubtitle(int)
*/
public void addSubtitle(final Title subtitle) {
if (subtitle == null) {
throw new IllegalArgumentException("Null 'subtitle' argument.");
}
this.subtitles.add(subtitle);
subtitle.addChangeListener(this);
this.fireChartChanged();
}
/**
* Clears all subtitles from the chart and sends a {@link ChartChangeEvent}
* to all registered listeners.
*
* @see #addSubtitle(Title)
*/
public void clearSubtitles() {
final Iterator iterator = this.subtitles.iterator();
while (iterator.hasNext()) {
final Title t = (Title) iterator.next();
t.removeChangeListener(this);
}
this.subtitles.clear();
this.fireChartChanged();
}
/**
* Creates a rectangle that is aligned to the frame.
*
* @param dimensions
* the dimensions for the rectangle.
* @param frame
* the frame to align to.
* @param hAlign
* the horizontal alignment.
* @param vAlign
* the vertical alignment.
*
* @return A rectangle.
*/
private Rectangle createAlignedRectangle2D(final Size2D dimensions,
final Rectangle frame, final HorizontalAlignment hAlign,
final VerticalAlignment vAlign) {
double x = Double.NaN;
double y = Double.NaN;
if (hAlign == HorizontalAlignment.LEFT) {
x = frame.x;
} else if (hAlign == HorizontalAlignment.CENTER) {
x = RectangleUtil.getCenterX(frame) - (dimensions.width / 2.0);
} else if (hAlign == HorizontalAlignment.RIGHT) {
x = RectangleUtil.getMaxX(frame) - dimensions.width;
}
if (vAlign == VerticalAlignment.TOP) {
y = frame.y;
} else if (vAlign == VerticalAlignment.CENTER) {
y = RectangleUtil.getCenterY(frame) - (dimensions.height / 2.0);
} else if (vAlign == VerticalAlignment.BOTTOM) {
y = RectangleUtil.getMaxY(frame) - dimensions.height;
}
return new Rectangle((int) x, (int) y, (int) dimensions.width,
(int) dimensions.height);
}
/**
* Disposes the chart and associated plot.
*/
public void dispose() {
this.plot.dispose();
}
/**
* Draws the chart on a SWT graphics device (such as the screen or a
* printer).
*
* @param gc
* the SWT graphic context.
* @param area
* the area within which the chart should be drawn.
*/
public void draw(final GC gc, final Rectangle area) {
this.draw(gc, area, null, null);
}
/**
* Draws the chart on a SWT graphics device (such as the screen or a
* printer).
*
* @param gc
* the SWT graphic context.
* @param area
* the area within which the chart should be drawn.
* @param info
* records info about the drawing (null means collect no info).
*/
public void draw(final GC gc, final Rectangle area,
final ChartRenderingInfo info) {
this.draw(gc, area, null, info);
}
/**
* Draw the chart using supplied SWT graphic context.
*
* @param gc
* the SWT graphic context.
* @param chartArea
* the area within which the chart should be drawn
* @param anchor
* the anchor point (in GC space) for the chart (
* <code>null</code> permitted).
* @param info
* records info about the drawing (<code>null</code> means
* collect no info).
*/
public void draw(final GC gc, final Rectangle chartArea,
final Point anchor, final ChartRenderingInfo info) {
// record the chart area, if info is requested...
if (info != null) {
info.clear();
info.setChartArea(chartArea);
}
// ensure no drawing occurs outside chart area...
// Shape savedClip = g2.getClip();
// g2.clip(chartArea);
//
// g2.addRenderingHints(this.renderingHints);
//
// // draw the chart background...
if (this.backgroundPaint != null) {
gc.setBackground(this.backgroundPaint);
gc.fillRectangle(chartArea);
}
//
// if (this.backgroundImage != null) {
// Composite originalComposite = g2.getComposite();
// g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
// this.backgroundImageAlpha));
// Rectangle2D dest = new Rectangle2D.Double(0.0, 0.0,
// this.backgroundImage.getWidth(null),
// this.backgroundImage.getHeight(null));
// Align.align(dest, chartArea, this.backgroundImageAlignment);
// g2.drawImage(this.backgroundImage, (int) dest.getX(),
// (int) dest.getY(), (int) dest.getWidth(),
// (int) dest.getHeight(), null);
// g2.setComposite(originalComposite);
// }
//
if (this.isBorderVisible()) {
final Color paint = this.getBorderPaint();
final Stroke stroke = this.getBorderStroke();
if ((paint != null) && (stroke != null)) {
final Rectangle borderArea = new Rectangle(chartArea.x,
chartArea.y, (chartArea.width - 1),
(int) (chartArea.height - 1.0));
gc.setForeground(paint);
stroke.set(gc);
gc.drawRectangle(borderArea);
// gc.draw(borderArea);
}
}
// draw the title and subtitles...
// Rectangle2D nonTitleArea = new Rectangle2D.Double();
final Rectangle nonTitleArea = new Rectangle(chartArea.x, chartArea.y,
chartArea.width, chartArea.height);
if (this.padding != null) {
this.padding.trim(nonTitleArea);
}
EntityCollection entities = null;
if (info != null) {
entities = info.getEntityCollection();
}
if (this.title != null) {
final EntityCollection e = this.drawTitle(this.title, gc,
nonTitleArea, (entities != null));
if (e != null) {
entities.addAll(e);
}
}
final Iterator iterator = this.subtitles.iterator();
while (iterator.hasNext()) {
final Title currentTitle = (Title) iterator.next();
final EntityCollection e = this.drawTitle(currentTitle, gc,
nonTitleArea, (entities != null));
if (e != null) {
entities.addAll(e);
}
}
final Rectangle plotArea = nonTitleArea;
this.drawPlot(gc, plotArea, anchor, info);
}
/**
* Draw the plot (axes and data visualization).
*/
private void drawPlot(final GC gc, final Rectangle plotArea,
final Point anchor, final ChartRenderingInfo info) {
PlotRenderingInfo plotInfo = null;
if (info != null) {
plotInfo = info.getPlotInfo();
}
this.plot.draw(gc, plotArea, anchor, null, plotInfo);
}
/**
* 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 t
* the title (<code>null</code> not permitted).
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param area
* the chart area, excluding any existing titles (
* <code>null</code> not permitted).
* @param entities
* a flag that controls whether or not an entity collection is
* returned for the title.
*
* @return An entity collection for the title (possibly <code>null</code>).
*/
protected EntityCollection drawTitle(final Title t, final GC g2,
final Rectangle area, final boolean entities) {
if (t == null) {
throw new IllegalArgumentException("Null 't' argument.");
}
if (area == null) {
throw new IllegalArgumentException("Null 'area' argument.");
}
Rectangle titleArea = null;
final RectangleEdge position = t.getPosition();
final double ww = area.width;
if (ww <= 0.0) {
return null;
}
final double hh = area.height;
if (hh <= 0.0) {
return null;
}
final RectangleConstraint constraint = new RectangleConstraint(ww,
new Range(0.0, ww), LengthConstraintType.RANGE, hh, new Range(
0.0, hh), LengthConstraintType.RANGE);
Object retValue = null;
final BlockParams p = new BlockParams();
p.setGenerateEntities(entities);
if (position == RectangleEdge.TOP) {
final Size2D size = t.arrange(g2, constraint);
titleArea = this.createAlignedRectangle2D(size, area, t
.getHorizontalAlignment(), VerticalAlignment.TOP);
retValue = t.draw(g2, titleArea, p);
RectangleUtil.setRect(area, area.x, (int) Math.min(area.y
+ size.height, RectangleUtil.getMaxY(area)), area.width,
(int) Math.max(area.height - size.height, 0));
} else if (position == RectangleEdge.BOTTOM) {
final Size2D size = t.arrange(g2, constraint);
titleArea = this.createAlignedRectangle2D(size, area, t
.getHorizontalAlignment(), VerticalAlignment.BOTTOM);
retValue = t.draw(g2, titleArea, p);
RectangleUtil.setRect(area, area.x, area.y, area.width,
(int) (area.height - size.height));
} else if (position == RectangleEdge.RIGHT) {
final Size2D size = t.arrange(g2, constraint);
titleArea = this.createAlignedRectangle2D(size, area,
HorizontalAlignment.RIGHT, t.getVerticalAlignment());
retValue = t.draw(g2, titleArea, p);
RectangleUtil.setRect(area, area.x, area.y,
area.width - size.width, area.height);
}
else if (position == RectangleEdge.LEFT) {
final Size2D size = t.arrange(g2, constraint);
titleArea = this.createAlignedRectangle2D(size, area,
HorizontalAlignment.LEFT, t.getVerticalAlignment());
retValue = t.draw(g2, titleArea, p);
RectangleUtil.setRect(area, area.x + size.width, area.y, area.width
- size.width, area.height);
} else {
throw new RuntimeException("Unrecognised title position.");
}
EntityCollection result = null;
if (retValue instanceof EntityBlockResult) {
final EntityBlockResult ebr = (EntityBlockResult) retValue;
result = ebr.getEntityCollection();
}
return result;
}
/**
* Sends a default {@link ChartChangeEvent} to all registered listeners.
* <P>
* This method is for convenience only.
*/
public void fireChartChanged() {
this.notifyListeners(new ChartChangeEvent(this));
}
public Color getBackgroundPaint() {
return this.backgroundPaint;
}
/**
* Returns the paint used to draw the chart border (if visible).
*
* @return The border paint.
*
* @see #setBorderPaint(Paint)
*/
public Color getBorderPaint() {
return this.borderPaint;
}
/**
* Returns the stroke used to draw the chart border (if visible).
*
* @return The border stroke.
*
* @see #setBorderStroke(Stroke)
*/
public Stroke getBorderStroke() {
return this.borderStroke;
}
/**
* Returns the legend for the chart, if there is one. Note that a chart can
* have more than one legend - this method returns the first.
*
* @return The legend (possibly <code>null</code>).
*
* @see #getLegend(int)
*/
public LegendTitle getLegend() {
return this.getLegend(0);
}
/**
* Returns the nth legend for a chart, or <code>null</code>.
*
* @param index
* the legend index (zero-based).
*
* @return The legend (possibly <code>null</code>).
*
* @see #addLegend(LegendTitle)
*/
public LegendTitle getLegend(final int index) {
int seen = 0;
final Iterator iterator = this.subtitles.iterator();
while (iterator.hasNext()) {
final Title subtitle = (Title) iterator.next();
if (subtitle instanceof LegendTitle) {
if (seen == index) {
return (LegendTitle) subtitle;
} else {
seen++;
}
}
}
return null;
}
/**
* Returns the padding between the chart border and the chart drawing area.
*
* @return The padding (never <code>null</code>).
*
* @see #setPadding(RectangleInsets)
*/
public RectangleInsets getPadding() {
return this.padding;
}
public Plot getPlot() {
return this.plot;
}
/**
* Returns a chart subtitle.
*
* @param index
* the index of the chart subtitle (zero based).
*
* @return A chart subtitle.
*
* @see #addSubtitle(Title)
*/
public Title getSubtitle(final int index) {
if ((index < 0) || (index >= this.getSubtitleCount())) {
throw new IllegalArgumentException("Index out of range.");
}
return (Title) this.subtitles.get(index);
}
/**
* Returns the number of titles for the chart.
*
* @return The number of titles for the chart.
*
* @see #getSubtitles()
*/
public int getSubtitleCount() {
return this.subtitles.size();
}
/**
* Returns the list of subtitles for the chart.
*
* @return The subtitle list (possibly empty, but never <code>null</code>).
*
* @see #setSubtitles(List)
*/
public List getSubtitles() {
return new ArrayList(this.subtitles);
}
/**
* Returns the main chart title. Very often a chart will have just one
* title, so we make this case simple by providing accessor methods for the
* main title. However, multiple titles are supported - see the
* {@link #addSubtitle(Title)} method.
*
* @return The chart title (possibly <code>null</code>).
*
* @see #setTitle(TextTitle)
*/
public TextTitle getTitle() {
return this.title;
}
/**
* Returns a flag that controls whether or not a border is drawn around the
* outside of the chart.
*
* @return A boolean.
*
* @see #setBorderVisible(boolean)
*/
public boolean isBorderVisible() {
return this.borderVisible;
}
/**
* Returns the current state of sending the {@link ChartChangeEvent}s.
*
* @return <code>true</code> iff the chart chage events are sent.
*/
public boolean isNotify() {
return this.notify;
}
/**
* Sends a {@link ChartChangeEvent} to all registered listeners.
*
* @param event
* information about the event that triggered the notification.
*/
protected void notifyListeners(final ChartChangeEvent event) {
if (this.notify) {
final Object[] listeners = this.changeListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final ChartChangeListener listener = (ChartChangeListener) listeners[i];
listener.chartChanged(event);
}
}
}
/**
* Receives notification that the plot has changed, and passes this on to
* registered listeners.
*
* @param event
* information about the plot change.
*/
public void plotChanged(final PlotChangeEvent event) {
event.setChart(this);
this.notifyListeners(event);
}
/**
* Deregisters an object for notification of changes to the chart.
*
* @param listener
* the listener (<code>null</code> not permitted)
*/
public void removeChangeListener(final ChartChangeListener listener) {
this.changeListeners.remove(listener);
}
/**
* Removes the first legend in the chart and sends a
* {@link ChartChangeEvent} to all registered listeners.
*
* @see #getLegend()
*/
public void removeLegend() {
this.removeSubtitle(this.getLegend());
}
/**
* Removes the specified subtitle and sends a {@link ChartChangeEvent} to
* all registered listeners.
*
* @param title
* the title.
*
* @see #addSubtitle(Title)
*/
public void removeSubtitle(final Title title) {
this.subtitles.remove(title);
this.fireChartChanged();
}
public void setBackgroundPaint(final Color backgroundPaint) {
this.backgroundPaint = backgroundPaint;
}
/**
* Sets the paint used to draw the chart border (if visible).
*
* @param paint
* the paint.
*
* @see #getBorderPaint()
*/
public void setBorderPaint(final Color paint) {
this.borderPaint = paint;
this.fireChartChanged();
}
/**
* Sets the stroke used to draw the chart border (if visible).
*
* @param stroke
* the stroke.
*
* @see #getBorderStroke()
*/
public void setBorderStroke(final Stroke stroke) {
this.borderStroke = stroke;
this.fireChartChanged();
}
/**
* Sets a flag that controls whether or not a border is drawn around the
* outside of the chart.
*
* @param visible
* the flag.
*
* @see #isBorderVisible()
*/
public void setBorderVisible(final boolean visible) {
this.borderVisible = visible;
this.fireChartChanged();
}
/**
* Enables/disables sending of {@link ChartChangeEvent}s. Can be used to
* force sending an event if flag is set to <code>true</code>.
*
* @param enable
* a state of notification sends.
*/
public void setNotify(final boolean enable) {
this.notify = enable;
if (enable) {
this.notifyListeners(new ChartChangeEvent(this));
}
}
/**
* Sets the padding between the chart border and the chart drawing area, and
* sends a {@link ChartChangeEvent} to all registered listeners.
*
* @param padding
* the padding (<code>null</code> not permitted).
*
* @see #getPadding()
*/
public void setPadding(final RectangleInsets padding) {
if (padding == null) {
throw new IllegalArgumentException("Null 'padding' argument.");
}
this.padding = padding;
this.notifyListeners(new ChartChangeEvent(this));
}
/**
* Sets the title list for the chart (completely replaces any existing
* titles) and sends a {@link ChartChangeEvent} to all registered listeners.
*
* @param subtitles
* the new list of subtitles (<code>null</code> not permitted).
*
* @see #getSubtitles()
*/
public void setSubtitles(final List subtitles) {
if (subtitles == null) {
throw new NullPointerException("Null 'subtitles' argument.");
}
this.setNotify(false);
this.clearSubtitles();
final Iterator iterator = subtitles.iterator();
while (iterator.hasNext()) {
final Title t = (Title) iterator.next();
if (t != null) {
this.addSubtitle(t);
}
}
this.setNotify(true); // this fires a ChartChangeEvent
}
/**
* Sets the chart title and sends a {@link ChartChangeEvent} to all
* registered listeners. This is a convenience method that ends up calling
* the {@link #setTitle(TextTitle)} method. If there is an existing title,
* its text is updated, otherwise a new title using the default font is
* added to the chart. If <code>text</code> is <code>null</code> the chart
* title is set to <code>null</code>.
*
* @param text
* the title text (<code>null</code> permitted).
*
* @see #getTitle()
*/
public void setTitle(final String text) {
if (text != null) {
if (this.title == null) {
this.setTitle(new TextTitle(text, TextTitle.DEFAULT_FONT));
} else {
this.title.setText(text);
}
} else {
this.setTitle((TextTitle) null);
}
}
/**
* Sets the main title for the chart and sends a {@link ChartChangeEvent} to
* all registered listeners. If you do not want a title for the chart, set
* it to <code>null</code>. If you want more than one title on a chart, use
* the {@link #addSubtitle(Title)} method.
*
* @param title
* the title (<code>null</code> permitted).
*
* @see #getTitle()
*/
public void setTitle(final TextTitle title) {
if (this.title != null) {
this.title.removeChangeListener(this);
}
this.title = title;
if (title != null) {
title.addChangeListener(this);
}
this.fireChartChanged();
}
public void titleChanged(final TitleChangeEvent event) {
event.setChart(this);
this.notifyListeners(event);
}
}