Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/3216 wccf map colors #3229

Merged
merged 5 commits into from
Oct 4, 2019
Merged
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
2 changes: 1 addition & 1 deletion fec/data/templates/macros/widgets.jinja
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% macro select__election_year(election_years, election_year, elementID, electionType = 'H') %}
<label for="election-year" class="breakdown__title label t-inline-block">Running in:</label>
<select id="{{ elementID }}" name="cycle" class="js-election-year form-element--inline" aria-controls="">
<select id="{{ elementID }}" name="cycle" class="js-widget-election-year form-element--inline" aria-controls="">
{% for eachYear in election_years | sort(reverse=True) %}
{% if electionType != 'P' or eachYear % 4 == 0 %}
<option
Expand Down
129 changes: 104 additions & 25 deletions fec/fec/static/js/modules/data-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* @example creation:
* this.map = new DataMap(htmlDomElement, {
* colorScale: ['#f0f9e8', '#a6deb4', '#7bccc4', '#2a9291', '#216a7a'],
* colorScale: ['#e2ffff', '#278887'],
* colorZero: '#ffffff',
* data: '',
* width: '300',
Expand All @@ -20,23 +20,23 @@
* addTooltips: true
* });
*
* @example data update:
* @example data update:
* this.map.handleDataRefresh(theData);
*/

const d3 = require('d3');
const chroma = require('chroma-js');
const topojson = require('topojson');
const colorbrewer = require('colorbrewer');
const states = require('../data/us-states-10m.json');
const stateFeatures = topojson.feature(states, states.objects.states).features;

const fips = require('./fips');

const compactRules = [['B', 9], ['M', 6], ['k', 3], ['', 0]];
const compactRules = [['B', 9], ['M', 6], ['K', 3], ['', 0]];

let defaultOpts = {
colorScale: colorbrewer.Set1,
colorScale: ['#e2ffff', '#278887'],
colorZero: '#ffffff',
quantiles: 4
};

