Skip to content

Commit

Permalink
Merge pull request #8537 from cfpb/tile-map-fixes
Browse files Browse the repository at this point in the history
Tile map for civil penalty fund
  • Loading branch information
wpears authored Oct 22, 2024
2 parents 87871db + 37b7d72 commit a9016d4
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 109 deletions.
7 changes: 5 additions & 2 deletions cfgov/unprocessed/css/on-demand/simple-chart.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
.highcharts-label {
font-family: 'Avenir Next', Arial, sans-serif;
font-size: 12px;
color: '#101820';
color: #101820;
text {
pointer-events: none;
}
Expand All @@ -60,6 +60,9 @@
}
}
}
.highcharts-data-label > span {
font-family: 'Avenir Next', Arial, sans-serif !important;
}
}
.filter-wrapper {
@include u-grid-column(1, 2);
Expand Down Expand Up @@ -236,7 +239,7 @@

.legend-title {
margin-bottom: 10px;
font-size: 14px;
font-size: 16px;
}

.legend-color,
Expand Down
24 changes: 24 additions & 0 deletions cfgov/unprocessed/js/routes/on-demand/simple-chart/chart-hooks.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import populations from './populations.js';

const hooks = {
// Example transform
monotonicY(data) {
Expand All @@ -6,6 +8,28 @@ const hooks = {
y: i + 1,
}));
},
//Includes very janky alignment hack
cpf_formatter() {
let v = this.point.value;
if (this.point.perCapita)
v = this.point.perCapita * populations[this.point.name];
const val = Math.round(v / 1e6);
const digits = Math.ceil(Math.log10(val));
return `<span style="visibility:hidden">${digits > 1 ? (digits > 2 ? 'o.' : 'o') : '.'}</span><span style="font-weight:500;">${
this.point.name
}</span><span style="visibility:hidden">${digits > 1 ? 'o' : ''}</span><br/><span style="font-weight:300">$${val}M</span>`;
},

cpf_labeller() {
let val = this.point.value;
if (this.point.perCapita) val *= populations[this.point.name];
return `<b style="font-size:18px; font-weight:600">${
this.point.label
}</b><br/>Payments to consumers: <b>$${Math.round(val).toLocaleString(
'en-US',
)}</b><br/>Number of consumers: <b>${this.point.consumers.toLocaleString('en-US')}
</b>`;
},

