/* * 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 2 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, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * ClassPanel.java * Copyright (C) 2000 University of Waikato, Hamilton, New Zealand * */ package weka.gui.visualize; import weka.core.FastVector; import weka.core.Instances; import weka.core.Utils; 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.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JColorChooser; import javax.swing.JLabel; import javax.swing.JPanel; /** * 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: 5086 $ */ 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; /** 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 FastVector 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 FastVector m_Repainters = new FastVector(); /** An optional list of listeners who want to know when a colour changes. Listeners are notified via an ActionEvent */ private FastVector m_ColourChangeListeners = new FastVector(); /** 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() { public void mouseClicked(MouseEvent e) { if ((e.getModifiers() & e.BUTTON1_MASK) == e.BUTTON1_MASK) { Color tmp = JColorChooser.showDialog (ClassPanel.this, "Select new Color", (Color)m_colorList.elementAt(m_index)); if (tmp != null) { m_colorList.setElementAt(tmp, m_index); m_oldWidth = -9000; ClassPanel.this.repaint(); if (m_Repainters.size() > 0) { for (int i=0;i<m_Repainters.size();i++) { ((Component)(m_Repainters.elementAt(i))).repaint(); } } if (m_ColourChangeListeners.size() > 0) { for (int i = 0; i < m_ColourChangeListeners.size(); i++) { ((ActionListener)(m_ColourChangeListeners.elementAt(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 FastVector(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.addElement(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.addElement(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.addElement(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.addElement(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(FastVector 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; // do the first row int ci, mp; Color pc; int numToDo = ((legendHeight==1) ? numClasses : (numClasses/2)); for (int i=0;i<numToDo;i++) { gx.setColor((Color)m_colorList.elementAt(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((Color)m_colorList. elementAt(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((Color)m_colorList. elementAt(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((Color)m_colorList.elementAt(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((Color)m_colorList. elementAt(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((Color)m_colorList. elementAt(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 / (double)(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 */ 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() { 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()); } } }