diff --git a/fec/data/templates/elections.jinja b/fec/data/templates/elections.jinja index 853cccfe5..f1857cac4 100644 --- a/fec/data/templates/elections.jinja +++ b/fec/data/templates/elections.jinja @@ -15,6 +15,7 @@ {% block css %} + {% endblock %} {% block body %} {{ header.header(title, crumb) }} @@ -56,6 +57,7 @@ election_full: true, duration: '{{ election_duration }}', office: '{{ office }}', + office_code: '{{ office_code }}', state: '{{ state or '' }}', stateFull: '{{ state_full or '' }}', district: '{{ district or '' }}' @@ -63,6 +65,7 @@ }; +{{ 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 %} diff --git a/fec/data/templates/partials/elections/election-data-and-compliance-tab.jinja b/fec/data/templates/partials/elections/election-data-and-compliance-tab.jinja index 5d82d57fb..cc1729294 100644 --- a/fec/data/templates/partials/elections/election-data-and-compliance-tab.jinja +++ b/fec/data/templates/partials/elections/election-data-and-compliance-tab.jinja @@ -33,6 +33,7 @@
{{ select.election_cycle_select(cycles, cycle, 'summary') }} + {% include 'partials/elections/raising-and-spending.jinja' %}

Candidate financial totals

diff --git a/fec/data/templates/partials/elections/raising-and-spending.jinja b/fec/data/templates/partials/elections/raising-and-spending.jinja new file mode 100644 index 000000000..ca9b9218d --- /dev/null +++ b/fec/data/templates/partials/elections/raising-and-spending.jinja @@ -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 %} + +
+

Raising and spending summary

+
+
+
+
+
+
+
+

$1,234,567,890

+

raised by candidates

+
+ +
+
+
+
+
+
+

$1,234,567,890

+

spent by candidates

+
+ +
+
+
+ +
+
+
+

$1,234,567,890

+

independent expenditures

