From 669d6f6c7c99bec9bd8bdab9e6b748e774e6d845 Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Thu, 14 Nov 2013 14:41:07 +0100 Subject: [PATCH 1/8] ignore intellij specific output --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 22969ab1..a1c7bea7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ temp-testng* tomcat *.log velocity.log* - +.idea/ +classes/ From a2968238debadebd4c33602a1fb30e576c27ceaa Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Thu, 14 Nov 2013 15:58:11 +0100 Subject: [PATCH 2/8] added servlet for new story burndown --- .../client/dashboard/DashboardWidget.java | 7 +++- .../client/dashboard/StoryBurndownWidget.java | 41 +++++++++++++++++++ src/main/java/scrum/client/sprint/Sprint.java | 5 +++ .../sprint/StoryBurndownChartServlet.java | 34 +++++++++++++++ src/main/webapp/WEB-INF/web.xml | 11 +++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/main/java/scrum/client/dashboard/StoryBurndownWidget.java create mode 100644 src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java diff --git a/src/main/java/scrum/client/dashboard/DashboardWidget.java b/src/main/java/scrum/client/dashboard/DashboardWidget.java index 05e86da3..21424893 100644 --- a/src/main/java/scrum/client/dashboard/DashboardWidget.java +++ b/src/main/java/scrum/client/dashboard/DashboardWidget.java @@ -37,6 +37,11 @@ protected Widget onInitialization() { new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); sprintBurndown.addSection(new SprintBurndownWidget()); + PagePanel storyBurndown = new PagePanel(); + storyBurndown.addHeader("Story Burndown", + new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); + storyBurndown.addSection(new StoryBurndownWidget()); + PagePanel teamsTasks = new PagePanel(); teamsTasks.addHeader("Teams current work", new HyperlinkWidget(nav.createSwitchAction(widgets.getWhiteboard())), @@ -66,7 +71,7 @@ protected Widget onInitialization() { events.addHeader("Latest Events", new HyperlinkWidget(nav.createSwitchAction(widgets.getProjectEventList()))); events.addSection(new LatestEventsWidget()); - Widget left = TableBuilder.column(5, sprintBurndown, teamsTasks, posTasks); + Widget left = TableBuilder.column(5, sprintBurndown, storyBurndown, teamsTasks, posTasks); Widget right = TableBuilder.column(5, impediments, risks, events); return TableBuilder.row(5, left, right); diff --git a/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java b/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java new file mode 100644 index 00000000..aa77438a --- /dev/null +++ b/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java @@ -0,0 +1,41 @@ +package scrum.client.dashboard; + +import ilarkesto.core.time.Tm; +import scrum.client.common.AScrumWidget; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.Widget; + +public class StoryBurndownWidget extends AScrumWidget { + + public static final int CHART_WIDTH = 800; + public static final int CHART_HEIGHT = 270; + + private Image sprintChart; + + @Override + protected Widget onInitialization() { + sprintChart = new Image(getChartUrl(200)); + return sprintChart; + } + + @Override + protected void onUpdate() { + int width = getChartWidth(); + sprintChart.setWidth(width + "px"); + sprintChart.setUrl(getChartUrl(width) + "×tamp=" + Tm.getCurrentTimeMillis()); + } + + private String getChartUrl(int width) { + return getCurrentSprint().getStoryBurndown(width, CHART_HEIGHT); + } + + private int getChartWidth() { + int width = Window.getClientWidth() - 280; + width = width / 2; + if (width < 100) width = 100; + return width; + } + +} diff --git a/src/main/java/scrum/client/sprint/Sprint.java b/src/main/java/scrum/client/sprint/Sprint.java index 91786e8b..aab3264a 100644 --- a/src/main/java/scrum/client/sprint/Sprint.java +++ b/src/main/java/scrum/client/sprint/Sprint.java @@ -124,6 +124,11 @@ public String getChartUrl(int width, int height) { + height; } + public String getStoryBurndown(int width, int height) { + return GWT.getModuleBaseURL() + "storyBurndownChart.png?sprintId=" + getId() + "&width=" + width + "&height=" + + height; + } + public boolean isCompleted() { return getVelocity() != null; } diff --git a/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java new file mode 100644 index 00000000..c1a5de8d --- /dev/null +++ b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java @@ -0,0 +1,34 @@ +package scrum.server.sprint; + +import ilarkesto.webapp.RequestWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import scrum.client.project.ProjectOverviewWidget; +import scrum.server.ScrumWebApplication; +import scrum.server.WebSession; +import scrum.server.common.AKunagiServlet; + +public class StoryBurndownChartServlet extends AKunagiServlet { + + @Override + protected void onRequest(RequestWrapper req) throws IOException { + String sprintId = req.get("sprintId"); + String width = req.get("width"); + if (width == null) width = String.valueOf(ProjectOverviewWidget.CHART_WIDTH); + String height = req.get("height"); + if (height == null) height = String.valueOf(ProjectOverviewWidget.CHART_HEIGHT); + + req.preventCaching(); + req.setContentType("image/png"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ScrumWebApplication.get().getBurndownChart() + .writeSprintBurndownChart(out, sprintId, Integer.parseInt(width), Integer.parseInt(height)); + + req.write(out.toByteArray()); + } + +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index b6a5b9f0..01190211 100755 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -153,6 +153,17 @@ /scrum.ScrumGwtApplication/sprintBurndownChart.png + + + storyBurndownChart + scrum.server.sprint.StoryBurndownChartServlet + 2 + + + storyBurndownChart + /scrum.ScrumGwtApplication/storyBurndownChart.png + + pdf From 2063d88ef92cb22a94f152da32e1bc43ef35d197 Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Thu, 14 Nov 2013 20:14:06 +0100 Subject: [PATCH 3/8] cooler zwischenstand --- .../scrum/server/ScrumWebApplication.java | 10 + .../server/common/StoryBurndownChart.java | 299 ++++++++++++++++++ .../server/sprint/SprintDaySnapshot.java | 23 ++ .../server/sprint/SprintDaySnapshotDao.java | 24 ++ .../sprint/StoryBurndownChartServlet.java | 4 +- 5 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 src/main/java/scrum/server/common/StoryBurndownChart.java diff --git a/src/main/java/scrum/server/ScrumWebApplication.java b/src/main/java/scrum/server/ScrumWebApplication.java index 5fd7dc2b..ce12cf5b 100644 --- a/src/main/java/scrum/server/ScrumWebApplication.java +++ b/src/main/java/scrum/server/ScrumWebApplication.java @@ -53,6 +53,7 @@ import scrum.server.admin.User; import scrum.server.admin.UserDao; import scrum.server.common.BurndownChart; +import scrum.server.common.StoryBurndownChart; import scrum.server.journal.ProjectEvent; import scrum.server.pr.EmailSender; import scrum.server.pr.SubscriptionService; @@ -67,6 +68,7 @@ public class ScrumWebApplication extends GScrumWebApplication { private static final Log log = Log.get(ScrumWebApplication.class); private BurndownChart burndownChart; + private StoryBurndownChart storyBurndownChart; private KunagiRootConfig config; private ScrumEntityfilePreparator entityfilePreparator; private SystemMessage systemMessage; @@ -94,6 +96,14 @@ public BurndownChart getBurndownChart() { return burndownChart; } + public StoryBurndownChart getStoryBurndownChart() { + if (storyBurndownChart == null) { + storyBurndownChart = new StoryBurndownChart(); + storyBurndownChart.setSprintDao(getSprintDao()); + } + return storyBurndownChart; + } + public SystemConfig getSystemConfig() { return getSystemConfigDao().getSystemConfig(); } diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java new file mode 100644 index 00000000..39fa7b22 --- /dev/null +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -0,0 +1,299 @@ +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.renderer.xy.XYItemRenderer; +import org.jfree.data.Range; +import org.jfree.data.xy.DefaultXYDataset; + +import scrum.client.common.WeekdaySelector; +import scrum.server.css.ScreenCssBuilder; +import scrum.server.sprint.Sprint; +import scrum.server.sprint.SprintDao; +import scrum.server.sprint.SprintDaySnapshot; + +public class StoryBurndownChart { + + private static final Log LOG = Log.get(StoryBurndownChart.class); + + private static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownLine); + private static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); + private static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); + + private SprintDao sprintDao; + + public void setSprintDao(SprintDao sprintDao) { + this.sprintDao = sprintDao; + } + + public void writeStoryBurndownChart(ByteArrayOutputStream out, String sprintId, int width, int height) { + Sprint sprint = sprintDao.getById(sprintId); + if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); + writeStoryBurndownChart(out, sprint, width, height); + } + + public void writeStoryBurndownChart(OutputStream out, Sprint sprint, int width, int height) { + List 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 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 burndownSnapshots = new ArrayList(snapshots); + JFreeChart chart = createStoryBurndownChart(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 createStoryBurndownChart(List snapshots, Date firstDay, Date lastDay, + Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay) { + DefaultXYDataset data = createStoryBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, + freeDays); + + double tick = 1.0; + double max = StoryBurndownChart.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); + + XYItemRenderer renderer = chart.getXYPlot().getRenderer(); + + 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); + + chart.getXYPlot().setDomainAxis(0, domainAxis2); + chart.getXYPlot().setDomainAxis(1, domainAxis1); + chart.getXYPlot().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); + + chart.getXYPlot().setRangeAxis(rangeAxis); + + chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); + + chart.setBackgroundPaint(Color.WHITE); + + return chart; + } + + static DefaultXYDataset createStoryBurndownChartDataset(final List 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(); + } + + static class ChartDataFactory { + + List mainDates = new ArrayList(); + List mainTotal = new ArrayList(); + List mainOpen = new ArrayList(); + + List snapshots; + WeekdaySelector freeDays; + + Date date; + long millisBegin; + long millisEnd; + boolean freeDay; + SprintDaySnapshot snapshot; + boolean workStarted; + boolean workFinished; + + int totalOpen; + int totalStories; + double totalRemaining; + int workDays; + + DefaultXYDataset dataset; + + public void createDataset(final List 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)) { + date = date.nextDay(); + } + + setDate(firstDay); + while (true) { + if (!workFinished) { + totalOpen = snapshot.getTotalStories() - snapshot.getClosedStories(); + totalStories = snapshot.getTotalStories(); + } + + if (workFinished) { + processSuffix(); + } else { + processCenter(); + } + if (date.equals(lastDay)) break; + setDate(date.nextDay()); + } + + dataset = new DefaultXYDataset(); + dataset.addSeries("Open", toArray(mainDates, mainOpen)); + dataset.addSeries("Total", toArray(mainDates, mainTotal)); + } + + 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 processCenter() { + mainDates.add((double) millisBegin); + mainTotal.add((double) totalStories); + mainOpen.add((double) totalOpen); + + mainDates.add((double) millisEnd); + mainTotal.add((double) totalStories); + mainOpen.add((double) totalOpen); + + if (!freeDay) { + workDays++; + } + } + + private void processSuffix() {} + + private SprintDaySnapshot getSnapshot() { + for (SprintDaySnapshot snapshot : snapshots) { + if (snapshot.getDate().equals(date)) return snapshot; + } + + workFinished = true; + totalRemaining = snapshot.getTotalStories() - snapshot.getClosedStories(); + return null; + } + + public DefaultXYDataset getDataset() { + return dataset; + } + + } + + private static double[][] toArray(List a, List 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 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; + } + +} diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java index 68ac1e17..386db618 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java @@ -20,6 +20,17 @@ public class SprintDaySnapshot extends GSprintDaySnapshot implements BurndownSnapshot { + public int getClosedStories() { + return closedStories; + } + + public int getTotalStories() { + return totalStories; + } + + private int closedStories; + private int totalStories; + public void addBurnedWorkFromDeleted(int work) { setBurnedWorkFromDeleted(getBurnedWorkFromDeleted() + work); } @@ -48,4 +59,16 @@ public String toString() { return getDate() + ": " + getBurnedWorkTotal() + ", " + getRemainingWork(); } + public void setClosedStories(int closedStories) { + this.closedStories = closedStories; + updateLastModified(); + fireModified("closedStories=" + closedStories); + } + + public void setTotalStories(int totalStories) { + this.totalStories = totalStories; + updateLastModified(); + fireModified("totalStories=" + totalStories); + } + } diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java index 2ea72488..0527233d 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java @@ -14,14 +14,20 @@ */ package scrum.server.sprint; +import ilarkesto.core.logging.Log; import ilarkesto.core.time.Date; import ilarkesto.fp.Predicate; import java.util.LinkedList; import java.util.List; +import java.util.Set; + +import scrum.server.project.Requirement; public class SprintDaySnapshotDao extends GSprintDaySnapshotDao { + private static final Log log = Log.get(SprintDaySnapshotDao.class); + public SprintDaySnapshot getSprintDaySnapshot(final Sprint sprint, final Date date, boolean autoCreate) { SprintDaySnapshot snapshot = getEntity(new Predicate() { @@ -35,6 +41,23 @@ public boolean test(SprintDaySnapshot e) { snapshot = newEntityInstance(); snapshot.setSprint(sprint); snapshot.setDate(date); + + Set requirements = sprint.getRequirements(); + + int closedStories = 0; + for (Requirement req : requirements) { + if (req.isClosed()) { + closedStories++; + } + } + snapshot.setClosedStories(closedStories); + + int totalStories = requirements.size(); + snapshot.setTotalStories(totalStories); + + log.info("total Stories: " + totalStories); + log.info("closed Stories: " + closedStories); + saveEntity(snapshot); } @@ -57,6 +80,7 @@ public List getSprintDaySnapshots(Sprint sprint) { snapshot.setBurnedWork(previousSnapshot.getBurnedWork()); } } + ret.add(snapshot); previousSnapshot = snapshot; date = date.nextDay(); diff --git a/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java index c1a5de8d..4bdb221a 100644 --- a/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java +++ b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java @@ -25,8 +25,8 @@ protected void onRequest(RequestWrapper req) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - ScrumWebApplication.get().getBurndownChart() - .writeSprintBurndownChart(out, sprintId, Integer.parseInt(width), Integer.parseInt(height)); + ScrumWebApplication.get().getStoryBurndownChart() + .writeStoryBurndownChart(out, sprintId, Integer.parseInt(width), Integer.parseInt(height)); req.write(out.toByteArray()); } From d559609f8668f89e810d581463dbce74fd850573 Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Thu, 14 Nov 2013 23:37:59 +0100 Subject: [PATCH 4/8] still puzzeling us ;-) --- .../server/common/StoryBurndownChart.java | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java index 39fa7b22..9327cdca 100644 --- a/src/main/java/scrum/server/common/StoryBurndownChart.java +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -183,6 +183,7 @@ static class ChartDataFactory { List mainDates = new ArrayList(); List mainTotal = new ArrayList(); List mainOpen = new ArrayList(); + List mainIdeal = new ArrayList(); List snapshots; WeekdaySelector freeDays; @@ -195,8 +196,10 @@ static class ChartDataFactory { boolean workStarted; boolean workFinished; - int totalOpen; - int totalStories; + double totalIdeal = 0; + int totalOpen = 0; + int totalStories = 0; + int totalWorkDays = 0; double totalRemaining; int workDays; @@ -209,27 +212,44 @@ public void createDataset(final List snapshots, final Date fi date = firstDay; while (date.isBeforeOrSame(lastDay)) { + if (!freeDays.isFree(date.getWeekday().getDayOfWeek())) { + totalWorkDays++; + } date = date.nextDay(); } setDate(firstDay); while (true) { - if (!workFinished) { + if (date.isBeforeOrSame(new Date())) { totalOpen = snapshot.getTotalStories() - snapshot.getClosedStories(); totalStories = snapshot.getTotalStories(); - } - - if (workFinished) { - processSuffix(); + processRealData(); + // TODO Was machen wir mit dem heutigen Tag? } else { - processCenter(); + mainDates.add((double) millisBegin); + mainDates.add((double) millisEnd); + + mainTotal.add((double) totalStories); + mainTotal.add((double) totalStories); + + double diff = (double) totalOpen / (double) (totalWorkDays - workDays); + + if (totalIdeal == 0) { + totalIdeal = totalOpen; + } + + mainIdeal.add(totalIdeal); + if (!freeDay) totalIdeal -= diff; + mainIdeal.add(totalIdeal); } + if (date.equals(lastDay)) break; setDate(date.nextDay()); } dataset = new DefaultXYDataset(); dataset.addSeries("Open", toArray(mainDates, mainOpen)); + dataset.addSeries("Ideal", toArray(mainDates, mainIdeal)); dataset.addSeries("Total", toArray(mainDates, mainTotal)); } @@ -241,22 +261,24 @@ private void setDate(Date newDate) { if (!workFinished) snapshot = getSnapshot(); } - private void processCenter() { + private void processRealData() { mainDates.add((double) millisBegin); - mainTotal.add((double) totalStories); - mainOpen.add((double) totalOpen); - mainDates.add((double) millisEnd); + + mainTotal.add((double) totalStories); mainTotal.add((double) totalStories); + + mainOpen.add((double) totalOpen); mainOpen.add((double) totalOpen); + mainIdeal.add((double) totalOpen); + mainIdeal.add((double) totalOpen); + if (!freeDay) { workDays++; } } - private void processSuffix() {} - private SprintDaySnapshot getSnapshot() { for (SprintDaySnapshot snapshot : snapshots) { if (snapshot.getDate().equals(date)) return snapshot; From 6da32d6ffadc543854ee2ac2632c099f019298b4 Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Fri, 15 Nov 2013 10:05:09 +0100 Subject: [PATCH 5/8] updating SprintDaySnapshot for current day --- .../client/dashboard/DashboardWidget.java | 14 +++---- .../server/common/StoryBurndownChart.java | 26 +++++++------ .../server/sprint/SprintDaySnapshot.java | 2 + .../server/sprint/SprintDaySnapshotDao.java | 37 ++++++++++--------- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/main/java/scrum/client/dashboard/DashboardWidget.java b/src/main/java/scrum/client/dashboard/DashboardWidget.java index 21424893..2618b6cf 100644 --- a/src/main/java/scrum/client/dashboard/DashboardWidget.java +++ b/src/main/java/scrum/client/dashboard/DashboardWidget.java @@ -32,16 +32,16 @@ protected Widget onInitialization() { ScrumNavigatorWidget nav = widgets.getSidebar().getNavigator(); - PagePanel sprintBurndown = new PagePanel(); - sprintBurndown.addHeader("Sprint Burndown", - new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); - sprintBurndown.addSection(new SprintBurndownWidget()); - PagePanel storyBurndown = new PagePanel(); storyBurndown.addHeader("Story Burndown", new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); storyBurndown.addSection(new StoryBurndownWidget()); + PagePanel sprintBurndown = new PagePanel(); + sprintBurndown.addHeader("Sprint Burndown", + new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); + sprintBurndown.addSection(new SprintBurndownWidget()); + PagePanel teamsTasks = new PagePanel(); teamsTasks.addHeader("Teams current work", new HyperlinkWidget(nav.createSwitchAction(widgets.getWhiteboard())), @@ -71,8 +71,8 @@ protected Widget onInitialization() { events.addHeader("Latest Events", new HyperlinkWidget(nav.createSwitchAction(widgets.getProjectEventList()))); events.addSection(new LatestEventsWidget()); - Widget left = TableBuilder.column(5, sprintBurndown, storyBurndown, teamsTasks, posTasks); - Widget right = TableBuilder.column(5, impediments, risks, events); + Widget left = TableBuilder.column(5, sprintBurndown, teamsTasks, posTasks); + Widget right = TableBuilder.column(5, storyBurndown, impediments, risks, events); return TableBuilder.row(5, left, right); } diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java index 9327cdca..a59be3bb 100644 --- a/src/main/java/scrum/server/common/StoryBurndownChart.java +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -69,11 +69,11 @@ public void writeStoryBurndownChart(OutputStream out, Sprint sprint, int width, WeekdaySelector freeDays = sprint.getProject().getFreeDaysAsWeekdaySelector(); writeSprintBurndownChart(out, snapshots, sprint.getBegin(), sprint.getEnd(), sprint.getOriginallyEnd(), - freeDays, width, height); + freeDays, width, height, sprint); } static void writeSprintBurndownChart(OutputStream out, List snapshots, Date firstDay, - Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height) { + Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height, Sprint sprint) { LOG.debug("Creating burndown chart:", snapshots.size(), "snapshots from", firstDay, "to", lastDay, "(" + width + "x" + height + " px)"); @@ -87,7 +87,7 @@ static void writeSprintBurndownChart(OutputStream out, List burndownSnapshots = new ArrayList(snapshots); JFreeChart chart = createStoryBurndownChart(burndownSnapshots, firstDay, lastDay, originallyLastDay, freeDays, - dateMarkTickUnit, widthPerDay); + dateMarkTickUnit, widthPerDay, sprint); try { ChartUtilities.writeScaledChartAsPNG(out, chart, width, height, 1, 1); out.flush(); @@ -97,9 +97,9 @@ static void writeSprintBurndownChart(OutputStream out, List snapshots, Date firstDay, Date lastDay, - Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay) { + Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay, Sprint sprint) { DefaultXYDataset data = createStoryBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, - freeDays); + freeDays, sprint); double tick = 1.0; double max = StoryBurndownChart.getMaximum(data); @@ -171,10 +171,11 @@ private static JFreeChart createStoryBurndownChart(List snaps } static DefaultXYDataset createStoryBurndownChartDataset(final List snapshots, - final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays) { + final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays, + Sprint sprint) { ChartDataFactory factory = new ChartDataFactory(); - factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays); + factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays, sprint); return factory.getDataset(); } @@ -206,7 +207,7 @@ static class ChartDataFactory { DefaultXYDataset dataset; public void createDataset(final List snapshots, final Date firstDay, final Date lastDay, - final Date originallyLastDay, final WeekdaySelector freeDays) { + final Date originallyLastDay, final WeekdaySelector freeDays, Sprint sprint) { this.snapshots = snapshots; this.freeDays = freeDays; @@ -220,11 +221,14 @@ public void createDataset(final List snapshots, final Date fi setDate(firstDay); while (true) { - if (date.isBeforeOrSame(new Date())) { - totalOpen = snapshot.getTotalStories() - snapshot.getClosedStories(); + if (date.isBefore(new Date())) { totalStories = snapshot.getTotalStories(); + totalOpen = totalStories - snapshot.getClosedStories(); + processRealData(); + } else if (date.isToday()) { + totalStories = sprint.getRequirements().size(); + totalOpen = totalStories - sprint.getClosedRequirements().size(); processRealData(); - // TODO Was machen wir mit dem heutigen Tag? } else { mainDates.add((double) millisBegin); mainDates.add((double) millisEnd); diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java index 386db618..6aa54dec 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java @@ -60,12 +60,14 @@ public String toString() { } public void setClosedStories(int closedStories) { + if (closedStories == this.closedStories) return; this.closedStories = closedStories; updateLastModified(); fireModified("closedStories=" + closedStories); } public void setTotalStories(int totalStories) { + if (totalStories == this.totalStories) return; this.totalStories = totalStories; updateLastModified(); fireModified("totalStories=" + totalStories); diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java index 0527233d..f3f09419 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java @@ -14,7 +14,6 @@ */ package scrum.server.sprint; -import ilarkesto.core.logging.Log; import ilarkesto.core.time.Date; import ilarkesto.fp.Predicate; @@ -26,8 +25,6 @@ public class SprintDaySnapshotDao extends GSprintDaySnapshotDao { - private static final Log log = Log.get(SprintDaySnapshotDao.class); - public SprintDaySnapshot getSprintDaySnapshot(final Sprint sprint, final Date date, boolean autoCreate) { SprintDaySnapshot snapshot = getEntity(new Predicate() { @@ -37,31 +34,37 @@ public boolean test(SprintDaySnapshot e) { } }); + if (snapshot != null && snapshot.getDate().isToday()) { + updateStoryCount(sprint, snapshot); + saveEntity(snapshot); + } + if (autoCreate && snapshot == null) { snapshot = newEntityInstance(); snapshot.setSprint(sprint); snapshot.setDate(date); - Set requirements = sprint.getRequirements(); + updateStoryCount(sprint, snapshot); - int closedStories = 0; - for (Requirement req : requirements) { - if (req.isClosed()) { - closedStories++; - } - } - snapshot.setClosedStories(closedStories); + saveEntity(snapshot); + } - int totalStories = requirements.size(); - snapshot.setTotalStories(totalStories); + return snapshot; + } - log.info("total Stories: " + totalStories); - log.info("closed Stories: " + closedStories); + private void updateStoryCount(final Sprint sprint, SprintDaySnapshot snapshot) { + Set requirements = sprint.getRequirements(); - saveEntity(snapshot); + int closedStories = 0; + for (Requirement req : requirements) { + if (req.isClosed()) { + closedStories++; + } } + snapshot.setClosedStories(closedStories); - return snapshot; + int totalStories = requirements.size(); + snapshot.setTotalStories(totalStories); } public List getSprintDaySnapshots(Sprint sprint) { From 9e56374d90417da410617e3ee27ab49b25d17042 Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Fri, 15 Nov 2013 10:23:12 +0100 Subject: [PATCH 6/8] no special treatment for today --- .../server/common/StoryBurndownChart.java | 84 ++++++++----------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java index a59be3bb..0244c56f 100644 --- a/src/main/java/scrum/server/common/StoryBurndownChart.java +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -173,18 +173,15 @@ private static JFreeChart createStoryBurndownChart(List snaps static DefaultXYDataset createStoryBurndownChartDataset(final List snapshots, final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays, Sprint sprint) { - - ChartDataFactory factory = new ChartDataFactory(); - factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays, sprint); - return factory.getDataset(); + return new ChartDataFactory().createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays, sprint); } static class ChartDataFactory { - List mainDates = new ArrayList(); - List mainTotal = new ArrayList(); - List mainOpen = new ArrayList(); - List mainIdeal = new ArrayList(); + List dateLine = new ArrayList(); + List totalStoriesLine = new ArrayList(); + List openStoriesLine = new ArrayList(); + List expectedStoriesLine = new ArrayList(); List snapshots; WeekdaySelector freeDays; @@ -194,20 +191,17 @@ static class ChartDataFactory { long millisEnd; boolean freeDay; SprintDaySnapshot snapshot; - boolean workStarted; boolean workFinished; - double totalIdeal = 0; - int totalOpen = 0; + double expectedStories = 0; + int openStories = 0; int totalStories = 0; + int totalWorkDays = 0; - double totalRemaining; int workDays; - DefaultXYDataset dataset; - - public void createDataset(final List snapshots, final Date firstDay, final Date lastDay, - final Date originallyLastDay, final WeekdaySelector freeDays, Sprint sprint) { + public DefaultXYDataset createDataset(final List snapshots, final Date firstDay, + final Date lastDay, final Date originallyLastDay, final WeekdaySelector freeDays, Sprint sprint) { this.snapshots = snapshots; this.freeDays = freeDays; @@ -221,40 +215,37 @@ public void createDataset(final List snapshots, final Date fi setDate(firstDay); while (true) { - if (date.isBefore(new Date())) { + if (date.isPastOrToday()) { totalStories = snapshot.getTotalStories(); - totalOpen = totalStories - snapshot.getClosedStories(); - processRealData(); - } else if (date.isToday()) { - totalStories = sprint.getRequirements().size(); - totalOpen = totalStories - sprint.getClosedRequirements().size(); + openStories = totalStories - snapshot.getClosedStories(); processRealData(); } else { - mainDates.add((double) millisBegin); - mainDates.add((double) millisEnd); + dateLine.add((double) millisBegin); + dateLine.add((double) millisEnd); - mainTotal.add((double) totalStories); - mainTotal.add((double) totalStories); + totalStoriesLine.add((double) totalStories); + totalStoriesLine.add((double) totalStories); - double diff = (double) totalOpen / (double) (totalWorkDays - workDays); + double diff = (double) openStories / (double) (totalWorkDays - workDays); - if (totalIdeal == 0) { - totalIdeal = totalOpen; + if (expectedStories == 0) { + expectedStories = openStories; } - mainIdeal.add(totalIdeal); - if (!freeDay) totalIdeal -= diff; - mainIdeal.add(totalIdeal); + expectedStoriesLine.add(expectedStories); + if (!freeDay) expectedStories -= diff; + expectedStoriesLine.add(expectedStories); } if (date.equals(lastDay)) break; setDate(date.nextDay()); } - dataset = new DefaultXYDataset(); - dataset.addSeries("Open", toArray(mainDates, mainOpen)); - dataset.addSeries("Ideal", toArray(mainDates, mainIdeal)); - dataset.addSeries("Total", toArray(mainDates, mainTotal)); + DefaultXYDataset dataset = new DefaultXYDataset(); + dataset.addSeries("Open", toArray(dateLine, openStoriesLine)); + dataset.addSeries("Ideal", toArray(dateLine, expectedStoriesLine)); + dataset.addSeries("Total", toArray(dateLine, totalStoriesLine)); + return dataset; } private void setDate(Date newDate) { @@ -266,17 +257,17 @@ private void setDate(Date newDate) { } private void processRealData() { - mainDates.add((double) millisBegin); - mainDates.add((double) millisEnd); + dateLine.add((double) millisBegin); + dateLine.add((double) millisEnd); - mainTotal.add((double) totalStories); - mainTotal.add((double) totalStories); + totalStoriesLine.add((double) totalStories); + totalStoriesLine.add((double) totalStories); - mainOpen.add((double) totalOpen); - mainOpen.add((double) totalOpen); + openStoriesLine.add((double) openStories); + openStoriesLine.add((double) openStories); - mainIdeal.add((double) totalOpen); - mainIdeal.add((double) totalOpen); + expectedStoriesLine.add((double) openStories); + expectedStoriesLine.add((double) openStories); if (!freeDay) { workDays++; @@ -289,14 +280,9 @@ private SprintDaySnapshot getSnapshot() { } workFinished = true; - totalRemaining = snapshot.getTotalStories() - snapshot.getClosedStories(); return null; } - public DefaultXYDataset getDataset() { - return dataset; - } - } private static double[][] toArray(List a, List b) { From 7c225df2c44e0cc41000cfad3f317512e0e2af9f Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Fri, 15 Nov 2013 10:53:30 +0100 Subject: [PATCH 7/8] better structure for burndown charts --- .../scrum/server/ScrumWebApplication.java | 8 +- .../scrum/server/common/BurndownChart.java | 365 +----------------- .../server/common/DefaultXYDatasetUtil.java | 20 + .../server/common/StoryBurndownChart.java | 102 ++--- .../server/common/TaskBurndownChart.java | 346 +++++++++++++++++ .../scrum/server/project/HomepageUpdater.java | 4 +- .../sprint/SprintBacklogPdfCreator.java | 4 +- .../server/sprint/SprintReportPdfCreator.java | 4 +- .../server/common/BurndownChartTest.java | 2 +- 9 files changed, 417 insertions(+), 438 deletions(-) create mode 100644 src/main/java/scrum/server/common/DefaultXYDatasetUtil.java create mode 100644 src/main/java/scrum/server/common/TaskBurndownChart.java diff --git a/src/main/java/scrum/server/ScrumWebApplication.java b/src/main/java/scrum/server/ScrumWebApplication.java index ce12cf5b..57f86d72 100644 --- a/src/main/java/scrum/server/ScrumWebApplication.java +++ b/src/main/java/scrum/server/ScrumWebApplication.java @@ -52,7 +52,7 @@ import scrum.server.admin.SystemConfig; import scrum.server.admin.User; import scrum.server.admin.UserDao; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.common.StoryBurndownChart; import scrum.server.journal.ProjectEvent; import scrum.server.pr.EmailSender; @@ -67,7 +67,7 @@ public class ScrumWebApplication extends GScrumWebApplication { private static final Log log = Log.get(ScrumWebApplication.class); - private BurndownChart burndownChart; + private TaskBurndownChart burndownChart; private StoryBurndownChart storyBurndownChart; private KunagiRootConfig config; private ScrumEntityfilePreparator entityfilePreparator; @@ -88,9 +88,9 @@ protected int getDataVersion() { // --- composites --- - public BurndownChart getBurndownChart() { + public TaskBurndownChart getBurndownChart() { if (burndownChart == null) { - burndownChart = new BurndownChart(); + burndownChart = new TaskBurndownChart(); burndownChart.setSprintDao(getSprintDao()); } return burndownChart; diff --git a/src/main/java/scrum/server/common/BurndownChart.java b/src/main/java/scrum/server/common/BurndownChart.java index ee9f8add..bdb27d30 100644 --- a/src/main/java/scrum/server/common/BurndownChart.java +++ b/src/main/java/scrum/server/common/BurndownChart.java @@ -1,221 +1,26 @@ -/* - * Copyright 2009, 2010, 2011 Fabian Hager, Witoslaw Koczewsi - * - * 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 - * . - */ 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.renderer.xy.XYItemRenderer; -import org.jfree.data.Range; -import org.jfree.data.xy.DefaultXYDataset; - -import scrum.client.common.WeekdaySelector; import scrum.server.css.ScreenCssBuilder; -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(ScreenCssBuilder.cBurndownLine); - private static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); - private static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); +public abstract class BurndownChart { - // --- dependencies --- + protected static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownLine); + protected static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); + protected static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); - private SprintDao sprintDao; + protected 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 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 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 burndownSnapshots = new ArrayList(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 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); - - XYItemRenderer renderer = chart.getXYPlot().getRenderer(); - - 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); - - chart.getXYPlot().setDomainAxis(0, domainAxis2); - chart.getXYPlot().setDomainAxis(1, domainAxis1); - chart.getXYPlot().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); - - chart.getXYPlot().setRangeAxis(rangeAxis); - - chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); - - chart.setBackgroundPaint(Color.WHITE); - - 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 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 a, List b) { + protected static double[][] toArray(List a, List b) { int min = Math.min(a.size(), b.size()); double[][] array = new double[2][min]; for (int i = 0; i < min; i++) { @@ -225,162 +30,4 @@ private static double[][] toArray(List a, List b) { return array; } - static class ChartDataFactory { - - List mainDates = new ArrayList(); - List mainValues = new ArrayList(); - - List extrapolationDates = new ArrayList(); - List extrapolationValues = new ArrayList(); - - List idealDates = new ArrayList(); - List idealValues = new ArrayList(); - - List 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 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; - } - - } - } diff --git a/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java b/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java new file mode 100644 index 00000000..80f49642 --- /dev/null +++ b/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java @@ -0,0 +1,20 @@ +package scrum.server.common; + +import org.jfree.data.xy.DefaultXYDataset; + +public class DefaultXYDatasetUtil { + + 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; + } + +} diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java index 0244c56f..53431ce8 100644 --- a/src/main/java/scrum/server/common/StoryBurndownChart.java +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -2,7 +2,6 @@ import ilarkesto.base.Str; import ilarkesto.base.Sys; -import ilarkesto.base.Utl; import ilarkesto.core.logging.Log; import ilarkesto.core.time.Date; @@ -31,25 +30,13 @@ import org.jfree.data.xy.DefaultXYDataset; import scrum.client.common.WeekdaySelector; -import scrum.server.css.ScreenCssBuilder; import scrum.server.sprint.Sprint; -import scrum.server.sprint.SprintDao; import scrum.server.sprint.SprintDaySnapshot; -public class StoryBurndownChart { +public class StoryBurndownChart extends BurndownChart { private static final Log LOG = Log.get(StoryBurndownChart.class); - private static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownLine); - private static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); - private static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); - - private SprintDao sprintDao; - - public void setSprintDao(SprintDao sprintDao) { - this.sprintDao = sprintDao; - } - public void writeStoryBurndownChart(ByteArrayOutputStream out, String sprintId, int width, int height) { Sprint sprint = sprintDao.getById(sprintId); if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); @@ -102,7 +89,7 @@ private static JFreeChart createStoryBurndownChart(List snaps freeDays, sprint); double tick = 1.0; - double max = StoryBurndownChart.getMaximum(data); + double max = DefaultXYDatasetUtil.getMaximum(data); while (max / tick > 25) { tick *= 2; @@ -191,9 +178,8 @@ static class ChartDataFactory { long millisEnd; boolean freeDay; SprintDaySnapshot snapshot; - boolean workFinished; - double expectedStories = 0; + double expectedStories = -1; int openStories = 0; int totalStories = 0; @@ -216,25 +202,9 @@ public DefaultXYDataset createDataset(final List snapshots, f setDate(firstDay); while (true) { if (date.isPastOrToday()) { - totalStories = snapshot.getTotalStories(); - openStories = totalStories - snapshot.getClosedStories(); - processRealData(); + calculateOnRealData(); } else { - dateLine.add((double) millisBegin); - dateLine.add((double) millisEnd); - - totalStoriesLine.add((double) totalStories); - totalStoriesLine.add((double) totalStories); - - double diff = (double) openStories / (double) (totalWorkDays - workDays); - - if (expectedStories == 0) { - expectedStories = openStories; - } - - expectedStoriesLine.add(expectedStories); - if (!freeDay) expectedStories -= diff; - expectedStoriesLine.add(expectedStories); + calculateForFuture(); } if (date.equals(lastDay)) break; @@ -248,15 +218,28 @@ public DefaultXYDataset createDataset(final List snapshots, f return dataset; } - 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 calculateForFuture() { + dateLine.add((double) millisBegin); + dateLine.add((double) millisEnd); + + totalStoriesLine.add((double) totalStories); + totalStoriesLine.add((double) totalStories); + + double diff = (double) openStories / (double) (totalWorkDays - workDays); + + if (expectedStories == -1) { + expectedStories = openStories; + } + + expectedStoriesLine.add(expectedStories); + if (!freeDay) expectedStories -= diff; + expectedStoriesLine.add(expectedStories); } - private void processRealData() { + private void calculateOnRealData() { + totalStories = snapshot.getTotalStories(); + openStories = totalStories - snapshot.getClosedStories(); + dateLine.add((double) millisBegin); dateLine.add((double) millisEnd); @@ -274,38 +257,21 @@ private void processRealData() { } } - private SprintDaySnapshot getSnapshot() { + private void setDate(Date newDate) { + date = newDate; + millisBegin = date.toMillis(); + millisEnd = date.nextDay().toMillis(); + freeDay = freeDays.isFree(date.getWeekday().getDayOfWeek()); + snapshot = getSnapshot(date); + } + + private SprintDaySnapshot getSnapshot(Date date) { for (SprintDaySnapshot snapshot : snapshots) { if (snapshot.getDate().equals(date)) return snapshot; } - - workFinished = true; return null; } } - private static double[][] toArray(List a, List 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 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; - } - } diff --git a/src/main/java/scrum/server/common/TaskBurndownChart.java b/src/main/java/scrum/server/common/TaskBurndownChart.java new file mode 100644 index 00000000..441e84df --- /dev/null +++ b/src/main/java/scrum/server/common/TaskBurndownChart.java @@ -0,0 +1,346 @@ +/* + * Copyright 2009, 2010, 2011 Fabian Hager, Witoslaw Koczewsi + * + * 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 + * . + */ +package scrum.server.common; + +import ilarkesto.base.Str; +import ilarkesto.base.Sys; +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.renderer.xy.XYItemRenderer; +import org.jfree.data.Range; +import org.jfree.data.xy.DefaultXYDataset; + +import scrum.client.common.WeekdaySelector; +import scrum.server.sprint.Sprint; +import scrum.server.sprint.SprintDaySnapshot; + +public class TaskBurndownChart extends BurndownChart { + + private static final Log LOG = Log.get(TaskBurndownChart.class); + + public static byte[] createBurndownChartAsByteArray(Sprint sprint, int width, int height) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new TaskBurndownChart().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 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 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 burndownSnapshots = new ArrayList(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 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 = DefaultXYDatasetUtil.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); + + XYItemRenderer renderer = chart.getXYPlot().getRenderer(); + + 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); + + chart.getXYPlot().setDomainAxis(0, domainAxis2); + chart.getXYPlot().setDomainAxis(1, domainAxis1); + chart.getXYPlot().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); + + chart.getXYPlot().setRangeAxis(rangeAxis); + + chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); + + chart.setBackgroundPaint(Color.WHITE); + + return chart; + } + + static DefaultXYDataset createSprintBurndownChartDataset(final List 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(); + } + + static class ChartDataFactory { + + List mainDates = new ArrayList(); + List mainValues = new ArrayList(); + + List extrapolationDates = new ArrayList(); + List extrapolationValues = new ArrayList(); + + List idealDates = new ArrayList(); + List idealValues = new ArrayList(); + + List 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 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; + } + + } + +} diff --git a/src/main/java/scrum/server/project/HomepageUpdater.java b/src/main/java/scrum/server/project/HomepageUpdater.java index fed34fe0..0d96ebfa 100644 --- a/src/main/java/scrum/server/project/HomepageUpdater.java +++ b/src/main/java/scrum/server/project/HomepageUpdater.java @@ -45,7 +45,7 @@ import scrum.server.collaboration.Comment; import scrum.server.collaboration.CommentDao; import scrum.server.collaboration.Wikipage; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.issues.Issue; import scrum.server.pr.BlogEntry; import scrum.server.release.Release; @@ -230,7 +230,7 @@ private void processBlogEntryTemplate(BlogEntry entry) { } private void createSprintBurndownChart(int width, int height) { - byte[] imageData = BurndownChart.createBurndownChartAsByteArray(project.getCurrentSprint(), width, height); + byte[] imageData = TaskBurndownChart.createBurndownChartAsByteArray(project.getCurrentSprint(), width, height); IO.copyDataToFile(imageData, new File(outputDir.getPath() + "/sprint-burndown-" + width + "x" + height + ".png")); } diff --git a/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java b/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java index c7a3e195..10fbc1a3 100644 --- a/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java +++ b/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java @@ -23,7 +23,7 @@ import java.util.List; import scrum.server.common.APdfCreator; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.project.Project; import scrum.server.project.Requirement; @@ -56,7 +56,7 @@ protected void build(APdfContainerElement pdf) { fields.field("Team").text(sprint.getTeamMembersAsString()); pdf.nl(); - pdf.image(BurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); + pdf.image(TaskBurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); pdf.nl(); List requirements = new ArrayList(sprint.getRequirements()); diff --git a/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java b/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java index 75994588..9aafa550 100644 --- a/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java +++ b/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java @@ -30,7 +30,7 @@ import scrum.client.sprint.SprintHistoryHelper.StoryInfo; import scrum.client.sprint.SprintHistoryHelper.TaskInfo; import scrum.server.common.APdfCreator; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.common.ScrumPdfContext; import scrum.server.common.WikiToPdfConverter; import scrum.server.project.Requirement; @@ -66,7 +66,7 @@ protected void build(APdfContainerElement pdf) { fields.field("Team").text(sprint.getTeamMembersAsString()); pdf.nl(); - pdf.image(BurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); + pdf.image(TaskBurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); ScrumPdfContext pdfContext = new ScrumPdfContext(sprint.getProject()); if (sprint.isGoalSet()) { diff --git a/src/test/java/scrum/server/common/BurndownChartTest.java b/src/test/java/scrum/server/common/BurndownChartTest.java index 42d92cde..e4ff84db 100644 --- a/src/test/java/scrum/server/common/BurndownChartTest.java +++ b/src/test/java/scrum/server/common/BurndownChartTest.java @@ -81,7 +81,7 @@ public void sprintBurndown() throws IOException { log.info(file.getAbsolutePath()); IO.createDirectory(file.getParentFile()); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); - BurndownChart.writeSprintBurndownChart(out, shots, sprintBeginDate, sprintEndDate, sprintOriginallyEndDate, + TaskBurndownChart.writeSprintBurndownChart(out, shots, sprintBeginDate, sprintEndDate, sprintOriginallyEndDate, freeDays, 1000, 500); out.close(); } From 7d4e694a5987918114681792e536bbd700bd0d9d Mon Sep 17 00:00:00 2001 From: Stefan Glase Date: Tue, 19 Nov 2013 09:56:31 +0100 Subject: [PATCH 8/8] readd lost header --- .../java/scrum/server/common/BurndownChart.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/scrum/server/common/BurndownChart.java b/src/main/java/scrum/server/common/BurndownChart.java index bdb27d30..18c99b69 100644 --- a/src/main/java/scrum/server/common/BurndownChart.java +++ b/src/main/java/scrum/server/common/BurndownChart.java @@ -1,3 +1,17 @@ +/* + * Copyright 2009, 2010, 2011 Fabian Hager, Witoslaw Koczewsi + * + * 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 + * . + */ package scrum.server.common; import ilarkesto.base.Utl;