Automatcially updating PaintScaleLegend, Multiple z ranges

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
paradoxoff
Posts: 1634
Joined: Sat Feb 17, 2007 1:51 pm

Automatcially updating PaintScaleLegend, Multiple z ranges

Post by paradoxoff » Fri May 01, 2009 3:10 pm

JFreeChart allows to create scatter plots where each data point, in addition to the x and y values that define the position of the point in the data area, can have an additional z-value that can be used to dynamically calculate the color or the size of the data item. I have frequently used a combination of an XYZDataset, an XYBlockRenderer and a PaintScaleLegend and encountered situations where these three objects run out of sync.

If the range of z-values of the XYZDataset changes, it can be necessary to adjust the range of the ValueAxis of the PaintScaleLegend. The range of the PaintScale might also need to be updated. At present, an eventual DatasetChangeEvent that is triggered if the range of z-values is changing has to be caught and dispatched by custom code outside the JFreeChart library. This piece of custom code needs to permanently keep or at least temporarily get a reference to the XYZDataset, the renderer and the PaintScaleLegend and might also have to be informed if any of these objects is changed or deleted, all in all a non-trivial task.
If we have accessed all the objects that we need to, the range of the ValueAxis of the PaintScaleLegend can then be adjusted, but an adjustement of the range of the PaintScale is not possible since the current PaintScale interface is “read only” and does not allow to set an upper or lower bound. Finally, there is no declared method that would allow to cache the range of z-values of the XYZDataset. In order to find the range, it is required to loop over the entire dataset.

If the PaintScale of the renderer is exchanged with another PaintScale, the changes should be reflected in the PaintScale of the PaintScaleLegend. Ideally, the same object should be used for both the renderer and the legend. At present, it is not ensured that the PaintScaleLegend and the XYBlockRenderer are indeed using the same z-value <-> paint “encoding”. In fact the PaintScaleLegend and the XYBlockRenderer can hold totally different PaintScales.

If the XYZDataset or the XYBlockRenderer is exchanged with a “normal” XYDataset and e. g. an XYLineAndShapeRenderer, the PaintScaleLegend is no longer necessary. At present the PaintScaleLegend must be explicitly removed or set to invisible. There is no way to “intercept” the drawing if the plot doesn´t contain any XYZDataset and XYBlockRenderers.

All in all, ensuring that a PaintScaleLegend is showing information that fit the content of the XYPlot involves much more manual work that keeping a LegendTitle in sync with the XYPlot contents. In fact, the contents of a LegendTitle never need to be updated manually.

Here are a couple of things that I thought would be helpful:

- A new interface RangePaintScale that extends PaintScale and contains setters for the lower and upper bound.
- A mechanism that ensures that, if the z value range of the XYZDataset changes, both the range of the ValueAxis of the PaintScaleLegend and, if a RangePaintScale is used, the range of the PaintScale are adjusted accordingly.
- A mechanism that ensures that both the XYBlockRenderer and the PaintScaleLegend use the same PaintScale.
- More flexibility with regard to how the z-coordinates are visualized. Instead of using a mapping between z-coordinates and paints, one could also map z-coordinates to symbol sizes. The JFreeChart library contains a renderer (XYBubbleRenderer) that maps z-values to circle sizes, and the sizes are dynamically calculated based on the z-coordinate and on the scaling of the axes. One could also map z-coordinates (between a minimum and a maximum z-value) to a symbol size defined in pixels (between a minimum and a maximum symbol size). In order to make this mapping traceable for the viewer, a specialized legend (“SizeScaleLegend”) would be useful. Such an option would make it even more complicated to keep the XYZDataset, the renderer and the legend in sync. If the XYBlockRenderer is exchanged with an “XYSizeRenderer”, the old “PaintScaleLegend” would have to be deleted and a new “SizeScaleLegend” created automatically. It would be desirable if the library would handle these changes automatically and adjust the legend if the type of the renderer or its properties are changing (similar to a normal LegendTitle).
- Finally: why stop with an XYZDataset? Several commercial packages can handle data sets with more than one z range, i.e. each xy-data point can contain several z coordinates that can be used to dynamically define the rendering of the data point (in addition to the xy-coordinates which define the position in the chart), and both the color and the size (and maybe more properties) are dynamically calculated based on additional z-values. It should not be too difficult to implement a dataset of type “XYZ1Z2”, an appropriate renderer that is a combination of an XYBlockRenderer and an XYBubbleRenderer, and ensure that the renderer and the dataset work together. But in order to make the visual information in the chart traceable for the viewer, two separate “ScaleLegends” are required, one that explains the paint encoding, and another one that explains the size encoding, i. e. more than one legend needs to be attached to a single dataset/renderer combination. Such an option involves additional complication. If the numbers of z-coordinates are reduced, i.e. if the “XYZ1Z2Dataset” is exchanged with an XYZDataset or with a normal XYDataset, the corresponding ScaleLegends that are no longer needed should disappear. And if we have a dataset with two z-ranges, use a renderer that renders the first z-range as a size and the second z-range as a paint, and the change the renderer to make it render the first z-range as a paint and the second z-range as a size, the two “ScaleLegends” would have to change places. To track such changes of the dataset and/or renderer and adjust the legends accordingly can be very complex. It would be desirable to make the library handle that automatically.

