Make a custom ring chart in JFreeChart
Asked Answered
G

1

6

I am currently using itext-pdf to generate PDFs. In addition to that, I am also using JFreeChart to create charts on it. I have created a donut chart with a explosion effect and it looks like this.

enter image description here

However I want to create a donut chart that looks more like this.

enter image description here

I want certain pieces to stand out but not completely get detached from the donut chart. I would highly appreciate inputs on how to achieve this.

Here is my current code:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Locale;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.PiePlotState;
import org.jfree.chart.plot.RingPlot;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.ui.RectangleInsets;


import com.itextpdf.awt.DefaultFontMapper;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.ColumnText;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;

public class RingChartTest {
    public static void main(String[] args) throws Exception {
        new RingChartTest().createPDF();
    }

    private void createPDF() throws Exception {
        String destination = "ringchart.pdf";

        Document document = new Document(PageSize.A4.rotate());
        try {
            PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(destination));
            document.open();

            // Create the pages
            PdfContentByte cb = writer.getDirectContent();
            addChart(cb);
        } catch (Exception e) {
            System.out.println("Failure to generate the PDF");
            e.printStackTrace();
        } finally {
            if (document != null) {
                document.close();        
            }
        }
    }

    private void addChart(PdfContentByte cb) throws Exception, IOException {
        long pctPM = Math.round(20);
        long pctOA = Math.round(15);
        long pctWPI = Math.round(5);
        long pctTDF = Math.round(25);
        long pctNE = 100 - (pctPM + pctOA + pctWPI + pctTDF);
        long pctEngaged = pctPM + pctOA + pctWPI;
        long numEngaged = 3400;
        String strNumEngaged = formatNumber(numEngaged, "#,###,###,##0");


        JFreeChart chart = createChart(pctPM, pctOA, pctWPI, pctTDF, pctNE);

        int width = 300;
        int height = 200;
        PdfTemplate template = cb.createTemplate(width, height);
        Graphics2D graphics2d = template.createGraphics(width, height, new DefaultFontMapper());
        Rectangle2D rectangle2d = new Rectangle2D.Double(0, 0, width, height);
        chart.draw(graphics2d, rectangle2d);
        graphics2d.dispose();
        cb.addTemplate(template, 30, 185);  

        // Add text inside chart
        Font engagementFont = createFont("OpenSans-Bold.ttf", 8, 116, 112, 100);
        Font percentFont1 = createFont("OpenSans-Light.ttf", 22, 116, 112, 100);
        Font percentFont2 = createFont("OpenSans-Light.ttf", 10, 116, 112, 100);
        Font numberFont = createFont("OpenSans-Regular.ttf", 8, 116, 112, 100);
        addPhrase(cb, "ENGAGE", engagementFont, 135, 290, 230, 310, 10, Element.ALIGN_CENTER);
        addPhrase(cb, String.valueOf(pctEngaged), percentFont1, 115, 270, 190, 289, 10, Element.ALIGN_RIGHT);
        addPhrase(cb, "%", percentFont2, 191, 275, 201, 299, 10, Element.ALIGN_LEFT);
        addPhrase(cb, "(" + strNumEngaged + ")", numberFont, 130, 258, 230, 278, 10, Element.ALIGN_CENTER);

        // Create legend
        // 290,420,370,520,10,Element.ALIGN_CENTER);
        BaseFont engagedPctFont = createBaseFont("OpenSans-Bold.ttf");
        BaseFont engagedDescFont = createBaseFont("OpenSans-SemiBold.ttf");
        BaseFont nonEngagedDescFont = createBaseFont("OpenSans-Regular.ttf");
        BaseColor pmBaseColor = new BaseColor(31, 160, 200);
        BaseColor oaBaseColor = new BaseColor(84, 193, 209);
        BaseColor wpiBaseColor = new BaseColor(248, 156, 36);
        BaseColor tdfBaseColor = new BaseColor(116, 112, 94);
        BaseColor nonEngagedBaseColor = new BaseColor(148, 144, 132);
        float x = 330;
        float y = 350;
        float radius = 3;

        // Create border around legend
        /*
        cb.setColorFill(new BaseColor(255, 255, 255));
        cb.rectangle(320, 300, 150, 70);
        cb.re
        cb.fill();
        */
        BaseColor borderColor = new BaseColor(192, 189, 178);
        cb.setColorStroke(borderColor);
        cb.moveTo(320, 300);
        cb.lineTo(320, 365);
        cb.lineTo(500, 365);
        cb.lineTo(500, 300);
        cb.lineTo(320, 300);
        cb.closePathStroke();

        // Prof Mgmt
        cb.setColorFill(pmBaseColor);
        cb.circle(x, y, radius);
        cb.fill();
        addTextToCanvas(cb, pctPM+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-2);
        addTextToCanvas(cb, "Pg", engagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-2);
        // Online Advice
        cb.setColorFill(oaBaseColor);
        cb.circle(x, y-20, radius);
        cb.fill();
        addTextToCanvas(cb, pctOA+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-22);
        addTextToCanvas(cb, "Oaa", engagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-22);
        // Clicked WPI/Online Guidance
        cb.setColorFill(wpiBaseColor);
        cb.circle(x, y-40, radius);
        cb.fill();
        addTextToCanvas(cb, pctWPI+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-42);
        addTextToCanvas(cb, "Ogg", engagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-42);
        if (pctTDF > 0) {
            // TDF Users
            cb.setColorFill(tdfBaseColor);
            cb.circle(x, y-60, radius);
            cb.fill();
            addTextToCanvas(cb, pctTDF+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-62);
            addTextToCanvas(cb, "Pti*", nonEngagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-62);
            // Non-engaged
            cb.setColorFill(nonEngagedBaseColor);
            cb.circle(x, y-80, radius);
            cb.fill();
            addTextToCanvas(cb, pctNE+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-82);
            addTextToCanvas(cb, "Nng", nonEngagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-82);
        } else {
            // Non-engaged
            cb.setColorFill(nonEngagedBaseColor);
            cb.circle(x, y-60, radius);
            cb.fill();
            addTextToCanvas(cb, pctNE+"%", engagedPctFont, 8, new BaseColor(116, 112, 100), x+20, y-62);
            addTextToCanvas(cb, "ngd", nonEngagedDescFont, 8, new BaseColor(116, 112, 100), x+50, y-62);
        }
    }

    private String formatNumber(double value, String strFormat) {
        DecimalFormat df = new DecimalFormat( strFormat );
        return df.format(value);
    }

    private void addPhrase(PdfContentByte cb, String strText, Font font, float llx, float lly, float urx, float ury, float leading, int alignment) throws DocumentException {
        Phrase phrase = new Phrase(strText, font);
        ColumnText ct = new ColumnText(cb);
        ct.setSimpleColumn(phrase, llx, lly, urx, ury, leading, alignment);
        ct.go();
    }

    private void addTextToCanvas(PdfContentByte cb, String strText, BaseFont font, float fontSize, BaseColor color, float x, float y) {
        cb.beginText();
        cb.setFontAndSize(font, fontSize);
        cb.setColorFill(color);
        cb.showTextAligned(Element.ALIGN_LEFT, strText, x, y, 0);
        cb.endText();
    }

    private BaseFont createBaseFont(String fileName) throws DocumentException, IOException {
        return BaseFont.createFont(PdfGenerationController.LOCATION_FONTS + fileName ,BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
    }

    private Font createFont(String fileName, float size, int red, int green, int blue) throws DocumentException, IOException {
        BaseFont baseFont = BaseFont.createFont(PdfGenerationController.LOCATION_FONTS + fileName ,BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
        Font font = new Font(baseFont, size);
        font.setColor(red, green, blue);
        return font;
    }

    public JFreeChart createChart(long pctPM, long pctOA, long pctWPI, long pctTDF, long pctNE) {
        // Set up the data set for the donut/ring chart
        DefaultPieDataset rDataSet =  new DefaultPieDataset();
        rDataSet.setValue("PM", pctPM );
        rDataSet.setValue("OA", pctOA);  
        rDataSet.setValue("WPI", pctWPI);
        rDataSet.setValue("TDF", pctTDF);
        rDataSet.setValue("NE", pctNE);

        // Initialize values
        boolean bShowLegend = false;
        String strTitle = null;

        // Create ring plot
        CustomDonutPlot rPlot = new CustomDonutPlot(rDataSet);
        //RingPlot rPlot = new RingPlot(rDataSet);
        rPlot.setLabelGenerator(new StandardPieSectionLabelGenerator(Locale.ENGLISH));
        rPlot.setInsets(new RectangleInsets(0.0, 5.0, 5.0, 5.0));
        rPlot.setSectionDepth(0.30);
        JFreeChart chart = new JFreeChart(strTitle, JFreeChart.DEFAULT_TITLE_FONT, rPlot, bShowLegend);
        ChartFactory.getChartTheme().apply(chart);        

        // Create the chart
        //JFreeChart rChart = ChartFactory.createRingChart(null, rDataSet , false, false, Locale.ENGLISH);
        //RingPlot rPlot = (RingPlot) rChart.getPlot();
        rPlot.setBackgroundPaint(Color.WHITE);
        rPlot.setCenterText(null);
        rPlot.setLabelGenerator(null); 
        rPlot.setOutlineVisible(false);
        rPlot.setShadowGenerator(null);
        rPlot.setSeparatorsVisible(false);
        rPlot.setShadowPaint(null);
        rPlot.setSectionOutlinesVisible(false);
        rPlot.setOuterSeparatorExtension(0);
        rPlot.setInnerSeparatorExtension(0);

        // Set colors of the chart
        rPlot.setSectionPaint("PM", new Color(31, 160, 200));
        rPlot.setSectionPaint("OA", new Color(84, 193, 209));
        rPlot.setSectionPaint("WPI", new Color(248, 156, 36));
        rPlot.setSectionPaint("TDF", new Color(116, 112, 94));
        rPlot.setSectionPaint("NE", new Color(148, 144, 132));

        rPlot.setExplodePercent("PM", 0.05);
        rPlot.setExplodePercent("OA", 0.05);
        rPlot.setExplodePercent("WPI", 0.05);

        return chart;
    }

    public static class CustomDonutPlot extends RingPlot {
        private static final long serialVersionUID = 1L;

        public CustomDonutPlot(DefaultPieDataset dataSet) {
            super(dataSet);
        }

        @Override
        protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea, PiePlotState state, int currentPass) {
            if (currentPass == 1 && section >=1 && section <= 3) {

            }
            Rectangle2D area = state.getPieArea();
            System.out.println("*** At section=" + section + ", pass="+currentPass);
            logDataArea(dataArea, "Data area");
            logDataArea(area, "Pie area");
            System.out.println(state.getInfo());

            super.drawItem(g2, section, dataArea, state, currentPass);


        }

        private void logDataArea(Rectangle2D dataArea, String msg) {
            System.out.println(msg + " h="+dataArea.getHeight() + ", w=" + dataArea.getWidth() + ", x=" + dataArea.getX() + ",y="+dataArea.getY());
        }


    }
}

