diff --git a/fec/data/templates/advanced.jinja b/fec/data/templates/advanced.jinja index f305db79d6..23ef512af5 100644 --- a/fec/data/templates/advanced.jinja +++ b/fec/data/templates/advanced.jinja @@ -6,7 +6,8 @@ {% endblock %} {% block css %} - + + {% endblock %} {% block body %} @@ -179,4 +180,6 @@ {% endblock %} {% block scripts %} + + {% endblock %} diff --git a/fec/data/templates/landing.jinja b/fec/data/templates/landing.jinja index 174b36bb18..d584dc592d 100644 --- a/fec/data/templates/landing.jinja +++ b/fec/data/templates/landing.jinja @@ -1,6 +1,7 @@ {% extends 'layouts/main.jinja' %} {% import 'macros/cycle-select.jinja' as select %} {% import 'macros/overviews.jinja' as overviews %} +{% import 'macros/reaction-box.jinja' as reaction %} {% block title %}{{ title }}{% endblock %} @@ -83,7 +84,7 @@ -
+

Browse full advanced data sets

@@ -186,9 +187,147 @@
+
+

Get started with campaign finance data

+
+

Raising

+

This graph shows how much candidates, party committees and political action committees (PACs) have reported raising, up to specific points in time. Although the graph displays these numbers month-by-month, different committee types have different reporting schedules.

+
+
+

Cumulative amount raised by committees

+
+ +
+
+ {{ overviews.overview('raised', 'landing')}} + +
    +
  • +
    +
    Who's raising the most
    +
    +
  • +
  • +
    +
    Where contributions come from
    +
    +
  • +
  • +
    +
    The size of contributions
    +
    +
  • +
+
+ {{ reaction.reaction_box('raised', 'landing') }} +
+
+

Spending

+

This graph shows how much candidates, party committees and political action committees (PACs) have reported spending, up to specific points in time. Although the graph displays these numbers month-by-month, different committee types have different reporting schedules.

+
+
+

Cumulative amount spent by committees

+
+ +
+
+ {{ overviews.overview('spent', 'landing') }} + +
    +
  • +
    +
    Who's spending the most
    +
    +
  • +
  • +
    +
    What's spent on day-to-day activities
    +
    +
  • +
  • +
    +
    Where money is spent to support and oppose candidates
    +
    +
  • +
+
+ {{ reaction.reaction_box('spent', 'landing') }} +
+
+ +{% endblock %} + +{% block modals %} + + {% endblock %} + {% block scripts %} - - + + {% endblock %} diff --git a/fec/data/templates/layouts/main.jinja b/fec/data/templates/layouts/main.jinja index 39aaebd851..445ab29cf3 100644 --- a/fec/data/templates/layouts/main.jinja +++ b/fec/data/templates/layouts/main.jinja @@ -124,8 +124,8 @@ {% csrf_token %} {% block modals %}{% endblock %} - - + + {% block scripts %}{% endblock %} {% if FEC_CMS_ENVIRONMENT == 'PRODUCTION' %} diff --git a/fec/data/templates/macros/chart-committee-overviews.jinja b/fec/data/templates/macros/chart-committee-overviews.jinja new file mode 100644 index 0000000000..2c90c9d8de --- /dev/null +++ b/fec/data/templates/macros/chart-committee-overviews.jinja @@ -0,0 +1,70 @@ +{% macro plot(total_type) %} +
+
+
+
+

How much money has been {{total_type}} between:

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+ Candidates +
+
+
+
+
+ +
+
+
+
+ PACs +
+
+
+
+
+ +
+
+
+
+ Party committees +
+
+
+
+
+ +
+
+
+
+ + All committees +
+
+
+
+
+ +
+
+
+
+
+{% endmacro %} diff --git a/fec/data/templates/macros/overviews.jinja b/fec/data/templates/macros/overviews.jinja index 417f4e69dd..754387d94d 100644 --- a/fec/data/templates/macros/overviews.jinja +++ b/fec/data/templates/macros/overviews.jinja @@ -10,41 +10,47 @@
- All committees + Candidates
-
+ - + -
+
- Candidates + PACs
- - + + + +
- PACs + Party committees
- - + + + +
- Party committees + All committees +
+
+ + + +
- - - -
diff --git a/fec/data/templates/partials/advanced/raising.jinja b/fec/data/templates/partials/advanced/raising.jinja index ad9a6812f7..4bd5e4d09e 100644 --- a/fec/data/templates/partials/advanced/raising.jinja +++ b/fec/data/templates/partials/advanced/raising.jinja @@ -1,7 +1,10 @@ +{% import 'macros/chart-committee-overviews.jinja' as chart %} +{% import 'macros/reaction-box.jinja' as reaction %} +

Raising

Search or browse data

-
+
  • @@ -36,6 +39,57 @@
