org.datacleaner.widgets.result.DateGapAnalyzerResultSwingRenderer Maven / Gradle / Ivy
/**
* DataCleaner (community edition)
* Copyright (C) 2014 Free Software Foundation, Inc.
*
* 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.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.MouseWheelEvent;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollBar;
import org.apache.metamodel.schema.Table;
import org.datacleaner.api.AnalyzerResult;
import org.datacleaner.api.InputColumn;
import org.datacleaner.api.RendererBean;
import org.datacleaner.beans.dategap.DateGapAnalyzer;
import org.datacleaner.beans.dategap.DateGapAnalyzerResult;
import org.datacleaner.beans.dategap.TimeInterval;
import org.datacleaner.bootstrap.WindowContext;
import org.datacleaner.components.convert.ConvertToStringTransformer;
import org.datacleaner.configuration.DataCleanerConfiguration;
import org.datacleaner.connection.Datastore;
import org.datacleaner.connection.DatastoreCatalog;
import org.datacleaner.connection.DatastoreConnection;
import org.datacleaner.data.MutableInputColumn;
import org.datacleaner.guice.DCModuleImpl;
import org.datacleaner.job.builder.AnalysisJobBuilder;
import org.datacleaner.job.runner.AnalysisResultFuture;
import org.datacleaner.job.runner.AnalysisRunner;
import org.datacleaner.job.runner.AnalysisRunnerImpl;
import org.datacleaner.panels.DCPanel;
import org.datacleaner.result.renderer.AbstractRenderer;
import org.datacleaner.result.renderer.RendererFactory;
import org.datacleaner.result.renderer.SwingRenderingFormat;
import org.datacleaner.util.ChartUtils;
import org.datacleaner.util.LabelUtils;
import org.datacleaner.util.LookAndFeelManager;
import org.datacleaner.util.VFSUtils;
import org.datacleaner.util.WidgetUtils;
import org.datacleaner.windows.DetailsResultWindow;
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.PlotEntity;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.data.gantt.SlidingGanttCategoryDataset;
import org.jfree.data.gantt.Task;
import org.jfree.data.gantt.TaskSeries;
import org.jfree.data.gantt.TaskSeriesCollection;
import org.jfree.data.time.SimpleTimePeriod;
import org.jfree.data.time.TimePeriod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
@RendererBean(SwingRenderingFormat.class)
public class DateGapAnalyzerResultSwingRenderer extends AbstractRenderer {
private static final Logger logger = LoggerFactory.getLogger(DateGapAnalyzerResultSwingRenderer.class);
private static final String LABEL_OVERLAPS = "Overlaps";
private static final String LABEL_GAPS = "Gaps";
private static final String LABEL_COMPLETE_DURATION = "Complete duration";
private static final int GROUPS_VISIBLE = 8;
@Override
public JComponent render(final DateGapAnalyzerResult result) {
final TaskSeriesCollection dataset = new TaskSeriesCollection();
final Set groupNames = result.getGroupNames();
final TaskSeries completeDurationTaskSeries = new TaskSeries(LABEL_COMPLETE_DURATION);
final TaskSeries gapsTaskSeries = new TaskSeries(LABEL_GAPS);
final TaskSeries overlapsTaskSeries = new TaskSeries(LABEL_OVERLAPS);
for (final String groupName : groupNames) {
final String groupDisplayName;
if (groupName == null) {
if (groupNames.size() == 1) {
groupDisplayName = "All";
} else {
groupDisplayName = LabelUtils.NULL_LABEL;
}
} else {
groupDisplayName = groupName;
}
final TimeInterval completeDuration = result.getCompleteDuration(groupName);
final Task completeDurationTask =
new Task(groupDisplayName, createTimePeriod(completeDuration.getFrom(), completeDuration.getTo()));
completeDurationTaskSeries.add(completeDurationTask);
// plot gaps
{
final SortedSet gaps = result.getGaps(groupName);
int i = 1;
Task rootTask = null;
for (final TimeInterval interval : gaps) {
final TimePeriod timePeriod = createTimePeriod(interval.getFrom(), interval.getTo());
if (rootTask == null) {
rootTask = new Task(groupDisplayName, timePeriod);
gapsTaskSeries.add(rootTask);
} else {
final Task task = new Task(groupDisplayName + " gap" + i, timePeriod);
rootTask.addSubtask(task);
}
i++;
}
}
// plot overlaps
{
final SortedSet overlaps = result.getOverlaps(groupName);
int i = 1;
Task rootTask = null;
for (final TimeInterval interval : overlaps) {
final TimePeriod timePeriod = createTimePeriod(interval.getFrom(), interval.getTo());
if (rootTask == null) {
rootTask = new Task(groupDisplayName, timePeriod);
overlapsTaskSeries.add(rootTask);
} else {
final Task task = new Task(groupDisplayName + " overlap" + i, timePeriod);
rootTask.addSubtask(task);
}
i++;
}
}
}
dataset.add(overlapsTaskSeries);
dataset.add(gapsTaskSeries);
dataset.add(completeDurationTaskSeries);
final SlidingGanttCategoryDataset slidingDataset = new SlidingGanttCategoryDataset(dataset, 0, GROUPS_VISIBLE);
final JFreeChart chart = ChartFactory.createGanttChart(
"Date gaps and overlaps in " + result.getFromColumnName() + " / " + result.getToColumnName(),
result.getGroupColumnName(), "Time", slidingDataset, true, true, false);
ChartUtils.applyStyles(chart);
// make sure the 3 timeline types have correct coloring
{
final CategoryPlot plot = (CategoryPlot) chart.getPlot();
plot.setDrawingSupplier(
new DCDrawingSupplier(WidgetUtils.BG_COLOR_GREEN_BRIGHT, WidgetUtils.ADDITIONAL_COLOR_RED_BRIGHT,
WidgetUtils.BG_COLOR_BLUE_BRIGHT));
}
final int visibleLines = Math.min(GROUPS_VISIBLE, groupNames.size());
final ChartPanel chartPanel = ChartUtils.createPanel(chart, ChartUtils.WIDTH_WIDE, visibleLines * 50 + 200);
chartPanel.addChartMouseListener(new ChartMouseListener() {
@Override
public void chartMouseMoved(final ChartMouseEvent event) {
Cursor cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
final ChartEntity entity = event.getEntity();
if (entity instanceof PlotEntity) {
cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
}
chartPanel.setCursor(cursor);
}
@Override
public void chartMouseClicked(final ChartMouseEvent event) {
// do nothing
}
});
final JComponent decoratedChartPanel;
final StringBuilder chartDescription = new StringBuilder("");
chartDescription.append("The chart displays the recorded timeline based on FROM and TO dates.
");
chartDescription.append("The red items represent gaps in the timeline and the green items "
+ "represent points in the timeline where more than one record show activity.
");
chartDescription.append("You can zoom in by clicking and dragging the area that you want to "
+ "examine in further detail.
");
if (groupNames.size() > GROUPS_VISIBLE) {
final JScrollBar scroll = new JScrollBar(JScrollBar.VERTICAL);
scroll.setMinimum(0);
scroll.setMaximum(groupNames.size());
scroll.addAdjustmentListener(e -> {
final int value = e.getAdjustable().getValue();
slidingDataset.setFirstCategoryIndex(value);
});
chartPanel.addMouseWheelListener(e -> {
final int scrollType = e.getScrollType();
if (scrollType == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
final int wheelRotation = e.getWheelRotation();
scroll.setValue(scroll.getValue() + wheelRotation);
}
});
final DCPanel outerPanel = new DCPanel();
outerPanel.setLayout(new BorderLayout());
outerPanel.add(chartPanel, BorderLayout.CENTER);
outerPanel.add(scroll, BorderLayout.EAST);
chartDescription.append("Use the right scrollbar to scroll up and down on the chart.
");
decoratedChartPanel = outerPanel;
} else {
decoratedChartPanel = chartPanel;
}
chartDescription.append("");
final JLabel chartDescriptionLabel = new JLabel(chartDescription.toString());
final DCPanel panel = new DCPanel();
panel.setLayout(new VerticalLayout());
panel.add(chartDescriptionLabel);
panel.add(decoratedChartPanel);
return panel;
}
private TimePeriod createTimePeriod(final long from, final long to) {
if (from > to) {
logger.warn("An illegal from/to combination occurred: {}, {}", from, to);
}
return new SimpleTimePeriod(from, to);
}
/**
* A main method that will display the results of a few example value
* distributions. Useful for tweaking the charts and UI.
*
* @param args
* @throws Throwable
*/
public static void main(final String[] args) throws Throwable {
LookAndFeelManager.get().init();
final Injector injector =
Guice.createInjector(new DCModuleImpl(VFSUtils.getFileSystemManager().resolveFile("."), null));
// run a small job
final AnalysisJobBuilder ajb = injector.getInstance(AnalysisJobBuilder.class);
final Datastore ds = injector.getInstance(DatastoreCatalog.class).getDatastore("orderdb");
ajb.setDatastore(ds);
final DataCleanerConfiguration conf = injector.getInstance(DataCleanerConfiguration.class);
final AnalysisRunner runner = new AnalysisRunnerImpl(conf);
final DatastoreConnection con = ds.openConnection();
final Table table = con.getSchemaNavigator().convertToTable("PUBLIC.ORDERS");
ajb.addSourceColumn(table.getColumnByName("ORDERDATE"));
ajb.addSourceColumn(table.getColumnByName("SHIPPEDDATE"));
ajb.addSourceColumn(table.getColumnByName("CUSTOMERNUMBER"));
@SuppressWarnings("unchecked") final InputColumn orderDateColumn =
(InputColumn) ajb.getSourceColumnByName("ORDERDATE");
@SuppressWarnings("unchecked") final InputColumn shippedDateColumn =
(InputColumn) ajb.getSourceColumnByName("SHIPPEDDATE");
@SuppressWarnings("unchecked") final InputColumn customerNumberColumn =
(InputColumn) ajb.getSourceColumnByName("CUSTOMERNUMBER");
@SuppressWarnings("unchecked") final MutableInputColumn customerNumberAsStringColumn =
(MutableInputColumn) ajb.addTransformer(ConvertToStringTransformer.class)
.addInputColumn(customerNumberColumn).getOutputColumns().get(0);
final DateGapAnalyzer dga = ajb.addAnalyzer(DateGapAnalyzer.class).getComponentInstance();
dga.setFromColumn(orderDateColumn);
dga.setToColumn(shippedDateColumn);
dga.setGroupColumn(customerNumberAsStringColumn);
final AnalysisResultFuture resultFuture = runner.run(ajb.toAnalysisJob());
if (resultFuture.isErrornous()) {
throw resultFuture.getErrors().get(0);
}
final List list = Collections.emptyList();
final RendererFactory rendererFactory = new RendererFactory(conf);
final DetailsResultWindow window =
new DetailsResultWindow("Example", list, injector.getInstance(WindowContext.class), rendererFactory);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final List results = resultFuture.getResults();
for (final AnalyzerResult analyzerResult : results) {
final JComponent renderedResult =
new DateGapAnalyzerResultSwingRenderer().render((DateGapAnalyzerResult) analyzerResult);
window.addRenderedResult(renderedResult);
}
window.repaint();
window.setPreferredSize(new Dimension(800, 600));
window.setVisible(true);
}
}