This alternate version isolates the chart from the PDF.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.Locale;
import javax.swing.JFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.PiePlotState;
import org.jfree.chart.plot.RingPlot;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.ui.RectangleInsets;

/**
 * @see https://mcmap.net/q/1785655/-make-a-custom-ring-chart-in-jfreechart/230513
 */
public class Test {

    private void display() {
        JFrame f = new JFrame("Test");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        long pctPM = Math.round(20);
        long pctOA = Math.round(15);
        long pctWPI = Math.round(5);
        long pctTDF = Math.round(25);
        long pctNE = 100 - (pctPM + pctOA + pctWPI + pctTDF);
        f.add(new ChartPanel(createChart(pctPM, pctOA, pctWPI, pctTDF, pctNE)));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public JFreeChart createChart(long pctPM, long pctOA, long pctWPI, long pctTDF, long pctNE) {
        // Set up the data set for the donut/ring chart
        DefaultPieDataset rDataSet = new DefaultPieDataset();
        rDataSet.setValue("PM", pctPM);
        rDataSet.setValue("OA", pctOA);
        rDataSet.setValue("WPI", pctWPI);
        rDataSet.setValue("TDF", pctTDF);
        rDataSet.setValue("NE", pctNE);

        // Initialize values
        boolean bShowLegend = false;
        String strTitle = null;

        // Create ring plot
        CustomDonutPlot rPlot = new CustomDonutPlot(rDataSet);
        //RingPlot rPlot = new RingPlot(rDataSet);
        rPlot.setLabelGenerator(new StandardPieSectionLabelGenerator(Locale.ENGLISH));
        rPlot.setInsets(new RectangleInsets(0.0, 5.0, 5.0, 5.0));
        rPlot.setSectionDepth(0.30);
        JFreeChart chart = new JFreeChart(strTitle, JFreeChart.DEFAULT_TITLE_FONT, rPlot, bShowLegend);
        ChartFactory.getChartTheme().apply(chart);

        // Create the chart
        //JFreeChart rChart = ChartFactory.createRingChart(null, rDataSet , false, false, Locale.ENGLISH);
        //RingPlot rPlot = (RingPlot) rChart.getPlot();
        rPlot.setBackgroundPaint(Color.WHITE);
        rPlot.setCenterText(null);
        rPlot.setLabelGenerator(null);
        rPlot.setOutlineVisible(false);
        rPlot.setShadowGenerator(null);
        rPlot.setSeparatorsVisible(false);
        rPlot.setShadowPaint(null);
        rPlot.setSectionOutlinesVisible(false);
        rPlot.setOuterSeparatorExtension(0);
        rPlot.setInnerSeparatorExtension(0);

        // Set colors of the chart
        rPlot.setSectionPaint("PM", new Color(31, 160, 200));
        rPlot.setSectionPaint("OA", new Color(84, 193, 209));
        rPlot.setSectionPaint("WPI", new Color(248, 156, 36));
        rPlot.setSectionPaint("TDF", new Color(116, 112, 94));
        rPlot.setSectionPaint("NE", new Color(148, 144, 132));

        rPlot.setExplodePercent("PM", 0.05);
        rPlot.setExplodePercent("OA", 0.05);
        rPlot.setExplodePercent("WPI", 0.05);

        return chart;
    }

    public static class CustomDonutPlot extends RingPlot {

        private static final long serialVersionUID = 1L;

        public CustomDonutPlot(DefaultPieDataset dataSet) {
            super(dataSet);
        }

        @Override
        protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea, PiePlotState state, int currentPass) {
            super.drawItem(g2, section, dataArea, state, currentPass);
            Rectangle2D area = state.getPieArea();
            System.out.println("*** At section=" + section + ", pass=" + currentPass);
            logDataArea(dataArea, "Data area");
            logDataArea(area, "Pie area");
        }

        private void logDataArea(Rectangle2D dataArea, String msg) {
            System.out.println(msg + " h=" + dataArea.getHeight() + ", w=" + dataArea.getWidth() + ", x=" + dataArea.getX() + ",y=" + dataArea.getY());
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Test()::display);
    }
}
Ganiats answered 13/5, 2016 at 14:44 Comment(3)
@trashgod Is the chart I'm looking to create even possible with jfreecharts if so I would appreciate inputsGaniats
i don't see why not, but you may have to recapitulate a lot of the existing implementation; I added the alternate version in case someone wants to give it a go.Benignant
@Benignant thanks for isolating that, i have also put up bounty so if yourself or anyone else can give it a go it would be great as I have no idea where to start i am not very familiar with jfreecharts library, i am trying various hacks in the meantime but nothing is working.Ganiats
L
4

