Using LinearGradientPaint (java 6, Batik) with a BarRenderer

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Using LinearGradientPaint (java 6, Batik) with a BarRenderer

Post by MattBallard » Fri Aug 24, 2007 8:09 pm

I am having a drawing issues.

How would I approach filling a single bar (renderer.setSeriesPaint() or renderer.setBasePaint()) with a single LinearGradientPaint?

Every time I try to draw it just applies a single gradient to however many bars i am displaying.

Check this link for an example.

http://www.flickr.com/photos/phirstube/1185042399/

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Mon Aug 27, 2007 2:37 pm

should i assume that this is a bug with jfreechart?

Taqua
JFreeReport Project Leader
Posts: 698
Joined: Fri Mar 14, 2003 3:34 pm
Contact:

Post by Taqua » Mon Aug 27, 2007 3:01 pm

well, simple: If JFreeChart exports to PNG correctly, draws to the screen correctly, then we can safely assume that JFreeChart is not the cause of your problems.

JFreeChart makes no use of any special features of Batik. However, JFreeChart assumes that Batik obeys to the Graphics2D contract spelled out by the reference implementation (also known as AWT). So if JFreeChart rendering on the AWT behaves correctly, then the implementor of the third-party Graphics2D context is to blame.

In your case: File a bug report at the Batik-project.

Have fun,
said Thomas

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

Post by david.gilbert » Tue Aug 28, 2007 11:32 am

MattBallard wrote:should i assume that this is a bug with jfreechart?
Maybe, LinearGradientPaint is a new class in Java 6, and since JFreeChart is supporting Java 1.3 upwards, we don't have any code that specifically recognises this gradient paint. Potentially we could modify the StandardGradientPaintTransformer to use reflection and handle this as a special case...I'll look into it.
David Gilbert
JFreeChart Project Leader

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

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Tue Aug 28, 2007 1:07 pm

david.gilbert wrote:
MattBallard wrote:should i assume that this is a bug with jfreechart?
Maybe, LinearGradientPaint is a new class in Java 6, and since JFreeChart is supporting Java 1.3 upwards, we don't have any code that specifically recognises this gradient paint. Potentially we could modify the StandardGradientPaintTransformer to use reflection and handle this as a special case...I'll look into it.
I appreciate you looking into this possibility. Please keep me posted.

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Thu Aug 30, 2007 3:42 pm

i can look into this, but am unsure on what mechanism allows the GradientPaint class to fill the bars seperately.

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

Post by david.gilbert » Thu Aug 30, 2007 4:37 pm

Only some renderers support the transform, for example BarRenderer uses the following code (in the drawItem() method) to detect and transform a GradientPaint:

Code: Select all

        Paint itemPaint = getItemPaint(row, column);
        GradientPaintTransformer t = getGradientPaintTransformer();
        if (t != null && itemPaint instanceof GradientPaint) {
            itemPaint = t.transform((GradientPaint) itemPaint, bar);
        }
        g2.setPaint(itemPaint);
        g2.fill(bar);
The existing paint plus a shape ('bar' in the code) is passed to the GradientPaintTransformer, which returns a new GradientPaint instance with coordinates modified in some way to fit the shape that has been passed in.

This same mechanism could be extended to handle other types of gradient paint...but to include it in the standard JFreeChart release, it would have to compile and run using JDK 1.3, so the code would need to use reflection to detect the new LinearGradientPaint class, and fail gracefully on Java 5 and lower.
David Gilbert
JFreeChart Project Leader

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

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Thu Aug 30, 2007 5:55 pm

My specialized DiagPaint extends GradientPaint which implements Paint (see below)

If you throw this inside a BarRenderer using the setPaint function it shows the bars as a gradient that is only two colors.

Inside a Java JPanel it displays the gradient correctly (all three colors)


Corrected Code as follows:

Code: Select all

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;
 
