Java tutorial
/* * Copyright 2009, 2010, 2011 Fabian Hager, Witoslaw Koczewsi <wi@koczewski.de> * * 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 General Public License along with this program. If not, see * <http://www.gnu.org/licenses/>. */ package scrum.server.common; import ilarkesto.base.Str; import ilarkesto.base.Sys; import ilarkesto.base.Utl; import ilarkesto.core.logging.Log; import ilarkesto.core.time.Date; import java.awt.BasicStroke; import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.DateTickUnit; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.NumberTickUnit; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.data.Range; import org.jfree.data.xy.DefaultXYDataset; import org.jfree.ui.RectangleInsets; import scrum.client.common.WeekdaySelector; import scrum.server.css.KunagiCssBuilder; import scrum.server.sprint.Sprint; import scrum.server.sprint.SprintDao; import scrum.server.sprint.SprintDaySnapshot; public class BurndownChart { private static final Log LOG = Log.get(BurndownChart.class); private static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(KunagiCssBuilder.cBurndownLine); private static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(KunagiCssBuilder.cBurndownProjectionLine); private static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(KunagiCssBuilder.cBurndownOptimalLine); // --- dependencies --- private SprintDao sprintDao; public void setSprintDao(SprintDao sprintDao) { this.sprintDao = sprintDao; } // --- --- public static byte[] createBurndownChartAsByteArray(Sprint sprint, int width, int height) { ByteArrayOutputStream out = new ByteArrayOutputStream(); new BurndownChart().writeSprintBurndownChart(out, sprint, width, height); return out.toByteArray(); } public void writeSprintBurndownChart(OutputStream out, String sprintId, int width, int height) { Sprint sprint = sprintDao.getById(sprintId); if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); writeSprintBurndownChart(out, sprint, width, height); } public void writeSprintBurndownChart(OutputStream out, Sprint sprint, int width, int height) { List<SprintDaySnapshot> snapshots = sprint.getDaySnapshots(); if (snapshots.isEmpty()) { Date date = Date.today(); date = Date.latest(date, sprint.getBegin()); date = Date.earliest(date, sprint.getEnd()); sprint.getDaySnapshot(date).updateWithCurrentSprint(); snapshots = sprint.getDaySnapshots(); } WeekdaySelector freeDays = sprint.getProject().getFreeDaysAsWeekdaySelector(); writeSprintBurndownChart(out, snapshots, sprint.getBegin(), sprint.getEnd(), sprint.getOriginallyEnd(), freeDays, width, height); } static void writeSprintBurndownChart(OutputStream out, List<? extends BurndownSnapshot> snapshots, Date firstDay, Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height) { LOG.debug("Creating burndown chart:", snapshots.size(), "snapshots from", firstDay, "to", lastDay, "(" + width + "x" + height + " px)"); int dayCount = firstDay.getPeriodTo(lastDay).toDays(); int dateMarkTickUnit = 1; float widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; while (widthPerDay < 20) { dateMarkTickUnit++; widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; } List<BurndownSnapshot> burndownSnapshots = new ArrayList<BurndownSnapshot>(snapshots); JFreeChart chart = createSprintBurndownChart(burndownSnapshots, firstDay, lastDay, originallyLastDay, freeDays, dateMarkTickUnit, widthPerDay); try { ChartUtilities.writeScaledChartAsPNG(out, chart, width, height, 1, 1); out.flush(); } catch (IOException e) { throw new RuntimeException(e); } } private static JFreeChart createSprintBurndownChart(List<BurndownSnapshot> snapshots, Date firstDay, Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay) { DefaultXYDataset data = createSprintBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays); double tick = 1.0; double max = BurndownChart.getMaximum(data); while (max / tick > 25) { tick *= 2; if (max / tick <= 25) break; tick *= 2.5; if (max / tick <= 25) break; tick *= 2; } double valueLabelTickUnit = tick; double upperBoundary = Math.min(max * 1.1f, max + 3); if (!Sys.isHeadless()) LOG.warn("GraphicsEnvironment is not headless"); JFreeChart chart = ChartFactory.createXYLineChart("", "", "", data, PlotOrientation.VERTICAL, false, true, false); chart.setBackgroundPaint(Color.WHITE); XYPlot plot = chart.getXYPlot(); // plot.setInsets(new RectangleInsets(0, 0, 0, 0)); plot.setAxisOffset(RectangleInsets.ZERO_INSETS); // plot.setOutlineVisible(false); plot.setBackgroundPaint(Color.white); plot.setRangeGridlinePaint(Color.lightGray); plot.setDomainGridlinePaint(Color.lightGray); // plot.setRangeCrosshairPaint(Color.lightGray); // plot.setRangeMinorGridlinePaint(Color.lightGray); // plot.setDomainCrosshairPaint(Color.blue); // plot.setDomainMinorGridlinePaint(Color.green); // plot.setDomainTickBandPaint(Color.green); XYItemRenderer renderer = plot.getRenderer(); renderer.setBaseStroke(new BasicStroke(2f)); renderer.setSeriesPaint(0, COLOR_PAST_LINE); renderer.setSeriesStroke(0, new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); renderer.setSeriesPaint(1, COLOR_PROJECTION_LINE); renderer.setSeriesStroke(1, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 1.0f, new float[] { 3f }, 0)); renderer.setSeriesPaint(2, COLOR_OPTIMUM_LINE); renderer.setSeriesStroke(2, new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); DateAxis domainAxis1 = new DateAxis(); String dateFormat = "d."; widthPerDay -= 5; if (widthPerDay > 40) { dateFormat = "EE " + dateFormat; } if (widthPerDay > 10) { float spaces = widthPerDay / 2.7f; dateFormat = Str.multiply(" ", (int) spaces) + dateFormat; } domainAxis1.setDateFormatOverride(new SimpleDateFormat(dateFormat, Locale.US)); domainAxis1.setTickUnit(new DateTickUnit(DateTickUnit.DAY, dateMarkTickUnit)); domainAxis1.setAxisLineVisible(false); Range range = new Range(firstDay.toMillis(), lastDay.nextDay().toMillis()); domainAxis1.setRange(range); DateAxis domainAxis2 = new DateAxis(); domainAxis2.setTickUnit(new DateTickUnit(DateTickUnit.DAY, 1)); domainAxis2.setTickMarksVisible(false); domainAxis2.setTickLabelsVisible(false); domainAxis2.setRange(range); plot.setDomainAxis(0, domainAxis2); plot.setDomainAxis(1, domainAxis1); plot.setDomainAxisLocation(1, AxisLocation.BOTTOM_OR_RIGHT); NumberAxis rangeAxis = new NumberAxis(); rangeAxis.setNumberFormatOverride(NumberFormat.getIntegerInstance()); rangeAxis.setTickUnit(new NumberTickUnit(valueLabelTickUnit)); rangeAxis.setLowerBound(0); rangeAxis.setUpperBound(upperBoundary); plot.setRangeAxis(rangeAxis); return chart; } static double getMaximum(DefaultXYDataset data) { double max = 0; for (int i = 0; i < data.getSeriesCount(); i++) { for (int j = 0; j < data.getItemCount(i); j++) { double value = data.getYValue(i, j); if (value > max) { max = value; } } } return max; } static DefaultXYDataset createSprintBurndownChartDataset(final List<BurndownSnapshot> snapshots, final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays) { ChartDataFactory factory = new ChartDataFactory(); factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays); return factory.getDataset(); } private static double[][] toArray(List<Double> a, List<Double> b) { int min = Math.min(a.size(), b.size()); double[][] array = new double[2][min]; for (int i = 0; i < min; i++) { array[0][i] = a.get(i); array[1][i] = b.get(i); } return array; } static class ChartDataFactory { List<Double> mainDates = new ArrayList<Double>(); List<Double> mainValues = new ArrayList<Double>(); List<Double> extrapolationDates = new ArrayList<Double>(); List<Double> extrapolationValues = new ArrayList<Double>(); List<Double> idealDates = new ArrayList<Double>(); List<Double> idealValues = new ArrayList<Double>(); List<BurndownSnapshot> snapshots; WeekdaySelector freeDays; Date date; long millisBegin; long millisEnd; boolean freeDay; BurndownSnapshot snapshot; boolean workStarted; boolean workFinished; int totalBurned; int totalBefore; int totalAfter; int burned; int jump; double totalRemaining; int workDays; double burnPerDay; double idealRemaining; double idealBurnPerDay; int totalWorkDays = 0; int totalOriginallyWorkDays = 0; boolean extrapolationFinished; DefaultXYDataset dataset; public void createDataset(final List<BurndownSnapshot> snapshots, final Date firstDay, final Date lastDay, final Date originallyLastDay, final WeekdaySelector freeDays) { this.snapshots = snapshots; this.freeDays = freeDays; date = firstDay; while (date.isBeforeOrSame(lastDay)) { if (!freeDays.isFree(date.getWeekday().getDayOfWeek())) { totalWorkDays++; if (date.isBeforeOrSame(originallyLastDay)) totalOriginallyWorkDays++; } date = date.nextDay(); } setDate(firstDay); while (true) { if (!workFinished) { burned = snapshot.getBurnedWorkTotal() - totalBurned; totalBurned = snapshot.getBurnedWorkTotal(); totalAfter = snapshot.getRemainingWork(); jump = totalAfter - totalBefore + burned; } if (workFinished) { processSuffix(); } else if (workStarted) { processCenter(); } else { processPrefix(); } if (date.equals(lastDay)) break; setDate(date.nextDay()); totalBefore = totalAfter; } dataset = new DefaultXYDataset(); dataset.addSeries("Main", toArray(mainDates, mainValues)); dataset.addSeries("Extrapolation", toArray(extrapolationDates, extrapolationValues)); dataset.addSeries("Ideal", toArray(idealDates, idealValues)); } private void setDate(Date newDate) { date = newDate; millisBegin = date.toMillis(); millisEnd = date.nextDay().toMillis(); freeDay = freeDays.isFree(date.getWeekday().getDayOfWeek()); if (!workFinished) snapshot = getSnapshot(); } private void processPrefix() { if (totalAfter > 0 || totalBurned > 0) { workStarted = true; idealRemaining = totalAfter + burned; idealDates.add((double) millisBegin); idealValues.add(idealRemaining); if (totalOriginallyWorkDays > 0) { idealBurnPerDay = (double) jump / (double) totalOriginallyWorkDays; } processCenter(); return; } totalWorkDays--; totalOriginallyWorkDays--; } private void processCenter() { mainDates.add((double) millisBegin); mainValues.add((double) totalBefore); if (jump != 0) { mainDates.add((double) millisBegin); mainValues.add((double) totalBefore + jump); } mainDates.add((double) millisEnd); mainValues.add((double) totalAfter); if (!freeDay) { workDays++; idealRemaining -= idealBurnPerDay; } if (idealRemaining > 0) { idealDates.add((double) millisEnd); idealValues.add(idealRemaining); } } private void processSuffix() { if (!freeDay) { totalRemaining -= burnPerDay; idealRemaining -= idealBurnPerDay; } if (!extrapolationFinished) { extrapolationDates.add((double) millisEnd); extrapolationValues.add(totalRemaining); } idealDates.add((double) millisEnd); idealValues.add(idealRemaining); if (totalRemaining <= 0) extrapolationFinished = true; } private BurndownSnapshot getSnapshot() { for (BurndownSnapshot snapshot : snapshots) { if (snapshot.getDate().equals(date)) return snapshot; } workFinished = true; totalRemaining = totalAfter; burnPerDay = (double) totalBurned / (double) workDays; extrapolationDates.add((double) millisBegin); extrapolationValues.add(totalRemaining); return null; } public DefaultXYDataset getDataset() { return dataset; } } }