Here are some classes that aim at handling these issues:

Code: Select all

public interface org.jfree.chart.MultiZRangeSource
public MultiZRangeDataset getMultiZRangeDataset(int index);
public MultiZRangeRenderer getMultiZRangeRenderer(int index);
public int getDatasetCount();
This interface declares accessor getters for MultiZRangeDatasets and MultiZRangeRenderers. The former holds data in multiple z-ranges, the latter can render these data sets in a plot and can contribute to the visualization of this range in a legend as e.g. color strip or a ruler. XYPlot implements this interface. XYPlot already defines a method getDatasetCount. The implementation of the getMultiZRangeDataset and getMultiZRangeRenderer methods is straightforward. If the renderer or dataset at the given index is indeed an instance of MultiZRangeDataset or MultiZRangeRenderer, it is returned, otherwise null is returned.

Code: Select all

public interface org.jfree.data.general.MultiZRangeDataset.java
public int getZRangeCount();
public Range getZRange(int index);
public String getZRangeName(int index);
This interface is implemented by datasets that contain additional “dimensions” which can be used to not only place a data item at the right position but which can be used for rendering. Every “dimension” or “z range” is associated with a value range and a name. This interface only declares methods that are needed for displaying the possible value range in a legend. It does not declare any methods for accessing the different z values of a given single data point! These methods are declared in subinterfaces of the XYDataset and CategoryDataset interfaces. For rendering of the ZRangeLegend this access to individual z values is not important, only range of values needs to be known.

Code: Select all

public interface org.jfree.chart.renderer.MultiZRangeRenderer
public int getZRangeCount();
public Range getZRange(int index);
public void setZRange(int index, Range range);
public void setZRange(int index, Range range, boolean notify);
public void drawZRangeLegendInfo(Graphics2D g2, Rectangle2D area, RectangleEdge legendPosition, ValueAxis axis, RectangleEdge axisEdge, int rangeIndex);
This interface is similar to the MultiZRangeDataset interface. It declares accessors for the range count and for the range at a given index and also allows to set the range at a given index. This information is important for e. g. setting the bounds of a PaintScale. The drawZRangeLegendInfo method finally performs the drawing of the crucial part of the ZRangeLegend.

Code: Select all

