1st: draw method: I put some verification before drawing somethings
2nd: using periods to calculate and put the divisors at correct position
3rd: improving performance using two class variables, firstMillisecond and lastMillisecond, call this method this.getFirst().getFirstMillisecond(this.calendar) all the time is terrible to performance
Take a look at the picture.
http://picasaweb.google.com.br/ricardo. ... 5919896594
Code: Select all
package br.com.crmsoft.finstock.chart.axis;
import static java.util.Calendar.MONTH;
import static org.jfree.chart.axis.PeriodAxisLabelInfo.DEFAULT_DIVIDER_PAINT;
import static org.jfree.chart.axis.PeriodAxisLabelInfo.DEFAULT_DIVIDER_STROKE;
import static org.jfree.chart.axis.PeriodAxisLabelInfo.DEFAULT_INSETS;
import static org.jfree.chart.axis.PeriodAxisLabelInfo.DEFAULT_LABEL_PAINT;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.lang.reflect.Constructor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.PeriodAxis;
import org.jfree.chart.axis.PeriodAxisLabelInfo;
import org.jfree.chart.event.AxisChangeEvent;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.ValueAxisPlot;
import org.jfree.data.Range;
import org.jfree.data.time.Day;
import org.jfree.data.time.Month;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.Year;
import org.jfree.text.TextUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
/**
* <p>Esta classe foi estendida com o intuito de melhorar a performance, algumas validações antes de renderizar na tela e
* ajuste das datas que serão mostradas na Axis. Antes estava mostrando em local errado os divisores</p>
*
* @author Celso Ricardo - CRMSoft Projetos e Consultoria
* @version $Id$
*/
// FIXME Verificar como excluir as datas que não estão no Dataset
public class FinStockPeriodAxis extends PeriodAxis {
private static final long serialVersionUID = -7679349583579345374L;
protected long firstMillisecond;
protected long lastMillisecond;
protected Calendar calendar;
public FinStockPeriodAxis(String label, RegularTimePeriod first, RegularTimePeriod last, TimeZone timeZone) {
super(label);
this.calendar = Calendar.getInstance(timeZone);
this.setFirst(first);
this.setLast(last);
this.setTimeZone(timeZone);
this.setAutoRangeTimePeriodClass(first.getClass());
this.setMajorTickTimePeriodClass(first.getClass());
this.setMinorTickMarksVisible(false);
this.setMinorTickTimePeriodClass(RegularTimePeriod.downsize(this.getMajorTickTimePeriodClass()));
this.setAutoRange(true);
PeriodAxisLabelInfo labelInfo[] = new PeriodAxisLabelInfo[2];
labelInfo[0] = new PeriodAxisLabelInfo(Month.class, new SimpleDateFormat("MMM"), DEFAULT_INSETS, new Font("SansSerif",
Font.PLAIN, 8), DEFAULT_LABEL_PAINT, true, DEFAULT_DIVIDER_STROKE, DEFAULT_DIVIDER_PAINT);
labelInfo[1] = new PeriodAxisLabelInfo(Year.class, new SimpleDateFormat("yyyy"), DEFAULT_INSETS, new Font("SansSerif",
Font.BOLD, 10), DEFAULT_LABEL_PAINT, true, DEFAULT_DIVIDER_STROKE, DEFAULT_DIVIDER_PAINT);
this.setLabelInfo(labelInfo);
}
public FinStockPeriodAxis(String label, RegularTimePeriod first, RegularTimePeriod last) {
this(label, first, last, TimeZone.getDefault());
}
public FinStockPeriodAxis(String label) {
this(label, new Day(), new Day());
}
@Override
public void setFirst(RegularTimePeriod first) {
super.setFirst(first);
this.firstMillisecond = first.getFirstMillisecond(this.calendar);
}
@Override
public void setLast(RegularTimePeriod last) {
super.setLast(last);
this.lastMillisecond = last.getLastMillisecond(this.calendar);
}
/**
* Returns the range for the axis.
*
* @return The axis range (never <code>null</code>).
*/
public Range getRange() {
return new Range(this.firstMillisecond, this.lastMillisecond);
}
/**
* Sets the range for the axis, if requested, sends an {@link AxisChangeEvent} to all registered listeners. As a side-effect,
* the auto-range flag is set to <code>false</code> (optional).
*
* @param range
* the range (<code>null</code> not permitted).
* @param turnOffAutoRange
* a flag that controls whether or not the auto range is turned off.
* @param notify
* a flag that controls whether or not listeners are notified.
*/
@Override
public void setRange(Range range, boolean turnOffAutoRange, boolean notify) {
super.setRange(range, turnOffAutoRange, false);
this.firstMillisecond = this.getFirst().getFirstMillisecond(this.calendar);
this.lastMillisecond = this.getLast().getLastMillisecond(this.calendar);
if (notify) {
notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Draws the axis on a Java 2D graphics device (such as the screen or a printer).
*
* @param g2
* the graphics device (<code>null</code> not permitted).
* @param cursor
* the cursor location (determines where to draw the axis).
* @param plotArea
* the area within which the axes and plot should be drawn.
* @param dataArea
* the area within which the data should be drawn.
* @param edge
* the axis location (<code>null</code> not permitted).
* @param plotState
* collects information about the plot (<code>null</code> permitted).
*
* @return The axis state (never <code>null</code>).
*/
public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
PlotRenderingInfo plotState) {
AxisState axisState = new AxisState(cursor);
if (isAxisLineVisible()) {
drawAxisLine(g2, cursor, dataArea, edge);
}
if (this.isTickMarksVisible()) {
this.drawTickMarks(g2, axisState, dataArea, edge);
}
if (this.isTickLabelsVisible()) {
for (int band = 0; band < this.getLabelInfo().length; band++) {
axisState = this.drawTickLabels(band, g2, axisState, dataArea, edge);
}
}
// draw the axis label (note that 'state' is passed in *and*
// returned)...
axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, axisState);
return axisState;
}
/**
* Draws the tick labels for one "band" of time periods.
*
* @param band
* the band index (zero-based).
* @param g2
* the graphics device.
* @param state
* the axis state.
* @param dataArea
* the data area.
* @param edge
* the edge where the axis is located.
*
* @return The updated axis state.
*/
protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
// work out the initial gap
double delta1 = 0.0;
PeriodAxisLabelInfo labelInfo = this.getLabelInfo()[band];
FontMetrics fm = g2.getFontMetrics(labelInfo.getLabelFont());
if (edge == RectangleEdge.BOTTOM) {
delta1 = labelInfo.getPadding().calculateTopOutset(fm.getHeight());
} else if (edge == RectangleEdge.TOP) {
delta1 = labelInfo.getPadding().calculateBottomOutset(fm.getHeight());
}
state.moveCursor(delta1, edge);
long axisMin = this.firstMillisecond;
long axisMax = this.lastMillisecond;
g2.setFont(labelInfo.getLabelFont());
g2.setPaint(labelInfo.getLabelPaint());
// work out the number of periods to skip for labelling
RegularTimePeriod p1 = labelInfo.createInstance(new Date(axisMin), this.getTimeZone());
RegularTimePeriod p2 = labelInfo.createInstance(new Date(axisMax), this.getTimeZone());
String label1 = labelInfo.getDateFormat().format(new Date(p1.getMiddleMillisecond(this.calendar)));
String label2 = labelInfo.getDateFormat().format(new Date(p2.getMiddleMillisecond(this.calendar)));
Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, g2.getFontMetrics());
Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, g2.getFontMetrics());
double w = Math.max(b1.getWidth(), b2.getWidth());
long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, dataArea, edge));
if (isInverted()) {
ww = axisMax - ww;
} else {
ww = ww - axisMin;
}
long length = p1.getLastMillisecond(this.calendar) - p1.getFirstMillisecond(this.calendar);
int periods = (int) (ww / length) + 1;
RegularTimePeriod p = labelInfo.createInstance(new Date(axisMin), this.getTimeZone());
Rectangle2D b = null;
long lastXX = 0L;
float y = (float) (state.getCursor());
TextAnchor anchor = TextAnchor.TOP_CENTER;
float yDelta = (float) b1.getHeight();
if (edge == RectangleEdge.TOP) {
anchor = TextAnchor.BOTTOM_CENTER;
yDelta = -yDelta;
}
// Flag para verificar se é a primeira vez no loop
boolean firstTimeAtLoop = true;
while (p.getFirstMillisecond(this.calendar) <= axisMax) {
long firstMillisecond = p.getFirstMillisecond(this.calendar);
// Verifica se o mês é par, para que o divisor do ano não fique em um mês que não seja janeiro
Calendar c = Calendar.getInstance(this.getTimeZone());
c.setTime(new Date(firstMillisecond));
if (firstTimeAtLoop && c.get(MONTH) % 2 != 0) {
p = p.next();
firstMillisecond = p.getFirstMillisecond(this.calendar);
}
// Seta falso na flag para não entrar novamente no if acima
firstTimeAtLoop = false;
long lastMillisecond = 0L;
// Código abaixo recupera o último milesegundo de acordo com a quantidade do periodo
int iPeriod = 1;
do {
lastMillisecond = p.getLastMillisecond(this.calendar);
iPeriod++;
p = p.next();
} while (iPeriod <= periods);
// Calcula a média entre o inicio e fim, é feito isso no caso de estar agrupando 2 meses
float x = (float) valueToJava2D(firstMillisecond + (lastMillisecond - firstMillisecond) / 2, dataArea, edge);
// Volta os períodos passados para recuperar o milesegundo final
for (int j = 0; j <= periods - 1; j++) {
p = p.previous();
}
DateFormat df = labelInfo.getDateFormat();
String label = df.format(new Date(p.getMiddleMillisecond(this.calendar)));
long first = firstMillisecond;
long last = lastMillisecond;
if (last > axisMax) {
// this is the last period, but it is only partially visible
// so check that the label will fit before displaying it...
Rectangle2D bb = TextUtilities.getTextBounds(label, g2, g2.getFontMetrics());
if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
float xstart = (float) valueToJava2D(Math.max(first, axisMin), dataArea, edge);
if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
x = ((float) dataArea.getMaxX() + xstart) / 2.0f;
} else {
label = null;
}
}
}
if (first < axisMin) {
// this is the first period, but it is only partially visible
// so check that the label will fit before displaying it...
Rectangle2D bb = TextUtilities.getTextBounds(label, g2, g2.getFontMetrics());
if ((x - bb.getWidth() / 2) < dataArea.getX()) {
float xlast = (float) valueToJava2D(Math.min(last, axisMax), dataArea, edge);
if (bb.getWidth() < (xlast - dataArea.getX())) {
x = (xlast + (float) dataArea.getX()) / 2.0f;
} else {
label = null;
}
}
}
if (label != null) {
g2.setPaint(labelInfo.getLabelPaint());
b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);
}
if (lastXX > 0L) {
if (labelInfo.getDrawDividers()) {
long nextXX = p.getFirstMillisecond(this.calendar);
long mid = (lastXX + nextXX) / 2;
float mid2d = (float) valueToJava2D(mid, dataArea, edge);
g2.setStroke(labelInfo.getDividerStroke());
g2.setPaint(labelInfo.getDividerPaint());
g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta));
}
}
lastXX = last;
for (int i = 0; i < periods; i++) {
p = p.next();
}
}
double used = 0.0;
if (b != null) {
used = b.getHeight();
// work out the trailing gap
if (edge == RectangleEdge.BOTTOM) {
used += labelInfo.getPadding().calculateBottomOutset(fm.getHeight());
} else if (edge == RectangleEdge.TOP) {
used += labelInfo.getPadding().calculateTopOutset(fm.getHeight());
}
}
state.moveCursor(used, edge);
return state;
}
/**
* Converts a data value to a coordinate in Java2D space, assuming that the axis runs along one edge of the specified
* dataArea.
* <p>
* Note that it is possible for the coordinate to fall outside the area.
*
* @param value
* the data value.
* @param area
* the area for plotting the data.
* @param edge
* the edge along which the axis lies.
*
* @return The Java2D coordinate.
*/
public double valueToJava2D(double value, Rectangle2D area, RectangleEdge edge) {
double result = Double.NaN;
double axisMin = this.firstMillisecond;
double axisMax = this.lastMillisecond;
if (RectangleEdge.isTopOrBottom(edge)) {
double minX = area.getX();
double maxX = area.getMaxX();
if (isInverted()) {
result = maxX + ((value - axisMin) / (axisMax - axisMin)) * (minX - maxX);
} else {
result = minX + ((value - axisMin) / (axisMax - axisMin)) * (maxX - minX);
}
} else if (RectangleEdge.isLeftOrRight(edge)) {
double minY = area.getMinY();
double maxY = area.getMaxY();
if (isInverted()) {
result = minY + (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
} else {
result = maxY - (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
}
}
return result;
}
/**
* Converts a coordinate in Java2D space to the corresponding data value, assuming that the axis runs along one edge of the
* specified dataArea.
*
* @param java2DValue
* the coordinate in Java2D space.
* @param area
* the area in which the data is plotted.
* @param edge
* the edge along which the axis lies.
*
* @return The data value.
*/
public double java2DToValue(double java2DValue, Rectangle2D area, RectangleEdge edge) {
double result = Double.NaN;
double min = 0.0;
double max = 0.0;
double axisMin = this.firstMillisecond;
double axisMax = this.lastMillisecond;
if (RectangleEdge.isTopOrBottom(edge)) {
min = area.getX();
max = area.getMaxX();
} else if (RectangleEdge.isLeftOrRight(edge)) {
min = area.getMaxY();
max = area.getY();
}
if (isInverted()) {
result = axisMax - ((java2DValue - min) / (max - min) * (axisMax - axisMin));
} else {
result = axisMin + ((java2DValue - min) / (max - min) * (axisMax - axisMin));
}
return result;
}
/**
* Rescales the axis to ensure that all data is visible.
*/
protected void autoAdjustRange() {
Plot plot = getPlot();
if (plot == null) {
return; // no plot, no data
}
if (plot instanceof ValueAxisPlot) {
ValueAxisPlot vap = (ValueAxisPlot) plot;
Range r = vap.getDataRange(this);
if (r == null) {
r = getDefaultAutoRange();
}
long upper = Math.round(r.getUpperBound());
long lower = Math.round(r.getLowerBound());
double range = upper - lower;
long minRange = (long) getAutoRangeMinimumSize();
if (range < minRange) {
long expand = (long) (minRange - range) / 2;
upper = upper + expand;
lower = lower - expand;
}
upper = upper + (long) (range * getUpperMargin());
lower = lower - (long) (range * getLowerMargin());
this.setFirst(this.createInstance(this.getAutoRangeTimePeriodClass(), new Date(lower), this.getTimeZone()));
this.setLast(this.createInstance(this.getAutoRangeTimePeriodClass(), new Date(upper), this.getTimeZone()));
r = new Range(lower, upper);
setRange(r, false, false);
}
}
/**
* A utility method used to create a particular subclass of the {@link RegularTimePeriod} class that includes the specified
* millisecond, assuming the specified time zone.
*
* @param periodClass
* the class.
* @param millisecond
* the time.
* @param zone
* the time zone.
*
* @return The time period.
*/
@SuppressWarnings("unchecked")
protected RegularTimePeriod createInstance(Class periodClass, Date millisecond, TimeZone zone) {
RegularTimePeriod result = null;
try {
Constructor c = periodClass.getDeclaredConstructor(new Class[] { Date.class, TimeZone.class });
result = (RegularTimePeriod) c.newInstance(new Object[] { millisecond, zone });
} catch (Exception e) {
// do nothing
}
return result;
}
}