public class Revisited extends JPanel implements ActionListener {
    boolean showGradient = true;
 
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        int w = getWidth();
        int h = getHeight();
        if(showGradient) {
            g2.setPaint(new DiagPaint(0,     h, Color.red,
                                      w/2, h/2, Color.yellow,
                                      w,     0, Color.green.darker()));
            g2.fillRect(0,0,w,h);
        } else {
            int x1 = 0,   y1 = h-1;
            int x2 = w/2, y2 = h/2;
            int x3 = w-1, y3 = 0;
 
            int[] xpoints = { x1, x1, x2 };
            int[] ypoints = { y2, y1, y1 };
            g2.draw(new Polygon(xpoints, ypoints, 3));    // sw corner
  
            xpoints = new int[] { x1, x2, x3, x1 };
            ypoints = new int[] { y2, y1, y1, y3 };
            g2.draw(new Polygon(xpoints, ypoints, 4));    // sw strip
 
            xpoints = new int[] { 0, x3, x3, x2 };
            ypoints = new int[] { 0, y1, y2, y3 };
            g2.draw(new Polygon(xpoints, ypoints, 4));    // ne strip
  
            xpoints = new int[] { x2, x3, x3 };
            ypoints = new int[] { y3, y2, y3 };
            g2.draw(new Polygon(xpoints, ypoints, 3));    // ne corner
 
            markColorOrigin(x1, y1, Color.red, g2);
            markColorOrigin(x2, y2, Color.yellow, g2);
            markColorOrigin(x3, y3, Color.green.darker(), g2);
        }
    }
 
    private void markColorOrigin(int x, int y, Color color, Graphics2D g2) {
        g2.setPaint(color);
        g2.fill(new Ellipse2D.Double(x-10, y-10, 20, 20));
    }
 
    public void actionPerformed(ActionEvent e) {
        String ac = e.getActionCommand();
        showGradient = Boolean.parseBoolean(ac);
        repaint();
    }
 
    private JPanel getLast() {
        String[] ids = { "gradient", "strategy" };
        ButtonGroup group = new ButtonGroup();
        JPanel panel = new JPanel();
        for(int j = 0; j < ids.length; j++) {
            JRadioButton rb = new JRadioButton(ids[j],j==0);
            rb.setActionCommand((j==0) ? "true" : "false");
            rb.addActionListener(this);
            group.add(rb);
            panel.add(rb);
        }
        return panel;
    }
 
    public static void main(String[] args) {
        Revisited test = new Revisited();
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(test);
        f.getContentPane().add(test.getLast(), "Last");
        f.setSize(500,400);
        f.setLocation(200,200);
        f.setVisible(true);
    }
}
 
class DiagContext implements PaintContext {
    float x1, y1;
    float x2, y2;
    float x3, y3;
    Color color1;
    Color color2;
    Color color3;
    Polygon swc, sws, nes, nec;
    Line2D.Double diagonalLine;
    double parallelDist;
    Map<Integer,Double> lookup;
 
    public DiagContext(float x1, float y1, Color color1,
                       float x2, float y2, Color color2,
                       float x3, float y3, Color color3) {
        this.x1 = x1;   this.y1 = y1;   this.color1 = color1;
        this.x2 = x2;   this.y2 = y2;   this.color2 = color2;
        this.x3 = x3;   this.y3 = y3;   this.color3 = color3;
 
        int[] xpoints = { (int)x1, (int)x1, (int)x2 };
        int[] ypoints = { (int)y2, (int)y1, (int)y1 };
        swc = new Polygon(xpoints, ypoints, 3);     // sw corner
 
        xpoints = new int[] { (int)x1, (int)x2, (int)x3, (int)x1 };
        ypoints = new int[] { (int)y2, (int)y1, (int)y1, (int)y3 };
        sws = new Polygon(xpoints, ypoints, 4);     // sw strip
 
        xpoints = new int[] { 0, (int)x3, (int)x3, (int)x2 };
        ypoints = new int[] { 0, (int)y1, (int)y2, (int)y3 };
        nes = new Polygon(xpoints, ypoints, 4);     // ne strip
 
        xpoints = new int[] { (int)x2, (int)x3, (int)x3 };
        ypoints = new int[] { (int)y3, (int)y2, (int)y3 };
        nec = new Polygon(xpoints, ypoints, 3);     // ne corner
 
        diagonalLine = new Line2D.Double(x1, y3, x3, y1);
        // Orthogonal distance between parallel lines.
        parallelDist = diagonalLine.ptLineDist(x1, y2);
        createLookup();
    }
 