public class org.jfree.chart.title.ZRangeLegend
This code for this class is based on the code for the PaintScaleLegend. The code for drawing the color strip has been removed since a core feature of the ZRangeLegend is that the drawing of color strip or any other “range visualization” is delegated to a MultiZRangeRenderer to make sure that the display in the legend and in the plot are consistent. The ValueAxis is still contained in the ZRangeLegend.
The ZRangeLegend contains a reference to a MultiZRangeSource, a datasetIndex and a rangeIndex, all of which are set in the constructor. By this datasetIndex, the ZRangeLegend can access the MultiZRangeDataset and MultiZRangeRenderer located in the MultiZRangeSource at the given datasetIndex.
As explained above, a “MultiZRangeDataset” and a “MultiZRangeRenderer” can contain multiple z ranges, but each ZRangeLegend can only visualize a single range (the ZRangeLegend only contains a single ValueAxis). The rangeIndex indicates the exact range about which the ZRangeLegend should care.
The ZRangeLegend implements PlotChangeListener. If a PlotChangeEvent is received, the ZRangeLegend accesses the MultiZRangeDataset, req uests the value range and the name of the range, and uses the returned range and the returned name as range and label for its axis. It also sets the returned range as range for the MultiZRangeRenderer, which in turn can use that range and forward it to e. g. a PaintScale. Without a ZRangeLegend, the MultiZRangeRenderer will not be notified if the z range of the MultiZRangeDataset has changed!
In the arrange and draw methods of the ZRangeLegend, it is checked whether the MultiZRangeSource indeed contains a MultiZRangeDataset and a MultiZRangeRenderer at the given datasetIndex, and whether they contain a sufficient number of z ranges. If that is not the case, the arrange method returns a new Size2D(0.0, 0.0) and the draw-method returns null, i.e. the ZRangeLegend does not appear on the chart (similar to a LegendTitle without LegendItems).

Code: Select all

public interface org.jfree.data.xy.MultiZRangeXYDataset extends XYZDataset, MultiZRangeDataset
public Number getZN(int series, int item, int range);
public double getZNValue(int series, int item, int range);
This interface is an extension of the XYZDataset interface and allows an arbitrary number of z-ranges. The accessor methods for the values stored in the z ranges are similar to that of the XYZDataset interface but get an additional “range” parameter that indicates the index of the range. MultiZRangeXYDataset.getZNValue(series, item, 0) should return the same value as MultiZRangeXYDataset.getZValue(series, item) (the latter method is declared in the XYZDataset interface).

Code: Select all

public class org.jfree.data.xy.DefaultMultiZRangeXYDataset extends AbstractXYZDataset implements MultiZRangeDataset.
This class represents a default implementation of the MultiZRangeXYDataset. The code is based on that of the DefaultXYZDataset (i.e. the data values are stored in a double[][] array) but with changes to allow more than a single z range.

Code: Select all

public class org.jfree.chart.renderer.xy.XYSizePaintRenderer extends AbstractXYItemRenderer implements MultiZRangeRenderer.
This class represents an example for a renderer that is able to render data from a dataset with two z ranges. The values for the ranges are either encoded as a Color or as Sizes of circles. Which dimension should be encoded in what form (e. g. z values of the first z range as colors and values of the second z range as sizes or vice versa) can be defined by a list of ZRangeRenderType instances.
If this renderer is used with default settings (ZRangeRenderType.PAINT) with a dataset that has one additional z range, the resulting plot will look like an XYPlot with an XYZDataset and an XYBlockRenderer. Note that this renderer does not draw Rectangles whose size is dependent on the scaling of the axes but simply uses the shapes returned by getItemShape(series, item).
If the range render type of the renderer is changed to ZRangeRenderType.SIZE, the plot looks a little bit like an XYPlot with an XYBubbleRenderer. However, this renderer does not care about the scaling of the axes and determines the sizes of the circles simply by translating the z value in the given z value range to a pixel value between a predefined minimum and maximum pixel size.
This class is also able to draw the relevant parts of a ZRangeLegend.

Finally, an interface RangePaintScale was defined that extends PaintScale and declares methods for setting the lower and upper bound of the PaintScale. An abstract class AbstractRangePaintScale is also included that simply provides storage capacity for the upper and lower bounds. GrayPaintScale was changed and now extends AbstractRangePaintScale. GrayPaintScale should behave as usual, but now the lower and upper bound can not only be set in the constructor but changed afterwards. In order to get a little bit color in the system, an additional AbstractRangePaintScale implementation is provided that maps z values to a range of colors going from dark green over yellow to dark red.
The uploaded file (patch id 2785069) also contains a demo program.

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Re: Automatcially updating PaintScaleLegend, Multiple z ranges

Post by david.gilbert » Sat May 02, 2009 5:11 am

Wow, that looks like a big chunk of work! Thanks! I like the automation for the PaintScaleLegend, and the generalization of the z-values in the dataset (from single to multi-values). It will take me a bit of time to absorb all of this, and play about with your code and the demo. I'd welcome comments from others that try this out too.
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

