Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions fec/data/templates/elections.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

{% block css %}
<link rel="stylesheet" type="text/css" href="{{ path_for_css('elections.css') }}" />
<link rel="stylesheet" type="text/css" href="/static/css/widgets/party-money-bars.css" />
{% endblock %}
{% block body %}
{{ header.header(title, crumb) }}
Expand Down Expand Up @@ -56,13 +57,15 @@
election_full: true,
duration: '{{ election_duration }}',
office: '{{ office }}',
office_code: '{{ office_code }}',
state: '{{ state or '' }}',
stateFull: '{{ state_full or '' }}',
district: '{{ district or '' }}'
}
};

</script>
{{ tags_for_js_chunks('election-summary.js', '')|safe }}
{#- data-init and global chunks are loaded by main.jinja -#}
{{ tags_for_js_chunks('elections.js', '')|safe }}
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<div class="slab slab--inline slab--neutral u-padding--left u-padding--right">
{{ select.election_cycle_select(cycles, cycle, 'summary') }}

{% include 'partials/elections/raising-and-spending.jinja' %}
<div id="candidate-financial-totals" class="entity__figure row">
<div class="heading--section heading--with-action u-no-margin">
<h3 class="heading__left">Candidate financial totals</h3>
Expand Down
55 changes: 55 additions & 0 deletions fec/data/templates/partials/elections/raising-and-spending.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{# used inside fec/data/templates/house-senate-overview.jinja #}
{% import 'macros/null.jinja' as null %}
{% import 'macros/disclaimer.jinja' as disclaimer %}
{% import 'macros/cycle-select.jinja' as select %}

<div id="raising-spending-summary" class="entity__figure row">
<h3 class="heading__left">Raising and spending summary</h3>
<section id="summary" class="option">
<div class="heading--section heading--with-action u-no-margin">
<div id="election-summary">
<div class="grid--flex grid--3-wide">
<div class="grid__item">
<figure class="js-party-money-bars" id="summary-total-raised" data-event-field-id="total_receipts">
<div class="total-wrapper">
<h1 class="value js-value-large">$1,234,567,890</h1>
<h2 class="description js-value-large-desc">raised by candidates</h2>
</div>
<div class="visual-divider" role="presentation"></div>
<div class="parties-wrapper"></div>
</figure>
</div>
<div class="grid__item">
<figure class="js-party-money-bars" id="summary-total-spent" data-event-field-id="total_disbursements">
<div class="total-wrapper">
<h1 class="value js-value-large">$1,234,567,890</h1>
<h2 class="description js-value-large-desc">spent by candidates</h2>
</div>
<div class="visual-divider" role="presentation"></div>
<div class="parties-wrapper"></div>
</figure>
</div>

<div class="grid__item" id="election-summary-ie">
<figure class="js-party-money-bars" id="summary-ie" data-event-field-id="total_independent_expenditures">
<div class="total-wrapper">
<h1 class="value js-value-large">$1,234,567,890</h1>
<h2 class="description js-value-large-desc">independent expenditures</h2>
</div>
<div class="visual-divider" role="presentation"></div>
<div class="parties-wrapper"></div>
</figure>
</div>
</div>
</div>

<div class="content__section--ruled-responsive methodology-block t-serif">
<div class="heading--with-action t-small t-serif">
<i class="data-disclaimer">Newly filed summary data may not appear for up to 48 hours. Money raised includes both itemized and unitemized totals. Independent expenditure totals are combined for all candidates, exclude 24- and 48-hour reports, and may take 30 days or more to appear.</i>
<div class="heading__right">
</div>
</div>
</div>
</div>
</section>
</div>
1 change: 1 addition & 0 deletions fec/data/templates/partials/elections/sidebar-nav.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Election data and reporting deadlines
</a>
<ul>
<li><a href="#raising-spending-summary">Raising and spending summary</a></li>
<li><a href="#candidate-financial-totals">Candidate financial totals</a></li>
<li><a href="#individual-contributions">Individual contributions to candidates</a></li>
<li><a href="#independent-expenditures">Independent expenditures</a></li>
Expand Down
18 changes: 18 additions & 0 deletions fec/fec/static/js/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ export function dollar(value) {
}
}

export const compactCurrency = (value) => {
const absValue = Math.abs(value);

// Choose fraction digits dynamically based on size
// Millions get 1 decimal place; everything else (thousands, hundreds) gets 0
const fractionDigits = absValue >= 1_000_000 ? 1 : 0;

return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
notation: 'compact',
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
}).format(value).toLowerCase();
};

