Zooming combined Plot

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
Martin Rupp

Zooming combined Plot

Post by Martin Rupp » Thu Feb 27, 2003 3:31 pm

Hi,

ChartPanel throws an Null-Pointer Exception, when I try zooming a combined Plot.

It is a result of setting one subplot-axis to null.

Is there a solution?

Thanks

Martin

David Gilbert

Re: Zooming combined Plot

Post by David Gilbert » Fri Feb 28, 2003 1:01 am

Hi Martin,

This is a bug, the zooming has never been tested properly with the combined charts. Can you open a bug report on SourceForge?

Regards,

Dave Gilbert

Joachim

Re: Zooming combined Plot

Post by Joachim » Fri Feb 28, 2003 8:53 am

It's not that difficult to add zooming to CombinedXYPlot.
For example I use a vertical CombinedXYPlot with a HorizontalDateAxis.
This vertical CombinedXYPlot has no Value Axis and this will give you the NullPointer in case you're trying to zoom using the built-in feature.

If you need a value axis for zooming then you have to determine, depending on the mouse position, the according subplot to get a valid value axis.

Joachim

Charles H Martin

Re: Zooming combined Plot

Post by Charles H Martin » Fri Feb 28, 2003 3:43 pm

I did not find zooming in combined plots so easy to implement
Could you share some more details about this, and maybe a simple example.

To get zooming in a combined polot, I modifed the existing CombinedXYPlot class as well as a number helper classes. I found this necessary to
(1) avoid this null pointer exception, and (2) to obtain the proper
scaling for each plot . My solution is rather complex, but I can share it if you like

Charles Martin

Charles H Martin

Re: Zooming combined Plot

Post by Charles H Martin » Fri Feb 28, 2003 5:09 pm

Here are some notes on the changes I made to ChartPanel and some associated classes in order to implement zoom in a CombinedXYPlot

I have included some sample code for various methods. If desired, I can work on
integrating this code into the SourceForge code base, but it may take some time
because there are (1) many changes and additional features that I needed and (2) many things I probably did poorly

I am posting this here to get some feedback, and to demonstrate my analysis of the problem, its complexity, and possible solution



Additions to ChartPanel:

(1) zoom accross multiple charts, aligned on the horizontal axis.

(2) add listeners/callbacks which display the x and y values of each plot

(3) adding support lines form an external source, and allow them to be zoomed
and cropped. Support lines are stored in an external ChartContext class....
...not discussed here

(4) allow dynamic changes to combined XY plot, supported by an interface IDynamicMultiChart


I will leave out the portions on the support lines and just discuss the zoom
Notice that I want to be able to save the zoom ranges between different instances
of the ChartPanel, so I save them externally in a ChartContext class.

Also, notice that in order to implement these changes, I had to recompile JFreeChart
and change the access permissions for a few class variables from private to protected,
such as CombinedXYPlots.subplots , and I added getSubplot() to OverlaidXYPlot.

I call these new classes QVChartPanel, QVCombinedXYPlot, QVOverlaidXYPlot, etc
simply for my own bookkeeping


Here are some methods I added to this and other classes
-----------------------------------------------


// setHorizontalRange is inserted into the zoom method , but may also be called externally:


public void zoom(Rectangle2D selection) {
try
{
if (chart.getPlot() instanceof QVCombinedXYPlot)
{
if ((selection.getHeight()>0) && (selection.getWidth()>0))
{
// only code for vertically combined charts for now
// resize Horizontal for the Combined Plot
Rectangle2D scaledDataArea = getScaledDataArea();
HorizontalValuePlot hvp = (HorizontalValuePlot)chart.getPlot();
ValueAxis axis = hvp.getHorizontalValueAxis();
double lower = axis.translateJava2DtoValue((float)selection.getX(), scaledDataArea);
double upper = axis.translateJava2DtoValue((float)selection.getMaxX(), scaledDataArea);
setHorizontalRange(lower, upper);
}

}
else
{
oldZoom(selection);
} // end of else


}
catch (Exception e)
{
e.printStackTrace();
System.out.println("zoom exception: "+e.getMessage());
} // end of try-catch
}





/**
* <code>setHorizontalRange</code> is used by both zoom
* and external calls to reset the range on HorizontalValueAxis
* and each axis for the combined plots.
*
* @param lower a <code>double</code> value
* @param upper a <code>double</code> value
*/
public void setHorizontalRange(double lower, double upper)
{
HorizontalValuePlot hvp = (HorizontalValuePlot)chart.getPlot();
ValueAxis axis = hvp.getHorizontalValueAxis();
axis.setRange(lower, upper);

// loop over all plots
// resize each vertical axis
QVCombinedXYPlot qvp = (QVCombinedXYPlot) chart.getPlot();
Range domain = new Range(lower,upper);
Range range = null;

for (int ip = 1; ip <= this.info.getNumCombinedPlots(); ip++)
{
axis = qvp.getVerticalValueAxis(ip);
range = qvp.getScaledVerticalDataRange(ip, domain);

axis.setRange(range.getLowerBound(), range.getUpperBound());

// call back to ChartContext, save range for when changing view of plots
setRange(ip, lower, upper, range.getLowerBound(), range.getUpperBound());
}
}