    public void dispose() {}
 
    public ColorModel getColorModel() { return ColorModel.getRGBdefault(); }
 
    public Raster getRaster(int x, int y, int w, int h) {
        WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h);
        int[] data = new int[w * h * 4];
        Color start = color1;
        Color end   = color1;
        double distance = 1.0, total = 1.0;
        for(int k = 0; k < h; k++) {
            for(int j = 0; j < w; j++) {
                if(swc.contains(x+j, y+k)) {
                    double dy = (y+k) - y1;
                    double dx = (x+j) - x1;
                    distance = Math.sqrt(dx*dx + dy*dy);
                    int theta = (int)Math.round(Math.toDegrees(Math.atan2(dy,dx)));
                    Integer key = Integer.valueOf(theta);
                    total = ((Double)lookup.get(key)).doubleValue();
                    start = color1;
                    end = getBlend(color1, color2);
                } else if(sws.contains(x+j, y+k)) {
                    distance = diagonalLine.ptSegDist((x+j), (y+k));
                    total = parallelDist;
                    start = color2;
                    end = getBlend(color1, color2);
                } else if(nes.contains(x+j, y+k)) {
                    distance = diagonalLine.ptSegDist((x+j), (y+k));
                    total = parallelDist;
                    start = color2;
                    end = getBlend(color2, color3);
                } else if(nec.contains(x+j, y+k)) {
                    double dy = (y+k);
                    double dx = (x+j) - x3;
                    distance = Math.sqrt(dx*dx + dy*dy);
                    int theta =
                        (int)Math.round(Math.toDegrees(Math.atan2(dy,dx)-Math.PI));
                    Integer key = Integer.valueOf(theta);
                    total = ((Double)lookup.get(key)).doubleValue();
                    start = color3;
                    end = getBlend(color2, color3);
                }
 
                double ratio = distance / total;
                if(ratio > 1.0)
                    ratio = 1.0;
 
                int base = (k * w + j) * 4;
                data[base + 0] = (int)(start.getRed() + ratio *
                                      (end.getRed() - start.getRed()));
                data[base + 1] = (int)(start.getGreen() + ratio *
                                      (end.getGreen() - start.getGreen()));
                data[base + 2] = (int)(start.getBlue() + ratio *
                                      (end.getBlue() - start.getBlue()));
                data[base + 3] = (int)(start.getAlpha() + ratio *
                                      (end.getAlpha() - start.getAlpha()));
            }
        }
        raster.setPixels(0, 0, w, h, data);
        return raster;
    }
 
    private Color getBlend(Color c1, Color c2) {
        int r = (c1.getRed() + c2.getRed())/2;
        int g = (c1.getGreen() + c2.getGreen())/2;
        int b = (c1.getBlue() + c2.getBlue())/2;
        return new Color(r, g, b);
    }
 
    /** Distance from corner to closest line for [0 - 90] degrees of arc. */
    private void createLookup() {
        lookup = new HashMap<Integer,Double>();
        double length = Math.sqrt(x2*x2 + y2*y2);
        Line2D.Double fixed = new Line2D.Double(x1,y2,x2,y1);
        // Clockwise from -90 (12 o'clock) by degree -90 -> 0.
        for(int j = -90; j <= 0; j++) {
            double theta = Math.toRadians(j);
            double x = x1 + length*Math.cos(theta);
            double y = y1 + length*Math.sin(theta);
            Line2D.Double sweep = new Line2D.Double(x1,y1,x,y);
            Point2D.Double p = getIntersection(fixed, sweep);
            double dx = x1 - p.x;
            double dy = y1 - p.y;
            double distance = Math.sqrt(dx*dx + dy*dy);
            lookup.put(Integer.valueOf(j), Double.valueOf(distance));
        }
    }
 
    private Point2D.Double getIntersection(Line2D.Double line1, Line2D.Double line2) {
        double x1 = line1.getX1();
        double y1 = line1.getY1();
        double x2 = line1.getX2();
        double y2 = line1.getY2();
        double x3 = line2.getX1();
        double y3 = line2.getY1();
        double x4 = line2.getX2();
        double y4 = line2.getY2();
        double aDividend = (x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3);
        double aDivisor  = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1);
        double ua = aDividend / aDivisor;
        double bDividend = (x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3);
        double bDivisor  = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1);
        double ub = bDividend / bDivisor;
        Point2D.Double p = new Point2D.Double();
        p.x = x1 + ua * (x2 - x1);
        p.y = y1 + ua * (y2 - y1);
        return p;
    }
}
 