Handlebars.registerHelper('compactCurrency', compactCurrency);

export const numberFormatter = function(number) {
return numeral(number).format('0,0');
};
Expand Down
152 changes: 152 additions & 0 deletions fec/fec/static/js/modules/ind-exp-money-bars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Handles the creation and animation for the animated three-row money comparison bars
*/

import { compactCurrency } from './helpers.js';

/**
* Quick lookup for converting api results to sometimes-abbreviated and punctuated versions
*/
const partyAbbrevs = {
S: 'Support.',
O: 'Oppose.',
Others: 'Other'
};

/**
* The defaults for a new IeMoneyBars, overridden by opts constructor argument
*/
const defaultSettings = {
animateChanges: true, // TODO: this isn't being read yet
currencyLabel: 'US Dollars', // TODO: is this needed? Maybe if we'd like to override it sometimes?
eventId: null,
figureClasses: '', // TODO: make this work?
figureGroupClasses: '', // TODO: make this work?
initValues: [
{ min: 0, max: 100, value: 50, party: 'S' },
{ min: 0, max: 100, value: 50, party: 'O' },
{ min: 0, max: 100, value: 50, party: 'Others' }
]
};

/**
* @class
* @param {string} putMetersHereSelector - How to find the HTMLElement that will be used for this.
* @param {string} [putTotalHereSelector] - Where to put the total.
* @param {Object} [opts] - Options to override defaults.
* @property {HTMLElement} metersHolder - The HTMLElement parent where the meters should go.
* @property {HTMLElement} totalElm - The HTMLElement whose innerText should be used to display the total $.
* @property {Object} settings - Combination of settings from opts overriding defaultSettings.
* @returns {IeMoneyBars} New instance of IeyMoneyBars.
*/
export default function IeMoneyBars(putMetersHereSelector, putTotalHereSelector, opts = {}) {
this.metersHolder = document.querySelector(putMetersHereSelector);
this.totalElm = putTotalHereSelector.length > 1 ? document.querySelector(putTotalHereSelector) : null;
this.settings = Object.assign({}, defaultSettings, opts);

this.init();
return this;
}

/**
* Does all of the setup work that doesn't involve constructor arguments
*/
IeMoneyBars.prototype.init = function() {

// Build elements
// Add the parties holder to this.metersHolder
this.metersHolder.innerHTML = partiesHolderTemplate;
// Then add three meters to the parties holder
const partiesHolder = this.metersHolder.querySelector('.js-parties-holder');
for (let i = 0; i < 3; i++) {
partiesHolder.innerHTML += meterRowTemplate;
}

const meters = this.metersHolder.querySelectorAll('meter');

// Let's use an ID so we can more easily update meters' labels.
// What ID can we give these meters? Base it on the first parent with an id.
// Use that ID and make its name a bit more custom for this class
const metersIdBase = this.metersHolder.closest('[id]').id;
meters.forEach((meter, i) => {
const thisMetersId = `${metersIdBase}-party-money-meter-${i}`;
meter.setAttribute('id', thisMetersId);
if (this.settings.currencyLabel) meter.setAttribute('title', this.settings.currencyLabel);
const thisMetersParent = meter.closest('.js-meter-row');
thisMetersParent.querySelector('.js-party-title').setAttribute('for', thisMetersId);
thisMetersParent.querySelector('.js-party-value').setAttribute('for', thisMetersId);
});

// Add listeners
document.body.addEventListener('fec_data_refresh', e => {
if (e.detail.id == this.settings.eventId) {
this.applyNewData(e.detail.value);
}
});
};