Expand Down Expand Up @@ -66,7 +66,7 @@ function DataMap(elm, opts) {
* Initialize the map
* Called from {@see handleDataRefresh() } when needed
* Very similar to {@see applyNewData() }—enough that changes to one should be made to the other.
* TODO - make init() and applyNewData() share more functionality
* TODO: make init() and applyNewData() share more functionality
*/
DataMap.prototype.init = function() {
let instance = this;
Expand Down Expand Up @@ -116,13 +116,13 @@ DataMap.prototype.init = function() {
// Of all of the values across all DataMap instances, these are the smallest and largest values:
let minValue = minValue || Math.min(...totals);
let maxValue = maxValue || Math.max(...totals);
maxValue = trimmedMaxValue(minValue, maxValue);

// Decide the legend color scale for our values
let legendScale = chroma
.scale(this.opts.colorScale)
.domain([minValue, maxValue]);
let legendQuantize = d3.scale.linear().domain([minValue, maxValue]);

// Create the states SVG, color them, initialize mouseover interactivity
// (`selectAll()` will select elements if they exist, or will create them if they don't.)
this.svg
Expand All @@ -132,9 +132,14 @@ DataMap.prototype.init = function() {
.enter()
.append('path')
.attr('fill', function(d) {
return instance.getStateValue(d.id)
? legendScale(instance.getStateValue(d.id))
: instance.opts.colorZero;
return calculateStateFill(
instance.getStateValue(d.id),
legendScale,
legendQuantize,
instance.opts.colorZero,
instance.opts.addLegend,
quantiles
);
})
.attr('data-state', function(d) {
return fips.fipsByCode[d.id].STATE_NAME;
Expand Down Expand Up @@ -181,7 +186,7 @@ DataMap.prototype.handleDataRefresh = function(newData) {
* Updates the map with new data
* Called from {@see handleDataRefresh() } as needed
* Very similar to {@see init() }—enough that changes to one should be made to the other.
* TODO - make init() and applyNewData() share more functionality
* TODO: make init() and applyNewData() share more functionality
*/
DataMap.prototype.applyNewData = function() {
let instance = this;
Expand All @@ -204,6 +209,7 @@ DataMap.prototype.applyNewData = function() {

let minValue = minValue || Math.min(...totals);
let maxValue = maxValue || Math.max(...totals);
maxValue = trimmedMaxValue(minValue, maxValue);

let legendScale = chroma
.scale(this.opts.colorScale)
Expand All @@ -223,9 +229,14 @@ DataMap.prototype.applyNewData = function() {
else return 20 * i;
})
.attr('fill', function(d) {
return instance.getStateValue(d.id)
? legendScale(instance.getStateValue(d.id))
: instance.opts.colorZero;
return calculateStateFill(
instance.getStateValue(d.id),
legendScale,
legendQuantize,
instance.opts.colorZero,
instance.opts.addLegend,
quantiles
);
});

// The rest of applyNewData is back to the same code from init()
Expand Down Expand Up @@ -253,9 +264,10 @@ DataMap.prototype.applyNewData = function() {
function drawStateLegend(svg, scale, quantize, quantiles) {
let legendWidth = 40;
let legendBar = 35;
let ticks = quantize.ticks(quantiles); // TODO - WHAT DOES .ticks DO / WHAT IS IT?
// The number of ticks is just a guide.
// If the data is more evenly split into one or two above this number, it will be.
let ticks = quantize.ticks(quantiles);
// .ticks() returns an array of the values at the various breaking points
// The number of ticks (quantiles) is just a guide.
// If the data range is more evenly split into one or two above this number, it will be.
// e.g., if our range is $1M-$3M and we ask for four ticks, we'll probably only get three: $1M, $2M, $3M
// instead of $750K, $1.5M, $2.25M, $3M

Expand Down Expand Up @@ -294,16 +306,85 @@ function drawStateLegend(svg, scale, quantize, quantiles) {
.attr('height', 20)
.attr('font-size', '10px')
.attr('text-anchor', 'middle')
.text(function(d) {
// function(d,i)
// TODO - If we want to add the "<" from the comps, we'll need the i
// let toReturn = '< $' + compactNumber(d, compactRule).toString();
// if (i >= ticks.length - 1) toReturn += '+';
let toReturn = compactNumber(d, compactRule).toString();
.text(function(d, i) {
// d is the data; i is the increment position of the loop
let toReturn = '';

if (i < ticks.length - 1) {
// If we're looking at any block other than the last,
toReturn += '<';
toReturn += compactNumber(d, compactRule).toString();
} else {
// Otherwise, for the last element, use the penultimate value plus a plus
toReturn += compactNumber(ticks[i - 1], compactRule).toString();
toReturn += '+';
}

return toReturn;
});
}

/**
* Used to determine the fill color based on the value, scale, and quantiles of the legend
* @param {Number} value Value to be used to determine the color.
* @param {Function} legendScale Determines the color scale for the current range of values.
* @param {d3.scale} legendQuantize Represents the range of data.
* @param {Number} quantiles How many bars to include in the legend.
* @param {*} colorZero Color code to use if the value is 0.
* @param {Boolean} hasLegend Default: false. If a legend is being used, will "round" colors to those in the legend. If no legend is being used, colors will not be rounded.
* @returns {String} 'fill' value based on the parameters provided.
*/
function calculateStateFill(
value,
legendScale,
legendQuantize,
colorZero,
hasLegend = false,
quantiles
) {
let colorToReturn = colorZero;
let legendValueTicks = legendQuantize.ticks(quantiles);

if (!value || value == 0) {
// If the state value is zero, use the zero color (default) and be done
colorToReturn = colorZero;
} else if (!hasLegend) {
// If we aren't using the legend we don't have to stick to its color stops
colorToReturn = legendScale(value);
} else {
// Otherwise, let's figure out which legend color we should use
// Let's change the default to the highest color because we're checking if each value is less than each legend block
colorToReturn = legendScale(legendValueTicks[legendValueTicks.length - 1]);
// For each block in the legend
for (let i = 0; i < legendValueTicks.length; i++) {
// If this block's value is greater than this state's value, that's the color we want
if (value < legendValueTicks[i]) {
// so we'll grab the color for this block's value instead of the color for the state's value
colorToReturn = legendScale(legendValueTicks[i]);
break;
}
// Otherwise, check the next one
}
}

if (colorToReturn._rgb)
colorToReturn = 'rgba(' + colorToReturn._rgb.join(',') + ')';

return colorToReturn;
}

/**
* Used to adjust scales so the higher values don't skew the range / blow the curve,
* to show more variation in our map colors.
* @param {Number} minValue The smaller number / the starting point of the return value.
* @param {Number} maxValue The largest number on the scale.
* @returns {Number} A new maxValue about half-way between minValue and maxValue
* @example trimmedMaxValue(10, 100); // 55
*/
function trimmedMaxValue(minValue, maxValue) {
return minValue + (maxValue - minValue) * 0.5;
}

/**
* Creates the tooltip element and adds mouse listeners to states
* Called from {@see init() } if needed
Expand All @@ -323,7 +404,6 @@ function buildStateTooltips(svg, path, instance) {
.style('display', 'none');

// Go through our svg/map and assign the mouse listeners to each path
// TODO - Test on touch devices, too
svg
.selectAll('path')
.on('mouseover', function(d) {
Expand Down Expand Up @@ -391,7 +471,6 @@ function chooseRule(value) {
return compactRules.find(rule => {
return value >= Math.pow(10, rule[1]);
});
// [['B', 9], ['M', 6], ['k', 3], ['', 0]];
}

/**
Expand Down
3 changes: 1 addition & 2 deletions fec/fec/static/js/widgets/contributions-by-state-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ ContributionsByState.prototype.init = function() {

// Fire up the map
this.map = new DataMap(this.map, {
colorScale: ['#f0f9e8', '#a6deb4', '#7bccc4', '#2a9291', '#216a7a'],
colorZero: '#ffffff',
color: '#36BDBB',
data: '',
addLegend: true,
addTooltips: true
Expand Down