Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,7 @@ CHECKSUMS
browser (6.2.0) sha256=281d5295788825c9396427c292c2d2be0a5c91875c93c390fde6e5d61a5ace2d
budgets (1.0.0)
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bundler (4.0.16) sha256=d6ca5dd440c24f9abce9844cf44cc8e18c6a553de65a47efb4544137af92c47d
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
capybara-screenshot (1.0.27) sha256=afa1896cc23df77be1774e8d3b3ce3953bf060aeaa04ff87607b5daf689174f2
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packag
import { MyPageComponent } from './features/my-page/my-page.component';
import { DashboardComponent } from './features/overview/dashboard.component';
import { BurndownChartComponent } from './features/backlogs/burndown-chart.component';
import { CreatedResolvedComponent } from './features/backlogs/created-resolved-chart.component';
import { BudgetByCostTypeComponent } from './shared/components/budget-graphs/overview/budget-by-cost-type.component';
import { ActualCostsComponent } from './shared/components/budget-graphs/overview/actual-costs.component';
import { ProjectTimelineGraphComponent } from './shared/components/project-timeline-graph/project-timeline-graph.component';
Expand Down Expand Up @@ -416,6 +417,7 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-my-page', MyPageComponent, { injector });
registerCustomElement('opce-dashboard', DashboardComponent, { injector });
registerCustomElement('opce-burndown-chart', BurndownChartComponent, { injector });
registerCustomElement('opce-created-resolved-chart', CreatedResolvedComponent, { injector });
registerCustomElement('opce-budget-by-cost-type', BudgetByCostTypeComponent, { injector });
registerCustomElement('opce-actual-costs', ActualCostsComponent, { injector });
registerCustomElement('opce-project-timeline-graph', ProjectTimelineGraphComponent, { injector });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@if (hasChartData()) {
<div class="position-relative mx-auto" style="height:60vh">
<canvas baseChart
[data]="lineChartData()"
[options]="lineChartOptions()"
[type]="'line'"></canvas>
</div>
} @else {
<op-no-results />
}

@if (isDevMode) {
<hr/>

<details>
<summary>Debug</summary>
<code>
<pre>{{maxValue() }}</pre>
<pre>{{lineChartData() | json}}</pre>
</code>
</details>
}
110 changes: 110 additions & 0 deletions frontend/src/app/features/backlogs/created-resolved-chart.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program 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; either version 2
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason ESLint is complaining here is this additional space within the address.

Suggested change
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then ESLint would have to flag every file, since there are two spaces in each file

//
// See COPYRIGHT and LICENSE files for more details.
//++

import { JsonPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, KeyValueDiffers, Signal, computed, inject, input } from '@angular/core';
import { ChartData, ChartOptions } from 'chart.js';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { NoResultsComponent } from 'core-app/shared/components/blankslate/no-results.component';
import PrimerColorsPlugin from 'core-app/shared/components/work-package-graphs/plugin.primer-colors';
import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { environment } from '../../../environments/environment';

const BURNDOWN_Y_SCALE_MIN = 10;

