Merge pull request #303 from dod-ccpo/to-expiration-projection

Task order expiration projection
This commit is contained in:
andrewdds 2018-09-20 10:08:45 -04:00 committed by GitHub
commit 6dba46af66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 295 additions and 150 deletions

View File

@ -4,151 +4,141 @@ from itertools import groupby
MONTHLY_SPEND_AARDVARK = {
"LC04": {
"Integ": {
"10/2018": 284,
"11/2018": 1210,
"12/2018": 1430,
"01/2019": 1366,
"02/2019": 1169,
"03/2019": 991,
"04/2019": 978,
"05/2019": 737,
"02/2018": 284,
"03/2018": 1210,
"04/2018": 1430,
"05/2018": 1366,
"06/2018": 1169,
"07/2018": 991,
"08/2018": 978,
"09/2018": 737,
},
"PreProd": {
"10/2018": 812,
"11/2018": 1389,
"12/2018": 1425,
"01/2019": 1306,
"02/2019": 1112,
"03/2019": 936,
"04/2019": 921,
"05/2019": 694,
"02/2018": 812,
"03/2018": 1389,
"04/2018": 1425,
"05/2018": 1306,
"06/2018": 1112,
"07/2018": 936,
"08/2018": 921,
"09/2018": 694,
},
"Prod": {
"10/2018": 1742,
"11/2018": 1716,
"12/2018": 1866,
"01/2019": 1809,
"02/2019": 1839,
"03/2019": 1633,
"04/2019": 1654,
"05/2019": 1103,
"02/2018": 1742,
"03/2018": 1716,
"04/2018": 1866,
"05/2018": 1809,
"06/2018": 1839,
"07/2018": 1633,
"08/2018": 1654,
"09/2018": 1103,
},
},
"SF18": {
"Integ": {
"12/2018": 1498,
"01/2019": 1400,
"02/2019": 1394,
"03/2019": 1171,
"04/2019": 1200,
"05/2019": 963,
"04/2018": 1498,
"05/2018": 1400,
"06/2018": 1394,
"07/2018": 1171,
"08/2018": 1200,
"09/2018": 963,
},
"PreProd": {
"12/2018": 1780,
"01/2019": 1667,
"02/2019": 1703,
"03/2019": 1474,
"04/2019": 1441,
"05/2019": 933,
"04/2018": 1780,
"05/2018": 1667,
"06/2018": 1703,
"07/2018": 1474,
"08/2018": 1441,
"09/2018": 933,
},
"Prod": {
"12/2018": 1686,
"01/2019": 1779,
"02/2019": 1792,
"03/2019": 1570,
"04/2019": 1539,
"05/2019": 986,
"04/2018": 1686,
"05/2018": 1779,
"06/2018": 1792,
"07/2018": 1570,
"08/2018": 1539,
"09/2018": 986,
},
},
"Canton": {
"Prod": {
"01/2019": 28699,
"02/2019": 26766,
"03/2019": 22619,
"04/2019": 24090,
"05/2019": 16719,
"05/2018": 28699,
"06/2018": 26766,
"07/2018": 22619,
"08/2018": 24090,
"09/2018": 16719,
}
},
"BD04": {
"Integ": {},
"PreProd": {
"10/2018": 7019,
"11/2018": 3004,
"12/2018": 2691,
"01/2019": 2901,
"02/2019": 3463,
"03/2019": 3314,
"04/2019": 3432,
"05/2019": 723,
"02/2018": 7019,
"03/2018": 3004,
"04/2018": 2691,
"05/2018": 2901,
"06/2018": 3463,
"07/2018": 3314,
"08/2018": 3432,
"09/2018": 723,
},
},
"SCV18": {"Dev": {"05/2019": 9797}},
"Crown": {
"CR Portal Dev": {
"11/2018": 208,
"12/2018": 457,
"01/2019": 671,
"02/2019": 136,
"03/2019": 1524,
"04/2019": 2077,
"05/2019": 1858,
"03/2018": 208,
"04/2018": 457,
"05/2018": 671,
"06/2018": 136,
"07/2018": 1524,
"08/2018": 2077,
"09/2018": 1858,
},
"CR Staging": {
"11/2018": 208,
"12/2018": 457,
"01/2019": 671,
"02/2019": 136,
"03/2019": 1524,
"04/2019": 2077,
"05/2019": 1858,
"03/2018": 208,
"04/2018": 457,
"05/2018": 671,
"06/2018": 136,
"07/2018": 1524,
"08/2018": 2077,
"09/2018": 1858,
},
"CR Portal Test 1": {"03/2019": 806, "04/2019": 1966, "05/2019": 2597},
"Jewels Prod": {"03/2019": 806, "04/2019": 1966, "05/2019": 2597},
"CR Portal Test 1": {"07/2018": 806, "08/2018": 1966, "09/2018": 2597},
"Jewels Prod": {"07/2018": 806, "08/2018": 1966, "09/2018": 2597},
"Jewels Dev": {
"11/2018": 145,
"12/2018": 719,
"01/2019": 1243,
"02/2019": 2214,
"03/2019": 2959,
"04/2019": 4151,
"05/2019": 4260,
"03/2018": 145,
"04/2018": 719,
"05/2018": 1243,
"06/2018": 2214,
"07/2018": 2959,
"08/2018": 4151,
"09/2018": 4260,
},
},
}
CUMULATIVE_BUDGET_AARDVARK = {
"10/2018": {"spend": 9857, "cumulative": 9857},
"11/2018": {"spend": 7881, "cumulative": 17738},
"12/2018": {"spend": 14010, "cumulative": 31748},
"01/2019": {"spend": 43510, "cumulative": 75259},
"02/2019": {"spend": 41725, "cumulative": 116984},
"03/2019": {"spend": 41328, "cumulative": 158312},
"04/2019": {"spend": 47491, "cumulative": 205803},
"05/2019": {"spend": 45826, "cumulative": 251629},
"06/2019": {"projected": 296511},
"07/2019": {"projected": 341393},
"08/2019": {"projected": 386274},
"09/2019": {"projected": 431156},
"02/2018": {"spend": 9857, "cumulative": 9857},
"03/2018": {"spend": 7881, "cumulative": 17738},
"04/2018": {"spend": 14010, "cumulative": 31748},
"05/2018": {"spend": 43510, "cumulative": 75259},
"06/2018": {"spend": 41725, "cumulative": 116984},
"07/2018": {"spend": 41328, "cumulative": 158312},
"08/2018": {"spend": 47491, "cumulative": 205803},
"09/2018": {"spend": 45826, "cumulative": 251629},
}
MONTHLY_SPEND_BELUGA = {
"NP02": {
"Integ": {"02/2019": 284, "03/2019": 1210},
"PreProd": {"02/2019": 812, "03/2019": 1389},
"Prod": {"02/2019": 3742, "03/2019": 4716},
"Integ": {"08/2018": 284, "09/2018": 1210},
"PreProd": {"08/2018": 812, "09/2018": 1389},
"Prod": {"08/2018": 3742, "09/2018": 4716},
},
"FM": {"Integ": {"03/2019": 1498}, "Prod": {"03/2019": 5686}},
"FM": {"Integ": {"08/2018": 1498}, "Prod": {"09/2018": 5686}},
}
CUMULATIVE_BUDGET_BELUGA = {
"02/2019": {"spend": 4838, "cumulative": 4838},
"03/2019": {"spend": 14500, "cumulative": 19338},
"04/2019": {"projected": 29007},
"05/2019": {"projected": 38676},
"06/2019": {"projected": 48345},
"07/2019": {"projected": 58014},
"08/2019": {"projected": 67683},
"09/2019": {"projected": 77352},
"08/2018": {"spend": 4838, "cumulative": 4838},
"09/2018": {"spend": 14500, "cumulative": 19338},
}

