|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
<link href="https://fonts.googleapis.com/css?family=Fira+Sans:300,400,500,700" rel="stylesheet" type="text/css"> |
|
<link href="style.css" rel="stylesheet" type="text/css"> |
|
<script src="https://d3js.org/d3.v5.min.js"></script> |
|
|
|
<body> |
|
|
|
<!-- |
|
if there is an order you prefer for your elements, consider pre-defining the |
|
g elements in your preferred order |
|
|
|
can also hard-code anything not based on the data itself (see the header/footer) |
|
//--> |
|
|
|
<svg width="600" height="750"> |
|
<!-- axis lines should be in back --> |
|
<g id="x"></g> <!-- x axis --> |
|
<g id="y"></g> <!-- y axis --> |
|
|
|
<!-- header (title and subtitle) --> |
|
<g id="header"> |
|
<text class="title" x="0" y="0" dx="5px" dy="36pt"> |
|
Gun deaths in Florida |
|
</text> |
|
|
|
<text class="subtitle" x="0" y="0" dx="5px" dy="58pt"> |
|
Number of murders committed using firearms |
|
</text> |
|
</g> |
|
|
|
<!-- footer (source and inspiration) --> |
|
<g id="footer"> |
|
<text x="0" y="750" dx="5px" dy="-22pt"> |
|
Source: Florida Department of Law Enforcement |
|
</text> |
|
|
|
<!-- there are links in svg too: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a --> |
|
<a href="https://tinyurl.com/theex4x"> |
|
<text x="0" y="750" dx="5px" dy="-6pt"> |
|
Inspiration: https://tinyurl.com/theex4x |
|
</text> |
|
</a> |
|
</g> |
|
|
|
<!-- actual visualization on top --> |
|
<g id="plot"> |
|
<g id="area"></g> <!-- area chart --> |
|
<g id="note"></g> <!-- area annotation --> |
|
<g id="line"></g> <!-- line chart --> |
|
<g id="points"></g> <!-- line markers --> |
|
<g id="bars"></g> <!-- heavy black axis lines --> |
|
</g> |
|
</svg> |
|
|
|
<script> |
|
// setup svg |
|
const svg = d3.select('body').select('svg'); |
|
|
|
// get current hard-coded size of svg |
|
const size = { |
|
width: parseInt(svg.attr('width')), |
|
height: parseInt(svg.attr('height')) |
|
}; |
|
|
|
// set padding |
|
const pad = {top: 100, right: 45, bottom: 75, left: 45}; |
|
|
|
// setup plot area |
|
const plot = svg.select('g#plot'); |
|
plot.attr("transform", translate(pad.left, pad.top)); |
|
|
|
// setup plot width and height |
|
const width = size.width - pad.left - pad.right; |
|
const height = size.height - pad.top - pad.bottom; |
|
|
|
// setup scales and ranges |
|
const x = d3.scaleLinear().range([0, width]); |
|
const y = d3.scaleLinear().range([0, height]); |
|
|
|
d3.csv("data.csv", convert).then(draw); |
|
|
|
function draw(data) { |
|
// filter out rows by year |
|
const filtered = data.filter(row => row.Year >= 1989 && row.Year < 2017); |
|
console.table(filtered); |
|
|
|
// setup our scale ranges now that we have data |
|
x.domain(d3.extent(filtered, row => row.Year)); |
|
y.domain([0, 1000]); |
|
|
|
// since our g elements are already in the proper order, we can draw our svg |
|
// elements in any order we want |
|
|
|
drawArea(filtered); |
|
drawLine(filtered); |
|
|
|
drawAxis(filtered); |
|
drawNote(filtered); |
|
} |
|
|
|
/* |
|
* converts data into easier to use format |
|
*/ |
|
function convert(input) { |
|
let output = {}; |
|
|
|
output.Year = +input['Year']; |
|
output.Population = +input['Population']; |
|
output.Total = +input['Total by Firearm']; |
|
output.Rate = +input['Murder by Firearm Rate per 100,000']; |
|
|
|
return output; |
|
} |
|
|
|
/* |
|
* draws the red area |
|
*/ |
|
function drawArea(data) { |
|
// svg area paths are time-consuming to create by hand |
|
// use the area generator in d3 to do it for us |
|
const area = d3.area() |
|
.curve(d3.curveLinear) |
|
.x(row => x(row.Year)) |
|
.y1(0) |
|
.y0(row => y(row.Total)); |
|
|
|
// we do not want to create one area per element so do not data join here |
|
// difference between calling .data(...) and .datum(...) |
|
// https://stackoverflow.com/questions/13728402/what-is-the-difference-d3-datum-vs-data |
|
plot.select('g#area') |
|
.append('path') |
|
.datum(data) |
|
.attr('d', area); |
|
} |
|
|
|
/* |
|
* draws the line and points |
|
*/ |
|
function drawLine(data) { |
|
// drawing the line is similar to the area |
|
const line = d3.line() |
|
.curve(d3.curveLinear) |
|
.x(row => x(row.Year)) |
|
.y(row => y(row.Total)); |
|
|
|
plot.select('g#line') |
|
.append('path') |
|
.datum(data) |
|
.attr('d', line); |
|
|
|
// drawing circles uses traditional data joins |
|
plot.select('g#points') |
|
.selectAll('circle') |
|
.data(data) |
|
.enter() |
|
.append('circle') |
|
.attr('cx', row => x(row.Year)) |
|
.attr('cy', row => y(row.Total)) |
|
.attr('r', 6); |
|
} |
|
|
|
/* |
|
* draws the axis lines |
|
*/ |
|
function drawAxis(data) { |
|
// setup x-axis to match inspiration |
|
const xaxis = d3.axisBottom(x) |
|
.tickSize(20, 0) |
|
.tickValues([1990, 2000, 2010]) |
|
.tickFormat(year => String(year) + "s"); |
|
|
|
// draw x-axis |
|
const xgroup = svg.select('g#x') |
|
.attr('transform', translate(pad.left, pad.top + height)) |
|
.call(xaxis); |
|
|
|
// shift each text label |
|
xgroup.selectAll('.tick text') |
|
.style('text-anchor', 'start') |
|
.attr('x', 6) |
|
.attr('y', 6); |
|
|
|
// setup y-axis and y-axis grid lines |
|
const yaxis = d3.axisLeft(y) |
|
.ticks(6) |
|
.tickSizeInner(-width); |
|
|
|
// draw y-axis |
|
svg.select('g#y') |
|
.attr('transform', translate(pad.left, pad.top)) |
|
.call(yaxis); |
|
|
|
// draw heavy axis lines |
|
plot.select('g#bars').selectAll('line') |
|
.data(y.range()) // one line for y-min and y-max |
|
.enter() |
|
.append('line') |
|
.attr('x1', 0) |
|
.attr('x2', width) |
|
.attr('y1', value => value) |
|
.attr('y2', value => value); |
|
} |
|
|
|
/* |
|
* draws annotation on top of area |
|
*/ |
|
function drawNote(data) { |
|
const note = plot.select('g#note'); |
|
|
|
// have to manually draw each line of annotation as separate text elements |
|
note.selectAll("text") |
|
// text with manual line breaks |
|
.data(['2005', 'Florida enacted', 'its "Stand Your', 'Ground" law'].reverse()) |
|
.enter() |
|
.append('text') |
|
.attr('id', function(value, index) { return 'note-' + index; }) |
|
.attr('x', x(2005)) |
|
.attr('y', y(450)) |
|
.attr('dx', x(2004) - x(2005)) |
|
.attr('dy', function(value, index) { return (-index * 2) + 'ex'; }) |
|
.text(value => value); |
|
|
|
note.append('line') |
|
.attr('x1', x(2005)) |
|
.attr('x2', x(2005)) |
|
.attr('y1', y(450) + 5) |
|
.attr('y2', y(521)); |
|
} |
|
|
|
/* |
|
* creates a svg translate string |
|
*/ |
|
function translate(x, y) { |
|
return "translate(" + String(x) + "," + String(y) + ")"; |
|
} |
|
</script> |
|
</body> |
|
</html> |