/**
* eobjects.org DataCleaner
* Copyright (C) 2010 eobjects.org
*
* 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.eobjects.datacleaner.widgets.result;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JSplitPane;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import org.eobjects.analyzer.beans.valuedist.ValueCount;
import org.eobjects.analyzer.result.ValueDistributionGroupResult;
import org.eobjects.datacleaner.panels.DCPanel;
import org.eobjects.datacleaner.util.ChartUtils;
import org.eobjects.datacleaner.util.ImageManager;
import org.eobjects.datacleaner.util.LabelUtils;
import org.eobjects.datacleaner.util.WidgetFactory;
import org.eobjects.datacleaner.util.WidgetUtils;
import org.eobjects.datacleaner.widgets.table.DCTable;
import org.jdesktop.swingx.VerticalLayout;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.PieSectionEntity;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.title.ShortTextTitle;
import org.jfree.chart.title.Title;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.util.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Delegate for {@link ValueDistributionResultSwingRenderer}, which renders a
* single Value Distribution group result.
*
* @author Kasper Sørensen
*/
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 static final int DEFAULT_MAX_SLICES = 40;
private final Map<String, PieSliceGroup> _groups = new HashMap<String, PieSliceGroup>();
private final DefaultPieDataset _dataset = new DefaultPieDataset();
private final JButton _backButton = WidgetFactory.createButton("Back",
ImageManager.getInstance().getImageIcon("images/actions/back.png"));
private final int _preferredSlices;
private final int _maxSlices;
private final String _groupOrColumnName;
private final DCTable _table;
/**
* Default constructor
*/
public ValueDistributionResultSwingRendererGroupDelegate(String groupOrColumnName) {
this(groupOrColumnName, DEFAULT_PREFERRED_SLICES, DEFAULT_MAX_SLICES);
}
/**
* Alternative constructor (primarily used for testing) with customizable
* slice count
*
* @param preferredSlices
* @param maxSlices
*/
public ValueDistributionResultSwingRendererGroupDelegate(String groupOrColumnName, int preferredSlices, int maxSlices) {
_groupOrColumnName = groupOrColumnName;
_preferredSlices = preferredSlices;
_maxSlices = maxSlices;
_table = new DCTable("Value", LabelUtils.COUNT_LABEL);
_table.setRowHeight(22);
}
public JSplitPane renderGroupResult(ValueDistributionGroupResult result) {
// create a special group for the unique values
final int uniqueCount = result.getUniqueCount();
final int distinctCount = result.getDistinctCount();
final int totalCount = result.getTotalCount();
final Collection<String> uniqueValues = result.getUniqueValues();
if (uniqueValues != null && !uniqueValues.isEmpty()) {
PieSliceGroup pieSliceGroup = new PieSliceGroup(LabelUtils.UNIQUE_LABEL, uniqueCount, uniqueValues, 1);
_groups.put(pieSliceGroup.getName(), pieSliceGroup);
} else {
if (uniqueCount > 0) {
_dataset.setValue(LabelUtils.UNIQUE_LABEL, uniqueCount);
}
}
// create a special slice for null values
final int nullCount = result.getNullCount();
if (nullCount > 0) {
_dataset.setValue(LabelUtils.NULL_LABEL, nullCount);
}
// create the remaining "normal" slices, either individually or in
// groups
{
final List<ValueCount> topValueCounts = result.getTopValues().getValueCounts();
final List<ValueCount> bottomValueCounts = result.getBottomValues().getValueCounts();
// result can be GC'ed now
result = null;
// if the user has specified the values of interest then show the
// graph correspondingly without any grouping
boolean userSpecifiedGroups = !topValueCounts.isEmpty() && !bottomValueCounts.isEmpty();
if (userSpecifiedGroups || topValueCounts.size() + bottomValueCounts.size() < _preferredSlices) {
// vanilla scenario for cleanly distributed datasets
for (ValueCount valueCount : topValueCounts) {
_dataset.setValue(LabelUtils.getLabel(valueCount.getValue()), valueCount.getCount());
}
for (ValueCount valueCount : bottomValueCounts) {
_dataset.setValue(LabelUtils.getLabel(valueCount.getValue()), valueCount.getCount());
}
} else {
// create groups of values
List<ValueCount> valueCounts = topValueCounts;
if (!bottomValueCounts.isEmpty()) {
valueCounts = bottomValueCounts;
}
createGroups(valueCounts);
for (ValueCount valueCount : valueCounts) {
_dataset.setValue(LabelUtils.getLabel(valueCount.getValue()), valueCount.getCount());
}
}
// if the is only a single group and it's size (plus the existing
// size) is smaller than
// _maxSlices, then "drill to detail" from the start.
boolean singleGroupExploded = false;
if (_groups.size() == 1) {
// only a single group in the complete value distribution!
PieSliceGroup singleGroup = _groups.values().iterator().next();
if (singleGroup.size() + _dataset.getItemCount() <= _preferredSlices) {
singleGroupExploded = true;
for (ValueCount vc : singleGroup) {
_dataset.setValue(LabelUtils.getLabel(vc.getValue()), vc.getCount());
}
_dataset.sortByValues(SortOrder.DESCENDING);
}
}
if (!singleGroupExploded) {
for (PieSliceGroup group : _groups.values()) {
_dataset.setValue(group.getName(), group.getTotalCount());
}
}
}
logger.info("Rendering with {} slices", _dataset.getItemCount());
drillToOverview();
// chart for display of the dataset
final JFreeChart chart = ChartFactory.createPieChart("Value distribution of " + _groupOrColumnName, _dataset, false,
true, false);
Title totalCountSubtitle = new ShortTextTitle("Total count: " + totalCount);
Title distinctCountSubtitle = new ShortTextTitle("Distinct count: " + distinctCount);
chart.setSubtitles(Arrays.asList(totalCountSubtitle, distinctCountSubtitle));
ChartUtils.applyStyles(chart);
// code-block for tweaking style and coloring of chart
{
final PiePlot plot = (PiePlot) chart.getPlot();
int colorIndex = 0;
for (int i = 0; i < _dataset.getItemCount(); i++) {
final String key = (String) _dataset.getKey(i);
if (!LabelUtils.UNIQUE_LABEL.equals(key) && !LabelUtils.NULL_LABEL.equals(key)) {
if (i == _dataset.getItemCount() - 1) {
// the last color should not be the same as the first
if (colorIndex == 0) {
colorIndex++;
}
}
Color color = SLICE_COLORS[colorIndex];
int darkAmount = i / SLICE_COLORS.length;
for (int j = 0; j < darkAmount; j++) {
color = WidgetUtils.slightlyDarker(color);
}
plot.setSectionPaint(key, color);
colorIndex++;
if (colorIndex >= SLICE_COLORS.length) {
colorIndex = 0;
}
}
}
plot.setSectionPaint(LabelUtils.UNIQUE_LABEL, WidgetUtils.BG_COLOR_BRIGHT);
plot.setSectionPaint(LabelUtils.NULL_LABEL, WidgetUtils.BG_COLOR_DARKEST);
}
final ChartPanel chartPanel = new ChartPanel(chart);
int chartHeight = 450;
if (_dataset.getItemCount() > 32) {
chartHeight += 200;
} else if (_dataset.getItemCount() > 25) {
chartHeight += 100;
}
chartPanel.setPreferredSize(new Dimension(0, chartHeight));
chartPanel.addChartMouseListener(new ChartMouseListener() {
@Override
public void chartMouseMoved(ChartMouseEvent event) {
ChartEntity entity = event.getEntity();
if (entity instanceof PieSectionEntity) {
PieSectionEntity pieSectionEntity = (PieSectionEntity) entity;
String sectionKey = (String) pieSectionEntity.getSectionKey();
if (_groups.containsKey(sectionKey)) {
chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
chartPanel.setCursor(Cursor.getDefaultCursor());
}
} else {
chartPanel.setCursor(Cursor.getDefaultCursor());
}
}
@Override
public void chartMouseClicked(ChartMouseEvent event) {
ChartEntity entity = event.getEntity();
if (entity instanceof PieSectionEntity) {
PieSectionEntity pieSectionEntity = (PieSectionEntity) entity;
String sectionKey = (String) pieSectionEntity.getSectionKey();
if (_groups.containsKey(sectionKey)) {
drillToGroup(sectionKey, true);
}
}
}
});
final DCPanel leftPanel = new DCPanel();
leftPanel.setLayout(new VerticalLayout());
leftPanel.add(WidgetUtils.decorateWithShadow(chartPanel, true, 4));
_backButton.setMargin(new Insets(0, 0, 0, 0));
_backButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
drillToOverview();
}
});
final DCPanel rightPanel = new DCPanel();
rightPanel.setLayout(new BorderLayout());
rightPanel.add(_backButton, BorderLayout.NORTH);
rightPanel.add(_table.toPanel(), BorderLayout.CENTER);
rightPanel.getSize().height = chartHeight;
final JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
split.setOpaque(false);
split.add(leftPanel);
split.add(rightPanel);
split.setDividerLocation(550);
return split;
}
public Map<String, PieSliceGroup> getGroups() {
return _groups;
}
public DefaultPieDataset getDataset() {
return _dataset;
}
private void drillToOverview() {
final TableModel model = new DefaultTableModel(new String[] { "Value", LabelUtils.COUNT_LABEL },
_dataset.getItemCount());
for (int i = 0; i < _dataset.getItemCount(); i++) {
final String key = (String) _dataset.getKey(i);
final int count = _dataset.getValue(i).intValue();
model.setValueAt(key, i, 0);
if (_groups.containsKey(key)) {
DCPanel panel = new DCPanel();
panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
JLabel label = new JLabel(count + "");
JButton button = WidgetFactory.createSmallButton("images/actions/drill-to-detail.png");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
drillToGroup(key, true);
}
});
panel.add(label);
panel.add(Box.createHorizontalStrut(4));
panel.add(button);
model.setValueAt(panel, i, 1);
} else {
model.setValueAt(count, i, 1);
}
}
_table.setModel(model);
_backButton.setVisible(false);
}
private void drillToGroup(String groupName, boolean showBackButton) {
final PieSliceGroup group = _groups.get(groupName);
final TableModel model = new DefaultTableModel(new String[] { groupName + " value", LabelUtils.COUNT_LABEL },
group.size());
final Iterator<ValueCount> valueCounts = group.getValueCounts();
int i = 0;
while (valueCounts.hasNext()) {
ValueCount vc = valueCounts.next();
model.setValueAt(LabelUtils.getLabel(vc.getValue()), i, 0);
model.setValueAt(vc.getCount(), i, 1);
i++;
}
_table.setModel(model);
_backButton.setVisible(showBackButton);
}
protected void createGroups(List<ValueCount> valueCounts) {
// this map will contain frequency counts that are not groupable in
// this block because there is only one occurrence
final Set<Integer> skipCounts = new HashSet<Integer>();
int previousGroupFrequency = 1;
final int datasetItemCount = _dataset.getItemCount();
while (datasetItemCount + _groups.size() < _preferredSlices) {
if (skipCounts.size() == valueCounts.size()) {
// only individual counted values remain
break;
}
int groupFrequency = -1;
for (ValueCount vc : valueCounts) {
int count = vc.getCount();
if (groupFrequency == -1) {
groupFrequency = count;
} else {
if (!skipCounts.contains(count)) {
groupFrequency = Math.min(groupFrequency, count);
}
}
if (groupFrequency == previousGroupFrequency + 1) {
// look no further - we've found the next lowest
// frequency, none can be lower
break;
}
}
if (groupFrequency < previousGroupFrequency) {
// could not find next group frequency - stop searching
break;
}
final String groupName = "<count=" + groupFrequency + ">";
final List<ValueCount> groupContent = new ArrayList<ValueCount>();
logger.debug("Lowest repeated frequency above {} found: {}. Fetching from {} ungrouped values", new Object[] {
previousGroupFrequency, groupFrequency, valueCounts.size() });
for (Iterator<ValueCount> it = valueCounts.iterator(); it.hasNext();) {
ValueCount vc = (ValueCount) it.next();
final int count = vc.getCount();
if (groupFrequency == count) {
groupContent.add(vc);
it.remove();
}
}
if (groupContent.size() == 1) {
logger.debug("Skipping this group because it has only one occurrence");
skipCounts.add(groupFrequency);
valueCounts.add(groupContent.get(0));
} else {
logger.info("Grouping {} values to group: {}", groupContent.size(), groupName);
Collection<String> groupContentValues = new ArrayList<String>(groupContent.size());
for (ValueCount valueCount : groupContent) {
groupContentValues.add(valueCount.getValue());
}
PieSliceGroup group = new PieSliceGroup(groupName, groupContentValues, groupFrequency);
_groups.put(groupName, group);
}
previousGroupFrequency = groupFrequency;
}
// code block for creating aggregated groups if slice count is still too
// high
{
if (datasetItemCount + _groups.size() + valueCounts.size() > _maxSlices) {
final int aggregatedGroupCount = _maxSlices - _groups.size();
logger.info("Amount of groups outgrowed the preferred count, creating {} aggregated groups",
aggregatedGroupCount);
//
//
// final int diffFrequency = maxFrequency - minFrequency;
final int aggregatedGroupSize = valueCounts.size() / aggregatedGroupCount;
logger.info("Creating {} range groups", aggregatedGroupCount);
for (int i = 0; i < aggregatedGroupCount; i++) {
final LinkedList<ValueCount> groupContent = new LinkedList<ValueCount>();
while (groupContent.size() < aggregatedGroupSize && !valueCounts.isEmpty()) {
int minFrequency = -1;
for (ValueCount vc : valueCounts) {
final int count = vc.getCount();
if (minFrequency == -1) {
minFrequency = count;
} else {
minFrequency = Math.min(count, minFrequency);
}
}
logger.debug("Adding values with count={} to range group {}.", minFrequency, i + 1);
for (Iterator<ValueCount> it = valueCounts.iterator(); it.hasNext();) {
ValueCount vc = it.next();
if (vc.getCount() == minFrequency) {
groupContent.add(vc);
it.remove();
}
}
}
if (groupContent.isEmpty()) {
break;
}
String groupName = "<count=[" + groupContent.getFirst().getCount() + "-"
+ groupContent.getLast().getCount() + "]>";
PieSliceGroup group = new PieSliceGroup(groupName, groupContent);
_groups.put(groupName, group);
}
}
}
}
}