Seems that you need to draw your exploded arc in the position of the unexploded one. To do this you can override RingPlot::getArcBounds and work with the bounds of the arc. Update your code (inner class) to get the image below:

image

public static class CustomDonutPlot extends RingPlot {
    private static final long serialVersionUID = 1L;

    public CustomDonutPlot(DefaultPieDataset dataSet) {
        super(dataSet);
    }

    @Override
    protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea, PiePlotState state, int currentPass) {
        if (currentPass == 1 && section >=1 && section <= 3) {

        }
        Rectangle2D area = state.getPieArea();
        System.out.println("*** At section=" + section + ", pass="+currentPass);
        logDataArea(dataArea, "Data area");
        logDataArea(area, "Pie area");
        System.out.println(state.getInfo());

        super.drawItem(g2, section, dataArea, state, currentPass);


    }
    @Override
    protected Rectangle2D getArcBounds(Rectangle2D unexploded, Rectangle2D exploded, double angle, double extent, double explodePercent) {
        if(explodePercent > 0.0){
            this.setSectionDepth(0.33);//to match inner arc
            java.awt.geom.Arc2D.Double arc1 = new java.awt.geom.Arc2D.Double(unexploded, angle, extent / 2.0D, 0);
            Point2D point1 = arc1.getEndPoint();
            //java.awt.geom.Arc2D.Double arc2 = new java.awt.geom.Arc2D.Double(exploded, angle, extent / 2.0D, 0); //original code
            Rectangle2D mix = new Rectangle2D.Double(exploded.getX(), exploded.getY(), unexploded.getWidth(), unexploded.getHeight());
            java.awt.geom.Arc2D.Double arc2 = new java.awt.geom.Arc2D.Double(mix, angle, extent / 2.0D, 0);

            Point2D point2 = arc2.getEndPoint();
            double deltaX = (point1.getX() - point2.getX()) * explodePercent;
            double deltaY = (point1.getY() - point2.getY()) * explodePercent;
            //return new java.awt.geom.Rectangle2D.Double(unexploded.getX() - deltaX, unexploded.getY() - deltaY, unexploded.getWidth(), unexploded.getHeight()); original code
            return new java.awt.geom.Rectangle2D.Double(unexploded.getX() - deltaX, unexploded.getY() - deltaY, exploded.getWidth(), exploded.getHeight());
        } else {
            this.setSectionDepth(0.3);//default depth
            return super.getArcBounds(unexploded, exploded, angle, extent, explodePercent);
        }
    }


    private void logDataArea(Rectangle2D dataArea, String msg) {
        System.out.println(msg + " h="+dataArea.getHeight() + ", w=" + dataArea.getWidth() + ", x=" + dataArea.getX() + ",y="+dataArea.getY());
    }


}
Lichi answered 17/5, 2016 at 11:59 Comment(6)
If you need the inner arc of the exploded section be in the same circle of the other sections, you will need to calculate and set the depth of the section with this.setSectionDepth(0.33).Lichi
this seems promising do i replace my current override of draw method with this ? If possible can you post your solution in context with my answer so i can try it out and accept your answer ?Ganiats
i trying your approach i am getting some error, maybe i am doing something wrong. can you please clarify for me where exactly that goes in my code.Ganiats
In the class that you extends RingPlot. I have edited the answer.Lichi
this is extremely close but postimg.org/image/td84kvtxt if you look the inside arcs are not align with the others which is kind of different from this i.sstatic.net/XHkJQ.pngGaniats
uncomment //this.setSectionDepth(0.33);//to match inner arc and //this.setSectionDepth(0.3);//default depth. And see if what you want.Lichi

© 2022 - 2024 — McMap. All rights reserved.