package info.limpet.stackedcharts.ui.view;
import info.limpet.stackedcharts.model.AbstractAnnotation;
import info.limpet.stackedcharts.model.AngleAxis;
import info.limpet.stackedcharts.model.AxisDirection;
import info.limpet.stackedcharts.model.AxisType;
import info.limpet.stackedcharts.model.Chart;
import info.limpet.stackedcharts.model.ChartSet;
import info.limpet.stackedcharts.model.DataItem;
import info.limpet.stackedcharts.model.Dataset;
import info.limpet.stackedcharts.model.Datum;
import info.limpet.stackedcharts.model.DependentAxis;
import info.limpet.stackedcharts.model.IndependentAxis;
import info.limpet.stackedcharts.model.LineType;
import info.limpet.stackedcharts.model.MarkerStyle;
import info.limpet.stackedcharts.model.Orientation;
import info.limpet.stackedcharts.model.PlainStyling;
import info.limpet.stackedcharts.model.SelectiveAnnotation;
import info.limpet.stackedcharts.model.Styling;
import info.limpet.stackedcharts.ui.view.StackedChartsView.ControllableDate;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.LogTick;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.TickType;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.axis.ValueTick;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.IntervalMarker;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueAxisPlot;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.util.AttrStringUtils;
import org.jfree.data.Range;
import org.jfree.data.general.Series;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.text.TextUtilities;
import org.jfree.ui.Layer;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;
import org.jfree.ui.TextAnchor;
import org.jfree.util.ShapeUtilities;
public class ChartBuilder
{
/**
* helper class that can handle either temporal or non-temporal datasets
*
* @author ian
*
*/
private static interface ChartHelper
{
/**
* add this item to this series
*
* @param series
* @param item
*/
void addItem(Series series, DataItem item);
/**
* clear the contents of the series
*
*/
void clear(Series series);
/**
* create the correct type of axis
*
* @param name
* @return
*/
ValueAxis createAxis(String name);
/**
* create a new collection of datasets (seroes)
*
* @return
*/
XYDataset createCollection();
/**
* create a series with the specified name
*
* @param name
* @return
*/
Series createSeries(String name);
/**
* put the series into the collection
*
* @param collection
* @param series
*/
void storeSeries(XYDataset collection, Series series);
}
/**
* support generation of a stacked chart with a shared time axis
*
* @author ian
*
*/
private static class DateHelper implements ChartHelper
{
@Override
public void addItem(final Series series, final DataItem item)
{
final TimeSeries ns = (TimeSeries) series;
final long time = (long) item.getIndependentVal();
final Millisecond milli = new Millisecond(new Date(time));
ns.addOrUpdate(milli, item.getDependentVal());
}
@Override
public void clear(final Series series)
{
final TimeSeries ns = (TimeSeries) series;
ns.clear();
}
@Override
public ValueAxis createAxis(final String name)
{
return new DateAxis(name);
}
@Override
public XYDataset createCollection()
{
return new TimeSeriesCollection();
}
@Override
public Series createSeries(final String name)
{
return new TimeSeries(name);
}
@Override
public void storeSeries(final XYDataset collection, final Series series)
{
final TimeSeriesCollection cc = (TimeSeriesCollection) collection;
cc.addSeries((TimeSeries) series);
}
}
/**
* support generation of a stacked chart with a shared number axis
*
* @author ian
*
*/
private static class NumberHelper implements ChartHelper
{
@Override
public void addItem(final Series series, final DataItem item)
{
final XYSeries ns = (XYSeries) series;
ns.add(item.getIndependentVal(), item.getDependentVal());
}
@Override
public void clear(final Series series)
{
final XYSeries ns = (XYSeries) series;
ns.clear();
}
@Override
public ValueAxis createAxis(final String name)
{
return new NumberAxis(name);
}
@Override
public XYDataset createCollection()
{
return new XYSeriesCollection();
}
@Override
public Series createSeries(final String name)
{
return new XYSeries(name);
}
@Override
public void storeSeries(final XYDataset collection, final Series series)
{
final XYSeriesCollection cc = (XYSeriesCollection) collection;
cc.addSeries((XYSeries) series);
}
}
/**
*
* @param subplot
* target plot for annotation
* @param annotations
* annotation list to be added to plot eg: Marker,Zone
* @param isRangeAnnotation
* is annotation added to Range or Domain
*/
private static void addAnnotationToPlot(final XYPlot subplot,
final List<AbstractAnnotation> annotations,
final boolean isRangeAnnotation)
{
for (final AbstractAnnotation annotation : annotations)
{
final Color color = annotation.getColor();
if (annotation instanceof info.limpet.stackedcharts.model.Marker)
{
// build value Marker
final info.limpet.stackedcharts.model.Marker marker =
(info.limpet.stackedcharts.model.Marker) annotation;
final Marker mrk = new ValueMarker(marker.getValue());
mrk.setLabel(annotation.getName());
mrk.setPaint(color == null ? Color.GRAY : color);
// move Text Anchor
mrk.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
mrk.setLabelAnchor(RectangleAnchor.TOP);
mrk.setLabelOffset(new RectangleInsets(2, 2, 2, 2));
if (isRangeAnnotation)
{
subplot.addRangeMarker(mrk, Layer.FOREGROUND);
}
else
{
subplot.addDomainMarker(mrk, Layer.FOREGROUND);
}
}
else if (annotation instanceof info.limpet.stackedcharts.model.Zone)
{
// build Zone
final info.limpet.stackedcharts.model.Zone zone =
(info.limpet.stackedcharts.model.Zone) annotation;
final Marker mrk = new IntervalMarker(zone.getStart(), zone.getEnd());
mrk.setLabel(annotation.getName());
if (color != null)
{
mrk.setPaint(color);
}
// move Text & Label Anchor
mrk.setLabelTextAnchor(TextAnchor.CENTER);
mrk.setLabelAnchor(RectangleAnchor.CENTER);
mrk.setLabelOffset(new RectangleInsets(2, 2, 2, 2));
if (isRangeAnnotation)
{
subplot.addRangeMarker(mrk, Layer.FOREGROUND);
}
else
{
subplot.addDomainMarker(mrk, Layer.FOREGROUND);
}
}
else if (annotation instanceof info.limpet.stackedcharts.model.ScatterSet)
{
// build ScatterSet
final info.limpet.stackedcharts.model.ScatterSet marker =
(info.limpet.stackedcharts.model.ScatterSet) annotation;
final EList<Datum> datums = marker.getDatums();
boolean addLabel = true;
for (final Datum datum : datums)
{
final Marker mrk = new ValueMarker(datum.getVal());
// only add label for first Marker
if (addLabel)
{
mrk.setLabel(annotation.getName());
addLabel = false;
}
final Color thisColor = datum.getColor();
final Color colorToUse = thisColor == null ? color : thisColor;
// apply some transparency to the color
if (colorToUse != null)
{
final Color transColor =
new Color(colorToUse.getRed(), colorToUse.getGreen(),
colorToUse.getBlue(), 120);
mrk.setPaint(transColor);
}
// move Text Anchor
mrk.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
mrk.setLabelAnchor(RectangleAnchor.TOP);
mrk.setLabelOffset(new RectangleInsets(2, 2, 2, 2));
if (isRangeAnnotation)
{
subplot.addRangeMarker(mrk, Layer.FOREGROUND);
}
else
{
subplot.addDomainMarker(mrk, Layer.FOREGROUND);
}
}
}
}
}
/**
*
* @param helper
* Helper object to map between axis types
* @param datasets
* list of datasets to add to this axis
* @param collection
* XYDataset need to be fill with Series
* @param renderer
* axis renderer
* @param seriesIndex
* counter for current series being assigned
*
* build axis dataset via adding series to Series & config renderer for styles
*/
private static void addDatasetToAxis(final ChartHelper helper,
final EList<Dataset> datasets, final XYDataset collection,
final XYLineAndShapeRenderer renderer, int seriesIndex)
{
for (final Dataset dataset : datasets)
{
final Series series = helper.createSeries(dataset.getName());
final Styling styling = dataset.getStyling();
if (styling != null)
{
if (styling instanceof PlainStyling)
{
final PlainStyling ps = (PlainStyling) styling;
renderer.setSeriesPaint(seriesIndex, ps.getColor());
}
else
{
System.err.println("Linear colors not implemented");
}
// legend visibility
final boolean isInLegend = styling.isIncludeInLegend();
renderer.setSeriesVisibleInLegend(seriesIndex, isInLegend);
// line thickness
// line style
final LineType lineType = styling.getLineStyle();
if (lineType != null)
{
final float thickness = (float) styling.getLineThickness();
Stroke stroke;
float[] pattern;
switch (lineType)
{
case NONE:
renderer.setSeriesLinesVisible(seriesIndex, false);
break;
case SOLID:
renderer.setSeriesLinesVisible(seriesIndex, true);
stroke = new BasicStroke(thickness);
renderer.setSeriesStroke(seriesIndex, stroke);
break;
case DOTTED:
renderer.setSeriesLinesVisible(seriesIndex, true);
pattern = new float[]
{3f, 3f};
stroke =
new BasicStroke(thickness, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL, 0, pattern, 0);
renderer.setSeriesStroke(seriesIndex, stroke);
break;
case DASHED:
renderer.setSeriesLinesVisible(seriesIndex, true);
pattern = new float[]
{8.0f, 4.0f};
stroke =
new BasicStroke(thickness, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL, 0, pattern, 0);
renderer.setSeriesStroke(seriesIndex, stroke);
break;
}
}
// marker size
double size = styling.getMarkerSize();
if (size == 0)
{
size = 2;// default
}
// marker style
final MarkerStyle marker = styling.getMarkerStyle();
if (marker != null)
{
switch (marker)
{
case NONE:
renderer.setSeriesShapesVisible(seriesIndex, false);
break;
case SQUARE:
renderer.setSeriesShape(seriesIndex, new Rectangle2D.Double(0, 0,
size, size));
break;
case CIRCLE:
renderer.setSeriesShape(seriesIndex, new Ellipse2D.Double(0, 0,
size, size));
break;
case TRIANGLE:
renderer.setSeriesShape(seriesIndex, ShapeUtilities
.createUpTriangle((float) size));
break;
case CROSS:
renderer.setSeriesShape(seriesIndex, ShapeUtilities
.createRegularCross((float) size, (float) size));
break;
case DIAMOND:
renderer.setSeriesShape(seriesIndex, ShapeUtilities
.createDiamond((float) size));
break;
default:
renderer.setSeriesShapesVisible(seriesIndex, false);
}
}
seriesIndex++;
}
helper.storeSeries(collection, series);
// also register as a listener
final Adapter adapter = new AdapterImpl()
{
@Override
public void notifyChanged(final Notification notification)
{
// switch off chart refreshes, we don't
// want to do it for every new data point
series.setNotify(false);
populateCollection(helper, dataset, series);
series.setNotify(true);
series.fireSeriesChanged();
}
};
dataset.eAdapters().add(adapter);
// perform the initial population
adapter.notifyChanged(null);
}
}
/**
* create a chart from chart object for preview
*
* @param chart
*
* @return
*/
public static JFreeChart build(final Chart chart)
{
final IndependentAxis sharedAxisModel = chart.getParent().getSharedAxis();
final ChartHelper helper;
final ValueAxis sharedAxis;
if (sharedAxisModel == null)
{
sharedAxis = new DateAxis("Time");
helper = new NumberHelper();
}
else
{
final AxisType axisType = sharedAxisModel.getAxisType();
if (axisType instanceof info.limpet.stackedcharts.model.NumberAxis)
{
helper = new NumberHelper();
}
else if (axisType instanceof info.limpet.stackedcharts.model.AngleAxis)
{
helper = new NumberHelper();
}
else if (axisType instanceof info.limpet.stackedcharts.model.DateAxis)
{
helper = new DateHelper();
}
else
{
System.err.println("UNEXPECTED AXIS TYPE RECEIVED");
helper = new NumberHelper();
}
sharedAxis = helper.createAxis(sharedAxisModel.getName());
if (sharedAxisModel.getDirection() == AxisDirection.DESCENDING)
{
sharedAxis.setInverted(true);
}
}
sharedAxis.setVisible(false);
final CombinedDomainXYPlot plot = new TimeBarPlot(sharedAxis);
// create this chart
final XYPlot subplot = createChart(sharedAxisModel, chart, helper);
// hide the y axis labels - since our GEF editors show the value
int aCnt = subplot.getRangeAxisCount();
for(int ctr = 0; ctr < aCnt; ctr++)
{
ValueAxis axis = subplot.getRangeAxis(ctr);
if (axis != null) {
axis.setLabel(null);
}
}
// add chart to stack
plot.add(subplot);
plot.setOrientation(chart.getParent()
.getOrientation() == Orientation.VERTICAL ? PlotOrientation.VERTICAL
: PlotOrientation.HORIZONTAL);
final JFreeChart jFreeChart = new JFreeChart(plot);
jFreeChart.getLegend().setVisible(false);
return jFreeChart;
}
/**
* create a chart from our dataset
*
* @param chartsSet
* @param controllableDate
*
* @return
*/
public static JFreeChart build(final ChartSet chartsSet,
ControllableDate controllableDate)
{
final IndependentAxis sharedAxisModel = chartsSet.getSharedAxis();
final ChartHelper helper;
final ValueAxis sharedAxis;
if (sharedAxisModel == null)
{
sharedAxis = new DateAxis("Time");
helper = new NumberHelper();
}
else
{
final AxisType axisType = sharedAxisModel.getAxisType();
if (axisType instanceof info.limpet.stackedcharts.model.NumberAxis)
{
helper = new NumberHelper();
}
else if (axisType instanceof info.limpet.stackedcharts.model.AngleAxis)
{
helper = new NumberHelper();
}
else if (axisType instanceof info.limpet.stackedcharts.model.DateAxis)
{
helper = new DateHelper();
}
else
{
System.err.println("UNEXPECTED AXIS TYPE RECEIVED");
helper = new NumberHelper();
}
sharedAxis = helper.createAxis(sharedAxisModel.getName());
if (sharedAxisModel.getDirection() == AxisDirection.DESCENDING)
{
sharedAxis.setInverted(true);
}
}
final TimeBarPlot plot = new TimeBarPlot(sharedAxis);
// now loop through the charts
final EList<Chart> charts = chartsSet.getCharts();
for (final Chart chart : charts)
{
// create this chart
final XYPlot subplot = createChart(sharedAxisModel, chart, helper);
// add chart to stack
plot.add(subplot);
}
// do we know a date?
if (controllableDate != null)
{
// ok, initialise it
Date theTime = controllableDate.getDate();
if (theTime != null)
{
plot.setTime(theTime);
}
}
plot.setGap(5.0);
plot.setOrientation(chartsSet.getOrientation() == Orientation.VERTICAL
? PlotOrientation.VERTICAL : PlotOrientation.HORIZONTAL);
return new JFreeChart(plot);
}
protected static XYPlot createChart(final IndependentAxis sharedAxisModel,
final Chart chart, ChartHelper helper)
{
final XYPlot subplot = new XYPlot(null, null, null, null);
// do some basic plot formatting
BasicStroke stroke =
new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0,
new float[]
{8.0f, 2.0f}, 0);
subplot.setDomainGridlineStroke(stroke);
subplot.setRangeGridlineStroke(stroke);
// keep track of how many axes we create
int indexAxis = 0;
// min axis create on bottom or left
final EList<DependentAxis> minAxes = chart.getMinAxes();
for (final DependentAxis axis : minAxes)
{
createDependentAxis(subplot, indexAxis, axis, helper);
subplot.setRangeAxisLocation(indexAxis, AxisLocation.BOTTOM_OR_LEFT);
indexAxis++;
}
// max axis create on top or right
final EList<DependentAxis> maxAxes = chart.getMaxAxes();
for (final DependentAxis axis : maxAxes)
{
createDependentAxis(subplot, indexAxis, axis, helper);
subplot.setRangeAxisLocation(indexAxis, AxisLocation.TOP_OR_RIGHT);
indexAxis++;
}
if (sharedAxisModel != null)
{
// build selective annotations to plot
final EList<SelectiveAnnotation> selectiveAnnotations =
sharedAxisModel.getAnnotations();
final List<AbstractAnnotation> annotations = new ArrayList<>();
for (final SelectiveAnnotation selectiveAnnotation : selectiveAnnotations)
{
final EList<Chart> appearsIn = selectiveAnnotation.getAppearsIn();
// check selective option to see is this applicable to current
// chart
if (appearsIn == null || appearsIn.isEmpty()
|| appearsIn.contains(chart))
{
annotations.add(selectiveAnnotation.getAnnotation());
}
}
addAnnotationToPlot(subplot, annotations, false);
}
return subplot;
}
/**
*
* @param subplot
* target plot for index
* @param indexAxis
* index of new axis
* @param dependentAxis
* model object of DependentAxis
*/
private static void createDependentAxis(final XYPlot subplot,
final int indexAxis, final DependentAxis dependentAxis,
ChartHelper axeshelper)
{
final XYLineAndShapeRenderer renderer;
// is this a special axis type
final AxisType axisType = dependentAxis.getAxisType();
// sort out the name
final String axisName;
if (axisType instanceof info.limpet.stackedcharts.model.NumberAxis)
{
info.limpet.stackedcharts.model.NumberAxis number =
(info.limpet.stackedcharts.model.NumberAxis) axisType;
if (number.getUnits() != null)
{
if (dependentAxis.getName() == number.getUnits())
{
axisName = dependentAxis.getName();
}
else
{
axisName = dependentAxis.getName() + " (" + number.getUnits() + ")";
}
}
else
{
axisName = dependentAxis.getName();
}
}
else
{
axisName = dependentAxis.getName();
}
final ValueAxis chartAxis;
if (axisType instanceof AngleAxis)
{
AngleAxis angle = (AngleAxis) axisType;
// ok, quick check for red-green
if (angle.isRedGreen())
{
final double min = angle.getMinVal();
final double max = angle.getMaxVal();
// use the renderer that "jumps" across zero/360 barrier
renderer = new WrappingRenderer(min, max);
// use the angular axis
chartAxis = new RedGreenAngularUnitAxis(axisName, min, max);
// put in the reg/green background shading
IntervalMarker greenM = new IntervalMarker(0, max);
greenM.setPaint(Color.green);
greenM.setStroke(new BasicStroke(3));
greenM.setAlpha(0.03f);
subplot.addRangeMarker(1, greenM, Layer.BACKGROUND);
IntervalMarker im = new IntervalMarker(min, 0);
im.setPaint(Color.red);
im.setStroke(new BasicStroke(3));
im.setAlpha(0.03f);
subplot.addRangeMarker(1, im, Layer.BACKGROUND);
}
else
{
// hmm, should it have a zero centre
final boolean midOrigin = angle.isMidOrigin();
final double min;
final double max;
if (midOrigin)
{
final double range = angle.getMaxVal() - angle.getMinVal();
final double demiRange = range / 2d;
min = angle.getMinVal() - demiRange;
max = angle.getMaxVal() - demiRange;
}
else
{
min = angle.getMinVal();
max = angle.getMaxVal();
}
// use the renderer that "jumps" across zero/360 barrier
renderer = new WrappingRenderer(min, max);
// use the angular axis
chartAxis = new AngularUnitAxis(axisName, min, max);
}
}
else
{
renderer = new XYLineAndShapeRenderer();
chartAxis = new NumberAxis(axisName);
}
renderer.setDrawSeriesLineAsPath(true);
final int indexSeries = 0;
// set the line width
renderer.setAutoPopulateSeriesPaint(false);
renderer.setAutoPopulateSeriesStroke(false);
final XYDataset collection = axeshelper.createCollection();
if (dependentAxis.getDirection() == AxisDirection.DESCENDING)
{
chartAxis.setInverted(true);
}
addDatasetToAxis(axeshelper, dependentAxis.getDatasets(), collection,
renderer, indexSeries);
final EList<AbstractAnnotation> annotations =
dependentAxis.getAnnotations();
addAnnotationToPlot(subplot, annotations, true);
subplot.setDataset(indexAxis, collection);
subplot.setRangeAxis(indexAxis, chartAxis);
subplot.setRenderer(indexAxis, renderer);
subplot.mapDatasetToRangeAxis(indexAxis, indexAxis);
}
protected static void populateCollection(final ChartHelper helper,
final Dataset dataset, final Series series)
{
helper.clear(series);
final EList<DataItem> measurements = dataset.getMeasurements();
for (final DataItem dataItem : measurements)
{
helper.addItem(series, dataItem);
}
}
/**
* marker interface for an axis that uses fancy label formatting
*
* @author ian
*
*/
public static interface FancyFormattedAxis
{
}
/**
* interface for axis that provides color for specific values
*
* @author ian
*
*/
public static interface ColorProvider
{
public Color getColorFor(double val);
}
private static class CoreAngularAxis extends NumberAxis
{
/**
*
*/
private static final long serialVersionUID = 1L;
public CoreAngularAxis(String axisName, final double min, final double max)
{
super(axisName);
this.setDefaultAutoRange(new Range(min, max));
this.setRange(min, max);
}
/**
* when the plot is resized, we want to return to the default auto range, not the freshly
* calculated one.
*/
protected final void autoAdjustRange()
{
Plot plot = getPlot();
if (plot != null && plot instanceof ValueAxisPlot)
{
Range r = getDefaultAutoRange();
setRange(r, false, false);
}
else
{
super.autoAdjustRange();
}
}
@Override
public final NumberTickUnit getTickUnit()
{
final NumberTickUnit tickUnit = super.getTickUnit();
if (tickUnit.getSize() < 15)
{
return tickUnit;
}
else if (tickUnit.getSize() < 45)
{
return new NumberTickUnit(30);
}
else if (tickUnit.getSize() < 90)
{
return new NumberTickUnit(45);
}
else if (tickUnit.getSize() < 180)
{
return new NumberTickUnit(90);
}
else
{
return new NumberTickUnit(180);
}
}
}
/**
* modified version of angle axis that prefers to use angular metric units.
*
* @author ian
*
*/
private static class AngularUnitAxis extends CoreAngularAxis implements
FancyFormattedAxis
{
/**
*
*/
private static final long serialVersionUID = 1L;
private static class AnglularWrappingFormatter extends NumberFormat
{
/**
*
*/
private static final long serialVersionUID = 1L;
private DecimalFormat df;
private double range;
public AnglularWrappingFormatter(final double range)
{
this.range = range;
df = new DecimalFormat();
}
@Override
public StringBuffer format(double number, StringBuffer toAppendTo,
FieldPosition pos)
{
// check if we're doing a mid-origin arrangement
if (number < 0)
{
number += range;
}
return toAppendTo.append(df.format(number));
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo,
FieldPosition pos)
{
throw new UnsupportedOperationException(
"Formatter not implemented in WrappingFormatter");
}
@Override
public Number parse(String source, ParsePosition parsePosition)
{
throw new UnsupportedOperationException(
"Formatter not implemented in WrappingFormatter");
}
}
public AngularUnitAxis(final String axisName, double min, double max)
{
super(axisName, min, max);
this.setNumberFormatOverride(new AnglularWrappingFormatter(max - min));
}
}
/**
* modified version of angle axis that prefers to use angular metric units.
*
* @author ian
*
*/
private static class RedGreenAngularUnitAxis extends CoreAngularAxis
implements FancyFormattedAxis
{
/**
*
*/
private static final long serialVersionUID = 1L;
private class RedGreenAnglularWrappingFormatter extends NumberFormat
implements ColorProvider
{
/**
*
*/
private static final long serialVersionUID = 1L;
private DecimalFormat df;
public RedGreenAnglularWrappingFormatter()
{
df = new DecimalFormat();
}
@Override
public StringBuffer format(double number, StringBuffer toAppendTo,
FieldPosition pos)
{
// check if we're doing a mid-origin arrangement
final String prefix;
if (number < 0)
{
prefix = "R";
}
else if (number > 0)
{
prefix = "G";
}
else
{
prefix = "";
}
return toAppendTo.append(prefix + df.format(Math.abs(number)));
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo,
FieldPosition pos)
{
throw new UnsupportedOperationException(
"Formatter not implemented in WrappingFormatter");
}
@Override
public Number parse(String source, ParsePosition parsePosition)
{
throw new UnsupportedOperationException(
"Formatter not implemented in WrappingFormatter");
}
@Override
public Color getColorFor(double val)
{
final Color res;
if (val < 0)
{
res = Color.red;
}
else if (val > 0)
{
res = Color.green.darker();
}
else
{
res = Color.black;
}
return res;
}
}
private final RedGreenAnglularWrappingFormatter _formatter;
public RedGreenAngularUnitAxis(final String axisName, double min, double max)
{
super(axisName, min, max);
_formatter = new RedGreenAnglularWrappingFormatter();
this.setNumberFormatOverride(_formatter);
}
/**
* Draws the axis line, tick marks and tick mark labels.
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param cursor
* the cursor.
* @param plotArea
* the plot area (<code>null</code> not permitted).
* @param dataArea
* the data area (<code>null</code> not permitted).
* @param edge
* the edge that the axis is aligned with (<code>null</code> not permitted).
*
* @return The width or height used to draw the axis.
*/
protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor,
Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge)
{
AxisState state = new AxisState(cursor);
if (isAxisLineVisible())
{
drawAxisLine(g2, cursor, dataArea, edge);
}
@SuppressWarnings("rawtypes")
List ticks = refreshTicks(g2, state, dataArea, edge);
state.setTicks(ticks);
g2.setFont(getTickLabelFont());
Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE);
@SuppressWarnings("rawtypes")
Iterator iterator = ticks.iterator();
while (iterator.hasNext())
{
ValueTick tick = (ValueTick) iterator.next();
if (isTickLabelsVisible())
{
g2.setPaint(getTickLabelPaintFor(tick.getText()));
float[] anchorPoint =
calculateAnchorPoint(tick, cursor, dataArea, edge);
if (tick instanceof LogTick)
{
LogTick lt = (LogTick) tick;
if (lt.getAttributedLabel() == null)
{
continue;
}
AttrStringUtils.drawRotatedString(lt.getAttributedLabel(), g2,
anchorPoint[0], anchorPoint[1], tick.getTextAnchor(), tick
.getAngle(), tick.getRotationAnchor());
}
else
{
if (tick.getText() == null)
{
continue;
}
TextUtilities.drawRotatedString(tick.getText(), g2, anchorPoint[0],
anchorPoint[1], tick.getTextAnchor(), tick.getAngle(), tick
.getRotationAnchor());
}
}
if ((isTickMarksVisible() && tick.getTickType().equals(TickType.MAJOR))
|| (isMinorTickMarksVisible() && tick.getTickType().equals(
TickType.MINOR)))
{
double ol =
(tick.getTickType().equals(TickType.MINOR))
? getMinorTickMarkOutsideLength()
: getTickMarkOutsideLength();
double il =
(tick.getTickType().equals(TickType.MINOR))
? getMinorTickMarkInsideLength() : getTickMarkInsideLength();
float xx = (float) valueToJava2D(tick.getValue(), dataArea, edge);
Line2D mark = null;
g2.setStroke(getTickMarkStroke());
g2.setPaint(getTickMarkPaint());
if (edge == RectangleEdge.LEFT)
{
mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
}
else if (edge == RectangleEdge.RIGHT)
{
mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
}
else if (edge == RectangleEdge.TOP)
{
mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
}
else if (edge == RectangleEdge.BOTTOM)
{
mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
}
g2.draw(mark);
}
}
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved);
// need to work out the space used by the tick labels...
// so we can update the cursor...
double used = 0.0;
if (isTickLabelsVisible())
{
if (edge == RectangleEdge.LEFT)
{
used +=
findMaximumTickLabelWidth(ticks, g2, plotArea,
isVerticalTickLabels());
state.cursorLeft(used);
}
else if (edge == RectangleEdge.RIGHT)
{
used =
findMaximumTickLabelWidth(ticks, g2, plotArea,
isVerticalTickLabels());
state.cursorRight(used);
}
else if (edge == RectangleEdge.TOP)
{
used =
findMaximumTickLabelHeight(ticks, g2, plotArea,
isVerticalTickLabels());
state.cursorUp(used);
}
else if (edge == RectangleEdge.BOTTOM)
{
used =
findMaximumTickLabelHeight(ticks, g2, plotArea,
isVerticalTickLabels());
state.cursorDown(used);
}
}
return state;
}
private Paint getTickLabelPaintFor(String string)
{
// ok, see if we have a leading character
double factor = 1;
if (string.startsWith("R") || string.startsWith("G"))
{
if (string.startsWith("R"))
{
factor = -1;
}
string = string.substring(1);
}
return _formatter.getColorFor(Double.parseDouble(string) * factor);
}
}
private ChartBuilder()
{
}
}