@Component({
selector: 'op-created-resolved-chart',
templateUrl: './created-resolved-chart.component.html',
imports: [BaseChartDirective, JsonPipe, NoResultsComponent],
providers: [provideCharts(withDefaultRegisterables(PrimerColorsPlugin))],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreatedResolvedComponent {
readonly isDevMode = !environment.production;
readonly i18n = inject(I18nService);
readonly chartData = input.required<string>();

readonly lineChartData = computed<ChartData<'line'>>(() => {
var data = JSON.parse(this.chartData()) as ChartData<'line'>;

var colors = {
border: {"Created" : "#f85461", "Resolved" : "#30a147"},
background: {"Created" : "#fda5a7", "Resolved" : "#54d961"}
}

data.datasets.forEach((dataset) => {
if(dataset.label == "Created" || dataset.label == "Resolved"){
dataset.backgroundColor = Array(dataset.data.length).fill(colors.background[dataset.label]);
dataset.borderColor = Array(dataset.data.length).fill(colors.border[dataset.label]);
dataset.borderWidth = 1;
}
})

return data;
});

readonly hasChartData = computed(() =>
this.lineChartData().datasets.some((ds) => ds.data.length > 0)
);

readonly maxValue = computed(() => {
return this.lineChartData().datasets
.flatMap((dataset) => dataset.data)
.filter((item):item is number => typeof item === 'number')
.reduce((a, b) => Math.max(a, b), 0);
});

readonly lineChartOptions:Signal<ChartOptions<'line'>> = computed<ChartOptions<'line'>>(() => ({
fill: true,
tension: 0,
pointRadius: 0,
scales: {
x: {
title: {
display: true,
}
},
y: {
title: {
display: true,
text: this.i18n.t('js.created_resolved.work_packages')
},
suggestedMin: 0,
max: this.maxValue() + BURNDOWN_Y_SCALE_MIN
}
},
plugins: {
legend: {
position: 'bottom'
},
'primer-colors': {
enabled: false
}
}
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.

OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team

This program 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; either version 2
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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

See COPYRIGHT and LICENSE files for more details.

++# %>

<%=
widget_wrapper do |container|
if created_resolved.present?
container.with_body do
helpers.angular_component_tag "opce-created-resolved-chart", "chart-data": chart_data, class: "d-block"
end
else
container.with_body do
render(Primer::Beta::Blankslate.new(spacious: true)) do |blankslate|
blankslate.with_visual_icon(icon: :graph)
blankslate.with_heading(tag: :h3).with_content(t("backlogs.created_resolved_chart.show.blankslate_title"))
blankslate.with_description_content(t("backlogs.created_resolved_chart.show.blankslate_description"))
end
end
end
end
%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program 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; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++

module Backlogs
module SprintReports
module Widgets
class CreatedResolvedChart < Grids::WidgetComponent
include Redmine::I18n

param :sprint
param :project

def title
t("backlogs.show_created_resolved_chart")
end

def chart_data
{
labels: xaxis_labels(created_resolved),
datasets: dataseries(created_resolved)
}.to_json
end

def wrapper_arguments
{ full_width: true }
end

private

def created_resolved
return nil unless sprint.date_range_set?

@created_resolved ||= CreatedResolved.new(sprint, project)
end

def xaxis_labels(created_resolved)
# 14 entries (plus the axis label) have come along as the best value for a good optical result.
# Thus it is enough space between the entries.
entries_displayed = (created_resolved.days.length / 14.0).ceil
created_resolved.days.enum_for(:each_with_index).map do |d, i|
if (i % entries_displayed) == 0
["#{format_date(d, format: I18n.t("date.formats.short"))}"]
end
end
end

def dataseries(created_resolved)
created_resolved.series.map do |s|
{
label: I18n.t("created_resolved.#{s.first}"),
data: s.last.enum_for(:each)
}
end
end
end
end
end
end
84 changes: 84 additions & 0 deletions modules/backlogs/app/models/created_resolved.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program 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; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class CreatedResolved
def initialize(sprint, project, _burn_direction = nil)
@sprint_id = sprint.id

make_date_series sprint

series_data = OpenProject::Backlogs::CreatedResolved::SeriesRawData.new(project,
sprint,
workpackages: ["wp_created", "wp_resolved"])


series_data.collect_data
calculate_series series_data

determine_max
end

attr_reader :days, :sprint_id, :max

def series(_select = :active)
@available_series
end

private

def make_date_series(sprint)
@days = if sprint.date_range_set?
Day.working.from_range(from: sprint.start_date, to: sprint.finish_date).map(&:date)
else
[]
end
end

def calculate_series(series_data)
series_data.collect_names.each do |c|
data = series_data[c].to_a.sort_by(&:first).map(&:last)
make_series(c.to_sym, series_data.unit_for(c), data)
end
end

def make_series(name, units, data)
@available_series ||= {}
s = OpenProject::Backlogs::CreatedResolved::Series.new(name, units, data)
@available_series[name] = s.data
instance_variable_set(:"@#{name}", s.data)
end

def determine_max
@max = {
wp_created: @available_series[:wp_created].flatten.compact.reject(&:nan?).max || 0.0,
wp_resolved: @available_series[:wp_resolved].flatten.compact.reject(&:nan?).max || 0.0
}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(Grids::WidgetGridComponent.new) do |grid| %>
<% grid.with_widget(Backlogs::SprintReports::Widgets::Goal, @sprint, @project) %>
<% grid.with_widget(Backlogs::SprintReports::Widgets::BurndownChart, @sprint, @project) %>
<% grid.with_widget(Backlogs::SprintReports::Widgets::CreatedResolvedChart, @sprint, @project) %>
<% end %>
Loading
Loading