Add initial line chart to show story point changes over time

Needs to be updated to consolidate by date and show the last point total only.
This commit is contained in:
James Skemp 2024-04-26 16:25:57 -05:00
parent bd34d3bd96
commit a87c8bae86
5 changed files with 146 additions and 70 deletions

View File

@ -2,7 +2,7 @@
"manifestVersion": 1,
"id": "enhanced-sprint-history",
"publisher": "JamesSkemp",
"version": "0.0.144",
"version": "0.0.151",
"name": "Enhanced Sprint History",
"description": "Azure DevOps Extension",
"categories": [

27
package-lock.json generated
View File

@ -12,7 +12,9 @@
"azure-devops-extension-api": "^2.226.0",
"azure-devops-extension-sdk": "^3.1.3",
"azure-devops-ui": "^2.236.0",
"chart.js": "^4.4.2",
"react": "~16.13.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "~16.13.1"
},
"devDependencies": {
@ -109,6 +111,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1033,6 +1040,17 @@
"node": ">=4"
}
},
"node_modules/chart.js": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
"integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -3935,6 +3953,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
"integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",

View File

@ -26,7 +26,9 @@
"azure-devops-extension-api": "^2.226.0",
"azure-devops-extension-sdk": "^3.1.3",
"azure-devops-ui": "^2.236.0",
"chart.js": "^4.4.2",
"react": "~16.13.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "~16.13.1"
},
"devDependencies": {

View File

@ -33,3 +33,9 @@ td {
margin-bottom: 1em;
overflow: visible;
}
.iteration-history-display {
.bolt-card-content {
display: grid;
}
}

View File

@ -5,6 +5,10 @@ import { TeamSettingsIteration } from "azure-devops-extension-api/Work";
import { IHubWorkItemHistory, IHubWorkItemIterationRevisions, ITypedWorkItem, ITypedWorkItemWithRevision } from "./HubInterfaces";
import { getFlattenedRelevantRevisions, getIterationRelevantWorkItems, getTypedWorkItem } from "./HubUtils";
import { Card } from "azure-devops-ui/Card";
import { CategoryScale, Chart as ChartJs, LineElement, LinearScale, PointElement, Tooltip } from "chart.js";
import { Line } from "react-chartjs-2";
ChartJs.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip);
export interface IterationHistoryDisplayProps {
iteration: TeamSettingsIteration | undefined;
@ -108,10 +112,103 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
let totalStoryPoints = 0;
let changedWorkItems = getChangedWorkItems(getFlattenedRelevantRevisions(iterationWorkItemRevisions));
const storyPointChanges = changedWorkItems.map((wi, i, a) => {
const workItemChange = getWorkItemChange(wi, i, a);
const storyClosed = isWorkItemClosed(wi);
const storyPointsChanged = workItemChange.change.indexOf('Story Points Changed') >= 0;
let addedStoryPoints = 0;
let subtractedStoryPoints = 0;
let showAddedPoints = false;
let showSubtractedPoints = false;
if (workItemChange.change.indexOf('Removed') >= 0) {
subtractedStoryPoints = wi.storyPoints;
showSubtractedPoints = true;
}
if (workItemChange.change.indexOf('Added') >= 0) {
addedStoryPoints = wi.storyPoints;
showAddedPoints = true;
}
if (workItemChange.change.indexOf('Reopened') >= 0) {
addedStoryPoints = wi.storyPoints;
showAddedPoints = true;
}
if (storyClosed) {
subtractedStoryPoints = wi.storyPoints;
showSubtractedPoints = true;
}
if (storyPointsChanged) {
if (!showAddedPoints && !showSubtractedPoints) {
addedStoryPoints = wi.storyPoints;
subtractedStoryPoints = workItemChange.lastRevision?.storyPoints ?? 0;
showAddedPoints = true;
showSubtractedPoints = true;
} else if (storyClosed) {
addedStoryPoints = wi.storyPoints;
subtractedStoryPoints += workItemChange.lastRevision?.storyPoints ?? 0;
showAddedPoints = true;
showSubtractedPoints = true;
} else {
// TODO potentially?
/*console.groupCollapsed(wi.id);
console.table(wi);
console.log(workItemChange);
console.groupEnd();*/
}
}
let changeCharacterCode = 160;
if (addedStoryPoints > subtractedStoryPoints) {
changeCharacterCode = 8593; //8599;
} else if (addedStoryPoints < subtractedStoryPoints) {
changeCharacterCode = 8595; //8600
}
let updatedTotalStoryPoints = addedStoryPoints - subtractedStoryPoints;
totalStoryPoints += updatedTotalStoryPoints;
const totalStoryPointsClass = 'story-points total' + (updatedTotalStoryPoints > 0 ? ' increase' : updatedTotalStoryPoints < 0 ? ' decrease' : '');
return {
changedDateFull: wi.changedDateFull,
url: wi.url,
title: wi.title,
id: wi.id,
workItemChange: workItemChange,
addedStoryPoints: addedStoryPoints,
showAddedPoints: showAddedPoints,
subtractedStoryPoints: subtractedStoryPoints,
showSubtractedPoints: showSubtractedPoints,
totalStoryPointsClass: totalStoryPointsClass,
totalStoryPoints: totalStoryPoints,
changeCharacterCode: changeCharacterCode
};
})
const chartOptions = {
responsive: true
};
const chartData = {
labels: storyPointChanges.map(cwi => cwi.changedDateFull.toLocaleString()),
datasets: [
{
label: 'Story Points',
data: storyPointChanges.map(cwi => cwi.totalStoryPoints),
borderColor: 'rgb(53, 162, 235)',
//backgroundColor: 'rgba(53, 162, 235, 0.5)'
}
]
};
return (
<Card className="iteration-history-display"
titleProps={{ text: "Iteration User Story History", ariaLevel: 3 }}>
<table>
<div className="display-child">
<Line options={chartOptions} data={chartData} />
</div>
<table className="display-child">
<thead>
<tr>
<th className="date">Date</th>
@ -123,74 +220,18 @@ export class IterationHistoryDisplay extends React.Component<IterationHistoryDis
</tr>
</thead>
<tbody>
{
getChangedWorkItems(getFlattenedRelevantRevisions(iterationWorkItemRevisions)).map((wi, i, a) => {
const workItemChange = getWorkItemChange(wi, i, a);
const storyClosed = isWorkItemClosed(wi);
const storyPointsChanged = workItemChange.change.indexOf('Story Points Changed') >= 0;
let addedStoryPoints = 0;
let subtractedStoryPoints = 0;
let showAddedPoints = false;
let showSubtractedPoints = false;
if (workItemChange.change.indexOf('Removed') >= 0) {
subtractedStoryPoints = wi.storyPoints;
showSubtractedPoints = true;
}
if (workItemChange.change.indexOf('Added') >= 0) {
addedStoryPoints = wi.storyPoints;
showAddedPoints = true;
}
if (workItemChange.change.indexOf('Reopened') >= 0) {
addedStoryPoints = wi.storyPoints;
showAddedPoints = true;
}
if (storyClosed) {
subtractedStoryPoints = wi.storyPoints;
showSubtractedPoints = true;
}
if (storyPointsChanged) {
if (!showAddedPoints && !showSubtractedPoints) {
addedStoryPoints = wi.storyPoints;
subtractedStoryPoints = workItemChange.lastRevision?.storyPoints ?? 0;
showAddedPoints = true;
showSubtractedPoints = true;
} else if (storyClosed) {
addedStoryPoints = wi.storyPoints;
subtractedStoryPoints += workItemChange.lastRevision?.storyPoints ?? 0;
showAddedPoints = true;
showSubtractedPoints = true;
} else {
// TODO potentially?
/*console.groupCollapsed(wi.id);
console.table(wi);
console.log(workItemChange);
console.groupEnd();*/
}
}
let changeCharacterCode = 160;
if (addedStoryPoints > subtractedStoryPoints) {
changeCharacterCode = 8593; //8599;
} else if (addedStoryPoints < subtractedStoryPoints) {
changeCharacterCode = 8595; //8600
}
let updatedTotalStoryPoints = addedStoryPoints - subtractedStoryPoints;
totalStoryPoints += updatedTotalStoryPoints;
const totalStoryPointsClass = 'story-points total' + (updatedTotalStoryPoints > 0 ? ' increase' : updatedTotalStoryPoints < 0 ? ' decrease' : '');
return (
<tr>
<td>{wi.changedDateFull.toLocaleString()}</td>
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a><br />{wi.title}</td>
<td>{workItemChange.change.join(', ')}</td>
<td className="story-points increase">{addedStoryPoints !== 0 || showAddedPoints ? '+' + addedStoryPoints : ''}</td>
<td className="story-points decrease">{subtractedStoryPoints !== 0 || showSubtractedPoints ? '-' + subtractedStoryPoints : ''}</td>
<td className={totalStoryPointsClass}>{totalStoryPoints} {String.fromCharCode(changeCharacterCode)}</td>
</tr>
);
})
{storyPointChanges.map((wi, i, a) => {
return (
<tr>
<td>{wi.changedDateFull.toLocaleString()}</td>
<td><a href={wi.url} target="_blank" title={wi.title}>{wi.id}</a><br />{wi.title}</td>
<td>{wi.workItemChange.change.join(', ')}</td>
<td className="story-points increase">{wi.addedStoryPoints !== 0 || wi.showAddedPoints ? '+' + wi.addedStoryPoints : ''}</td>
<td className="story-points decrease">{wi.subtractedStoryPoints !== 0 || wi.showSubtractedPoints ? '-' + wi.subtractedStoryPoints : ''}</td>
<td className={wi.totalStoryPointsClass}>{wi.totalStoryPoints} {String.fromCharCode(wi.changeCharacterCode)}</td>
</tr>
);
})
}
</tbody>
</table>