package de.pbauerochse.worklogviewer.fx.tabs; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import de.pbauerochse.worklogviewer.domain.TimerangeProvider; import de.pbauerochse.worklogviewer.excel.ExcelColumnRenderer; import de.pbauerochse.worklogviewer.excel.columns.TaskDescriptionExcelColumn; import de.pbauerochse.worklogviewer.excel.columns.TaskStatusExcelColumn; import de.pbauerochse.worklogviewer.excel.columns.TaskWorklogSummaryExcelColumn; import de.pbauerochse.worklogviewer.excel.columns.WorklogExcelColumn; import de.pbauerochse.worklogviewer.fx.tablecolumns.TaskDescriptionTreeTableColumn; import de.pbauerochse.worklogviewer.fx.tablecolumns.TaskStatusTreeTableColumn; import de.pbauerochse.worklogviewer.fx.tablecolumns.TaskWorklogSummaryTreeTableColumn; import de.pbauerochse.worklogviewer.fx.tablecolumns.WorklogTreeTableColumn; import de.pbauerochse.worklogviewer.fx.tabs.domain.DisplayData; import de.pbauerochse.worklogviewer.fx.tabs.domain.DisplayDayEntry; import de.pbauerochse.worklogviewer.fx.tabs.domain.DisplayRow; import de.pbauerochse.worklogviewer.fx.tasks.FetchTimereportContext; import de.pbauerochse.worklogviewer.util.ExceptionUtil; import de.pbauerochse.worklogviewer.util.FormattingUtil; import de.pbauerochse.worklogviewer.util.SettingsUtil; import de.pbauerochse.worklogviewer.youtrack.domain.TaskWithWorklogs; import de.pbauerochse.worklogviewer.youtrack.domain.WorklogItem; import de.pbauerochse.worklogviewer.youtrack.domain.WorklogReport; import javafx.collections.ObservableList; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.StackedBarChart; import javafx.scene.chart.XYChart; import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.Sheet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.Collator; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; /** * @author Patrick Bauerochse * @since 02.07.15 */ public abstract class WorklogTab extends Tab { protected static final Collator COLLATOR = Collator.getInstance(Locale.GERMANY); // height adjustment parameters for bargraph private static final int HEIGHT_PER_Y_AXIS_ELEMENT = 40; private static final int HEIGHT_PER_X_AXIS_ELEMENT = 35; private static final int ADDITIONAL_HEIGHT = 150; private Logger LOGGER = LoggerFactory.getLogger(WorklogTab.class); private TreeTableView<DisplayRow> taskTableView; private Optional<TimerangeProvider> lastUsedTimerangeProvider = Optional.empty(); private Optional<Integer> lastHighlightState = Optional.empty(); private Optional<Integer> lastCollapseState = Optional.empty(); private Optional<FetchTimereportContext> fetchTimereportContext = Optional.empty(); private boolean resultToDisplayChangedSinceLastRender; private VBox statisticsView; private TaskDescriptionTreeTableColumn taskDescriptionTreeTableColumn; private TaskStatusTreeTableColumn taskStatusTreeTableColumn; private Optional<DisplayData> resultItemsToDisplay = Optional.empty(); protected abstract List<TaskWithWorklogs> getFilteredList(List<TaskWithWorklogs> tasks); public WorklogTab(String name) { super(name); setContent(getContentNode()); // when this tab becomes active render the data selectedProperty().addListener((observable, oldValue, newValue) -> { if (newValue && resultToDisplayChangedSinceLastRender) { // tab switched to active LOGGER.debug("Showing tab {}", getText()); refreshWorklogTableViewAndResults(); } }); } /** * Update the items to display. If this tab is currently selected * the content will be refreshed. Else it will be refreshed whenever * this tab becomes active * * @param timereportContext The TaskWithWorklogs to show in this tab */ public void updateItems(FetchTimereportContext timereportContext) { this.fetchTimereportContext = Optional.of(timereportContext); resultToDisplayChangedSinceLastRender = true; if (isSelected()) { refreshWorklogTableViewAndResults(); } } protected Node getContentNode() { Node taskView = getTaskView(); if (SettingsUtil.loadSettings().isShowStatistics()) { LOGGER.debug("Statistics enabled in settings"); statisticsView = new VBox(20); // wrap statistics in scrollpane ScrollPane scrollPane = new ScrollPane(statisticsView); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setFitToWidth(true); scrollPane.setPadding(new Insets(7)); // both statistics and task view present // show both in a split pane SplitPane splitPane = new SplitPane(); splitPane.setOrientation(Orientation.HORIZONTAL); splitPane.setDividerPosition(0, 0.8); splitPane.getItems().addAll(taskView, scrollPane); return splitPane; } else { LOGGER.debug("Statistics disabled in settings"); return taskView; } } protected Node getTaskView() { if (taskTableView == null) { taskTableView = new TreeTableView<>(new TreeItem<>()); taskTableView.setShowRoot(false); } if (taskDescriptionTreeTableColumn == null) { taskDescriptionTreeTableColumn = new TaskDescriptionTreeTableColumn(); taskDescriptionTreeTableColumn.setPrefWidth(300); taskDescriptionTreeTableColumn.setMinWidth(300); } if (taskStatusTreeTableColumn == null) { taskStatusTreeTableColumn = new TaskStatusTreeTableColumn(); taskStatusTreeTableColumn.setPrefWidth(30); taskStatusTreeTableColumn.setMinWidth(30); } AnchorPane anchorPane = new AnchorPane(taskTableView); anchorPane.setPadding(new Insets(6)); AnchorPane.setTopAnchor(taskTableView, 0d); AnchorPane.setRightAnchor(taskTableView, 0d); AnchorPane.setBottomAnchor(taskTableView, 0d); AnchorPane.setLeftAnchor(taskTableView, 0d); return anchorPane; } protected void refreshWorklogTableViewAndResults() { // return early if no data present or still the same data // as the last time this tab was active Optional<FetchTimereportContext> reportContextOptional = this.fetchTimereportContext; if (!reportContextOptional.isPresent() || !reportContextOptional.get().getResult().isPresent() || !resultToDisplayChangedSinceLastRender) { LOGGER.debug("[{}] No results to display or data not changed. Not refreshing TableView and data", getText()); return; } SettingsUtil.Settings settings = SettingsUtil.loadSettings(); if (settings.isShowStatistics() && statisticsView == null || !settings.isShowStatistics() && statisticsView != null) { // statistics are disabled and were previously rendered // or statistics are enabled and weren't rendered before // update content view LOGGER.debug("Updating contentView since settings for statistics seemed to have changed"); setContent(getContentNode()); } FetchTimereportContext timereportContext = reportContextOptional.get(); TimerangeProvider timerangeProvider = timereportContext.getTimerangeProvider(); // render the table columns if the timerange changed from last result if (!lastUsedTimerangeProvider.isPresent() || !lastUsedTimerangeProvider.get().equals(timerangeProvider) || (lastCollapseState.isPresent() && lastCollapseState.get() != settings.getCollapseState()) || (lastHighlightState.isPresent() && lastHighlightState.get() != settings.getHighlightState())) { LOGGER.debug("[{}] Regenerating columns for timerange {}", getText(), timerangeProvider.getReportTimerange().name()); taskTableView.getColumns().clear(); taskTableView.getColumns().add(taskStatusTreeTableColumn); taskTableView.getColumns().add(taskDescriptionTreeTableColumn); // render tables for all days in the selected timerange // e.g. timerange current month renders a column for // each day of the current month long amountOfDaysToDisplay = ChronoUnit.DAYS.between(timerangeProvider.getStartDate(), timerangeProvider.getEndDate()); for (int days = 0; days <= amountOfDaysToDisplay; days++) { LocalDate currentColumnDate = timerangeProvider.getStartDate().plus(days, ChronoUnit.DAYS); String displayDate = FormattingUtil.formatDate(currentColumnDate); // worklog column taskTableView.getColumns().add(new WorklogTreeTableColumn(displayDate, currentColumnDate)); } // also add another summary per task column taskTableView.getColumns().add(new TaskWorklogSummaryTreeTableColumn()); lastUsedTimerangeProvider = Optional.of(timerangeProvider); lastCollapseState = Optional.of(settings.getCollapseState()); lastHighlightState = Optional.of(settings.getHighlightState()); } // refresh data LOGGER.debug("[{}] Refreshing items in TableView", getText()); TreeItem<DisplayRow> root = taskTableView.getRoot(); root.getChildren().clear(); DisplayData displayData = getDisplayData(timereportContext, resultToDisplayChangedSinceLastRender); root.getChildren().addAll(displayData.getTreeRows()); resultToDisplayChangedSinceLastRender = false; } private DisplayData getDisplayData(FetchTimereportContext reportContext, boolean changedSinceLastRender) { if (changedSinceLastRender) { LOGGER.debug("Refreshing display data"); DisplayData displayData = new DisplayData(); WorklogReport result = reportContext.getResult().get(); ImmutableList<TaskWithWorklogs> originalTasks = result.getTasks(); // create a copy of the original task list // and pass it on to the getFilteredList method // which then may freely modify the list and its items List<TaskWithWorklogs> deepCopiedList = Lists.newArrayList(); originalTasks.forEach(taskWithWorklogs -> deepCopiedList.add(taskWithWorklogs.createDeepCopy())); List<TaskWithWorklogs> filteredList = getFilteredList(deepCopiedList); // render the treetabledata if (reportContext.getGroupByCategory().isPresent()) { // grouping present processWithGrouping(filteredList, displayData); } else { // no grouping processWithoutGrouping(filteredList, displayData); } // add grandtotal column DisplayRow grandTotal = new DisplayRow(); grandTotal.setIsGrandTotalSummary(true); displayData.addRow(new TreeItem<>(grandTotal)); filteredList.stream() .map(TaskWithWorklogs::getWorklogItemList) .flatMap(Collection::stream) .forEach(worklogItem -> { LocalDate date = worklogItem.getDate(); DisplayDayEntry workdayEntry = grandTotal.getWorkdayEntry(date) .orElseGet(() -> { DisplayDayEntry displayDayEntry = new DisplayDayEntry(); displayDayEntry.setDate(date); grandTotal.addDisplayDayEntry(displayDayEntry); return displayDayEntry; }); workdayEntry.getSpentTime().addAndGet(worklogItem.getDurationInMinutes()); }); // call statistics update updateStatisticsData(filteredList); resultItemsToDisplay = Optional.of(displayData); } return resultItemsToDisplay.get(); } private void processWithGrouping(List<TaskWithWorklogs> tasks, DisplayData displayData) { LOGGER.debug("Processing with grouping"); List<String> distinctGroupByCriteria = tasks.stream() .map(TaskWithWorklogs::getDistinctGroupByCriteriaValues) .flatMap(Collection::stream) .distinct() .sorted(COLLATOR) .collect(Collectors.toList()); distinctGroupByCriteria.forEach(groupByCriteria -> { LOGGER.debug("Gathering data for group criteria value {}", groupByCriteria); DisplayRow groupCaptionRow = new DisplayRow(); groupCaptionRow.setIsGroupContainer(true); groupCaptionRow.setLabel(groupByCriteria); TreeItem<DisplayRow> groupRow = new TreeItem<>(groupCaptionRow); groupRow.setExpanded(true); Map<String, DisplayRow> ticketIdToDisplayRow = Maps.newHashMap(); // add sub rows to groupRow tasks.stream() .filter(taskWithWorklogs -> taskWithWorklogs.getDistinctGroupByCriteriaValues().contains(groupByCriteria)) .sorted((o1, o2) -> COLLATOR.compare(o1.getIssue(), o2.getIssue())) .forEach(taskWithWorklogs -> { // this task with worklogs contains at least one workitem // having the group by criteria DisplayRow ticketRowWithinThisGroup = ticketIdToDisplayRow.get(taskWithWorklogs.getIssue()); if (ticketRowWithinThisGroup == null) { ticketRowWithinThisGroup = new DisplayRow(); ticketRowWithinThisGroup.setLabel(taskWithWorklogs.getSummary()); ticketRowWithinThisGroup.setIssueId(taskWithWorklogs.getIssue()); ticketRowWithinThisGroup.setResolvedDate(taskWithWorklogs.getResolved()); groupRow.getChildren().add(new TreeItem<>(ticketRowWithinThisGroup)); ticketIdToDisplayRow.put(taskWithWorklogs.getIssue(), ticketRowWithinThisGroup); } DisplayRow ticketRowWithinThisGroupAsFinal = ticketRowWithinThisGroup; taskWithWorklogs.getWorklogItemList().stream() .filter(worklogItem -> StringUtils.equals(worklogItem.getGroup(), groupByCriteria)) .sorted((o1, o2) -> o1.getDate().compareTo(o2.getDate())) .forEach(worklogItem -> { // this worklog item matches the critera // add workday entry to current row LocalDate date = worklogItem.getDate(); DisplayDayEntry workdayEntry = ticketRowWithinThisGroupAsFinal.getWorkdayEntry(date) .orElseGet(() -> { DisplayDayEntry displayDayEntry = new DisplayDayEntry(); displayDayEntry.setDate(date); ticketRowWithinThisGroupAsFinal.addDisplayDayEntry(displayDayEntry); return displayDayEntry; }); workdayEntry.getSpentTime().addAndGet(worklogItem.getDurationInMinutes()); // also add up the spent time in the group header per group workdayEntry = groupCaptionRow.getWorkdayEntry(date) .orElseGet(() -> { DisplayDayEntry newWorkdayEntry = new DisplayDayEntry(); newWorkdayEntry.setDate(date); groupCaptionRow.addDisplayDayEntry(newWorkdayEntry); return newWorkdayEntry; }); workdayEntry.getSpentTime().addAndGet(worklogItem.getDurationInMinutes()); }); }); // add groupRow to result displayData.addRow(groupRow); }); } private void processWithoutGrouping(List<TaskWithWorklogs> tasks, DisplayData displayData) { LOGGER.debug("Processing without grouping"); tasks.stream() .sorted((o1, o2) -> COLLATOR.compare(o1.getIssue(), o2.getIssue())) .forEach(taskWithWorklogs -> { DisplayRow row = new DisplayRow(); row.setIssueId(taskWithWorklogs.getIssue()); row.setLabel(taskWithWorklogs.getSummary()); row.setResolvedDate(taskWithWorklogs.getResolved()); taskWithWorklogs.getWorklogItemList().forEach(worklogItem -> { LocalDate date = worklogItem.getDate(); DisplayDayEntry workdayEntry = row.getWorkdayEntry(date) .orElseGet(() -> { DisplayDayEntry newWorkdayEntry = new DisplayDayEntry(); newWorkdayEntry.setDate(date); row.addDisplayDayEntry(newWorkdayEntry); return newWorkdayEntry; }); workdayEntry.getSpentTime().addAndGet(worklogItem.getDurationInMinutes()); }); displayData.addRow(new TreeItem<>(row)); }); } private void updateStatisticsData(List<TaskWithWorklogs> displayResult) { if (!SettingsUtil.loadSettings().isShowStatistics()) { return; } statisticsView.getChildren().clear(); WorklogStatistics statistics = new WorklogStatistics(); // generic statistics displayResult.forEach(taskWithWorklogs -> { statistics.getTotalTimeSpent().addAndGet(taskWithWorklogs.getTotalInMinutes()); for (WorklogItem worklogItem : taskWithWorklogs.getWorklogItemList()) { String employee = worklogItem.getUserDisplayname(); // employee total time spent AtomicLong totalTimeSpent = statistics.getEmployeeToTotaltimeSpent().get(employee); if (totalTimeSpent == null) { totalTimeSpent = new AtomicLong(0); statistics.getEmployeeToTotaltimeSpent().put(employee, totalTimeSpent); } totalTimeSpent.addAndGet(worklogItem.getDurationInMinutes()); // distinct tasks per employee Set<String> totalDistinctTasks = statistics.getEmployeeToTotalDistinctTasks().get(employee); if (totalDistinctTasks == null) { totalDistinctTasks = new HashSet<>(); statistics.getEmployeeToTotalDistinctTasks().put(employee, totalDistinctTasks); } totalDistinctTasks.add(taskWithWorklogs.getIssue()); // distinct tasks per employee per project Map<String, Set<String>> projectToDistinctTasks = statistics.getEmployeeToProjectToDistinctTasks().get(employee); if (projectToDistinctTasks == null) { projectToDistinctTasks = new HashMap<>(); statistics.getEmployeeToProjectToDistinctTasks().put(employee, projectToDistinctTasks); } Set<String> distinctTasks = projectToDistinctTasks.get(taskWithWorklogs.getProject()); if (distinctTasks == null) { distinctTasks = new HashSet<>(); projectToDistinctTasks.put(taskWithWorklogs.getProject(), distinctTasks); } distinctTasks.add(taskWithWorklogs.getIssue()); // time spent per project Map<String, AtomicLong> projectToTimespent = statistics.getEmployeeToProjectToWorktime().get(employee); if (projectToTimespent == null) { projectToTimespent = new HashMap<>(); statistics.getEmployeeToProjectToWorktime().put(employee, projectToTimespent); } AtomicLong timespentOnProject = projectToTimespent.get(taskWithWorklogs.getProject()); if (timespentOnProject == null) { timespentOnProject = new AtomicLong(0); projectToTimespent.put(taskWithWorklogs.getProject(), timespentOnProject); } timespentOnProject.addAndGet(worklogItem.getDurationInMinutes()); } }); // render grid and bar graph final AtomicInteger currentGridRow = new AtomicInteger(0); GridPane employeeProjectSummaryGrid = new GridPane(); employeeProjectSummaryGrid.setHgap(5); employeeProjectSummaryGrid.setVgap(5); NumberAxis projectEmployeeXAxis = new NumberAxis(); projectEmployeeXAxis.setLabel(FormattingUtil.getFormatted("view.statistics.timespentinhours")); projectEmployeeXAxis.setTickLabelRotation(90); NumberAxis employeeProjectXAxis = new NumberAxis(); employeeProjectXAxis.setLabel(FormattingUtil.getFormatted("view.statistics.timespentinhours")); employeeProjectXAxis.setTickLabelRotation(90); CategoryAxis projectEmployeeYAxis = new CategoryAxis(); CategoryAxis employeeProjectYAxis = new CategoryAxis(); StackedBarChart<Number, String> projectEmployeeBargraph = new StackedBarChart<>(projectEmployeeXAxis, projectEmployeeYAxis); StackedBarChart<Number, String> employeeProjectBargraph = new StackedBarChart<>(employeeProjectXAxis, employeeProjectYAxis); projectEmployeeBargraph.setTitle(FormattingUtil.getFormatted("view.statistics.byprojectandemployee")); employeeProjectBargraph.setTitle(FormattingUtil.getFormatted("view.statistics.byemployeeandproject")); Set<String> projectsToDisplay = new HashSet<>(); displayResult.forEach(taskWithWorklogs -> { projectsToDisplay.add(taskWithWorklogs.getProject()); }); int projectEmployeeBargraphPreferedHeight = HEIGHT_PER_Y_AXIS_ELEMENT * projectsToDisplay.size() + HEIGHT_PER_X_AXIS_ELEMENT * statistics.getEmployeeToTotaltimeSpent().keySet().size() + ADDITIONAL_HEIGHT; projectEmployeeBargraph.setPrefHeight(projectEmployeeBargraphPreferedHeight); VBox.setVgrow(projectEmployeeBargraph, Priority.ALWAYS); int employeeProjectBargraphPreferedHeight = HEIGHT_PER_Y_AXIS_ELEMENT * statistics.getEmployeeToProjectToWorktime().keySet().size() + HEIGHT_PER_X_AXIS_ELEMENT * projectsToDisplay.size() + ADDITIONAL_HEIGHT; employeeProjectBargraph.setPrefHeight(employeeProjectBargraphPreferedHeight); VBox.setVgrow(employeeProjectBargraph, Priority.ALWAYS); Map<String, XYChart.Series<Number, String>> projectNameToSeries = Maps.newHashMap(); statistics.getEmployeeToProjectToWorktime().keySet().stream() .sorted(COLLATOR::compare) .forEach(employee -> { // employee headline label Set<String> totalDistinctTasksOfEmployee = statistics.getEmployeeToTotalDistinctTasks().get(employee); Label employeeLabel = getBoldLabel(FormattingUtil.getFormatted("view.statistics.somethingtoamountoftickets", employee, totalDistinctTasksOfEmployee.size())); employeeLabel.setPadding(new Insets(20, 0, 0, 0)); GridPane.setConstraints(employeeLabel, 0, currentGridRow.getAndIncrement()); GridPane.setColumnSpan(employeeLabel, 4); employeeProjectSummaryGrid.getChildren().addAll(employeeLabel); // bar graph data container XYChart.Series<Number, String> projectEmployeeSeries = new XYChart.Series<>(); projectEmployeeSeries.setName(employee); projectEmployeeBargraph.getData().add(projectEmployeeSeries); // time spent per project Map<String, AtomicLong> projectToWorktime = statistics.getEmployeeToProjectToWorktime().get(employee); Map<String, Label> projectToPercentageLabel = Maps.newHashMap(); projectToWorktime.keySet().stream() .sorted(COLLATOR::compare) .forEach(projectName -> { XYChart.Series<Number, String> employeeProjectSeries = projectNameToSeries.get(projectName); if (employeeProjectSeries == null) { employeeProjectSeries = new XYChart.Series<>(); employeeProjectSeries.setName(projectName); employeeProjectBargraph.getData().add(employeeProjectSeries); projectNameToSeries.put(projectName, employeeProjectSeries); } // percentage label Label percentageLabel = getBoldLabel("PLACEHOLDER"); percentageLabel.setAlignment(Pos.CENTER_RIGHT); percentageLabel.setPadding(new Insets(0, 0, 0, 20)); GridPane.setConstraints(percentageLabel, 1, currentGridRow.get()); GridPane.setHalignment(percentageLabel, HPos.RIGHT); projectToPercentageLabel.put(projectName, percentageLabel); // project label Set<String> distinctTasksPerProject = statistics.getEmployeeToProjectToDistinctTasks().get(employee).get(projectName); Label projectLabel = getBoldLabel(FormattingUtil.getFormatted("view.statistics.somethingtoamountoftickets", projectName, distinctTasksPerProject.size())); GridPane.setConstraints(projectLabel, 2, currentGridRow.get()); // time spent for project label long timespentInMinutes = projectToWorktime.get(projectName).longValue(); Label timespentLabel = new Label(FormattingUtil.formatMinutes(timespentInMinutes, true)); GridPane.setConstraints(timespentLabel, 3, currentGridRow.get()); GridPane.setHgrow(timespentLabel, Priority.ALWAYS); GridPane.setHalignment(timespentLabel, HPos.RIGHT); employeeProjectSummaryGrid.getChildren().addAll(percentageLabel, projectLabel, timespentLabel); currentGridRow.incrementAndGet(); // bargraph data projectEmployeeSeries.getData().add(new XYChart.Data<>(timespentInMinutes / 60d, projectName)); employeeProjectSeries.getData().addAll(new XYChart.Data<>(timespentInMinutes / 60d, employee)); }); // total time spent Label totalLabel = getBoldLabel(FormattingUtil.getFormatted("view.statistics.totaltimespent")); GridPane.setConstraints(totalLabel, 0, currentGridRow.get()); GridPane.setColumnSpan(totalLabel, 4); Label timespentLabel = new Label(FormattingUtil.formatMinutes(statistics.getEmployeeToTotaltimeSpent().get(employee).get(), true)); GridPane.setConstraints(timespentLabel, 3, currentGridRow.get()); GridPane.setHgrow(timespentLabel, Priority.ALWAYS); GridPane.setHalignment(timespentLabel, HPos.RIGHT); employeeProjectSummaryGrid.getChildren().addAll(totalLabel, timespentLabel); // set label now that we can calculate the percentage projectToWorktime.keySet().forEach(projectName -> { Label percentageLabel = projectToPercentageLabel.get(projectName); double totalSpentTime = statistics.getEmployeeToTotaltimeSpent().get(employee).doubleValue(); double spentTimeOnProject = projectToWorktime.get(projectName).doubleValue(); double percentage = spentTimeOnProject / totalSpentTime; String percentageFormatted = FormattingUtil.formatPercentage(percentage); percentageLabel.setText(percentageFormatted); }); currentGridRow.incrementAndGet(); }); // employeeProjectBargraph statisticsView.getChildren().addAll(employeeProjectSummaryGrid, projectEmployeeBargraph, employeeProjectBargraph); // custom view statistics addAdditionalStatistics(statisticsView, statistics, displayResult); } protected void addAdditionalStatistics(VBox statisticsView, WorklogStatistics statistics, List<TaskWithWorklogs> displayResult) { // for you to override } protected Label getBoldLabel(String text) { Label label = new Label(text); label.setFont(Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize())); return label; } public String getExcelDownloadSuggestedFilename() { if (!lastUsedTimerangeProvider.isPresent()) throw ExceptionUtil.getIllegalStateException("exceptions.excel.nodata"); TimerangeProvider timerangeProvider = lastUsedTimerangeProvider.get(); return new StringBuilder(getText()) .append('_') .append(FormattingUtil.formatDate(timerangeProvider.getStartDate())) .append('-') .append(FormattingUtil.formatDate(timerangeProvider.getEndDate())) .append(".xls") .toString(); } public void writeDataToExcel(Sheet sheet) { LOGGER.debug("[{}] Exporting data to excel", getText()); List<ExcelColumnRenderer> columnRendererList = new ArrayList<>(); columnRendererList.add(new TaskStatusExcelColumn()); columnRendererList.add(new TaskDescriptionExcelColumn()); TimerangeProvider timerangeProvider = fetchTimereportContext.get().getTimerangeProvider(); LocalDate startDate = timerangeProvider.getStartDate(); LocalDate endDate = timerangeProvider.getEndDate(); long amountOfDaysToDisplay = ChronoUnit.DAYS.between(startDate, endDate); for (int days = 0; days <= amountOfDaysToDisplay; days++) { LocalDate currentColumnDate = timerangeProvider.getStartDate().plus(days, ChronoUnit.DAYS); String displayDate = FormattingUtil.formatDate(currentColumnDate); columnRendererList.add(new WorklogExcelColumn(displayDate, currentColumnDate)); } columnRendererList.add(new TaskWorklogSummaryExcelColumn()); TreeItem<DisplayRow> root = taskTableView.getRoot(); ObservableList<TreeItem<DisplayRow>> children = root.getChildren(); for (int columnIndex = 0; columnIndex < columnRendererList.size(); columnIndex++) { ExcelColumnRenderer excelColumnRenderer = columnRendererList.get(columnIndex); excelColumnRenderer.renderCells(columnIndex, sheet, children, fetchTimereportContext.get().getGroupByCategory().isPresent()); } // autosize column widths for (int i = 0; i < columnRendererList.size(); i++) { sheet.autoSizeColumn(i); } } protected class WorklogStatistics { private Map<String, Map<String, AtomicLong>> employeeToProjectToWorktime = new HashMap<>(); private Map<String, Map<String, Set<String>>> employeeToProjectToDistinctTasks = new HashMap<>(); private Map<String, Set<String>> employeeToTotalDistinctTasks = new HashMap<>(); private AtomicLong totalTimeSpent = new AtomicLong(0); private Map<String, AtomicLong> employeeToTotaltimeSpent = new HashMap<>(); public Map<String, Map<String, AtomicLong>> getEmployeeToProjectToWorktime() { return employeeToProjectToWorktime; } public Map<String, Map<String, Set<String>>> getEmployeeToProjectToDistinctTasks() { return employeeToProjectToDistinctTasks; } public Map<String, Set<String>> getEmployeeToTotalDistinctTasks() { return employeeToTotalDistinctTasks; } public AtomicLong getTotalTimeSpent() { return totalTimeSpent; } public Map<String, AtomicLong> getEmployeeToTotaltimeSpent() { return employeeToTotaltimeSpent; } } }