Skip to content

Commit aa384d5

Browse files
authored
Agile development burndown chart (#1484)
* Create script.py This Python code snippet creates a current week burndown chart from rm_story in a very nice format. * Create README.md Create README.md * Create requirements.txt add equirements.txt * Update README.md Improved the README to be more detailed and easier to understand. * Update script.py Minor tweaks to the chart appearance * Update requirements.txt Fix library name * Fixing duplicate imports of datetime Fixing duplicate imports of datetime * Update requirements.txt Modify requirements.txt to match what was output by pip freeze * Update script.py Translate Japanese comments to English * Update script.py Reflecting the review of Pull Request 1484 * Rename script.py to sprint_burndown_chart.py Change FileName * Update README.md Reflecting changes to Python executable file name * Update README.md Modified the installation method to make it easier to understand. * Update README.md * Update README.md README improvements * Update README.md Fixed README * Update requirements.txt Remove unnecessary libraries * Update README.md The Requirements have been refined. * Update sprint_burndown_chart.py Removed the condition to check Japan's unique holidays
1 parent 8d14e10 commit aa384d5

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## Description
2+
The planned lines of the ServiceNow burndown chart do not take holidays into account.
3+
By using this Python snippet, you can create a burndown chart with planned lines that take holidays into account.
4+
The generated burndown chart can also be automatically deployed as an image to Slack and other tools.
5+
6+
## Requirements
7+
OS: Windows/MacOS/Unix
8+
Python: Python3.x
9+
ServiceNow: More than Vancouver
10+
Plugins: Agile Development 2.0 (com.snc.sdlc.agile.2.0) is installed
11+
12+
## Installation
13+
Clone the repository and place the "Burndown Chart" directory in any location.
14+
Execute the following command to create a virtual environment.
15+
<code>python3 -m venv .venv
16+
macOS/Unix: source .venv/bin/activate
17+
Windows: .venv\Scripts\activate
18+
pip install -r requirements.txt
19+
</code>
20+
21+
## Usage
22+
1. Go to the Burndown Chart directory.
23+
2. Prepare the following values ​​according to your environment:
24+
- InstanceName: Your instance name (e.g. dev000000 for PDI)
25+
- Credentials: Instance login information in Base64 (Read permission to the rm_sprint table is required.)
26+
- Sprint Name: Target sprint name from the Sprint[rm_sprint] table
27+
28+
3. Run the command to install the required modules.
29+
<code>pip install -r equirements.txt</code>
30+
31+
5. Run sprint_burndown_chart.py.
32+
<code>python3 sprint_burndown_chart.py INSTANCE_NAME BASE64_ENCODED_STRING(USERID:PASSWORD) SPRINT_NAME</code>
33+
example:
34+
<code>python3 sprint_burndown_chart.py dev209156 YXBpOmpkc0RhajNAZDdKXnNmYQ== Sprint1</code>
35+
36+
When you run it, a burndown chart image like the one shown will be created.
37+
![figure](https://github.com/user-attachments/assets/50d3ffc2-4c66-4f4d-bb69-c2b98763621d)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
jpholiday==0.1.10
2+
matplotlib==3.9.2
3+
numpy==2.0.2
4+
pandas==2.2.3
5+
pytz==2024.2
6+
requests==2.32.3
7+
urllib3==1.26.13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import argparse
2+
import pprint
3+
import requests
4+
import datetime
5+
import matplotlib.dates as mdates
6+
import pandas as pd
7+
import matplotlib.pyplot as plt
8+
import urllib.request
9+
import urllib.parse
10+
import json
11+
import datetime
12+
from pytz import timezone
13+
14+
# ---- #
15+
# init #
16+
# ---- #
17+
point_dict = {}
18+
total_points = 0
19+
done = 0
20+
undone = 0
21+
parser = argparse.ArgumentParser()
22+
parser.add_argument('instancename')
23+
parser.add_argument('authstring')
24+
parser.add_argument('sprintname')
25+
args = parser.parse_args()
26+
BASIC = 'Basic ' + args.authstring
27+
28+
# ---------- #
29+
# Get Sprint #
30+
# ---------- #
31+
params = {
32+
'sysparm_query': 'short_description=' + args.sprintname
33+
}
34+
param = urllib.parse.urlencode(params)
35+
url = "https://" + args.instancename + ".service-now.com/api/now/table/rm_sprint?" + param
36+
req = urllib.request.Request(url)
37+
req.add_header("authorization", BASIC)
38+
with urllib.request.urlopen(req) as res:
39+
r = res.read().decode("utf-8")
40+
obj = json.loads(r)
41+
# Get the start and end dates of a Sprint
42+
start_date = obj['result'][0]['start_date']
43+
start_date = (datetime.datetime.strptime(start_date, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(hours=9)).date()
44+
print(start_date)
45+
end_date = obj['result'][0]['end_date']
46+
end_date = (datetime.datetime.strptime(end_date, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(hours=9)).date()
47+
# Initializing the points array
48+
while start_date <= end_date:
49+
point_dict[str(start_date)] = 0
50+
start_date = start_date + datetime.timedelta(days=1)
51+
# --------- #
52+
# Get Story #
53+
# --------- #
54+
params = {
55+
'sysparm_query': 'sprint.short_descriptionLIKE' + args.sprintname
56+
}
57+
param = urllib.parse.urlencode(params)
58+
url = "https://" + args.instancename + ".service-now.com/api/now/table/rm_story?" + param
59+
req = urllib.request.Request(url)
60+
req.add_header("authorization", BASIC)
61+
with urllib.request.urlopen(req) as res:
62+
r = res.read().decode("utf-8")
63+
obj = json.loads(r)
64+
# Story Loop
65+
for name in obj['result']:
66+
if len(name['story_points']) > 0:
67+
total_points += int(name['story_points'])
68+
if name['closed_at'] != '':
69+
close_date = datetime.datetime.strptime(
70+
name['closed_at'], '%Y-%m-%d %H:%M:%S')
71+
close_date = close_date.date()
72+
if name['state'] == '3':
73+
if str(close_date) in point_dict:
74+
point_dict[str(close_date)] += int(name['story_points'])
75+
else:
76+
point_dict[str(close_date)] = int(name['story_points'])
77+
if name['state'] == '3':
78+
done += int(name['story_points'])
79+
else:
80+
undone += int(name['story_points'])
81+
counta = 0
82+
for i in point_dict.items():
83+
counta += int(i[1])
84+
point_dict[i[0]] = total_points - counta
85+
plt.xkcd()
86+
fig, ax = plt.subplots()
87+
# Creating a performance line
88+
x = []
89+
y = []
90+
plt.ylim(0, total_points + 5)
91+
counta = 0
92+
for key in point_dict.keys():
93+
if datetime.datetime.today() >= datetime.datetime.strptime(key, '%Y-%m-%d'):
94+
x.append(datetime.datetime.strptime(key, '%Y-%m-%d'))
95+
y.append(point_dict[key])
96+
# Holiday determination
97+
DATE = "yyyymmdd"
98+
def isBizDay(DATE):
99+
Date = datetime.date(int(DATE[0:4]), int(DATE[4:6]), int(DATE[6:8]))
100+
if Date.weekday() >= 5:
101+
return 0
102+
else:
103+
return 1
104+
# Get the number of weekdays
105+
total_BizDay = 0
106+
for key in point_dict.keys():
107+
if isBizDay(key.replace('-', '')) == 1:
108+
total_BizDay += 1
109+
# Creating an ideal line
110+
x2 = []
111+
y2 = []
112+
point_dict_len = len(point_dict)
113+
average = total_points / (total_BizDay - 1)
114+
for key in point_dict.keys():
115+
dtm = datetime.datetime.strptime(key, '%Y-%m-%d')
116+
x2.append(dtm)
117+
y2.append(total_points)
118+
# If the next day is a weekday, consume the ideal line.
119+
if isBizDay((dtm + datetime.timedelta(days=1)).strftime("%Y%m%d")) == 1:
120+
total_points -= average
121+
days = mdates.DayLocator()
122+
daysFmt = mdates.DateFormatter('%m/%d')
123+
ax.xaxis.set_major_locator(days)
124+
ax.xaxis.set_major_formatter(daysFmt)
125+
plt.title("" + args.sprintname + " Burndown")
126+
plt.plot(x2, y2, label="Ideal", color='green')
127+
plt.plot(x2, y2, marker='.', markersize=20, color='green')
128+
plt.plot(x, y, label="Actual", color='red')
129+
plt.plot(x, y, marker='.', markersize=20, color='red')
130+
plt.grid()
131+
plt.xlabel("Days")
132+
plt.ylabel("Remaining Effort(pts)")
133+
plt.subplots_adjust(bottom=0.2)
134+
plt.legend()
135+
# Viewing the graph
136+
# plt.show()
137+
# Saving a graph
138+
plt.savefig('figure.png')

0 commit comments

Comments
 (0)