package charts.graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.NumberFormat;
import java.util.List;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRendererState;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import boxrenderer.ContentBoxImpl;
import boxrenderer.TableBox;
import boxrenderer.TableCellBox;
import boxrenderer.TableRowBox;
import boxrenderer.TextBox;
import charts.ChartType;
import charts.Drawable;
import charts.jfree.ADCDataset;
import charts.jfree.Attribute;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
public class TrackingTowardsTargets {
private static final Color GRADIENT_START = new Color(227, 246, 253);
private static final Color GRADIENT_END = Color.white;
private static final Color SERIES_1_COLOR = new Color(30, 172, 226);
private static final Color SERIES_2_COLOR = new Color(187, 34, 51);
private static final float LINE_WIDTH = 8.0f;
private static final double ARROWHEAD_EDGE_LENGTH = 30.0;
private static double sqr(double a) {
return a*a;
}
private static Point2D rotate(double x, double y, double a) {
double x2 = x * Math.cos(a) - y * Math.sin(a);
double y2 = x * Math.sin(a) + y * Math.cos(a);
return new Point2D.Double(x2, y2);
}
private static Point2D translate(Point2D point, double x, double y) {
return new Point2D.Double(point.getX() + x, point.getY() + y);
}
private static double gradient(Point2D p1, Point2D p2) {
return Math.atan((p2.getY() - p1.getY()) / (p2.getX() - p1.getX()));
}
private static class LineSection {
private Point2D w1Start;
private Point2D w1End;
private Point2D w2Start;
private Point2D w2End;
private boolean first;
public LineSection(Point2D p1, Point2D p2, boolean first, boolean last, double lh) {
this.first = first;
double gradient = gradient(p1, p2);
Point2D p3 = rotate(lh, 0, gradient + (-Math.PI/2));
// Point p3 is on the parallel line of p1 -> p2 with the sought line width
if(last) {
Point2D pTipTrans = translate(p2, -p1.getX(), -p1.getY());
double length = Math.sqrt(sqr(pTipTrans.getX())+sqr(pTipTrans.getY()));
double c3 = length-(ARROWHEAD_EDGE_LENGTH/2*Math.sqrt(3));
double x3 = Math.cos(gradient) * c3;
double y3 = Math.sin(gradient) * c3;
// line p1 -> p(x3,y3) is the same direction as line p1 -> p2 but shortened so
// that there is some space for the arrowhead
// point(ahx1,ahy1) and point(ahx2,ahy2) are both
// orthogonally translated from line p1 -> point(x3, y3) each by LINE_WIDTH / 2
double ograd = gradient+(-Math.PI/2);
Point2D p1Trans = rotate(lh, 0, ograd);
Point2D p2Trans = rotate(lh, 0, ograd+Math.PI);
double ahx1 = x3+p1.getX()+p1Trans.getX();
double ahy1 = y3+p1.getY()+p1Trans.getY();
double ahx2 = x3+p1.getX()+p2Trans.getX();
double ahy2 = y3+p1.getY()+p2Trans.getY();
w1End = new Point2D.Double(ahx1, ahy1);
w2Start = new Point2D.Double(ahx2, ahy2);
} else {
w1End = new Point2D.Double(p2.getX()+p3.getX(), p2.getY()+p3.getY());
w2Start = new Point2D.Double(p2.getX()-p3.getX(), p2.getY()-p3.getY());
}
if(first) {
// The formula for a line with a point (p3) and the gradient known is
// f(x) = gradient * ( x - p.x ) + p.y
// The formula below calculates the intersection of that line and the x-axis (f(x) = 0)
double x = p3.getX() - p3.getY() / Math.tan(gradient);
w1Start = new Point2D.Double(p1.getX()+x, p1.getY());
w2End = new Point2D.Double(p1.getX()-x, p1.getY());
} else {
w1Start = new Point2D.Double(p1.getX()+p3.getX(), p1.getY()+p3.getY());
w2End = new Point2D.Double(p1.getX()-p3.getX(), p1.getY()-p3.getY());
}
}
public void addW1(Path2D path) {
if(first) {
path.moveTo(w1Start.getX(), w1Start.getY());
} else {
path.lineTo(w1Start.getX(), w1Start.getY());
}
path.lineTo(w1End.getX(), w1End.getY());
}
public void addW2(Path2D path) {
path.lineTo(w2Start.getX(), w2Start.getY());
path.lineTo(w2End.getX(), w2End.getY());
}
}
private static class Renderer extends LineAndShapeRenderer {
private Font legendFont;
public Renderer(Font legendFont) {
super(true, false);
this.legendFont = legendFont;
}
@Override
public void drawItem(Graphics2D g2,
CategoryItemRendererState state, Rectangle2D dataArea,
CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis,
CategoryDataset dataset, int row, int column, int pass) {
if(column == 0) {
Shape shape = createShape(state, dataArea, plot, domainAxis, rangeAxis, dataset, row);
if(shape != null) {
if(pass == 0) {
// render all drop shadows first (pass 0)
Graphics2D g0 = (Graphics2D)g2.create();
g0.translate(2, 2);
g0.setPaint(Color.darkGray);
g0.fill(shape);
g0.dispose();
} else if(pass == 1) {
g2.setPaint(getSeriesPaint(row));
g2.fill(shape);
}
}
}
}
private Shape createShape(CategoryItemRendererState state, Rectangle2D dataArea,
CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis,
CategoryDataset dataset, int row) {
List<Point2D> points = getPoints(row, dataArea, dataset, plot, domainAxis, rangeAxis, state);
double lineWidth = ((BasicStroke)getSeriesStroke(row)).getLineWidth();
double lh = lineWidth / 2;
if(points.isEmpty()) {
return null;
}
if(points.size() == 1) {
Point2D p = points.get(0);
return new Ellipse2D.Double(p.getX()-lh,p.getY()-lh,lineWidth,lineWidth);
}
Path2D path = new Path2D.Double();
List<LineSection> sections = Lists.newArrayList();
for(int i=0; i<points.size()-1; i++) {
Point2D p1 = points.get(i);
Point2D p2 = points.get(i+1);
LineSection section = new LineSection(p1, p2, i==0, i==(points.size()-2), lh);
sections.add(section);
}
for(LineSection section : sections) {
section.addW1(path);
}
addArrowhead(path, points.get(points.size()-2), points.get(points.size()-1), lh);
for(LineSection section : Lists.reverse(sections)) {
section.addW2(path);
}
path.closePath();
return path;
}
private void addArrowhead(Path2D path, Point2D p1, Point2D p2, double lh) {
double gradient = gradient(p1, p2);
Point2D pTipTrans = translate(p2, -p1.getX(), -p1.getY());
double length = Math.sqrt(sqr(pTipTrans.getX())+sqr(pTipTrans.getY()));
double c3 = length-(ARROWHEAD_EDGE_LENGTH/2*Math.sqrt(3));
double x3 = Math.cos(gradient) * c3;
double y3 = Math.sin(gradient) * c3;
// line p1 -> p(x3,y3) is the same direction as line p1 -> p2 but shortened so
// that there is some space for the arrowhead
// point(ahx1,ahy1) and point(ahx2,ahy2) are both
// orthogonally translated from line p1 -> point(x3, y3) each by LINE_WIDTH / 2
double ograd = gradient+(-Math.PI/2);
Point2D p1Trans = rotate(lh, 0, ograd);
Point2D p2Trans = rotate(lh, 0, ograd+Math.PI);
double ahx1 = x3+p1.getX()+p1Trans.getX();
double ahy1 = y3+p1.getY()+p1Trans.getY();
double ahx2 = x3+p1.getX()+p2Trans.getX();
double ahy2 = y3+p1.getY()+p2Trans.getY();
path.lineTo(ahx1, ahy1);
Point2D pV1Trans = rotate(ARROWHEAD_EDGE_LENGTH/2, 0, ograd);
Point2D pV2Trans = rotate(ARROWHEAD_EDGE_LENGTH/2, 0, ograd+Math.PI);
double ahx3 = x3+p1.getX()+pV1Trans.getX();
double ahy3 = y3+p1.getY()+pV1Trans.getY();
// line to one of the base vertices of arrowhead
path.lineTo(ahx3, ahy3);
// line to tip of arrowhead
path.lineTo(p2.getX(), p2.getY());
double ahx4 = x3+p1.getX()+pV2Trans.getX();
double ahy4 = y3+p1.getY()+pV2Trans.getY();
path.lineTo(ahx4, ahy4);
path.lineTo(ahx2, ahy2);
}
private List<Point2D> getPoints(int row, Rectangle2D dataArea, CategoryDataset dataset,
CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, CategoryItemRendererState state) {
List<Point2D> points = Lists.newArrayList();
for(int column = 0; true; column++) {
Number v = null;
try {
v = dataset.getValue(row, column);
} catch(Exception e) {}
if (v == null) {
break;
}
int visibleRow = state.getVisibleSeriesIndex(row);
if (visibleRow < 0) {
break;
}
int visibleRowCount = state.getVisibleSeriesCount();
double x1;
if (this.getUseSeriesOffset()) {
x1 = domainAxis.getCategorySeriesMiddle(column,
dataset.getColumnCount(), visibleRow, visibleRowCount,
getItemMargin(), dataArea, plot.getDomainAxisEdge());
}
else {
x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
dataArea, plot.getDomainAxisEdge());
}
double value = v.doubleValue();
double y1 = rangeAxis.valueToJava2D(value, dataArea,
plot.getRangeAxisEdge());
points.add(new Point2D.Double(x1, y1));
}
return points;
}
@Override
public void drawBackground(Graphics2D g2, CategoryPlot plot,
Rectangle2D dataArea) {
super.drawBackground(g2, plot, dataArea);
Graphics2D g0 = null;
try {
CategoryDataset dataset = this.getPlot().getDataset();
if(dataset.getRowCount() == 2) {
TableRowBox row = new TableRowBox();
for(int i = 0; i < 2;i++) {
ContentBoxImpl c = new ContentBoxImpl();
c.setBackground(this.getSeriesPaint(i));
c.setWidth(25);
c.setHeight(2);
c.getMargin().setRight(5);
TableCellBox legendLineBox = new TableCellBox(c);
TableCellBox labelBox = new TableCellBox(
new TextBox(dataset.getRowKey(i).toString(), legendFont));
labelBox.getMargin().setRight(i==0?20:40);
row.addCell(legendLineBox);
row.addCell(labelBox);
}
TableBox legendBox = new TableBox();
legendBox.addRow(row);
Dimension d = legendBox.getDimension(g2);
g0 = (Graphics2D)g2.create((int)(dataArea.getMaxX()-d.getWidth()), 75, d.width, d.height);
legendBox.render(g0);
}
} catch(Exception e) {
throw new RuntimeException(e);
} finally {
if(g0 != null) {
g0.dispose();
}
}
}
}
private static NumberFormat percentFormatter() {
NumberFormat percentFormat = NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(0);
return percentFormat;
}
public Drawable createChart(ChartType type, double target,
final ADCDataset dataset, Dimension dimension) {
if(dataset.getRowCount() == 0) {
throw new RuntimeException("no series");
}
if(dataset.getRowCount() > 2) {
throw new RuntimeException("too many series");
}
final JFreeChart chart = ChartFactory.createLineChart(
dataset.get(Attribute.TITLE),
dataset.get(Attribute.X_AXIS_LABEL),
dataset.get(Attribute.Y_AXIS_LABEL),
dataset,
PlotOrientation.VERTICAL,
false,
false,
false);
final CategoryPlot plot = chart.getCategoryPlot();
CategoryAxis caxis = plot.getDomainAxis();
caxis.setTickMarksVisible(false);
plot.setRenderer(new Renderer(caxis.getTickLabelFont()));
plot.getRenderer().setSeriesStroke(0, new BasicStroke(LINE_WIDTH));
plot.getRenderer().setSeriesStroke(1, new BasicStroke(LINE_WIDTH));
Color[] seriesColors = dataset.get(Attribute.SERIES_COLORS);
for(int i=0;i<2;i++) {
if(seriesColors != null && i < seriesColors.length && seriesColors[i] != null) {
plot.getRenderer().setSeriesPaint(i, seriesColors[i]);
} else {
plot.getRenderer().setSeriesPaint(i, ImmutableList.of(SERIES_1_COLOR,
SERIES_2_COLOR).get(i));
}
}
plot.getRenderer().setBaseOutlinePaint(Color.black);
plot.setBackgroundPaint(new GradientPaint(0, 0, GRADIENT_END, 0, 0, GRADIENT_START));
plot.setDomainGridlinesVisible(false);
plot.setRangeGridlinesVisible(false);
plot.setOutlinePaint(Color.black);
plot.setOutlineVisible(true);
plot.setOutlineStroke(new BasicStroke(2.0f));
plot.setAxisOffset(RectangleInsets.ZERO_INSETS);
NumberAxis vaxis = (NumberAxis)plot.getRangeAxis();
vaxis.setRange(0, target);
vaxis.setAutoTickUnitSelection(true);
double tickSize;
if(target <= 0.2) {
tickSize = 0.02;
} else if(target <= 0.5) {
tickSize = 0.05;
} else {
tickSize = 0.1;
}
vaxis.setTickUnit(new NumberTickUnit(tickSize, percentFormatter()));
vaxis.setTickMarksVisible(false);
if(dataset.getColumnCount() > 5) {
plot.getDomainAxis().setCategoryLabelPositions(CategoryLabelPositions.UP_45);
}
return new JFreeChartDrawable(chart, dimension);
}
}