weka.gui.visualize.ClassPanel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of weka-stable Show documentation
Show all versions of weka-stable Show documentation
The Waikato Environment for Knowledge Analysis (WEKA), a machine
learning workbench. This is the stable version. Apart from bugfixes, this version
does not receive any other updates.
/*
* 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 .
*/
/*
* 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 ([email protected])
* @author Malcolm Ware ([email protected])
* @version $Revision: 10222 $
*/
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 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 m_Repainters = new ArrayList();
/**
* An optional list of listeners who want to know when a colour changes.
* Listeners are notified via an ActionEvent
*/
private final ArrayList m_ColourChangeListeners = new ArrayList();
/** 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(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 ActionListener
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 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 "
+ "[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());
}
}
}