View File

@ -106,6 +106,11 @@ def workspace_reports(workspace_id):
prev_month = current_month - timedelta(days=28)
two_months_ago = prev_month - timedelta(days=28)
# lets just say it expires on Christmas... ho ho ho
expiration_date = date(2018, 12, 25)
remaining_difference = expiration_date - today
remaining_days = remaining_difference.days
return render_template(
"workspaces/reports/index.html",
cumulative_budget=Reports.cumulative_budget(alternate_reports),
@ -114,6 +119,8 @@ def workspace_reports(workspace_id):
current_month=current_month,
prev_month=prev_month,
two_months_ago=two_months_ago,
expiration_date=expiration_date,
remaining_days=remaining_days,
)

View File

@ -1,14 +1,15 @@
import { format } from 'date-fns'
import { format, isWithinRange, addMonths, isSameMonth, getMonth } from 'date-fns'
import { abbreviateDollars, formatDollars } from '../../lib/dollars'
const TOP_OFFSET = 20
const BOTTOM_OFFSET = 60
const BOTTOM_OFFSET = 70
const CHART_HEIGHT = 360
export default {
name: 'budget-chart',
props: {
currentMonth: String,
expirationDate: String,
months: Object,
budget: String
},
@ -46,18 +47,15 @@ export default {
let lastSpendPoint = ''
for (let i = 0; i < this.numMonths; i++) {
const { metrics, budget } = this.displayedMonths[i]
const { metrics, budget, rollingAverage, cumulativeTotal } = this.displayedMonths[i]
const blockWidth = (this.width / this.numMonths)
const blockX = blockWidth * i
const spend = budget
? budget.spend || lastSpend
: 0
const cumulative = budget
? budget.cumulative || budget.projected
: 0
const spend = budget && budget.spend
? budget.spend
: rollingAverage
const barHeight = spend / this.heightScale
lastSpend = spend
const cumulativeY = this.height - (cumulative / this.heightScale) - BOTTOM_OFFSET
const cumulativeY = this.height - (cumulativeTotal / this.heightScale) - BOTTOM_OFFSET
const cumulativeX = blockX + blockWidth/2
const cumulativePoint = `${cumulativeX} ${cumulativeY}`
@ -77,9 +75,7 @@ export default {
this.spendPath += this.spendPath === '' ? 'M' : ' L'
this.spendPath += cumulativePoint
lastSpendPoint = cumulativePoint
}
if (budget && budget.projected) {
} else {
this.projectedPath += this.projectedPath === '' ? `M${lastSpendPoint} L` : ' L'
this.projectedPath += cumulativePoint
}
@ -88,32 +84,76 @@ export default {
_setDisplayedMonths: function () {
const [month, year] = this.currentMonth.split('/')
const [expYear, expMonth, expDate] = this.expirationDate.split('-') // assumes format 'YYYY-MM-DD'
const monthsRange = []
const monthsBack = this.focusedMonthPosition
const monthsBack = this.focusedMonthPosition + 1
const monthsForward = this.numMonths - this.focusedMonthPosition - 1
const start = new Date(year, month - 1 - monthsBack)
let previousAmount = 0
// currently focused date
const current = new Date(year, month)
// starting date of the chart
const start = addMonths(current, -monthsBack)
// ending date of the chart
const end = addMonths(start, this.numMonths + 1)
// expiration date
const expires = new Date(expYear, expMonth-1, expDate)
// is the expiration date within the displayed date range?
const expirationWithinRange = isWithinRange(expires, start, end)
let rollingAverage = 0
let cumulativeTotal = 0
for (let i = 0; i < this.numMonths; i++) {
const date = new Date(start.getFullYear(), start.getMonth() + i)
const date = addMonths(start, i)
const dateMinusOne = addMonths(date, -1)
const dateMinusTwo = addMonths(date, -2)
const dateMinusThree = addMonths(date, -3)
const index = format(date, 'MM/YYYY')
const indexMinusOne = format(dateMinusOne, 'MM/YYYY')
const indexMinusTwo = format(dateMinusTwo, 'MM/YYYY')
const indexMinusThree = format(dateMinusThree, 'MM/YYYY')
const budget = this.months[index] || null
const spendAmount = budget ? budget.spend || previousAmount : 0
const cumulativeAmount = budget ? budget.cumulative || budget.projected : 0
previousAmount = spendAmount
const spendAmount = budget ? budget.spend : rollingAverage
const spendMinusOne = this.months[indexMinusOne] ? this.months[indexMinusOne].spend : rollingAverage
const spendMinusTwo = this.months[indexMinusTwo] ? this.months[indexMinusTwo].spend : rollingAverage
const spendMinusThree = this.months[indexMinusThree] ? this.months[indexMinusThree].spend : rollingAverage
const isExpirationMonth = isSameMonth(date, expires)
if (budget && budget.cumulative) {
cumulativeTotal = budget.cumulative
} else {
cumulativeTotal += spendAmount
}
rollingAverage = (
spendAmount
+ spendMinusOne
+ spendMinusTwo
+ spendMinusThree
) / 4
monthsRange.push({
budget,
rollingAverage,
cumulativeTotal,
isExpirationMonth,
spendAmount: formatDollars(spendAmount),
abbreviatedSpend: abbreviateDollars(spendAmount),
cumulativeAmount: formatDollars(cumulativeAmount),
abbreviatedCumulative: abbreviateDollars(cumulativeAmount),
cumulativeAmount: formatDollars(cumulativeTotal),
abbreviatedCumulative: abbreviateDollars(cumulativeTotal),
date: {
monthIndex: format(date, 'M'),
month: format(date, 'MMM'),
year: format(date,'YYYY')
},
showYear: isExpirationMonth || (i === 0) || getMonth(date) === 0,
isHighlighted: this.currentMonth === index,
metrics: {
blockWidth: 0,

View File

@ -60,21 +60,39 @@
}
.budget-chart__block {
fill: $color-white;
fill: transparent;
cursor: pointer;
&--highlighted {
fill: $color-aqua-lightest;
fill: rgba($color-aqua, .15);
}
&--is-expiration {
border-left: 2px dotted $color-gray;
}
&:hover {
fill: $color-aqua-lightest;
fill: rgba($color-aqua, .15);
}
}
svg {
display: block;
.filter__text-background {
feFlood {
flood-color: $color-white;
flood-opacity: 1;
}
&--highlighted {
feFlood {
flood-color: $color-aqua-lightest;
flood-opacity: 1;
}
}
}
a {
text-decoration: none;
&:focus {
@ -82,6 +100,15 @@
stroke: $color-gray-light;
stroke-dasharray: 2px;
}
&:hover {
.filter__text-background {
feFlood {
flood-color: $color-aqua-lightest;
flood-opacity: 1;
}
}
}
}
}
@ -96,6 +123,12 @@
}
}
.budget-chart__expiration-line {
stroke-width: 2px;
stroke: $color-gray-light;
stroke-dasharray: 4px;
}
.budget-chart__cumulative__dot {
fill: $color-gold;
}
@ -122,6 +155,7 @@
.budget-chart__label {
@include small-label;
fill: $color-gray;
pointer-events: none;
&--strong {
fill: $color-black;

View File

@ -63,12 +63,17 @@
<dl>
<div>
<dt>Expires</dt>
<dd>November 1, 2019</dd>
<dd>
<local-datetime
timestamp='{{ expiration_date }}'
format='MMMM D, YYYY'>
</local-datetime>
</dd>
</div>
<div>
<dt>Remaining</dt>
<dd>200 days</dd>
<dd>{{ remaining_days }} days</dd>
</div>
</dl>
@ -97,7 +102,12 @@
{% set two_months_ago_index = two_months_ago.strftime('%m/%Y') %}
{% set reports_url = url_for("workspaces.workspace_reports", workspace_id=workspace.id) %}
<budget-chart budget={{ budget }} current-month='{{ current_month_index }}' v-bind:months='{{ cumulative_budget.months | tojson }}' inline-template>
<budget-chart
budget={{ budget }}
current-month='{{ current_month_index }}'
expiration-date='{{ expiration_date }}'
v-bind:months='{{ cumulative_budget.months | tojson }}'
inline-template>
<div class='budget-chart panel' ref='panel'>
<header class='budget-chart__header panel__heading panel__heading--tight'>
<h2 class='h3'>Cumulative Budget</h2>
@ -127,18 +137,54 @@
</header>
<svg v-bind:height='height' v-bind:width='width'>
<defs>
<filter x="-0.04" y="0" width="1.08" height="1" class='filter__text-background' id="text-background">
<feFlood/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
{# spend/projected budget path lines #}
<path class='budget-chart__projected-path' v-bind:d='projectedPath'></path>
<path class='budget-chart__spend-path' v-bind:d='spendPath'></path>
{# max budget line #}
<line
class='budget-chart__budget-line'
x1='0'
v-bind:x2='width'
v-bind:y1='budgetHeight'
v-bind:y2='budgetHeight'></line>
<g v-for='month in displayedMonths' >
{# make this clickable to focus on that month #}
<a v-bind:href='"{{ reports_url }}?month=" + month.date.monthIndex + "&year=" + month.date.year'>
<defs>
<filter
x="-0.04"
y="0"
width="1.08"
height="1"
class='filter__text-background'
v-bind:class='{ "filter__text-background--highlighted": month.isHighlighted }'
v-bind:id="'text-background__' +month.date.month + month.date.year">
<feFlood/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<title>
<span v-html='month.date.month + " " + month.date.year'></span>&nbsp;|&nbsp;<!--
--><template v-if='month.budget'><!--
--><template v-if='month.budget.spend'>Spend:</template><!--
--><template v-if='month.budget.projected'>Projected Spend:</template><!--
--><template v-if='month.cumulativeTotal'><!--
--><template v-if='month.budget && month.budget.spend'>Spend:</template><!--
--><template v-else>Projected Spend:</template><!--
--><span v-html='month.spendAmount'></span><!--
-->&nbsp;|&nbsp;<!--
--><template v-if='month.budget.cumulative'>Total:</template><!--
--><template v-if='month.budget.projected'>Projected Total:</template><!--
--><template v-if='month.budget'>Total:</template><!--
--><template v-else>Projected Total:</template><!--
--><span v-html='month.cumulativeAmount'></span><!--
--></template><!--
@ -148,7 +194,7 @@
{# container block #}
<rect
class='budget-chart__block'
v-bind:class='{ "budget-chart__block--highlighted": month.isHighlighted }'
v-bind:class='{ "budget-chart__block--highlighted": month.isHighlighted, "budget-chart__block-is-expiration": month.isExpirationMonth }'
v-bind:width='month.metrics.blockWidth'
v-bind:x='month.metrics.blockX'
v-bind:height='height'></rect>
@ -163,9 +209,36 @@
v-bind:x='month.metrics.barX'
v-bind:y='month.metrics.barY'></rect>
{# projected budget bar #}
<rect
v-if='!month.budget'
class='budget-chart__bar budget-chart__bar--projected'
v-bind:width='month.metrics.barWidth'
v-bind:height='month.metrics.barHeight'
v-bind:x='month.metrics.barX'
v-bind:y='month.metrics.barY'></rect>
{# task order expiration line #}
<line
v-if='month.isExpirationMonth'
class='budget-chart__expiration-line'
v-bind:x1='month.metrics.cumulativeX'
v-bind:x2='month.metrics.cumulativeX'
y1='0'
v-bind:y2='baseHeight'></line>
{# task order expiration label #}
<text
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
v-if='month.isExpirationMonth'
text-anchor='middle'
v-bind:x='month.metrics.cumulativeX'
v-bind:y='budgetHeight + 20'
class='budget-chart__label'>T.O. Expires</text>
{# cumulative dot #}
<circle
v-if='month.budget'
v-if='month.cumulativeTotal'
class='budget-chart__cumulative__dot'
v-bind:r='month.metrics.cumulativeR'
v-bind:cx='month.metrics.cumulativeX'
@ -173,7 +246,8 @@
{# abbreviated cumulative label #}
<text
v-if='month.budget'
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
v-if='month.cumulativeTotal'
v-bind:x='month.metrics.cumulativeX'
v-bind:y='month.metrics.cumulativeY - 10'
text-anchor='middle'
@ -182,6 +256,7 @@
{# abbreviated spend label #}
<text
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
v-bind:x='month.metrics.cumulativeX'
v-bind:y='baseHeight + 20'
text-anchor='middle'
@ -190,26 +265,25 @@
{# month label #}
<text
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
v-bind:x='month.metrics.cumulativeX'
v-bind:y='baseHeight + 40'
text-anchor='middle'
class='budget-chart__label budget-chart__label--strong'
v-html='month.date.month'></text>
{# year label #}
<text
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
v-if='month.showYear'
v-bind:x='month.metrics.cumulativeX'
v-bind:y='baseHeight + 55'
text-anchor='middle'
class='budget-chart__label budget-chart__label--strong'
v-html='month.date.year'></text>
</g>
</a>
{# spend/projected budget path lines #}
<path class='budget-chart__projected-path' v-bind:d='projectedPath'></path>
<path class='budget-chart__spend-path' v-bind:d='spendPath'></path>
{# max budget line #}
<line
class='budget-chart__budget-line'
x1='0'
v-bind:x2='width'
v-bind:y1='budgetHeight'
v-bind:y2='budgetHeight'></line>
<text
x='20'
v-bind:y='budgetHeight + 20'