kirrr
Posts: 3
Joined: Wed Apr 14, 2010 9:27 pm
antibot: No, of course not.

Re: Automatcially updating PaintScaleLegend, Multiple z ranges

Post by kirrr » Mon Jul 26, 2010 2:57 pm

Dear Paradoxoff,

I have stumbled upon this post trying to find a solution to my small problem related to the plotting of the z values. I have a method to create 2D histograms based on an DefaultXYZDataset, an XYBlockRenderer and a PaintScaleLegend as you suggested, and it works fine, thank you! But, I cannot make the crosshair visible on this plot no matter what I try :-(. Have you encountered a similar problem? Here is my code:

Code: Select all

public ChartPanel createHistogramChart(double[][] hist_matrix, String title){
        double zMaxHist = 0.0;
        DefaultXYZDataset waterfallHist = new DefaultXYZDataset();
        for(int gammachannel=0; gammachannel<hist_matrix.length;gammachannel++)
            for(int betachannel=0; betachannel<hist_matrix[0].length;betachannel++)

            {
                if (hist_matrix[gammachannel][betachannel] >= zMaxHist)
                    zMaxHist = hist_matrix[betachannel][gammachannel];
                    waterfallHist.addValue("histogram",
                                        betachannel+1,
                                        gammachannel+1,
                                        hist_matrix[gammachannel][betachannel]);

            }
        NumberAxis xAxis = new NumberAxis("Beta");
        xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
        xAxis.setLowerMargin(0.0);
        xAxis.setUpperMargin(0.0);
        NumberAxis yAxis = new NumberAxis("Gamma");
        yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
        yAxis.setLowerMargin(0.0);
        yAxis.setUpperMargin(0.0);

        XYBlockRenderer renderer = new XYBlockRenderer();
        LookupPaintScale ps = new LookupPaintScale(0,zMaxHist , Color.RED);
        ps.add(0, Color.BLACK);
        for(int i=1; i<(int)zMaxHist ;i++)
         {
              float value = (float)(java.lang.Math.log10(i+1)/java.lang.Math.log10(1.2*(zMaxHist +1)));
              ps.add((double)i, new Color(Color.HSBtoRGB(value, (float).99, (float)0.99)));
         }

        renderer.setPaintScale(ps);
        XYPlot plot = new XYPlot(waterfallHist, xAxis, yAxis, renderer);
        plot.setBackgroundPaint(Color.black);

        plot.setDomainCrosshairStroke(new BasicStroke(1.0f));
        plot.setDomainCrosshairPaint(Color.WHITE);
        plot.setDomainCrosshairVisible(true);
        plot.setDomainCrosshairLockedOnData(true);

        plot.setRangeCrosshairStroke(new BasicStroke(1.0f));
        plot.setRangeCrosshairPaint(Color.WHITE);
        plot.setRangeCrosshairVisible(true);
        plot.setRangeCrosshairLockedOnData(true);
      
        plot.setDomainGridlinesVisible(false);
        plot.setDomainGridlinePaint(Color.black);
        plot.setRangeGridlinePaint(Color.black);
        plot.setRangeGridlinesVisible(false);
        JFreeChart chart = new JFreeChart(title, plot);
        NumberAxis scaleAxis = new NumberAxis("Scale");
        scaleAxis.setUpperBound(zMaxHist );
        scaleAxis.setAxisLinePaint(Color.white);
        scaleAxis.setTickMarkPaint(Color.white);
        scaleAxis.setTickLabelFont(new Font("counts", Font.PLAIN, 12));
        scaleAxis.setAutoTickUnitSelection(true);
        PaintScaleLegend legend = new PaintScaleLegend(ps, scaleAxis);
        legend.setAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
        legend.setPadding(new RectangleInsets(5, 5, 5, 5));
        legend.setStripWidth(50);
        legend.setPosition(RectangleEdge.RIGHT);
        legend.setBackgroundPaint(Color.WHITE);
        chart.addSubtitle(legend);
        chart.removeLegend();
        chart.setBackgroundPaint(Color.white);
        ChartPanel chartPanel = new ChartPanel(chart);
        chartPanel.setMouseZoomable(true, true);
        return chartPanel;
     }
thanks!
Kirill

Locked