class DiagPaint extends GradientPaint {
    float x1, y1;
    float x2, y2;
    float x3, y3;
    Color color1;
    Color color2;
    Color color3;
 
    public DiagPaint(float x1, float y1, Color color1,
                     float x2, float y2, Color color2,
                     float x3, float y3, Color color3) {
        super(x1,y1,color1,x2,y2,color2);
        this.x1 = x1;   this.y1 = y1;   this.color1 = color1;
        this.x2 = x2;   this.y2 = y2;   this.color2 = color2;
        this.x3 = x3;   this.y3 = y3;   this.color3 = color3;
        checkGeometry();
    }
 
    public PaintContext createContext(ColorModel cm,
                                      Rectangle deviceBounds,
                                      Rectangle2D userBounds,
                                      AffineTransform xform,
                                      RenderingHints hints) {
        return new DiagContext(x1, y1, color1,
                               x2, y2, color2,
                               x3, y3, color3);
    }
 
    public int getTransparency() {
        int a1 = color1.getAlpha();
        int a2 = color2.getAlpha();
        return (((a1 & a2) == 0xff) ? OPAQUE : TRANSLUCENT);
    }
 
    private void checkGeometry() {
        if(Math.abs((x3 - x2) - (x2 - x1)) > 1f ||
           Math.abs((y3 - y2) - (y2 - y1)) > 1f) {
            throw new IllegalArgumentException("First point must be northwest " +
                             "with second in center and third southeast.");
        }
    }
}

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Thu Aug 30, 2007 6:53 pm

I have verified this behavior with both 1.0.3 and 1.0.6 versions of JfreeChart

Taqua
JFreeReport Project Leader
Posts: 698
Joined: Fri Mar 14, 2003 3:34 pm
Contact:

Post by Taqua » Thu Aug 30, 2007 7:13 pm

It seems as if your implementation is totally ignoring the bounds and affine-transform it got when DiagPaint.createContext(..) gets called. This is very likely to cause your troubles, as your code will never work for scaled and translated operations.

Code: Select all

public class Revisited extends JPanel implements ActionListener
{
  boolean showGradient = true;

  protected void paintComponent(Graphics g)
  {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    int w = getWidth();
    int h = getHeight();
    if (showGradient)
    {
      g2.setPaint(new DiagPaint(0, h, Color.red,
          w / 2, h / 2, Color.yellow,
          w, 0, Color.green.darker()));
      final Rectangle2D rect = new Rectangle2D.Double(0,0,100,100);
      g2.fill(rect);
      g2.translate(200, 100);
      g2.fill(rect);
      g2.translate(200, 100);
      g2.scale(2, 1.5);
      g2.fill(rect);

    }
    else
    {
      int x1 = 0, y1 = h - 1;
      int x2 = w / 2, y2 = h / 2;
      int x3 = w - 1, y3 = 0;

      int[] xpoints = {x1, x1, x2};
      int[] ypoints = {y2, y1, y1};
      g2.draw(new Polygon(xpoints, ypoints, 3));    // sw corner

      xpoints = new int[]{x1, x2, x3, x1};
      ypoints = new int[]{y2, y1, y1, y3};
      g2.draw(new Polygon(xpoints, ypoints, 4));    // sw strip

      xpoints = new int[]{0, x3, x3, x2};
      ypoints = new int[]{0, y1, y2, y3};
      g2.draw(new Polygon(xpoints, ypoints, 4));    // ne strip

      xpoints = new int[]{x2, x3, x3};
      ypoints = new int[]{y3, y2, y3};
      g2.draw(new Polygon(xpoints, ypoints, 3));    // ne corner

      markColorOrigin(x1, y1, Color.red, g2);
      markColorOrigin(x2, y2, Color.yellow, g2);
      markColorOrigin(x3, y3, Color.green.darker(), g2);
    }
  }