//
// These are helper methods used for the support lines and other features.
// I show them here for completeness
//

/**
* Returns the data area for the plot that the mouse is over,
* with the current scaling applied.
*
* @return The scaled data area.
*/
public Rectangle2D getVerticallyCombinedScaledDataArea(int iy) {

/**
* Returns the data area for the plot that the given plotId
* with the current scaling applied.
*
* @return The scaled data area.
*/
public Rectangle2D getPlotScaledDataArea(int plotIndex) {


//
// These methods allow me to save the Range externally
// Note that I also have to keep track of the subPlot index externally
// since I will be turning plots on and off as I change the view
//

public void setRangeListener(IRangeListener listener)

public void setHorizontalRange(double lower, double upper)

private void setRange(int plotIndex, double xmin, double xmax, double ymin, double ymax)

public void setXValueListener(IValueListener listener)

public void setYValueListener(IValueListener listener)



---------------------------------------------------
Auxillary classes modified and/or added:

QVCombinedXYPlot: A CombinedXYPlot, fixed to Vertical, that provides

(a) access to each subPlot by index

public int getNumPlots()

(b) access to axes for each plot:

public ValueAxis getVerticalValueAxis(int plotIndex) {
return ((VerticalValuePlot) subplots.get(plotIndex-1)).getVerticalValueAxis();
}

public ValueAxis getDomainAxis(int plotIndex) {
return ((XYPlot) subplots.get(plotIndex-1)).getDomainAxis();
}

public ValueAxis getRangeAxis(int plotIndex) {
return ((XYPlot) subplots.get(plotIndex-1)).getRangeAxis();
}

(c) methods to find the plot index given the position of the mouse

see QVMultiChartRenderingInfo



(d) assists implementing zooming accross multiple charts by providing:

/**
* Returns the range for the vertical axis, scaled by the horizontnal
* for Overlaid plots, and determined by the maximum range from all data sets
* in a given plot
*/
public Range getScaledVerticalDataRange(int plotIndex, Range domain)
{
Range result = null;

XYPlot plot = ((XYPlot) subplots.get(plotIndex-1)); // notice access to CombinedXYPlot.subplots

dataset = (Dataset) plot.getXYDataset();
if (dataset!=null)
{
result = QVDatasetUtilities.getScaledRange(dataset, domain);
// if the range is out of bounds, utitlity class will return a null
// so just return (0,0) range
if (result != null)
{
return result;
}
else
{
return new Range(0,0);
} // end of else

}
else if (plot instanceof QVOverlaidXYPlot) // notice I need getSubplot() method on OverlaidXYPlot
{

QVOverlaidXYPlot qvp = (QVOverlaidXYPlot) plot;
double lower = Double.POSITIVE_INFINITY;
double upper = Double.NEGATIVE_INFINITY;
for (int ip=0; ip < qvp.getNumSubplots(); ip++)
{
dataset = (Dataset) qvp.getSubplot(ip).getXYDataset();
result = QVDatasetUtilities.getScaledRange(dataset, domain);

// if range is out of bounds, utility will return a null
if (result != null)
{
lower = Math.min(lower, result.getLowerBound());
upper = Math.max(upper, result.getUpperBound());
}

}

// if range is out of bounds, just return (0,0)
if (lower==Double.POSITIVE_INFINITY) return new Range(0,0);
return new Range(lower,upper);
}
else
{
// this only works for CombinedXYPlot
System.err.println("QVCombinedPlot: getScaledVerticalDataRange: can not handle plot "+plot.toString());
} // end of else


return result;

}
}



QVMultiChartRenderingInfo:
a variant of a (Gang-of-Four) Decorator that holds both the ChartRenderingInfo for the QVCombinedXYPlot
or CombinedXYPlot, and a collection of ChartRenderingInfo instances for each subplot in a QVCombinedXYPlot
I needed this because I did not know how to get access to the sclaed data areas for each subPlot directly
from the CombinedXYPlot.

/**
* <code>getInfo</code> returns the info for the combined plot
* specified by an index (starting at 1). if there are no combined plots,
* index will be zero, and it just returns this object.
*
* @param i an <code>int</code> value
* @return a <code>ChartRenderingInfo</code> value
*/
public ChartRenderingInfo getInfo(int i)


/**
* <code>getVerticallyCombinedPlotIndex</code> returns the index of the
* vertically combined plot within the x region.
*
* May want to move this to insize QVChartPanel since the iy coordinate
* needs to be rescaled in order to work correctly
*
* Index for combined plots starts at 1
* if no combined plots, returns 0
* returns -1 if there is no plot in the region
*
* iy must be "unscaled" to work properly
*
* @param ix an <code>int</code> value
* @return an <code>int</code> value
*/
public int getVerticallyCombinedPlotIndex(int iy)
{
for (int ip=1; ip <= this.numCombinedPlots; ip++)
{
Rectangle2D dataArea = this.getInfo(ip).getDataArea();
if (((int)dataArea.getMinY()<iy) && (iy<(int)dataArea.getMaxY())) return ip;
}
return -1;
}

public int getVerticallyCombinedPlotIndex(int ix, int iy)



QVDatasetUtilities:

getScaledRange(Dataset data, Range domain) which finds the correct range for a series of datasets,
including HighLow datasets (but NOT decorations such as support lines):

/**
* Returns the range of values in the range for the dataset. This method
* is the partner for the getDomainExtent method.
*
* Returns null if domain has no data...is converted to (0,0) later, where appropriate
*
* @param data The dataset.
*
* @return the range of values in the range for the dataset.
*/
public static Range getScaledRange(Dataset data, Range domain)
{

// check parameters...
if (data==null)
{
throw new IllegalArgumentException(
"Datasets.getMinimumRangeValue: null dataset not allowed.");
}

else if (data instanceof XYDataset)
{

XYDataset xyData = (XYDataset)data;
int seriesCount = xyData.getSeriesCount();

// find startx, endx values based on Horizontal range
double startx = domain.getLowerBound();
double endx = domain.getUpperBound();



Number astartx = xyData.getXValue(0, 0);
Number aendx = xyData.getXValue(0, xyData.getItemCount(0)-1);

// find istart and iend for each member of the series
int istart[] = new int[seriesCount];
int iend[] = new int[seriesCount];
int series = 0;
int itemCount = 0;
double x = 0.0;
int item = 0;
boolean scanx = true;

for (series=0; series < seriesCount; series++)
{
itemCount = xyData.getItemCount(series);
x = 0.0;
item = 0;
istart[series] = item;
scanx = true;
while (scanx)
{
if (data instanceof IntervalXYDataset)
{
IntervalXYDataset intervalXYData = (IntervalXYDataset)data;
x = intervalXYData.getStartXValue(series, item).doubleValue();
}
else
{
x = xyData.getXValue(series, item).doubleValue();
}
if (x > startx)
{
istart[series] = item;
scanx = false;
}
item++;
} // end of while (scanRange)

// scan for endx
iend[series] = itemCount;
x = 0;
item=istart[series];
scanx = true;
while (scanx)
{
if (data instanceof IntervalXYDataset)
{
IntervalXYDataset intervalXYData =
(IntervalXYDataset)data;
x = intervalXYData.getStartXValue(series, item).doubleValue();
}
else
{
x = xyData.getXValue(series, item).doubleValue();
}
if (x >= endx || item == iend[series]-1)
{
iend[series] = item;
scanx = false;
}
item++;
} // end of while (scanRange)


} // end of for (series=0; series < seriesCount; series++)

// find y range based on domain specificed
double minimum = Double.POSITIVE_INFINITY;
double maximum = Double.NEGATIVE_INFINITY;
for (series=0; series<seriesCount; series++) {
itemCount = xyData.getItemCount(series);
for (item=istart[series]; item<iend[series]; item++) {

Number lvalue=null;
Number uvalue=null;
if (data instanceof IntervalXYDataset) {
IntervalXYDataset intervalXYData =
(IntervalXYDataset)data;
lvalue = intervalXYData.getStartYValue(series, item);
uvalue = intervalXYData.getEndYValue(series, item);
}
else if (data instanceof HighLowDataset) {
HighLowDataset highLowData = (HighLowDataset)data;
lvalue = highLowData.getLowValue(series, item);
uvalue = highLowData.getHighValue(series, item);
}

else {
lvalue = xyData.getYValue(series, item);
uvalue = lvalue;
}
if (lvalue!=null) {
minimum = Math.min(minimum, lvalue.doubleValue());
}
if (uvalue!=null) {
maximum = Math.max(maximum, uvalue.doubleValue());
}
} // end of for (item=istart[series]; item<iend[series]; item++)
} // end of for (series=0; series < seriesCount; series++)}
if (minimum==Double.POSITIVE_INFINITY) return null;
System.out.println("scaled range [xmin,xmax] ["+minimum+","+maximum+"]");
return new Range(minimum, maximum);
}
else
{
return domain;
} // end of else

}

ChartContext:
contains context information, such as support lines and zoom ranges, that are persisted
across multiple implementations of the a IDynamicMultiChart

IDynamicMultiChart
an interface that allows me to control the configuration of the chart externally
(i.e: I have buttons that allow me to hide / show various subPlots )


Listeners:


public interface IValueListener
{
public void setValue(double value);
}


public interface IRangeListener
{
public void setRange(int plotId, double xmin, double xmax, double ymin, double ymax);

public void setAutoRange(int plotId);

public void setAutoRange();
}




QVOverlaidXYPlot extends OverlaidXYPlot
{
...

public int getNumSubplots()
{
return subplots.size();
}

public XYPlot getSubplot(int ip)
{
return (XYPlot) subplots.get(ip);
}
}

Locked