package nl.helixsoft.chart; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.RenderingHints.Key; import java.awt.geom.Arc2D; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import nl.helixsoft.recordstream.Record; import nl.helixsoft.recordstream.ReduceFunctions; import nl.helixsoft.stats.DataFrame; import nl.helixsoft.stats.Factor; public class DonutChart { private final DataFrame dfLong; private final int outerColIdx; private final int innerColIdx; private final int valueColIdx; DonutChart (DataFrame dfLong, String outerColumn, String innerColumn, String valueColumn) { this.dfLong = dfLong; this.outerColIdx = dfLong.getColumnIndex(outerColumn); this.innerColIdx = dfLong.getColumnIndex(innerColumn); this.valueColIdx = dfLong.getColumnIndex(valueColumn); } private void drawLabel(Graphics2D g2d, String lab, double lx, double ly) { String label = lab == null ? "null" : lab; g2d.setColor (Color.BLACK); Rectangle2D tBounds = g2d.getFontMetrics().getStringBounds(label, g2d); double tx = lx - tBounds.getCenterX(); double ty = ly - tBounds.getCenterY(); g2d.drawString(label, (int)tx, (int)ty); } private Path2D ringSegment(double cx, double cy, double outerRadius, double innerRadius, double startFraction, double extentFraction) { assert outerRadius > innerRadius; Path2D p = new Path2D.Double(); double top = cy - outerRadius; double left = cx - outerRadius; double width = 2 * outerRadius; double height = 2 * outerRadius; Arc2D arc = new Arc2D.Double ( left, top, width, height, startFraction * 360, extentFraction * 360, Arc2D.OPEN ); p.append (arc.getPathIterator(null), false); top = cy - innerRadius; left = cx - innerRadius; width = 2 * innerRadius; height = 2 * innerRadius; arc = new Arc2D.Double ( left, top, width, height, (startFraction + extentFraction) * 360, -extentFraction * 360, Arc2D.OPEN ); p.append (arc.getPathIterator(null), true); p.closePath(); return p; } public void draw(Graphics2D g2d, Rectangle2D area) { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // calculate total triples double sum = dfLong.getColumn(Long.class, valueColIdx).apply (0L, ReduceFunctions.LONG_SUM); // draw arcs List<Color> cycleColors = Arrays.asList (new Color[] { new Color(0x8080FF), Color.YELLOW, new Color(0x80FFFF), Color.GREEN, Color.CYAN, new Color(0x80FF80), Color.MAGENTA, new Color (0xFF2060), Color.LIGHT_GRAY, new Color (0xFFFF80), Color.PINK, Color.RED, new Color(0xFF80FF), Color.ORANGE, Color.DARK_GRAY, new Color(0x8080FF), new Color(0xFF8080) } ); int outerColor = 0; int innerColor = 0; double outerCumulative = 0; double innerCumulative = 0; Factor innerFactor = dfLong.getColumnAsFactor(String.class, innerColIdx); List<String> sortedCategories = new ArrayList<String>(innerFactor.getFactors()); Collections.sort (sortedCategories); double width = area.getWidth(); double height = area.getHeight(); for (String category : sortedCategories) { DataFrame slice = innerFactor.getRows(category).sort(outerColIdx); double innerTriples = slice.getColumn(Long.class, valueColIdx).apply(0L, ReduceFunctions.LONG_SUM); g2d.setColor(cycleColors.get ((innerColor++) % cycleColors.size())); double cx = area.getCenterX(); double cy = area.getCenterY(); Path2D p = ringSegment (cx, cy, width / 2 - 120, width / 2 - 220, innerCumulative / sum, innerTriples / sum); g2d.fill(p); g2d.setColor(Color.BLACK); g2d.draw(p); drawLabel (g2d, category, p.getBounds().getCenterX(), p.getBounds().getCenterY()); innerCumulative += innerTriples; for (Record row : slice.asRecordIterable()) { double outerTriples = (Long)row.get (valueColIdx); String shortName = (String)row.get(outerColIdx); g2d.setColor(cycleColors.get ((outerColor++) % cycleColors.size())); Path2D outerP = ringSegment (width / 2, height / 2, width / 2, width / 2 - 100, outerCumulative / sum, outerTriples / sum); g2d.fill(outerP); g2d.setColor(Color.BLACK); g2d.draw(outerP); double lx = cx + ((width / 2) - 50) * Math.cos ((outerCumulative + outerTriples / 2) * 2 * Math.PI / sum ); double ly = cy - ((height / 2) - 50) * Math.sin ((outerCumulative + outerTriples / 2) * 2 * Math.PI / sum ); Rectangle2D tBounds0 = g2d.getFontMetrics().getStringBounds(shortName, g2d); Rectangle2D tBounds = new Rectangle2D.Double(lx - tBounds0.getWidth(), ly - tBounds0.getHeight(), tBounds0.getWidth(), tBounds0.getHeight()); if (outerP.contains(tBounds)) { drawLabel (g2d, shortName, lx, ly); } outerCumulative += outerTriples; } } } }