  private void markColorOrigin(int x, int y, Color color, Graphics2D g2)
  {
    g2.setPaint(color);
    g2.fill(new Ellipse2D.Double(x - 10, y - 10, 20, 20));
  }

  public void actionPerformed(ActionEvent e)
  {
    String ac = e.getActionCommand();
    showGradient = Boolean.parseBoolean(ac);
    repaint();
  }

  private JPanel getLast()
  {
    String[] ids = {"gradient", "strategy"};
    ButtonGroup group = new ButtonGroup();
    JPanel panel = new JPanel();
    for (int j = 0; j < ids.length; j++)
    {
      JRadioButton rb = new JRadioButton(ids[j], j == 0);
      rb.setActionCommand((j == 0) ? "true" : "false");
      rb.addActionListener(this);
      group.add(rb);
      panel.add(rb);
    }
    return panel;
  }

  public static void main(String[] args)
  {
    Revisited test = new Revisited();
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.getContentPane().add(test);
    f.getContentPane().add(test.getLast(), "Last");
    f.setSize(500, 400);
    f.setLocation(200, 200);
    f.setVisible(true);
  }
}
The (slightly changed) code above exposes the same behaviour as your image of the chart. All I did here, was to change the simple unscaled draw operation into a slightly more complex drawing with some scaling and translation. However, your paint implementation still draws the same gradient as if I would have requested a huge rectangle. I assume the output of this test-programm is not what you expected from your paint ;)

Have fun,
said Thomas

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Thu Aug 30, 2007 7:23 pm

That is what I was afraid of. That is why I was considering LinearGradientPaint as a alternative.

I wish I understood better how to apply affine-transform.

Until then, I will keep shooting in the dark.

Thanks for your comments.

MattBallard
Posts: 29
Joined: Wed Jun 06, 2007 7:41 pm

Post by MattBallard » Thu Aug 30, 2007 11:03 pm

This is my new approach:

Since my paintContext does not take into consideration bounds and affine transforms. I have decided to make my createContext call LinearGradientPaints Context. I have also made my TriPaint extend Gradient Paint.

Please note I am using Apache Batik's version of LinearGradientPaint and not Java 6.

Code: Select all

public class TriGradientPaint extends GradientPaint
inside my constructor it looks like this:

Code: Select all

public TriGradientPaint(Point2D start, Point2D end, float[] fractions, Color[] colors) {
        super((float)start.getX(), (float)start.getY(), colors[1], (float)end.getX(), (float)end.getY(), colors[2]);
        this.colors = colors;
        LinearGradientPaint lgp = new LinearGradientPaint(start,end,fractions,colors);
inside my paintContext method i simply call:

Code: Select all

public PaintContext createContext(ColorModel cm,
                                      Rectangle deviceBounds,
                                      Rectangle2D userBounds,
                                      AffineTransform xform,
                                      RenderingHints hints) {
        
        return lgp.createContext( cm,
                                                    deviceBounds,
                                                    userBounds,
                                                    xform,
                                                    hints);
Everthing compiles fine, however when my gradient is returned it looks exactly like a GradientPaint. I figured out why. My createContext is NEVER getting called.

I started checking the stack and noticed that BarRenderer.drawItem never calls my paintContext method. Kinda strange behavior. If I was working in C, I would have chalked it up to slicing. Am I overlooking something? Do I need to override the drawItem in order to get it to call my createContext method?

Locked