/** * DataCleaner (community edition) * Copyright (C) 2014 Neopost - Customer Information Management * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.datacleaner.widgets.result; import java.awt.Color; import java.awt.FlowLayout; import java.awt.Insets; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JSplitPane; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import org.datacleaner.api.AnalyzerResult; import org.datacleaner.bootstrap.WindowContext; import org.datacleaner.panels.DCPanel; import org.datacleaner.result.AnnotatedRowsResult; import org.datacleaner.result.ValueCountingAnalyzerResult; import org.datacleaner.result.ValueFrequency; import org.datacleaner.result.renderer.RendererFactory; import org.datacleaner.util.ChartUtils; import org.datacleaner.util.IconUtils; import org.datacleaner.util.LabelUtils; import org.datacleaner.util.WidgetFactory; import org.datacleaner.util.WidgetUtils; import org.datacleaner.widgets.Alignment; import org.datacleaner.widgets.table.DCTable; import org.datacleaner.windows.DetailsResultWindow; import org.jdesktop.swingx.VerticalLayout; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.title.ShortTextTitle; import org.jfree.chart.title.Title; import org.jfree.data.category.CategoryDataset; import org.jfree.data.category.DefaultCategoryDataset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Delegate for {@link ValueDistributionResultSwingRenderer}, which renders a * single Value Distribution group result. */ final class ValueDistributionResultSwingRendererGroupDelegate { private static final Logger logger = LoggerFactory.getLogger(ValueDistributionResultSwingRendererGroupDelegate.class); private static final Color[] SLICE_COLORS = DCDrawingSupplier.DEFAULT_FILL_COLORS; private static final int DEFAULT_PREFERRED_SLICES = 32; private final DefaultCategoryDataset _dataset = new DefaultCategoryDataset(); private final Map<String, Color> _valueColorMap; private final JButton _backButton = WidgetFactory.createDefaultButton("Back", IconUtils.ACTION_BACK); private final int _preferredSlices; private final String _groupOrColumnName; private final DCTable _table; private final RendererFactory _rendererFactory; private final WindowContext _windowContext; private final DCPanel _rightPanel = new DCPanel(); private Collection<ValueFrequency> _valueCounts; /** * Default constructor */ public ValueDistributionResultSwingRendererGroupDelegate(final String groupOrColumnName, final RendererFactory rendererFactory, final WindowContext windowContext) { this(groupOrColumnName, DEFAULT_PREFERRED_SLICES, rendererFactory, windowContext); } /** * Alternative constructor (primarily used for testing) with customizable * slice count * * @param groupOrColumnName * @param preferredSlices * @param rendererFactory * @param windowContext */ public ValueDistributionResultSwingRendererGroupDelegate(final String groupOrColumnName, final int preferredSlices, final RendererFactory rendererFactory, final WindowContext windowContext) { _groupOrColumnName = groupOrColumnName; _preferredSlices = preferredSlices; _rendererFactory = rendererFactory; _windowContext = windowContext; _table = new DCTable("Value", LabelUtils.COUNT_LABEL); _table.setColumnControlVisible(false); _table.setRowHeight(22); // create a map of predefined color mappings _valueColorMap = new HashMap<>(); _valueColorMap.put(LabelUtils.BLANK_LABEL.toUpperCase(), WidgetUtils.BG_COLOR_BRIGHTEST); _valueColorMap.put(LabelUtils.UNIQUE_LABEL.toUpperCase(), WidgetUtils.BG_COLOR_BRIGHT); _valueColorMap.put(LabelUtils.NULL_LABEL.toUpperCase(), WidgetUtils.BG_COLOR_DARKEST); _valueColorMap.put(LabelUtils.UNEXPECTED_LABEL.toUpperCase(), WidgetUtils.BG_COLOR_LESS_DARK); _valueColorMap.put("RED", WidgetUtils.ADDITIONAL_COLOR_RED_BRIGHT); _valueColorMap.put("ORANGE", WidgetUtils.BG_COLOR_ORANGE_BRIGHT); _valueColorMap.put("GREEN", WidgetUtils.BG_COLOR_GREEN_BRIGHT); _valueColorMap.put("PURPLE", WidgetUtils.ADDITIONAL_COLOR_PURPLE_BRIGHT); _valueColorMap.put("CYAN", WidgetUtils.ADDITIONAL_COLOR_CYAN_BRIGHT); _valueColorMap.put("BLUE", WidgetUtils.BG_COLOR_BLUE_BRIGHT); _valueColorMap.put("GREY", WidgetUtils.BG_COLOR_MEDIUM); _valueColorMap.put("GRAY", WidgetUtils.BG_COLOR_MEDIUM); _valueColorMap.put("NOT_PROCESSED", WidgetUtils.BG_COLOR_LESS_DARK); _valueColorMap.put("FAILURE", WidgetUtils.BG_COLOR_DARKEST); } public JComponent renderGroupResult(final ValueCountingAnalyzerResult result) { _valueCounts = result.getReducedValueFrequencies(_preferredSlices); _valueCounts = moveUniqueToEnd(_valueCounts); for (final ValueFrequency valueCount : _valueCounts) { setDataSetValue(valueCount.getName(), valueCount.getCount()); } final ChartPanel chartPanel = createChartPanel(result); logger.info("Rendering with {} slices", getDataSetItemCount()); drillToOverview(result); _backButton.setMargin(new Insets(0, 0, 0, 0)); _backButton.addActionListener(e -> drillToOverview(result)); _rightPanel.setLayout(new VerticalLayout()); _rightPanel.add(_backButton); _rightPanel.add(WidgetUtils.decorateWithShadow(_table.toPanel())); if (chartPanel == null) { return _rightPanel; } else { final DCPanel leftPanel = new DCPanel(); leftPanel.setLayout(new VerticalLayout()); leftPanel.add(WidgetUtils.decorateWithShadow(chartPanel)); final JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); split.setOpaque(false); split.add(leftPanel); split.add(_rightPanel); split.setDividerLocation(550); return split; } } /** * Creates a chart panel, or null if chart display is not applicable. * * @param result * @return */ private ChartPanel createChartPanel(final ValueCountingAnalyzerResult result) { if (_valueCounts.size() > ChartUtils.CATEGORY_COUNT_DISPLAY_THRESHOLD) { logger.info("Display threshold of {} in chart surpassed (got {}). Skipping chart.", ChartUtils.CATEGORY_COUNT_DISPLAY_THRESHOLD, _valueCounts.size()); return null; } final Integer distinctCount = result.getDistinctCount(); final Integer unexpectedValueCount = result.getUnexpectedValueCount(); final int totalCount = result.getTotalCount(); // chart for display of the dataset final String title = "Value distribution of " + _groupOrColumnName; final JFreeChart chart = ChartFactory .createBarChart(title, "Value", "Count", _dataset, PlotOrientation.HORIZONTAL, true, true, false); final List<Title> titles = new ArrayList<>(); titles.add(new ShortTextTitle("Total count: " + totalCount)); if (distinctCount != null) { titles.add(new ShortTextTitle("Distinct count: " + distinctCount)); } if (unexpectedValueCount != null) { titles.add(new ShortTextTitle("Unexpected value count: " + unexpectedValueCount)); } chart.setSubtitles(titles); ChartUtils.applyStyles(chart); // code-block for tweaking style and coloring of chart { final CategoryPlot plot = (CategoryPlot) chart.getPlot(); plot.getDomainAxis().setVisible(false); int colorIndex = 0; for (int i = 0; i < getDataSetItemCount(); i++) { final String key = getDataSetKey(i); final Color color; final String upperCaseKey = key.toUpperCase(); if (_valueColorMap.containsKey(upperCaseKey)) { color = _valueColorMap.get(upperCaseKey); } else { if (i == getDataSetItemCount() - 1) { // the last color should not be the same as the first if (colorIndex == 0) { colorIndex++; } } Color colorCandidate = SLICE_COLORS[colorIndex]; final int darkAmount = i / SLICE_COLORS.length; for (int j = 0; j < darkAmount; j++) { colorCandidate = WidgetUtils.slightlyDarker(colorCandidate); } color = colorCandidate; colorIndex++; if (colorIndex >= SLICE_COLORS.length) { colorIndex = 0; } } plot.getRenderer().setSeriesPaint(i, color); } } return ChartUtils.createPanel(chart, false); } private Collection<ValueFrequency> moveUniqueToEnd(final Collection<ValueFrequency> valueCounts) { ValueFrequency uniqueValueFrequency = null; for (final ValueFrequency valueFrequency : valueCounts) { if ("<unique>".equals(valueFrequency.getName())) { uniqueValueFrequency = valueFrequency; break; } } if (uniqueValueFrequency != null) { final List<ValueFrequency> valueCountsList = new ArrayList<>(valueCounts); valueCountsList.remove(uniqueValueFrequency); valueCountsList.add(uniqueValueFrequency); return valueCountsList; } else { return valueCounts; } } public CategoryDataset getDataset() { return _dataset; } private void drillToOverview(final ValueCountingAnalyzerResult result) { final TableModel model = new DefaultTableModel(new String[] { "Value", LabelUtils.COUNT_LABEL }, _valueCounts.size()); int i = 0; for (final ValueFrequency valueFreq : _valueCounts) { final String key = valueFreq.getName(); final int count = valueFreq.getCount(); model.setValueAt(key, i, 0); if (valueFreq.isComposite() && valueFreq.getChildren() != null && !valueFreq.getChildren().isEmpty()) { final DCPanel panel = new DCPanel(); panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); final JLabel label = new JLabel(count + ""); final JButton button = WidgetFactory.createSmallButton(IconUtils.ACTION_DRILL_TO_DETAIL); button.addActionListener(e -> drillToGroup(result, valueFreq, true)); panel.add(label); panel.add(Box.createHorizontalStrut(4)); panel.add(button); model.setValueAt(panel, i, 1); } else { setCountValue(result, model, i, valueFreq); } i++; } _table.setModel(model); _backButton.setVisible(false); _rightPanel.updateUI(); } protected void setDataSetValue(final String label, final Integer value) { _dataset.setValue(value, label, ""); } protected int getDataSetValue(final int index) { return _dataset.getValue(index, 0).intValue(); } public int getDataSetValue(final String label) { return _dataset.getValue(label, "").intValue(); } protected String getDataSetKey(final int index) { final Comparable<?> key = _dataset.getRowKey(index); assert key instanceof String; return key.toString(); } protected int getDataSetItemCount() { return _dataset.getRowCount(); } private void setCountValue(final ValueCountingAnalyzerResult result, final TableModel model, final int index, final ValueFrequency vc) { final String value = vc.getName(); final int count = vc.getCount(); final boolean hasAnnotation; final boolean isNullValue = value == null || LabelUtils.NULL_LABEL.equals(value); final boolean isUnexpectedValues = LabelUtils.UNEXPECTED_LABEL.equals(value); final boolean isBlank = "".equals(value) || LabelUtils.BLANK_LABEL.equals(value); if (count == 0) { hasAnnotation = false; } else { if (isNullValue) { hasAnnotation = result.hasAnnotatedRows(null); } else if (isUnexpectedValues) { hasAnnotation = result.hasAnnotatedRows(null); } else if (isBlank) { hasAnnotation = result.hasAnnotatedRows(""); } else { hasAnnotation = result.hasAnnotatedRows(value); } } if (hasAnnotation) { final ActionListener action = action1 -> { final String title = "Detailed results for [" + value + "]"; final List<AnalyzerResult> results = new ArrayList<>(); final AnnotatedRowsResult annotatedRows; if (isNullValue) { annotatedRows = result.getAnnotatedRowsForNull(); } else if (isUnexpectedValues) { annotatedRows = result.getAnnotatedRowsForUnexpectedValues(); } else if (isBlank) { annotatedRows = result.getAnnotatedRowsForValue(""); } else { annotatedRows = result.getAnnotatedRowsForValue(value); } results.add(annotatedRows); final DetailsResultWindow window = new DetailsResultWindow(title, results, _windowContext, _rendererFactory); window.setVisible(true); }; final DCPanel panel = AbstractCrosstabResultSwingRenderer .createActionableValuePanel(count, Alignment.LEFT, action, IconUtils.ACTION_DRILL_TO_DETAIL); model.setValueAt(panel, index, 1); } else { model.setValueAt(count, index, 1); } } private void drillToGroup(final ValueCountingAnalyzerResult result, final ValueFrequency valueFrequency, final boolean showBackButton) { final List<ValueFrequency> children = valueFrequency.getChildren(); final TableModel model = new DefaultTableModel(new String[] { valueFrequency.getName() + " value", LabelUtils.COUNT_LABEL }, children.size()); final Iterator<ValueFrequency> valueCounts = children.iterator(); int i = 0; while (valueCounts.hasNext()) { final ValueFrequency vc = valueCounts.next(); model.setValueAt(LabelUtils.getLabel(vc.getValue()), i, 0); setCountValue(result, model, i, vc); i++; } _table.setModel(model); _backButton.setVisible(showBackButton); _rightPanel.updateUI(); } }