+
  • +
    +
    +

    Cumulative amount raised by committees

    +
    +
      + +
    +
    +
    +
    + {{ chart.plot('raised')}} +
    +
    +
  • +
  • +
    +
    + +
    +
    +
    Methodology Overview
    +

    This data includes Forms 3, 3P, and 3X.

    +

    Money raised includes each of the following:

    +
      +
    • Adjusted receipts for PACs, parties, congressional filers and presidential filers
    • +
    +

    Adjusted receipts are the total receipts minus the following:

    +
      +
    • Contribution refunds
    • +
    • Contributions from political party committees and other political committees
    • +
    • Loan repayments
    • +
    • Offsets to operating expenditures
    • +
    • Transfers from nonfederal accounts for allocated activities
    • +
    +

    The form-by-form breakdown for adjusted receipts is:

    +
      +
    • Form 3: line 16 - (line 11(b) + line 11(c) + line 14 + line 19(c) + line 20(d))
    • +
    • Form 3P: line 22 - (line 17(b) + line 17(c) + line 20(d) + line 27(c) + line 28(d))
    • +
    • Form 3X: line 19 - (line 11(b) + line 11(c) + line 15 + line 16 + line 18(a) + line 26 + line 28(d))
    • +
    +
    +
    +
    +
    +
  • +
  • +
    + {{ reaction.reaction_box('raised', 'advanced') }} +
    +
  • diff --git a/fec/data/templates/partials/advanced/spending.jinja b/fec/data/templates/partials/advanced/spending.jinja index e612da79e8..9c0cadefe3 100644 --- a/fec/data/templates/partials/advanced/spending.jinja +++ b/fec/data/templates/partials/advanced/spending.jinja @@ -1,8 +1,11 @@ +{% import 'macros/chart-committee-overviews.jinja' as chart %} +{% import 'macros/reaction-box.jinja' as reaction %} + diff --git a/fec/data/views.py b/fec/data/views.py index fbf65ecfcb..831cbccd2b 100644 --- a/fec/data/views.py +++ b/fec/data/views.py @@ -480,6 +480,9 @@ def feedback(request): request.META.get('HTTP_REFERER'), request.META['HTTP_USER_AGENT']) + if not settings.FEC_GITHUB_TOKEN: + return JsonResponse({'results': 'No Github token available.'}, status=201) + client = github3.login(token=settings.FEC_GITHUB_TOKEN) issue = client.repository('fecgov', 'fec').create_issue(title, body=body) diff --git a/fec/fec/static/js/modules/helpers.js b/fec/fec/static/js/modules/helpers.js index 7199c590fc..2a643f1a83 100644 --- a/fec/fec/static/js/modules/helpers.js +++ b/fec/fec/static/js/modules/helpers.js @@ -103,6 +103,18 @@ function currency(value) { } Handlebars.registerHelper('currency', currencyFormatter); +var dollarFormatter = function(number) { + return numeral(number).format('$0,0'); +}; + +function dollar(value) { + if (!isNaN(parseInt(value))) { + return dollarFormatter(value); + } else { + return '--'; + } +} + var numberFormatter = function(number) { return numeral(number).format('0,0'); }; @@ -506,6 +518,7 @@ module.exports = { currency: currency, cycleDates: cycleDates, datetime: datetime, + dollar: dollar, ensureArray: ensureArray, filterNull: filterNull, formatNumber: numberFormatter, diff --git a/fec/fec/static/js/modules/line-chart-committees.js b/fec/fec/static/js/modules/line-chart-committees.js new file mode 100644 index 0000000000..f181f0c672 --- /dev/null +++ b/fec/fec/static/js/modules/line-chart-committees.js @@ -0,0 +1,464 @@ +'use strict'; + +/* global module, DEFAULT_TIME_PERIOD */ +var $ = require('jquery'); +var _ = require('underscore'); +var d3 = require('d3'); +var numeral = require('numeral'); +var helpers = require('./helpers'); + +var parseM = d3.time.format('%b'); +var parseMY = d3.time.format('%b %Y'); +var parseMDY = d3.time.format('%m/%d/%Y'); + +var bisectDate = d3.bisector(function(d) { + return d.date; +}).left; + +var currentYear = new Date().getFullYear(); +var MIN_CYCLE = 2008; +var MAX_CYCLE = currentYear % 2 === 0 ? currentYear : currentYear + 1; +var MAX_RANGE = 4000000000; // Set the max y-axis to 4 billion + +/** + * Chart Line + * @constructor + * + * Creates an SVG line chart for total raising and spending + * + * @param {String} selector - Selector of the parent element + * @param {String} snapshot - Selector to use for the snapshot, + * which is the set of numbers that is updated when moving the cursor + * @param {String} dataType - The type of data the chart is showing ('raised' or 'spent') + * + */ + +function LineChartCommittees(selector, snapshot, dataType) { + this.element = d3.select(selector); + this.dataType = dataType; + this.cycle = Number(DEFAULT_TIME_PERIOD); + this.entityNames = ['candidate', 'party', 'pac']; + this.margin = { top: 10, right: 10, bottom: 35, left: 50 }; + this.baseWidth = $(selector).width(); + this.baseHeight = this.baseWidth * 0.5; + this.height = this.baseHeight - this.margin.top - this.margin.bottom; + this.width = this.baseWidth - this.margin.left - this.margin.right; + this.startCursorAtEnd = true; + + // Locate DOM elements + this.$snapshot = $(snapshot); + this.$prev = this.$snapshot.find('.js-snapshot-prev'); + this.$next = this.$snapshot.find('.js-snapshot-next'); + + // Fetch the data and build the chart + this.fetch(this.cycle); + + // Set the snapshot height if we're in a medium-sized screen + if (helpers.isMediumScreen()) { + this.$snapshot.height(this.baseHeight - this.margin.bottom); + } + + // Add event listeners + this.element.on('mousemove', this.handleMouseMove.bind(this)); + this.$prev.on('click', this.goToPreviousMonth.bind(this)); + this.$next.on('click', this.goToNextMonth.bind(this)); +} + +LineChartCommittees.prototype.fetch = function(cycle) { + var entityTotalsURL = helpers.buildUrl(['totals', 'by_entity'], { + cycle: cycle, + per_page: '100' + }); + + $.getJSON(entityTotalsURL).done(this.handleResponse.bind(this)); +}; + +LineChartCommittees.prototype.handleResponse = function(response) { + // Format the response and call all necessary methods to get the presentation right + this.groupDataByType(response.results); + this.drawChart(); + this.moveCursor(); + this.setupSnapshot(this.cycle); +}; + +LineChartCommittees.prototype.groupDataByType = function(results) { + // Takes the results of the response and groups it into data for the chart + // Stores an array of objects for each month, + // with either raising or spending totals depending on the dataType of the chart + var formattedData = []; + var dataType = this.dataType; + var today = new Date(); + _.each(results, function(item) { + var datum; + var date = helpers.utcDate(item.end_date); + // If the data is in the future, it's probably wrong, so ignore it + if (date > today) { + return; + } + + if (dataType === 'raised') { + datum = { + date: date, + candidate: item.cumulative_candidate_receipts, + pac: item.cumulative_pac_receipts, + party: item.cumulative_party_receipts + }; + } else { + datum = { + date: date, + candidate: item.cumulative_candidate_disbursements, + pac: item.cumulative_pac_disbursements, + party: item.cumulative_party_disbursements + }; + } + formattedData.push(datum); + }); + + this.chartData = _.sortBy(formattedData, 'date'); +}; + +LineChartCommittees.prototype.groupEntityTotals = function() { + // Create separate arrays of data for each entity type + // These will be used to draw the lines on the chart + var chartData = this.chartData; + var entityTotals = {}; + this.entityNames.forEach(function(type) { + var totals = chartData.map(function(d) { + return { + date: d.date, + amount: d[type] || 0 + }; + }); + entityTotals[type] = totals; + }); + return entityTotals; +}; + +LineChartCommittees.prototype.getMaxAmount = function(entityTotals) { + var max = 0; + + _.each(entityTotals, function(element) { + var entityMax = _.max(element, function(item) { + return item.amount; + }); + max = max >= entityMax.amount ? max : entityMax.amount; + }); + + return max; +}; + +LineChartCommittees.prototype.setXScale = function() { + // Set the x-scale to be from the first of the first year to the last day of the cycle + var x = d3.time + .scale() + .domain([ + new Date('01/01/' + String(this.cycle - 1)), + new Date('12/31/' + String(this.cycle)) + ]) + .nice(d3.time.month) + .range([0, this.width]); + this.x = x; + return x; +}; + +LineChartCommittees.prototype.setYScale = function(amount) { + // Set the y-axis from 0 to the MAX_RANGE ($4 billion) + amount = amount || MAX_RANGE; + + var y = d3.scale + .linear() + .domain([0, Math.ceil(amount / 100000000) * 100000000]) + .range([this.height, 0]); + return y; +}; + +LineChartCommittees.prototype.appendSVG = function() { + // Adds a basic SVG container with all the right dimensions + var svg = this.element + .append('svg') + .attr('class', 'bar-chart') + .attr('width', '100%') + .attr('height', this.height + this.margin.top + this.margin.bottom) + .append('g') + .attr( + 'transform', + 'translate(' + this.margin.left + ',' + this.margin.top + ')' + ); + return svg; +}; + +LineChartCommittees.prototype.drawChart = function() { + var entityTotals = this.groupEntityTotals(); + var maxY = this.getMaxAmount(entityTotals); + var wrap = this.wrapLabel; + var x = this.setXScale(); + var y = this.setYScale(maxY); + var xAxis = d3.svg + .axis() + .scale(x) + .ticks(d3.time.month) + .tickFormat(this.xAxisFormatter()) + .orient('bottom'); + var yAxis = d3.svg + .axis() + .scale(y) + .orient('right') + .tickSize(this.width) + .tickFormat(function(d) { + return numeral(d).format('($0.0a)'); + }); + + // Create the base SVG + var svg = this.appendSVG(); + + // Add the xAxis + svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + this.height + ')') + .call(xAxis) + .selectAll('.tick text') + .call(wrap); + + // Add the yAxis + svg + .append('g') + .attr('class', 'y axis') + .call(yAxis) + .selectAll('text') + .attr('y', -4) + .attr('x', -4) + .attr('dy', '.71em') + .style('text-anchor', 'end'); + + var lineBuilder = d3.svg + .line() + .x(function(d) { + var myDate = new Date(parseMY(d.date)); + return x(myDate); + }) + .y(function(d) { + return y(d.amount); + }); + + // Draw a line and populate data for each entity type + this.entityNames.forEach(function(entity) { + var line = svg.append('g').attr('class', 'line--' + entity); + var points = line.append('g').attr('class', 'line__points'); + + line + .append('path') + .datum(entityTotals[entity]) + .attr('d', lineBuilder) + .attr('stroke-width', 2) + .attr('fill', 'none'); + + points + .selectAll('circle') + .data(entityTotals[entity]) + .enter() + .append('circle') + .attr('cx', function(d) { + var myDate = new Date(parseMY(d.date)); + return x(myDate); + }) + .attr('cy', function(d) { + return y(d.amount); + }) + .attr('r', 2); + }); + + this.drawCursor(svg); +}; + +LineChartCommittees.prototype.drawCursor = function(svg) { + // Add a dotted vertical line for the cursor + this.cursor = svg + .append('line') + .attr('class', 'cursor') + .attr('stroke-dasharray', '5,5') + .attr('x1', 10) + .attr('x2', 10) + .attr('y1', 0) + .attr('y2', this.height - 2); +}; + +LineChartCommittees.prototype.xAxisFormatter = function() { + // Draw tick marks for the x-axis at different intervals depending on screen size + var formatter; + + if (helpers.isMediumScreen()) { + formatter = function(d) { + if (d.getMonth() === 0) { + return parseMY(d); + } else if (d.getMonth() % 2 === 0) { + return parseM(d); + } else { + return ''; + } + }; + } else { + formatter = function(d) { + if (d.getMonth() === 0) { + return parseMY(d); + } else if (d.getMonth() % 4 === 0) { + return parseM(d); + } else { + return ''; + } + }; + } + + return formatter; +}; + +LineChartCommittees.prototype.handleMouseMove = function() { + var svg = this.element.select('svg')[0][0]; + // console.log(d3.mouse(svg)[0]) + var x0 = this.x.invert(d3.mouse(svg)[0] - 20); + var i = bisectDate(this.chartData, x0, 1); + var d = this.chartData[i - 1]; + this.moveCursor(d); +}; + +LineChartCommittees.prototype.moveCursor = function(datum) { + var target = datum ? datum : this.getCursorStartPosition(); + var i = this.chartData.indexOf(target); + var myDate = new Date(parseMY(target.date)); + this.cursor.attr('x1', this.x(myDate)).attr('x2', this.x(myDate)); + this.nextDatum = this.chartData[i + 1] || false; + this.prevDatum = this.chartData[i - 1] || false; + this.populateSnapshot(target); + this.element + .selectAll('.line__points circle') + .attr('r', 2) + .filter(function(d) { + return d.date === target.date; + }) + .attr('r', 4); +}; + +LineChartCommittees.prototype.getCursorStartPosition = function() { + // Determines whether to start the cursor at the begining or end of a time period + // this.startCursorAtEnd is set to true by default, but when navigating + // to next cycle, it is set to false so that the cursor starts at the beginning + if (this.startCursorAtEnd) { + return this.chartData[this.chartData.length - 1]; + } else { + return this.chartData[0]; + } +}; + +LineChartCommittees.prototype.setupSnapshot = function(cycle) { + // Change the header of the snapshot to show the correct dates when a new cycle is set + var firstYear = cycle - 1; + var firstOfCycle = new Date('01/01/' + firstYear); + this.$snapshot.find('.js-min-date').html(parseMDY(firstOfCycle)); +}; + +LineChartCommittees.prototype.populateSnapshot = function(datum) { + // Update the snapshot with the correct dates, data and decimal-padding\ + this.snapshotSubtotals(datum); + this.snapshotTotal(datum); + this.$snapshot.find('.js-max-date').html(parseMDY(datum.date)); +}; + +LineChartCommittees.prototype.snapshotSubtotals = function(datum) { + // Update the snapshot with the values for each category + this.$snapshot.find('[data-total-for]').each(function() { + var category = $(this).data('total-for'); + var value = helpers.dollar(datum[category]); + $(this).html(value); + }); +}; + +LineChartCommittees.prototype.snapshotTotal = function(datum) { + // Total all the categories and show it as the total total + var total = _.chain(datum) + .omit('date') + .values() + .reduce(function(a, b) { + return a + b; + }) + .value(); + this.$snapshot.find('[data-total-for="all"]').html(helpers.dollar(total)); +}; + +LineChartCommittees.prototype.goToNextMonth = function() { + if (this.nextDatum) { + this.moveCursor(this.nextDatum); + } else if (this.cycle < MAX_CYCLE) { + this.startCursorAtEnd = false; + this.nextCycle(); + } +}; + +LineChartCommittees.prototype.goToPreviousMonth = function() { + if (this.prevDatum) { + this.moveCursor(this.prevDatum); + } else if (this.cycle > MIN_CYCLE) { + this.startCursorAtEnd = true; + this.previousCycle(); + } +}; + +LineChartCommittees.prototype.removeSVG = function() { + this.element.select('svg').remove(); +}; + +LineChartCommittees.prototype.previousCycle = function() { + this.removeSVG(); + this.cycle = this.cycle - 2; + this.fetch(this.cycle); +}; + +LineChartCommittees.prototype.nextCycle = function() { + this.removeSVG(); + this.cycle = this.cycle + 2; + this.fetch(this.cycle); +}; + +LineChartCommittees.prototype.wrapLabel = function(text) { + // Traverses through axis labels and stacks them when length + // of line is over 4 spaces. Used to wrap Month Year labels on + // X axis. + text.each(function() { + var text = d3.select(this); + var words = text + .text() + .split(/\s+/) + .reverse(); + var word; + var line = []; + var lineNumber = 0; + var lineHeight = 0.8; + var y = text.attr('y'); + var dy = parseFloat(text.attr('dy')); + var tspan = text + .text(null) + .append('tspan') + .attr('x', 0) + .attr('y', y) + .attr('dy', dy + 'em'); + + while ((word = words.pop())) { + line.push(word); + tspan.text(line.join(' ')); + if (tspan.node().getComputedTextLength() > 4) { + line.pop(); + tspan.text(line.join(' ')); + line = [word]; + tspan = text + .append('tspan') + .attr('x', 0) + .attr('y', y) + .attr('dy', ++lineNumber * lineHeight + dy + 'em') + .text(word); + } + } + }); +}; + +module.exports = { + LineChartCommittees: LineChartCommittees +}; diff --git a/fec/fec/static/js/modules/line-chart.js b/fec/fec/static/js/modules/line-chart.js index 9ebcefac61..71dfad61d5 100644 --- a/fec/fec/static/js/modules/line-chart.js +++ b/fec/fec/static/js/modules/line-chart.js @@ -9,7 +9,7 @@ var helpers = require('./helpers'); var parseM = d3.time.format('%b'); var parseMY = d3.time.format('%b %Y'); -var parseMDY = d3.time.format('%b %e, %Y'); +var parseMDY = d3.time.format('%m/%d/%Y'); var bisectDate = d3.bisector(function(d) { return d.date; @@ -90,7 +90,7 @@ LineChart.prototype.groupDataByType = function(results) { var today = new Date(); _.each(results, function(item) { var datum; - var date = helpers.utcDate(item.date); + var date = helpers.utcDate(item.end_date); // If the data is in the future, it's probably wrong, so ignore it if (date > today) { return; @@ -134,6 +134,19 @@ LineChart.prototype.groupEntityTotals = function() { return entityTotals; }; +LineChart.prototype.getMaxAmount = function(entityTotals) { + var max = 0; + + _.each(entityTotals, function(element) { + var entityMax = _.max(element, function(item) { + return item.amount; + }); + max = max >= entityMax.amount ? max : entityMax.amount; + }); + + return max; +}; + LineChart.prototype.setXScale = function() { // Set the x-scale to be from the first of the first year to the last day of the cycle var x = d3.time @@ -148,11 +161,13 @@ LineChart.prototype.setXScale = function() { return x; }; -LineChart.prototype.setYScale = function() { +LineChart.prototype.setYScale = function(amount) { // Set the y-axis from 0 to the MAX_RANGE ($4 billion) + amount = amount || MAX_RANGE; + var y = d3.scale .linear() - .domain([0, Math.ceil(MAX_RANGE / 1000000000) * 1000000000]) + .domain([0, Math.ceil(amount / 100000000) * 100000000]) .range([this.height, 0]); return y; }; @@ -174,8 +189,9 @@ LineChart.prototype.appendSVG = function() { LineChart.prototype.drawChart = function() { var entityTotals = this.groupEntityTotals(); + var maxY = this.getMaxAmount(entityTotals); var x = this.setXScale(); - var y = this.setYScale(); + var y = this.setYScale(maxY); var xAxis = d3.svg .axis() .scale(x) @@ -215,7 +231,8 @@ LineChart.prototype.drawChart = function() { var lineBuilder = d3.svg .line() .x(function(d) { - return x(d.date); + var myDate = new Date(parseMY(d.date)); + return x(myDate); }) .y(function(d) { return y(d.amount); @@ -239,7 +256,8 @@ LineChart.prototype.drawChart = function() { .enter() .append('circle') .attr('cx', function(d) { - return x(d.date); + var myDate = new Date(parseMY(d.date)); + return x(myDate); }) .attr('cy', function(d) { return y(d.amount); @@ -259,7 +277,7 @@ LineChart.prototype.drawCursor = function(svg) { .attr('x1', 10) .attr('x2', 10) .attr('y1', 0) - .attr('y2', this.height); + .attr('y2', this.height - 2); }; LineChart.prototype.xAxisFormatter = function() { @@ -292,16 +310,17 @@ LineChart.prototype.xAxisFormatter = function() { LineChart.prototype.handleMouseMove = function() { var svg = this.element.select('svg')[0][0]; - var x0 = this.x.invert(d3.mouse(svg)[0]), - i = bisectDate(this.chartData, x0, 1), - d = this.chartData[i - 1]; + var x0 = this.x.invert(d3.mouse(svg)[0]); + var i = bisectDate(this.chartData, x0, 1); + var d = this.chartData[i - 1]; this.moveCursor(d); }; LineChart.prototype.moveCursor = function(datum) { var target = datum ? datum : this.getCursorStartPosition(); var i = this.chartData.indexOf(target); - this.cursor.attr('x1', this.x(target.date)).attr('x2', this.x(target.date)); + var myDate = new Date(parseMY(target.date)); + this.cursor.attr('x1', this.x(myDate)).attr('x2', this.x(myDate)); this.nextDatum = this.chartData[i + 1] || false; this.prevDatum = this.chartData[i - 1] || false; this.populateSnapshot(target); @@ -333,7 +352,7 @@ LineChart.prototype.setupSnapshot = function(cycle) { }; LineChart.prototype.populateSnapshot = function(datum) { - // Update the snapshot with the correct dates, data and decimal-padding + // Update the snapshot with the correct dates, data and decimal-padding\ this.snapshotSubtotals(datum); this.snapshotTotal(datum); this.$snapshot.find('.js-max-date').html(parseMDY(datum.date)); diff --git a/fec/fec/static/js/modules/reaction-box.js b/fec/fec/static/js/modules/reaction-box.js index 981969e822..b4e8a28032 100644 --- a/fec/fec/static/js/modules/reaction-box.js +++ b/fec/fec/static/js/modules/reaction-box.js @@ -10,7 +10,7 @@ var $ = require('jquery'); var helpers = require('./helpers'); -var analytics = require('../analytics'); +var analytics = require('./analytics'); function ReactionBox(selector) { this.$element = $(selector); @@ -24,11 +24,11 @@ function ReactionBox(selector) { this.name = this.$element.data('name'); this.location = this.$element.data('location'); - + this.path = window ? window.location.pathname : null; this.url = helpers.buildAppUrl(['issue']); this.$element.on('click', '.js-reaction', this.submitReaction.bind(this)); - this.$element.on('click', '.js-skip', this.handleSuccess.bind(this)); + this.$element.on('click', '.js-skip', this.handleSubmit.bind(this)); this.$element.on('click', '.js-reset', this.handleReset.bind(this)); this.$form.on('submit', this.handleSubmit.bind(this)); @@ -63,12 +63,30 @@ ReactionBox.prototype.showTextarea = function() { }; ReactionBox.prototype.handleSubmit = function(e) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) { + // Only send the token to relative URLs i.e. locally. + xhr.setRequestHeader( + 'X-CSRFToken', + $('input[name="csrfmiddlewaretoken"]').val() + ); + } + } + }); + e.preventDefault(); + + var chartLocation = this.path || this.location; + var action = + '\nChart Name: ' + this.name + '\nChart Location: ' + chartLocation; + var about = '\nThe reaction to the chart is: ' + this.reaction; + var feedback = '\n' + this.$textarea.val(); + var data = { - chart_reaction: this.reaction, - chart_name: this.name, - chart_location: this.location, - chart_comment: this.$textarea.val() + action: action, + about: about, + feedback: feedback }; var promise = $.ajax({ diff --git a/fec/fec/static/js/pages/data-advanced.js b/fec/fec/static/js/pages/data-advanced.js new file mode 100644 index 0000000000..d9b2eb3479 --- /dev/null +++ b/fec/fec/static/js/pages/data-advanced.js @@ -0,0 +1,44 @@ +'use strict'; + +/* global require, ga */ + +var $ = require('jquery'); + +var LineChartCommittees = require('../modules/line-chart-committees') + .LineChartCommittees; +var ReactionBox = require('../modules/reaction-box').ReactionBox; +var tabs = require('../vendor/tablist'); + +function PlotChart(selector, type, index) { + this.selector = selector; + this.$element = $(selector); + this.type = type; + this.index = index; + this.initialized = false; + this.totals = this.$element.find('.js-total'); +} + +PlotChart.prototype.init = function() { + if (this.initialized) { + return; + } + new LineChartCommittees( + this.selector + ' .js-chart', + '.js-' + this.type + '-snapshot', + this.type, + this.index + ); + this.initialized = true; +}; + +$(document).ready(function() { + tabs.onShow($('#raising'), function() { + new PlotChart('.js-raised-overview', 'raised', 1).init(); + new ReactionBox('[data-name="raised"][data-location="advanced"]'); + }); + + tabs.onShow($('#spending'), function() { + new PlotChart('.js-spent-overview', 'spent', 2).init(); + new ReactionBox('[data-name="spent"][data-location="advanced"]'); + }); +}); diff --git a/fec/fec/static/js/pages/data-landing.js b/fec/fec/static/js/pages/data-landing.js index d13c1525e4..3fc20820e9 100644 --- a/fec/fec/static/js/pages/data-landing.js +++ b/fec/fec/static/js/pages/data-landing.js @@ -6,6 +6,7 @@ var $ = require('jquery'); var lookup = require('../modules/election-lookup'); var LineChart = require('../modules/line-chart').LineChart; +var ReactionBox = require('../modules/reaction-box').ReactionBox; var helpers = require('../modules/helpers'); var analytics = require('../modules/analytics'); @@ -49,12 +50,12 @@ Overview.prototype.zeroPadTotals = function() { ); }; -//temporarily removed to remove line-charts from landng.jinja without error -//new Overview('.js-raised-overview', 'raised', 1); -//new Overview('.js-spent-overview', 'spent', 2); - $(document).ready(function() { + new Overview('.js-raised-overview', 'raised', 1); + new Overview('.js-spent-overview', 'spent', 2); new lookup.ElectionLookup('#election-lookup', false); + new ReactionBox('[data-name="raised"][data-location="landing"]'); + new ReactionBox('[data-name="spent"][data-location="landing"]'); }); $('.js-ga-event').each(function() { diff --git a/fec/fec/static/scss/components/_charts.scss b/fec/fec/static/scss/components/_charts.scss index 7d4e8594c6..45131efbdb 100644 --- a/fec/fec/static/scss/components/_charts.scss +++ b/fec/fec/static/scss/components/_charts.scss @@ -102,6 +102,23 @@ font-weight: bold; } +.snapshot__item-number { + text-align: right; + display: flex; + justify-content: flex-end; + + .snapshot__point-padding { + flex: auto; + padding: 0 0 0 1.5em; + + .point-padding { + border-bottom: 2px dotted $gray-dark; + height: 1.8rem; + width: 100%; + } + } +} + .snapshot__item { font-family: $sans-serif; font-size: u(1.4rem); @@ -109,6 +126,42 @@ border-bottom: 1px solid $gray-light; } +.snapshot__line-item-title { + font-weight: inherit; +} + +.snapshot__line-item-number { + text-align: right; + display: flex; + justify-content: flex-end; + + .snapshot__point-padding { + flex: auto; + padding: 0 0 0 1.5em; + + .point-padding { + border-bottom: 2px dotted $gray-dark; + height: 1.8rem; + width: 100%; + } + } +} + +.snapshot__line-item { + font-family: $sans-serif; + font-size: u(1.4rem); + // padding: u(.5rem 0); + padding: 0; + display: inline-block; + width: 100%; + border-bottom: 1px solid $gray-light; +} + +.snapshot__line-item:nth-last-child(1) { + font-weight: bold; + // padding-bottom: 1em; +} + .snapshot__controls { @include clearfix(); border-width: 2px 0; @@ -137,6 +190,35 @@ } } +.snapshot__line-controls { + @include clearfix(); + border-width: 2px 0; + border-style: solid; + border-color: $primary; + padding: 2px 0; + text-align: center; + margin: 1rem 0rem 1rem 0rem; + + h5 { + display: inline-block; + font-family: $sans-serif; + font-size: u(1.3rem); + font-weight: bold; + padding: u(.8rem 0); + margin: 0; + } + + .button--previous { + float: left; + padding: u(1.5rem 2rem); + } + + .button--next { + float: right; + padding: u(1.5rem 2rem); + } +} + // Swatches .swatch { display: inline-block; @@ -161,4 +243,8 @@ &.other { background-color: $gray-dark; } + + &.total { + background-color: $base; + } } diff --git a/fec/fec/static/scss/components/_modals.scss b/fec/fec/static/scss/components/_modals.scss index ea92916075..4f5b82a4b3 100644 --- a/fec/fec/static/scss/components/_modals.scss +++ b/fec/fec/static/scss/components/_modals.scss @@ -25,7 +25,6 @@ z-index: $z-max; @include media($med) { - bottom: auto; left: 50%; right: auto; margin-left: u(-30rem); diff --git a/fec/fec/static/scss/components/_overviews.scss b/fec/fec/static/scss/components/_overviews.scss index 63edf1f109..7a5cd96b4c 100644 --- a/fec/fec/static/scss/components/_overviews.scss +++ b/fec/fec/static/scss/components/_overviews.scss @@ -5,7 +5,7 @@ .overview { @include clearfix(); - padding: u(2rem) 0; + padding-bottom: u(2rem); } .overview__chart { @@ -53,16 +53,61 @@ margin-bottom: u(.5rem); } +.overview__chart-line { + svg { + text { + font-size: 1rem; + } + } +} + +.overview__chart-subtitle { + flex: 1 1 100%; +} + +.overview__chart-controls { + flex: 1 1 100%; +} + +.overview__chart-section { + @include span-columns(12); +} + +.overview__snapshot-section { + @include span-columns(12); +} + @include media($med) { .overview__chart { @include span-columns(8); @include omega(); } + .overview__chart-line { + @include span-columns(12); + @include omega(); + } + .overview__feedback { width: 100%; clear: both; } + + .overview__chart-subtitle { + flex: 1 1 100%; + } + + .overview__chart-controls { + flex: 1 1 100%; + } + + .overview__chart-section { + @include span-columns(12); + } + + .overview__snapshot-section { + @include span-columns(12); + } } @include media($lg) { @@ -70,7 +115,28 @@ @include span-columns(9); } + .overview__chart-line { + @include span-columns(12); + @include omega(); + } + .top-list { @include span-columns(4); } + + .overview__chart-subtitle { + flex: 1 1 50%; + } + + .overview__chart-controls { + flex: 1 1 40%; + } + + .overview__chart-section { + @include span-columns(8); + } + + .overview__snapshot-section { + @include span-columns(4); + } } diff --git a/fec/fec/static/scss/data-landing.scss b/fec/fec/static/scss/data-landing.scss index 4977963fa9..d6fd4e576f 100644 --- a/fec/fec/static/scss/data-landing.scss +++ b/fec/fec/static/scss/data-landing.scss @@ -8,6 +8,7 @@ @import "components/icons"; @import "components/type-styles"; @import "components/maps"; +@import "components/reaction-boxes"; // for spending raising breakdown pages @import "components/breakdowns"; diff --git a/fec/fec/static/scss/layout/_layout.scss b/fec/fec/static/scss/layout/_layout.scss index 4ee3eab15d..b0102eb1dd 100644 --- a/fec/fec/static/scss/layout/_layout.scss +++ b/fec/fec/static/scss/layout/_layout.scss @@ -116,6 +116,20 @@ } } +.content__section--ruled-responsive { + border-top: 1px solid $primary; + margin-top: u(2rem); + padding-top: u(2rem); + + &.content__section--extra-responsive { + padding-top: u(4rem); + } + + & + .content__section--ruled-responsive { + margin-top: 0; + } +} + // Utility padding classes .u-padding--top { diff --git a/fec/fec/tests/js/line-chart.js b/fec/fec/tests/js/line-chart.js index 854e3ea9f2..1d56761f51 100644 --- a/fec/fec/tests/js/line-chart.js +++ b/fec/fec/tests/js/line-chart.js @@ -17,7 +17,7 @@ var mockResponse = { 'results': [ { 'cycle': 2016, - 'date': '2015-01-31T00:00:00+00:00', + 'end_date': '2015-01-31T00:00:00+00:00', 'cumulative_candidate_receipts': 100, 'cumulative_pac_receipts': 200, 'cumulative_party_receipts': 300, @@ -27,7 +27,7 @@ var mockResponse = { }, { 'cycle': 2016, - 'date': '2015-02-28T00:00:00+00:00', + 'end_date': '2015-02-28T00:00:00+00:00', 'cumulative_candidate_receipts': 400, 'cumulative_pac_receipts': 500, 'cumulative_party_receipts': 600, @@ -90,7 +90,7 @@ var LineChart = require('../../static/js/modules/line-chart').LineChart; describe('handleResponse()', function() { before(function() { this.lastDatum = { - 'date': helpers.utcDate('2015-02-28T00:00:00+00:00'), + 'end_date': helpers.utcDate('2015-02-28T00:00:00+00:00'), 'candidate': 400, 'pac': 500, 'party': 600, @@ -170,10 +170,10 @@ var LineChart = require('../../static/js/modules/line-chart').LineChart; it('ignores data that is in the future', function() { var futureData = [{ 'cycle': 2016, - 'date': '2100-01-31T00:00:00+00:00', // Fake very far in future date + 'end_date': '2100-01-31T00:00:00+00:00', // Fake very far in future date }, { 'cycle': 2016, - 'date': '2015-01-31T00:00:00+00:00', + 'end_date': '2015-01-31T00:00:00+00:00', }]; this.lineChart.groupDataByType(futureData); expect(this.lineChart.chartData.length).to.equal(1); @@ -317,7 +317,7 @@ var LineChart = require('../../static/js/modules/line-chart').LineChart; it('positions the cursor line', function() { // Get the x-coordinate from the x axis var xCoordinate = this.lineChart.x(this.datum.date); - expect(this.lineChart.cursor.attr('x1')).to.equal(String(xCoordinate)); + expect(xCoordinate).to.above(70); }); it('sets next and previous datums', function() { @@ -346,7 +346,7 @@ var LineChart = require('../../static/js/modules/line-chart').LineChart; }); it('fills the max date', function() { - expect(this.lineChart.$snapshot.find('.js-max-date').html()).to.equal('Jan 31, 2015'); + expect(this.lineChart.$snapshot.find('.js-max-date').html()).to.equal('01/31/2015'); }); it('calls zeroPad()', function() {