Java tutorial
/* * 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()); } } }