From ca53d16e09d456da7dd2e015f107207b9e7cb21c Mon Sep 17 00:00:00 2001 From: rfultz Date: Mon, 10 Feb 2020 09:26:57 -0500 Subject: [PATCH 01/26] WIP --- fec/data/templates/pres-finance-map.jinja | 42 + .../templates/widgets/pres-finance-map.jinja | 64 ++ fec/data/urls.py | 2 + fec/data/views.py | 27 +- fec/fec/static/js/polyfills.js | 23 + .../static/js/widgets/pres-finance-map-box.js | 1006 +++++++++++++++++ .../static/scss/widgets/pres-finance-map.scss | 470 ++++++++ fec/webpack.config.js | 3 +- 8 files changed, 1635 insertions(+), 2 deletions(-) create mode 100644 fec/data/templates/pres-finance-map.jinja create mode 100644 fec/data/templates/widgets/pres-finance-map.jinja create mode 100644 fec/fec/static/js/widgets/pres-finance-map-box.js create mode 100644 fec/fec/static/scss/widgets/pres-finance-map.scss diff --git a/fec/data/templates/pres-finance-map.jinja b/fec/data/templates/pres-finance-map.jinja new file mode 100644 index 0000000000..01645c5c6a --- /dev/null +++ b/fec/data/templates/pres-finance-map.jinja @@ -0,0 +1,42 @@ +{% extends 'layouts/main.jinja' %} +{# {% import 'macros/cycle-select.jinja' as select %} #} +{% import 'macros/page-header.jinja' as header %} + +{% block title %}{{ title }}{% endblock %} + +{% block css %} + + +{% endblock %} + +{% block body %} + {{ header.header(title) }} +
+
+
+

Section title?

+
+
+ +
+
+
+ {% include './widgets/pres-finance-map.jinja' %} +
+
+
+
+{% endblock %} + +{% block scripts %} +{# Load jQuery from the CDN #} + +{# +{% endblock %} diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja new file mode 100644 index 0000000000..f41f0983d9 --- /dev/null +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -0,0 +1,64 @@ +{% import "macros/widgets.jinja" as widgets %} + +
+
+
+
+ Candidates running in: +
+
+
+
+ View as: + + +
+
+
+
+
+
+ + + + + + + + + + + + +
CandidateTotal raised
loading…
+
+
+
+ +
+
+

 

+

 

+

 

+
+
+ By state, total amount received + +
+
+
The map
+
+ +
+ \ No newline at end of file diff --git a/fec/data/urls.py b/fec/data/urls.py index 661fa12ec0..58b51db0f9 100644 --- a/fec/data/urls.py +++ b/fec/data/urls.py @@ -16,6 +16,8 @@ url(r'^data/raising-bythenumbers/$', views.raising), url(r'^data/spending-bythenumbers/$', views.spending), + # Presidential Campaign Finance Map + url(r'^data/candidates/president/presidential-map/$', views.pres_finance_map), # Feedback Tool url(r'^data/issue/reaction/$', views.reactionFeedback), diff --git a/fec/data/views.py b/fec/data/views.py index 72e1bcac37..2f5a69907a 100644 --- a/fec/data/views.py +++ b/fec/data/views.py @@ -633,7 +633,6 @@ def elections(request, office, cycle, state=None, district=None): }, ) - def raising(request): office = request.GET.get("office", "P") @@ -681,6 +680,32 @@ def spending(request): ) + +def pres_finance_map(request): + # office = request.GET.get("office", "P") + + election_year = int( + request.GET.get("election_year", constants.DEFAULT_ELECTION_YEAR) + ) + + # max_election_year = utils.current_cycle() + 4 + # election_years = utils.get_cycles(max_election_year) + + return render( + request, + "pres-finance-map.jinja", + { + "parent": "data", + "title": "Presidential Campaign Finance Map", + # "election_years": election_years, + "election_year": election_year, + # "office": "P", + "social_image_identifier": "data", + "page_specific_css": "/static/css/widgets/pres-finance-map.css", + + }, + ) + def feedback(request): if request.method == "POST": diff --git a/fec/fec/static/js/polyfills.js b/fec/fec/static/js/polyfills.js index 9450e75f0e..d3d0a59941 100644 --- a/fec/fec/static/js/polyfills.js +++ b/fec/fec/static/js/polyfills.js @@ -66,3 +66,26 @@ import 'element-remove'; * Used for reporting-dates for non-standard css selectors in IE */ import 'css.escape'; + +/** + * Adds CustomEvent capabilities to IE < 11 + */ +(function() { + if (typeof window.CustomEvent === 'function') return false; + + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent( + event, + params.bubbles, + params.cancelable, + params.detail + ); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(); diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js new file mode 100644 index 0000000000..54710fe377 --- /dev/null +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -0,0 +1,1006 @@ +'use strict'; + +/* global CustomEvent */ + +/** + * @fileoverview Controls all functionality inside the Where Contributions Come From widget + * in cooperation with data-map + * @copyright 2019 Federal Election Commission + * @license CC0-1.0 + * @owner fec.gov + * @version 1.0 + * TODO: For v2 or whatever, convert to datatable.net (start with the simpliest implementation; no columns.js, etc.) + */ + +// Editable vars +const stylesheetPath = '/static/css/widgets/pres-finance-map.css'; +// // const breakpointToXS = 0; // retaining just in case +const breakpointToSmall = 430; +const breakpointToMedium = 675; +const breakpointToLarge = 700; +const breakpointToXL = 860; +const availElectionYears = [2020, 2016]; // defaults to [0] +const specialCandidateIDs = ['P00000001', 'P00000002', 'P00000003']; +// const rootPathToIndividualContributions = +// '/data/receipts/individual-contributions/'; + +import { buildUrl } from '../modules/helpers'; +// import { defaultElectionYear } from './widget-vars'; +import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; + +const DataMap = require('../modules/data-map').DataMap; +const AbortController = window.AbortController; + +// Custom event names +const EVENT_APP_ID = 'gov.fec.presFinMap'; +const YEAR_CHANGE_EVENT = EVENT_APP_ID + '_yearChange'; +const ENTER_LOADING_EVENT = EVENT_APP_ID + '_loading'; +const FINISH_LOADING_EVENT = EVENT_APP_ID + '_loaded'; +const CHANGE_CANDIDATES_DATA = EVENT_APP_ID + '_candidates_change'; +const CHANGE_CANDIDATE = EVENT_APP_ID + '_candidate_change'; + +/** + * Formats the given value and puts it into the dom element. + * @param {Number} passedValue The number to format and plug into the element + * @param {Boolean} roundToWhole Should we round the cents or no? + * @returns {String} A string of the given value formatted with a dollar sign, commas, and (if roundToWhole === false) decimal + */ +function formatAsCurrency(passedValue, abbreviateMillions) { + let toReturn = passedValue; + if (abbreviateMillions) toReturn = (passedValue / 1000000).toFixed(1); + else toReturn = Math.round(passedValue.round); + return '$' + toReturn.toLocaleString(); +} + +/** + * Builds the link/url to a filtered Individual Contributions page/list + * @param {Number} cycle The candidate's election year + * @param {String} office 'H', 'P', or 'S' + * @param {Array} committeeIDs An array of strings of the candidate's committees + * @param {String} stateID Optional. A null value will not filter for any state but show entries for the entire country + * @returns {String} URL or empty string depending on + */ +function buildIndividualContributionsUrl( + cycle, + office, + committeeIDs, + stateID, + candidateState +) { + // If we're missing required params, just return '' and be done + // if (!cycle || !office || !committeeIDs) return ''; + // let transactionPeriodsString = 'two_year_transaction_period=' + cycle; + // // TODO: Do we need maxDate and minDate? + // // let maxDate = `12-13-${this.baseStatesQuery.cycle}`; + // // let minDate = `01-01-${this.baseStatesQuery.cycle - 1}`; + // let committeesString = ''; + // // The API currently wants a two_year_transaction_period value for each set of two years + // // so we'll add the previous two-year period for presidential races + // // + // // Also, Puerto Rico's House elections are for four years so we'll need to + // // add the previous two-year period to the query string for House candidates from Puerto Rico + // if (office == 'P' || (office == 'H' && candidateState == 'PR')) { + // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 2); + // // and the two earlier two-year periods for Senate races + // } else if (office == 'S') { + // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 2); + // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 4); + // } + // for (let i = 0; i < committeeIDs.length; i++) { + // committeesString += '&committee_id=' + committeeIDs[i]; + // } + // let stateString = stateID ? '&contributor_state=' + stateID : ''; + // let toReturn = + // rootPathToIndividualContributions + + // '?' + + // transactionPeriodsString + + // stateString + + // committeesString; + // // TODO: Do we need maxDate and minDate? + // // `&min_date=${minDate}&max_date=${maxDate}` + + // return toReturn; +} + +/** + * @constructor + */ +function PresidentialFundsMap() { + // Get ready to abort a fetch if we need to + this.fetchAbortController = new AbortController(); + this.fetchAbortSignal = this.fetchAbortController.signal; + + // // Where to find individual candidate details + this.basePath_candidatesPath = [ + 'presidential', + 'contributions', + 'by_candidate' + ]; + + // // Where to find individual candidate details + // this.basePath_candidateCommitteesPath = [ + // 'candidate', + // '000', // candidate ID + // 'committees', + // 'history', + // 2020 // election year / cycle + // ]; + // // Where to find candidate's coverage dates + // this.basePath_candidateCoverageDatesPath = [ + // 'candidate', + // '000', //candidate ID + // 'totals' + // ]; + // // Where to find the highest-earning candidates: + // this.basePath_highestRaising = ['candidates', 'totals']; + // // Where to find the list of states: + // this.basePath_states = [ + // 'schedules', + // 'schedule_a', + // 'by_state', + // 'by_candidate' + // ]; + // // Where to find the states list grand total: + // this.basePath_statesTotal = [ + // 'schedules', + // 'schedule_a', + // 'by_state', + // 'by_candidate', + // 'totals' + // ]; + this.data_candidates; + // // Details about the candidate. Comes from the typeahead + // this.candidateDetails = {}; + // // Information retruned by API candidate committees API {@see loadCandidateCommitteeDetails} + // this.data_candidateCommittees = {}; + // // Init the list/table of states and their totals + // this.data_states = { + // results: [ + // { + // candidate_id: '', + // count: 0, + // cycle: 2020, + // state: '', + // state_full: '', + // total: 0 + // } + // ] + // }; + // // Shared settings for every fetch(): + this.fetchInitObj = { + cache: 'no-cache', + mode: 'cors', + signal: null + }; + // this.fetchingStates = false; // Are we waiting for data? + this.element = document.querySelector('#gov-fec-pres-finance'); // The visual element associated with this, this.instance + // this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID + this.current_electionYear = availElectionYears[0]; + this.current_electionState = 'US'; + this.current_candidate_id = ''; + // this.map; // Starts as the element for the map but then becomes a DataMap object + // this.table; // The for the list of states and their totals + // this.statesTotalHolder; // Element at the bottom of the states list + // this.typeahead; // The typeahead candidate element: + // this.typeahead_revertValue; // Temporary var saved while user is typing + // this.yearControl; // The changes? + this.yearControl = this.element.querySelector('#filter-year'); + let theFieldset = this.yearControl.querySelector('fieldset'); + for (let i = 0; i < availElectionYears.length; i++) { + let thisYear = availElectionYears[i]; + let newElem = document.createElement('label'); + let switched = i == 0 ? ' checked' : ''; + newElem.setAttribute('class', `toggle`); + newElem.setAttribute('for', `switcher-${thisYear}`); + newElem.innerHTML = `${thisYear}`; + theFieldset.appendChild(newElem); + } + this.yearControl.addEventListener( + 'change', + this.handleElectionYearChange.bind(this) + ); + + this.element.addEventListener( + PresidentialFundsMap.YEAR_CHANGE_EVENT, + this.handleYearChange.bind(this) + ); + // // Initialize the various queries + // this.baseCandidateQuery = {}; // Calls for candidate details + this.baseCandidatesQuery = { + // cycle: defaultElectionYear(), + // election_full: true, + // office: 'P', + // page: 1, + // per_page: 200, + // sort_hide_null: false, + // sort_null_only: false, + // sort_nulls_last: false + // candidate_id: '', // 'P60007168', + // is_active_candidate: true, + // sort: 'total' + }; + + // // Find the visual elements + // this.map = document.querySelector('.map-wrapper .election-map'); + // this.candidateDetailsHolder = document.querySelector('.candidate-details'); + this.table = document.querySelector('#pres-fin-map-candidates-table'); + // this.statesTotalHolder = document.querySelector('.js-states-total'); + + // // Fire up the map + // this.map = new DataMap(this.map, { + // color: '#36BDBB', + // data: '', + // addLegend: true, + // addTooltips: true + // }); + + // // Listen for the Browse Individual Contributions button to be clicked + // this.buttonIndivContribs = this.element.querySelector( + // '.js-browse-indiv-contribs-by-state' + // ); + // this.buttonIndivContribs.addEventListener( + // 'click', + // this.updateBrowseIndivContribsButton.bind(this) + // ); + + // Internet Explorer doesn't like flex display + // so we're going to keep the states table from switching to flex. + let userAgent = window.navigator.userAgent; + // Test for IE and IE 11 + let is_ie = + userAgent.indexOf('MSIE ') > 0 || userAgent.indexOf('Trident/7.0') > 0; + + // // Initialize the remote table header + // // Find the remote header and save it + // this.remoteTableHeader = this.element.querySelector( + // '.js-remote-table-header' + // ); + // // Save its for a few lines + // let theRemoteTableHead = this.remoteTableHeader.querySelector('thead'); + // // Look at the data-for attribute of remoteTableHeader and save that element + // this.remoteTable = this.element.querySelector( + // '#' + this.remoteTableHeader.getAttribute('data-for') + // ); + // // Remember the in remoteTable for few lines + // let theRemotedTableHead = this.remoteTable.querySelector('thead'); + // // If we have both elements, we're ready to manipulate them + // if (theRemoteTableHead && theRemotedTableHead) { + // this.remoteTableHeader.style.display = 'table'; + // theRemotedTableHead.style.display = 'none'; + // } + + if (is_ie) { + this.remoteTable.classList.add('table-display'); + this.remoteTableHeader.classList.add('table-display'); + } + + this.element.addEventListener( + CHANGE_CANDIDATES_DATA, + this.handleCandidatesDataLoad.bind(this) + ); + this.element.addEventListener( + CHANGE_CANDIDATE, + this.handleCandidateChange.bind(this) + ); + + // Listen for resize events + window.addEventListener('resize', this.handleResize.bind(this)); + // Call for a resize on init + this.handleResize(); + + // And start the first load + this.loadCandidatesList(); +}; + +/** + * Called by {@see init() , @see handleTypeaheadSelect() } + * Finds the highest-earning presidential candidate of the default year + * Similar to {@see loadCandidateDetails() } + */ +PresidentialFundsMap.prototype.loadCandidatesList = function() { + let newEvent = new CustomEvent(PresidentialFundsMap.ENTER_LOADING_EVENT); + document.dispatchEvent(newEvent); + + // sort_hide_null=false + // &page=1 + // &sort_nulls_last=false + // &sort_null_only=false + // &election_year=2020 + // &per_page=20 + + let instance = this; + let candidatesListQuery = Object.assign({}, this.baseCandidatesQuery, { + sort: '-net_receipts', + per_page: 100, + election_year: this.current_electionYear, + contributor_state: this.current_electionState + // sort_hide_null: false + }); + window + .fetch( + buildUrl(this.basePath_candidatesPath, candidatesListQuery), + this.fetchInitObj + ) + .then(function(response) { + if (response.status !== 200) + throw new Error('The network rejected the candidate raising request.'); + // else if (response.type == 'cors') throw new Error('CORS error'); + response.json().then(data => { + // Save the candidate query reply + // and the candidate details specifically + // instance.candidateDetails = data.results[0]; + // Update the candidate_id for the main query + // instance.baseStatesQuery.candidate_id = + // instance.candidateDetails.candidate_id; + // Update the office to the main query, too. + // instance.baseStatesQuery.office = instance.candidateDetails.office; + // Put the new candidate information on the page + instance.element.dispatchEvent( + new CustomEvent(CHANGE_CANDIDATES_DATA, { detail: data }) + ); + // instance.displayUpdatedData_candidate(); + }); + }) + .catch(function() {}); +}; + +/** + * TODO - + */ +PresidentialFundsMap.prototype.handleCandidatesDataLoad = function(e) { + this.data_candidates = e.detail; + this.displayUpdatedData_candidates(this.data_candidates.results); +}; + +/** + * Retrieves full candidate details when the typeahead is used + * Called from {@see handleTypeaheadSelect() } + * Similar to {@see loadCandidatesList() } + * @param {String} cand_id Comes from the typeahead + */ +PresidentialFundsMap.prototype.loadCandidateDetails = function(cand_id) { + // let instance = this; + // this.basePath_candidatePath[1] = cand_id; + // window + // .fetch( + // buildUrl(this.basePath_candidatePath, this.baseCandidateQuery), + // this.fetchInitObj + // ) + // .then(function(response) { + // if (response.status !== 200) + // throw new Error('The network rejected the candidate details request.'); + // // else if (response.type == 'cors') throw new Error('CORS error'); + // response.json().then(data => { + // // Save the candidate query response + // instance.data_candidate = data; + // // Save the candidate details + // instance.candidateDetails = data.results[0]; + // // Update the base query with the new candidate ID + // instance.baseStatesQuery.candidate_id = + // instance.candidateDetails.candidate_id; + // // Save the office to the base query, too + // instance.baseStatesQuery.office = instance.candidateDetails.office; + // // Then put the new candidate details into the page + // instance.displayUpdatedData_candidate(); + // }); + // }) + // .catch(function() {}); +}; + +/** + * Queries the API for the candidate's coverage dates for the currently-selected election + * Called by {@see displayUpdatedData_candidate() } and {@see displayUpdatedData_candidates() } + */ +PresidentialFundsMap.prototype.loadCandidateCoverageDates = function() { + // let instance = this; + // this.basePath_candidateCoverageDatesPath[1] = this.candidateDetails.candidate_id; + // let coverageDatesQuery = Object.assign( + // {}, + // { + // per_page: 100, + // cycle: this.baseStatesQuery.cycle, + // election_full: true + // } + // ); + // /** + // * Format the dates into MM/DD/YYYY format. + // * Pads single digits with leading 0. + // */ + // var formatDate = function(date) { + // // Adds one since js month uses zero based index + // let month = date.getMonth() + 1; + // if (month < 10) { + // month = '0' + month; + // } + // let day = date.getDate(); + // if (day < 10) { + // day = '0' + day; + // } + // return month + '/' + day + '/' + date.getFullYear(); + // }; + // let theFetchUrl = buildUrl( + // instance.basePath_candidateCoverageDatesPath, + // coverageDatesQuery + // ); + // window + // .fetch(theFetchUrl, instance.fetchInitObj) + // .then(function(response) { + // if (response.status !== 200) + // throw new Error('The network rejected the coverage dates request.'); + // // else if (response.type == 'cors') throw new Error('CORS error'); + // response.json().then(data => { + // if (data.results.length === 1) { + // document + // .querySelector('.states-table-timestamp') + // .removeAttribute('style'); + // // Parse coverage date from API that is formatted like this: 2019-06-30T00:00:00+00:00 + // // into a string without timezone + // let coverage_start_date = new Date( + // data.results[0].coverage_start_date.substring(0, 19) + // ); + // let coverage_end_date = new Date( + // data.results[0].transaction_coverage_date.substring(0, 19) + // ); + // // Remember the in-page elements + // let theStartTimeElement = document.querySelector( + // '.js-cycle-start-time' + // ); + // let theEndTimeElement = document.querySelector('.js-cycle-end-time'); + // // Format the date and put it into the start time + // theStartTimeElement.innerText = formatDate(coverage_start_date); + // // Format the date and put it into the end time + // theEndTimeElement.innerText = formatDate(coverage_end_date); + // } else { + // // Hide coverage dates display when there are zero results + // document + // .querySelector('.states-table-timestamp') + // .setAttribute('style', 'opacity: 0;'); + // } + // }); + // }) + // .catch(function() {}); +}; + +/** + * Asks the API for the details of the candidate's committees for the currently-selected election + * Called by {@see displayUpdatedData_candidate() } + */ +PresidentialFundsMap.prototype.loadCandidateCommitteeDetails = function() { + // let instance = this; + // // Before we fetch, make sure the query path has the current candidate id + // this.basePath_candidateCommitteesPath[1] = this.candidateDetails.candidate_id; + // // and the current election year/cycle + // this.basePath_candidateCommitteesPath[4] = this.baseStatesQuery.cycle; + // let committeesQuery = Object.assign( + // {}, + // { + // per_page: 100, + // election_full: true + // } + // ); + // let theFetchUrl = buildUrl( + // instance.basePath_candidateCommitteesPath, + // committeesQuery + // ); + // // because the API wants two `designation` values, and that's a violation of key:value law, + // // we'll add them ourselves: + // theFetchUrl += '&designation=P&designation=A'; + // window + // .fetch(theFetchUrl, instance.fetchInitObj) + // .then(function(response) { + // if (response.status !== 200) + // throw new Error( + // 'The network rejected the candidate committee details request.' + // ); + // // else if (response.type == 'cors') throw new Error('CORS error'); + // response.json().then(data => { + // // Save the candidate committees query response for when we build links later + // instance.data_candidateCommittees = data; + // // Now that we have the committee info, load the new states data + // instance.loadStatesData(); + // }); + // }) + // .catch(function() { + // // TODO: handle catch. Maybe we remove the links if the committee data didn't load? + // }); +}; + +/** + * Starts the fetch to go get the big batch of states data, called by {@see init() } + */ +PresidentialFundsMap.prototype.loadStatesData = function() { + // let instance = this; + // let baseStatesQueryWithCandidate = Object.assign({}, this.baseStatesQuery, { + // candidate_id: this.candidateDetails.candidate_id + // }); + // // Let's stop any currently-running states fetches + // if (this.fetchingStates) this.fetchAbortController.abort(); + // // Start loading the states data + // this.fetchingStates = true; + // this.setLoadingState(true); + // window + // .fetch( + // buildUrl(this.basePath_states, baseStatesQueryWithCandidate), + // this.fetchInitObj + // ) + // .then(function(response) { + // instance.fetchingStates = false; + // if (response.status !== 200) + // throw new Error('The network rejected the states request.'); + // // else if (response.type == 'cors') throw new Error('CORS error'); + // response.json().then(data => { + // // Now that we have all of the values, let's sort them by total, descending + // data.results.sort((a, b) => { + // return b.total - a.total; + // }); + // // After they're sorted, let's hang on to them + // instance.data_states = data; + // instance.displayUpdatedData_candidates(); + // }); + // }) + // .catch(function() { + // instance.fetchingStates = false; + // }); + // // Start loading the states total + // window + // .fetch( + // buildUrl(this.basePath_statesTotal, baseStatesQueryWithCandidate), + // this.fetchInitObj + // ) + // .then(function(response) { + // if (response.status !== 200) + // throw new Error('The network rejected the states total request.'); + // // else if (response.type == 'cors') throw new Error('CORS error'); + // response.json().then(data => { + // instance.displayUpdatedData_total(data); + // }); + // }) + // .catch(function() {}); + // logUsage(this.baseStatesQuery.candidate_id, this.baseStatesQuery.cycle); +}; + +/** + * Puts the candidate details in the page, + * then loads the states data with {@see loadStatesData() } + */ +PresidentialFundsMap.prototype.displayUpdatedData_candidate = function() { + // If this is the first load, the typeahead won't have a value; let's set it + // let theTypeahead = document.querySelector('#contribs-by-state-cand'); + // if (!theTypeahead.value) theTypeahead.value = this.candidateDetails.name; + // // …their desired office during this election… + // let candidateOfficeHolder = this.candidateDetailsHolder.querySelector('h2'); + // let theOfficeName = this.candidateDetails.office_full; + // candidateOfficeHolder.innerText = `Candidate for ${ + // theOfficeName == 'President' ? theOfficeName.toLowerCase() : theOfficeName + // }`; + // // …and their candidate ID for this office + // let candidateIdHolder = this.candidateDetailsHolder.querySelector('h3'); + // candidateIdHolder.innerText = 'ID: ' + this.candidateDetails.candidate_id; + // // Update the + // validElectionYears.sort((a, b) => b - a); + // // Remember what year's election we're currently showing (will help if we were switching between candidates of the same year) + // let previousElectionYear = this.yearControl.value; + // // Otherwise we'll show the most recent election of these options + // let nextElectionYear = validElectionYears[0]; + // // validElectionYears.includes(previousElectionYear) wasn't working so let's go through the validElectionYears + // // and stick with previousElectionYear if it's a valid year for this candidate + // for (let i = 0; i < validElectionYears.length; i++) { + // if (previousElectionYear == validElectionYears[i]) { + // nextElectionYear = previousElectionYear; + // break; + // } + // } + // // Build the `; + // } + // // Put the new options into the `; + // Total raised cell + newRowContent += ''; + theNewRow.innerHTML = newRowContent; + theTableBody.appendChild(theNewRow); + theNewRow.addEventListener( + 'click', + this.handleCandidateListClick.bind(this) + ); + } + // theTableBody.innerHTML = theTbodyString; + } + // Update candidate's coverage dates above the states list + // this.loadCandidateCoverageDates(); + // Update the Individual Contributions button/link at the bottom + // this.updateBrowseIndivContribsButton(); + // Let the map know that the data has been updated + // this.map.handleDataRefresh(theData); + // Clear the classes and reset functionality so the tool is usable again + // this.setLoadingState(false); +}; + +/** + * Puts the states grand total into the total field at the bottom of the table + * Called by its fetch inside {@see loadStatesData() } + * @param {Object} data The results from the fetch + */ +PresidentialFundsMap.prototype.displayUpdatedData_total = function(data) { + // Set the states total dollars to the number we received, or empty it if there are no results + // this.statesTotalHolder.innerText = + // data.results.length > 0 + // ? (this.statesTotalHolder.innerText = formatAsCurrency( + // data.results[0].total + // )) + // : ''; + // let statesHolder = this.element.querySelector('.states-total'); + // if (data.results.length > 0) statesHolder.setAttribute('style', ''); + // else statesHolder.setAttribute('style', 'opacity: 0;'); +}; + +/** + * + */ +PresidentialFundsMap.prototype.updateBreadcrumbs = function() { + console.log('updateBreadcrumbs()'); + let theHolder = this.element.querySelector('.breadcrumb-nav'); + let theSeparator = theHolder.querySelector('span'); + let theSecondItem = theHolder.querySelectorAll('a')[1]; + let theSecondLabel = ''; + + if ( + this.current_candidate_id == specialCandidateIDs[0] && + this.current_electionState == 'US' + ) { + // If we're showing the US map and 'All' candidates, + // TODO - done, let's hide the span and the second element + } else if (this.current_electionState == 'US') { + // Or if we're showing the US map and not-'All' candidates + theSecondLabel = 'Nationwide: '; + } else { + // Otherwise, we're showing a state so we need a state lookup + // TODO: theSecondLabel = (lookup the state name for this.current_electionState) + theSecondLabel = 'State: '; + } + + if (theSecondLabel != '') { + if (specialCandidateIDs.includes(this.current_candidate_id)) { + // If we're looking at a special candidate (Dems, Reps ('all' is hidden from above)) + // TODO: theSecondLabel += this.candidate_last_name? + theSecondLabel += 'party name here'; + } else { + // We're dealing with a real candidate so we need to get the name from somewhere else + // TODO: theSecondLabel += this.find the last name + theSecondLabel += 'candidate name here'; + } + } + theSecondItem.style.display = theSecondLabel != '' ? 'block' : 'none'; + theSeparator.style.display = theSecondLabel != '' ? 'block' : 'none'; + theSecondItem.innerHTML = theSecondLabel; +}; + +/** + * + */ +PresidentialFundsMap.prototype.handleYearChange = function(e) { + console.log('handleYearChange(): ', e); + this.current_electionYear = e.detail; + this.loadCandidatesList(); +}; + +/** + * + */ +PresidentialFundsMap.prototype.handleCandidateListClick = function(e) { + console.log('handleCandidateListClick(): ', e); + let newCandidateId = e.target.dataset.candidate_id; + if (newCandidateId != this.current_candidate_id) { + this.current_candidate_id = newCandidateId; + this.element.dispatchEvent( + new CustomEvent(CHANGE_CANDIDATE, { detail: newCandidateId }) + ); + } +}; + +PresidentialFundsMap.prototype.handleCandidateChange = function(e) { + console.log('handleCandidateChange(): ', e); + this.updateBreadcrumbs(); + // TODO: this should trigger the map to load change to the candidate's data (or, 'US') +}; + +// Set the candidate's name and link change +PresidentialFundsMap.prototype.setCandidateName = function( + id, + candidateName, + party, + cycle +) { + // let candidateNameElement = this.candidateDetailsHolder.querySelector('h1'); + // candidateNameElement.innerHTML = `${candidateName} [${party}]`; +}; + +/** + * Called on the election year control's change event + * Starts loading the new data + * @param {Event} e + */ +PresidentialFundsMap.prototype.handleElectionYearChange = function(e) { + console.log('handleElectionYearChange() e: ', e); + // this.baseStatesQuery.cycle = this.yearControl.value; + // // Update candidate name and link + // this.setCandidateName( + // this.candidateDetails.candidate_id, + // this.candidateDetails.name, + // this.candidateDetails.party, + // this.baseStatesQuery.cycle + // ); + let yearChangeEvent = new CustomEvent( + PresidentialFundsMap.YEAR_CHANGE_EVENT, + { detail: e.target.value } + ); + this.element.dispatchEvent(yearChangeEvent); + + // // We don't need to load the candidate details for a year change, + // // so we'll just jump right to loading the committees data for the newly-chosen year. + // this.loadCandidateCommitteeDetails(); +}; + +/** + * Called from throughout the widget + * @param {String} errorCode + */ +PresidentialFundsMap.prototype.handleErrorState = function(errorCode) { + // if (errorCode == 'NO_RESULTS_TO_DISPLAY') { + // // Empty the states list and update the date range + // let theStatesTableBody = this.table.querySelector('tbody'); + // let theDateRange = this.baseStatesQuery.cycle; + // if (this.baseStatesQuery.office == 'P') + // theDateRange = theDateRange - 3 + '-' + theDateRange; + // else if (this.baseStatesQuery.office == 'S') + // theDateRange = theDateRange - 5 + '-' + theDateRange; + // else theDateRange = theDateRange - 1 + '-' + theDateRange; + // let theErrorMessageHTML = ``; + // theStatesTableBody.innerHTML = theErrorMessageHTML; + // } +}; + +/** + * Updates the href of the Individual Contributions link/button at the bottom of the widget + */ +PresidentialFundsMap.prototype.updateBrowseIndivContribsButton = function() { + // We need to go through the committee results and build an array of the committee IDs + // to send to {@see buildIndividualContributionsUrl() } + // let theCommittees = this.data_candidateCommittees.results; + // let theCommitteeIDs = []; + // for (let i = 0; i < theCommittees.length; i++) { + // theCommitteeIDs.push(theCommittees[i].committee_id); + // } + // let theButton = this.element.querySelector( + // '.js-browse-indiv-contribs-by-state' + // ); + // theButton.setAttribute( + // 'href', + // buildIndividualContributionsUrl( + // this.baseStatesQuery.cycle, + // this.baseStatesQuery.office, + // theCommitteeIDs + // ) + // ); +}; + +/** + * Listens to window resize events and adjusts the classes for the handling them + + // &:first-child { + // flex-grow: 0; + // flex-shrink: 0; + // width: 2em; + // } + &:last-child { + flex-grow: 0; + flex-shrink: 0; + width: 100px; + } + &:only-child { + width: 100%; + } + } + th { + font-weight: $fontWeightSemibold; + + &:last-child { + flex-grow: 1; + flex-shrink: 1; + padding-right: #{$fontSizeBase * 2}; + } + } + } + .map-wrapper { + width: 100%; + border-bottom: solid 2px $colorBorder; + border-top: solid 2px $colorBorder; + padding: $fontSizeBase 0; + + .election-map { + border: none; + height: auto; + min-height: 30rem; + + svg { + min-width: 360px; + min-height: 230px; + } + } + } + .map-details { + display: flex; + justify-content: space-between; + + .candidate-details { + margin-bottom: 2rem; + padding: 0 $fontSizeBase; + + h1 { + font-family: $fontFamilySansSerif; + font-size: $fontSizeBase * 2; + font-weight: $fontWeightRegular; + line-height: 1.25em; + margin: 0 0 .25em 0; + + a { + line-height: 1.25em; + } + } + h2, h3 { + font-family: $fontFamilySansSerif; + font-size: $fontSizeBase * 1.4; + font-weight: $fontWeightRegular; + line-height: 1.25em; + margin: 0 0 .25em 0; + } + } + .legend-container { + text-align: right; + + span { + text-align: right; + line-height: 1.25em; + margin-bottom: .25em; + } + } + } + .right-column-wrapper { + padding: 1rem; + text-align: right; + width: 100%; + + a, button { + margin: 0 0 1rem 1rem; + } + } + .overlay__container { + display: none; + left: 0px; + position: absolute; + width: 100%; + z-index: #{$z1 - 1}; // The typeahead menu is at 100 and we want it to be visible above this + + &.is-loading { + display: block; + } + .overlay { + display: none; + + &.is-loading { + background-image: url('../../img/loading.gif'); + display: block; + } + } + } + + // Small sizes + &.w-s { + // Leaving this here and empty so others know it's an option + $deleteThis: true; + } + // Medium sizes + &.w-m { + @extend .w-s !optional; + display: flex; + flex-wrap: wrap; + + .controls-wrapper { + width: 100%; + + .typeahead-filter { + display: inline-block; + float: left; + height: 36px; + margin-bottom: 2rem; + margin-right: 1rem; + max-width: none; + right: 0; + position: relative; + + label { + float: left; + height: 36px; + line-height: 36px; + margin-right: .5em; + position: relative; + } + .filter__typeahead { + float: left; + height: 36px; + width: 23rem; + position: relative; + } + .filter__instructions { + padding-left: 21rem; + position: absolute; + top: 36px; + } + #contribs-by-state-typeahead-error { + background-color: transparent; + line-height: 1.1em; + max-width: none; + padding-left: 210px; + top: 55px; + } + &.is-error { + #contribs-by-state-typeahead-error { + height: auto; + } + } + } + + fieldset { + display: inline-block; + float: left; + + &:first-child { + margin-right: 7.5pt; + } + } + + label { + display: inline-block;// + } + } + .candidate-list-wrapper { + margin-right: 1rem; + width: calc(33.33% - 1rem); + + .remote-table-header { + margin-bottom: 0.5px; // Make sure the border is visible above the scrollbar below it + width: calc(100% + 1px); // align the right side with the parent's right boundary + } + .table-scroller { + height: 100%; + max-height: 400px; + + table { + min-height: 400px; + } + } + td, th { + &:nth-child(2) { + max-width: 90px; + } + } + tr { + + &.selected { + background: $colorTableTdBorder; + border-left: 4px; + font-weight: $fontWeightBold; + + } + &:hover { + background: $colorTableTdBorder; + } + } + } + .map-wrapper { + margin-top: 2.5rem; + margin-left: 1rem; + width: calc(66.66% - 1rem); + + .election-map { + svg { + min-width: 445px; + min-height: 285px; + } + } + } + .map-details { + .more-info-wrapper { + a, button { + margin: 0 0 1rem 1rem; + } + } + } + } + // Large sizes + &.w-l { + @extend .w-m; + } + + // Some overrides for table.table-display *, which was added because IE doesn't like Flex + table.table-display { + display: table; + + thead { + display: table-header-group; + } + tbody { + display: table-row-group; + } + tr { + display: table-row; + } + td, th { + display: table-cell; + } + } +} + +/* #map-tooltip is outside .contribs-by-state */ +#map-tooltip { + border-radius: 4px; + border: 2px solid #112e51; + background-color: #fff; + color: #112e51; + font-family: karla, sans-serif; + padding: 1.5rem; + position: absolute; + text-align: center; + z-index: #{$z1 - 2}; + + .tooltip__title { + border-bottom: 1px solid #112e51; + text-transform: uppercase; + font-weight: 700; + } + + &.tooltip--above { + min-width: 12rem; + left: -4rem; + bottom: calc(100% + 1.5rem); + } + &::before { + @include triangle(2rem, $primary, down); + bottom: -1rem; + content: ''; + display: block; + left: calc(50% - 1rem); + position: absolute; + } + + &::after { + @include triangle(1.6rem, $inverse, down); + bottom: -.7rem; + content: ''; + display: block; + left: calc(50% - .8rem); + position: absolute; + } +} diff --git a/fec/webpack.config.js b/fec/webpack.config.js index 460dedf8a6..ba3cc2bb24 100644 --- a/fec/webpack.config.js +++ b/fec/webpack.config.js @@ -141,7 +141,8 @@ module.exports = [ 'contributions-by-state': './fec/static/js/widgets/contributions-by-state.js', 'contributions-by-state-box': - './fec/static/js/widgets/contributions-by-state-box.js' + './fec/static/js/widgets/contributions-by-state-box.js', + 'pres-finance-map-box': './fec/static/js/widgets/pres-finance-map-box.js' }, output: { filename: 'widgets/[name].js', From 84f2f60c7c4ed55e6da085289b2bd5a5aa8a65ad Mon Sep 17 00:00:00 2001 From: rfultz Date: Mon, 10 Feb 2020 09:54:13 -0500 Subject: [PATCH 02/26] Fix currency formatting --- .../static/js/widgets/pres-finance-map-box.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 54710fe377..562c8ec653 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -47,9 +47,20 @@ const CHANGE_CANDIDATE = EVENT_APP_ID + '_candidate_change'; */ function formatAsCurrency(passedValue, abbreviateMillions) { let toReturn = passedValue; - if (abbreviateMillions) toReturn = (passedValue / 1000000).toFixed(1); - else toReturn = Math.round(passedValue.round); - return '$' + toReturn.toLocaleString(); + + if (abbreviateMillions) { + // There's an issue when adding commas to the currency when there's a decimal + // So we're going to break it apart, add commas, then put it back together + toReturn = (passedValue / 1000000).toFixed(1); + let commaPos = toReturn.indexOf('.'); + let firstHalf = toReturn.substr(0, commaPos); + let secondHalf = toReturn.substr(commaPos); // grabs the decimal, too + toReturn = firstHalf.replace(/\d(?=(\d{3})+$)/g, '$&,') + secondHalf; + } else { + toReturn = Math.round(passedValue.round).replace(/\d(?=(\d{3})+$)/g, '$&,'); + } + + return '$' + toReturn; } /** From b6366226b1ee998fec2b3db6c704265a9ad4758a Mon Sep 17 00:00:00 2001 From: rfultz Date: Tue, 11 Feb 2020 14:18:10 -0500 Subject: [PATCH 03/26] WIP --- .../templates/widgets/pres-finance-map.jinja | 11 +- .../static/js/widgets/pres-finance-map-box.js | 324 +++++++++--------- 2 files changed, 165 insertions(+), 170 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index f41f0983d9..fc5dcc72d7 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -18,6 +18,10 @@ +
+ + +
@@ -45,7 +49,7 @@
-

 

+

Candidate Name 

 

 

@@ -57,8 +61,9 @@
The map
\ No newline at end of file diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 562c8ec653..196715d423 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -3,18 +3,16 @@ /* global CustomEvent */ /** - * @fileoverview Controls all functionality inside the Where Contributions Come From widget - * in cooperation with data-map - * @copyright 2019 Federal Election Commission + * TODO - @fileoverview + * @copyright 2020 Federal Election Commission * @license CC0-1.0 * @owner fec.gov * @version 1.0 - * TODO: For v2 or whatever, convert to datatable.net (start with the simpliest implementation; no columns.js, etc.) */ // Editable vars const stylesheetPath = '/static/css/widgets/pres-finance-map.css'; -// // const breakpointToXS = 0; // retaining just in case +// const breakpointToXS = 0; // retaining just in case const breakpointToSmall = 430; const breakpointToMedium = 675; const breakpointToLarge = 700; @@ -38,6 +36,7 @@ const ENTER_LOADING_EVENT = EVENT_APP_ID + '_loading'; const FINISH_LOADING_EVENT = EVENT_APP_ID + '_loaded'; const CHANGE_CANDIDATES_DATA = EVENT_APP_ID + '_candidates_change'; const CHANGE_CANDIDATE = EVENT_APP_ID + '_candidate_change'; +const CANDIDATE_DETAILS_LOADED = EVENT_APP_ID + '_cand_detail_loaded'; /** * Formats the given value and puts it into the dom element. @@ -120,13 +119,16 @@ function PresidentialFundsMap() { this.fetchAbortController = new AbortController(); this.fetchAbortSignal = this.fetchAbortController.signal; - // // Where to find individual candidate details - this.basePath_candidatesPath = [ + // Where to find the list of candidates + this.basePath_candidatesList = [ 'presidential', 'contributions', 'by_candidate' ]; + // Where to find individual candidate details + this.basePath_candidateDetails = ['candidate']; + // // Where to find individual candidate details // this.basePath_candidateCommitteesPath = [ // 'candidate', @@ -160,7 +162,7 @@ function PresidentialFundsMap() { // ]; this.data_candidates; // // Details about the candidate. Comes from the typeahead - // this.candidateDetails = {}; + this.data_candidate; // // Information retruned by API candidate committees API {@see loadCandidateCommitteeDetails} // this.data_candidateCommittees = {}; // // Init the list/table of states and their totals @@ -176,7 +178,7 @@ function PresidentialFundsMap() { // } // ] // }; - // // Shared settings for every fetch(): + // Shared settings for every fetch(): this.fetchInitObj = { cache: 'no-cache', mode: 'cors', @@ -184,10 +186,13 @@ function PresidentialFundsMap() { }; // this.fetchingStates = false; // Are we waiting for data? this.element = document.querySelector('#gov-fec-pres-finance'); // The visual element associated with this, this.instance - // this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID + this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID this.current_electionYear = availElectionYears[0]; this.current_electionState = 'US'; + this.current_electionState_name = 'United States'; this.current_candidate_id = ''; + this.current_candidate_name = ''; + this.current_candidate_last_name = ''; // this.map; // Starts as the element for the map but then becomes a DataMap object // this.table; // The
${results[i].candidate_last_name}`; + if (!specialCandidateIDs.includes(results[i].candidate_id)) { + newRowContent += ` [${results[i].candidate_party_affiliation}]`; + } + newRowContent += `'; + newRowContent += formatAsCurrency( + results[i].net_receipts, + this.current_electionState == 'US' + ); + newRowContent += '
We don't have itemized individual contributions for this candidate for ${theDateRange}.
for the list of states and their totals // this.statesTotalHolder; // Element at the bottom of the states list @@ -245,11 +250,16 @@ PresidentialFundsMap.prototype.init = function() { ); this.element.addEventListener( - PresidentialFundsMap.YEAR_CHANGE_EVENT, + YEAR_CHANGE_EVENT, this.handleYearChange.bind(this) ); + + this.element.addEventListener( + CANDIDATE_DETAILS_LOADED, + this.handleCandidateDetailsLoaded.bind(this) + ); // // Initialize the various queries - // this.baseCandidateQuery = {}; // Calls for candidate details + this.baseCandidateQuery = { office: 'P' }; // Calls for candidate details this.baseCandidatesQuery = { // cycle: defaultElectionYear(), // election_full: true, @@ -266,7 +276,7 @@ PresidentialFundsMap.prototype.init = function() { // // Find the visual elements // this.map = document.querySelector('.map-wrapper .election-map'); - // this.candidateDetailsHolder = document.querySelector('.candidate-details'); + this.candidateDetailsHolder = document.querySelector('.candidate-details'); this.table = document.querySelector('#pres-fin-map-candidates-table'); // this.statesTotalHolder = document.querySelector('.js-states-total'); @@ -337,20 +347,12 @@ PresidentialFundsMap.prototype.init = function() { }; /** - * Called by {@see init() , @see handleTypeaheadSelect() } + * Called by {@see init() } * Finds the highest-earning presidential candidate of the default year * Similar to {@see loadCandidateDetails() } */ PresidentialFundsMap.prototype.loadCandidatesList = function() { - let newEvent = new CustomEvent(PresidentialFundsMap.ENTER_LOADING_EVENT); - document.dispatchEvent(newEvent); - - // sort_hide_null=false - // &page=1 - // &sort_nulls_last=false - // &sort_null_only=false - // &election_year=2020 - // &per_page=20 + document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); let instance = this; let candidatesListQuery = Object.assign({}, this.baseCandidatesQuery, { @@ -358,11 +360,10 @@ PresidentialFundsMap.prototype.loadCandidatesList = function() { per_page: 100, election_year: this.current_electionYear, contributor_state: this.current_electionState - // sort_hide_null: false }); window .fetch( - buildUrl(this.basePath_candidatesPath, candidatesListQuery), + buildUrl(this.basePath_candidatesList, candidatesListQuery), this.fetchInitObj ) .then(function(response) { @@ -370,19 +371,9 @@ PresidentialFundsMap.prototype.loadCandidatesList = function() { throw new Error('The network rejected the candidate raising request.'); // else if (response.type == 'cors') throw new Error('CORS error'); response.json().then(data => { - // Save the candidate query reply - // and the candidate details specifically - // instance.candidateDetails = data.results[0]; - // Update the candidate_id for the main query - // instance.baseStatesQuery.candidate_id = - // instance.candidateDetails.candidate_id; - // Update the office to the main query, too. - // instance.baseStatesQuery.office = instance.candidateDetails.office; - // Put the new candidate information on the page instance.element.dispatchEvent( new CustomEvent(CHANGE_CANDIDATES_DATA, { detail: data }) ); - // instance.displayUpdatedData_candidate(); }); }) .catch(function() {}); @@ -393,42 +384,70 @@ PresidentialFundsMap.prototype.loadCandidatesList = function() { */ PresidentialFundsMap.prototype.handleCandidatesDataLoad = function(e) { this.data_candidates = e.detail; + + this.element.dispatchEvent(new CustomEvent(FINISH_LOADING_EVENT)); + this.displayUpdatedData_candidates(this.data_candidates.results); }; +/** + * + */ +PresidentialFundsMap.prototype.handleCandidateDetailsLoaded = function(e) { + console.log('handleCandidateDetailsLoaded(): ', e); + + let dataObj = { + candidate_id: e.detail.candidate_id, + name: e.detail.name, + party: e.detail.party, + year: this.current_electionYear, + currentState: this.current_electionState, // for breadcrumbs + currentStateName: this.current_electionState_name, // for breadcrumbs + candidateLastName: this.current_candidate_last_name // for breadcrumbs + }; + + this.displayUpdatedData_candidate(dataObj); + this.updateBreadcrumbs(dataObj); +}; + /** * Retrieves full candidate details when the typeahead is used - * Called from {@see handleTypeaheadSelect() } + * Called from * Similar to {@see loadCandidatesList() } * @param {String} cand_id Comes from the typeahead */ PresidentialFundsMap.prototype.loadCandidateDetails = function(cand_id) { - // let instance = this; - // this.basePath_candidatePath[1] = cand_id; - // window - // .fetch( - // buildUrl(this.basePath_candidatePath, this.baseCandidateQuery), - // this.fetchInitObj - // ) - // .then(function(response) { - // if (response.status !== 200) - // throw new Error('The network rejected the candidate details request.'); - // // else if (response.type == 'cors') throw new Error('CORS error'); - // response.json().then(data => { - // // Save the candidate query response - // instance.data_candidate = data; - // // Save the candidate details - // instance.candidateDetails = data.results[0]; - // // Update the base query with the new candidate ID - // instance.baseStatesQuery.candidate_id = - // instance.candidateDetails.candidate_id; - // // Save the office to the base query, too - // instance.baseStatesQuery.office = instance.candidateDetails.office; - // // Then put the new candidate details into the page - // instance.displayUpdatedData_candidate(); - // }); - // }) - // .catch(function() {}); + console.log('loadCandidateDetails(): ', cand_id); + let instance = this; + this.basePath_candidateDetails[1] = cand_id; + window + .fetch( + buildUrl(this.basePath_candidateDetails, this.baseCandidateQuery), + this.fetchInitObj + ) + .then(function(response) { + if (response.status !== 200) + throw new Error('The network rejected the candidate details request.'); + // else if (response.type == 'cors') throw new Error('CORS error'); + response.json().then(data => { + // Save the candidate query response + instance.data_candidate = data; + console.log('candidate details loaded: ', data); + // Save the candidate details + // instance.candidateDetails = data.results[0]; + // Update the base query with the new candidate ID + // instance.baseStatesQuery.candidate_id = + // instance.candidateDetails.candidate_id; + // Save the office to the base query, too + // instance.baseStatesQuery.office = instance.candidateDetails.office; + // Then put the new candidate details into the page + // instance.displayUpdatedData_candidate(); + instance.element.dispatchEvent( + new CustomEvent(CANDIDATE_DETAILS_LOADED, { detail: data.results[0] }) + ); + }); + }) + .catch(function() {}); }; /** @@ -605,75 +624,29 @@ PresidentialFundsMap.prototype.loadStatesData = function() { /** * Puts the candidate details in the page, - * then loads the states data with {@see loadStatesData() } */ -PresidentialFundsMap.prototype.displayUpdatedData_candidate = function() { - // If this is the first load, the typeahead won't have a value; let's set it - // let theTypeahead = document.querySelector('#contribs-by-state-cand'); - // if (!theTypeahead.value) theTypeahead.value = this.candidateDetails.name; - // // …their desired office during this election… - // let candidateOfficeHolder = this.candidateDetailsHolder.querySelector('h2'); - // let theOfficeName = this.candidateDetails.office_full; - // candidateOfficeHolder.innerText = `Candidate for ${ - // theOfficeName == 'President' ? theOfficeName.toLowerCase() : theOfficeName - // }`; - // // …and their candidate ID for this office - // let candidateIdHolder = this.candidateDetailsHolder.querySelector('h3'); - // candidateIdHolder.innerText = 'ID: ' + this.candidateDetails.candidate_id; - // // Update the - // validElectionYears.sort((a, b) => b - a); - // // Remember what year's election we're currently showing (will help if we were switching between candidates of the same year) - // let previousElectionYear = this.yearControl.value; - // // Otherwise we'll show the most recent election of these options - // let nextElectionYear = validElectionYears[0]; - // // validElectionYears.includes(previousElectionYear) wasn't working so let's go through the validElectionYears - // // and stick with previousElectionYear if it's a valid year for this candidate - // for (let i = 0; i < validElectionYears.length; i++) { - // if (previousElectionYear == validElectionYears[i]) { - // nextElectionYear = previousElectionYear; - // break; - // } - // } - // // Build the `; - // } - // // Put the new options into the - + From d9885f0170618035e1958e574b388508949da082 Mon Sep 17 00:00:00 2001 From: rfultz Date: Wed, 12 Feb 2020 19:06:22 -0500 Subject: [PATCH 09/26] Renaming Cash on Hand to match the new PR --- fec/data/templates/widgets/pres-finance-map.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index 5d8ac20a4c..c0debb3b8b 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -73,7 +73,7 @@ - +
${results[i].candidate_last_name}`; if (!specialCandidateIDs.includes(results[i].candidate_id)) { @@ -764,7 +742,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_total = function(data) { /** * */ -PresidentialFundsMap.prototype.updateBreadcrumbs = function() { +PresidentialFundsMap.prototype.updateBreadcrumbs = function(dataObj) { console.log('updateBreadcrumbs()'); let theHolder = this.element.querySelector('.breadcrumb-nav'); let theSeparator = theHolder.querySelector('span'); @@ -772,29 +750,29 @@ PresidentialFundsMap.prototype.updateBreadcrumbs = function() { let theSecondLabel = ''; if ( - this.current_candidate_id == specialCandidateIDs[0] && - this.current_electionState == 'US' + dataObj.candidate_id == specialCandidateIDs[0] && + dataObj.currentState == 'US' ) { // If we're showing the US map and 'All' candidates, // TODO - done, let's hide the span and the second element - } else if (this.current_electionState == 'US') { + } else if (dataObj.currentState == 'US') { // Or if we're showing the US map and not-'All' candidates theSecondLabel = 'Nationwide: '; } else { // Otherwise, we're showing a state so we need a state lookup // TODO: theSecondLabel = (lookup the state name for this.current_electionState) - theSecondLabel = 'State: '; + theSecondLabel = `${dataObj.current_candidate_name}: `; } if (theSecondLabel != '') { - if (specialCandidateIDs.includes(this.current_candidate_id)) { + if (specialCandidateIDs.includes(dataObj.candidate_id)) { // If we're looking at a special candidate (Dems, Reps ('all' is hidden from above)) // TODO: theSecondLabel += this.candidate_last_name? - theSecondLabel += 'party name here'; + theSecondLabel += dataObj.name; } else { // We're dealing with a real candidate so we need to get the name from somewhere else // TODO: theSecondLabel += this.find the last name - theSecondLabel += 'candidate name here'; + theSecondLabel += dataObj.candidateLastName; } } theSecondItem.style.display = theSecondLabel != '' ? 'block' : 'none'; @@ -816,52 +794,64 @@ PresidentialFundsMap.prototype.handleYearChange = function(e) { */ PresidentialFundsMap.prototype.handleCandidateListClick = function(e) { console.log('handleCandidateListClick(): ', e); - let newCandidateId = e.target.dataset.candidate_id; - if (newCandidateId != this.current_candidate_id) { - this.current_candidate_id = newCandidateId; + let newCandidateID = e.target.dataset.candidate_id; + let name = e.target.cells[0].innerText; + if (newCandidateID != this.current_candidate_id) { + this.current_candidate_id = newCandidateID; + this.current_candidate_last_name = name.substr(0, name.indexOf(' [')); this.element.dispatchEvent( - new CustomEvent(CHANGE_CANDIDATE, { detail: newCandidateId }) + new CustomEvent(CHANGE_CANDIDATE, { + detail: { candidate_id: newCandidateID, name: name } + }) ); } }; PresidentialFundsMap.prototype.handleCandidateChange = function(e) { console.log('handleCandidateChange(): ', e); - this.updateBreadcrumbs(); - // TODO: this should trigger the map to load change to the candidate's data (or, 'US') -}; -// Set the candidate's name and link change -PresidentialFundsMap.prototype.setCandidateName = function( - id, - candidateName, - party, - cycle -) { - // let candidateNameElement = this.candidateDetailsHolder.querySelector('h1'); - // candidateNameElement.innerHTML = `${candidateName} [${party}]`; + // We only need to load candidate details if our new candidate_id is an individual + if (specialCandidateIDs.includes(e.detail.candidate_id)) { + // Otherwise, dispatch the event now + + this.element.dispatchEvent( + new CustomEvent(CANDIDATE_DETAILS_LOADED, { + detail: { + candidate_id: e.detail.candidate_id, + name: e.detail.name // Sending this as name because that's how the candidate details query returns it + } + }) + ); + } else { + this.loadCandidateDetails(e.detail.candidate_id); + } + + // TODO: this should trigger the map to change to the candidate's data (or, 'US') }; +// // Set the candidate's name and link change +// PresidentialFundsMap.prototype.setCandidateName = function( +// id, +// candidateName, +// party, +// cycle +// ) { +// let candidateNameElement = this.candidateDetailsHolder.querySelector('h1'); +// // candidateNameElement.innerHTML = `${candidateName} [${party}]`; +// }; + /** * Called on the election year control's change event * Starts loading the new data - * @param {Event} e + * @param {MouseEvent} e */ PresidentialFundsMap.prototype.handleElectionYearChange = function(e) { console.log('handleElectionYearChange() e: ', e); - // this.baseStatesQuery.cycle = this.yearControl.value; - // // Update candidate name and link - // this.setCandidateName( - // this.candidateDetails.candidate_id, - // this.candidateDetails.name, - // this.candidateDetails.party, - // this.baseStatesQuery.cycle - // ); - let yearChangeEvent = new CustomEvent( - PresidentialFundsMap.YEAR_CHANGE_EVENT, - { detail: e.target.value } + this.element.dispatchEvent( + new CustomEvent(YEAR_CHANGE_EVENT, { + detail: e.target.value + }) ); - this.element.dispatchEvent(yearChangeEvent); // // We don't need to load the candidate details for a year change, // // so we'll just jump right to loading the committees data for the newly-chosen year. From 1cdb043795079c221963a75f01fe8d4a4b7baf89 Mon Sep 17 00:00:00 2001 From: rfultz Date: Tue, 11 Feb 2020 23:37:27 -0500 Subject: [PATCH 04/26] Getting the summary information in, sorting --- .../templates/widgets/pres-finance-map.jinja | 66 +- fec/fec/static/js/modules/data-map.js | 29 +- .../static/js/widgets/pres-finance-map-box.js | 583 +++++++++--------- 3 files changed, 397 insertions(+), 281 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index fc5dcc72d7..936844d453 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -44,7 +44,7 @@
@@ -61,9 +61,67 @@
The map
-
- Browse TODO stuff - +
    +
  • + + NAME, CANDIDATE [PAR] +
    + + + + + + + +
    Receipts$000
    Disbursements$000
    Cash-on-handNEED THIS $000
    Debts owed by committee$000
    +
    +
  • +
  • + +
    + NAME, CANDIDATE [PAR] + + + + + + + + + + + + + + + + +
    Contributions from
      Individuals$000
      PACs$000
      Parties$000
      Candidates$000
    Federal fundsNEED THIS $000
    Transfers-in$000
    Contributions by size
    $200 and under$000
    $200.01-$499$000
    $500-$999$000
    $1000-$1,999$000
    $2000 and over$000
    + +
    +
  • +
  • + +
    + + + + + + + + + + + + + +
    Operating Expenditures$000
    Transfers to other authorized$000
    Fundraising expenditures$000
    Exempt legal and accounting disbursements$000
    Loan repayments
      Candidate$000
      Other$000
    Other disbursements$000
    Offsets to expenditures$000
    Contribution refunds$000
    + +
    +
  • +
+ {# Browse TODO stuff #}
\ No newline at end of file diff --git a/fec/fec/static/js/modules/data-map.js b/fec/fec/static/js/modules/data-map.js index cfbedb99f5..fe9250e765 100644 --- a/fec/fec/static/js/modules/data-map.js +++ b/fec/fec/static/js/modules/data-map.js @@ -37,7 +37,8 @@ const compactRules = [['B', 9], ['M', 6], ['K', 3], ['', 0]]; let defaultOpts = { colorScale: ['#e2ffff', '#278887'], colorZero: '#ffffff', - quantiles: 4 + quantiles: 4, + eventAppID: '' }; /** @@ -51,10 +52,12 @@ let defaultOpts = { * @param {String} opts.colorZero - hex color code to use when no value is present */ function DataMap(elm, opts) { + console.log('new DataMap(): ', elm, opts); // Data, vars this.data; this.mapData; // saves results from init() and applyNewData(), formatted like {1: 123456789, 2: 6548, 4: 91835247} / {stateID: stateValue, stateID: stateValue} this.opts = Object.assign({}, defaultOpts, opts); + this.eventAppID = this.opts.eventAppID; // Elements this.elm = elm; @@ -158,6 +161,20 @@ DataMap.prototype.init = function() { if (this.opts.addTooltips) { buildStateTooltips(this.svg, path, this); } + + console.log('map this: ', this); + console.log(' this.parent: ', this.parent); + document.addEventListener( + 'START_MAP_REFRESH', + this.handleParentRefreshEvent.bind(this) + ); +}; + +/** + * + */ +DataMap.prototype.handleParentRefreshEvent = function(e) { + console.log('THE MAP HEARD ITS PARENT!:', e); }; /** @@ -177,6 +194,7 @@ DataMap.prototype.getStateValue = function(pathID) { * @param {json} newData */ DataMap.prototype.handleDataRefresh = function(newData) { + console.log('handleDataRefresh(): ', newData); this.data = newData; if (!this.svg) this.init(); @@ -428,6 +446,15 @@ function buildStateTooltips(svg, path, instance) { } else { tooltip.style('display', 'none'); } + }) + .on('click', function(d) { + this.dispatchEvent( + new CustomEvent('STATE_CLICKED', { + detail: fips.fipsByCode[d.id].STUSAB, + bubbles: true + }) + ); + console.log('clicked a state!'); }); // Add the mouseleave listeners to the dom elements rather than relying on d3 diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 196715d423..e1cd6d5a67 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -35,8 +35,11 @@ const YEAR_CHANGE_EVENT = EVENT_APP_ID + '_yearChange'; const ENTER_LOADING_EVENT = EVENT_APP_ID + '_loading'; const FINISH_LOADING_EVENT = EVENT_APP_ID + '_loaded'; const CHANGE_CANDIDATES_DATA = EVENT_APP_ID + '_candidates_change'; -const CHANGE_CANDIDATE = EVENT_APP_ID + '_candidate_change'; +const CHANGE_CANDIDATE = EVENT_APP_ID + '_cand_change'; const CANDIDATE_DETAILS_LOADED = EVENT_APP_ID + '_cand_detail_loaded'; +const MAP_DATA_LOADED = EVENT_APP_ID + '_map_data_loaded'; +const FINANCIAL_SUMMARY_LOADED = EVENT_APP_ID + '_cand_summary_loaded'; +const CONTRIBUTION_SIZES_LOADED = EVENT_APP_ID + '_cand_sizes_loaded'; /** * Formats the given value and puts it into the dom element. @@ -51,9 +54,9 @@ function formatAsCurrency(passedValue, abbreviateMillions) { // There's an issue when adding commas to the currency when there's a decimal // So we're going to break it apart, add commas, then put it back together toReturn = (passedValue / 1000000).toFixed(1); - let commaPos = toReturn.indexOf('.'); - let firstHalf = toReturn.substr(0, commaPos); - let secondHalf = toReturn.substr(commaPos); // grabs the decimal, too + let decimalPos = toReturn.indexOf('.'); + let firstHalf = toReturn.substr(0, decimalPos); + let secondHalf = toReturn.substr(decimalPos); // grabs the decimal, too toReturn = firstHalf.replace(/\d(?=(\d{3})+$)/g, '$&,') + secondHalf; } else { toReturn = Math.round(passedValue.round).replace(/\d(?=(\d{3})+$)/g, '$&,'); @@ -70,45 +73,13 @@ function formatAsCurrency(passedValue, abbreviateMillions) { * @param {String} stateID Optional. A null value will not filter for any state but show entries for the entire country * @returns {String} URL or empty string depending on */ -function buildIndividualContributionsUrl( - cycle, - office, - committeeIDs, - stateID, - candidateState +function buildCandidateNameAndPartyLink( + candidateID, + candidateName, + electionYear, + party ) { - // If we're missing required params, just return '' and be done - // if (!cycle || !office || !committeeIDs) return ''; - // let transactionPeriodsString = 'two_year_transaction_period=' + cycle; - // // TODO: Do we need maxDate and minDate? - // // let maxDate = `12-13-${this.baseStatesQuery.cycle}`; - // // let minDate = `01-01-${this.baseStatesQuery.cycle - 1}`; - // let committeesString = ''; - // // The API currently wants a two_year_transaction_period value for each set of two years - // // so we'll add the previous two-year period for presidential races - // // - // // Also, Puerto Rico's House elections are for four years so we'll need to - // // add the previous two-year period to the query string for House candidates from Puerto Rico - // if (office == 'P' || (office == 'H' && candidateState == 'PR')) { - // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 2); - // // and the two earlier two-year periods for Senate races - // } else if (office == 'S') { - // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 2); - // transactionPeriodsString += '&two_year_transaction_period=' + (cycle - 4); - // } - // for (let i = 0; i < committeeIDs.length; i++) { - // committeesString += '&committee_id=' + committeeIDs[i]; - // } - // let stateString = stateID ? '&contributor_state=' + stateID : ''; - // let toReturn = - // rootPathToIndividualContributions + - // '?' + - // transactionPeriodsString + - // stateString + - // committeesString; - // // TODO: Do we need maxDate and minDate? - // // `&min_date=${minDate}&max_date=${maxDate}` + - // return toReturn; + return `${candidateName} [${party}]`; } /** @@ -129,14 +100,9 @@ function PresidentialFundsMap() { // Where to find individual candidate details this.basePath_candidateDetails = ['candidate']; - // // Where to find individual candidate details - // this.basePath_candidateCommitteesPath = [ - // 'candidate', - // '000', // candidate ID - // 'committees', - // 'history', - // 2020 // election year / cycle - // ]; + // Where to find + this.basePath_financialSummary = ['presidential', 'financial_summary']; + // // Where to find candidate's coverage dates // this.basePath_candidateCoverageDatesPath = [ // 'candidate', @@ -145,13 +111,8 @@ function PresidentialFundsMap() { // ]; // // Where to find the highest-earning candidates: // this.basePath_highestRaising = ['candidates', 'totals']; - // // Where to find the list of states: - // this.basePath_states = [ - // 'schedules', - // 'schedule_a', - // 'by_state', - // 'by_candidate' - // ]; + // Where to find the data for the map + this.basePath_mapData = ['presidential', 'contributions', 'by_state']; // // Where to find the states list grand total: // this.basePath_statesTotal = [ // 'schedules', @@ -163,6 +124,7 @@ function PresidentialFundsMap() { this.data_candidates; // // Details about the candidate. Comes from the typeahead this.data_candidate; + this.data_map; // // Information retruned by API candidate committees API {@see loadCandidateCommitteeDetails} // this.data_candidateCommittees = {}; // // Init the list/table of states and their totals @@ -184,16 +146,16 @@ function PresidentialFundsMap() { mode: 'cors', signal: null }; - // this.fetchingStates = false; // Are we waiting for data? + this.fetchingData = false; // Are we waiting for data? this.element = document.querySelector('#gov-fec-pres-finance'); // The visual element associated with this, this.instance this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID this.current_electionYear = availElectionYears[0]; this.current_electionState = 'US'; - this.current_electionState_name = 'United States'; - this.current_candidate_id = ''; - this.current_candidate_name = ''; - this.current_candidate_last_name = ''; - // this.map; // Starts as the element for the map but then becomes a DataMap object + this.current_electionStateName = 'United States'; + this.current_candidateID = specialCandidateIDs[0]; + this.current_candidateName = ''; + this.current_candidateLastName = ''; + this.map; // Starts as the element for the map but then becomes a DataMap object // this.table; // The for the list of states and their totals // this.statesTotalHolder; // Element at the bottom of the states list // this.typeahead; // The typeahead candidate element: @@ -258,35 +220,52 @@ PresidentialFundsMap.prototype.init = function() { CANDIDATE_DETAILS_LOADED, this.handleCandidateDetailsLoaded.bind(this) ); - // // Initialize the various queries - this.baseCandidateQuery = { office: 'P' }; // Calls for candidate details - this.baseCandidatesQuery = { - // cycle: defaultElectionYear(), - // election_full: true, - // office: 'P', - // page: 1, - // per_page: 200, - // sort_hide_null: false, - // sort_null_only: false, - // sort_nulls_last: false - // candidate_id: '', // 'P60007168', - // is_active_candidate: true, - // sort: 'total' - }; - // // Find the visual elements - // this.map = document.querySelector('.map-wrapper .election-map'); + this.element.addEventListener( + MAP_DATA_LOADED, + this.handleMapDataLoaded.bind(this) + ); + + this.element.addEventListener( + FINANCIAL_SUMMARY_LOADED, + this.handleFinancialSummaryLoaded.bind(this) + ); + + this.element.addEventListener( + CONTRIBUTION_SIZES_LOADED, + this.handleContributionSizesLoaded.bind(this) + ); + + this.element + .querySelector('.js-reset-app') + .addEventListener('click', this.handleResetClick.bind(this)); + + this.element.addEventListener( + 'STATE_CLICKED', + this.handleStateClick.bind(this) + ); + + // Initialize the various queries + this.baseCandidateQuery = { office: 'P' }; // Calls for candidate details + this.baseCandidatesQuery = { per_page: 100, sort: '-net_receipts' }; + this.baseStatesQuery = { per_page: 100 }; + this.baseMapQuery = { per_page: 100 }; + this.baseSummaryQuery = { per_page: 100 }; // just in case + this.baseSizesQuery = { per_page: 100 }; // just in case + + // Find the visual elements + this.map = document.querySelector('.map-wrapper .election-map'); this.candidateDetailsHolder = document.querySelector('.candidate-details'); this.table = document.querySelector('#pres-fin-map-candidates-table'); - // this.statesTotalHolder = document.querySelector('.js-states-total'); - // // Fire up the map - // this.map = new DataMap(this.map, { - // color: '#36BDBB', - // data: '', - // addLegend: true, - // addTooltips: true - // }); + // Fire up the map + this.map = new DataMap(this.map, { + color: '#36BDBB', + data: '', + addLegend: true, + addTooltips: true, + eventAppID: EVENT_APP_ID + }); // // Listen for the Browse Individual Contributions button to be clicked // this.buttonIndivContribs = this.element.querySelector( @@ -330,7 +309,7 @@ PresidentialFundsMap.prototype.init = function() { this.element.addEventListener( CHANGE_CANDIDATES_DATA, - this.handleCandidatesDataLoad.bind(this) + this.handleCandidatesDataLoaded.bind(this) ); this.element.addEventListener( CHANGE_CANDIDATE, @@ -354,18 +333,18 @@ PresidentialFundsMap.prototype.init = function() { PresidentialFundsMap.prototype.loadCandidatesList = function() { document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); + // Let's stop any currently-running candidates fetches + if (this.fetchingData) this.fetchAbortController.abort(); + // Start loading the candidates + this.fetchingData = true; + let instance = this; - let candidatesListQuery = Object.assign({}, this.baseCandidatesQuery, { - sort: '-net_receipts', - per_page: 100, + let thisQuery = Object.assign({}, this.baseCandidatesQuery, { election_year: this.current_electionYear, contributor_state: this.current_electionState }); window - .fetch( - buildUrl(this.basePath_candidatesList, candidatesListQuery), - this.fetchInitObj - ) + .fetch(buildUrl(this.basePath_candidatesList, thisQuery), this.fetchInitObj) .then(function(response) { if (response.status !== 200) throw new Error('The network rejected the candidate raising request.'); @@ -376,18 +355,47 @@ PresidentialFundsMap.prototype.loadCandidatesList = function() { ); }); }) - .catch(function() {}); + .catch(function() { + instance.fetchingData = false; + }); }; /** * TODO - */ -PresidentialFundsMap.prototype.handleCandidatesDataLoad = function(e) { +PresidentialFundsMap.prototype.handleCandidatesDataLoaded = function(e) { this.data_candidates = e.detail; - this.element.dispatchEvent(new CustomEvent(FINISH_LOADING_EVENT)); + this.fetchingData = false; // clear the abort controller + + // Not going to remove the loading state until we've updated the map data, too + // this.element.dispatchEvent(new CustomEvent(FINISH_LOADING_EVENT)); this.displayUpdatedData_candidates(this.data_candidates.results); + + // Now that we have a new list of candidates, we need to get new numbers for the states + this.loadMapData(); +}; + +/** + * + * TODO: Would like to make this into an event for the map to hear, rather than send the data into the map + */ +PresidentialFundsMap.prototype.handleMapDataLoaded = function(e) { + this.fetchingData = false; // clear the abort controller + + // The map will map 'value' attributes but we have 'net_receipts' + // We also need to change 'contribution_state' to 'state' + // So let's fix that + let dataForMap = Object.assign({}, e.detail); + dataForMap.results.forEach(item => { + item.total = item.contribution_receipt_amount; + item.state = item.contribution_state; + }); + + this.map.handleDataRefresh(dataForMap); + + document.dispatchEvent(new CustomEvent(FINISH_LOADING_EVENT)); }; /** @@ -402,14 +410,57 @@ PresidentialFundsMap.prototype.handleCandidateDetailsLoaded = function(e) { party: e.detail.party, year: this.current_electionYear, currentState: this.current_electionState, // for breadcrumbs - currentStateName: this.current_electionState_name, // for breadcrumbs - candidateLastName: this.current_candidate_last_name // for breadcrumbs + currentStateName: this.current_electionStateName, // for breadcrumbs + candidateLastName: this.current_candidateLastName // for breadcrumbs }; this.displayUpdatedData_candidate(dataObj); this.updateBreadcrumbs(dataObj); }; +ROBERT, DO THESE +/** + * + */ +PresidentialFundsMap.prototype.handleFinancialSummaryLoaded = function(e) { + console.log('handleFinancialSummaryLoaded(): ', e); + + // let dataObj = { + // candidate_id: e.detail.candidate_id, + // name: e.detail.name, + // party: e.detail.party, + // year: this.current_electionYear, + // currentState: this.current_electionState, // for breadcrumbs + // currentStateName: this.current_electionStateName, // for breadcrumbs + // candidateLastName: this.current_candidateLastName // for breadcrumbs + // }; + + // this.displayUpdatedData_candidate(dataObj); + // this.updateBreadcrumbs(dataObj); +}; + +/** + * + */ +PresidentialFundsMap.prototype.handleContributionSizesLoaded = function(e) { + console.log('handleFinancialSummaryLoaded(): ', e); + + // let dataObj = { + // candidate_id: e.detail.candidate_id, + // name: e.detail.name, + // party: e.detail.party, + // year: this.current_electionYear, + // currentState: this.current_electionState, // for breadcrumbs + // currentStateName: this.current_electionStateName, // for breadcrumbs + // candidateLastName: this.current_candidateLastName // for breadcrumbs + // }; + + // this.displayUpdatedData_candidate(dataObj); + // this.updateBreadcrumbs(dataObj); +}; + +/ ROBERT, DO THESE + /** * Retrieves full candidate details when the typeahead is used * Called from @@ -418,6 +469,8 @@ PresidentialFundsMap.prototype.handleCandidateDetailsLoaded = function(e) { */ PresidentialFundsMap.prototype.loadCandidateDetails = function(cand_id) { console.log('loadCandidateDetails(): ', cand_id); + document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); + let instance = this; this.basePath_candidateDetails[1] = cand_id; window @@ -447,179 +500,120 @@ PresidentialFundsMap.prototype.loadCandidateDetails = function(cand_id) { ); }); }) - .catch(function() {}); + .catch(function() { + instance.fetchingData = false; + }); }; /** - * Queries the API for the candidate's coverage dates for the currently-selected election - * Called by {@see displayUpdatedData_candidate() } and {@see displayUpdatedData_candidates() } + * Starts the fetch to go get the values for the states */ -PresidentialFundsMap.prototype.loadCandidateCoverageDates = function() { - // let instance = this; - // this.basePath_candidateCoverageDatesPath[1] = this.candidateDetails.candidate_id; - // let coverageDatesQuery = Object.assign( - // {}, - // { - // per_page: 100, - // cycle: this.baseStatesQuery.cycle, - // election_full: true - // } - // ); - // /** - // * Format the dates into MM/DD/YYYY format. - // * Pads single digits with leading 0. - // */ - // var formatDate = function(date) { - // // Adds one since js month uses zero based index - // let month = date.getMonth() + 1; - // if (month < 10) { - // month = '0' + month; - // } - // let day = date.getDate(); - // if (day < 10) { - // day = '0' + day; - // } - // return month + '/' + day + '/' + date.getFullYear(); - // }; - // let theFetchUrl = buildUrl( - // instance.basePath_candidateCoverageDatesPath, - // coverageDatesQuery - // ); - // window - // .fetch(theFetchUrl, instance.fetchInitObj) - // .then(function(response) { - // if (response.status !== 200) - // throw new Error('The network rejected the coverage dates request.'); - // // else if (response.type == 'cors') throw new Error('CORS error'); - // response.json().then(data => { - // if (data.results.length === 1) { - // document - // .querySelector('.states-table-timestamp') - // .removeAttribute('style'); - // // Parse coverage date from API that is formatted like this: 2019-06-30T00:00:00+00:00 - // // into a string without timezone - // let coverage_start_date = new Date( - // data.results[0].coverage_start_date.substring(0, 19) - // ); - // let coverage_end_date = new Date( - // data.results[0].transaction_coverage_date.substring(0, 19) - // ); - // // Remember the in-page elements - // let theStartTimeElement = document.querySelector( - // '.js-cycle-start-time' - // ); - // let theEndTimeElement = document.querySelector('.js-cycle-end-time'); - // // Format the date and put it into the start time - // theStartTimeElement.innerText = formatDate(coverage_start_date); - // // Format the date and put it into the end time - // theEndTimeElement.innerText = formatDate(coverage_end_date); - // } else { - // // Hide coverage dates display when there are zero results - // document - // .querySelector('.states-table-timestamp') - // .setAttribute('style', 'opacity: 0;'); - // } - // }); - // }) - // .catch(function() {}); +PresidentialFundsMap.prototype.loadMapData = function() { + document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); + + let instance = this; + let thisQuery = Object.assign({}, this.baseMapQuery, { + candidate_id: this.current_candidateID, + election_year: this.current_electionYear + }); + // Let's stop any currently-running map data fetches + if (this.fetchingData) this.fetchAbortController.abort(); + // Start loading the map data + this.fetchingData = true; + window + .fetch(buildUrl(this.basePath_mapData, thisQuery), this.fetchInitObj) + .then(function(response) { + // instance.fetchingStates = false; + if (response.status !== 200) + throw new Error('The network rejected the states request.'); + // else if (response.type == 'cors') throw new Error('CORS error'); + response.json().then(data => { + console.log('map data loaded: ', data); + instance.data_map = data; + instance.element.dispatchEvent( + new CustomEvent(MAP_DATA_LOADED, { + detail: data + }) + ); + }); + }) + .catch(function() { + instance.fetchingData = false; + }); }; /** - * Asks the API for the details of the candidate's committees for the currently-selected election - * Called by {@see displayUpdatedData_candidate() } + * Starts the fetch to go get the values for the candidate's financial summary */ -PresidentialFundsMap.prototype.loadCandidateCommitteeDetails = function() { - // let instance = this; - // // Before we fetch, make sure the query path has the current candidate id - // this.basePath_candidateCommitteesPath[1] = this.candidateDetails.candidate_id; - // // and the current election year/cycle - // this.basePath_candidateCommitteesPath[4] = this.baseStatesQuery.cycle; - // let committeesQuery = Object.assign( - // {}, - // { - // per_page: 100, - // election_full: true - // } - // ); - // let theFetchUrl = buildUrl( - // instance.basePath_candidateCommitteesPath, - // committeesQuery - // ); - // // because the API wants two `designation` values, and that's a violation of key:value law, - // // we'll add them ourselves: - // theFetchUrl += '&designation=P&designation=A'; - // window - // .fetch(theFetchUrl, instance.fetchInitObj) - // .then(function(response) { - // if (response.status !== 200) - // throw new Error( - // 'The network rejected the candidate committee details request.' - // ); - // // else if (response.type == 'cors') throw new Error('CORS error'); - // response.json().then(data => { - // // Save the candidate committees query response for when we build links later - // instance.data_candidateCommittees = data; - // // Now that we have the committee info, load the new states data - // instance.loadStatesData(); - // }); - // }) - // .catch(function() { - // // TODO: handle catch. Maybe we remove the links if the committee data didn't load? - // }); +PresidentialFundsMap.prototype.loadFinancialSummary = function() { + document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); + + let instance = this; + let thisQuery = Object.assign({}, this.baseSummaryQuery, { + candidate_id: this.current_candidateID, + election_year: this.current_electionYear + }); + // Let's stop any currently-running map data fetches + if (this.fetchingData) this.fetchAbortController.abort(); + // Start loading the map data + this.fetchingData = true; + window + .fetch(buildUrl(this.basePath_mapData, thisQuery), this.fetchInitObj) + .then(function(response) { + // instance.fetchingStates = false; + if (response.status !== 200) + throw new Error('The network rejected the states request.'); + // else if (response.type == 'cors') throw new Error('CORS error'); + response.json().then(data => { + console.log('financial summary loaded: ', data); + instance.data_summary = data; + instance.element.dispatchEvent( + new CustomEvent(FINANCIAL_SUMMARY_LOADED, { + detail: data + }) + ); + }); + }) + .catch(function() { + instance.fetchingData = false; + }); }; /** - * Starts the fetch to go get the big batch of states data, called by {@see init() } + * Starts the fetch to go get the values for the contributions by size */ -PresidentialFundsMap.prototype.loadStatesData = function() { - // let instance = this; - // let baseStatesQueryWithCandidate = Object.assign({}, this.baseStatesQuery, { - // candidate_id: this.candidateDetails.candidate_id - // }); - // // Let's stop any currently-running states fetches - // if (this.fetchingStates) this.fetchAbortController.abort(); - // // Start loading the states data - // this.fetchingStates = true; - // this.setLoadingState(true); - // window - // .fetch( - // buildUrl(this.basePath_states, baseStatesQueryWithCandidate), - // this.fetchInitObj - // ) - // .then(function(response) { - // instance.fetchingStates = false; - // if (response.status !== 200) - // throw new Error('The network rejected the states request.'); - // // else if (response.type == 'cors') throw new Error('CORS error'); - // response.json().then(data => { - // // Now that we have all of the values, let's sort them by total, descending - // data.results.sort((a, b) => { - // return b.total - a.total; - // }); - // // After they're sorted, let's hang on to them - // instance.data_states = data; - // instance.displayUpdatedData_candidates(); - // }); - // }) - // .catch(function() { - // instance.fetchingStates = false; - // }); - // // Start loading the states total - // window - // .fetch( - // buildUrl(this.basePath_statesTotal, baseStatesQueryWithCandidate), - // this.fetchInitObj - // ) - // .then(function(response) { - // if (response.status !== 200) - // throw new Error('The network rejected the states total request.'); - // // else if (response.type == 'cors') throw new Error('CORS error'); - // response.json().then(data => { - // instance.displayUpdatedData_total(data); - // }); - // }) - // .catch(function() {}); - // logUsage(this.baseStatesQuery.candidate_id, this.baseStatesQuery.cycle); +PresidentialFundsMap.prototype.loadContributionSizes = function() { + document.dispatchEvent(new CustomEvent(ENTER_LOADING_EVENT)); + + let instance = this; + let thisQuery = Object.assign({}, this.baseSizesQuery, { + candidate_id: this.current_candidateID, + election_year: this.current_electionYear + }); + // Let's stop any currently-running map data fetches + if (this.fetchingData) this.fetchAbortController.abort(); + // Start loading the map data + this.fetchingData = true; + window + .fetch(buildUrl(this.basePath_mapData, thisQuery), this.fetchInitObj) + .then(function(response) { + // instance.fetchingStates = false; + if (response.status !== 200) + throw new Error('The network rejected the sizes request.'); + // else if (response.type == 'cors') throw new Error('CORS error'); + response.json().then(data => { + console.log('sizes loaded: ', data); + instance.data_sizes = data; + instance.element.dispatchEvent( + new CustomEvent(CONTRIBUTION_SIZES_LOADED, { + detail: data + }) + ); + }); + }) + .catch(function() { + instance.fetchingData = false; + }); }; /** @@ -640,7 +634,12 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidate = function(detail) { theNameString = detail.name; theOfficeString = 'for president'; } else { - theNameString = `${detail.name} [${detail.party}]`; + theNameString = buildCandidateNameAndPartyLink( + detail.candidate_id, + detail.name, + this.current_electionYear, + detail.party + ); theOfficeString = 'Candidate for president'; theIDString = `ID: ${detail.candidate_id}`; } @@ -651,8 +650,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidate = function(detail) { /** * Put the list of states and totals into the table - * Called by {@see loadStatesData() } - * TODO: This will eventually be replaced by the datatables functionality + * Called by {@see loadMapData() } */ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( results @@ -674,7 +672,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( // ); // } for (let i = 0; i < results.length; i++) { - // let theStateTotalUrl = buildIndividualContributionsUrl( + // let theStateTotalUrl = buildCandidateNameAndPartyLink( // this.baseStatesQuery.cycle, // this.baseStatesQuery.office, // theCommitteeIDs, @@ -683,7 +681,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( // ); // Candidate name cell let rowClasses = 'TODO-myRowClass'; - if (results[i].candidate_id == this.current_candidate_id) + if (results[i].candidate_id == this.current_candidateID) rowClasses += ' selected'; let theNewRow = document.createElement('tr'); @@ -723,7 +721,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( /** * Puts the states grand total into the total field at the bottom of the table - * Called by its fetch inside {@see loadStatesData() } + * Called by its fetch inside {@see loadMapData() } * @param {Object} data The results from the fetch */ PresidentialFundsMap.prototype.displayUpdatedData_total = function(data) { @@ -761,7 +759,7 @@ PresidentialFundsMap.prototype.updateBreadcrumbs = function(dataObj) { } else { // Otherwise, we're showing a state so we need a state lookup // TODO: theSecondLabel = (lookup the state name for this.current_electionState) - theSecondLabel = `${dataObj.current_candidate_name}: `; + theSecondLabel = `${dataObj.current_candidateName}: `; } if (theSecondLabel != '') { @@ -796,9 +794,9 @@ PresidentialFundsMap.prototype.handleCandidateListClick = function(e) { console.log('handleCandidateListClick(): ', e); let newCandidateID = e.target.dataset.candidate_id; let name = e.target.cells[0].innerText; - if (newCandidateID != this.current_candidate_id) { - this.current_candidate_id = newCandidateID; - this.current_candidate_last_name = name.substr(0, name.indexOf(' [')); + if (newCandidateID != this.current_candidateID) { + this.current_candidateID = newCandidateID; + this.current_candidateLastName = name.substr(0, name.indexOf(' [')); this.element.dispatchEvent( new CustomEvent(CHANGE_CANDIDATE, { detail: { candidate_id: newCandidateID, name: name } @@ -826,7 +824,8 @@ PresidentialFundsMap.prototype.handleCandidateChange = function(e) { this.loadCandidateDetails(e.detail.candidate_id); } - // TODO: this should trigger the map to change to the candidate's data (or, 'US') + // Start the map refresh, too + this.loadMapData(); }; // // Set the candidate's name and link change @@ -858,6 +857,13 @@ PresidentialFundsMap.prototype.handleElectionYearChange = function(e) { // this.loadCandidateCommitteeDetails(); }; +/** + * + */ +PresidentialFundsMap.prototype.handleStateClick = function(e) { + console.log('A STATE WAS CLICKED! ', e); +}; + /** * Called from throughout the widget * @param {String} errorCode @@ -882,7 +888,7 @@ PresidentialFundsMap.prototype.handleErrorState = function(errorCode) { */ PresidentialFundsMap.prototype.updateBrowseIndivContribsButton = function() { // We need to go through the committee results and build an array of the committee IDs - // to send to {@see buildIndividualContributionsUrl() } + // to send to {@see buildCandidateNameAndPartyLink() } // let theCommittees = this.data_candidateCommittees.results; // let theCommitteeIDs = []; // for (let i = 0; i < theCommittees.length; i++) { @@ -893,7 +899,7 @@ PresidentialFundsMap.prototype.updateBrowseIndivContribsButton = function() { // ); // theButton.setAttribute( // 'href', - // buildIndividualContributionsUrl( + // buildCandidateNameAndPartyLink( // this.baseStatesQuery.cycle, // this.baseStatesQuery.office, // theCommitteeIDs @@ -961,9 +967,34 @@ PresidentialFundsMap.prototype.refreshOverlay = function() { // theOverlay.style.height = `${theHeight}px`; }; +PresidentialFundsMap.prototype.handleResetClick = function(e) { + if (e) e.preventDefault(); + + this.current_candidateID = specialCandidateIDs[0]; + this.current_electionState = 'US'; + this.current_electionStateName = ''; + this.current_candidateLastName = ''; + this.current_candidateName = 'All candidates'; + + this.loadCandidatesList(); + + let dataObj = { + candidate_id: this.current_candidateID, + name: this.current_candidateName, + party: '', + year: this.current_electionYear, + currentState: this.current_electionState, // for breadcrumbs + currentStateName: this.current_electionStateName, // for breadcrumbs + candidateLastName: this.current_candidateLastName // for breadcrumbs + }; + + this.displayUpdatedData_candidate(dataObj); + this.updateBreadcrumbs(dataObj); +}; + /** * Controls class names and functionality of the widget. - * Called when we both start and complete (@see loadStatesData() ) + * Called when we both start and complete (@see loadMapData() ) * @param {Boolean} newState */ PresidentialFundsMap.prototype.setLoadingState = function(newState) { From 3d39bb7c528ad0576369ab9d6d637b5e2720da98 Mon Sep 17 00:00:00 2001 From: rfultz Date: Wed, 12 Feb 2020 02:01:54 -0500 Subject: [PATCH 05/26] Summaries are working except the exports --- .../templates/widgets/pres-finance-map.jinja | 9 +- .../static/js/widgets/pres-finance-map-box.js | 230 ++++++++++-------- 2 files changed, 132 insertions(+), 107 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index 936844d453..226b40e391 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -49,7 +49,7 @@
-

Candidate Name 

+

Candidate Name 

 

 

@@ -61,10 +61,10 @@
The map
-
@@ -79,7 +79,7 @@
  • - NAME, CANDIDATE [PAR] +
    NAME, CANDIDATE [PAR]
  • @@ -103,6 +103,7 @@
  • +
    NAME, CANDIDATE [PAR]
  • Contributions from
    diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index e1cd6d5a67..f690bfc66b 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -2,6 +2,8 @@ /* global CustomEvent */ +ROBERT, NEXT UP IS THE VARIOUS EXPORT BUTTONS + /** * TODO - @fileoverview * @copyright 2020 Federal Election Commission @@ -41,6 +43,18 @@ const MAP_DATA_LOADED = EVENT_APP_ID + '_map_data_loaded'; const FINANCIAL_SUMMARY_LOADED = EVENT_APP_ID + '_cand_summary_loaded'; const CONTRIBUTION_SIZES_LOADED = EVENT_APP_ID + '_cand_sizes_loaded'; +// Element selectors +// TODO: Update so we're using IDs everywhere? +const selector_mainElement = '#gov-fec-pres-finance'; +const selector_yearControl = '#filter-year'; +const selector_resetApp = '.js-reset-app'; +const selector_map = '.map-wrapper .election-map'; +const selector_candidateDetails = '.candidate-details'; +const selector_candidatesTable = '#pres-fin-map-candidates-table'; +const selector_breadcrumbNav = '.breadcrumb-nav'; +const selector_summariesHolder = '#financial-summaries'; +const selector_candidateNamePartyAndLink = '.js-cand-name-par-a'; + /** * Formats the given value and puts it into the dom element. * @param {Number} passedValue The number to format and plug into the element @@ -59,7 +73,8 @@ function formatAsCurrency(passedValue, abbreviateMillions) { let secondHalf = toReturn.substr(decimalPos); // grabs the decimal, too toReturn = firstHalf.replace(/\d(?=(\d{3})+$)/g, '$&,') + secondHalf; } else { - toReturn = Math.round(passedValue.round).replace(/\d(?=(\d{3})+$)/g, '$&,'); + toReturn = String(Math.round(passedValue)); + toReturn = toReturn.replace(/\d(?=(\d{3})+$)/g, '$&,'); } return '$' + toReturn; @@ -103,6 +118,11 @@ function PresidentialFundsMap() { // Where to find this.basePath_financialSummary = ['presidential', 'financial_summary']; + this.basePath_contributionSizes = [ + 'presidential', + 'contributions', + 'by_size' + ]; // // Where to find candidate's coverage dates // this.basePath_candidateCoverageDatesPath = [ // 'candidate', @@ -124,6 +144,8 @@ function PresidentialFundsMap() { this.data_candidates; // // Details about the candidate. Comes from the typeahead this.data_candidate; + this.data_summary; + this.data_sizes; this.data_map; // // Information retruned by API candidate committees API {@see loadCandidateCommitteeDetails} // this.data_candidateCommittees = {}; @@ -147,13 +169,13 @@ function PresidentialFundsMap() { signal: null }; this.fetchingData = false; // Are we waiting for data? - this.element = document.querySelector('#gov-fec-pres-finance'); // The visual element associated with this, this.instance + this.element = document.querySelector(selector_mainElement); // The visual element associated with this, this.instance this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID this.current_electionYear = availElectionYears[0]; this.current_electionState = 'US'; this.current_electionStateName = 'United States'; this.current_candidateID = specialCandidateIDs[0]; - this.current_candidateName = ''; + this.current_candidateName = 'All candidates'; this.current_candidateLastName = ''; this.map; // Starts as the element for the map but then becomes a DataMap object // this.table; // The
    Operating Expenditures$000
    for the list of states and their totals @@ -195,7 +217,7 @@ PresidentialFundsMap.prototype.init = function() { // // Init the election year selector (The element ID is set in data/templates/partials/widgets/contributions-by-state.jinja) // // TODO: Can we remove the default listener (like with the typeahead above) and not change the URL when the
    @@ -80,6 +83,7 @@
    NAME, CANDIDATE [PAR]
    +
    coverage date
    @@ -97,13 +101,14 @@
    Contributions from
    $2000 and over$000
    - + Export raising data
  • NAME, CANDIDATE [PAR]
    +
    coverage date
    @@ -118,11 +123,69 @@
    Operating Expenditures$000
    Contribution refunds$000
    - + Export spending data
  • {# Browse TODO stuff #} +
    +
    Export raising data
    + +
    + AL + AK + AZ + AR + CA + CO + CT + DE + DC + FL + GA + HI + ID + IL + IN + IA + KS + KY + LA + ME + MD + MA + MI + MN + MS + MO + MT + NE + NV + NH + NJ + NM + NY + NC + ND + OH + OK + OR + PA + RI + SC + SD + TN + TX + UT + VT + VA + WA + WV + WI + WY + Other +
    +
    - \ No newline at end of file + diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index f690bfc66b..a4ca78fef4 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -2,8 +2,6 @@ /* global CustomEvent */ -ROBERT, NEXT UP IS THE VARIOUS EXPORT BUTTONS - /** * TODO - @fileoverview * @copyright 2020 Federal Election Commission @@ -21,8 +19,16 @@ const breakpointToLarge = 700; const breakpointToXL = 860; const availElectionYears = [2020, 2016]; // defaults to [0] const specialCandidateIDs = ['P00000001', 'P00000002', 'P00000003']; -// const rootPathToIndividualContributions = -// '/data/receipts/individual-contributions/'; + +const pathFormat_download_contribs = + '/files/bulk-downloads/Presidential_Map/{election_year}/{candidate_id}/{candidate_id}-{state}.zip'; +// {state} can be the two-letter abbreviation, ALL, or OTHER +const pathFormat_download_expends = + '/files/bulk-downloads/Presidential_Map/{election_year}/{candidate_id}/{candidate_id}-ALL.zip'; +const pathFormat_download_summary = + '/files/bulk-downloads/Presidential_Map/{election_year}/report_summaries_form_3p.zip'; +const pathFormat_download_state = + '/files/bulk-downloads/Presidential_Map/{election_year}/{candidate_id}/{candidate_id}-{state}.zip'; import { buildUrl } from '../modules/helpers'; // import { defaultElectionYear } from './widget-vars'; @@ -42,6 +48,7 @@ const CANDIDATE_DETAILS_LOADED = EVENT_APP_ID + '_cand_detail_loaded'; const MAP_DATA_LOADED = EVENT_APP_ID + '_map_data_loaded'; const FINANCIAL_SUMMARY_LOADED = EVENT_APP_ID + '_cand_summary_loaded'; const CONTRIBUTION_SIZES_LOADED = EVENT_APP_ID + '_cand_sizes_loaded'; +const COVERAGE_DATES_LOADED = EVENT_APP_ID + '_coverage_dates_loaded'; // Element selectors // TODO: Update so we're using IDs everywhere? @@ -54,11 +61,19 @@ const selector_candidatesTable = '#pres-fin-map-candidates-table'; const selector_breadcrumbNav = '.breadcrumb-nav'; const selector_summariesHolder = '#financial-summaries'; const selector_candidateNamePartyAndLink = '.js-cand-name-par-a'; +const selector_downloadsWrapper = '#downloads-wrapper'; +const selector_coverageDates = '.js-coverage-date'; +const selector_exportRaisingButton = '.js-export-raising-data'; +const selector_exportSpending = '.js-export-spending-data'; +const selector_exportSummary = '.js-export-report-summary'; +const selector_stateDownloadLinks = + selector_downloadsWrapper + ' [data-stateID]'; +const selector_exportStateData = '.js-export-state-data'; /** * Formats the given value and puts it into the dom element. * @param {Number} passedValue The number to format and plug into the element - * @param {Boolean} roundToWhole Should we round the cents or no? + * @param {Boolean} abbreviateMillions Should we abbreviate the millions? (1,100,000 to 1.1) * @returns {String} A string of the given value formatted with a dollar sign, commas, and (if roundToWhole === false) decimal */ function formatAsCurrency(passedValue, abbreviateMillions) { @@ -118,50 +133,26 @@ function PresidentialFundsMap() { // Where to find this.basePath_financialSummary = ['presidential', 'financial_summary']; + // this.basePath_contributionSizes = [ 'presidential', 'contributions', 'by_size' ]; - // // Where to find candidate's coverage dates - // this.basePath_candidateCoverageDatesPath = [ - // 'candidate', - // '000', //candidate ID - // 'totals' - // ]; - // // Where to find the highest-earning candidates: - // this.basePath_highestRaising = ['candidates', 'totals']; + + // + this.basePath_coverageDates = ['presidential', 'coverage_end_date']; + // Where to find the data for the map this.basePath_mapData = ['presidential', 'contributions', 'by_state']; - // // Where to find the states list grand total: - // this.basePath_statesTotal = [ - // 'schedules', - // 'schedule_a', - // 'by_state', - // 'by_candidate', - // 'totals' - // ]; + this.data_candidates; - // // Details about the candidate. Comes from the typeahead this.data_candidate; this.data_summary; this.data_sizes; + this.data_coverage; this.data_map; - // // Information retruned by API candidate committees API {@see loadCandidateCommitteeDetails} - // this.data_candidateCommittees = {}; - // // Init the list/table of states and their totals - // this.data_states = { - // results: [ - // { - // candidate_id: '', - // count: 0, - // cycle: 2020, - // state: '', - // state_full: '', - // total: 0 - // } - // ] - // }; + // Shared settings for every fetch(): this.fetchInitObj = { cache: 'no-cache', @@ -178,27 +169,8 @@ function PresidentialFundsMap() { this.current_candidateName = 'All candidates'; this.current_candidateLastName = ''; this.map; // Starts as the element for the map but then becomes a DataMap object - // this.table; // The for the list of states and their totals - // this.statesTotalHolder; // Element at the bottom of the states list - // this.typeahead; // The typeahead candidate element: - // this.typeahead_revertValue; // Temporary var saved while user is typing - // this.yearControl; // The changes? + // Init the election year selector (The element ID is set in data/templates/partials/widgets/pres-finance-map.jinja) this.yearControl = this.element.querySelector(selector_yearControl); let theFieldset = this.yearControl.querySelector('fieldset'); for (let i = 0; i < availElectionYears.length; i++) { @@ -273,15 +263,6 @@ PresidentialFundsMap.prototype.init = function() { eventAppID: EVENT_APP_ID }); - // // Listen for the Browse Individual Contributions button to be clicked - // this.buttonIndivContribs = this.element.querySelector( - // '.js-browse-indiv-contribs-by-state' - // ); - // this.buttonIndivContribs.addEventListener( - // 'click', - // this.updateBrowseIndivContribsButton.bind(this) - // ); - // Internet Explorer doesn't like flex display // so we're going to keep the states table from switching to flex. let userAgent = window.navigator.userAgent; @@ -289,20 +270,21 @@ PresidentialFundsMap.prototype.init = function() { let is_ie = userAgent.indexOf('MSIE ') > 0 || userAgent.indexOf('Trident/7.0') > 0; - // // Initialize the remote table header - // // Find the remote header and save it + // TODO: Activate the remote table header + // Initialize the remote table header + // Find the remote header and save it // this.remoteTableHeader = this.element.querySelector( // '.js-remote-table-header' // ); - // // Save its for a few lines + // Save its for a few lines // let theRemoteTableHead = this.remoteTableHeader.querySelector('thead'); - // // Look at the data-for attribute of remoteTableHeader and save that element + // Look at the data-for attribute of remoteTableHeader and save that element // this.remoteTable = this.element.querySelector( // '#' + this.remoteTableHeader.getAttribute('data-for') // ); - // // Remember the in remoteTable for few lines + // Remember the in remoteTable for few lines // let theRemotedTableHead = this.remoteTable.querySelector('thead'); - // // If we have both elements, we're ready to manipulate them + // If we have both elements, we're ready to manipulate them // if (theRemoteTableHead && theRemotedTableHead) { // this.remoteTableHeader.style.display = 'table'; // theRemotedTableHead.style.display = 'none'; @@ -388,8 +370,6 @@ PresidentialFundsMap.prototype.handleCandidatesDataLoaded = function(e) { } }) ); - - // this.loadCandidateDetails(this.current_candidateID); }; /** @@ -709,7 +689,6 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidate = function(detail) { /** * Put the list of states and totals into the table - * Called by {@see loadMapData() } */ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( results @@ -758,16 +737,7 @@ PresidentialFundsMap.prototype.displayUpdatedData_candidates = function( this.handleCandidateListClick.bind(this) ); } - // theTableBody.innerHTML = theTbodyString; } - // Update candidate's coverage dates above the states list - // this.loadCandidateCoverageDates(); - // Update the Individual Contributions button/link at the bottom - // this.updateBrowseIndivContribsButton(); - // Let the map know that the data has been updated - // this.map.handleDataRefresh(theData); - // Clear the classes and reset functionality so the tool is usable again - // this.setLoadingState(false); }; /** @@ -955,7 +925,7 @@ PresidentialFundsMap.prototype.handleCandidateListClick = function(e) { ); } - // TODO: re-decorate the candidate rows here + // TODO: re-decorate the candidate rows here? }; PresidentialFundsMap.prototype.handleCandidateChange = function(e) { @@ -990,10 +960,6 @@ PresidentialFundsMap.prototype.handleElectionYearChange = function(e) { detail: e.target.value }) ); - - // // We don't need to load the candidate details for a year change, - // // so we'll just jump right to loading the committees data for the newly-chosen year. - // this.loadCandidateCommitteeDetails(); }; /** @@ -1011,6 +977,7 @@ PresidentialFundsMap.prototype.handleStateClick = function(e) { /** * Called from throughout the widget + * TODO: Do we still need this? * @param {String} errorCode */ PresidentialFundsMap.prototype.handleErrorState = function(errorCode) { @@ -1031,6 +998,7 @@ PresidentialFundsMap.prototype.handleErrorState = function(errorCode) { /** * Listens to window resize events and adjusts the classes for the - +
    Receipts$000
    Disbursements$000
    Cash-on-handNEED THIS $000
    Cash-on-handCHECK THIS $000
    Debts owed by committee$000
    @@ -91,7 +91,7 @@
      PACs$000
      Parties$000
      Candidates$000
    Federal fundsNEED THIS $000
    Federal fundsCHECK THIS $000
    Transfers-in$000
    Contributions by size
    $200 and under$000
    Receipts$000
    Disbursements$000
    Cash-on-handCHECK THIS $000
    Cash-on-handCHECK THIS $000
    Debts owed by committee$000
    From 0d84ce943d93f001f91e090b220d5ea0b9c87ae6 Mon Sep 17 00:00:00 2001 From: john carroll Date: Wed, 12 Feb 2020 23:48:35 -0500 Subject: [PATCH 10/26] dispatcher and handler for state click, breadcrumb state name --- fec/fec/static/js/modules/data-map.js | 5 ++++- fec/fec/static/js/widgets/pres-finance-map-box.js | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/fec/fec/static/js/modules/data-map.js b/fec/fec/static/js/modules/data-map.js index fe9250e765..12fc1dde57 100644 --- a/fec/fec/static/js/modules/data-map.js +++ b/fec/fec/static/js/modules/data-map.js @@ -450,7 +450,10 @@ function buildStateTooltips(svg, path, instance) { .on('click', function(d) { this.dispatchEvent( new CustomEvent('STATE_CLICKED', { - detail: fips.fipsByCode[d.id].STUSAB, + detail: { + abbr: fips.fipsByCode[d.id].STUSAB, + name: fips.fipsByCode[d.id].STATE_NAME + }, bubbles: true }) ); diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index f690bfc66b..5fbc3555bd 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -2,7 +2,7 @@ /* global CustomEvent */ -ROBERT, NEXT UP IS THE VARIOUS EXPORT BUTTONS +/*ROBERT, NEXT UP IS THE VARIOUS EXPORT BUTTONS*/ /** * TODO - @fileoverview @@ -817,8 +817,7 @@ PresidentialFundsMap.prototype.updateBreadcrumbs = function(dataObj) { theSecondLabel = 'Nationwide: '; } else { // Otherwise, we're showing a state so we need a state lookup - // TODO: theSecondLabel = (lookup the state name for this.current_electionState) - theSecondLabel = `${dataObj.current_candidateName}: `; + theSecondLabel = `${dataObj.currentStateName}: `; } if (theSecondLabel != '') { @@ -905,8 +904,13 @@ PresidentialFundsMap.prototype.handleElectionYearChange = function(e) { /** * */ + PresidentialFundsMap.prototype.handleStateClick = function(e) { console.log('A STATE WAS CLICKED! ', e); + this.current_electionState = e.detail.abbr; + this.current_electionStateName = e.detail.name; + this.loadCandidatesList(); + console.log(e.detail.abb); }; /** From 5acc407a54361b6c308498d7360f98ffa6691330 Mon Sep 17 00:00:00 2001 From: john carroll Date: Thu, 13 Feb 2020 10:07:03 -0500 Subject: [PATCH 11/26] candidate full name for breadcrumb --- fec/fec/static/js/widgets/pres-finance-map-box.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 5fea437487..30e87aeca5 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -413,7 +413,8 @@ PresidentialFundsMap.prototype.handleCandidateDetailsLoaded = function(e) { year: this.current_electionYear, currentState: this.current_electionState, // for breadcrumbs currentStateName: this.current_electionStateName, // for breadcrumbs - candidateLastName: this.current_candidateLastName // for breadcrumbs + //candidateLastName: this.current_candidateLastName // for breadcrumbs + candidateLastName: e.detail.name // for breadcrumbs }; this.displayUpdatedData_candidate(dataObj); From 852d7019cb99fbc2439d5869c6e333eb67a8d3c8 Mon Sep 17 00:00:00 2001 From: john carroll Date: Thu, 13 Feb 2020 11:37:08 -0500 Subject: [PATCH 12/26] last breadcrumb not a link --- fec/data/templates/widgets/pres-finance-map.jinja | 2 +- fec/fec/static/js/widgets/pres-finance-map-box.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index c0debb3b8b..851305750c 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -45,7 +45,7 @@
    diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 30e87aeca5..ef81ed7aba 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -869,7 +869,7 @@ PresidentialFundsMap.prototype.updateBreadcrumbs = function(dataObj) { console.log('updateBreadcrumbs()'); let theHolder = this.element.querySelector(selector_breadcrumbNav); let theSeparator = theHolder.querySelector('span'); - let theSecondItem = theHolder.querySelectorAll('a')[1]; + let theSecondItem = theHolder.querySelectorAll('span')[1]; let theSecondLabel = ''; if ( From 52ea039c7fcf3a672bee51d00bfe99b89fd64ed9 Mon Sep 17 00:00:00 2001 From: rfultz Date: Thu, 13 Feb 2020 12:41:04 -0800 Subject: [PATCH 13/26] Removing styles copied from the live site --- fec/data/templates/widgets/pres-finance-map.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index c0debb3b8b..dadac1bec3 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -2,13 +2,13 @@
    -
    +
    Candidates running in:
    -
    +
    View as:
    + +
    {% endblock %} {% block scripts %} From 904bae2f418083ae40e80d229d3495c0ef967036 Mon Sep 17 00:00:00 2001 From: john carroll Date: Fri, 14 Feb 2020 00:05:28 -0500 Subject: [PATCH 19/26] open download area, scroll to view --- fec/fec/static/js/widgets/pres-finance-map-box.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index d231bd803a..16e579d5d6 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -171,6 +171,9 @@ function PresidentialFundsMap() { this.current_candidateLastName = ''; this.map; // Starts as the element for the map but then becomes a DataMap object + this.downloadsWrapper = document.querySelector(selector_downloadsWrapper); + this.downloadsWrapper.style.display='none'; + // If we have the element on the page, fire it up if (this.element) this.init(); } @@ -1156,6 +1159,8 @@ PresidentialFundsMap.prototype.refreshOverlay = function() { PresidentialFundsMap.prototype.handleExportRaisingClick = function(e) { console.log('handleExportRaisingClick(): ', e); e.preventDefault(); + $(this.downloadsWrapper).slideDown(); + this.downloadsWrapper.scrollIntoView() // TODO: show {selector_downloadsWrapper} // TODO: animate the page scroll to the downloads section // TODO then: Hide {selector_downloadsWrapper} when we're no longer interested in the raising downloads From 69b30e2f9b4a70553726ae73e4a0968eb3b5729a Mon Sep 17 00:00:00 2001 From: john carroll Date: Fri, 14 Feb 2020 14:52:00 -0500 Subject: [PATCH 20/26] scrollIntoView and slidedown for export area --- .../static/js/widgets/pres-finance-map-box.js | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 16e579d5d6..48a235c26b 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -55,6 +55,7 @@ const selector_breadcrumbNav = '.breadcrumb-nav'; const selector_summariesHolder = '#financial-summaries'; const selector_candidateNamePartyAndLink = '.js-cand-name-par-a'; const selector_downloadsWrapper = '#downloads-wrapper'; +//const selector_downloadsContent = '#downloads-wrapper div'; const selector_coverageDates = '.js-coverage-date'; const selector_exportRaisingButton = '.js-export-raising-data'; const selector_exportSpending = '.js-export-spending-data'; @@ -97,7 +98,7 @@ function formatAsCurrency(passedValue, abbreviateMillions) { } /** - * Builds the link/url the candidate's + * Builds the link/url the candidate's * @param {String} candidateID The requested candidate's ID * @param {String} candidateName The name that will be displayed in the link * @param {Number} electionYear The currently-selected election year @@ -172,7 +173,8 @@ function PresidentialFundsMap() { this.map; // Starts as the element for the map but then becomes a DataMap object this.downloadsWrapper = document.querySelector(selector_downloadsWrapper); - this.downloadsWrapper.style.display='none'; + this.downloadsWrapper.style.height = 0; + this.downloadsWrapper.style.overflow = 'hidden'; // If we have the element on the page, fire it up if (this.element) this.init(); @@ -538,7 +540,9 @@ PresidentialFundsMap.prototype.loadCandidateDetails = function( // .fetch(`/static/temp-data/candidate-${this.current_candidateID}.json`) .then(function(response) { if (response.status !== 200) - throw new Error('The network rejected the candidate details request.'); + throw new Error( + 'The network rejected the candidate details request.' + ); // else if (response.type == 'cors') throw new Error('CORS error'); response.json().then(data => { // Let the audience know the load is complete @@ -924,7 +928,9 @@ PresidentialFundsMap.prototype.displayCoverageDates = function(data) { // Start with an empty coverage date let theCoverageString = ''; if (data[0] && data[0].coverage_end_date) { - theCoverageString = new Date(data[0].coverage_end_date).toLocaleDateString('en-US'); + theCoverageString = new Date(data[0].coverage_end_date).toLocaleDateString( + 'en-US' + ); theCoverageString = `through ${theCoverageString}`; } @@ -1159,8 +1165,23 @@ PresidentialFundsMap.prototype.refreshOverlay = function() { PresidentialFundsMap.prototype.handleExportRaisingClick = function(e) { console.log('handleExportRaisingClick(): ', e); e.preventDefault(); - $(this.downloadsWrapper).slideDown(); - this.downloadsWrapper.scrollIntoView() + + this.downloadsWrapper.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); + + $(this.downloadsWrapper).animate( + { + height: $(this.downloadsWrapper).get(0).scrollHeight + }, + 1000, + function() { + $(this).height('auto'); + } + ); + // TODO: show {selector_downloadsWrapper} // TODO: animate the page scroll to the downloads section // TODO then: Hide {selector_downloadsWrapper} when we're no longer interested in the raising downloads From 9dd0e2da9316560d992084161801822f85088961 Mon Sep 17 00:00:00 2001 From: john carroll Date: Mon, 17 Feb 2020 01:46:02 -0500 Subject: [PATCH 21/26] downloads area styling, edits to scroll-to/show JS --- .../templates/widgets/pres-finance-map.jinja | 10 +++-- .../static/js/widgets/pres-finance-map-box.js | 42 ++++++++++++++----- .../static/scss/widgets/pres-finance-map.scss | 22 +++++++++- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index 5bd4c4029c..6eee20a5e5 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -130,9 +130,13 @@ {# Browse TODO stuff #}
    -
    Export raising data
    - -
    +
    +

    + Export raising data +

    +
    + +
    -
    The map
    +
    Export ST raising data diff --git a/fec/fec/static/js/modules/data-map.js b/fec/fec/static/js/modules/data-map.js index 31eaf6a3aa..bc785a72b0 100644 --- a/fec/fec/static/js/modules/data-map.js +++ b/fec/fec/static/js/modules/data-map.js @@ -86,8 +86,8 @@ DataMap.prototype.init = function() { // Create the base-level state/country shapes let projection = d3.geo .albersUsa() - .scale(450) - .translate([220, 150]); + .scale(450) // lower numbers make the map smaller + .translate([220, 150]); // lower numbers move the map up and to the left // Create the path based on those base-level shapes let path = d3.geo.path().projection(projection); @@ -149,7 +149,12 @@ DataMap.prototype.init = function() { return fips.fipsByCode[d.id].STATE_NAME; }) .attr('class', 'shape') - .attr('d', path); + .attr('d', path) + .append('circle') + .attr('cx', -106.661513) + .attr('cy', 35.05917399) + .attr('r', '10px') + .style('fill', 'red'); // If we're supposed to add a legend, let's do it if (this.opts.addLegend || typeof this.opts.addLegend === 'undefined') { @@ -451,6 +456,9 @@ function buildStateTooltips(svg, path, instance) { bubbles: true }) ); + d3.transition() + .scale(500) + .translate([120, 50]); console.log('clicked a state!'); }); diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index d231bd803a..d6705e231d 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -160,6 +160,7 @@ function PresidentialFundsMap() { mode: 'cors', signal: null }; + console.log('(change no-cors to cors '); this.fetchingData = false; // Are we waiting for data? this.element = document.querySelector(selector_mainElement); // The visual element associated with this, this.instance this.candidateDetailsHolder; // Element to hold candidate name, party, office, and ID From 87ed1c1ba49864c87bf2c64df8de3ff5e30b1e55 Mon Sep 17 00:00:00 2001 From: Laura Beaufort Date: Tue, 18 Feb 2020 13:24:12 -0500 Subject: [PATCH 23/26] Add feature flag for presidential map --- fec/data/urls.py | 10 +++++++--- fec/fec/settings/base.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fec/data/urls.py b/fec/data/urls.py index 58b51db0f9..c1dd4a0b83 100644 --- a/fec/data/urls.py +++ b/fec/data/urls.py @@ -2,6 +2,7 @@ from data import views from data import views_datatables +from fec import settings urlpatterns = [ url(r'^data/$', views.landing), @@ -16,9 +17,6 @@ url(r'^data/raising-bythenumbers/$', views.raising), url(r'^data/spending-bythenumbers/$', views.spending), - # Presidential Campaign Finance Map - url(r'^data/candidates/president/presidential-map/$', views.pres_finance_map), - # Feedback Tool url(r'^data/issue/reaction/$', views.reactionFeedback), url(r'^data/issue/$', views.feedback), @@ -49,3 +47,9 @@ url(r'^widgets/aggregate-totals/$', views.aggregate_totals), ] + +if settings.FEATURES.get('presidential_map'): + # Presidential Campaign Finance Map + urlpatterns.append( + url(r'^data/candidates/president/presidential-map/$', views.pres_finance_map) + ) diff --git a/fec/fec/settings/base.py b/fec/fec/settings/base.py index 66afaff2bd..53f5fc7055 100644 --- a/fec/fec/settings/base.py +++ b/fec/fec/settings/base.py @@ -53,6 +53,7 @@ 'use_tag_manager': bool(env.get_credential('FEC_FEATURE_USE_TAG_MANAGER', '')), 'contributionsbystate': bool(env.get_credential('FEC_FEATURE_CONTRIBUTIONS_BY_STATE', '')), 'ierawfilters': bool(env.get_credential('FEC_FEATURE_IE_RAW_FILTERS', '')), + 'presidential_map': bool(env.get_credential('FEC_FEATURE_PRESIDENTIAL_MAP', '')), } ENVIRONMENTS = { From c6a5b902d78a8b41ad2e9cb6ae6c9e7167b036b7 Mon Sep 17 00:00:00 2001 From: Laura Beaufort Date: Wed, 19 Feb 2020 13:23:37 -0500 Subject: [PATCH 24/26] Fix formatting, silence tests for map --- fec/fec/static/js/modules/data-map.js | 4 ++ .../static/js/widgets/pres-finance-map-box.js | 42 +++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/fec/fec/static/js/modules/data-map.js b/fec/fec/static/js/modules/data-map.js index 31eaf6a3aa..136af32acf 100644 --- a/fec/fec/static/js/modules/data-map.js +++ b/fec/fec/static/js/modules/data-map.js @@ -1,3 +1,5 @@ +/* eslint-disable no-undef */ + 'use strict'; /** @@ -537,3 +539,5 @@ function tooltipTemplate(obj) { module.exports = { DataMap }; + +/* eslint-enable no-undef */ diff --git a/fec/fec/static/js/widgets/pres-finance-map-box.js b/fec/fec/static/js/widgets/pres-finance-map-box.js index 4f3b65123e..4d66f79604 100644 --- a/fec/fec/static/js/widgets/pres-finance-map-box.js +++ b/fec/fec/static/js/widgets/pres-finance-map-box.js @@ -1,3 +1,6 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ + 'use strict'; /* global CustomEvent */ @@ -1171,27 +1174,28 @@ PresidentialFundsMap.prototype.handleExportRaisingClick = function(e) { eg: function openDownloads() VS. const openDownloads = function() */ - const openDownloads = function(){ - console.log('callback') - + const openDownloads = function() { + console.log('callback'); + $(instance.downloadsWrapper).animate( - { - height: $(instance.downloadsWrapper).get(0).scrollHeight - }, - 1000, - function() { - $(this).height('auto'); - }) - } + { + height: $(instance.downloadsWrapper).get(0).scrollHeight + }, + 1000, + function() { + $(this).height('auto'); + } + ); + }; // Wait until the export area is in view before opening window.onscroll = function() { var wS = this.scrollY, - hT = instance.downloadsWrapper.getBoundingClientRect().top + wS, - hH = instance.downloadsWrapper.offsetHeight, - wH = window.innerHeight; - if (wS > (hT+hH-wH)){ - openDownloads() + hT = instance.downloadsWrapper.getBoundingClientRect().top + wS, + hH = instance.downloadsWrapper.offsetHeight, + wH = window.innerHeight; + if (wS > hT + hH - wH) { + openDownloads(); } }; //scroll to export area @@ -1199,8 +1203,7 @@ PresidentialFundsMap.prototype.handleExportRaisingClick = function(e) { behavior: 'smooth', block: 'center', inline: 'nearest' - }); - + }); // TODO-done: show {selector_downloadsWrapper} // TODO-done: animate the page scroll to the downloads section @@ -1309,3 +1312,6 @@ function logUsage(candID, electionYear) { } new PresidentialFundsMap(); + +/* eslint-enable no-undef */ +/* eslint-enable no-unused-vars */ From 1f1fc42605b2fef31d4c01164aed417deb2fcce8 Mon Sep 17 00:00:00 2001 From: Laura Beaufort Date: Wed, 19 Feb 2020 13:39:18 -0500 Subject: [PATCH 25/26] Clean up python logic, fix default year --- fec/data/views.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/fec/data/views.py b/fec/data/views.py index 2f5a69907a..e64ff6fcf8 100644 --- a/fec/data/views.py +++ b/fec/data/views.py @@ -682,30 +682,24 @@ def spending(request): def pres_finance_map(request): - # office = request.GET.get("office", "P") election_year = int( - request.GET.get("election_year", constants.DEFAULT_ELECTION_YEAR) + request.GET.get("election_year", constants.DEFAULT_PRESIDENTIAL_YEAR) ) - # max_election_year = utils.current_cycle() + 4 - # election_years = utils.get_cycles(max_election_year) - return render( request, "pres-finance-map.jinja", { "parent": "data", "title": "Presidential Campaign Finance Map", - # "election_years": election_years, "election_year": election_year, - # "office": "P", "social_image_identifier": "data", "page_specific_css": "/static/css/widgets/pres-finance-map.css", - + }, ) - + def feedback(request): if request.method == "POST": From 8f473ab6bd5f7a9773f993f2a2ce430e8e948ab7 Mon Sep 17 00:00:00 2001 From: Laura Beaufort Date: Wed, 19 Feb 2020 13:46:59 -0500 Subject: [PATCH 26/26] Use constants.states_excl_territories for state list --- .../templates/widgets/pres-finance-map.jinja | 54 ++----------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/fec/data/templates/widgets/pres-finance-map.jinja b/fec/data/templates/widgets/pres-finance-map.jinja index 6eee20a5e5..de91c054ad 100644 --- a/fec/data/templates/widgets/pres-finance-map.jinja +++ b/fec/data/templates/widgets/pres-finance-map.jinja @@ -137,57 +137,9 @@