package charts.graphics; import graphics.GraphUtils; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.util.Collections; import java.util.List; import org.jfree.chart.axis.AxisState; import org.jfree.chart.axis.NumberTick; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.data.Range; import org.jfree.ui.RectangleEdge; import org.jfree.ui.TextAnchor; import com.google.common.collect.Lists; import com.google.common.math.DoubleMath; public class PartitionedNumberAxis extends ValueAxis { public static class Partition { private Range range; private double size; public Partition(Range range, double size) { super(); this.range = range; this.size = size; } public Range getRange() { return range; } public double getSize() { return size; } } private static final Double BOUNDARY_SIZE_PX = 5.0; private static final Double CURVATURE = 7.5; private static final Paint BOUNDARY_PAINT = Color.lightGray; private List<Partition> partitions = Lists.newArrayList(); public PartitionedNumberAxis(String label) { super(label, null); } public void addPartition(Partition p) { if(p.range == null) { throw new RuntimeException("partition.range is null"); } if(p.size <= 0) { throw new RuntimeException("partition.size <= 0"); } if(p.size > 1) { throw new RuntimeException("partition.size > 1"); } if(!partitions.isEmpty()) { for(Partition partition : partitions) { if(partition.range.intersects(p.range)) { throw new RuntimeException("partitions intersect"); } } if(partitions.get(partitions.size()-1).range.getLowerBound() > p.getRange().getLowerBound()) { throw new RuntimeException("partition range order"); } } partitions.add(p); } @Override public double valueToJava2D(double value, Rectangle2D area, RectangleEdge edge) { return valueToJava2D(value, area); } private double valueToJava2D(double value, Rectangle2D area) { if(partitions.isEmpty()) { throw new RuntimeException("no partitions"); } double axisLength = axisLength(area); double current = area.getMaxY(); if(value < partitions.get(0).range.getLowerBound()) { return current; } for(int i=0;i<partitions.size();i++) { Partition p = partitions.get(i); Range r = p.getRange(); double s = p.getSize(); double length = axisLength * s; if(r.contains(value)) { return current - (length * (value - r.getLowerBound()) / (r.getUpperBound()-r.getLowerBound())); } else if(((i+1) < partitions.size()) && (value < partitions.get(i+1).range.getLowerBound())) { return current - length - BOUNDARY_SIZE_PX / 2.0; } else { current = current - length - BOUNDARY_SIZE_PX; } } return current; } @Override public double java2DToValue(double java2dValue, Rectangle2D area, RectangleEdge edge) { throw new RuntimeException("java2DToValue not implemented"); } @Override protected void autoAdjustRange() {} @Override public void configure() {} private void checkSize() { double size = 0; for(Partition p : partitions) { size += p.getSize(); } if(!DoubleMath.fuzzyEquals(size, 1.0, 0.001)) { throw new RuntimeException(String.format( "sum of partitions size must be 1 dude! (%s)", size)); } } @Override public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState) { if(partitions.isEmpty()) { throw new RuntimeException("no partitions"); } checkSize(); if(edge != RectangleEdge.LEFT) { throw new RuntimeException("axis not on left"); } if (isAxisLineVisible()) { drawAxisLine(g2, cursor, dataArea, edge); } drawPartitions(g2, dataArea); AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea, dataArea, edge); state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); createAndAddEntity(cursor, state, dataArea, edge, plotState); return state; } @Override protected void drawAxisLine(Graphics2D g2, double cursor, Rectangle2D dataArea, RectangleEdge edge) { g2.setPaint(getAxisLinePaint()); g2.setStroke(getAxisLineStroke()); double start = dataArea.getMaxY(); double x = cursor; for(Double boundary : getPartitionBoundaries(dataArea)) { double end = boundary+BOUNDARY_SIZE_PX; g2.draw(new Line2D.Double(x, start, x, end)); drawAxisPartitionMark(g2, x, end); start = boundary; drawAxisPartitionMark(g2, x, start); } g2.draw(new Line2D.Double(x, start, x, dataArea.getY())); } private void drawAxisPartitionMark(Graphics2D g2, double x, double y) { g2.draw(new Line2D.Double(x-2, y, x+2, y)); } private int gaps() { return partitions.size()-1; } private double axisLength(Rectangle2D dataArea) { return dataArea.getMaxY() - dataArea.getY() - (gaps() * BOUNDARY_SIZE_PX); } private void drawPartitions(Graphics2D g2, Rectangle2D dataArea) { for(Double boundary : getPartitionBoundaries(dataArea)) { Rectangle2D.Double r = new Rectangle2D.Double(dataArea.getX(), boundary, dataArea.getWidth(), BOUNDARY_SIZE_PX); g2.setPaint(BOUNDARY_PAINT); g2.fill(r); } } private List<Double> getPartitionBoundaries(Rectangle2D dataArea) { List<Double> result = Lists.newArrayList(); double axisLength = axisLength(dataArea); double current = dataArea.getMaxY(); for(int i=0;i<partitions.size()-1;i++) { Partition p = partitions.get(i); double s = p.getSize(); current = current - axisLength * s - BOUNDARY_SIZE_PX; result.add(current); } return result; } @Override public List<NumberTick> refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) { return createTicks(g2, dataArea); } private List<NumberTick> createTicks(Graphics2D g2, Rectangle2D dataArea) { List<NumberTick> ticks = Lists.newArrayList(); for(Partition p : partitions) { ticks.addAll(createTicks(p, g2, dataArea)); } return ticks; } private List<NumberTick> createTicks(Partition p, Graphics2D g2, Rectangle2D dataArea) { List<NumberTick> ticks = Lists.newArrayList(); Range r = p.getRange(); double l = r.getLength(); if(DoubleMath.fuzzyEquals(l, 0.0, 0.001)) { ticks.add(tick(r.getLowerBound())); } else { double[] tf = {1.0, 0.5, 0.25}; double space = this.axisLength(dataArea) * p.getSize(); Range numberOfTicks = getNumberOfTicks(g2, space); List<Double> result = Lists.newArrayList(); for(int exp = 0;exp < 20;exp++) { for(int i = 0;i<tf.length;i++) { double tickUnit = tf[i] * Math.pow(10.0, exp); if(numberOfTicks.contains(l/tickUnit)) { result.add(tickUnit); } } if(exp > 0) { for(int i = 0;i<tf.length;i++) { double tickUnit = tf[i] * Math.pow(10.0, -exp); if(numberOfTicks.contains(l/tickUnit)) { result.add(tickUnit); } } } } if(result.isEmpty()) { throw new RuntimeException("trouble calculating tick unit"); } Collections.sort(result); double tickUnit = result.get(0); // make range a bit smaller to remove ticks that are close to the partition border Range smaller = shrinkNotZero(r,0.01); double current = Math.round(smaller.getLowerBound()/tickUnit)*tickUnit; while(current <= smaller.getUpperBound()) { if(smaller.contains(current)) { ticks.add(tick(current)); } current += tickUnit; } } return ticks; } private Range shrinkNotZero(Range r, double factor) { double l = Math.abs(r.getLength()); if(DoubleMath.fuzzyEquals(l, 0.0, 0.00001)) { return r; } double shrink = l * factor; if(r.getLowerBound() == 0.0) { return new Range(r.getLowerBound(), r.getUpperBound()-shrink); } else if(r.getUpperBound() == 0.0) { return new Range(r.getLowerBound()+shrink, r.getUpperBound()); } else { return new Range(r.getLowerBound()+shrink/2, r.getUpperBound()-shrink/2); } } private NumberTick tick(double value) { return new NumberTick(value, new DecimalFormat("#.######").format(value), TextAnchor.CENTER_RIGHT, TextAnchor.CENTER_RIGHT, 0); } private Range getNumberOfTicks(Graphics2D g2, double space) { GraphUtils g = new GraphUtils(g2); double tickLabelSize = g.getTextHeight(this.getTickLabelFont(), "0"); double margin = getTickLabelInsets().getTop()+getTickLabelInsets().getBottom(); return new Range(1, space/(tickLabelSize+margin)); } @Override public Range getRange() { if(partitions.isEmpty()) { throw new RuntimeException("no partitions"); } return new Range(partitions.get(0).range.getLowerBound(), partitions.get(partitions.size()-1).range.getUpperBound()); } public void drawPatitionBoundaries(Graphics2D g2, Range r, Rectangle2D dataArea, double width, double x) { List<Double> boundaries = getPartitionBoundaries(dataArea); for(int i=0;i<partitions.size()-1;i++) { Partition p = partitions.get(i); if(crossesBoundary(p.range.getUpperBound(), r)) { g2.setStroke(new BasicStroke(1.0f)); g2.setPaint(BOUNDARY_PAINT); Shape closed = createClosedBoundaryShape(x, boundaries.get(i), width); g2.fill(closed); g2.draw(closed); g2.setPaint(Color.black); g2.draw(createOpenBoundaryShape(x, boundaries.get(i), width)); } } } private Shape createOpenBoundaryShape(double x, double y, double width) { return createBoundaryShape(x,y,width, true); } private Shape createClosedBoundaryShape(double x, double y, double width) { return createBoundaryShape(x,y,width, false); } private Shape createBoundaryShape(double x, double y, double width, boolean open) { Path2D.Double p = new Path2D.Double(); p.moveTo(x,y); p.curveTo(x+width*.25, y-CURVATURE, x+width*.75, y+CURVATURE, x+width, y); if(open) { p.moveTo(x+width, y+BOUNDARY_SIZE_PX); } else { p.lineTo(x+width, y+BOUNDARY_SIZE_PX); } p.curveTo(x+width*.75, y+BOUNDARY_SIZE_PX+CURVATURE, x+width*.25, y+BOUNDARY_SIZE_PX-CURVATURE, x, y+BOUNDARY_SIZE_PX); if(!open) { p.closePath(); } return p; } private boolean crossesBoundary(double boundary, Range r) { return (r.getLowerBound() <= boundary) && (r.getUpperBound() > boundary); } }