From 882d43dc078ef59a6542915c9dda90d05d331396 Mon Sep 17 00:00:00 2001 From: Francois Gerthoffert Date: Mon, 2 May 2022 16:46:20 -0400 Subject: [PATCH] Added temporary visualization based on table --- ui/src/views/teams/forecast/RoadmapChart.tsx | 730 +++++++++++-------- ui/src/views/teams/forecast/index.tsx | 4 +- 2 files changed, 415 insertions(+), 319 deletions(-) diff --git a/ui/src/views/teams/forecast/RoadmapChart.tsx b/ui/src/views/teams/forecast/RoadmapChart.tsx index d6b7a0d..f5d94c8 100644 --- a/ui/src/views/teams/forecast/RoadmapChart.tsx +++ b/ui/src/views/teams/forecast/RoadmapChart.tsx @@ -1,339 +1,433 @@ import React, { FC } from 'react'; +// //https://github.com/plouc/nivo/issues/1967 +// const RoadmapChart: FC = ({ streams, metric }) => { +// const streamWithWeeks: Array = addWeeksToStreams(streams, metric); +// console.log(streamWithWeeks); +// return TO BE IMPLEMENTED +// }; +// export default RoadmapChart; -//https://github.com/plouc/nivo/issues/1967 -const RoadmapChart: FC = ({ - defaultPoints, - issues, - jiraHost, - setGraphInitiative, - updateGraph, - setOpenGraph, - setJiraHost, -}) => { - return IMPLEMENTATION PENDING; -}; -export default RoadmapChart; +import { + endOfWeek, + startOfWeek, + startOfMonth, + differenceInDays, + differenceInHours, + differenceInMinutes, + add, + getYear, + getWeek, + format, + isEqual, +} from 'date-fns'; -// import { -// endOfWeek, -// startOfWeek, -// differenceInDays, -// differenceInHours, -// differenceInMinutes, -// add, -// getYear, -// getWeek, -// format, -// } from 'date-fns'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { styled } from '@mui/material/styles'; -// import toMaterialStyle from 'material-color-hash'; +import toMaterialStyle from 'material-color-hash'; // import { ResponsiveHeatMap, ComputedCell } from '@nivo/heatmap'; -// import { Stream, StreamWeek, StreamItem } from '../../../global'; +import { Stream, StreamWeek, StreamItem } from '../../../global'; -// interface DatasetObj { -// [key: string]: any; -// } +interface DatasetObj { + [key: string]: any; +} -// // Note: -// // differenceInDays only return full days which means the the differenceInDays -// // between first and last day of the week is going to be 6. We can instead use -// // differenceInMinutes / 1440 which gives a number close enough +interface MonthTable { + startOfMonth: Date; + colSpan: number; +} -// const buildWeekData = ( -// firstDay: Date, -// lastDay: Date, -// dailyVelocity: number, -// remainingAtWeekdStart: number, -// remainingAtWeekEnd: number, -// ) => { -// return { -// firstWeekDay: firstDay, -// lastWeekDay: lastDay, -// startOfWeek: startOfWeek(firstDay), -// endOfWeek: endOfWeek(firstDay), -// weekTxt: endOfWeek(firstDay).toISOString(), -// completed: -// Math.round( -// (differenceInMinutes(endOfWeek(firstDay), firstDay) / 1440) * -// dailyVelocity * -// 10, -// ) / 10, -// remaining: { -// atWeekStart: Math.round(remainingAtWeekdStart * 10) / 10, -// atWeekEnd: Math.round(remainingAtWeekEnd * 10) / 10, -// }, -// }; -// }; +// Note: +// differenceInDays only return full days which means the the differenceInDays +// between first and last day of the week is going to be 6. We can instead use +// differenceInMinutes / 1440 which gives a number close enough -// // Builds a future calendar based on the weeks necessary to complete the initiative -// const buildCalendar = ( -// weeklyVelocity: number, -// remaining: number, -// firstDay: Date, -// ) => { -// const perDayVelocity = weeklyVelocity / 7; // No particular logic for business days -// const weeks: Array = []; +const buildWeekData = ( + firstDay: Date, + lastDay: Date, + dailyVelocity: number, + remainingAtWeekdStart: number, + remainingAtWeekEnd: number, +) => { + return { + firstWeekDay: firstDay, + lastWeekDay: lastDay, + startOfWeek: startOfWeek(firstDay), + endOfWeek: endOfWeek(firstDay), + weekTxt: endOfWeek(firstDay).toISOString(), + completed: + Math.round( + (differenceInMinutes(endOfWeek(firstDay), firstDay) / 1440) * + dailyVelocity * + 10, + ) / 10, + remaining: { + atWeekStart: Math.round(remainingAtWeekdStart * 10) / 10, + atWeekEnd: Math.round(remainingAtWeekEnd * 10) / 10, + }, + }; +}; -// let currentRemaining = -// remaining - -// differenceInDays(endOfWeek(firstDay), firstDay) * perDayVelocity; +// Builds a future calendar based on the weeks necessary to complete the initiative +const buildCalendar = ( + weeklyVelocity: number, + remaining: number, + firstDay: Date, +) => { + const perDayVelocity = weeklyVelocity / 7; // No particular logic for business days + const weeks: Array = []; -// // The first week is always calculated by day -// weeks.push( -// buildWeekData( -// firstDay, -// endOfWeek(firstDay), -// perDayVelocity, -// remaining, -// currentRemaining, -// ), -// ); + let currentRemaining = + remaining - + differenceInDays(endOfWeek(firstDay), firstDay) * perDayVelocity; -// let weekStartCursor = startOfWeek(firstDay); -// while (currentRemaining > 0) { -// weekStartCursor = add(weekStartCursor, { days: 7 }); -// let completed = weeklyVelocity; -// let atWeekEnd = currentRemaining - weeklyVelocity; -// let lastWeekDay = endOfWeek(weekStartCursor); -// // If weekly velocity is greater than remaining, it means -// // completion is going to be in the middle of the week -// if (weeklyVelocity > currentRemaining) { -// const effortDays = currentRemaining / perDayVelocity; -// lastWeekDay = add(weekStartCursor, { days: effortDays }); -// } -// if (atWeekEnd <= 0) { -// completed = atWeekEnd + completed; -// atWeekEnd = 0; -// } -// weeks.push( -// buildWeekData( -// weekStartCursor, -// lastWeekDay, -// perDayVelocity, -// currentRemaining, -// atWeekEnd, -// ), -// ); -// currentRemaining = currentRemaining - weeklyVelocity; -// } -// return weeks; -// }; + // The first week is always calculated by day + weeks.push( + buildWeekData( + firstDay, + endOfWeek(firstDay), + perDayVelocity, + remaining, + currentRemaining, + ), + ); -// const addWeeksToStreams = (streams: Array, metric: string) => { -// return streams.map((s) => { -// if (s.metrics[metric].velocity === 0) { -// return { -// ...s, -// weeks: [], -// }; -// } -// const items = []; -// const streamVelocity = s.metrics[metric].velocity; -// if (s.items !== undefined) { -// let startDay = new Date(); -// for (const i of s.items) { -// const weeks: Array = buildCalendar( -// streamVelocity, -// i.remaining, -// startDay, -// ); -// items.push({ -// ...i, -// stream: s.name, -// name: `${s.name}: ${i.name}`, -// weeks, -// }); -// startDay = weeks.slice(-1)[0].lastWeekDay; -// } -// } -// return { -// ...s, -// items, -// weeks: buildCalendar(streamVelocity, s.remaining, new Date()), -// }; -// }); -// }; + let weekStartCursor = startOfWeek(firstDay); + while (currentRemaining > 0) { + weekStartCursor = add(weekStartCursor, { days: 7 }); + let completed = weeklyVelocity; + let atWeekEnd = currentRemaining - weeklyVelocity; + let lastWeekDay = endOfWeek(weekStartCursor); + // If weekly velocity is greater than remaining, it means + // completion is going to be in the middle of the week + if (weeklyVelocity > currentRemaining) { + const effortDays = currentRemaining / perDayVelocity; + lastWeekDay = add(weekStartCursor, { days: effortDays }); + } + if (atWeekEnd <= 0) { + completed = atWeekEnd + completed; + atWeekEnd = 0; + } + weeks.push( + buildWeekData( + weekStartCursor, + lastWeekDay, + perDayVelocity, + currentRemaining, + atWeekEnd, + ), + ); + currentRemaining = currentRemaining - weeklyVelocity; + } + return weeks; +}; -// const RoadmapChart: FC = ({ streams, metric }) => { -// // Build a weekly completion forecast calendar -// const streamWithWeeks: Array = addWeeksToStreams(streams, metric); -// // Flatten the list of items and build full array of weeks -// const items: Array = []; -// const weeks: Array = []; -// for (const s of streamWithWeeks) { -// if (s.items.length === 0) { -// items.push(s); -// if (s.weeks === undefined) { -// break; -// } -// for (const w of s.weeks) { -// if (!weeks.includes(w.weekTxt)) { -// weeks.push(w.weekTxt); -// } -// } -// } else { -// for (const i of s.items) { -// items.push(i); -// if (i.weeks === undefined) { -// break; -// } -// for (const w of i.weeks) { -// if (!weeks.includes(w.weekTxt)) { -// weeks.push(w.weekTxt); -// } -// } -// } -// } -// } +const addWeeksToStreams = (streams: Array, metric: string) => { + return streams.map((s) => { + if (s.metrics[metric].velocity === 0) { + return { + ...s, + weeks: [], + }; + } + const items = []; + const streamVelocity = s.metrics[metric].velocity; + if (s.items !== undefined) { + let startDay = new Date(); + for (const i of s.items) { + const weeks: Array = buildCalendar( + streamVelocity, + i.remaining, + startDay, + ); + items.push({ + ...i, + stream: s.name, + name: `${s.name}: ${i.name}`, + weeks, + }); + startDay = weeks.slice(-1)[0].lastWeekDay; + } + } + return { + ...s, + items, + weeks: buildCalendar(streamVelocity, s.remaining, new Date()), + }; + }); +}; -// const formattedItems: DatasetObj[] = []; -// for (const i of items) { -// const initiativeData: DatasetObj = { -// item: i.name, -// }; -// if (i.weeks === undefined) { -// return null; -// } -// for (const week of i.weeks) { -// initiativeData[week.weekTxt] = week.completed; -// } -// formattedItems.push(initiativeData); -// } +const RoadmapChart: FC = ({ streams, metric }) => { + // Build a weekly completion forecast calendar + const streamWithWeeks: Array = addWeeksToStreams(streams, metric); -// const chartHeight = 50 + items.length * 25; + // Flatten the list of items and build full array of weeks + const items: Array = []; + const weeks: Array = []; + for (const s of streamWithWeeks) { + if (s.items.length === 0) { + items.push(s); + if (s.weeks === undefined) { + break; + } + for (const w of s.weeks) { + if (!weeks.includes(w.weekTxt)) { + weeks.push(w.weekTxt); + } + } + } else { + for (const i of s.items) { + items.push(i); + if (i.weeks === undefined) { + break; + } + for (const w of i.weeks) { + if (!weeks.includes(w.weekTxt)) { + weeks.push(w.weekTxt); + } + } + } + } + } -// const getCompletionColor = (data: any, value: any) => { -// if (value === undefined) { -// return 'rgb(255, 255, 255)'; -// } -// const item = items.find((i: StreamItem) => i.name === data.yKey); -// if (item !== undefined) { -// return toMaterialStyle( -// item.stream === undefined ? item.name : item.stream, -// 200, -// ).backgroundColor; -// } -// return toMaterialStyle(data.yKey, 200).backgroundColor; -// }; + const formattedItems: DatasetObj[] = []; + for (const i of items) { + const initiativeData: DatasetObj = { + item: i.name, + }; + if (i.weeks === undefined) { + return null; + } + for (const week of i.weeks) { + initiativeData[week.weekTxt] = week.completed; + } + formattedItems.push(initiativeData); + } -// return ( -//
-// format(new Date(weekTxt), 'LLL do'), -// }} -// axisLeft={{ -// orient: 'middle', -// tickSize: 5, -// tickPadding: 5, -// tickRotation: 0, -// legend: '', -// legendPosition: 'middle', -// legendOffset: -40, -// }} -// cellOpacity={1} -// cellBorderColor={'#a4a3a5'} -// labelTextColor={{ from: 'color', modifiers: [['darker', 1.8]] }} -// cellShape={({ -// data, -// value, -// x, -// y, -// width, -// height, -// color, -// opacity, -// borderWidth, -// borderColor, -// enableLabel, -// textColor, -// onHover, -// onLeave, -// onClick, -// theme, -// }: any) => { -// if (value === 0) { -// return ( -// -// -// -// ); -// } -// return ( -// { -// console.log('Cell Click'); -// }} -// style={{ cursor: 'pointer' }} -// > -// -// {enableLabel && ( -// -// {value} -// -// )} -// -// ); -// }} -// animate={false} -// motionStiffness={80} -// motionDamping={9} -// hoverTarget="row" -// cellHoverOthersOpacity={0.1} -// /> -//
-// ); -// }; + const chartHeight = 50 + items.length * 25; -// export default RoadmapChart; + const getCompletionColor = (data: any, value: any) => { + if (value === undefined) { + return 'rgb(255, 255, 255)'; + } + const item = items.find((i: StreamItem) => i.name === data.yKey); + if (item !== undefined) { + return toMaterialStyle( + item.stream === undefined ? item.name : item.stream, + 200, + ).backgroundColor; + } + return toMaterialStyle(data.yKey, 200).backgroundColor; + }; + + // Create an array of Months cased on the included weeks + const months = weeks + .sort() + .reduce((acc: Array, weekStart: string) => { + const currentMonth = acc.find((m: any) => + isEqual(m.startOfMonth, startOfMonth(new Date(weekStart))), + ); + if (currentMonth === undefined) { + acc.push({ + startOfMonth: startOfMonth(new Date(weekStart)), + colSpan: 1, + }); + } else { + return acc.map((m: MonthTable) => { + if (isEqual(m.startOfMonth, startOfMonth(new Date(weekStart)))) { + return { + ...m, + colSpan: m.colSpan + 1, + }; + } else { + return m; + } + }); + } + return acc; + }, []); + + const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.root}`]: { + padding: 0, + width: 30, + }, + [`&.${tableCellClasses.head}`]: { + fontSize: 10, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 10, + }, + })); + + return ( + + + + Activities + {months.sort().map((m: any) => ( + + {format(new Date(m.startOfMonth), 'LLL')} + + ))} + + + {weeks.sort().map((weekStartTxt) => ( + + {format(new Date(weekStartTxt), 'do')} + + ))} + + + + {formattedItems.map((i) => ( + + + {i.item} + + {weeks.sort().map((weekStartTxt) => { + let value = ''; + if (i[weekStartTxt] !== undefined) { + value = i[weekStartTxt]; + } + return ( + + {value} + + ); + })} + + ))} + +
+ ); + + // return Test; + // return ( + //
+ // format(new Date(weekTxt), 'LLL do'), + // }} + // axisLeft={{ + // orient: 'middle', + // tickSize: 5, + // tickPadding: 5, + // tickRotation: 0, + // legend: '', + // legendPosition: 'middle', + // legendOffset: -40, + // }} + // cellOpacity={1} + // cellBorderColor={'#a4a3a5'} + // labelTextColor={{ from: 'color', modifiers: [['darker', 1.8]] }} + // cellShape={({ + // data, + // value, + // x, + // y, + // width, + // height, + // color, + // opacity, + // borderWidth, + // borderColor, + // enableLabel, + // textColor, + // onHover, + // onLeave, + // onClick, + // theme, + // }: any) => { + // if (value === 0) { + // return ( + // + // + // + // ); + // } + // return ( + // { + // console.log('Cell Click'); + // }} + // style={{ cursor: 'pointer' }} + // > + // + // {enableLabel && ( + // + // {value} + // + // )} + // + // ); + // }} + // animate={false} + // motionStiffness={80} + // motionDamping={9} + // hoverTarget="row" + // cellHoverOthersOpacity={0.1} + // /> + //
+ // ); +}; + +export default RoadmapChart; diff --git a/ui/src/views/teams/forecast/index.tsx b/ui/src/views/teams/forecast/index.tsx index 94fbd64..30adb58 100644 --- a/ui/src/views/teams/forecast/index.tsx +++ b/ui/src/views/teams/forecast/index.tsx @@ -57,7 +57,9 @@ const Forecast = () => { Naive Forecast - Remaining work calculated using {metric} + Remaining work calculated using {metric}. ==={' '} + TEMPORARY VIZ{' '} + ===