/**
* Triggered by the body#fec_data_refresh event (if the event id matches this instance's id).
* Takes the new values, sorts them, and puts them in the right (ranked) <meter> elements
* @param {Object} newValObj - New value to represent. Expected structure is {total: 9.87, DEM: 7.65, REP: 5.43, Other: 3.21}
* @param {number} [newValObj.total] - Grand total, if we're showing that // TODO: make sure it appears and disappears as needed
* @param {number} newValObj.DEM - Value for Democrats
* @param {number} newValObj.REP - Value for Republicans
* @param {number} [newValObj.Others] - Value for Other parties // TODO: will this disappear if not included? Will it ever not be included?
*/
IeMoneyBars.prototype.applyNewData = function(newValObj) {
// If we're doing the total, it's at newValObj.total;
if (this.totalElm) {
if (newValObj.total) {
this.totalElm.textContent = compactCurrency(newValObj.total, true);
this.totalElm.dataset.totalValue = newValObj.total;
} else {
// TODO Handle if we have a total element but didn't get a new value for it
}
}

// Sort the party values so the highest is on top
let rankedPartiesAndValues = [
{ party: 'S', value: newValObj.S },
{ party: 'O', value: newValObj.O },
{ party: 'Others', value: newValObj.Others }
];
// sort them based so the largest value is [0] (descending)
rankedPartiesAndValues.sort((a, b) => {
if (a.value < b.value) return 1;
else if (a.value > b.value) return -1;
else return 0;
});

const meters = this.metersHolder.querySelectorAll('meter');
meters.forEach((meter, i) => {
meter.max = rankedPartiesAndValues[0].value;
meter.value = rankedPartiesAndValues[i].value;
meter.dataset.party = rankedPartiesAndValues[i].party.toUpperCase();
// Update the meter's labels
meter.labels.forEach(label => {
if (label.classList.contains('js-party-title')) {
label.textContent = partyAbbrevs[rankedPartiesAndValues[i].party];
} else if (label.classList.contains('js-party-value')) {
label.textContent = compactCurrency(meter.value, true);
}
});
});
};

/**
* The template for the wrapper where the individual js-meter-row elements will live
*/
const partiesHolderTemplate = `<div class="js-parties-holder" role="grid"></div>`;

/**
* Template for the rows/meters. There will generally be three of these inside one js-parties-holder
*/
const meterRowTemplate = `
<div class="js-meter-row" role="row">
<label for="" role="cell" class=" js-party-title"></label>
<label for="" role="cell" class=" js-party-value t-mono-stacked-currency">$0</label>
<div>
<meter id="" min="0" max="0" value="0" title="" data-party=""></meter>
</div>
</div>`;
16 changes: 10 additions & 6 deletions fec/fec/static/js/modules/party-money-bars.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
* Handles the creation and animation for the animated three-row money comparison bars
*/

import { currency } from './helpers.js';
import { compactCurrency } from './helpers.js';

/**
* Quick lookup for converting api results to sometimes-abbreviated and punctuated versions
*/
const partyAbbrevs = {
DEM: 'Dem.',
REP: 'Rep.',
Other: 'Other'
OTHER: 'Other'
};

/**
Expand All @@ -25,7 +25,7 @@ const defaultSettings = {
initValues: [
{ min: 0, max: 100, value: 50, party: 'DEM' },
{ min: 0, max: 100, value: 50, party: 'REP' },
{ min: 0, max: 100, value: 50, party: 'Other' }
{ min: 0, max: 100, value: 50, party: 'OTHER' }
]
};

Expand Down Expand Up @@ -98,7 +98,9 @@ PartyMoneyBars.prototype.applyNewData = function(newValObj) {
// If we're doing the total, it's at newValObj.total;
if (this.totalElm) {
if (newValObj.total) {
this.totalElm.textContent = currency(newValObj.total, true);
this.totalElm.textContent = compactCurrency(newValObj.total);
this.totalElm.dataset.totalValue = newValObj.total;
//this.totalElm.textContent = this.actual_values ? currency(newValObj.total) : compactCurrency(newValObj.total);
} else {
// TODO Handle if we have a total element but didn't get a new value for it
}
Expand All @@ -108,7 +110,7 @@ PartyMoneyBars.prototype.applyNewData = function(newValObj) {
let rankedPartiesAndValues = [
{ party: 'DEM', value: newValObj.DEM },
{ party: 'REP', value: newValObj.REP },
{ party: 'Other', value: newValObj.Other }
{ party: 'OTHER', value: newValObj.OTHER }
];
// sort them based so the largest value is [0] (descending)
rankedPartiesAndValues.sort((a, b) => {
Expand All @@ -127,7 +129,9 @@ PartyMoneyBars.prototype.applyNewData = function(newValObj) {
if (label.classList.contains('js-party-title')) {
label.textContent = partyAbbrevs[rankedPartiesAndValues[i].party];
} else if (label.classList.contains('js-party-value')) {
label.textContent = currency(meter.value, true);
//label.textContent = this.actual_values ? currency(meter.value) :compactCurrency(meter.value);
label.textContent = compactCurrency(meter.value);

}
});
});
Expand Down
Loading