A few weeks ago I inquired whether jFreeChart could implement what the gnuplot folks call an "impulse plot", i.e., one composed of vertical lines,whose height represents their data points. I prefer this type of presentation for essentially random data since it does make trends (if any) easy to see, but unlike the line plot, doesn't subtly hint at a mathematical function that may not really exist.
Anyway, David pointed out that XYDotRenderer could easily be modified to produce impulse plots, and that was indeed very easy to do, as I reported soon after.
My applications usually involve plotting 2 or more distinct data streams against a common timebase. Also, it frequently becomes necessary to "switch" the display from one set of data streams to another. These requirements led me to experiment with various performance optimizations.
In conjunction with a "FastTimeSeriesCollection" class (to be posted soon), we have reduced the time to switch between one set of (3 * 1440) data points to another from 11 seconds down to less than 1 1/2 seconds.
I hereby contribute two versions of the impulse renderer class, "XYImpulseRenderer" and "FastXYImpulseRenderer". The double lines mark the boundaries between the two files.
=====================================================
* XYImpulseRenderer.java - V1.0 - 10-28-2002
*
* Irv Thomae ithomae@ists.dartmouth.edu
* (C) 2002 ISTS/Dartmouth College
*
* This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
*
* "XYImpulseRenderer" represents each (x,y) point drawn as a vertical line
* extending from (x, 0) upward to the point (x,y) itself.
* This class is derived from, and very closely resembles, XYDotRenderer,
* except that it uses Graphics2D's drawLine() instead of drawRect().
* Because that line will always start at (y = 0) as translated to Java2D,
* that translated value is treated as an instance variable to improve
* performance. It remains in effect until retranslateVerticalBase() is called,
* for example by an EventListener object which has recognized that the plot is being
* moved up or down on the screen.
*/
package com.jrefinery.chart;
import java.awt.Graphics2D;
import java.awt.geom.*;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import com.jrefinery.data.XYDataset;
//import com.jrefinery.chart.XYDotRenderer;
public class XYImpulseRenderer extends AbstractXYItemRenderer
implements XYItemRenderer
{
private int transZero; // initially zero
public XYImpulseRenderer()
{
}
/**
* Draws the visual representation of a single data item.
*
* @param g2 the graphics device.
* @param dataArea the area within which the data is being drawn.
* @param info collects information about the drawing.
* @param plot the plot (can be used to obtain standard color information etc).
* @param domainAxis the domain (horizontal) axis.
* @param rangeAxis the range (vertical) axis.
* @param data the dataset.
* @param series the series index.
* @param item the item index.
* @param crosshairInfo information about crosshairs on a plot.
*/
public void drawItem(Graphics2D g2,
Rectangle2D dataArea,
ChartRenderingInfo info,
XYPlot plot,
ValueAxis domainAxis,
ValueAxis rangeAxis,
XYDataset data,
int series,
int item,
CrosshairInfo crosshairInfo)
{
Paint seriesPaint = plot.getSeriesPaint(series);
Stroke seriesStroke = plot.getSeriesStroke(series);
g2.setPaint(seriesPaint);
g2.setStroke(seriesStroke);
if (transZero == 0)
retranslateVerticalBase(dataArea, rangeAxis);
// get the data point...
Number xn = data.getXValue(series, item);
Number yn = data.getYValue(series, item);
if (yn != null) {
float x = xn.floatValue();
float y = yn.floatValue();
int transX = (int)domainAxis.translateValueToJava2D(x, dataArea);
int transY = (int)rangeAxis.translateValueToJava2D(y, dataArea);
g2.drawLine(transX,transY, transX,transZero );
// do we need to update the crosshair values?
if (domainAxis.isCrosshairLockedOnData()) {
if (rangeAxis.isCrosshairLockedOnData()) {
// both axes
crosshairInfo.updateCrosshairPoint(x, y);
}
else {
// just the horizontal axis...
crosshairInfo.updateCrosshairX(x);
}
}
else {
if (rangeAxis.isCrosshairLockedOnData()) {
// just the vertical axis...
crosshairInfo.updateCrosshairY(y);
}
}
}
}
// Call this when the vertical location of the x-axis changes:
public void retranslateVerticalBase(Rectangle2D dataArea,
ValueAxis rangeAxis)
{
transZero = (int)rangeAxis.translateValueToJava2D(0, dataArea);
}
}
=====================================================
/* FastXYImpulseRenderer.java V1.0 11-18-2002
*
* Irv Thomae ithomae@ists.dartmouth.edu
* (C) 2002 ISTS/Dartmouth College
*
* This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
*
* This class is an XYImpulseRenderer optimized for use with XYDataset's which have
* 2 or more series with varying y-values for the same set of x-axis points, a
* case that frequently arises when the x-axis represents time
* If the horizontal-axis spacing between successive points leaves sufficient
* room, data from different series are drawn adjacent to each other; otherwise
* at each x-position the last data-point drawn will typically hide other series'
* data from view.
* To avoid redundant conversions of x-values each time another series is drawn,
* this class caches the converted-to-int x-values which are computed while
* drawing a series for the first time.
* "transZero", i.e. the y=0 value as translated to Java2D coordinates,
* is also "cached" by making it an instance variable, and floats are used rather
* than doubles for additional performance benefit.
*/
package com.jrefinery.chart;
import java.awt.Graphics2D;
import java.awt.geom.*;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import com.jrefinery.data.XYDataset;
public class FastXYImpulseRenderer extends AbstractXYItemRenderer
implements XYItemRenderer
{
private int[] xValues; // initially null
private int transZero; // initially zero
private int nPositions;
private boolean separable; // true iff horizontal spacing >= # of series
public FastXYImpulseRenderer()
{
}
/**
* Draws the visual representation of a single data item.
*
* @param g2 the graphics device.
* @param dataArea the area within which the data is being drawn.
* @param info collects information about the drawing.
* @param plot the plot (can be used to obtain standard color information etc).
* @param domainAxis the domain (horizontal) axis.
* @param rangeAxis the range (vertical) axis.
* @param data the dataset.
* @param series the series index.
* @param item the item index.
* @param crosshairInfo information about crosshairs on a plot.
*/
public void drawItem(Graphics2D g2,
Rectangle2D dataArea,
ChartRenderingInfo info,
XYPlot plot,
ValueAxis domainAxis,
ValueAxis rangeAxis,
XYDataset data,
int series,
int item,
CrosshairInfo crosshairInfo)
{
Paint seriesPaint = plot.getSeriesPaint(series);
Stroke seriesStroke = plot.getSeriesStroke(series);
g2.setPaint(seriesPaint);
g2.setStroke(seriesStroke);
if (transZero == 0)
retranslateVerticalBase(dataArea, rangeAxis);
if ((xValues == null)/* || (dataArea != theDataArea)*/)
{ // if first-series use:
nPositions = data.getItemCount(series);
xValues = new int[nPositions];
// Build the entire array of x-values:
for (int i = 0; i < nPositions; i++)
{
Number xn = data.getXValue(series, i);
float x = xn.floatValue();
xValues = (int)domainAxis.translateValueToJava2D(x, dataArea);
int nSeries = data.getSeriesCount();
if ((nSeries > 1) && (deltaX >= nSeries))
separable = true;
}
}
// get the data point...
Number yn = data.getYValue(series, item);
if (yn != null)
{
float y = yn.floatValue();
int transX = xValues[item];
if (separable)
transX += series;
int transY = (int)rangeAxis.translateValueToJava2D(y, dataArea);
// It is the following line tha makes this an ImpulseRenderer,
// instead of a DotRenderer:
g2.drawLine(transX,transY, transX,transZero );
// do we need to update the crosshair values?
if (domainAxis.isCrosshairLockedOnData())
{ // Now we need 'x', a float
Number xn = data.getXValue(series, item);
float x = xn.floatValue();
if (rangeAxis.isCrosshairLockedOnData())
{ // both axes
crosshairInfo.updateCrosshairPoint(x, y);
}
else // just the horizontal axis...
crosshairInfo.updateCrosshairX(x);
}
else
{ // just the vertical axis...
if (rangeAxis.isCrosshairLockedOnData())
crosshairInfo.updateCrosshairY(y);
}
}
}
// Use this when and if about to plot new data having a different
// x-axis range - alternatively, call it from a suitable EventListener:
public void flushXCache()
{
xValues = null;
transZero = 0;
}
public void buildXArray(Rectangle2D dataArea,
ValueAxis domainAxis,
XYDataset data,
int series)
{
nPositions = data.getItemCount(series);
xValues = new int[nPositions];
// Build the entire array of x-values:
for (int i = 0; i < nPositions; i++)
{
Number xn = data.getXValue(series, i);
float x = xn.floatValue();
xValues = (int)domainAxis.translateValueToJava2D(x, dataArea);
}
}
// Use this when the vertical location of the x-axis changes:
public void retranslateVerticalBase(Rectangle2D dataArea,
ValueAxis rangeAxis)
{
transZero = (int)rangeAxis.translateValueToJava2D(0, dataArea);
}
}
XYImpulseRenderer class(es)
Re: XYImpulseRenderer class(es)
Hi Irv,
Thanks for posting your code. I'll get this added to the repository as soon as I can.
Regards,
DG
Thanks for posting your code. I'll get this added to the repository as soon as I can.
Regards,
DG