package com.metservice.kanban.charts.cumulativeflow; import static com.metservice.kanban.utils.DateUtils.currentLocalDate; import java.awt.Color; import java.io.IOException; import java.util.List; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.CategoryPointerAnnotation; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.category.CategoryDataset; import org.jfree.data.general.DatasetUtilities; import org.joda.time.LocalDate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.metservice.kanban.charts.ChartUtils; import com.metservice.kanban.charts.KanbanDrawingSupplier; import com.metservice.kanban.model.KanbanJournalItem; import com.metservice.kanban.model.KanbanProject; import com.metservice.kanban.model.WorkItem; import com.metservice.kanban.utils.Day; import com.metservice.kanban.utils.WorkingDayUtils; public class CumulativeFlowChartBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(CumulativeFlowChartBuilder.class); private LocalDate startDate; private LocalDate endDate; public CumulativeFlowChartBuilder(LocalDate startDate, LocalDate endDate) { this.startDate = startDate; this.endDate = endDate; } public CategoryDataset createDataset(List<String> phases, List<WorkItem> workItemList) throws IOException { // add start date and end date instead of local date here if (endDate == null) { endDate = currentLocalDate(); } if (startDate == null) { startDate = ChartUtils.getFirstDate(workItemList); if (startDate == null) { startDate = endDate; } } CumulativeFlowChartMatrix matrix = new CumulativeFlowChartMatrix(phases, startDate, endDate); for (WorkItem workItem : workItemList) { matrix.registerWorkItem(workItem); } List<LocalDate> dates = matrix.getOrderedListOfDates(); double[][] data = matrix.getData(); Day[] days = getListOfDays(dates); return DatasetUtilities.createCategoryDataset(getPhasesInInverseOrder(phases), days, data); } private String[] getPhasesInInverseOrder(List<String> phases) { String[] invertedPhases = new String[phases.size()]; int k = phases.size() - 1; for (String phase : phases) { invertedPhases[k--] = phase; } return invertedPhases; } public JFreeChart createChart(CategoryDataset dataset, KanbanProject project) { JFreeChart chart = ChartFactory.createStackedAreaChart( "Cumulative Flow Chart", // chart title "", // domain axis label "", // range axis label dataset, // data PlotOrientation.VERTICAL, // orientation true, // include legend true, false ); CategoryPlot plot = (CategoryPlot) chart.getPlot(); plot.setForegroundAlpha(1f); plot.setBackgroundPaint(Color.WHITE); plot.setDomainGridlinesVisible(true); plot.setDomainGridlinePaint(Color.GRAY); plot.setRangeGridlinePaint(Color.GRAY); plot.setDrawingSupplier(new KanbanDrawingSupplier(dataset.getRowCount())); insertJournalEntries(dataset, project, plot, ((Day) dataset.getColumnKey(0)).getDate(), ((Day) dataset.getColumnKey(dataset.getColumnCount() - 1)).getDate()); final CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setLowerMargin(0.0); domainAxis.setUpperMargin(0.0); domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); // change the auto tick unit selection to integer units only... final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); // this is to show annotations when they are above the chart rangeAxis.setUpperMargin(0.12); return chart; } public static void insertJournalEntries(CategoryDataset dataset, KanbanProject project, CategoryPlot plot, LocalDate startDate, LocalDate endDate) { for (KanbanJournalItem journalItem : project.getJournal()) { // check if the item is visible on the chart if (!journalItem.getDate().toLocalDate().isBefore(startDate) && !journalItem.getDate().toLocalDate().isAfter(endDate)) { int columnId = WorkingDayUtils.getWorkingDaysBetween(startDate, journalItem.getDate().toLocalDate()); if (columnId < dataset.getColumnCount()) { String text; if (journalItem.getText().length() > 12) { text = journalItem.getText().substring(0, 10) + "..."; } else { text = journalItem.getText(); } int yValue = 0; for (int i = 0; i < dataset.getRowCount(); i++) { yValue += dataset.getValue(i, columnId).intValue(); } CategoryPointerAnnotation annotation = new CategoryPointerAnnotation(text, dataset.getColumnKey(columnId), yValue, 30); annotation.setBaseRadius(30); annotation.setLabelOffset(7); plot.addAnnotation(annotation); } else { LOGGER.warn("Cannot display journal entry {} for day {}, not sure why", journalItem.getText(), journalItem.getDate()); } } } } private Day[] getListOfDays(List<LocalDate> dates) { Day[] days = new Day[dates.size()]; int i = 0; for (LocalDate date : dates) { days[i++] = new Day(date); } return days; } }