cct_yoy_transform(d) {
return d['Number of Loans'].map((v, i) => {
Expand Down
54 changes: 54 additions & 0 deletions cfgov/unprocessed/js/routes/on-demand/simple-chart/populations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const populations = {
AL: 5024294,
AK: 733374,
AZ: 7157902,
AR: 3011490,
CA: 39538212,
CO: 5773707,
CT: 3605912,
DE: 989946,
DC: 689548,
FL: 21538216,
GA: 10713771,
HI: 1455274,
ID: 1839117,
IL: 12813469,
IN: 6785442,
IA: 3190427,
KS: 2937835,
KY: 4506297,
LA: 4657785,
ME: 1363177,
MD: 6177253,
MA: 7032933,
MI: 10077674,
MN: 5706804,
MS: 2961306,
MO: 6154889,
MT: 1084244,
NE: 1961965,
NV: 3104617,
NH: 1377524,
NJ: 9289039,
NM: 2117525,
NY: 20202320,
NC: 10439459,
ND: 779079,
OH: 11799331,
OK: 3959411,
OR: 4237279,
PA: 13002788,
RI: 1097371,
SC: 5118422,
SD: 886668,
TN: 6910786,
TX: 29145459,
UT: 3271614,
VT: 643077,
VA: 8631373,
WA: 7705267,
WV: 1793713,
WI: 5893713,
WY: 576850,
};
export default populations;
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function makeChartOptions(data, dataAttributes) {
let defaultObj = cloneDeep(getDefaultChartObject(chartType));

if (styleOverrides) {
overrideStyles(styleOverrides, defaultObj, data);
overrideStyles(JSON.parse(styleOverrides), defaultObj, data);
}

if (xAxisSource && chartType !== 'datetime') {
Expand Down
179 changes: 129 additions & 50 deletions cfgov/unprocessed/js/routes/on-demand/simple-chart/tilemap-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import tilemap from 'highcharts/modules/tilemap';
import cloneDeep from 'lodash.clonedeep';
import defaultTilemap from './tilemap-styles.js';
import usLayout from './us-layout.js';
import populations from './populations.js';
import { alignMargin, formatSeries, overrideStyles } from './utils.js';

tilemap(Highmaps);
Expand All @@ -18,8 +19,10 @@ function makeTilemapOptions(data, dataAttributes) {

let defaultObj = cloneDeep(defaultTilemap);

let styles;
if (styleOverrides) {
overrideStyles(styleOverrides, defaultObj, data);
styles = JSON.parse(styleOverrides);
overrideStyles(styles, defaultObj, data);
}

const formattedSeries = formatSeries(data);
Expand All @@ -31,17 +34,19 @@ function makeTilemapOptions(data, dataAttributes) {

defaultObj = {
...defaultObj,
...getMapConfig(formattedSeries),
...getMapConfig(formattedSeries, defaultObj, styles.perCapita),
};

defaultObj.tooltip.formatter = function () {
const label = yAxisLabel ? yAxisLabel + ': ' : '';
return `<span style="font-weight:600">${
this.point.name
}</span><br/>${label}<span style="font-weight:600">${
Math.round(this.point.value * 10) / 10
}</span>`;
};
if (!defaultObj.tooltip.formatter) {
defaultObj.tooltip.formatter = function () {
const label = yAxisLabel ? yAxisLabel + ': ' : '';
return `<span style="font-weight:600">${
this.point.label
}</span><br/>${label}<span style="font-weight:600">${
Math.round(this.point.value * 10) / 10
}</span>`;
};
}

defaultObj.title = { text: undefined };
defaultObj.accessibility.description = description;
Expand All @@ -56,7 +61,7 @@ function makeTilemapOptions(data, dataAttributes) {
* Makes a legend for the tilemap
* @param {object} node - The chart node
* @param {object} data - The data object
* @param {string } legendTitle - The legend title
* @param {string} legendTitle - The legend title
*/
function updateTilemapLegend(node, data, legendTitle) {
const classes = data.colorAxis.dataClasses;
Expand All @@ -82,66 +87,140 @@ function updateTilemapLegend(node, data, legendTitle) {
title.innerText = legendTitle;
legend.appendChild(title);
}
if (data.perCapita) {
labels[0].innerText = 'Less';
labels[labels.length - 1].innerText = 'More';
for (let i = 1; i < labels.length - 1; i++) {
labels[i].innerText = '\xa0';
}
}
colors.forEach((v) => legend.appendChild(v));
labels.forEach((v) => legend.appendChild(v));
}

/**
*
* @param {number} v - A given step min or max
* @returns {Array} - An array with the step adjusted to label with a millions value or not
*/
function mLabel(v) {
if (v >= 1e6) {
return [v / 1e6, 'M'];
}
return [v, ''];
}

/**
*
* @param {number} v - Upper end of a given step
* @returns {number} - The step trimmed so the bins don't overlap
*/
function trimTenth(v) {
return Math.round((v - 0.1) * 10) / 10;
}

/**
*
* @param {number} s1 - step min
* @param {number} s2 - step max
* @param {boolean} isLast - whether we're operating on the last data class
* @returns {string} formatted legend label
*/
function formatLegendValues(s1, s2, isLast) {
const f1 = mLabel(s1);
const f2 = mLabel(s2);
return `$${f1[0]}${f1[1]} - $${isLast ? f2[0] : trimTenth(f2[0])}${f2[1]}`;
}

/**
*
* @param {number} s1 - step min
* @param {number} s2 - step max
* @param {string} color - hex color for legend class
* @param {boolean} isLast - whether we're operating on the last data class
* @returns {object} - dataClass object for highcharts
*/
function makeDataClass(s1, s2, color, isLast = 0) {
return {
from: s1,
to: s2,
color,
name: formatLegendValues(s1, s2, isLast),
};
}

/**
*
* @param {number} v - The raw number to get the divisor for
* @returns {number} a divisor which can round the number to its largest digit
*/
function makeDivisor(v) {
const precision = Math.floor(v).toString().length;
return Math.pow(10, precision - 1);
}

/**
* Generates a config object to be added to the chart config
* @param {Array} series - The formatted series data
* @param {object} defaultObj - The style object with overrides applied
* @param {boolean} perCapita - Whether data should be perCapita
* @returns {Array} series data with a geographic component added
*/
function getMapConfig(series) {
function getMapConfig(series, defaultObj, perCapita) {
let min = Infinity;
let max = -Infinity;
const data = series[0].data;
let dataMin = Infinity;
let data = series[0].data;
if (perCapita) {
data = data.map((v) => {
return {
...v,
perCapita: v.value / populations[v.name],
};
});
}

data.forEach((v) => {
const val = perCapita ? v.perCapita : v.value;
if (val < dataMin) dataMin = val;
});

const added = data.map((v) => {
const val = Math.round(Number(v.value) * 100) / 100;
const val =
Math.round(Number(perCapita ? v.perCapita : v.value) * 100) / 100;
if (val <= min) min = val;
if (val >= max) max = val;
return {
...usLayout[v.name],
state: v.name,
...v,
value: val,
};
});
min = Math.floor(min);
max = Math.ceil(max);
const step = Math.round((max - min) / 5);
const step1 = min + step;
const step2 = step1 + step;
const step3 = step2 + step;
const step4 = step3 + step;
const trimTenth = (v) => Math.round((v - 0.1) * 10) / 10;

const divisor = makeDivisor(min);
min = Math.floor(min / divisor) * divisor;
max = Math.ceil(max / divisor) * divisor;

let step = (max - min) / 5;
const stepDivisor = makeDivisor(step);
step = Math.round(step / stepDivisor) * stepDivisor;

const step1 = Math.round(min + step);
const step2 = Math.round(step1 + step);
const step3 = Math.round(step2 + step);
const step4 = Math.round(step3 + step);

return {
colorAxis: {
dataClasses: [
{
from: min,
to: step1,
color: '#addc91',
name: `${min} - ${trimTenth(step1)}`,
},
{
from: step1,
to: step2,
color: '#e2efd8',
name: `${step1} - ${trimTenth(step2)}`,
},
{
from: step2,
to: step3,
color: '#ffffff',
name: `${step2} - ${trimTenth(step3)}`,
},
{
from: step3,
to: step4,
color: '#d6e8fa',
name: `${step3} - ${trimTenth(step4)}`,
},
{ from: step4, color: '#7eb7e8', name: `${step4} - ${max}` },
],
dataClasses: defaultObj.dataClasses
? defaultObj.dataClasses
: [
makeDataClass(min, step1, '#d4eac6'),
makeDataClass(step1, step2, '#addc91'),
makeDataClass(step2, step3, '#48b753'),
makeDataClass(step3, step4, '#1e9642'),
makeDataClass(step4, max, '#187835', 1),
],
},
series: [{ clip: false, data: added }],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ const tilemap = {
enabled: true,
formatter: function () {
return `<span style="font-weight:500">${
this.point.state
this.point.name
}</span><br/><span style="font-weight:300">${Math.round(
this.point.value,
)}</span>`;
},
style: {
textOutline: false,
fontSize: 14,
fontSize: '13px',
},
},
},
Expand All @@ -46,6 +46,7 @@ const tilemap = {
style: {
fontFamily: 'Avenir Next',
fontSize: '16px',
lineHeight: '24px',
},
},
legend: {
Expand Down
Loading

0 comments on commit a9016d4

Please sign in to comment.