net.sourceforge.processdash.ui.web.reports.RadarPlot.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.processdash.ui.web.reports.RadarPlot.java

Source

// Copyright (C) 2002-2008 Tuma Solutions, LLC
// Process Dashboard - Data Automation Tool for high-maturity processes
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 3
// of the License, or (at your option) any later version.
//
// Additional permissions also apply; see the README-license.txt
// file in the project root directory for more information.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
//
// The author(s) may be contacted at:
//     processdash@tuma-solutions.com
//     processdash-devel@lists.sourceforge.net

package net.sourceforge.processdash.ui.web.reports;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.jfree.chart.event.PlotChangeEvent;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.PlotState;
import org.jfree.data.general.PieDataset;
import org.jfree.ui.RectangleInsets;

/**
 * A plot that displays data in the form of a radar chart, using data
 * from any class that implements the CategoryDataSource interface.
 * <P>
 * Notes:
 * (1) negative values in the dataset are ignored;
 * (2) vertical axis and horizontal axis are set to null;
 * (3) there are utility methods for creating a CategoryDataSource from a
 * CategoryDataset;
 * @see Plot
 * @see CategoryDataSource */
public class RadarPlot extends Plot {

    /** The default interior gap percent (currently 20%). */
    public static final double DEFAULT_INTERIOR_GAP = 0.20;

    /** The maximum interior gap (currently 40%). */
    public static final double MAX_INTERIOR_GAP = 0.40;

    /** The default radius percent (currently 100%). */
    public static final double DEFAULT_RADIUS = 1.00;

    /** The maximum radius (currently 100%). */
    public static final double MAX_RADIUS = 1.00;

    /** The default axis label font. */
    public static final Font DEFAULT_AXIS_LABEL_FONT = new Font("SansSerif", Font.PLAIN, 10);

    /** The default axis label paint. */
    public static final Paint DEFAULT_AXIS_LABEL_PAINT = Color.black;

    /** The default axis label gap (currently 10%). */
    public static final double DEFAULT_AXIS_LABEL_GAP = 0.10;

    /** The maximum interior gap (currently 30%). */
    public static final double MAX_AXIS_LABEL_GAP = 0.30;

    /** The default stroke for the series line */
    public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(3.0f);

    /** A magic color object used to designate adaptive coloring, based
     *  on the computed quality index */
    public static final Paint ADAPTIVE_COLORING = new Color(0);

    /** The dataset for the radar chart. */
    private PieDataset dataset;

    /** The amount of space left around the outside of the radar
    chart, expressed as a percentage. */
    protected double interiorGap;

    //    /** Flag determining whether to draw an ellipse or a perfect circle. */
    //    protected boolean circular;

    /** The radius as a percentage of the available drawing area. */
    protected double radius;

    /** The font used to display the axis labels. */
    protected Font axisLabelFont;

    /** The color used to draw the axis labels. */
    protected Paint axisLabelPaint;

    /** The gap between the labels and the radar axes, as a
    percentage of the radius. */
    protected double axisLabelGap;

    /** Whether or not axis labels should be drawn */
    protected boolean showAxisLabels;

    /** The color used to paint the axis lines (i.e. spokes) */
    protected Paint axisPaint;

    /** The stroke used to paint the axis lines (i.e. spokes) */
    protected Stroke axisStroke;

    /** The color used to paint the grid lines */
    protected Paint gridLinePaint;

    /** The stroke used to paint the grid lines */
    protected Stroke gridLineStroke;

    /** The color to use to draw the data polygon */
    protected Paint plotLinePaint;

    /** The stroke used to draw the data polygon */
    protected Stroke plotLineStroke;

    /**
     * Constructs a new radar chart, using default attributes as required.
     */
    public RadarPlot() {
        this(null);
    }

    public RadarPlot(PieDataset dataset) {
        super();
        this.dataset = dataset;
        initialise();
    }

    private void initialise() {
        this.interiorGap = DEFAULT_INTERIOR_GAP;
        //        this.circular = true;
        this.radius = DEFAULT_RADIUS;
        this.showAxisLabels = true;
        this.axisLabelFont = DEFAULT_AXIS_LABEL_FONT;
        this.axisLabelPaint = DEFAULT_AXIS_LABEL_PAINT;
        this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP;
        //        this.itemLabelGenerator = null;
        //        this.urlGenerator = null;

        this.plotLinePaint = ADAPTIVE_COLORING;
        this.axisPaint = Color.black;
        this.axisStroke = DEFAULT_OUTLINE_STROKE;
        this.gridLinePaint = Color.lightGray;
        this.gridLineStroke = DEFAULT_OUTLINE_STROKE;
        this.plotLineStroke = DEFAULT_LINE_STROKE;
        setForegroundAlpha(0.5f);
        setInsets(new RectangleInsets(0, 5, 5, 5));
    }

