From fc5034eff4cb6d08f9c7a39d88e0536c0d7ded4d Mon Sep 17 00:00:00 2001 From: Francois Gerthoffert Date: Thu, 17 Oct 2019 17:52:51 -0400 Subject: [PATCH] Added a breakdown by ticket type and project to the completed heatmap --- cli/src/utils/data/fetchCompleted.ts | 2 +- ui/package.json | 5 + .../Charts/Nivo/RoadmapCompletionChart.tsx | 5 +- .../components/Charts/Nivo/RoadmapTooltip.tsx | 55 -------- .../Nivo/RoadmapTooltip/DataBreakdown.tsx | 78 +++++++++++ .../Charts/Nivo/RoadmapTooltip/index.tsx | 123 ++++++++++++++++++ 6 files changed, 210 insertions(+), 58 deletions(-) delete mode 100644 ui/src/components/Charts/Nivo/RoadmapTooltip.tsx create mode 100644 ui/src/components/Charts/Nivo/RoadmapTooltip/DataBreakdown.tsx create mode 100644 ui/src/components/Charts/Nivo/RoadmapTooltip/index.tsx diff --git a/cli/src/utils/data/fetchCompleted.ts b/cli/src/utils/data/fetchCompleted.ts index e9d84ac..f6473cb 100644 --- a/cli/src/utils/data/fetchCompleted.ts +++ b/cli/src/utils/data/fetchCompleted.ts @@ -39,7 +39,7 @@ const fetchCompleted = async ( const issuesJira = await jiraSearchIssues( config.jira, jqlQuery, - 'labels,summary,' + + 'labels,summary,issuetype,assignee,' + config.jira.fields.points + ',' + config.jira.fields.originalPoints diff --git a/ui/package.json b/ui/package.json index 9b4ef17..8555ccf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,9 @@ "@types/colornames": "^1.1.1", "@types/cytoscape": "^3.8.2", "@types/jest": "24.0.18", + "@types/lodash": "^4.14.144", "@types/node": "12.7.2", + "@types/randomcolor": "^0.5.3", "@types/react": "16.9.2", "@types/react-dom": "16.9.0", "@types/react-redux": "^7.1.2", @@ -23,6 +25,7 @@ "@types/socket.io-client": "^1.4.32", "axios": "^0.19.0", "chart.js": "^2.8.0", + "chartjs-plugin-datalabels": "^0.7.0", "colornames": "^1.1.1", "cytoscape": "^3.10.0", "cytoscape-cose-bilkent": "^4.1.0", @@ -30,10 +33,12 @@ "date-fns": "^2.2.1", "font-color-contrast": "^1.0.3", "install": "^0.13.0", + "lodash": "^4.17.15", "loglevel": "^1.6.3", "material-color-hash": "^0.1.6", "material-table": "^1.50.0", "npm": "^6.11.3", + "randomcolor": "^0.5.4", "react": "^16.9.0", "react-bootstrap": "^1.0.0-beta.12", "react-cytoscapejs": "^1.2.0", diff --git a/ui/src/components/Charts/Nivo/RoadmapCompletionChart.tsx b/ui/src/components/Charts/Nivo/RoadmapCompletionChart.tsx index 215f815..438fc9d 100644 --- a/ui/src/components/Charts/Nivo/RoadmapCompletionChart.tsx +++ b/ui/src/components/Charts/Nivo/RoadmapCompletionChart.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; // let's also import Component import { Theme, createStyles, withStyles } from '@material-ui/core/styles'; import { ResponsiveHeatMap } from '@nivo/heatmap'; -import RoadmapTooltip from './RoadmapTooltip'; +import RoadmapTooltip from './RoadmapTooltip/index'; import { getInitiativeTitle, @@ -37,12 +37,13 @@ class RoadmapCompletionChart extends Component { dataset: any = {}; getTooltip = (data: any) => { - const { roadmap } = this.props; + const { roadmap, defaultPoints } = this.props; return ( ); }; diff --git a/ui/src/components/Charts/Nivo/RoadmapTooltip.tsx b/ui/src/components/Charts/Nivo/RoadmapTooltip.tsx deleted file mode 100644 index 988a00a..0000000 --- a/ui/src/components/Charts/Nivo/RoadmapTooltip.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { getCellDataInitiatives } from './utils'; - -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; - -export interface TooltipProps { - data: any; - roadmap: any; - completionWeeks: any; -} - -export default function RoadmapTooltip(props: TooltipProps) { - const { data, roadmap, completionWeeks } = props; - const initiatives = getCellDataInitiatives( - data.yKey, - data.xKey, - roadmap, - completionWeeks - ); - - return ( - - - - - Key - Summary - Points - - - - {initiatives.slice(0, 5).map((i: any) => ( - - - {i.key} - - {i.fields.summary} - {i.points} - - ))} - -
- {initiatives.length > 10 && ( - - Caution: The tooltip only displays the first 5 results. - - )} -
- ); -} diff --git a/ui/src/components/Charts/Nivo/RoadmapTooltip/DataBreakdown.tsx b/ui/src/components/Charts/Nivo/RoadmapTooltip/DataBreakdown.tsx new file mode 100644 index 0000000..712c528 --- /dev/null +++ b/ui/src/components/Charts/Nivo/RoadmapTooltip/DataBreakdown.tsx @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; // let's also import Component +import { Theme, createStyles, withStyles } from '@material-ui/core/styles'; +import Chart from 'chart.js'; +import randomColor from 'randomcolor'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +const styles = (theme: Theme) => + createStyles({ + root: { + // height: 200 + } + }); + +class DataBreakdown extends Component { + chartRef: any = React.createRef(); + chart: any = {}; + allowClick: boolean = true; + + componentDidMount() { + this.buildChart(); + } + + componentDidUpdate() { + this.buildChart(); + } + + resetAllowClick = () => { + this.allowClick = true; + }; + + buildChart = () => { + const { dataset, defaultPoints } = this.props; + const myChartRef = this.chartRef.current.getContext('2d'); + let metric = 'points'; + if (!defaultPoints) { + metric = 'issues'; + } + + if (this.chart.destroy !== undefined) { + this.chart.destroy(); + } + this.chart = new Chart(myChartRef, { + type: 'pie', + plugins: [ChartDataLabels], + data: { + datasets: [ + { + data: dataset.map((value: any) => value[metric].count), + backgroundColor: dataset.map((value: any) => + randomColor({ seed: value.name }) + ) + } + ], + labels: dataset.map((value: any) => value.name) + }, + options: { + responsive: true, + legend: { + position: 'left' + }, + animation: { + animateRotate: false + } + } + }); + }; + + render() { + const { classes } = this.props; + return ( +
+ +
+ ); + } +} + +export default withStyles(styles)(DataBreakdown); diff --git a/ui/src/components/Charts/Nivo/RoadmapTooltip/index.tsx b/ui/src/components/Charts/Nivo/RoadmapTooltip/index.tsx new file mode 100644 index 0000000..66ada3b --- /dev/null +++ b/ui/src/components/Charts/Nivo/RoadmapTooltip/index.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import Grid from '@material-ui/core/Grid'; +import _ from 'lodash'; + +import { getCellDataInitiatives } from '../utils'; + +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Typography from '@material-ui/core/Typography'; + +import DataBreakdown from './DataBreakdown'; + +export interface TooltipProps { + data: any; + roadmap: any; + completionWeeks: any; + defaultPoints: boolean; +} + +export default function RoadmapTooltip(props: TooltipProps) { + const { data, roadmap, completionWeeks, defaultPoints } = props; + const initiatives = getCellDataInitiatives( + data.yKey, + data.xKey, + roadmap, + completionWeeks + ); + + const typesGroup = _.groupBy(initiatives, (value: any) => { + if (value.fields.issuetype !== undefined) { + return value.fields.issuetype.name; + } else { + return null; + } + }); + + const types: any = []; + Object.keys(typesGroup).forEach((key: any) => { + types.push({ + name: key, + list: typesGroup[key], + issues: { + count: typesGroup[key].length + }, + points: { + count: typesGroup[key] + .map(issue => issue.points) + .reduce((acc, count) => acc + count, 0) + } + }); + }); + + const projectsGroup = _.groupBy(initiatives, value => + value.key.replace(/[^a-z+]+/gi, '') + ); + const projects: any = []; + Object.keys(projectsGroup).forEach((key: any) => { + projects.push({ + name: key, + list: projectsGroup[key], + issues: { + count: projectsGroup[key].length + }, + points: { + count: projectsGroup[key] + .map((issue: any) => { + return issue.points; + }) + .reduce((acc: number, count: number) => acc + count, 0) + } + }); + }); + return ( + + + + + Key + Summary + Points + + + + {initiatives.slice(0, 5).map((i: any) => ( + + + {i.key} + + {i.fields.summary} + {i.points} + + ))} + +
+ {initiatives.length > 5 && ( + + Caution:{' '} + + This table only displays the first 5 results (out of{' '} + {initiatives.length}). + + + )} + + + + Projects + + + + + + Types + + + + +
+ ); +}