From 5cdf84354656efb4fd7718d84d8d30f3c26aab95 Mon Sep 17 00:00:00 2001 From: rfultz Date: Tue, 30 Jun 2020 13:00:26 -0400 Subject: [PATCH 1/5] WIP --- fec/data/templates/browse-data.jinja | 5 +- .../partials/browse-data/committees.jinja | 30 ++ fec/fec/static/js/modules/pac-comparator.js | 300 ++++++++++++++++++ fec/webpack.config.js | 3 +- 4 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 fec/fec/static/js/modules/pac-comparator.js diff --git a/fec/data/templates/browse-data.jinja b/fec/data/templates/browse-data.jinja index d31f7abf75..5f66c87ee6 100644 --- a/fec/data/templates/browse-data.jinja +++ b/fec/data/templates/browse-data.jinja @@ -180,6 +180,7 @@ {% endblock %} {% block scripts %} - - + + + {% endblock %} diff --git a/fec/data/templates/partials/browse-data/committees.jinja b/fec/data/templates/partials/browse-data/committees.jinja index 8cc94a0b05..d408a082d1 100644 --- a/fec/data/templates/partials/browse-data/committees.jinja +++ b/fec/data/templates/partials/browse-data/committees.jinja @@ -2,6 +2,36 @@

Committees

The term committee encompasses several different political groups that receive and spend money in federal elections.

Search or browse data

+
+
+

PAC Comparator

