Search code examples
javaswingdrawpie-chartpaintcomponent

Draw PieChart with triangle in the middle of PieChart slice


I want to draw a piechart with a triangle in the middle of the piechart slice. At the moment I draw a piechat with slices and triangles in the middle of the slices, but the triangles are not in the right angle. I need to know how to position the triangles in the right way. My Code and the result:

import java.awt.*;
import java.awt.geom.Ellipse2D;
import javax.swing.*;

class Slice {

   double value;
   Color color;
   public Slice(double value, Color color) {  
      this.value = value;
      this.color = color;
   }
}

class PieChart extends JPanel {

    private Color a = Color.RED;
    private Color b = Color.BLUE;
    private Color c = Color.YELLOW;
    Slice[] slices = { 
               new Slice(60, a),
               new Slice(100, b),
               new Slice(200, c)
    };

    public PieChart(){

    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D)g;
        super.paintComponent(g2d);
        this.setBackground(new Color(255,255,255));

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        double total = 0.0D;
        for (int i = 0; i < slices.length; i++) {
            total += slices[i].value;
        }

        double curValue = 90.0D;
        int startAngle = 0;

        for (int i = 0; i < slices.length; i++) {
            startAngle = (int) (curValue * 360 / total);
            int arcAngle = (int) (slices[i].value * 360 / total);
            g2d.setColor(slices[i].color);
            g2d.fillArc(20, 20, 200, 200, startAngle, arcAngle);

            g2d.setPaint(Color.BLACK);
            int x = (int)(110+100*Math.cos(((-(startAngle+(arcAngle/2)))*Math.PI)/180));
            int y = (int)(110+100*Math.sin(((-(startAngle+(arcAngle/2)))*Math.PI)/180));

            Polygon p = new Polygon(new int[] {x, x+14, x+7}, new int[] {y, y, y-14}, 3); // this values seems to be important
            g2d.draw(p);
            g2d.fill(p);

            curValue += slices[i].value;
        }
    }
}

enter image description here

Edit: should look like this:

enter image description here


Solution

  • I made the first arc to start from 0 o'clock (I think you meant to do this).

    Since you are using fillArc which takes ints, the rounded down doubles might not add up to the full amount and you will have gaps between the slices:

    enter image description here

    Instead, use Arc2D.Double to get better precision:

    enter image description here

    class Slice {
    
        double value;
        Color color;
    
        public Slice(double value, Color color) {
    
            this.value = value;
            this.color = color;
        }
    
        public Color getColor() {
    
            return color;
        }
    
        public double getValue() {
    
            return value;
        }
    }
    
    class PieChart extends JPanel {
    
        private final int SIZE = 500, START = 40, START_DEG = 90;
        private final int TRIG_HBASE = 66, TRIG_HEIGHT = 36;
        private final int x0 =(START + SIZE / 2), y0 = START;
        private final Polygon poly;
    
        private Color a = Color.RED;
        private Color b = Color.BLUE;
        private Color c = Color.YELLOW;
        Slice[] slices = {new Slice(65, a), new Slice(123, b), new Slice(212, c)};
    
        PieChart() {
    
            setBackground(Color.WHITE);
    
            int x1 = x0 + TRIG_HBASE,  y1 = y0;
            int x2 = x0 - TRIG_HBASE,  y2 = y0;
            int x3 = x0,               y3 = y0 - TRIG_HEIGHT;
            poly = new Polygon(new int[] {x1, x2, x3}, new int[] {y1, y2, y3}, 3);
        }
    
        @Override
        protected void paintComponent(Graphics g) {
    
            Graphics2D g2d = (Graphics2D) g;
            super.paintComponent(g2d);
    
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    
            g2d.setColor(Color.LIGHT_GRAY);
            g2d.fillRect(START, START, SIZE, SIZE);
    
            double total = 0d;
            for (Slice slice : slices) {
                total += slice.getValue();
            }
    
            double startAngle = START_DEG;
            double arcAngle, centerAngle;
            double x, y;
    
            for (Slice slice : slices) {
                arcAngle = (slice.getValue() * 360 / total);
                g2d.setColor(slice.getColor());
                g2d.fill(new Arc2D.Double(START, START, SIZE, SIZE, startAngle, arcAngle, Arc2D.PIE));
    
                centerAngle = Math.toRadians(((startAngle - START_DEG) + arcAngle / 2));
                x = (START + SIZE / 2 * (1 - Math.sin(centerAngle)));
                y = (START + SIZE / 2 * (1 - Math.cos(centerAngle)));
    
                AffineTransform trans = AffineTransform.getTranslateInstance(x - x0, y - y0);
                AffineTransform rot = AffineTransform.getRotateInstance(-centerAngle, x, y);
                Shape s = trans.createTransformedShape(poly);
                s = rot.createTransformedShape(s);
    
                g2d.setColor(slice.getColor().darker());
                g2d.fill(s);
    
                startAngle += arcAngle;
            }
        }
    
        @Override
        public Dimension getPreferredSize() {
    
            return new Dimension(START * 2 + SIZE, START * 2 + SIZE);
        }
    }
    

    poly serves as the basic triangle and is facing upwards with its base centered on the 0 o'clock point. Each arc translates and transforms (a copy of) this polygon so that its base is centered at the center of the arc length and so that it points outwards.

    Notes:

    • Don't call setBackground inside paintComponent, call it outside. It causes the paint mechanism to automatically paint the background on each repaint. If you put it inside you are just overriding the instruction with every repaint. Alternatively, you can use g.clearRect to set the background to white (or fillRect for a different color).
    • Override the panel's getPreferredSize method to be compatible with its contents.
    • Work with constants (final) instead of inline numbers. This way you only need to change them in one place and all dependencies are accounted for.
    • Slice could use getter methods (generally preferred over direct field access), and it also allows for each loops.
    • Work with doubles and only convert to int at the latest point, otherwise you lose precision (you convert your angles to int and then use them as a double argument).
    • Math.toRadians and Math.toDegrees are worth being acquainted with.
    • I made the triangles wide to show how they intersect with the arcs, change the TRIG constants to play with their sizes. I also colored them to know which triangle belongs to which arc.
    • I added a background to the arcs just to see the perimeter better.

    Here is the result with your parameters (and no special coloring):

    private final int SIZE = 200, START = 20, START_DEG = 90;
    private final int TRIG_HBASE = 7, TRIG_HEIGHT = 14;
    

    enter image description here