Search code examples
javaplotjfreechart

How can I share DomainAxis/RangeAxis across subplots without drawing them on each plot?


Ok, I've been hacking at this for almost all day with no success. I'm trying to use JFreeChart to create a grid of XYPlots where the domain and range axes are linked for each column and row of plots, respectively. That is to say, the plots in the same row have the same range axis range, and the plots in a column have the same domain axis range.

I was able to achieve the functionality by using a hacked CombinedDomainXYPlot of CombinedRangeXYPlots of XYPlots. Basically I made some XYPlot objects and added them to CombinedRangeXYPlot objects, then added those CombinedRangeXYPlot objects to an instance of CombinedDomainXYPlot that doesnt draw a domain axis. (Maybe there is another way to stack plots instead of CombinedDomainXYPlot, since I'm not using the combined domain axis functionality.)

The ranges scale together for each row, as expected. By adding the same domain axis to each subplot in a column, I was able to get the domains to scale together for each column. Result is shown below. enter image description here

I have two problems right now - first, I would like to get rid of the axis labels below each row and just have them on the bottom, but keep the scales linked.

Second, the labels for the range axes are of the edge of the window - how do I get them back?

And, in general, I would like to understand how CombinedRangeXYPlot and CombinedRangeXYPlot use the same axis range for multiple plots without drawing the axes below each plot.

EDIT: Here is the code for a working demo:

Main class

public class GridBlockPlotFrameExample {

  private final JFrame frame;
  private final XYPlot[][] phiPhiPlots;
  private final XYPlot[] phiDPlots;

  public GridBlockPlotFrameExample() {
    frame = new JFrame("Density Plot");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    phiDPlots = new XYPlot[4];
    phiPhiPlots = new XYPlot[4][4];

    createSubPlots();
    CombinedRangeXYPlot[] rowPlots = new CombinedRangeXYPlot[phiDPlots.length + 1];
    for (int i = 0; i < phiPhiPlots.length; i++) {
      rowPlots[i + 1] = new CombinedRangeXYPlot();
      for (int j = 0; j < phiPhiPlots[i].length; j++) {
        if (phiPhiPlots[i][j] != null) {
          rowPlots[i + 1].add(phiPhiPlots[i][j]);
        } else {
          rowPlots[i + 1].add(new XYPlot());
        }
      }
    }
    rowPlots[0] = new CombinedRangeXYPlot();
    for (XYPlot phiDPlot : phiDPlots) {
      rowPlots[0].add(phiDPlot);
    }

    StackedXYPlot gridPlot = new StackedXYPlot();

    for (int i = rowPlots.length - 1; i >= 1; i--) {
      XYPlot rowPlot = rowPlots[i];
      gridPlot.add(rowPlot, 2);
    }
    gridPlot.add(rowPlots[0], 1);

    JFreeChart chart = new JFreeChart("gridplot", JFreeChart.DEFAULT_TITLE_FONT, gridPlot, false);
    chart.setBackgroundPaint(Color.WHITE);

    ChartPanel panel = new ChartPanel(chart);
    panel.setPreferredSize(new Dimension(300, 300));
    panel.setMouseWheelEnabled(false);
    panel.setRangeZoomable(true);
    panel.setDomainZoomable(true);

    frame.setContentPane(panel);
    frame.pack();

    RefineryUtilities.centerFrameOnScreen(frame);
  }

  private void createSubPlots() {
    for (int i = 0; i < phiDPlots.length; i++) {
      phiDPlots[i] = createPlot(createDataset());
    }
    XYPlot tempPlot;
    for (int i = 0; i < phiPhiPlots.length; i++) {
      for (int j = 0; j < phiPhiPlots.length; j++) {
        tempPlot = createPlot(createDataset());
        phiPhiPlots[j][i] = tempPlot; // (sic) YES this inversion is intentional
        tempPlot.setDomainAxis((NumberAxis) phiDPlots[i].getDomainAxis());
      }
    }
  }

  private XYPlot createPlot(XYZDataset data) {
    NumberAxis xAxis = new NumberAxis("X");
    xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
    xAxis.setLowerMargin(0.0);
    xAxis.setUpperMargin(0.0);
    xAxis.setAxisLinePaint(Color.white);
    xAxis.setTickMarkPaint(Color.white);
    NumberAxis yAxis = new NumberAxis("Y");
    yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
    yAxis.setLowerMargin(0.0);
    yAxis.setUpperMargin(0.0);
    yAxis.setAxisLinePaint(Color.white);
    yAxis.setTickMarkPaint(Color.white);
    XYBlockRenderer renderer = new XYBlockRenderer();
    PaintScale scale = new GrayPaintScale(-2.0, 1.0);
    renderer.setPaintScale(scale);
    XYPlot plot = new XYPlot(data, xAxis, yAxis, renderer);
    plot.setBackgroundPaint(Color.lightGray);
    plot.setDomainGridlinesVisible(false);
    plot.setRangeGridlinePaint(Color.white);
    plot.setAxisOffset(new RectangleInsets(5, 5, 5, 5));
    plot.setOutlinePaint(Color.blue);
    return plot;
  }

  private XYZDataset createDataset() {
    return new XYZDataset() {

      @Override
      public int getSeriesCount() {
        return 1;
      }

      @Override
      public int getItemCount(int series) {
        return 10000;
      }

      @Override
      public Number getX(int series, int item) {
        return new Double(getXValue(series, item));
      }

      @Override
      public double getXValue(int series, int item) {
        return item / 100 - 50;
      }

      @Override
      public Number getY(int series, int item) {
        return new Double(getYValue(series, item));
      }

      @Override
      public double getYValue(int series, int item) {
        return item - (item / 100) * 100 - 50;
      }

      @Override
      public Number getZ(int series, int item) {
        return new Double(getZValue(series, item));
      }

      @Override
      public double getZValue(int series, int item) {
        double x = getXValue(series, item);
        double y = getYValue(series, item);
        return Math.sin(Math.sqrt(x * x + y * y) / 5.0);
      }

      @Override
      public void addChangeListener(DatasetChangeListener listener) {
        // ignore - this dataset never changes
      }

      @Override
      public void removeChangeListener(DatasetChangeListener listener) {
        // ignore
      }

      @Override
      public DatasetGroup getGroup() {
        return null;
      }

      @Override
      public void setGroup(DatasetGroup group) {
        // ignore
      }

      @Override
      public Comparable getSeriesKey(int series) {
        return "sin(sqrt(x + y))";
      }

      @Override
      public int indexOf(Comparable seriesKey) {
        return 0;
      }

      @Override
      public DomainOrder getDomainOrder() {
        return DomainOrder.ASCENDING;
      }
    };
  }


  public void show() {
    frame.setVisible(true);
  }

  public static void main(String[] args) {
    GridBlockPlotFrameExample example = new GridBlockPlotFrameExample();
    example.show();
  }
}

StackedXYPlot class

public class StackedXYPlot extends CombinedDomainXYPlot {

  public StackedXYPlot() {
    super(null);
  }

  @Override
  public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, PlotState parentState,
      PlotRenderingInfo info) {

    // set up info collection...
    if (info != null) {
      info.setPlotArea(area);
    }

    // adjust the drawing area for plot insets (if any)...
    RectangleInsets insets = getInsets();
    insets.trim(area);

    setFixedRangeAxisSpaceForSubplots(null);
    AxisSpace space = calculateAxisSpace(g2, area);
    Rectangle2D dataArea = space.shrink(area, null);

    // set the width and height of non-shared axis of all sub-plots
    setFixedRangeAxisSpaceForSubplots(space);

    // draw all the subplots
    for (int i = 0; i < getSubplots().size(); i++) {
      XYPlot plot = (XYPlot) getSubplots().get(i);
      PlotRenderingInfo subplotInfo = null;
      if (info != null) {
        subplotInfo = new PlotRenderingInfo(info.getOwner());
        info.addSubplotInfo(subplotInfo);
      }
      plot.draw(g2, this.subplotAreas[i], anchor, parentState, subplotInfo);
    }

    if (info != null) {
      info.setDataArea(dataArea);
    }
  }

  public int findSubplotIndex(PlotRenderingInfo info, Point2D source) {
    ParamChecks.nullNotPermitted(info, "info");
    ParamChecks.nullNotPermitted(source, "source");
    XYPlot result = null;
    return info.getSubplotIndex(source);
  }

  /**
   * Multiplies the range on the range axis/axes by the specified factor.
   *
   * @param factor the zoom factor.
   * @param info the plot rendering info (<code>null</code> not permitted).
   * @param source the source point (<code>null</code> not permitted).
   */
  @Override
  public void zoomDomainAxes(double factor, PlotRenderingInfo info, Point2D source) {
    zoomDomainAxes(factor, info, source, false);
  }

  /**
   * Multiplies the range on the range axis/axes by the specified factor.
   *
   * @param factor the zoom factor.
   * @param state the plot state.
   * @param source the source point (in Java2D coordinates).
   * @param useAnchor use source point as zoom anchor?
   */
  @Override
  public void zoomDomainAxes(double factor, PlotRenderingInfo state, Point2D source,
      boolean useAnchor) {
    // delegate 'state' and 'source' argument checks...
    int subplotIndex = findSubplotIndex(state, source);
    XYPlot subplot = null;
    if (subplotIndex >= 0) {
      subplot = (XYPlot) getSubplots().get(subplotIndex);
    }
    if (subplot != null) {
      subplot.zoomDomainAxes(factor, state.getSubplotInfo(subplotIndex), source, useAnchor);
    } else {
      // if the source point doesn't fall within a subplot, we do the
      // zoom on all subplots...
      Iterator iterator = getSubplots().iterator();
      while (iterator.hasNext()) {
        subplot = (XYPlot) iterator.next();
        subplot.zoomDomainAxes(factor, state, source, useAnchor);
      }
    }
  }


  /**
   * Zooms in on the range axes.
   *
   * @param lowerPercent the lower bound.
   * @param upperPercent the upper bound.
   * @param info the plot rendering info (<code>null</code> not permitted).
   * @param source the source point (<code>null</code> not permitted).
   */
  @Override
  public void zoomDomainAxes(double lowerPercent, double upperPercent, PlotRenderingInfo info,
      Point2D source) {
    // delegate 'info' and 'source' argument checks...
    int subplotIndex = findSubplotIndex(info, source);
    XYPlot subplot = null;
    if (subplotIndex >= 0) {
      subplot = (XYPlot) getSubplots().get(subplotIndex);
    }
    if (subplot != null) {
      subplot.zoomDomainAxes(lowerPercent, upperPercent, info.getSubplotInfo(subplotIndex), source);
    } else {
      // if the source point doesn't fall within a subplot, we do the
      // zoom on all subplots...
      Iterator iterator = getSubplots().iterator();
      while (iterator.hasNext()) {
        subplot = (XYPlot) iterator.next();
        subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source);
      }
    }
  }

  /**
   * Multiplies the range on the range axis/axes by the specified factor.
   *
   * @param factor the zoom factor.
   * @param info the plot rendering info (<code>null</code> not permitted).
   * @param source the source point (<code>null</code> not permitted).
   */
  @Override
  public void zoomRangeAxes(double factor, PlotRenderingInfo info, Point2D source) {
    zoomRangeAxes(factor, info, source, false);
  }

  /**
   * Multiplies the range on the range axis/axes by the specified factor.
   *
   * @param factor the zoom factor.
   * @param state the plot state.
   * @param source the source point (in Java2D coordinates).
   * @param useAnchor use source point as zoom anchor?
   */
  @Override
  public void zoomRangeAxes(double factor, PlotRenderingInfo state, Point2D source,
      boolean useAnchor) {
    // delegate 'state' and 'source' argument checks...
    int subplotIndex = findSubplotIndex(state, source);
    XYPlot subplot = null;
    if (subplotIndex >= 0) {
      subplot = (XYPlot) getSubplots().get(subplotIndex);
    }
    if (subplot != null) {
      subplot.zoomRangeAxes(factor, state.getSubplotInfo(subplotIndex), source, useAnchor);
    } else {
      // if the source point doesn't fall within a subplot, we do the
      // zoom on all subplots...
      Iterator iterator = getSubplots().iterator();
      while (iterator.hasNext()) {
        subplot = (XYPlot) iterator.next();
        subplot.zoomRangeAxes(factor, state, source, useAnchor);
      }
    }
  }

  /**
   * Zooms in on the range axes.
   *
   * @param lowerPercent the lower bound.
   * @param upperPercent the upper bound.
   * @param info the plot rendering info (<code>null</code> not permitted).
   * @param source the source point (<code>null</code> not permitted).
   */
  @Override
  public void zoomRangeAxes(double lowerPercent, double upperPercent, PlotRenderingInfo info,
      Point2D source) {
    // delegate 'info' and 'source' argument checks...
    int subplotIndex = findSubplotIndex(info, source);
    XYPlot subplot = null;
    if (subplotIndex >= 0) {
      subplot = (XYPlot) getSubplots().get(subplotIndex);
    }
    if (subplot != null) {
      subplot.zoomRangeAxes(lowerPercent, upperPercent, info.getSubplotInfo(subplotIndex), source);
    } else {
      // if the source point doesn't fall within a subplot, we do the
      // zoom on all subplots...
      Iterator iterator = getSubplots().iterator();
      while (iterator.hasNext()) {
        subplot = (XYPlot) iterator.next();
        subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
      }
    }
  }

}