    /**
     * Returns the interior gap, measured as a percentage of the
     * available drawing space.
     *
     * @return The gap percentage.  */
    public double getInteriorGap() {
        return this.interiorGap;
    }

    /**
     * Sets the interior gap.
     *
     * @param percent The gap.
     */
    public void setInteriorGap(double percent) {

        // check arguments...
        if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
            throw new IllegalArgumentException(
                    "RadarPlot.setInteriorGap(double): percentage " + "outside valid range.");
        }

        // make the change...
        if (this.interiorGap != percent) {
            this.interiorGap = percent;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    //    /**
    //     * Returns a flag indicating whether the pie chart is circular, or
    //     * stretched into an elliptical shape.
    //     *
    //     * @return a flag indicating whether the pie chart is circular.
    //     */
    //    public boolean isCircular() {
    //        return circular;
    //    }
    //
    //    /**
    //     * A flag indicating whether the pie chart is circular, or stretched
    //     * into an elliptical shape.
    //     *
    //     * @param flag  the new value.
    //     */
    //    public void setCircular(boolean flag) {
    //
    //        // no argument checking required...
    //        // make the change...
    //        if (circular != flag) {
    //            circular = flag;
    //            notifyListeners(new PlotChangeEvent(this));
    //        }
    //
    //    }

    /**
     * Returns the radius (a percentage of the available space).
     *
     * @return The radius percentage.
     */
    public double getRadius() {
        return this.radius;
    }

    /**
     * Sets the radius.
     *
     * @param percent  the new value.
     */
    public void setRadius(double percent) {

        // check arguments...
        if ((percent <= 0.0) || (percent > MAX_RADIUS)) {
            throw new IllegalArgumentException("RadarPlot.setRadius(double): percentage " + "outside valid range.");
        }

        // make the change (if necessary)...
        if (this.radius != percent) {
            this.radius = percent;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    /**
     * Returns the axis label font.
     * @return The axis label font.
     */
    public Font getAxisLabelFont() {
        return this.axisLabelFont;
    }

    /**
     * Sets the axis label font.
     * <P>
     * Notifies registered listeners that the plot has been changed.
     * @param font The new axis label font.
     */
    public void setAxisLabelFont(Font font) {

        // check arguments...
        if (font == null) {
            throw new IllegalArgumentException("RadarPlot.setAxisLabelFont(...): " + "null font not allowed.");
        }

        // make the change...
        if (!this.axisLabelFont.equals(font)) {
            this.axisLabelFont = font;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    /**
     * Returns the axis label paint.
     * @return The axis label paint.
     */
    public Paint getAxisLabelPaint() {
        return this.axisLabelPaint;
    }

    /**
     * Sets the axis label paint.
     * <P>
     * Notifies registered listeners that the plot has been changed.
     * @param paint The new axis label paint.
     */
    public void setAxisLabelPaint(Paint paint) {

        // check arguments...
        if (paint == null) {
            throw new IllegalArgumentException("RadarPlot.setAxisLabelPaint(...): " + "null paint not allowed.");
        }

        // make the change...
        if (!this.axisLabelPaint.equals(paint)) {
            this.axisLabelPaint = paint;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    /**
     * Returns the plot line paint.
     * @return The plot line paint.
     */
    public Paint getPlotLinePaint() {
        return this.plotLinePaint;
    }

    /**
     * Sets the plot line paint.
     * <P>
     * Notifies registered listeners that the plot has been changed.
     * @param paint The new plot line paint.
     */
    public void setPlotLinePaint(Paint paint) {

        // check arguments...
        if (paint == null) {
            throw new IllegalArgumentException("RadarPlot.setPlotPaint(...): " + "null paint not allowed.");
        }

        // make the change...
        if (!this.plotLinePaint.equals(paint)) {
            this.plotLinePaint = paint;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    /**
     * Returns the axis label gap, measures as a percentage of the radius.
     * @return The axis label gap, measures as a percentage of the radius.
     */
    public double getAxisLabelGap() {
        return this.axisLabelGap;
    }

    /**
     * Sets the axis label gap percent.
     */
    public void setAxisLabelGap(double percent) {

        // check arguments...
        if ((percent < 0.0) || (percent > MAX_AXIS_LABEL_GAP)) {
            throw new IllegalArgumentException(
                    "RadarPlot.setAxisLabelGap(double): " + "percentage outside valid range.");
        }

        // make the change...
        if (this.axisLabelGap != percent) {
            this.axisLabelGap = percent;
            notifyListeners(new PlotChangeEvent(this));
        }

    }

    /**
     * Returns the show axis labels flag.
     *
     * @return the show axis label flag.
     */
    public boolean getShowAxisLabels() {
        return (this.showAxisLabels);
    }

    /**
     * Sets the show axis labels flag.
     * <P>
     * Notifies registered listeners that the plot has been changed.
     *
     * @param flag  the new show axis labels flag.
     */
    public void setShowAxisLabels(boolean flag) {
        if (this.showAxisLabels != flag) {
            this.showAxisLabels = flag;
            notifyListeners(new PlotChangeEvent(this));
        }
    }

    /**
     * Returns the dataset for the plot, cast as a CategoryDataSource.
     * <P>
     * Provided for convenience.
     * @return The dataset for the plot, cast as a CategoryDataSource.
     */
    public PieDataset getPieDataset() {
        return dataset;
    }

    /**
     * Returns a collection of the section keys (or categories) in the dataset.
     *
     * @return the categories.
     */
    public Collection getKeys() {
        if (dataset != null)
            return Collections.unmodifiableCollection(dataset.getKeys());
        else
            return null;
    }

    /**
     * Draws the plot on a Java 2D graphics device (such as the screen
     * or a printer).
     * @param g2 The graphics device.
     * @param plotArea The area within which the plot should be drawn.
     */
    @Override
    public void draw(Graphics2D g2, Rectangle2D plotArea, Point2D anchor, PlotState state, PlotRenderingInfo info) {
        // adjust for insets...
        RectangleInsets insets = getInsets();
        if (insets != null) {
            plotArea.setRect(plotArea.getX() + insets.getLeft(), plotArea.getY() + insets.getTop(),
                    plotArea.getWidth() - insets.getLeft() - insets.getRight(),
                    plotArea.getHeight() - insets.getTop() - insets.getBottom());
        }

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

        drawBackground(g2, plotArea);
        drawOutline(g2, plotArea);

        Shape savedClip = g2.getClip();
        g2.clip(plotArea);

        Composite originalComposite = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getForegroundAlpha()));

        if (this.dataset != null) {
            drawRadar(g2, plotArea, info, 0, this.dataset);
        } else {
            drawNoDataMessage(g2, plotArea);
        }

        g2.clip(savedClip);
        g2.setComposite(originalComposite);

        drawOutline(g2, plotArea);

    }

    protected void drawRadar(Graphics2D g2, Rectangle2D plotArea, PlotRenderingInfo info, int pieIndex,
            PieDataset data) {

        // adjust the plot area by the interior spacing value
        double gapHorizontal = plotArea.getWidth() * this.interiorGap;
        double gapVertical = plotArea.getHeight() * this.interiorGap;
        double radarX = plotArea.getX() + gapHorizontal / 2;
        double radarY = plotArea.getY() + gapVertical / 2;
        double radarW = plotArea.getWidth() - gapHorizontal;
        double radarH = plotArea.getHeight() - gapVertical;

        // make the radar area a square if the radar chart is to be circular...
        // NOTE that non-circular radar charts are not currently supported.
        if (true) { //circular) {
            double min = Math.min(radarW, radarH) / 2;
            radarX = (radarX + radarX + radarW) / 2 - min;
            radarY = (radarY + radarY + radarH) / 2 - min;
            radarW = 2 * min;
            radarH = 2 * min;
        }

        double radius = radarW / 2;
        double centerX = radarX + radarW / 2;
        double centerY = radarY + radarH / 2;

        Rectangle2D radarArea = new Rectangle2D.Double(radarX, radarY, radarW, radarH);

        // plot the data (unless the dataset is null)...
        if ((data != null) && (data.getKeys().size() > 0)) {

            // get a list of categories...
            List keys = data.getKeys();
            int numAxes = keys.size();

            // draw each of the axes on the radar chart, and register
            // the shape of the radar line.

            double multiplier = 1.0;
            GeneralPath lineShape = new GeneralPath(GeneralPath.WIND_NON_ZERO, numAxes + 1);
            GeneralPath gridShape = new GeneralPath(GeneralPath.WIND_NON_ZERO, numAxes + 1);

            int axisNumber = -1;
            Iterator iterator = keys.iterator();
            while (iterator.hasNext()) {
                Comparable currentKey = (Comparable) iterator.next();
                axisNumber++;
                Number dataValue = data.getValue(currentKey);

                double value = (dataValue != null ? dataValue.doubleValue() : 0);
                if (value > 1 || Double.isNaN(value) || Double.isInfinite(value))
                    value = 1.0;
                if (value < 0)
                    value = 0.0;
                multiplier *= value;

                double angle = 2 * Math.PI * axisNumber / numAxes;
                double deltaX = Math.sin(angle) * radius;
                double deltaY = -Math.cos(angle) * radius;

                // draw the spoke
                g2.setPaint(axisPaint);
                g2.setStroke(axisStroke);
                Line2D line = new Line2D.Double(centerX, centerY, centerX + deltaX, centerY + deltaY);
                g2.draw(line);

                // register the grid line and the shape line
                if (axisNumber == 0) {
                    gridShape.moveTo((float) deltaX, (float) deltaY);
                    lineShape.moveTo((float) (deltaX * value), (float) (deltaY * value));
                } else {
                    gridShape.lineTo((float) deltaX, (float) deltaY);
                    lineShape.lineTo((float) (deltaX * value), (float) (deltaY * value));
                }

                if (showAxisLabels) {
                    // draw the label
                    double labelX = centerX + deltaX * (1 + axisLabelGap);
                    double labelY = centerY + deltaY * (1 + axisLabelGap);
                    String label = currentKey.toString();
                    drawLabel(g2, radarArea, label, axisNumber, labelX, labelY);
                }

            }
            gridShape.closePath();
            lineShape.closePath();

            // draw five gray concentric gridlines
            g2.translate(centerX, centerY);
            g2.setPaint(gridLinePaint);
            g2.setStroke(gridLineStroke);
            for (int i = 5; i > 0; i--) {
                Shape scaledGrid = gridShape
                        .createTransformedShape(AffineTransform.getScaleInstance(i / 5.0, i / 5.0));
                g2.draw(scaledGrid);
            }

            // get the color for the plot shape.
            Paint dataPaint = plotLinePaint;
            if (dataPaint == ADAPTIVE_COLORING) {
                //multiplier = Math.exp(Math.log(multiplier) * 2 / numAxes);
                dataPaint = getMultiplierColor((float) multiplier);
            }

            // compute a slightly transparent version of the plot color for
            // the fill.
            Paint dataFill = null;
            if (dataPaint instanceof Color && getForegroundAlpha() != 1.0)
                dataFill = new Color(((Color) dataPaint).getRed() / 255f, ((Color) dataPaint).getGreen() / 255f,
                        ((Color) dataPaint).getBlue() / 255f, getForegroundAlpha());
            else
                dataFill = dataPaint;

            // draw the plot shape.  First fill with a parially
            // transparent color, then stroke with the opaque color.
            g2.setPaint(dataFill);
            g2.fill(lineShape);
            g2.setPaint(dataPaint);
            g2.setStroke(plotLineStroke);
            g2.draw(lineShape);

            // cleanup the graphics context.
            g2.translate(-centerX, -centerY);
        }
    }

    /** Calculate an appropriate color for the quality chart.
     * if the multiplier is 0, use red; if it is 1, use green;
     * use yellow in between, and fade proportionately.
     */
    private Paint getMultiplierColor(float value) {
        if (value > 0.4)
            return Color.green;
        else if (value > 0.2)
            // at 0.4, red component should be 0.0; at 0.2, it should be 1.0
            return new Color(2 - 5 * value, 1, 0, 1);
        else
            // at 0.0, green component should be 0.0; at 0.2, it should be 1.0
            return new Color(1, 5 * value, 0, 1);
    }

    /**
     * Draws the label for one radar axis.
     *
     * @param g2 The graphics device.
     * @param chartArea The area for the radar chart.
     * @param data The data for the plot.
     * @param axis The axis (zero-based index).
     * @param startAngle The starting angle.
     */
    protected void drawLabel(Graphics2D g2, Rectangle2D chartArea, String label, int axis, double labelX,
            double labelY) {

        // handle label drawing...
        FontRenderContext frc = g2.getFontRenderContext();
        Rectangle2D labelBounds = this.axisLabelFont.getStringBounds(label, frc);
        LineMetrics lm = this.axisLabelFont.getLineMetrics(label, frc);
        double ascent = lm.getAscent();

        if (labelX == chartArea.getCenterX())
            labelX -= labelBounds.getWidth() / 2;
        else if (labelX < chartArea.getCenterX())
            labelX -= labelBounds.getWidth();
        if (labelY > chartArea.getCenterY())
            labelY += ascent;

        g2.setPaint(this.axisLabelPaint);
        g2.setFont(this.axisLabelFont);
        g2.drawString(label, (float) labelX, (float) labelY);
    }

    /**
     * Returns a short string describing the type of plot.
     */
    @Override
    public String getPlotType() {
        return "Radar Chart";
    }

    /**
     * A zoom method that does nothing.
     * <p>
     * Plots are required to support the zoom operation.  In the case
     * of a radar chart, it doesn't make sense to zoom in or out, so
     * the method is empty.
     *
     * @param percent The zoom percentage.
     */
    @Override
    public void zoom(double percent) {
    }

}