+
+ +
+
+
+
+
+ +
+
+ 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. +
+
+
+
+
+
+
diff --git a/fec/data/templates/partials/elections/sidebar-nav.jinja b/fec/data/templates/partials/elections/sidebar-nav.jinja index 4a8ea632b..ea09b298d 100644 --- a/fec/data/templates/partials/elections/sidebar-nav.jinja +++ b/fec/data/templates/partials/elections/sidebar-nav.jinja @@ -12,6 +12,7 @@ Election data and reporting deadlines
    +
  • Raising and spending summary
  • Candidate financial totals
  • Individual contributions to candidates
  • Independent expenditures
  • diff --git a/fec/fec/static/js/modules/helpers.js b/fec/fec/static/js/modules/helpers.js index f6aa0c25b..586db4fc0 100644 --- a/fec/fec/static/js/modules/helpers.js +++ b/fec/fec/static/js/modules/helpers.js @@ -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'); }; diff --git a/fec/fec/static/js/modules/ind-exp-money-bars.js b/fec/fec/static/js/modules/ind-exp-money-bars.js new file mode 100644 index 000000000..78fe9fcfb --- /dev/null +++ b/fec/fec/static/js/modules/ind-exp-money-bars.js @@ -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) 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 = `
    `; + +/** + * Template for the rows/meters. There will generally be three of these inside one js-parties-holder + */ +const meterRowTemplate = ` +
    + + +
    + +
    +
    `; diff --git a/fec/fec/static/js/modules/party-money-bars.js b/fec/fec/static/js/modules/party-money-bars.js index eebab431d..f6a0ccfdb 100644 --- a/fec/fec/static/js/modules/party-money-bars.js +++ b/fec/fec/static/js/modules/party-money-bars.js @@ -2,7 +2,7 @@ * 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 @@ -10,7 +10,7 @@ import { currency } from './helpers.js'; const partyAbbrevs = { DEM: 'Dem.', REP: 'Rep.', - Other: 'Other' + OTHER: 'Other' }; /** @@ -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' } ] }; @@ -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 } @@ -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) => { @@ -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); + } }); }); diff --git a/fec/fec/static/js/pages/election-summary.js b/fec/fec/static/js/pages/election-summary.js new file mode 100644 index 000000000..48918af39 --- /dev/null +++ b/fec/fec/static/js/pages/election-summary.js @@ -0,0 +1,250 @@ +/** + * + */ +import FECContainerQuery from '../modules/container-queries.js'; +//import { supportOppose } from '../modules/decoders.js'; +import IeMoneyBars from '../modules/ind-exp-money-bars.js'; +import PartyMoneyBars from '../modules/party-money-bars.js'; + +/** + * Runs the Summary tab at /data/elections/house/ and /data/elections/senate/ + * @class + * @property {HTMLInputElement} cycleSelector - the + */ +export default function ElectionSummary() { + this.tabPanel = document.querySelector('#election-summary'); + this.cycleSelector = document.querySelector('.cycle-select #summary-cycle'); + + this.init(); + this.startLoadingData(this.cycleSelector.value); +} + +/** + * Gets this instance built and working + */ +ElectionSummary.prototype.init = function() { + this.cycleSelector.addEventListener('change', this.handleCycleChange.bind(this)); + + //TODO, can I combibe the two below and use "new PartyMoneyBars" for both? + const theFigures = [...document.querySelectorAll('#election-summary figure')]; + // Get first two figures for raising and spending + theFigures.slice(0, 2).forEach(el => { + new PartyMoneyBars( + `#${el.id} .parties-wrapper`, + `#${el.id} .js-value-large`, + { + eventId: el.dataset.eventFieldId, + figureGroupClasses: 'grid--flex grid--3-wide', + figureClasses: 'grid__item' + } + ); + }); + + const theFiguresIe = document.querySelectorAll('#election-summary-ie figure'); + // Get the last figure for individual expenditures, loop is best approach even though there is only one + theFiguresIe.forEach(el => { + new IeMoneyBars( + `#${el.id} .parties-wrapper`, + `#${el.id} .js-value-large`, + { + eventId: el.dataset.eventFieldId, + figureGroupClasses: 'grid--flex grid--3-wide', + figureClasses: 'grid__item' + } + ); + }); + + new FECContainerQuery('#election-summary', '#summary'); + +}; + +/** + * Takes an election year, deactivates cycleSelect, and starts the data load + * @param {number|string} electionYear - Which election year / cycle to load + */ + +ElectionSummary.prototype.startLoadingData = function(electionYear) { +// Only do anything if electionYear is an year between 1970 and 2050 +//if (parseInt(electionYear) && electionYear >= 1970 && electionYear <= 2050 && electionYear % 2 === 0) { + //this.deactivateInput(); + const instance = this; + + const url_params = { + aggregate_by: 'office-state-district', + api_key: window.API_KEY_PUBLIC, + election_full: true, + election_year: electionYear, + is_active_candidate: true, + office: window.context.election.office_code.toUpperCase(), + sort_hide_null: false, + sort_null_only: false, + sort_nulls_last: false, + page: 1, + per_page: 10 + }; + + url_params.state = url_params.office == 'P' ? 'US' : window.context.election.state; + url_params.district = url_params.office == 'P' ? '00' : window.context.election.district; + + let theQstring = ''; + for (let n in url_params) { + theQstring += `${n}=${url_params[n]}&`; + } + + const theURL = `${window.API_LOCATION}/${window.API_VERSION}/candidates/totals/aggregates/?${theQstring}`; + const ieQstring = theQstring.replace('election_year', 'cycle'); + const ieUrl = `${window.API_LOCATION}/${window.API_VERSION}/schedules/schedule_e/all_candidates/support_oppose_totals/?${ieQstring}`; + //const ieUrl = window.location.origin + '/static/az_suppose_totals.json'; + + const allTotals = {}; + const partys = ['DEM', 'REP', 'OTHER']; + + // Helper track to see when the candidate data block is completely ready + let candidatesRequestsFinished = 0; + + function checkAndRenderCandidates() { + candidatesRequestsFinished++; + // Once the 'total' call + all 3 party calls are done (4 requests total) + if (candidatesRequestsFinished === 4) { + instance.handleDataLoaded(allTotals); + } + } + + // 1. Fetch main totals + fetch(theURL, { cache: 'no-cache', mode: 'cors' }) + .then(res => res.json()) + .then(data => { + allTotals['total'] = data.results; + checkAndRenderCandidates(); + }) + //.catch(err => console.error('Error fetching grand total:', err)); + .catch(() => { + // console.log(`The fetch failed because: ${error}`); + }); + + // 2. Fetch party totals + partys.forEach(party => { + fetch(`${theURL}party=${party}`, { cache: 'no-cache', mode: 'cors' }) + .then(res => res.json()) + .then(data => { + allTotals[party] = data.results; + checkAndRenderCandidates(); + }) + //.catch(err => console.error(`Error fetching ${party} total:`, err)); + .catch(() => { + // console.log(`The fetch failed because: ${error}`); + }); + }); + + // 3. Fetch the slow independent expenditures independently + // This will NOT block the rest of the page from loading + fetch(ieUrl, { cache: 'no-cache', mode: 'cors' }) + .then(res => res.json()) + .then(data => { + const result = data.results.reduce((accumulator, currentItem) => { + const amount = currentItem.ie_total; + const indicator = currentItem.support_oppose_indicator; + + accumulator.total += amount; + if (Object.hasOwn(accumulator, indicator)) { + accumulator[indicator] += amount; + } + return accumulator; + }, { total: 0, S: 0, O: 0, Others: 0 }); + + // Save to instance directly + instance.total_independent_expenditures = result; + allTotals['support_oppose'] = result; + + // Proactively update the UI with just the independent expenditure data once it arrives + if (typeof instance.handleIndependentExpendituresLoaded === 'function') { + instance.handleIndependentExpendituresLoaded(result); + } else { + // Fallback: re-trigger main loader if you don't have a specific sub-render function + instance.handleDataLoaded(allTotals); + } + }) + //.catch(err => console.error('Error fetching independent expenditures:', err)); + .catch(() => { + // console.log(`The fetch failed because: ${error}`); + }); + +//}; END - if (parseInt(electionYear) && ... +}; + +/** + * Removes functionality from the input/select assigned to this.cycleSelector + */ +ElectionSummary.prototype.deactivateInput = function() { + this.cycleSelector.setAttribute('aria-disabled', 'true'); + this.cycleSelector.setAttribute('disabled', 'true'); +}; + +/** + * Restores functionality from the input/select assigned to this.cycleSelector + */ +ElectionSummary.prototype.reactivateInput = function() { + this.cycleSelector.removeAttribute('aria-disabled'); + this.cycleSelector.removeAttribute('disabled'); +}; + +/** + * Handles the change/input event from this.cycleSelector + * @param {Event} e - Change event + * @param {number} e.target.value - A valid 4-digit integer for an even/election year + */ +ElectionSummary.prototype.handleCycleChange = function (e) { + this.startLoadingData(e.target.value); +}; + +/** + * Takes the results, parses them from parties divisions to type combinations, then reactivates cycleSelect. + * (Takes [{dem numbers}, {rep numbers}, {other numbers}] and converts to [{receipts: {total, dem numbers, rep numbers, other numbers}}]) + * @param {Object} results - response.results from the api + */ + +ElectionSummary.prototype.handleDataLoaded = function(results) { + const usefulResults = { + total_cash_on_hand_end_period: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_debts_owed_by_committee: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_disbursements: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_individual_itemized_contributions: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_other_political_committee_contributions: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_receipts: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_transfers_from_other_authorized_committee: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, + total_independent_expenditures: this.total_independent_expenditures//{ total: 0, S: 0, A: 0, O: 0 }, + }; + + // Google's + for (const [key, value] of Object.entries(results)) { + for (const val of Object.values(value)) { + for (const item in val) { + if (item.startsWith('total_')) { + // Updated to use the local 'value' variable instead of reaching back to 'results[key]' + usefulResults[item][key] = value[0][item]; + } + } + } + } + + for (let key in usefulResults) { + const newEvent = new CustomEvent('fec_data_refresh', { + bubbles: true, + detail: { + id: key, + value: usefulResults[key] + } + }); + this.tabPanel.dispatchEvent(newEvent); + } + + this.reactivateInput(); + +}; + +/** + * Now let's wait for the page to load and start making elements work + */ +window.addEventListener('load', () => { + new ElectionSummary(); +}); diff --git a/fec/fec/static/js/pages/house-senate-overview-summary.js b/fec/fec/static/js/pages/house-senate-overview-summary.js index bf7199f44..7110878ff 100644 --- a/fec/fec/static/js/pages/house-senate-overview-summary.js +++ b/fec/fec/static/js/pages/house-senate-overview-summary.js @@ -117,12 +117,12 @@ HSOverviewSummary.prototype.handleCycleChange = function (e) { */ HSOverviewSummary.prototype.handleDataLoaded = function(results) { const usefulResults = { - total_cash_on_hand_end_period: { total: 0, DEM: 0, REP: 0, Other: 0 }, + total_cash_on_hand_end_period: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, total_debts_owed_by_committee: { total: 0, DEM: 0, REP: 0, Other: 0 }, - total_disbursements: { total: 0, DEM: 0, REP: 0, Other: 0 }, + total_disbursements: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, total_individual_itemized_contributions: { total: 0, DEM: 0, REP: 0, Other: 0 }, total_other_political_committee_contributions: { total: 0, DEM: 0, REP: 0, Other: 0 }, - total_receipts: { total: 0, DEM: 0, REP: 0, Other: 0 }, + total_receipts: { total: 0, DEM: 0, REP: 0, OTHER: 0 }, total_transfers_from_other_authorized_committee: { total: 0, DEM: 0, REP: 0, Other: 0 } }; diff --git a/fec/fec/static/scss/_fonts.scss b/fec/fec/static/scss/_fonts.scss index 7c1b4cf13..1673fdeda 100644 --- a/fec/fec/static/scss/_fonts.scss +++ b/fec/fec/static/scss/_fonts.scss @@ -66,6 +66,8 @@ font-weight: bold; font-style: normal; font-display: swap; + unicode-range: U+0030-0039, U+002E, U+002C, U+0024; + } @font-face { @@ -96,4 +98,5 @@ font-weight: normal; font-style: normal; font-display: swap; + unicode-range: U+0030-0039, U+002E, U+002C, U+0024; } diff --git a/fec/fec/static/scss/widgets/house-senate-overview.scss b/fec/fec/static/scss/widgets/house-senate-overview.scss index f6a6f3358..62ef172ed 100644 --- a/fec/fec/static/scss/widgets/house-senate-overview.scss +++ b/fec/fec/static/scss/widgets/house-senate-overview.scss @@ -6,6 +6,7 @@ url('/static/fonts/fec-currencymono-regular.woff2') format('woff2'), url('/static/fonts/fec-currencymono-regular.woff') format('woff'), url('/static/fonts/fec-currencymono-regular.ttf') format('truetype'); + unicode-range: U+0030-0039, U+002E, U+002C, U+0024; } #contributions-over-time { diff --git a/fec/fec/static/scss/widgets/party-money-bars.scss b/fec/fec/static/scss/widgets/party-money-bars.scss index 474d714af..62846d4e5 100644 --- a/fec/fec/static/scss/widgets/party-money-bars.scss +++ b/fec/fec/static/scss/widgets/party-money-bars.scss @@ -6,6 +6,7 @@ $colorTextOnLight: #212121; $colorBarDef: #5b616b; $colorBarDem: #21adf7; $colorBarRep: #da3c18; +$colorBarExp: #36bdbb; $colorBarOtherOnDark: #d6d7d9; $colorBorderBottom: #e8e8e8; $fontFamilyMonospace: 'fec_currency_mono', 'karla', sans-serif; @@ -30,6 +31,8 @@ $fontWeightBold: 700; .js-parties-holder { grid-template-columns: 4em auto 25%; display: grid; + justify-content: start; + .js-meter-row { display: contents; @@ -134,7 +137,7 @@ $fontWeightBold: 700; position: absolute; border-right: thin solid $colorBarDef; } - &[data-party='DEM'] { + &[data-party='DEM'] { &::-webkit-meter-optimum-value { // note#1 background: $colorBarDem; @@ -164,6 +167,46 @@ $fontWeightBold: 700; border-right-color: $colorBarRep; } } + + &[data-party='S'], + &[data-party='O'], + &[data-party='OTHER'] { + &::-webkit-meter-optimum-value { + // note#1 + background: $colorBarExp; + transition: all 1s; + } + &:-moz-meter-optimum::-moz-meter-bar { + // note#1 + background: $colorBarExp; + transition: all 1s; + } + &::before { + border-right-color: $colorBarExp; + } + } + + + /* &#raising-spending-summary { + transition: none !important; + + ::-webkit-meter-inner-element { + display: block !important; + height: 100% !important; + box-sizing: border-box !important; + } + + ::-webkit-meter-bar { + display: block !important; + height: 100% !important; + } + + ::-webkit-meter-optimum-value { + transition: width 0.3s ease !important; + } + + } */ + } // Trying to get the widths to animate when the values change meter::-webkit-meter-suboptimum-value, @@ -178,6 +221,10 @@ $fontWeightBold: 700; } } + + + + footer { -ms-flex-align: baseline; align-items: baseline; @@ -406,3 +453,25 @@ $fontWeightBold: 700; } } } + +#raising-spending-summary { + transition: none !important; + + ::-webkit-meter-inner-element{ + display: block !important; + } + + ::-webkit-meter-bar { + margin-top: 2.7rem !important + + } + + ::-webkit-meter-optimum-value { + transition: width 1 ease !important; + } + + meter::before { + margin-top: .35rem + } + + }