In the KNIME framework the technique known as "linking and brushing" is called HiLiting. This means that whenever a datapoint is hilited in one view it is immediately also hilited in all other views displaying this data point. If we had a view that displayed the datapoints directly we would have to implement the HiLiteListener interface to be informed about any change in the hiliting. The HiLiteListener interface has three methods:
void hiLite(final KeyEvent event);
void unHiLite(final KeyEvent event);
void unHiLiteAll();
But since we have an aggregated view of the datapoints it does not make much sense to implement the HiLiteListener interface. We would rather hilite a bin and see all the datapoints in that bin hilited in other views. In the following we explain how this is implemented. First of all we have to prepare our NumericBin to know when it has been selected and if it is hilited. Therefore we simply introduce two flags to indicate the status:
public void setHilited(final boolean isHilite) {
m_isHilite = isHilite;
}
public boolean isHilited() {
return m_isHilite;
}
public boolean isSelected() {
return m_isSelected;
}
public void setSelected(final boolean selected) {
m_isSelected = selected;
}
The next step is to listen to the mouse events to be informed about whether a bin is selected or not. Then the bin has to know its graphical representation, i.e. the painted rectangle (otherwise we cannot know if it is clicked or not):
public Rectangle getViewRepresentation() {
return m_viewRepresentation;
}
public void setViewRepresentation(final Rectangle rectangle) {
m_viewRepresentation = rectangle;
}
In order to listen to mouse events we have to add a MouseListener in the NodeView's constructor to the drawing component. The selected bins are stored in a local datastructure m_selected:
...
m_selected = new HashSet();
m_panel.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(final MouseEvent e) {
if (!e.isControlDown()) {
m_selected.clear();
for (NumericBin bin : m_panel.getBins()) {
bin.setSelected(false);
}
}
for (NumericBin bin : m_panel.getBins()) {
if (bin.getViewRepresentation() != null &&
bin.getViewRepresentation().contains(
e.getX(), e.getY())){
bin.setSelected(true);
m_selected.add(bin);
break;
}
}
...
So far we are able to select one or more bins. If we want to hilite them we have to add a menu to the NodeView, to enable us to hilite or unhilite the selected bins, or clear the hiliting.
m_hilite = new JMenuItem(HiLiteHandler.HILITE_SELECTED);
m_hilite.setEnabled(false);
m_hilite.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
Set toBeHilited = new HashSet();
for (NumericBin bin : m_selected) {
toBeHilited.addAll(bin.getContainedRowIds());
bin.setHilited(true);
m_numberOfHilitedBins++;
}
getNodeModel().getInHiLiteHandler(
NumericBinnerNodeModel.IN_PORT).fireHiLiteEvent(toBeHilited);
m_panel.repaint();
}
});
m_unhilite = new JMenuItem(HiLiteHandler.UNHILITE_SELECTED);
m_unhilite.setEnabled(false);
m_unhilite.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
Set toBeUnhilited = new HashSet();
for (NumericBin bin : m_selected) {
toBeUnhilited.addAll(bin.getContainedRowIds());
bin.setHilited(false);
m_numberOfHilitedBins--;
}
getNodeModel().getInHiLiteHandler(
NumericBinnerNodeModel.IN_PORT).fireUnHiLiteEvent(toBeUnhilited);
m_panel.repaint();
}
});
JMenuItem clear = new JMenuItem(HiLiteHandler.CLEAR_HILITE);
clear.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
getNodeModel().getInHiLiteHandler(
NumericBinnerNodeModel.IN_PORT).fireClearHiLiteEvent();
for (NumericBin bin : m_panel.getBins()) {
bin.setHilited(false);
}
m_numberOfHilitedBins = 0;
m_panel.repaint();
}
});
JMenu menu = new JMenu(HiLiteHandler.HILITE);
menu.add(m_hilite);
menu.add(m_unhilite);
menu.add(clear);
getJMenuBar().add(menu);
...
The HiLiteHandler provides standard names for the menu items. The getJMenuBar method returns the MenuBar of the NodeView to which the additional menu can be added. To further improve our small human computer interface we can enable and disable the menu items dependent on the current selection and hilite status, i.e. the hilite menu entry should only be enabled when some bins are selected. And accourdingly should the unhilite menu entry only be enabled when some bins are selected and hilited. We add this functionality to the MouseListener. (By the way this is the reason, why the two menu items are local fields.)
public void mouseReleased(final MouseEvent e) {
...
if (m_selected.size() > 0) {
m_hilite.setEnabled(true);
} else {
m_hilite.setEnabled(false);
}
if (m_numberOfHilitedBins > 0 && m_selected.size() > 0) {
m_unhilite.setEnabled(true);
} else {
m_unhilite.setEnabled(false);
}
m_panel.repaint();
}
});
So far we are able to select and hilite (unhilite) the bins and the contained rows. But if you run the code implemented so far you immediately encounter the frustrating fact, that in our view you cannot distinguish between selected, hilited and normal bins. Thus, we have to add this to the paint method of the drawing component, the NumericBinnerViewPanel (it also shows how the graphical rectangle of the bins is updated in every paint):
...
Rectangle rect = new Rectangle(x, height - binHeight, binWidth,
binHeight);
m_bins[i].setViewRepresentation(rect);
Color color = Color.BLACK;
if (m_bins[i].isHilited()) {
color = ColorAttr.HILITE;
}
if (m_bins[i].isSelected()) {
color = ColorAttr.SELECTED;
}
if (m_bins[i].isHilited() && m_bins[i].isSelected()) {
color = ColorAttr.SELECTED_HILITE;
}
Graphics2D g2 = (Graphics2D)g;
g2.setColor(color);
g2.fillRect(rect.x+2, rect.y+2, rect.width-2, rect.height-2);
g2.setColor(Color.WHITE);
g2.setStroke(new BasicStroke(2));
g2.drawRect(rect.x, rect.y, rect.width, rect.height);
...
Now the view looks good and is correctly displayed if a bin is selected, hilited, both, or none. We use the KNIME standard colors defined in the ColorAttr to have a uniform coloring in all views.