weka.gui.visualize.ClassPanel.java Source code

Java tutorial

Introduction

Here is the source code for weka.gui.visualize.ClassPanel.java

Source

/*
 *   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.
 *
 *   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/>.
 */

/*
 *    ClassPanel.java
 *    Copyright (C) 2000-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.visualize;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;

import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;

import weka.core.Instances;
import weka.core.Utils;

/**
 * This panel displays coloured labels for nominal attributes and a spectrum for
 * numeric attributes. It can also be told to colour on the basis of an array of
 * doubles (this can be useful for displaying coloured labels that correspond to
 * a clusterers predictions).
 * 
 * @author Mark Hall (mhall@cs.waikato.ac.nz)
 * @author Malcolm Ware (mfw4@cs.waikato.ac.nz)
 * @version $Revision$
 */
public class ClassPanel extends JPanel {

    /** for serialization */
    private static final long serialVersionUID = -7969401840501661430L;

    /**
     * True when the panel has been enabled (ie after setNumeric or setNominal has
     * been called
     */
    private boolean m_isEnabled = false;

    /** True if the colouring attribute is numeric */
    private boolean m_isNumeric = false;

    /** The height of the spectrum for numeric class */
    private final int m_spectrumHeight = 5;

    /** The maximum value for the colouring attribute */
    private double m_maxC;

    /** The minimum value for the colouring attribute */
    private double m_minC;

    /** The size of the ticks */
    private final int m_tickSize = 5;

    /** Font metrics */
    private FontMetrics m_labelMetrics = null;

    /** The font used in labeling */
    private Font m_labelFont = null;

    /** The amount of space to leave either side of the legend */
    private int m_HorizontalPad = 0;

    /** The precision with which to display real values */
    private int m_precisionC;

    /** Field width for numeric values */
    // private int m_fieldWidthC; NOT USED

    /** The old width. */
    private int m_oldWidth = -9000;

    /** Instances being plotted */
    private Instances m_Instances = null;

    /** Index of the colouring attribute */
    private int m_cIndex;

    /** the list of colours to use for colouring nominal attribute labels */
    private ArrayList<Color> m_colorList;

    /**
     * An optional list of Components that use the colour list maintained by this
     * class. If the user changes a colour using the colour chooser, then these
     * components need to be repainted in order to display the change
     */
    private final ArrayList<Component> m_Repainters = new ArrayList<Component>();

    /**
     * An optional list of listeners who want to know when a colour changes.
     * Listeners are notified via an ActionEvent
     */
    private final ArrayList<ActionListener> m_ColourChangeListeners = new ArrayList<ActionListener>();

    /** default colours for colouring discrete class */
    protected Color[] m_DefaultColors = { Color.blue, Color.red, Color.green, Color.cyan, Color.pink,
            new Color(255, 0, 255), Color.orange, new Color(255, 0, 0), new Color(0, 255, 0), Color.white };

    /**
     * if set, it allows this panel to steer away from setting up a color in the
     * color list that is equal to the background color
     */
    protected Color m_backgroundColor = null;

    /**
     * Inner Inner class used to create labels for nominal attributes so that
     * there color can be changed.
     */
    private class NomLabel extends JLabel {

        /** for serialization */
        private static final long serialVersionUID = -4686613106474820655L;

        private int m_index = 0;

