/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.viewer.metadata; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Vector; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultComboBoxModel; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.jdesktop.swingx.prompt.PromptSupport; import com.rapidminer.example.Attribute; import com.rapidminer.example.AttributeRole; import com.rapidminer.example.ExampleSet; import com.rapidminer.example.set.ExampleSetUtilities; import com.rapidminer.gui.actions.export.PrintableComponent; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ScrollableJPopupMenu; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.TextFieldWithAction; import com.rapidminer.gui.tools.components.DropDownPopupButton; import com.rapidminer.gui.tools.components.DropDownPopupButton.PopupMenuProvider; import com.rapidminer.gui.viewer.metadata.actions.CopyAllMetaDataToClipboardAction; import com.rapidminer.gui.viewer.metadata.actions.ShowConstructionValueAction; import com.rapidminer.gui.viewer.metadata.event.AttributeStatisticsEvent; import com.rapidminer.gui.viewer.metadata.event.AttributeStatisticsEvent.EventType; import com.rapidminer.gui.viewer.metadata.event.AttributeStatisticsEventListener; import com.rapidminer.gui.viewer.metadata.event.MetaDataStatisticsEvent; import com.rapidminer.gui.viewer.metadata.event.MetaDataStatisticsEventListener; import com.rapidminer.gui.viewer.metadata.model.AbstractAttributeStatisticsModel; import com.rapidminer.gui.viewer.metadata.model.DateTimeAttributeStatisticsModel; import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel; import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel.SortingDirection; import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel.SortingType; import com.rapidminer.gui.viewer.metadata.model.NominalAttributeStatisticsModel; import com.rapidminer.gui.viewer.metadata.model.NumericalAttributeStatisticsModel; import com.rapidminer.report.Renderable; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.Ontology; /** * This is the GUI to display meta data statistics for {@link ExampleSet}s. Note that EDT blocking * is minimal because the GUI for each {@link Attribute} and the subsequent population with the data * are done in {@link SwingWorker}s. The attributes are displayed via a pagination system, i.e. when * more than {@link MetaDataStatisticsModel#PAGE_SIZE} attributes exist they are displayed on pages * which show up to {@link MetaDataStatisticsModel#PAGE_SIZE}. This prevents performance problems * even when the number of attributes exceeds <code>100,000</code>. * <p> * The GUI itself does allow filtering as well as sorting via attribute type, name and missing * values. * * @author Marco Boeck * */ public class MetaDataStatisticsViewer extends JPanel implements Renderable, PrintableComponent { private static final long serialVersionUID = -1027619839144846140L; /** the background color */ private static final Color COLOR_BACKGROUND = Color.WHITE; /** the dimension for the attribute name header */ private static final Dimension DIMENSION_HEADER_ATTRIBUTE_NAME = new Dimension(200, 30); /** the dimension for the attribute type header */ private static final Dimension DIMENSION_HEADER_TYPE = new Dimension(90, 20); /** the dimension for the attribute missings label */ private static final Dimension DIMENSION_HEADER_MISSINGS = new Dimension(75, 20); /** the minimum size for the search field */ private static final Dimension DIMENSION_SEARCH_FIELD = new Dimension(140, 20); /** minimum margin which must be left when enlarging the name column */ private static final int RESIZE_MARGIN_ENLARGE = 450; /** minimum margin which must be left when shrinking the name column */ private static final int RESIZE_MARGIN_SHRINK = 50; /** the font size of the special/regular labels */ private static final float FONT_SIZE_LABEL = 12; /** arrow icon with two arrows pointing left */ private static final ImageIcon ICON_ARROW_FIRST = SwingTools.createIcon("16/" + "navigate_left2.png"); /** arrow icon with an arrow pointing left */ static final ImageIcon ICON_ARROW_LEFT = SwingTools.createIcon("16/" + "navigate_left.png"); /** arrow icon with an arrow pointing right */ static final ImageIcon ICON_ARROW_RIGHT = SwingTools.createIcon("16/" + "navigate_right.png"); /** arrow icon with two arrows pointing right */ static final ImageIcon ICON_ARROW_LAST = SwingTools.createIcon("16/" + "navigate_right2.png"); /** arrow icon with an arrow pointing up */ static final ImageIcon ICON_ARROW_UP = SwingTools.createIcon("16/" + "navigate_up.png"); /** arrow icon with an arrow pointing down */ static final ImageIcon ICON_ARROW_DOWN = SwingTools.createIcon("16/" + "navigate_down.png"); /** * icon used in the {@link TextFieldWithAction} when the filter remove action is hovered */ private final ImageIcon CLEAR_FILTER_HOVERED_ICON = SwingTools.createIcon("16/x-mark_orange.png"); /** * the delay before filtering is started after the user finished typing in milliseconds: * * * * * * {@value} */ private static final int FILTER_TIMER_DELAY = 500; /** the identifier of the search focus action */ private static final String ACTION_NAME_SEARCH = "focusSearchField"; /** the client property key to indicate the scrollbar should scroll down */ private static final String PROPERTY_SCROLL_DOWN = "scrollDown"; /** header panel which contains column header components */ private JPanel columnHeaderPanel; /** the attribute name sorting label */ private JLabel sortingLabelAttName; /** the attribute type sorting label */ private JLabel sortingLabelAttType; /** the attribute missing count sorting label */ private JLabel sortingLabelAttMissings; /** the resize label for the name column */ private JLabel resizeNameColumnLabel; /** * the label displaying the number of currently displayed attributes (taking filters into * account) */ private JLabel filterLabel; /** the panel housing the attribute panels */ private JPanel attPanel; /** the loading placeholder label */ private JLabel labelLoading; /** the label indicating no attributes are there */ private JLabel labelNoAttributes; /** the label displaying the currently shown attributes on the page */ private JLabel labelDisplayedAttribute; /** the combobox used for pagination */ private JComboBox<Integer> pagesComboBox; /** the page change listener for the page combobox */ private ItemListener pageComboListener; /** the panel housing the pagination elements */ private JPanel paginationPanel; /** the button to switch to the first page */ private JButton buttonFirstPage; /** the button to switch to the previous page */ private JButton buttonPreviousPage; /** the button to switch to the next page */ private JButton buttonNextPage; /** the button to switch to the last page */ private JButton buttonLastPage; /** the {@link GridBagConstraints} for the attribute panel */ private GridBagConstraints gbcAttPanel; /** the backing model of the GUI */ private final MetaDataStatisticsModel model; /** the controller between GUI and model */ private final MetaDataStatisticsController controller; /** the scrollpane in which this entire panel is placed */ private JScrollPane scrollPane; /** if set, the custom dimension for the attribute name column */ private Dimension nameDim; /** * this map maps the respective {@link AttributeStatisticsPanel} to their {@link Integer} index */ private final Map<Integer, AttributeStatisticsPanel> mapOfAttributeStatisticsPanels; private JPanel outerPanel; private final class HoverBorderMouseListener extends MouseAdapter { private final JButton button; public HoverBorderMouseListener(final JButton pageButton) { this.button = pageButton; } @Override public void mouseReleased(final MouseEvent e) { if (!button.isEnabled()) { button.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); } super.mouseReleased(e); } @Override public void mouseExited(final MouseEvent e) { button.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); super.mouseExited(e); } @Override public void mouseEntered(final MouseEvent e) { if (button.isEnabled()) { button.setBorder(BorderFactory.createLineBorder(Color.gray, 1, true)); } super.mouseEntered(e); } } private final class ResizeAttributeNameMouseListener extends MouseAdapter { int startingX = -1; @Override public void mousePressed(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } // we somehow missed the drag end event, so finish resize now if (startingX > -1) { int diff = e.getX() - startingX; resizeNameColumn(diff, true); } // start new resize in any case startingX = e.getX(); } @Override public void mouseDragged(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } // if dragging is in progress, visualize in header column if (startingX > -1) { int diff = e.getX() - startingX; resizeNameColumn(diff, false); } } @Override public void mouseReleased(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } // if a new drag was started it ends here if (startingX > -1) { int diff = e.getX() - startingX; resizeNameColumn(diff, true); } startingX = -1; } /** * Called when resizing event occurs to resize the attribute name column. Resizing is * restricted to a mininmum and a maximum amount depending on the actual size of the header * panel. * * @param diff * the horizontal movement in px (can be negative to shrink). * @param resizeStatisticPanels * if <code>true</code> will update the ASPs as well (costly) * */ private void resizeNameColumn(int diff, boolean resizeStatisticPanels) { if (diff != 0) { if (nameDim == null) { nameDim = new Dimension(DIMENSION_HEADER_ATTRIBUTE_NAME.width + diff, DIMENSION_HEADER_ATTRIBUTE_NAME.height); } else { int newWidth = nameDim.width + diff; int minWidth = RESIZE_MARGIN_SHRINK; // max width is the size of the header panel minus the size // of the other // elements to the right int maxWidth = columnHeaderPanel.getWidth() - (DIMENSION_HEADER_MISSINGS.width + DIMENSION_HEADER_TYPE.width + DIMENSION_SEARCH_FIELD.width + RESIZE_MARGIN_ENLARGE); // do not allow shrinking/enlarging over a limit to prevent // GUI breaking if (newWidth > maxWidth) { newWidth = maxWidth; } if (newWidth < minWidth) { newWidth = minWidth; } nameDim = new Dimension(newWidth, nameDim.height); } sortingLabelAttName.setMinimumSize(nameDim); sortingLabelAttName.setPreferredSize(nameDim); // update header panel columnHeaderPanel.revalidate(); columnHeaderPanel.repaint(); } if (resizeStatisticPanels) { revalidateAttributePanels(); } } }; /** * Creates a new {@link MetaDataStatisticsViewer} instance. * * @param model * the {@link MetaDataStatisticsModel} backing the GUI */ public MetaDataStatisticsViewer(final MetaDataStatisticsModel model) { this.model = model; this.controller = new MetaDataStatisticsController(this, model); mapOfAttributeStatisticsPanels = new HashMap<>(); MetaDataStatisticsEventListener listener = new MetaDataStatisticsEventListener() { @Override public void modelChanged(final MetaDataStatisticsEvent e) { switch (e.getEventType()) { case INIT_DONE: revalidateAttributePanels(); break; case FILTER_CHANGED: revalidateAttributePanels(); break; case ORDER_CHANGED: // this is fired when UpdateQueue triggers, which is // outside the EDT SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateSortingIcons(); revalidateAttributePanels(); } }); break; case PAGINATION_CHANGED: revalidateAttributePanels(); break; default: break; } } }; model.registerEventListener(listener); initGUI(); createAttributeStatisticsPanels(); } /** * Creates all {@link AttributeStatisticsPanel}s in a {@link SwingWorker}. */ private void createAttributeStatisticsPanels() { final SwingWorker<Void, AttributeStatisticsPanel> worker = new SwingWorker<Void, AttributeStatisticsPanel>() { @Override protected Void doInBackground() throws Exception { // prepare attribute lists and settings List<AbstractAttributeStatisticsModel> orderedAttributeStatisticsModelList = new LinkedList<>(); List<Attribute> listOfAttributes = new LinkedList<>(); List<AttributeRole> listOfAttributeRoles = new ArrayList<>( model.getExampleSetOrNull().getAttributes().specialSize()); Iterator<AttributeRole> specialAttributes = model.getExampleSetOrNull().getAttributes().specialAttributes(); while (specialAttributes.hasNext()) { listOfAttributeRoles.add(specialAttributes.next()); } Collections.sort(listOfAttributeRoles, ExampleSetUtilities.SPECIAL_ATTRIBUTES_ROLE_COMPARATOR); for (int i = 0; i < listOfAttributeRoles.size(); i++) { listOfAttributes.add(listOfAttributeRoles.get(i).getAttribute()); } Iterator<AttributeRole> regularAttributes = model.getExampleSetOrNull().getAttributes().regularAttributes(); while (regularAttributes.hasNext()) { listOfAttributes.add(regularAttributes.next().getAttribute()); } // we want to be notified of enlarge events, because we want to // keep the scrollbar // at the bottom AttributeStatisticsEventListener attEventListener = new AttributeStatisticsEventListener() { @Override public void modelChanged(final AttributeStatisticsEvent e) { if (e.getEventType() == EventType.ENLARGED_CHANGED) { JScrollBar scrollBar = scrollPane.getVerticalScrollBar(); // if the scrollbar was at the bottom before, ask it // to place itself at // the bottom again next adjustment if (scrollBar.getValue() + scrollBar.getHeight() >= scrollBar.getMaximum()) { scrollBar.putClientProperty(PROPERTY_SCROLL_DOWN, true); } } } }; // iterate over all attributes, create models for them for (Attribute att : listOfAttributes) { AbstractAttributeStatisticsModel statModel; if (att.isNumerical()) { statModel = new NumericalAttributeStatisticsModel(model.getExampleSetOrNull(), att); } else if (att.isNominal()) { statModel = new NominalAttributeStatisticsModel(model.getExampleSetOrNull(), att); } else { statModel = new DateTimeAttributeStatisticsModel(model.getExampleSetOrNull(), att); } statModel.registerEventListener(attEventListener); orderedAttributeStatisticsModelList.add(statModel); } controller.setAttributeStatisticsModels(orderedAttributeStatisticsModelList); // iterate over all attributes and create the GUI for them int index = 0; for (int i = 0; i < Math.min(MetaDataStatisticsModel.PAGE_SIZE, orderedAttributeStatisticsModelList.size()); i++) { AttributeStatisticsPanel asp = new AttributeStatisticsPanel(); mapOfAttributeStatisticsPanels.put(index++, asp); publish(asp); } controller.waitAtBarrier(); return null; } @Override public void process(final List<AttributeStatisticsPanel> list) { // add the AttributeStatisticsPanel on the attribute panel to // display them for (AttributeStatisticsPanel asp : list) { gbcAttPanel.gridy += 1; attPanel.add(asp, gbcAttPanel); } } @Override protected void done() { try { // do this to see if any errors popped up while doing the // above get(); // remove placeholder attPanel.remove(labelLoading); // once all are done refresh the GUI so they are shown MetaDataStatisticsViewer.this.revalidate(); MetaDataStatisticsViewer.this.repaint(); // allow resizing now resizeNameColumnLabel.setVisible(true); } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.meta_data_view.calc_error", e); } } }; worker.execute(); } /** * Setup the GUI. This does NOT include creating the {@link AttributeStatisticsPanel}s, as that * is done via a {@link SwingWorker} above. Reason is that we do not want to risk GUI freezes. * */ private void initGUI() { outerPanel = new JPanel(); outerPanel.setLayout(new GridBagLayout()); outerPanel.setBackground(COLOR_BACKGROUND); attPanel = new JPanel(); attPanel.setOpaque(false); attPanel.setLayout(new GridBagLayout()); JPanel footerPanel = new JPanel(); footerPanel.setLayout(new GridBagLayout()); footerPanel.setOpaque(false); footerPanel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TEXTFIELD_BORDER)); GridBagConstraints gbcOuter = new GridBagConstraints(); gbcOuter.insets = new Insets(5, 30, 5, 30); gbcOuter.anchor = GridBagConstraints.WEST; gbcOuter.fill = GridBagConstraints.NONE; gbcOuter.weightx = 1.0; gbcOuter.gridx = 0; gbcOuter.gridy = 0; // create attribute 'column' headers columnHeaderPanel = new JPanel(); columnHeaderPanel.setOpaque(false); columnHeaderPanel.setLayout(new GridBagLayout()); columnHeaderPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 5)); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(2, 53, 2, 10); gbc.weightx = 0.0; gbc.anchor = GridBagConstraints.CENTER; gbc.fill = GridBagConstraints.NONE; sortingLabelAttName = new JLabel( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.name.label")); sortingLabelAttName .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.name.tip")); sortingLabelAttName.setMinimumSize(DIMENSION_HEADER_ATTRIBUTE_NAME); sortingLabelAttName.setPreferredSize(DIMENSION_HEADER_ATTRIBUTE_NAME); sortingLabelAttName.setFont(sortingLabelAttName.getFont().deriveFont(FONT_SIZE_LABEL)); sortingLabelAttName.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); sortingLabelAttName.setHorizontalTextPosition(SwingConstants.LEADING); sortingLabelAttName.addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } controller.cycleAttributeNameSorting(); } }); columnHeaderPanel.add(sortingLabelAttName, gbc); // this label can be dragged and dropped to resize the attribute name // column resizeNameColumnLabel = new JLabel( SwingTools.createIcon("16/" + I18N.getGUIMessage("gui.label.meta_data_stats.resize.icon"))); resizeNameColumnLabel.setToolTipText(I18N.getGUIMessage("gui.label.meta_data_stats.resize.tip")); resizeNameColumnLabel.setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); // only show once GUI is initialized resizeNameColumnLabel.setVisible(false); ResizeAttributeNameMouseListener resizeMouseListener = new ResizeAttributeNameMouseListener(); resizeNameColumnLabel.addMouseListener(resizeMouseListener); resizeNameColumnLabel.addMouseMotionListener(resizeMouseListener); gbc.gridx += 1; gbc.insets = new Insets(0, 10, 0, 10); columnHeaderPanel.add(resizeNameColumnLabel, gbc); gbc.gridx += 1; gbc.insets = new Insets(2, 10, 2, 10); sortingLabelAttType = new JLabel( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.type.label")); sortingLabelAttType .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.type.tip")); sortingLabelAttType.setMinimumSize(DIMENSION_HEADER_TYPE); sortingLabelAttType.setPreferredSize(DIMENSION_HEADER_TYPE); sortingLabelAttType.setFont(sortingLabelAttType.getFont().deriveFont(FONT_SIZE_LABEL)); sortingLabelAttType.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); sortingLabelAttType.setHorizontalTextPosition(SwingConstants.LEADING); sortingLabelAttType.addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } controller.cycleAttributeTypeSorting(); } }); columnHeaderPanel.add(sortingLabelAttType, gbc); gbc.gridx += 1; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0.0; sortingLabelAttMissings = new JLabel( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.missing.label")); sortingLabelAttMissings .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.missing.tip")); sortingLabelAttMissings.setMinimumSize(DIMENSION_HEADER_MISSINGS); sortingLabelAttMissings.setPreferredSize(DIMENSION_HEADER_MISSINGS); sortingLabelAttMissings.setFont(sortingLabelAttMissings.getFont().deriveFont(FONT_SIZE_LABEL)); sortingLabelAttMissings.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); sortingLabelAttMissings.setHorizontalTextPosition(SwingConstants.LEADING); sortingLabelAttMissings.addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } controller.cycleAttributeMissingSorting(); } }); columnHeaderPanel.add(sortingLabelAttMissings, gbc); gbc.gridx += 1; gbc.insets = new Insets(2, 10, 2, 10); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; JLabel labelAttStats = new JLabel( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.headers.stats.label")); labelAttStats.setFont(labelAttStats.getFont().deriveFont(FONT_SIZE_LABEL)); columnHeaderPanel.add(labelAttStats, gbc); // create dropdown filters filterLabel = new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.filters.label")); gbc.gridx += 1; gbc.weightx = 0.0; gbc.fill = GridBagConstraints.VERTICAL; gbc.insets = new Insets(2, 5, 2, 10); columnHeaderPanel.add(filterLabel, gbc); List<JCheckBox> listOfValueTypeCheckboxses = new LinkedList<>(); for (final String valueTypeName : Ontology.ATTRIBUTE_VALUE_TYPE.getNames()) { // we only want filters for numerical/nominal/date_time if (Ontology.ATTRIBUTE_VALUE_TYPE.mapName(valueTypeName) != Ontology.NUMERICAL && Ontology.ATTRIBUTE_VALUE_TYPE.mapName(valueTypeName) != Ontology.NOMINAL && Ontology.ATTRIBUTE_VALUE_TYPE.mapName(valueTypeName) != Ontology.DATE_TIME) { continue; } String valueTypeString = valueTypeName; valueTypeString = valueTypeString.replaceAll("_", " "); final JCheckBox filterValueTypeCheckbox = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.value_type.label", valueTypeString)); filterValueTypeCheckbox.setSelected(true); filterValueTypeCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setAttributeTypeVisibility(Ontology.ATTRIBUTE_VALUE_TYPE.mapName(valueTypeName), filterValueTypeCheckbox.isSelected()); } }); listOfValueTypeCheckboxses.add(filterValueTypeCheckbox); } final JCheckBox filterMissingsCheckbox = new JCheckBox( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.missings.label")); filterMissingsCheckbox.setSelected(false); filterMissingsCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setShowOnlyMissingsAttributes(filterMissingsCheckbox.isSelected()); } }); final JCheckBox filterSpecialCheckbox = new JCheckBox( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.special.label")); filterSpecialCheckbox.setSelected(true); filterSpecialCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setShowSpecialAttributes(filterSpecialCheckbox.isSelected()); } }); final JCheckBox filterRegularCheckbox = new JCheckBox( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.regular.label")); filterRegularCheckbox.setSelected(true); filterRegularCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setShowRegularAttributes(filterRegularCheckbox.isSelected()); } }); final ScrollableJPopupMenu filterMenu = new ScrollableJPopupMenu(); for (JCheckBox valueTypeBox : listOfValueTypeCheckboxses) { filterMenu.add(valueTypeBox); } filterMenu.addSeparator(); filterMenu.add(filterMissingsCheckbox); filterMenu.addSeparator(); filterMenu.add(filterSpecialCheckbox); filterMenu.add(filterRegularCheckbox); final JTextField filterNameField = new JTextField(10); filterNameField.setMinimumSize(new Dimension(300, 20)); filterNameField.setPreferredSize(new Dimension(300, 20)); final ResourceAction filterAction = new ResourceAction(true, "meta_data_stats.filter") { private static final long serialVersionUID = 5334802828535128169L; @Override public void actionPerformed(final ActionEvent e) { controller.setFilterNameString(filterNameField.getText()); } }; filterNameField.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter_field.tip")); filterNameField.addActionListener(filterAction); filterNameField.getDocument().addDocumentListener(new DocumentListener() { private Timer updateTimer; { updateTimer = new Timer(FILTER_TIMER_DELAY, filterAction); updateTimer.setRepeats(false); } @Override public void removeUpdate(final DocumentEvent e) { updateTimer.restart(); } @Override public void insertUpdate(final DocumentEvent e) { updateTimer.restart(); } @Override public void changedUpdate(final DocumentEvent e) { updateTimer.restart(); } }); filterNameField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), ACTION_NAME_SEARCH); filterNameField.getActionMap().put(ACTION_NAME_SEARCH, new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(final ActionEvent e) { filterNameField.requestFocusInWindow(); } }); PromptSupport.setPrompt(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter_field.prompt"), filterNameField); PromptSupport.setFontStyle(Font.ITALIC, filterNameField); ResourceAction deleteFilterAction = new ResourceAction(true, "meta_data_stats.filter_delete") { private static final long serialVersionUID = 8540175790623212824L; @Override public void actionPerformed(final ActionEvent e) { filterNameField.setText(""); } }; gbc.gridx += 1; gbc.insets = new Insets(2, 0, 2, 0); TextFieldWithAction searchField = new TextFieldWithAction(filterNameField, deleteFilterAction, CLEAR_FILTER_HOVERED_ICON); searchField.setMinimumSize(DIMENSION_SEARCH_FIELD); searchField.setPreferredSize(DIMENSION_SEARCH_FIELD); columnHeaderPanel.add(searchField, gbc); final DropDownPopupButton filterDropdownButton = new DropDownPopupButton("gui.label.meta_data_stats.filter_select", new PopupMenuProvider() { @Override public JPopupMenu getPopupMenu() { return filterMenu; } }); filterDropdownButton.setArrowSize(15); filterDropdownButton.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); filterDropdownButton.addMouseListener(new HoverBorderMouseListener(filterDropdownButton)); gbc.gridx += 1; gbc.fill = GridBagConstraints.VERTICAL; gbc.weightx = 0.0; gbc.insets = new Insets(2, 5, 2, 5); columnHeaderPanel.add(filterDropdownButton, gbc); // add copy all meta data to clipboard popup MouseListener copyMetaDataListener = new MouseAdapter() { @Override public void mouseReleased(final MouseEvent e) { handlePopup(e); } @Override public void mousePressed(final MouseEvent e) { handlePopup(e); } private void handlePopup(final MouseEvent e) { if (e.isPopupTrigger()) { JPopupMenu menu = new JPopupMenu(); menu.add(new CopyAllMetaDataToClipboardAction(model)); menu.add(new ShowConstructionValueAction(model)); menu.show(e.getComponent(), e.getX(), e.getY()); } } }; columnHeaderPanel.addMouseListener(copyMetaDataListener); attPanel.addMouseListener(copyMetaDataListener); outerPanel.addMouseListener(copyMetaDataListener); // prepare attributes GUI gbcAttPanel = (GridBagConstraints) gbcOuter.clone(); gbcAttPanel.gridx = 0; gbcAttPanel.gridy = 0; gbcAttPanel.fill = GridBagConstraints.HORIZONTAL; gbcAttPanel.insets = new Insets(3, 0, 0, 0); gbcAttPanel.weightx = 1.0; // add placeholder loading label to top of panel labelLoading = new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.loading.label")); labelLoading.setIcon(SwingTools .createIcon("16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.loading.icon"))); labelLoading.setHorizontalAlignment(SwingConstants.CENTER); attPanel.add(labelLoading, gbcAttPanel); // add no attributes label labelNoAttributes = new JLabel( I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.empty_filtered_attributes.label")); labelNoAttributes.setVisible(false); labelNoAttributes.setIcon(SwingTools.createIcon( "16/" + I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.empty_filtered_attributes.icon"))); labelNoAttributes.setHorizontalAlignment(SwingConstants.CENTER); gbcAttPanel.gridy += 1; attPanel.add(labelNoAttributes, gbcAttPanel); // add to outer panel gbcOuter.gridy += 1; gbcOuter.insets = new Insets(0, 10, 5, 10); gbcOuter.fill = GridBagConstraints.HORIZONTAL; outerPanel.add(attPanel, gbcOuter); // add filler so elements added stay at the top gbcOuter.weighty = 1.0; gbcOuter.insets = new Insets(20, 0, 5, 0); gbcOuter.fill = GridBagConstraints.BOTH; gbcOuter.gridy += 1; outerPanel.add(Box.createVerticalBox(), gbcOuter); // footer panel JPanel footerStatPanel = new JPanel(); footerStatPanel.setOpaque(false); footerStatPanel.setLayout(new GridBagLayout()); GridBagConstraints gbcFooterStat = new GridBagConstraints(); gbcFooterStat.gridx = 0; gbcFooterStat.gridy = 0; gbcFooterStat.weightx = 1.0; gbcFooterStat.fill = GridBagConstraints.HORIZONTAL; footerStatPanel.add(Box.createVerticalBox(), gbcFooterStat); String countExaHeader = I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.count_examples.label", model.getExampleSetOrNull().size()); JLabel countExaLabel = new JLabel(countExaHeader); gbcFooterStat.gridx += 1; gbcFooterStat.insets = new Insets(2, 5, 2, 5); gbcFooterStat.weightx = 0.0; gbcFooterStat.fill = GridBagConstraints.NONE; footerStatPanel.add(countExaLabel, gbcFooterStat); String countSpecAttHeader = I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.count_att_special.label", model.getExampleSetOrNull().getAttributes().specialSize()); JLabel countSpecAttLabel = new JLabel(countSpecAttHeader); gbcFooterStat.gridx += 1; footerStatPanel.add(countSpecAttLabel, gbcFooterStat); String countRegAttHeader = I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.count_att_regular.label", model.getExampleSetOrNull().getAttributes().size()); JLabel countRegAttLabel = new JLabel(countRegAttHeader); gbcFooterStat.gridx += 1; gbcFooterStat.insets = new Insets(2, 5, 2, 10); footerStatPanel.add(countRegAttLabel, gbcFooterStat); // add footer stat panel GridBagConstraints gbcFooter = new GridBagConstraints(); gbcFooter.gridx = 0; gbcFooter.gridy = 0; gbcFooter.fill = GridBagConstraints.HORIZONTAL; gbcFooter.weightx = 1.0; gbcFooter.insets = new Insets(2, 10, 5, 0); labelDisplayedAttribute = new JLabel("..."); labelDisplayedAttribute.setMinimumSize(footerStatPanel.getPreferredSize()); labelDisplayedAttribute.setPreferredSize(footerStatPanel.getPreferredSize()); labelDisplayedAttribute.setHorizontalTextPosition(SwingConstants.LEFT); footerPanel.add(labelDisplayedAttribute, gbcFooter); paginationPanel = new JPanel(); paginationPanel.setOpaque(false); paginationPanel.setLayout(new GridBagLayout()); buttonFirstPage = new JButton(ICON_ARROW_FIRST); buttonFirstPage.setContentAreaFilled(false); buttonFirstPage.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.page_first.tip")); buttonFirstPage.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setCurrentPageIndexToFirstPage(); } }); buttonFirstPage.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); buttonFirstPage.addMouseListener(new HoverBorderMouseListener(buttonFirstPage)); GridBagConstraints gbcPagination = new GridBagConstraints(); gbcPagination.gridx = 0; gbcPagination.gridy = 0; paginationPanel.add(buttonFirstPage, gbcPagination); buttonPreviousPage = new JButton(ICON_ARROW_LEFT); buttonPreviousPage.setContentAreaFilled(false); buttonPreviousPage .setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.page_previous.tip")); buttonPreviousPage.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.decrementCurrentPageIndex(); } }); buttonPreviousPage.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); buttonPreviousPage.addMouseListener(new HoverBorderMouseListener(buttonPreviousPage)); gbcPagination.gridx += 1; paginationPanel.add(buttonPreviousPage, gbcPagination); pageComboListener = new ItemListener() { @Override public void itemStateChanged(final ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { controller.jumpToHumanPageIndex((Integer) pagesComboBox.getSelectedItem()); } } }; pagesComboBox = new JComboBox<>(); pagesComboBox.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.page_select.tip")); pagesComboBox.addItemListener(pageComboListener); gbcPagination.gridx += 1; paginationPanel.add(pagesComboBox, gbcPagination); buttonNextPage = new JButton(ICON_ARROW_RIGHT); buttonNextPage.setContentAreaFilled(false); buttonNextPage.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.page_next.tip")); buttonNextPage.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.incrementCurrentPageIndex(); } }); buttonNextPage.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); buttonNextPage.addMouseListener(new HoverBorderMouseListener(buttonNextPage)); gbcPagination.gridx += 1; paginationPanel.add(buttonNextPage, gbcPagination); buttonLastPage = new JButton(ICON_ARROW_LAST); buttonLastPage.setContentAreaFilled(false); buttonLastPage.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.page_last.tip")); buttonLastPage.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { controller.setCurrentPageIndexToLastPage(); } }); buttonLastPage.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); buttonLastPage.addMouseListener(new HoverBorderMouseListener(buttonLastPage)); gbcPagination.gridx += 1; paginationPanel.add(buttonLastPage, gbcPagination); gbcFooter.gridx += 1; gbcFooter.fill = GridBagConstraints.NONE; gbcFooter.weightx = 0.0; gbcFooter.insets = new Insets(0, 0, 0, 0); gbcFooter.anchor = GridBagConstraints.CENTER; footerPanel.add(paginationPanel, gbcFooter); // add filler gbcFooter.gridx += 1; gbcFooter.fill = GridBagConstraints.HORIZONTAL; gbcFooter.weightx = 1.0; footerPanel.add(footerStatPanel, gbcFooter); // build main GUI setLayout(new BorderLayout()); // add header panel add(columnHeaderPanel, BorderLayout.NORTH); // add outer panel which contains all attribute stat panels to // scrollpane scrollPane = new ExtendedJScrollPane(outerPanel); scrollPane.setOpaque(false); scrollPane.setBorder(BorderFactory.createEmptyBorder()); scrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() { @Override public void adjustmentValueChanged(final AdjustmentEvent e) { JScrollBar scrollBar = scrollPane.getVerticalScrollBar(); if (scrollBar.getClientProperty(PROPERTY_SCROLL_DOWN) != null) { scrollBar.setValue(scrollBar.getMaximum()); scrollBar.putClientProperty(PROPERTY_SCROLL_DOWN, null); } } }); add(scrollPane, BorderLayout.CENTER); // add footer panel add(footerPanel, BorderLayout.SOUTH); setBackground(COLOR_BACKGROUND); } @Override public void prepareRendering() {} @Override public void finishRendering() {} @Override public void render(final Graphics graphics, final int width, final int height) { paintComponent(graphics); } @Override public int getRenderWidth(final int preferredWidth) { return (int) getPreferredSize().getWidth(); } @Override public int getRenderHeight(final int preferredHeight) { return (int) getPreferredSize().getHeight(); } /** * Updates the sorting icons. */ private void updateSortingIcons() { for (SortingType type : SortingType.values()) { SortingDirection direction = model.getSortingDirection(type); ImageIcon icon; switch (direction) { case DESCENDING: icon = ICON_ARROW_DOWN; break; case ASCENDING: icon = ICON_ARROW_UP; break; case UNDEFINED: icon = null; break; default: icon = null; } switch (type) { case NAME: sortingLabelAttName.setIcon(icon); break; case TYPE: sortingLabelAttType.setIcon(icon); break; case MISSING: sortingLabelAttMissings.setIcon(icon); break; default: sortingLabelAttName.setIcon(null); sortingLabelAttType.setIcon(null); sortingLabelAttMissings.setIcon(null); } } } /** * Reorders the attribute panels according to the current sorting and respect filtering. */ private void revalidateAttributePanels() { // update pagination system to current data int pages = model.getNumberOfPages(); Vector<Integer> pagesVector = new Vector<>(); for (int j = 1; j <= pages; j++) { pagesVector.add(j); } if (pagesComboBox.getSelectedItem() != null) { pagesComboBox.setModel(new DefaultComboBoxModel<>(pagesVector)); // try to restore selection after model change pagesComboBox.removeItemListener(pageComboListener); pagesComboBox.setSelectedItem(model.getCurrentPageIndex() + 1); pagesComboBox.addItemListener(pageComboListener); } else { pagesComboBox.setModel(new DefaultComboBoxModel<>(pagesVector)); } updatePagingDisplay(); int index = 0; List<AbstractAttributeStatisticsModel> orderedList = controller.getPagedAndVisibleAttributeStatisticsModels(); for (AbstractAttributeStatisticsModel statModel : orderedList) { mapOfAttributeStatisticsPanels.get(index++).setModel(statModel, true); } // update alternating and visibility int i = 0; for (AbstractAttributeStatisticsModel statModel : orderedList) { mapOfAttributeStatisticsPanels.get(i).setVisible(true); statModel.setAlternating(i % 2 == 1); i++; } // we may have less than Math.min(size, PAGE_SIZE) panels to show, hide // unused ones for (; i < Math.min(model.getTotalSize(), MetaDataStatisticsModel.PAGE_SIZE); i++) { mapOfAttributeStatisticsPanels.get(i).setVisible(false); } // show a "no attributes visible in filter" when nothing is to display if (model.getVisibleSize() == 0 && model.isFiltering()) { labelNoAttributes.setVisible(true); } else { labelNoAttributes.setVisible(false); } // make sure attribute name column width is updated for (AttributeStatisticsPanel asp : mapOfAttributeStatisticsPanels.values()) { // sync size of header column with actual ASP columns asp.updateNameColumnWidth(nameDim); } // repaint everything revalidate(); repaint(); } /** * Updates the paging buttons state as well as the label displaying the currently visible number * of attributes. */ private void updatePagingDisplay() { // only enable first page button if we are not on the first page buttonFirstPage.setEnabled(model.getCurrentPageIndex() != 0); // only enable previous button if there are previous pages buttonPreviousPage.setEnabled(model.getCurrentPageIndex() != 0); // only enable next button if we are not on the last page buttonNextPage.setEnabled(model.getCurrentPageIndex() < model.getNumberOfPages() - 1); // only enable last page button if we are not on the last page buttonLastPage.setEnabled(model.getCurrentPageIndex() < model.getNumberOfPages() - 1); // hide pagination system if only one page is to show paginationPanel.setVisible(model.getNumberOfPages() > 1); // show how many attributes are visible / total filterLabel.setText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.filters.label", model.getVisibleSize(), model.getTotalSize())); // show the number of attributes displayed on the current page int minNumber = Math.min(model.getCurrentPageIndex() * MetaDataStatisticsModel.PAGE_SIZE + 1, model.getVisibleSize()); int maxNumber = Math.min((model.getCurrentPageIndex() + 1) * MetaDataStatisticsModel.PAGE_SIZE, model.getVisibleSize()); labelDisplayedAttribute.setText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.meta_data_stats.filter.showing_attributes.label", minNumber, maxNumber)); } @Override public Component getExportComponent() { return outerPanel; } @Override public String getExportName() { return I18N.getMessage(I18N.getGUIBundle(), "gui.cards.result_view.meta_data_view.title"); } @Override public String getIdentifier() { ExampleSet exampleSetOrNull = model.getExampleSetOrNull(); if (exampleSetOrNull != null) { return exampleSetOrNull.getSource(); } return null; } @Override public String getExportIconName() { return I18N.getGUIMessage("gui.cards.result_view.meta_data_view.icon"); } /** * Stops the statistics calculation. */ public void stop() { controller.stop(); } }