html`
<div style="position: fixed; top: 1%; right: 1%;">
${viewof stateSelection}
</div>
<div style="display: inline; float: left; width: 75vw;">
<span style="font-weight: 700; font-size: 18px;">Per capita fines and fees<br>collected by local governments in 2020</span>
</div>
${chart1}
`
chart1 = {
let height = 350;
let margin = {top: 20, right: 20, bottom: 20, left: 50};
let innerHeight = height - margin.top - margin.bottom;
let innerWidth = width - margin.left - margin.right;
let selectType = "per_capita";
let chartName = "per_capita";
let baseFilter = dataPerCapita.filter(d => d.state == "United States");
let baseExtDates = d3.extent(baseFilter, d => d.date);
// let baseExtData = d3.extent(baseFilter, d => d["value"]);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("id", chartName)
.attr("font-family", "'Open Sans', sans-serif");
const group = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
const x = d3.scaleTime()
.domain(baseExtDates)
.range([0, innerWidth]);
const y = d3.scaleLinear()
.domain([0, 75])
.range([innerHeight, 0]);
const xAxis = d3.axisBottom(x);
const yAxis = d3.axisLeft(y);
// X-Axis
group.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${innerHeight})`)
.call(xAxis)
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%Y"))
.tickSize(5)
.ticks(5)
)
.call(g => g.select(".domain").remove())
.style('color', 'rgb(129, 129, 129)')
.style('font-size', '14px')
.style('font-weight', 500)
.style('font-family', "'Open Sans', sans-serif")
.attr("stroke-opacity", 0.5);
// Y-Axis
group.append("g")
.attr("class", "y-axis")
.call(yAxis)
.call(d3.axisLeft(y)
.tickFormat(d3.format("$,.0f"))
.ticks(7)
)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", innerWidth + 50))
.call(g => g.selectAll(".tick text")
.style("font-size", "14px")
.style("font-weight", 500)
.attr("fill", "rgb(129, 129, 129)")
.attr("transform", "translate(0, -7)"))
.call(g => g.selectAll(".tick line")
.attr("transform", "translate(-20, 0)"))
.attr("stroke-opacity", 0.2)
.call(g => g.append("text")
.attr("text-anchor", "middle")
.style("font-size", "16px")
.attr("fill", "currentColor"))
.style('font-family', "'Open Sans', sans-serif");
// Area
let lineGroup = group.append("g")
.attr("class", "line-group");
let area = d3.area()
.defined(d => !isNaN(d[selectType]))
.curve(d3.curveCardinal.tension(0.8))
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d[selectType]));
let areaPath = lineGroup
.append("g")
.append("path")
.attr("stroke-width", 4);
group.append("line")
.attr("class", "y-highlight")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", innerHeight)
.attr("stroke", "#d3d3d3")
.attr("stroke-width", '30px')
.style("opacity", 0);
svg.node().drawData = function(selectState) {
let filtered = dataPerCapita.filter(d => selectState === d.state);
let selectLookup = d3.group(filtered, d => d.date.toISOString());
let selectDates = Array.from(selectLookup).map(d => d[1][0].date);
// let extData = d3.extent(filtered, d => d["value"]);
let extDates = d3.extent(filtered, d => d.date);
let selectSeries = d3.groups(filtered, d => d.state);
x.domain(extDates);
// y.domain([0, 75]).nice();
areaPath
.data(selectSeries)
.transition()
.duration(1000)
.attr("d", d => area(d[1]))
.attr("class", function(d) {
return 'ts-line ts-line-' + d[0]; //
})
.attr("fill", dataDict[selectType].color);
// Chart toolTip Area (defines where hover with be active)
let tipArea = group.append("g")
.attr("class", "tip-area");
tipArea.append('svg:rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('opacity', 0)
.attr('pointer-events', 'all')
.on('mouseover', (event) => {
console.log('mouseover');
})
.on('mousemove', (event) => {
console.log('mousemove');
// Get the date on the y axis for the mouse position
let invert = x.invert(d3.pointer(event)[0])
let bisect = d3.bisector(function(d) { return d; }).center;
let hoverDate = (selectDates[bisect(selectDates, invert)]);
console.log(hoverDate);
// 1) Lookup/Get values by date 2) Filter by selection group 3) Sort by top values
let lookup = selectLookup.get(hoverDate.toISOString());
// Display and position vertical line
d3.select("#" + chartName + " .y-highlight")
.attr('x1',x(hoverDate))
.attr('x2',x(hoverDate))
.style('opacity', 0.4);
// Custom toolTip Content
let tipContent = `<hr style='margin:2px; padding:0; border-top: 3px solid #333;'>`;
lookup.map( d => {
tipContent += `
<b>Per Capita: ${format.per_capita(d["per_capita"])}</b><br>
Total: ${format.raw(d["raw"] * 1e3)}`
})
chartTip.style("left", () => {
if ((innerWidth - d3.pointer(event)[0]) < 199) {
return event.pageX - (200) + "px";
} else {
return event.pageX + 20 + "px";
}
})
.style("top", event.pageY + 5 + "px")
.style("display", "inline-block")
.html(`<strong>${format.date1(hoverDate)}</strong>${tipContent}`); // toolTip Content
})
.on('mouseout', (event) => {
console.log('mouseout');
chartTip.style("display", "none"); // Hide toolTip
d3.select("#" + chartName + " .y-highlight").style('opacity', 0); // Hide y-highlight line
d3.selectAll("#" + chartName + " .y-highlight-point").remove(); // Remove y-highlight points
});
};
return svg.node();
}
chartTip = d3.select("body").append("div").attr("class", "toolTip chartTip");
format = ({
date1: d3.timeFormat("%Y"),
per_capita: d3.format("$,.2f"),
raw: d3.format("$,")
})
dataDict = ({
"per_capita": {
label: "Local Fines & Fees",
color: "#2879cbbf",
lineColor: "#2879cbbf"
}
})
tempData = FileAttachment("./data/local_revenue_v3.csv").csv().then(
function(data) {
data.forEach(function(d) {
d.date = d3.timeParse("%Y")(d.year);
});
return data;
}
)
// dataPerCapita = tempData;
// dataRaw = aq.from(tempData)
// .derive({value: d => +d.value})
// .filter(d => d.type == "raw")
// .orderby("date")
// .objects();
dataPerCapita = aq.from(tempData)
// .derive({value: d => +d.value})
// .filter(d => d.type == "per capita")
.orderby("date")
.objects();
viewof stateSelection = {
const options = [
{name: "United States", value: ["United States", null, null], selected: true},
{name: "Alabama", value: ["Alabama", 0, "01"]},
{name: "Alaska", value: ["Alaska", 1, "02"]},
{name: "Arizona", value: ["Arizona", 2, "04"]},
{name: "Arkansas", value: ["Arkansas", 3, "05"]},
{name: "California", value: ["California", 4, "06"]},
{name: "Colorado", value: ["Colorado", 5, "08"]},
{name: "Connecticut", value: ["Connecticut", 6, "09"]},
{name: "Delaware", value: ["Delaware", 7, "10"]},
// {name: "District of Columbia", value: ["DC", 8, "11"]},
{name: "Florida", value: ["Florida", 9, "12"]},
{name: "Georgia", value: ["Georgia", 10, "13"]},
{name: "Hawaii", value: ["Hawaii", 11, "15"]},
{name: "Idaho", value: ["Idaho", 12, "16"]},
{name: "Illinois", value: ["Illinois", 13, "17"]},
{name: "Indiana", value: ["Indiana", 14, "18"]},
{name: "Iowa", value: ["Iowa", 15, "19"]},
{name: "Kansas", value: ["Kansas", 16, "20"]},
{name: "Kentucky", value: ["Kentucky", 17, "21"]},
{name: "Louisiana", value: ["Louisiana", 18, "22"]},
{name: "Maine", value: ["Maine", 19, "23"]},
{name: "Maryland", value: ["Maryland", 20, "24"]},
{name: "Massachusetts", value: ["Massachusetts", 21, "25"]},
{name: "Michigan", value: ["Michigan", 22, "26"]},
{name: "Minnesota", value: ["Minnesota", 23, "27"]},
{name: "Mississippi", value: ["Mississippi", 24, "28"]},
{name: "Missouri", value: ["Missouri", 25, "29"]},
{name: "Montana", value: ["Montana", 26, "30"]},
{name: "Nebraska", value: ["Nebraska", 27, "31"]},
{name: "Nevada", value: ["Nevada", 28, "32"]},
{name: "New Hampshire", value: ["New Hampshire", 29, "33"]},
{name: "New Jersey", value: ["New Jersey", 30, "34"]},
{name: "New Mexico", value: ["New Mexico", 31, "35"]},
{name: "New York", value: ["New York", 32, "36"]},
{name: "North Carolina", value: ["North Carolina", 33, "37"]},
{name: "North Dakota", value: ["North Dakota", 34, "38"]},
{name: "Ohio", value: ["Ohio", 35, "39"]},
{name: "Oklahoma", value: ["Oklahoma", 36, "40"]},
{name: "Oregon", value: ["Oregon", 37, "41"]},
{name: "Pennsylvania", value: ["Pennsylvania", 38, "42"]},
{name: "Rhode Island", value: ["Rhode Island", 39, "44"]},
{name: "South Carolina", value: ["South Carolina", 40, "45"]},
{name: "South Dakota", value: ["South Dakota", 41, "46"]},
{name: "Tennessee", value: ["Tennessee", 42, "47"]},
{name: "Texas", value: ["Texas", 43, "48"]},
{name: "Utah", value: ["Utah", 44, "49"]},
{name: "Vermont", value: ["Vermont", 45, "50"]},
{name: "Virginia", value: ["Virginia", 46, "51"]},
{name: "Washington", value: ["Washington", 47, "53"]},
{name: "West Virginia", value: ["West Virginia", 48, "54"]},
{name: "Wisconsin", value: ["Wisconsin", 49, "55"]},
{name: "Wyoming", value: ["Wyoming", 50, "56"]}
];
const form = html`<form style="display: flex; align-items: center; font-size: 16px;"><select name=i>${options.map(o => Object.assign(html`<option>`, {textContent: o.name, selected: o.selected}))}`;
form.i.onchange = () => form.dispatchEvent(new CustomEvent("input"));
form.oninput = () => form.value = options[form.i.selectedIndex].value;
form.oninput();
return form;
}
aq = {
const aq = await require(`arquero@${aq_version}`);
// load and install any additional packages
(await Promise.all(aq_packages.map(pkg => require(pkg))))
.forEach(pkg => aq.addPackage(pkg));
// Add HTML table view method to tables
aq.addTableMethod('view', toView, { override: true });
return aq;
};
aq_version = "5.1.0";
aq_packages = [];
op = aq.op;
toView = {
const DEFAULT_LIMIT = 100;
const DEFAULT_NULL = value => `<span style="color: #999;">${value}</span>`;
const tableStyle = 'margin: 0; border-collapse: collapse; width: initial;';
const cellStyle = 'padding: 1px 5px; white-space: nowrap; overflow-x: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums;';
// extend table prototype to provide an HTML table view
return function(dt, opt = {}) {
// permit shorthand for limit
if (typeof opt === 'number') opt = { limit: opt };
// marshal cell color options
const color = { ...opt.color };
if (typeof opt.color === 'function') {
// if function, apply to all columns
dt.columnNames().forEach(name => color[name] = opt.color);
} else {
// otherwise, gather per-column color options
for (const key in color) {
const value = color[key];
color[key] = typeof value === 'function' ? value : () => value;
}
}
// marshal CSS styles as toHTML() options
const table = `${tableStyle}`;
const td = (name, index, row) => {
return `${cellStyle} max-width: ${+opt.maxCellWidth || 300}px;`
+ (color[name] ? ` background-color: ${color[name](index, row)};` : '');
};
opt = {
limit: DEFAULT_LIMIT,
null: DEFAULT_NULL,
...opt,
style: { table, td, th: td }
};
// return container div, bind table value to support viewof operator
const size = `max-height: ${+opt.height || 270}px`;
const style = `${size}; overflow-x: auto; overflow-y: auto;`;
const view = html`<div style="${style}">${dt.toHTML(opt)}</div>`;
view.value = dt;
return view;
};
};
function formValue(form) {
const object = {};
for (const input of form.elements) {
if (input.disabled || !input.hasAttribute("name")) continue;
let value = input.value;
switch (input.type) {
case "range":
case "number": {
value = input.valueAsNumber;
break;
}
case "date": {
value = input.valueAsDate;
break;
}
case "radio": {
if (!input.checked) continue;
break;
}
case "checkbox": {
if (input.checked) value = true;
else if (input.name in object) continue;
else value = false;
break;
}
case "file": {
value = input.multiple ? input.files : input.files[0];
break;
}
case "select-multiple": {
value = Array.from(input.selectedOptions, option => option.value);
break;
}
}
object[input.name] = value;
}
return object;
}
function form(form) {
const container = html`<div>${form}`;
form.addEventListener("submit", event => event.preventDefault());
form.addEventListener("change", () => container.dispatchEvent(new CustomEvent("input")));
form.addEventListener("input", () => container.value = formValue(form));
container.value = formValue(form);
return container
}
style_sheet = html`<style>
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap');
body {
font-family: 'Open Sans', sans-serif;
}
select {
font-family: 'Open Sans', sans-serif;
// border-radius: 20px;
background-color: #fff;
}
.chip-circle {
border-radius: 50%;
display: inline-block;
position: relative;
width: 10px;
height: 10px;
}
label {
margin-top: 20px;
padding: 10px;
padding-right:15px;
text-align: center;
cursor: pointer;
background-color: #fff;
color: #2e3745;
font-weight: 600;
}
input[type="range"] {
-webkit-appearance: none;
border: none;
margin: 18px 0;
width: 100%;
box-shadow: -2px -2px 8px white, 2px 2px 8px rgba(black, 0.5);
}
input[type="range"]:focus {
outline: none;
}
input[type="range"]::-webkit-slider-runnable-track {
border: none;
width: 100%;
height: 5px;
cursor: pointer;
background: #fff;
border-radius: 10px;
box-shadow: rgb(204, 219, 232) 3px 3px 6px 0px inset, rgba(255, 255, 255, 0.5) -3px -3px 6px 1px inset;
}
input[type="range"]::-webkit-slider-thumb {
height: 15px;
width: 15px;
border-radius: 30px;
box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.37);
/* border: 1px solid #333; */
background: #fff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -5px;
}
input[type="range"]:focus::-webkit-slider-runnable-track {
background: #fff;
border: none;
}
input[type="range"]::-moz-range-track {
border: none;
width: 100%;
height: 5px;
cursor: pointer;
background: #fff;
border-radius: 10px;
box-shadow: rgb(204, 219, 232) 3px 3px 6px 0px inset, rgba(255, 255, 255, 0.5) -3px -3px 6px 1px inset;
}
input[type="range"]::-moz-range-thumb {
height: 15px;
width: 15px;
border-radius: 30px;
box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.37);
/* border: 1px solid #333; */
background: #fff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -5px;
}
input[type="range"]::-ms-track {
border: none;
width: 100%;
height: 5px;
cursor: pointer;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
}
input[type="range"]::-ms-fill-lower {
background: #e0e0e0;
border-radius: 2.6px;
}
input[type="range"]::-ms-fill-upper {
background: #e0e0e0;
border-radius: 2.6px;
}
input[type="range"]::-ms-thumb {
border: none;
height: 20px;
width: 16px;
border-radius: 3px;
background: #ffffff;
cursor: pointer;
}
input[type="range"]:focus::-ms-fill-lower {
background: #fff;
}
input[type="range"]:focus::-ms-fill-upper {
background: #fff;
}
#grid-3 {
display: grid;
grid-template-columns: auto auto auto;
background-color: #fff;
}
#grid-4 {
display: grid;
grid-template-columns: auto auto auto auto;
background-color: #fff;
}
#grid-4-full {
display: grid;
grid-template-columns: auto auto auto auto;
background-color: #fff;
border-top: 1px solid #d3d3d3;
border-right: 1px solid #d3d3d3;
border-bottom: 1px solid #d3d3d3;
border-left: 1px solid #d3d3d3;
}
#grid-4-top {
display: grid;
grid-template-columns: auto auto auto auto;
background-color: #fff;
border-top: 1px solid #d3d3d3;
border-right: 1px solid #d3d3d3;
border-left: 1px solid #d3d3d3;
}
#grid-4-bottom {
display: grid;
grid-template-columns: auto auto auto auto;
background-color: #fff;
border-bottom: 1px solid #d3d3d3;
border-right: 1px solid #d3d3d3;
border-left: 1px solid #d3d3d3;
}
.grid-item {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid #d3d3d3;
padding: 20px;
font-size: 16px;
text-align: left;
}
.toolTip {
position: absolute;
display: none;
min-width: 30px;
border-radius: 0;
height: auto;
background: #fff;
border: 1px solid #d3d3d3;
padding: 4px 8px;
font-size: .85rem;
text-align: left;
}
.eventTip { max-width: 140px;}
.chartTip { max-width: 240px;}
.tip-values {
font-weight: bold;
color: #787878;
}
.tip-values .secondary{
font-weight: normal;
}
.tip-table {
font-size: 12px;
text-align: right;
color: #787878;
}
.tip-table td, .tip-table th {
padding-right: 4px;
padding-left: 4px;
}
.tip-table th {
font-size: 10px;
text-align: right;
color: #AAA;
vertical-align: bottom;
}
.tip-table .row-header {
text-align: left;
}
.tip-table .primary {
font-weight: bold;
}
</style>`