package info.limpet.stackedcharts.ui.view; import info.limpet.stackedcharts.ui.view.ChartBuilder.ColorProvider; import info.limpet.stackedcharts.ui.view.ChartBuilder.FancyFormattedAxis; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Stroke; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jfree.chart.axis.Axis; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.CombinedDomainXYPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.chart.plot.PlotState; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.data.Range; import org.jfree.data.general.Dataset; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.time.TimeSeriesDataItem; import org.jfree.data.xy.XYDataItem; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; /** * provide new version of domain plot, that is able to render a time bar on the independent axis * * @author ian * */ public class TimeBarPlot extends CombinedDomainXYPlot { public static final String CHART_FONT_SIZE_NODE = "FONT_SIZE"; /** * strategy for classes that can provide an interpolated value for JFreeChart datasets * * @author ian * */ protected static interface AxisHelper { Double getValueAt(Dataset dataset, int index, double time, NumberAxis rangeA); } protected static class CacheingDateHelper implements AxisHelper { private static class IndexSet { Double previousY = null; Double nextY = null; Long previousX = null; Long nextX = null; public int index; } private final Map<TimeSeries, IndexSet> _cache; public CacheingDateHelper() { _cache = new HashMap<TimeSeries, IndexSet>(); } @Override public Double getValueAt(final Dataset dataset, final int seriesIndex, final double tNow, NumberAxis rangeA) { final TimeSeriesCollection tsc = (TimeSeriesCollection) dataset; final TimeSeries series = tsc.getSeries(seriesIndex); // see if we've cached this IndexSet set = _cache.get(series); if (set == null) { set = new IndexSet(); _cache.put(series, set); } @SuppressWarnings("unchecked") final List<TimeSeriesDataItem> items = series.getItems(); // just check if the time is still between the existing items final boolean pending = set.previousX == null; final boolean outOfRange = pending || set.previousX > tNow || set.nextX < tNow; if (pending || outOfRange) { final int len = items.size(); // try the index final long indexVal = items.get(set.index).getPeriod().getFirstMillisecond(); if (indexVal < tNow) { // walk forwards for (int index = set.index; index < len; index++) { final TimeSeriesDataItem thisItem = items.get(index); // does this class as a previous value? if (thisItem.getPeriod().getFirstMillisecond() < tNow) { // ok, it's before use the previous set.previousY = (Double) thisItem.getValue(); set.previousX = thisItem.getPeriod().getFirstMillisecond(); set.index = index; } else { // ok, we've passed it. stop set.nextY = (Double) thisItem.getValue(); set.nextX = thisItem.getPeriod().getFirstMillisecond(); break; } } } else { // walk backwards for (int index = set.index; index > 0; index--) { final TimeSeriesDataItem thisItem = items.get(index); // does this class as a previous value? if (thisItem.getPeriod().getFirstMillisecond() > tNow) { // ok, it's before use the previous set.nextY = (Double) thisItem.getValue(); set.nextX = thisItem.getPeriod().getFirstMillisecond(); } else { // ok, we've passed it. stop set.previousY = (Double) thisItem.getValue(); set.previousX = thisItem.getPeriod().getFirstMillisecond(); set.index = index; break; } } } } // have we found values? final Double interpolated; if (set.previousY != null && set.nextY != null) { // ok, interpolate the time (X) final double proportion = (tNow - set.previousX) / (set.nextX - set.previousX); // ok, now generate interpolate the value to use interpolated = set.previousY + proportion * (set.nextY - set.previousY); } else { interpolated = null; } return interpolated; } } protected static class DateHelper implements AxisHelper { public DateHelper() { } @Override public Double getValueAt(final Dataset dataset, final int index, final double tNow, NumberAxis rangeA) { final TimeSeriesCollection tsc = (TimeSeriesCollection) dataset; final TimeSeries series = tsc.getSeries(index); @SuppressWarnings("unchecked") final List<TimeSeriesDataItem> items = series.getItems(); // ok, get ready to interpolate the value Double previousY = null; Double nextY = null; Long previousX = null; Long nextX = null; // loop through the data for (final TimeSeriesDataItem thisItem : items) { // does this class as a previous value? if (thisItem.getPeriod().getFirstMillisecond() < tNow) { // ok, it's before use the previous previousY = (Double) thisItem.getValue(); previousX = thisItem.getPeriod().getFirstMillisecond(); } else { // ok, we've passed it. stop nextY = (Double) thisItem.getValue(); nextX = thisItem.getPeriod().getFirstMillisecond(); break; } } // have we found values? Double interpolated; if (previousY != null && nextY != null) { final Range rangeObj; // is this our fancy cyclic range axis? if (rangeA instanceof FancyFormattedAxis) { // is this a fancy range axis? rangeObj = rangeA.getDefaultAutoRange(); } else { rangeObj = null; } // yes - do some cyclic processing if (rangeObj != null) { double range = rangeObj.getUpperBound() - rangeObj.getLowerBound(); // just check that we shouldn't be wrapping the data if (Math.abs(nextY - previousY) > range / 2d) { if (previousY > nextY) { nextY += range; } else { nextY -= range; } } } // ok, interpolate the time (X) final double proportion = (tNow - previousX) / (nextX - previousX); // ok, now generate interpolate the value to use interpolated = previousY + proportion * (nextY - previousY); // is this a fancy range axis? if (rangeObj != null) { double range = rangeObj.getUpperBound() - rangeObj.getLowerBound(); if (interpolated < rangeObj.getLowerBound()) { interpolated += range; } else if (interpolated > rangeObj.getUpperBound()) { interpolated -= range; } } } else { interpolated = null; } return interpolated; } } protected static class NumberHelper implements AxisHelper { public NumberHelper() { } @Override public Double getValueAt(final Dataset dataset, final int index, final double tNow, NumberAxis rangeA) { final XYSeriesCollection tsc = (XYSeriesCollection) dataset; final XYSeries series = tsc.getSeries(index); @SuppressWarnings("unchecked") final List<XYDataItem> items = series.getItems(); // ok, get ready to interpolate the value Double previousY = null; Double nextY = null; Double previousX = null; Double nextX = null; // loop through the data for (final XYDataItem thisItem : items) { // does this class as a previous value? if (thisItem.getX().doubleValue() < tNow) { // ok, it's before use the previous previousY = thisItem.getYValue(); previousX = thisItem.getXValue(); } else { // ok, we've passed it. stop nextY = thisItem.getYValue(); nextX = thisItem.getXValue(); break; } } // have we found values? final Double interpolated; if (previousY != null && nextY != null) { // ok, interpolate the time (X) final double proportion = (tNow - previousX) / (nextX - previousX); // ok, now generate interpolate the value to use interpolated = previousY + proportion * (nextY - previousY); } else { interpolated = null; } return interpolated; } } /** * */ private static final long serialVersionUID = 1L; protected static void paintThisMarker(final Graphics2D g2, final String label, final Color color, final float markerX, final float markerY, final boolean vertical, Rectangle2D thisPlotArea) { // find the size of the label, so we can draw a background final FontMetrics fc = g2.getFontMetrics(); final Rectangle2D bounds = fc.getStringBounds(label, g2); // reflect the plot orientation final float xPos; final float yPos; if (vertical) { yPos = 3f + markerX; // if the marker height is too near the top it's not legible if (markerY < thisPlotArea.getY() + bounds.getHeight()) { // too high, move it down a little xPos = (float) (markerY + bounds.getHeight()); } else { xPos = markerY; } } else { xPos = 4f + markerX; yPos = 2f + markerY; } g2.setColor(Color.white); // just double check that we appear in the plot g2.fill3DRect((int) yPos, (int) (xPos - bounds.getHeight()), 3 + (int) bounds.getWidth(), 3 + (int) bounds.getHeight(), true); // double check we have a color final Color colorToUse; if (color == null) { System.err.println("using fallback color"); colorToUse = Color.magenta; } else { colorToUse = color; } g2.setColor(colorToUse.darker()); g2.drawString(label, yPos, xPos); } Date _currentTime = null; boolean _showLine = true; boolean _showLabels = true; final java.awt.Color _orange = new java.awt.Color(247, 153, 37); AxisHelper _helper; public TimeBarPlot(final ValueAxis sharedAxis) { super(sharedAxis); } /** * Draws the XY plot on a Java 2D graphics device (such as the screen or a printer), together with * a current time marker * <P> * XYPlot relies on an XYItemRenderer to draw each item in the plot. This allows the visual * representation of the data to be changed easily. * <P> * The optional info argument collects information about the rendering of the plot (dimensions, * tooltip information etc). Just pass in null if you do not need this information. * * @param g2 * The graphics device. * @param plotArea * The area within which the plot (including axis labels) should be drawn. * @param renderInfo * Collects chart drawing information (null permitted). */ @Override public final void draw(final Graphics2D g2, final Rectangle2D plotArea, final Point2D anchor, final PlotState state, final PlotRenderingInfo renderInfo) { super.draw(g2, plotArea, anchor, state, renderInfo); // NOTE: our WMF plotting isn't passing the renderInfo // object. We need this to plot the // time marker & label. Try to find it from somewhere. if (renderInfo == null) { return; } // do we have a time if (_currentTime != null) { // hmm, are we stacked vertically or horizontally? final boolean vertical = this.getOrientation() == PlotOrientation.VERTICAL; // find the screen area for the dataset final Rectangle2D dataArea = renderInfo.getDataArea(); // determine the time we are plotting the line at final long theTime = _currentTime.getTime(); final Axis domainAxis = this.getDomainAxis(); // hmm, see if we are wroking with a date or number axis Double linePosition; if (domainAxis instanceof DateAxis) { // ok, now scale the time to graph units final DateAxis dateAxis = (DateAxis) domainAxis; // find the new x value linePosition = dateAxis.dateToJava2D(new Date(theTime), dataArea, this .getDomainAxisEdge()); } else if (domainAxis instanceof NumberAxis) { final NumberAxis numberAxis = (NumberAxis) domainAxis; linePosition = numberAxis.valueToJava2D(theTime, dataArea, this .getDomainAxisEdge()); } else { linePosition = null; } if (linePosition == null) { return; } // trim the linePositiion to the visible area double trimmedLinePosition; if (vertical) { trimmedLinePosition = Math.max(linePosition, dataArea.getMinX()); trimmedLinePosition = Math.min(trimmedLinePosition, dataArea.getMaxX()); } else { trimmedLinePosition = Math.max(linePosition, dataArea.getMinY()); trimmedLinePosition = Math.min(trimmedLinePosition, dataArea.getMaxY()); } // did we do any clipping? final boolean clipped = !linePosition.equals(trimmedLinePosition); if (_showLine) { plotStepperLine(g2, dataArea, vertical, domainAxis, theTime, (int) trimmedLinePosition, clipped); } // ok, have a got at the time values if (_showLabels && !clipped) { // sort out the helper if (_helper == null) { final CombinedDomainXYPlot comb = this; final ValueAxis sharedAxis = comb.getDomainAxis(); if (sharedAxis instanceof DateAxis) { _helper = new DateHelper(); } else if (sharedAxis instanceof NumberAxis) { _helper = new NumberHelper(); } } final Font oldFont = g2.getFont(); final Font domainFont = this.getDomainAxis().getTickLabelFont(); final Font tempFont = domainFont.deriveFont(domainFont.getSize() * 1.2f).deriveFont( Font.BOLD); g2.setFont(tempFont); plotStepperMarkers(g2, dataArea, vertical, domainAxis, theTime, renderInfo, (int) trimmedLinePosition, _helper); g2.setFont(oldFont); } } } /** * draw the new stepper line into the plot * * @param g2 * @param linePosition * @param dataArea * @param axis * @param theTime * @param linePosition * @param clipped */ protected void plotStepperLine(final Graphics2D g2, final Rectangle2D dataArea, final boolean vertical, final Axis axis, final long theTime, final int linePosition, final boolean clipped) { // prepare to draw final Stroke oldStroke = g2.getStroke(); g2.setColor(_orange); // thicken up the line final int wid; if (clipped) { g2.setStroke(new BasicStroke(1)); wid = 1; } else { g2.setStroke(new BasicStroke(3)); wid = 1; } if (vertical) { // draw the line g2.drawLine(linePosition - wid + 2, (int) dataArea.getY(), linePosition - wid + 2, (int) dataArea.getY() + (int) dataArea.getHeight()); } else { // draw the line g2.drawLine((int) dataArea.getX() + 1, linePosition - 1, (int) dataArea .getX() + (int) dataArea.getWidth() - 1, linePosition - 1); } // and restore everything g2.setStroke(oldStroke); g2.setPaintMode(); } protected void plotStepperMarkers(final Graphics2D g2, final Rectangle2D dataArea, final boolean vertical, final Axis domainAxis, final long theTime, final PlotRenderingInfo info, final int linePosition, final AxisHelper axisHelper) { // ok, loop through the charts final CombinedDomainXYPlot comb = this; @SuppressWarnings("unchecked") final List<XYPlot> plots = comb.getSubplots(); final NumberFormat oneDP = new DecimalFormat("0.0"); final NumberFormat noDP = new DecimalFormat("0"); // keep track of how many series we've used for each renderer final Map<XYItemRenderer, Integer> seriesCounter = new HashMap<XYItemRenderer, Integer>(); // what's the current time? final double tNow = _currentTime.getTime(); // keep track of how many plots we've processed int plotCounter = 0; // loop through the stack of plots for (final XYPlot plot : plots) { // ok, get the area for this subplot final Rectangle2D thisPlotArea = info.getSubplotInfo(plotCounter++).getPlotArea(); // how many datasets? final int numC = plot.getDatasetCount(); // loop through the datasets for this plot for (int i = 0; i < numC; i++) { final XYDataset dataset = plot.getDataset(i); if (axisHelper != null && dataset != null) { final int num = dataset.getSeriesCount(); for (int j = 0; j < num; j++) { // and find the y coordinate of this data value final NumberAxis rangeA = (NumberAxis) plot.getRangeAxisForDataset(i); final Double interpolated = axisHelper.getValueAt(dataset, j, tNow, rangeA); if (interpolated != null) { // we may need to trim the interpolated value to be in the correct range final double chartLocation; if (interpolated > rangeA.getUpperBound()) { chartLocation = rangeA.getUpperBound(); } else if (interpolated < rangeA.getLowerBound()) { chartLocation = rangeA.getLowerBound(); } else { chartLocation = interpolated; } // convert to screen coords final float markerY = (float) rangeA.valueToJava2D(chartLocation, thisPlotArea, this.getRangeAxisEdge()); final double rounded = Math.round(interpolated); // quick check that we're not on a fancy axis final String label; NumberFormat fancyFormat = rangeA.getNumberFormatOverride(); if (fancyFormat != null) { label = fancyFormat.format(rounded); } else { // prepare the label final NumberFormat formatter; if (interpolated > 100) { formatter = noDP; } else { formatter = oneDP; } label = formatter.format(rounded); } // and the color final XYItemRenderer renderer = plot.getRendererForDataset(dataset); // find the series number Integer counter = seriesCounter.get(renderer); if (counter == null) { // first series, initialise counter = 0; seriesCounter.put(renderer, counter); } else { // already exists, increment seriesCounter.put(renderer, ++counter); } final Color paint; // just double-check we're not providing a color override if (fancyFormat != null && fancyFormat instanceof ColorProvider) { ColorProvider cp = (ColorProvider) fancyFormat; paint = cp.getColorFor(rounded); } else { paint = (Color) renderer.getSeriesPaint(counter); } // done, render it paintThisMarker(g2, label, paint, linePosition, markerY, vertical, thisPlotArea); } } } } } } public void setTime(final Date time) { _currentTime = time; } public Date getTime() { return _currentTime; } }