enter image description hereenter image description here

I believe the only other thing I had to do to get the StackedXYPlot to work is change visibility of CombinedDomainXYPlot.subplotAreas to protected.

I noticed with this example that the mouse zoom of the domain axis is off - but it does propagate to the other plots in the column.

Thanks,

Igor

P.S. the reason I want to eliminate the plots below the plot is because in the end I need to plot at least a 6x7 grid of plots and with that many, the labels take up most of the space.

Edit: I have accepted Eric's answer as functional, but I am working on a less hackish way of doing it - How CombinedDomainXYPlot and CombinedRangeXYPlot share Axis information with subplots. I'll update there if I get it completely functional.


Solution

  • That's really a tough one...

    This is how close I got, starting from your code:

    1. Set the domain axes invisible:

      ValueAxis a = phiDPlots[i].getDomainAxis();
      a.setVisible(false);
      tempPlot.setDomainAxis((NumberAxis) phiDPlots[i].getDomainAxis());
      
    2. Set the subplot's domain axes visible if drawing the last row:

      // draw all the subplots
      for (int i = 0; i < this.getSubplots().size(); i++) {
          CombinedRangeXYPlot plot = (CombinedRangeXYPlot) this.getSubplots().get(i);
          PlotRenderingInfo subplotInfo = null;
          if (info != null) {
              subplotInfo = new PlotRenderingInfo(info.getOwner());
              info.addSubplotInfo(subplotInfo);
          }
      
          if(i==getSubplots().size()-1){  // If the last row
              for(int j=0; j < plot.getSubplots().size(); j++)
                  ((XYPlot)plot.getSubplots().get(j)).getDomainAxis().setVisible(true);
          }
      
          plot.draw(g2, this.subplotAreas[i], anchor, parentState, subplotInfo);
      
          if(i==getSubplots().size()-1){  // If the last row
              for(int j=0; j < plot.getSubplots().size(); j++)
                  ((XYPlot)plot.getSubplots().get(j)).getDomainAxis().setVisible(false);
          }
      }
      

    This works, but somehow only after a refresh/resize of the window, because the last row of graphs is too compressed vertically...

    enter image description here