///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition 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 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 org.projectforge.statistics; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Calendar; import java.util.Iterator; import java.util.List; import org.jfree.chart.JFreeChart; import org.jfree.chart.renderer.xy.XYDifferenceRenderer; import org.jfree.data.time.Day; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.projectforge.calendar.DayHolder; import org.projectforge.charting.XYChartBuilder; import org.projectforge.core.OrderDirection; import org.projectforge.timesheet.TimesheetDO; import org.projectforge.timesheet.TimesheetDao; import org.projectforge.timesheet.TimesheetFilter; /** * Erzeugt wahlweise eins von zwei Diagrammen:<br/> * <ol> * <li>Ein Diagramm, welches über die letzten n Tage die kummulierten IST-Arbeitsstunden und als Soll-Wert die tatsächlich gebuchten * Zeitberichte aufträgt. Dies wird in einem Differenz-XY-Diagramm visualisiert. Die Darstellung soll motivieren, dass Projektmitarbeiter * ihre Zeitberichte möglichst zeitnah eintragen.</li> * <li>Ein Diagramm, welches über die letzten n Tage die Tage visualisiert, die zwischen Zeitberichtsdatum und Zeitpunkt der tatsächlichen * Buchung liegen.</li> * </ol> * * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class TimesheetDisciplineChartBuilder { private static final double PLANNED_AVERAGE_DIFFERENCE_BETWEEN_TIMESHEET_AND_BOOKING = 2.0; // days. private double planWorkingHours; private double actualWorkingHours; private BigDecimal averageDifferenceBetweenTimesheetAndBooking; /** * Calculated total plan working hours calculated after call of create #1. */ public double getPlanWorkingHours() { return planWorkingHours; } /** * Calculated total actual working hours calculated after call of create #1. */ public double getActualWorkingHours() { return actualWorkingHours; } /** * Gets the calculated average number of days between the date of a time sheet and the date of creation (after call create #2). */ public BigDecimal getAverageDifferenceBetweenTimesheetAndBooking() { return averageDifferenceBetweenTimesheetAndBooking; } public double getPlannedAverageDifferenceBetweenTimesheetAndBooking() { return PLANNED_AVERAGE_DIFFERENCE_BETWEEN_TIMESHEET_AND_BOOKING; } /** * Ein Diagramm, welches über die letzten n Tage die kummulierten IST-Arbeitsstunden und als Soll-Wert die tatsächlich gebuchten * Zeitberichte aufträgt. Dies wird in einem Differenz-XY-Diagramm visualisiert. Die Darstellung soll motivieren, dass Projektmitarbeiter * ihre Zeitberichte möglichst zeitnah eintragen. * @param timesheetDao * @param userId * @param workingHoursPerDay * @param forLastNDays * @param shape e. g. new Ellipse2D.Float(-3, -3, 6, 6) or null, if no marker should be printed. * @param stroke e. g. new BasicStroke(3.0f). * @param showAxisValues * @return */ public JFreeChart create(final TimesheetDao timesheetDao, final Integer userId, final double workingHoursPerDay, final short forLastNDays, final boolean showAxisValues) { final DayHolder dh = new DayHolder(); final TimesheetFilter filter = new TimesheetFilter(); filter.setStopTime(dh.getDate()); dh.add(Calendar.DATE, -forLastNDays); filter.setStartTime(dh.getDate()); filter.setUserId(userId); filter.setOrderType(OrderDirection.ASC); final List<TimesheetDO> list = timesheetDao.getList(filter); final TimeSeries sollSeries = new TimeSeries("Soll"); final TimeSeries istSeries = new TimeSeries("Ist"); planWorkingHours = 0; actualWorkingHours = 0; final Iterator<TimesheetDO> it = list.iterator(); TimesheetDO current = null; if (it.hasNext() == true) { current = it.next(); } for (int i = 0; i <= forLastNDays; i++) { while (current != null && (dh.isSameDay(current.getStartTime()) == true || current.getStartTime().before(dh.getDate()) == true)) { actualWorkingHours += ((double) current.getWorkFractionDuration()) / 3600000; if (it.hasNext() == true) { current = it.next(); } else { current = null; break; } } if (dh.isWorkingDay() == true) { final BigDecimal workFraction = dh.getWorkFraction(); if (workFraction != null) { planWorkingHours += workFraction.doubleValue() * workingHoursPerDay; } else { planWorkingHours += workingHoursPerDay; } } final Day day = new Day(dh.getDayOfMonth(), dh.getMonth() + 1, dh.getYear()); sollSeries.add(day, planWorkingHours); istSeries.add(day, actualWorkingHours); dh.add(Calendar.DATE, 1); } final TimeSeriesCollection dataset = new TimeSeriesCollection(); dataset.addSeries(sollSeries); dataset.addSeries(istSeries); final XYChartBuilder cb = new XYChartBuilder(null, null, null, dataset, false); final XYDifferenceRenderer diffRenderer = new XYDifferenceRenderer(cb.getRedFill(), cb.getGreenFill(), true); diffRenderer.setSeriesPaint(0, cb.getRedMarker()); diffRenderer.setSeriesPaint(1, cb.getGreenMarker()); cb.setRenderer(0, diffRenderer).setStrongStyle(diffRenderer, false, sollSeries, istSeries); cb.setDateXAxis(true).setYAxis(true, "hours"); return cb.getChart(); } /** * Ein Diagramm, welches über die letzten n Tage die Tage visualisiert, die zwischen Zeitberichtsdatum und Zeitpunkt der tatsächlichen * Buchung liegen. * @param timesheetDao * @param userId * @param forLastNDays * @param shape e. g. new Ellipse2D.Float(-3, -3, 6, 6) or null, if no marker should be printed. * @param stroke e. g. new BasicStroke(3.0f). * @param showAxisValues * @return */ public JFreeChart create(final TimesheetDao timesheetDao, final Integer userId, final short forLastNDays, final boolean showAxisValues) { final DayHolder dh = new DayHolder(); final TimesheetFilter filter = new TimesheetFilter(); filter.setStopTime(dh.getDate()); dh.add(Calendar.DATE, -forLastNDays); filter.setStartTime(dh.getDate()); filter.setUserId(userId); filter.setOrderType(OrderDirection.ASC); final List<TimesheetDO> list = timesheetDao.getList(filter); final TimeSeries planSeries = new TimeSeries("Soll"); final TimeSeries actualSeries = new TimeSeries("Ist"); final Iterator<TimesheetDO> it = list.iterator(); TimesheetDO current = null; if (it.hasNext() == true) { current = it.next(); } long numberOfBookedDays = 0; long totalDifference = 0; for (int i = 0; i <= forLastNDays; i++) { long difference = 0; long totalDuration = 0; // Weight for average. while (current != null && (dh.isSameDay(current.getStartTime()) == true || current.getStartTime().before(dh.getDate()) == true)) { final long duration = current.getWorkFractionDuration(); difference += (current.getCreated().getTime() - current.getStartTime().getTime()) * duration; totalDuration += duration; if (it.hasNext() == true) { current = it.next(); } else { current = null; break; } } final double averageDifference = difference > 0 ? ((double) difference) / totalDuration / 86400000 : 0; // In days. final Day day = new Day(dh.getDayOfMonth(), dh.getMonth() + 1, dh.getYear()); if (averageDifference > 0) { planSeries.add(day, PLANNED_AVERAGE_DIFFERENCE_BETWEEN_TIMESHEET_AND_BOOKING); // plan average // (PLANNED_AVERAGE_DIFFERENCE_BETWEEN_TIMESHEET_AND_BOOKING // days). actualSeries.add(day, averageDifference); totalDifference += averageDifference; numberOfBookedDays++; } dh.add(Calendar.DATE, 1); } averageDifferenceBetweenTimesheetAndBooking = numberOfBookedDays > 0 ? new BigDecimal(totalDifference).divide(new BigDecimal( numberOfBookedDays), 1, RoundingMode.HALF_UP) : BigDecimal.ZERO; final TimeSeriesCollection dataset = new TimeSeriesCollection(); dataset.addSeries(actualSeries); dataset.addSeries(planSeries); final XYChartBuilder cb = new XYChartBuilder(null, null, null, dataset, false); final XYDifferenceRenderer diffRenderer = new XYDifferenceRenderer(cb.getRedFill(), cb.getGreenFill(), true); diffRenderer.setSeriesPaint(0, cb.getRedMarker()); diffRenderer.setSeriesPaint(1, cb.getGreenMarker()); cb.setRenderer(0, diffRenderer).setStrongStyle(diffRenderer, false, actualSeries, planSeries); cb.setDateXAxis(true).setYAxis(true, "days"); return cb.getChart(); } }