+
+ +
@@ -246,7 +314,8 @@ new Vue({
+ :pacs="pacs" + :slider-width="sliderWidth">
`, @@ -255,20 +324,31 @@ new Vue({ for (let i = 2020; i >= 1980; i -= 2) { this.electionCycles.push({ value: i, label: `${i - 1}-${i}` }); } + + // Listen to window resize to determine internet component sizes + window.addEventListener('resize', this.handleWindowResize); + // And grab the width now + this.sliderWidth = this.$el.offsetWidth * 0.45; }, methods: { handleElectionYearChange: function(newValue) { this.election_cycle = parseInt(newValue); // Check the pacs that are currently loaded and load either that need it for (let i = 0; i < this.pacs.length; i++) { - if (this.pacs[i].dataSummary.id != '') this.startLoadingCommitteeData(i); + if (this.pacs[i].dataSummary.id != '') + this.startLoadingCommitteeData(i); } }, handleTypeaheadSelect: function(typeaheadResults, vueComponent) { this.pacs[vueComponent.id].dataSummary = typeaheadResults; this.pacs[vueComponent.id].cmteDetails = {}; // reset committee details this.pacs[vueComponent.id].dataDetails = {}; // reset committee data - this.pacs[vueComponent.id].calc = {}; // reset calculated fields + this.pacs[vueComponent.id].calc = { + contribs: 0, + indExpend: 0, + operExpend: 0, + otherSpend: 0 + }; // reset calculated fields this.startLoadingCommitteeData(vueComponent.id); }, handleCommitteeHeaderClick: function( @@ -375,8 +455,6 @@ new Vue({ this.pacs[i].errorMessage = 'You entered a committee that is not a nonconnected PAC. Please enter an active nonconnected PAC name or ID.'; } - - // this.pacs[i].isLoading = false; break; // no reason to check another } } @@ -387,9 +465,8 @@ new Vue({ requestedPacID, requestedElectionCycle ) { - console.log('handleCommitteeRequestRejected(): ', error, requestedPacID, requestedElectionCycle); - // Since we had a requested rejected, let's check to make sure it's still a legit request - // (e.g., if we requested on pac-cycle combo but the user has changed the year since then, this rejection is outdated) + // Since we had a request rejected, let's check to make sure it's still a legit request + // (e.g., if we requested a pac-cycle combo but the user has changed the year since then, this rejection is outdated) if ( requestedElectionCycle && requestedElectionCycle == this.election_cycle @@ -397,10 +474,15 @@ new Vue({ for (let i = 0; i < this.pacs.length; i++) { if (this.pacs[i].dataSummary.id == requestedPacID) { if (error.message == 'ERROR:REJECTED_DATA_REQUEST') { - this.pacs[i].errorMessage = - 'You entered a committee that was not active during 2017-2018 period. Please enter a nonconnected PAC name or ID active during 2017-2018.'; + let yearSpanStr = requestedElectionCycle - 1; + yearSpanStr += `-${requestedElectionCycle}`; + + let message = `You entered a committee that was not active during the ${yearSpanStr} period. \ + Please enter a nonconnected PAC name or ID active during ${yearSpanStr}.`; + this.pacs[i].errorMessage = message; } else { this.pacs[i].errorMessage = 'HANDLE THIS'; + // TODO } this.pacs[i].isLoading = false; // re-enable the typeahead @@ -480,18 +562,22 @@ new Vue({ } } }, + handleWindowResize: function() { + this.sliderWidth = this.$el.offsetWidth / 2; + }, + /** + * The rules are complicated for whether a committee is nonconnected so they're their own function + * @returns {(Boolean|String)} Boolean if a committee IS or is NOT a nonconnected committee, 'maybe' if more data is needed (i.e., organization_type) + * TODO: move this to calcPacValues when the API returns a is_nonconnected value + */ isNonconnectedPac: function(pacSlotPos) { /* Valid rules to find nonconnected committees: - 1. committee type in [O, U, V, W] 2. committee designation NOT in [A, J, P] - OR - 1. committee type in [N, Q] 2. committee designation NOT in [A, J, P] 3. org type NOT in [C, L, M, T, W] - */ let cType = this.pacs[pacSlotPos].dataDetails.committee_type; let cDesig = this.pacs[pacSlotPos].dataDetails.committee_designation; @@ -502,10 +588,6 @@ new Vue({ ) oType = String(this.pacs[pacSlotPos].cmteDetails.organization_type); - console.log('cmteDetails: ', this.pacs[pacSlotPos].cmteDetails); - console.log('organization_type: ', this.pacs[pacSlotPos].cmteDetails.organization_type); - console.log('cType, cDesig, oType: ', cType, cDesig, oType); - let validCmteTypesWithOrgType = ['O', 'U', 'V', 'W']; let validCmteTypesWithoutOrgType = ['N', 'Q']; let invalidOrgTypes = ['C', 'L', 'M', 'T', 'W']; @@ -514,7 +596,7 @@ new Vue({ let toReturn = false; if (invalidDesigs.includes(cDesig)) { - // If we have an invalid designation, its not nonconnected + // If we have an invalid designation, it's not nonconnected // We'll let toReturn stay false } else if (validCmteTypesWithoutOrgType.includes(cType)) { // If the committee type doesn't need an org type, we're safe to return @@ -532,10 +614,12 @@ new Vue({ toReturn = true; } - console.log('isNonConnectedPac: ', toReturn); - return toReturn; }, + /** + * + * @param {number} pacSlotPos {integer} + */ calcPacValues: function(pacSlotPos) { let toReturn = {}; let d = this.pacs[pacSlotPos].dataDetails; @@ -554,7 +638,7 @@ new Vue({ day: '2-digit', month: '2-digit', year: '2-digit', - timeZone: 'America/New_York' + timeZone: 'GMT' }; let startDate = new Date(d.coverage_start_date).toLocaleDateString( 'us-EN', @@ -632,10 +716,8 @@ new Vue({ 'refunds_relating_convention_exp' ]; otherSpend.forEach(value => { - console.log('otherSpend.forEach(): ', value, d[value]); if (d[value]) toReturn.otherSpending += d[value]; }); - console.log('toReturn.otherSpending: ', toReturn.otherSpending); toReturn.otherSpendingStr = this.formatAsCurrency(toReturn.otherSpending); toReturn.otherSpendingPct = toReturn.otherSpending / toReturn.totDisburse; toReturn.otherSpendingPctStr = this.percentString( @@ -656,7 +738,8 @@ new Vue({ }, percentString: function(val) { let v = val * 100; - return v < 1 ? '<1%' : `${Math.round(v)}%`; + if (val === 0) return '0%'; + else return v < 1 ? '<1%' : `${Math.round(v)}%`; } } }); From cbf53bbb6e7c7690c16fd34c2cb1acac439ac99e Mon Sep 17 00:00:00 2001 From: rfultz Date: Fri, 17 Jul 2020 17:12:02 -0400 Subject: [PATCH 4/5] Added a couple more chart formats --- .../partials/browse-data/committees.jinja | 79 +++++- fec/fec/static/js/modules/pac-comparator.js | 262 ++++++++++++++---- 2 files changed, 280 insertions(+), 61 deletions(-) diff --git a/fec/data/templates/partials/browse-data/committees.jinja b/fec/data/templates/partials/browse-data/committees.jinja index 3e10e74a74..fe5ebe08c4 100644 --- a/fec/data/templates/partials/browse-data/committees.jinja +++ b/fec/data/templates/partials/browse-data/committees.jinja @@ -119,6 +119,12 @@ position: absolute; width: 1rem; } + .icon-bar { + border-radius: 0; + height: .5rem; + min-width: 1px; + width: 1px; + } .icon-pac.pac-0 { /* Sketch */ background-color: #F77B42; @@ -192,10 +198,21 @@ height: 2px; background-color: #E8E8E8; } + table.sliders .slider.bars { + height: 1rem; + } table.sliders .slider .icon-pac { top: calc(-.5rem + 1px); transition: left 1s; } + table.sliders .slider .icon-bar.pac-0 { + top: 0; + transition: width 1s; + } + table.sliders .slider .icon-bar.pac-1 { + top: .5rem; + transition: width 1s; + } table.sliders .t-right-aligned span { display: block; line-height: 1.1em; @@ -211,10 +228,11 @@ position: relative; } table.sliders .ticks .tick { - width: 4rem; - text-align: center; - position: relative; display: inline-block; + font-weight: normal; + position: relative; + text-align: center; + width: 4rem; /* Sketch */ color: #4A4A4A; @@ -246,6 +264,61 @@ padding-top: 0; } + .barsV { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + .barsV figure { + width: 25%; + text-align: center; + } + .barsV figure figcaption { + line-height: 1.25em; + } + .bar-v-group { + height: 100px; + display: flex; + justify-content: center; + align-items: flex-end; + } + .bar-v-group .icon-pac { + position: relative; + } + .icon-bar-v { + border-radius: 0; + height: 1px; + min-height: 1px; + width: 4rem; + } + .icon-bar-v label { + line-height: 1.1em; + padding-top: .25rem; + position: absolute; + top: .5rem; + } + .icon-bar-v.pac-0 label { + right: .5rem; + text-align: right; + } + .icon-bar-v.pac-1 label { + left: .5rem; + text-align: left; + } + .icon-bar-v label span { + display: block; + font-style: normal; + } + .icon-bar-v label span:first-child { + font-weight: bold; + font-size: 1.5rem; + } + .icon-bar-v label span:last-child { + font-size: 1.1rem; + } + .icon-bar-v label.above { + top: -35px; + }

Search or browse data

diff --git a/fec/fec/static/js/modules/pac-comparator.js b/fec/fec/static/js/modules/pac-comparator.js index 092c115534..19094e6f6b 100644 --- a/fec/fec/static/js/modules/pac-comparator.js +++ b/fec/fec/static/js/modules/pac-comparator.js @@ -129,14 +129,13 @@ Vue.component('slider', { }, template: `
     
@@ -170,70 +169,109 @@ Vue.component('sliders', { - - Operating expenditures + + {{ pacs[0].calc[root + 'Label'] }} - {{ pacs[0].calc.operExpendPctStr }} - {{ pacs[0].calc.operExpendStr }} + {{ pacs[0].calc[root + 'PctStr'] }} + {{ pacs[0].calc[root + 'Str'] }} - {{ pacs[1].calc.operExpendPctStr }} - {{ pacs[1].calc.operExpendStr }} + {{ pacs[1].calc[root + 'PctStr'] }} + {{ pacs[1].calc[root + 'Str'] }} + + + - Contributions to federal candidates / committees - - - - - {{ pacs[0].calc.contribsPctStr }} - {{ pacs[0].calc.contribsStr }} - - - {{ pacs[1].calc.contribsPctStr }} - {{ pacs[1].calc.contribsStr }} - +   + +   + 0% + 25% + 50% + 75% + 100% + +   +   + + + ` +}); + +Vue.component('sliderBar', { + props: { + data: { + type: Object, + required: true + } + }, + template: ` +
+   +   +   +
+ ` +}); + +Vue.component('barsH', { + props: { + pacs: { + type: Array, + required: true + }, + sliderWidth: { + type: Number, + required: true + } + }, + template: ` + + - - - - + + + - + + + + + + + + + + @@ -254,6 +292,63 @@ Vue.component('sliders', { ` }); +Vue.component('barV', { + props: { + data: { + type: Object, + required: true + } + }, + data: function() { + return { + labelPos: { top: '0px;' } + }; + }, + template: ` + + + + ` +}); + +Vue.component('barsV', { + props: { + pacs: { + type: Array, + required: true + } + }, + template: ` +
+
+
+ + +
+
{{ pacs[0].calc[root + 'Label'] }}
+
+
+ ` +}); + new Vue({ el: '#pac-comparator', data: { @@ -264,7 +359,7 @@ new Vue({ pacs: [ { key: 'pacSlot0', - calc: { contribs: 0, indExpend: 0, operExpend: 0, otherSpend: 0 }, // Calculated values (derived from dataDetails when they're loaded) + calc: { contribs: 0, indExpend: 0, operExpend: 0, otherSpending: 0 }, // Calculated values (derived from dataDetails when they're loaded) cmteDetails: {}, // Details about the committee org dataDetails: {}, // Committee's financial query results dataSummary: { id: '', name: '' }, // Typeahead results @@ -273,7 +368,7 @@ new Vue({ }, { key: 'pacSlot1', - calc: { contribs: 0, indExpend: 0, operExpend: 0, otherSpend: 0 }, + calc: { contribs: 0, indExpend: 0, operExpend: 0, otherSpending: 0 }, cmteDetails: {}, dataDetails: {}, dataSummary: { id: '', name: '' }, @@ -317,6 +412,13 @@ new Vue({ :pacs="pacs" :slider-width="sliderWidth"> + + + + `, beforeMount: function() { @@ -325,6 +427,9 @@ new Vue({ this.electionCycles.push({ value: i, label: `${i - 1}-${i}` }); } + // Init (reset) pac values + this.calcPacValues(null, { reset: true }); + // Listen to window resize to determine internet component sizes window.addEventListener('resize', this.handleWindowResize); // And grab the width now @@ -343,12 +448,7 @@ new Vue({ this.pacs[vueComponent.id].dataSummary = typeaheadResults; this.pacs[vueComponent.id].cmteDetails = {}; // reset committee details this.pacs[vueComponent.id].dataDetails = {}; // reset committee data - this.pacs[vueComponent.id].calc = { - contribs: 0, - indExpend: 0, - operExpend: 0, - otherSpend: 0 - }; // reset calculated fields + this.calcPacValues(vueComponent.id, { reset: true }); // reset calculated fields this.startLoadingCommitteeData(vueComponent.id); }, handleCommitteeHeaderClick: function( @@ -454,6 +554,8 @@ new Vue({ } else { this.pacs[i].errorMessage = 'You entered a committee that is not a nonconnected PAC. Please enter an active nonconnected PAC name or ID.'; + this.calcPacValues(i, { reset: true }); + this.pacs[i].isLoading = false; } break; // no reason to check another } @@ -618,12 +720,56 @@ new Vue({ }, /** * - * @param {number} pacSlotPos {integer} + * @param {(number|null)} pacSlotPos Indicates which this.pacs to save. A null value will reset all of them regardless of options.reset + * @param {Object} options Object + * @param {Boolean} options.reset Used to reset an item in this.pacs */ - calcPacValues: function(pacSlotPos) { - let toReturn = {}; - let d = this.pacs[pacSlotPos].dataDetails; + calcPacValues: function(pacSlotPos, options = { reset: false }) { + let d = this.pacs[pacSlotPos] ? this.pacs[pacSlotPos].dataDetails : {}; + + let toReturn = { + // Default, universal + contribsLabel: 'Contributions to federal candidates / committees', + indExpendLabel: 'Independent expenditures', + operExpendLabel: 'Operating expenditures', + otherSpendingLabel: 'Other spending' + }; + + // If we need to reset, let's just do that and jump out + if (options.reset === true) { + toReturn = Object.assign(toReturn, { + pacType: '', + coverageDatesStatement: '', + totDisburse: 0, + totDisburseStr: '', + contribs: 0, + contribsStr: '', + contribsPct: 0, + contribsPctStr: '', + indExpend: 0, + indExpendStr: '', + indExpendPct: 0, + indExpendPctStr: '', + operExpend: 0, + operExpendStr: '', + operExpendPct: 0, + operExpendPctStr: '', + otherSpending: 0, + otherSpendingStr: '', + otherSpendingPct: 0, + otherSpendingPctStr: '' + }); + if (options.reset === true && pacSlotPos === null) { + // If there's no slot provided, let's hit them all + for (let i = 0; i < this.pacs.length; i++) { + this.pacs[i].calc = toReturn; + } + } else if (options.reset === true && this.pacs[pacSlotPos]) { + this.pacs[pacSlotPos].calc = toReturn; + } + return; + } // Total disbursements toReturn.totDisburse = d.disbursements; toReturn.totDisburseStr = this.formatAsCurrency(d.disbursements); From 0d93053ea87b90011edbf18e9221038d66055de1 Mon Sep 17 00:00:00 2001 From: rfultz Date: Mon, 20 Jul 2020 17:21:33 -0400 Subject: [PATCH 5/5] Fixed some math --- fec/fec/static/js/modules/pac-comparator.js | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/fec/fec/static/js/modules/pac-comparator.js b/fec/fec/static/js/modules/pac-comparator.js index 19094e6f6b..668d7aa53a 100644 --- a/fec/fec/static/js/modules/pac-comparator.js +++ b/fec/fec/static/js/modules/pac-comparator.js @@ -802,14 +802,16 @@ new Vue({ toReturn.operExpendStr = ''; toReturn.operExpendPct = 0; toReturn.operExpendPctStr = ''; - if (d.operating_expenditures || d.operating_expenditures === 0) { - toReturn.operExpend = d.operating_expenditures; + if ( + d.other_fed_operating_expenditures || + d.other_fed_operating_expenditures === 0 + ) { + toReturn.operExpend = d.other_fed_operating_expenditures; toReturn.operExpendStr = this.formatAsCurrency( - d.operating_expenditures, + d.other_fed_operating_expenditures, true ); - toReturn.operExpendPct = - d.operating_expenditures / toReturn.totDisburse; + toReturn.operExpendPct = toReturn.operExpend / toReturn.totDisburse; toReturn.operExpendPctStr = this.percentString(toReturn.operExpendPct); } @@ -819,10 +821,10 @@ new Vue({ toReturn.contribsPct = 0; toReturn.contribsPctStr = ''; if ( - (d.contributions && d.contribution_refunds) || - (d.contributions === 0 && d.contribution_refunds === 0) + d.fed_candidate_committee_contributions || + d.fed_candidate_committee_contributions === 0 ) { - toReturn.contribs = d.contributions - d.contribution_refunds; + toReturn.contribs = d.fed_candidate_committee_contributions; toReturn.contribsStr = this.formatAsCurrency(toReturn.contribs, true); toReturn.contribsPct = toReturn.contribs / toReturn.totDisburse; toReturn.contribsPctStr = this.percentString(toReturn.contribsPct); @@ -847,19 +849,13 @@ new Vue({ toReturn.otherSpendingPctStr = ''; // To make the code shorter, here's a list of var names included with otherSpending let otherSpend = [ + 'contribution_refunds', 'coordinated_expenditures_by_party_committee', 'fed_election_activity', - 'other_disbursements', - 'total_transfers', - 'loan_repayments_made', - 'loan_repayments_received', - 'loans_and_loan_repayments_made', - 'loans_and_loan_repayments_received', + 'loan_repayments', 'loans_made', - 'refunded_individual_contributions', - 'refunded_other_political_committee_contributions', - 'refunded_political_party_committee_contributions', - 'refunds_relating_convention_exp' + 'other_disbursements', + 'transfers_to_affiliated_committee' ]; otherSpend.forEach(value => { if (d[value]) toReturn.otherSpending += d[value];
Independent expenditures - - - {{ pacs[0].calc.indExpendPctStr }} - {{ pacs[0].calc.indExpendStr }} - - {{ pacs[1].calc.indExpendPctStr }} - {{ pacs[1].calc.indExpendStr }} - Expenditure typePercentage of total expendituresTotals
All other spending    
{{ pacs[0].calc[root + 'Label'] }} - + - {{ pacs[0].calc.otherSpendingPctStr }} - {{ pacs[0].calc.otherSpendingStr }} + {{ pacs[0].calc[root + 'PctStr'] }} + {{ pacs[0].calc[root + 'Str'] }} - {{ pacs[1].calc.otherSpendingPctStr }} - {{ pacs[1].calc.otherSpendingStr }} + {{ pacs[1].calc[root + 'PctStr'] }} + {{ pacs[1].calc[root + 'Str'] }}