        /**
         * Creates a label with its name and class index value.
         * 
         * @param name The name of the nominal class value.
         * @param id The index value for that class value.
         */
        public NomLabel(String name, int id) {
            super(name);
            m_index = id;

            this.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {

                    if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
                        Color tmp = JColorChooser.showDialog(ClassPanel.this, "Select new Color",
                                m_colorList.get(m_index));

                        if (tmp != null) {
                            m_colorList.set(m_index, tmp);
                            m_oldWidth = -9000;
                            ClassPanel.this.repaint();
                            if (m_Repainters.size() > 0) {
                                for (int i = 0; i < m_Repainters.size(); i++) {
                                    (m_Repainters.get(i)).repaint();
                                }
                            }

                            if (m_ColourChangeListeners.size() > 0) {
                                for (int i = 0; i < m_ColourChangeListeners.size(); i++) {
                                    (m_ColourChangeListeners.get(i)).actionPerformed(new ActionEvent(this, 0, ""));
                                }
                            }
                        }
                    }
                }
            });
        }
    }

    public ClassPanel() {
        this(null);
    }

    public ClassPanel(Color background) {
        m_backgroundColor = background;

        /** Set up some default colours */
        m_colorList = new ArrayList<Color>(10);
        for (int noa = m_colorList.size(); noa < 10; noa++) {
            Color pc = m_DefaultColors[noa % 10];
            int ija = noa / 10;
            ija *= 2;
            for (int j = 0; j < ija; j++) {
                pc = pc.darker();
            }

            m_colorList.add(pc);
        }
    }

    /**
     * Adds a component that will need to be repainted if the user changes the
     * colour of a label.
     * 
     * @param c the component to be repainted
     */
    public void addRepaintNotify(Component c) {
        m_Repainters.add(c);
    }

    /**
     * Add an action listener that will be notified if the user changes the colour
     * of a label
     * 
     * @param a an <code>ActionListener</code> value
     */
    public void addActionListener(ActionListener a) {
        m_ColourChangeListeners.add(a);
    }

    /**
     * Set up fonts and font metrics
     * 
     * @param gx the graphics context
     */
    private void setFonts(Graphics gx) {
        if (m_labelMetrics == null) {
            m_labelFont = new Font("Monospaced", Font.PLAIN, 12);
            m_labelMetrics = gx.getFontMetrics(m_labelFont);
            int hf = m_labelMetrics.getAscent();
            if (this.getHeight() < (3 * hf)) {
                m_labelFont = new Font("Monospaced", Font.PLAIN, 11);
                m_labelMetrics = gx.getFontMetrics(m_labelFont);
            }
        }
        gx.setFont(m_labelFont);
    }

    /**
     * Enables the panel
     * 
     * @param e true to enable the panel
     */
    public void setOn(boolean e) {
        m_isEnabled = e;
    }

    /**
     * Set the instances.
     * 
     * @param insts the instances
     */
    public void setInstances(Instances insts) {
        m_Instances = insts;
    }

    /**
     * Set the index of the attribute to display coloured labels for
     * 
     * @param cIndex the index of the attribute to display coloured labels for
     */
    public void setCindex(int cIndex) {
        if (m_Instances.numAttributes() > 0) {
            m_cIndex = cIndex;
            if (m_Instances.attribute(m_cIndex).isNumeric()) {
                setNumeric();
            } else {
                if (m_Instances.attribute(m_cIndex).numValues() > m_colorList.size()) {
                    extendColourMap();
                }
                setNominal();
            }
        }
    }

    /**
     * Extends the list of colours if a new attribute with more values than the
     * previous one is chosen
     */
    private void extendColourMap() {
        if (m_Instances.attribute(m_cIndex).isNominal()) {
            for (int i = m_colorList.size(); i < m_Instances.attribute(m_cIndex).numValues(); i++) {
                Color pc = m_DefaultColors[i % 10];
                int ija = i / 10;
                ija *= 2;
                for (int j = 0; j < ija; j++) {
                    pc = pc.brighter();
                }
                if (m_backgroundColor != null) {
                    pc = Plot2D.checkAgainstBackground(pc, m_backgroundColor);
                }

                m_colorList.add(pc);
            }
        }
    }

    protected void setDefaultColourList(Color[] list) {
        m_DefaultColors = list;
    }

    /**
     * Set a list of colours to use for colouring labels
     * 
     * @param cols a list containing java.awt.Colors
     */
    public void setColours(ArrayList<Color> cols) {
        m_colorList = cols;
    }

    /**
     * Sets the legend to be for a nominal variable
     */
    protected void setNominal() {
        m_isNumeric = false;
        m_HorizontalPad = 0;
        setOn(true);
        m_oldWidth = -9000;

        this.repaint();
    }

    /**
     * Sets the legend to be for a numeric variable
     */
    protected void setNumeric() {
        m_isNumeric = true;
        /*
         * m_maxC = mxC; m_minC = mnC;
         */

        double min = Double.POSITIVE_INFINITY;
        double max = Double.NEGATIVE_INFINITY;
        double value;

        for (int i = 0; i < m_Instances.numInstances(); i++) {
            if (!m_Instances.instance(i).isMissing(m_cIndex)) {
                value = m_Instances.instance(i).value(m_cIndex);
                if (value < min) {
                    min = value;
                }
                if (value > max) {
                    max = value;
                }
            }
        }

        // handle case where all values are missing
        if (min == Double.POSITIVE_INFINITY) {
            min = max = 0.0;
        }

        m_minC = min;
        m_maxC = max;

        int whole = (int) Math.abs(m_maxC);
        double decimal = Math.abs(m_maxC) - whole;
        int nondecimal;
        nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

        m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
        if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
            m_precisionC = 1;
        }

        String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1 + m_precisionC, m_precisionC);
        if (m_labelMetrics != null) {
            m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
        }

        whole = (int) Math.abs(m_minC);
        decimal = Math.abs(m_minC) - whole;
        nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

        m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math.abs(m_minC)) / Math.log(10)))) + 2 : 1;
        if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
            m_precisionC = 1;
        }

        maxStringC = Utils.doubleToString(m_minC, nondecimal + 1 + m_precisionC, m_precisionC);
        if (m_labelMetrics != null) {
            if (m_labelMetrics.stringWidth(maxStringC) > m_HorizontalPad) {
                m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
            }
        }

        setOn(true);
        this.repaint();
    }

    /**
     * Renders the legend for a nominal colouring attribute
     * 
     * @param gx the graphics context
     */
    protected void paintNominal(Graphics gx) {
        setFonts(gx);

        int numClasses;

        numClasses = m_Instances.attribute(m_cIndex).numValues();

        int maxLabelLen = 0;
        int idx = 0;
        int legendHeight;
        int w = this.getWidth();
        int hf = m_labelMetrics.getAscent();

        for (int i = 0; i < numClasses; i++) {
            if (m_Instances.attribute(m_cIndex).value(i).length() > maxLabelLen) {
                maxLabelLen = m_Instances.attribute(m_cIndex).value(i).length();
                idx = i;
            }
        }

        maxLabelLen = m_labelMetrics.stringWidth(m_Instances.attribute(m_cIndex).value(idx));

        if (((w - (2 * m_HorizontalPad)) / (maxLabelLen + 5)) >= numClasses) {
            legendHeight = 1;
        } else {
            legendHeight = 2;
        }

        int x = m_HorizontalPad;
        int y = 1 + hf;

        int numToDo = ((legendHeight == 1) ? numClasses : (numClasses / 2));
        for (int i = 0; i < numToDo; i++) {

            gx.setColor(m_colorList.get(i));
            // can we fit the full label or will each need to be trimmed?
            if ((numToDo * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
                String val;
                val = m_Instances.attribute(m_cIndex).value(i);

                int sw = m_labelMetrics.stringWidth(val);
                int rm = 0;
                // truncate string if necessary
                if (sw > ((w - (m_HorizontalPad * 2)) / (numToDo))) {
                    int incr = (sw / val.length());
                    rm = (sw - ((w - (m_HorizontalPad * 2)) / numToDo)) / incr;
                    if (rm <= 0) {
                        rm = 0;
                    }
                    if (rm >= val.length()) {
                        rm = val.length() - 1;
                    }
                    val = val.substring(0, val.length() - rm);
                    sw = m_labelMetrics.stringWidth(val);
                }
                NomLabel jj = new NomLabel(val, i);
                jj.setFont(gx.getFont());

                jj.setSize(m_labelMetrics.stringWidth(jj.getText()), m_labelMetrics.getAscent() + 4);
                this.add(jj);
                jj.setLocation(x, y);
                jj.setForeground(m_colorList.get(i % m_colorList.size()));

                x += sw + 2;
            } else {

                NomLabel jj;
                jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

                jj.setFont(gx.getFont());

                jj.setSize(m_labelMetrics.stringWidth(jj.getText()), m_labelMetrics.getAscent() + 4);
                this.add(jj);
                jj.setLocation(x, y);
                jj.setForeground(m_colorList.get(i % m_colorList.size()));

                x += ((w - (m_HorizontalPad * 2)) / numToDo);
            }
        }

        x = m_HorizontalPad;
        y = 1 + hf + 5 + hf;
        for (int i = numToDo; i < numClasses; i++) {

            gx.setColor(m_colorList.get(i));
            if (((numClasses - numToDo + 1) * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
                String val;
                val = m_Instances.attribute(m_cIndex).value(i);

                int sw = m_labelMetrics.stringWidth(val);
                int rm = 0;
                // truncate string if necessary
                if (sw > ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo + 1))) {
                    int incr = (sw / val.length());
                    rm = (sw - ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo))) / incr;
                    if (rm <= 0) {
                        rm = 0;
                    }
                    if (rm >= val.length()) {
                        rm = val.length() - 1;
                    }
                    val = val.substring(0, val.length() - rm);
                    sw = m_labelMetrics.stringWidth(val);
                }
                // this is the clipped string
                NomLabel jj = new NomLabel(val, i);
                jj.setFont(gx.getFont());

                jj.setSize(m_labelMetrics.stringWidth(jj.getText()), m_labelMetrics.getAscent() + 4);

                this.add(jj);
                jj.setLocation(x, y);
                jj.setForeground(m_colorList.get(i % m_colorList.size()));

                x += sw + 2;
            } else {
                // this is the full string
                NomLabel jj;
                jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

                jj.setFont(gx.getFont());

                jj.setSize(m_labelMetrics.stringWidth(jj.getText()), m_labelMetrics.getAscent() + 4);
                this.add(jj);
                jj.setLocation(x, y);
                jj.setForeground(m_colorList.get(i % m_colorList.size()));

                x += ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo));
            }
        }

    }

    /**
     * Renders the legend for a numeric colouring attribute
     * 
     * @param gx the graphics context
     */
    protected void paintNumeric(Graphics gx) {

        setFonts(gx);
        if (m_HorizontalPad == 0) {
            setCindex(m_cIndex);
        }

        int w = this.getWidth();
        double rs = 15;
        double incr = 240.0 / (w - (m_HorizontalPad * 2));
        int hf = m_labelMetrics.getAscent();

        for (int i = m_HorizontalPad; i < (w - m_HorizontalPad); i++) {
            Color c = new Color((int) rs, 150, (int) (255 - rs));
            gx.setColor(c);
            gx.drawLine(i, 0, i, 0 + m_spectrumHeight);
            rs += incr;
        }

        int whole = (int) Math.abs(m_maxC);
        double decimal = Math.abs(m_maxC) - whole;
        int nondecimal;
        nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

        m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
        if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
            m_precisionC = 1;
        }

        String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1 + m_precisionC, m_precisionC);

        int mswc = m_labelMetrics.stringWidth(maxStringC);
        int tmsc = mswc;
        if (w > (2 * tmsc)) {
            gx.setColor(Color.black);
            gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5), w - m_HorizontalPad, (m_spectrumHeight + 5));

            gx.drawLine(w - m_HorizontalPad, (m_spectrumHeight + 5), w - m_HorizontalPad,
                    (m_spectrumHeight + 5 + m_tickSize));

            gx.drawString(maxStringC, (w - m_HorizontalPad) - (mswc / 2), (m_spectrumHeight + 5 + m_tickSize + hf));

            gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5), m_HorizontalPad,
                    (m_spectrumHeight + 5 + m_tickSize));

            whole = (int) Math.abs(m_minC);
            decimal = Math.abs(m_minC) - whole;
            nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

            m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math.abs(m_minC)) / Math.log(10)))) + 2 : 1;

            if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
                m_precisionC = 1;
            }

            maxStringC = Utils.doubleToString(m_minC, nondecimal + 1 + m_precisionC, m_precisionC);

            mswc = m_labelMetrics.stringWidth(maxStringC);
            gx.drawString(maxStringC, m_HorizontalPad - (mswc / 2), (m_spectrumHeight + 5 + m_tickSize + hf));

            // draw the middle value if there is space
            if (w > (3 * tmsc)) {
                double mid = m_minC + ((m_maxC - m_minC) / 2.0);
                gx.drawLine(m_HorizontalPad + ((w - (2 * m_HorizontalPad)) / 2), (m_spectrumHeight + 5),
                        m_HorizontalPad + ((w - (2 * m_HorizontalPad)) / 2), (m_spectrumHeight + 5 + m_tickSize));

                whole = (int) Math.abs(mid);
                decimal = Math.abs(mid) - whole;
                nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

                m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math.abs(mid)) / Math.log(10)))) + 2 : 1;
                if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
                    m_precisionC = 1;
                }

                maxStringC = Utils.doubleToString(mid, nondecimal + 1 + m_precisionC, m_precisionC);

                mswc = m_labelMetrics.stringWidth(maxStringC);
                gx.drawString(maxStringC, m_HorizontalPad + ((w - (2 * m_HorizontalPad)) / 2) - (mswc / 2),
                        (m_spectrumHeight + 5 + m_tickSize + hf));
            }
        }
    }

    /**
     * Renders this component
     * 
     * @param gx the graphics context
     */
    @Override
    public void paintComponent(Graphics gx) {
        super.paintComponent(gx);
        if (m_isEnabled) {
            if (m_isNumeric) {
                m_oldWidth = -9000; // done so that if change back to nom, it will
                // work
                this.removeAll();
                paintNumeric(gx);
            } else {
                if (m_Instances != null && m_Instances.numInstances() > 0 && m_Instances.numAttributes() > 0) {
                    if (m_oldWidth != this.getWidth()) {
                        this.removeAll();
                        m_oldWidth = this.getWidth();
                        paintNominal(gx);
                    }
                }
            }
        }
    }

    /**
     * Main method for testing this class.
     * 
     * @param args first argument must specify an arff file. Second can specify an
     *          optional index to colour labels on
     */
    public static void main(String[] args) {
        try {
            if (args.length < 1) {
                System.err.println("Usage : weka.gui.visualize.ClassPanel <dataset> " + "[class col]");
                System.exit(1);
            }
            final javax.swing.JFrame jf = new javax.swing.JFrame("Weka Explorer: Class");
            jf.setSize(500, 100);
            jf.getContentPane().setLayout(new BorderLayout());
            final ClassPanel p2 = new ClassPanel();
            jf.getContentPane().add(p2, BorderLayout.CENTER);
            jf.addWindowListener(new java.awt.event.WindowAdapter() {
                @Override
                public void windowClosing(java.awt.event.WindowEvent e) {
                    jf.dispose();
                    System.exit(0);
                }
            });

            if (args.length >= 1) {
                System.err.println("Loading instances from " + args[0]);
                java.io.Reader r = new java.io.BufferedReader(new java.io.FileReader(args[0]));
                Instances i = new Instances(r);
                i.setClassIndex(i.numAttributes() - 1);
                p2.setInstances(i);
            }
            if (args.length > 1) {
                p2.setCindex((Integer.parseInt(args[1])) - 1);
            } else {
                p2.setCindex(0);
            }
            jf.setVisible(true);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.err.println(ex.getMessage());
        }
    }
}