Skip to content

Instantly share code, notes, and snippets.

@sjengle
Last active February 18, 2020 19:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sjengle/8c2b8064e0e6971a89f0 to your computer and use it in GitHub Desktop.
Save sjengle/8c2b8064e0e6971a89f0 to your computer and use it in GitHub Desktop.
Florida Gun Deaths
height: 750

Florida Gun Deaths

This block provides a rough re-creation of the Florida Gun Deaths visualization. Keeping in mind the inspiration and issues discussed, try to redesign this visualization. Focus first on editing the style (colors, thickness of lines, etc.) before changing the encoding or visualization technique.

The goal of this exercise is to illustrate how small design choices can have a large impact on how we perceive the data, as well as how hard it is to come up with a visualization that is effective, accurate, and impactful.

Original Visualization

The original visualization appeared in the article This Chart Shows An Alarming Rise In Florida Gun Deaths After 'Stand Your Ground' Was Enacted article by Pamela Engel on Business Insider on February 18, 2014.

⚠️ The author of the original chart is no longer on Twitter, so the following links no longer work. An archive of the original discussion can be found on the Wayback Machine for reference. They are kept here for reference:

The chart was created by Christine Chen. She responded to the discussion, and linked to her inspiration for the graphic.

The inspiration quoted appeared in the infographic Iraq's bloody toll by Simon Scarr on South China Morning Post on December 17, 2011.

Online Discussion

There was a lot of discussion, especially on Twitter and various blogs, when this visualization came out. Here are some of those posts:

Original Data

You can access the original (and updated) data for this visualization from the Florida Statistical Analysis Center. Specifically, you can download the "Murder: Firearm Type and Rate for Florida" dataset in PDF or Excel format the UCR Offense Data page.

Trifacta Recipe

To generate the data.csv file, the original 1971_fwd_murder_firearms.xlsx file was transformed using the following Trifacta Wrangler recipe:

header sourcerownumber: 3
delete row: IN($sourcerownumber, [1,2,4])
delete row: IN($sourcerownumber, [53, 54, 55, 56, 57, 58])
textformat col: Year type: removesymbols
replacemismatched col: Year~{Population % Change} type: Float with: null text: ''
derive type: single value: {Total by Firearm} / (Population / 100000) as: 'Murder by Firearm Rate per 100,000'
Year Population Total Murders Murder Rate per 100,000 Total by Firearm Murder by Firearm Rate per 100,000 Total Handgun Total Other Firearm Total Percent by Firearms Percent by Handguns Percent by Other Firearms Total by Firearm % change Population % Change
1971 7041074 932 13.2 628 8.919093876871624 509 119 67.4 54.6 12.8
1972 7441545 924 12.4 642 8.627240714125897 539 103 69.5 58.3 11.1 2.2 5.7
1973 7845092 1182 15.1 769 9.80230697103361 637 132 65.1 53.9 11.2 19.8 5.4
1974 8248851 1190 14.4 848 10.28021963301313 687 161 71.3 57.7 13.5 10.3 5.1
1975 8485230 1132 13.3 747 8.803532726867745 616 131 66 54.4 11.6 -11.9 2.9
1976 8551814 902 10.5 575 6.723719669300571 466 109 63.7 51.7 12.1 -23 0.8
1977 8717334 857 9.8 507 5.815998331600006 415 92 59.2 48.4 10.7 -11.8 1.9
1978 8967206 949 10.6 542 6.04424611188814 437 105 57.1 46 11.1 6.9 2.9
1979 9245231 1084 11.7 650 7.030651803075553 526 124 60 48.5 11.4 19.9 3.1
1980 9579497 1387 14.5 839 8.758288665887154 702 137 60.5 50.6 9.9 29.1 3.6
1981 10097754 1523 15.1 890 8.813841176958757 768 122 58.4 50.4 8 6.1 5.4
1982 10375332 1410 13.6 807 7.778064354952689 678 129 57.2 48.1 9.1 -9.3 2.7
1983 10591701 1203 11.4 571 5.391013209304152 483 88 47.5 40.1 7.3 -29.2 2.1
1984 10930389 1264 11.6 585 5.352051056920298 486 99 46.3 38.4 7.8 2.5 3.2
1985 11278547 1297 11.5 553 4.903113849682942 456 97 42.6 35.2 7.5 -5.5 3.2
1986 11657843 1371 11.8 628 5.386931355997846 532 96 45.8 38.8 7 13.6 3.4
1987 12043608 1368 11.4 697 5.787302276859227 569 128 51 41.6 9.4 11 3.3
1989 12797318 1405 10.978862914870131 888 6.938953927690162 700 188 63.2 49.8 13.4 3.1
1990 13150027 1387 10.5 873 6.6387696390281175 585 288 62.9 42.2 20.8 -1.7 2.8
1991 13195952 1276 9.7 806 6.107933705730363 565 241 63.2 44.3 18.9 -7.7 0.3
1992 13424416 1191 8.9 789 5.877350642292373 554 235 66.2 46.5 19.7 -2.1 1.7
1993 13608627 1187 8.7 800 5.878623905262448 525 275 67.4 44.2 23.2 1.4 1.4
1994 13878905 1152 8.3 739 5.324627555271832 517 222 64.1 44.9 19.3 -7.6 2
1995 14149317 1030 7.3 687 4.855358036009795 460 227 66.7 44.7 22 -7 1.9
1996 14411563 1077 7.5 668 4.635166914234077 62 -2.8 1.9
1997 14712922 1014 6.9 634 4.309137233242995 62.5 -5.1 2.1
1998 15000475 966 6.4 589 3.9265423261596717 392 197 60.97308488612836 40.57971014492754 20.39337474120083 -7.097791798107256 1.9544248246541374
1999 15322040 856 5.6 460 3.002211193809701 347 113 53.73831775700935 40.53738317757009 13.200934579439252 -21.901528013582343 2.143698782871876
2000 15982378 890 5.6 499 3.1221887005801015 366 133 56.067415730337075 41.12359550561798 14.9438202247191 8.478260869565217 4.309726381082415
2001 16331739 867 5.3 502 3.0737694252890035 339 163 57.90080738177624 39.10034602076124 18.800461361014996 0.6012024048096193 2.1859137607682664
2002 16674608 906 5.433411088284655 552 3.310422649815816 392 160 60.9271523178808 43.26710816777042 17.660044150110377 9.9601593625498 2.099402886612381
2003 17071508 924 5.412527118283868 586 3.432620012244964 396 190 63.41991341991342 42.857142857142854 20.562770562770563 6.159420289855073 2.3802658509273504
2004 17516732 946 5.400550741999136 555 3.1683992196717976 341 214 58.66807610993657 36.04651162790697 22.621564482029598 -5.290102389078498 2.6079945602930916
2005 17918227 881 4.91678110786296 521 2.9076537539121476 321 200 59.13734392735528 36.43586833144155 22.701475595913735 -6.126126126126126 2.2920656661299605
2006 18349132 1129 6.2 740 4.032888313191054 450 290 65.54472984942427 39.85828166519043 25.686448184233836 42.034548944337814 2.4048417290393744
2007 18680367 1202 6.4 825 4.416401455067772 506 319 68.63560732113145 42.09650582362729 26.53910149750416 11.486486486486488 1.8051807573241065
2008 18807219 1168 6.21038123711964 780 4.147343634377842 456 324 66.78082191780823 39.04109589041096 27.73972602739726 -5.454545454545454 0.6790658877312207
2009 18750483 1017 5.423860281359152 695 3.706571185392931 412 283 68.33824975417896 40.511307767944935 27.826941986234022 -10.897435897435898 -0.30167139543597593
2010 18771768 987 5.25789579329981 669 3.5638624981940965 389 280 67.78115501519757 39.41236068895643 28.368794326241137 -3.741007194244604 0.11351707580012739
2011 18905048 985 5.210248606615545 691 3.6551084133719205 390 301 70.15228426395939 39.59390862944163 30.558375634517766 3.288490284005979 0.7100023823009106
2012 19074434 1009 5.3 722 3.7851712926318024 358 364 71.55599603567889 35.4806739345887 36.075322101090194 4.486251808972503 0.8959829141930769
2013 19259543 971 5 695 3.6086006817503407 314 381 71.57569515962925 32.33779608650875 39.237899073120495 -3.739612188365651 0.9704560565204714
2014 19507369 983 5.03912136998075 690 3.5371248680434557 289 401 70.19328585961343 29.399796541200406 40.79348931841302 -0.7194244604316548 1.286769888569007
2015 19815183 1040 5.2 767 3.870769197539079 340 427 73.75 32.69230769230769 41.05769230769231 11.159420289855072 1.577937034973809
2016 20148654 1108 5.5 847 4.203754752054405 405 442 76.44404332129963 36.55234657039711 39.891696750902526 10.430247718383312 1.6829064864048948
2017 20484142 1057 5.160089204614965 791 3.861523709413848 379 412 74.83443708609272 35.856196783349105 38.978240302743615 -6.6115702479338845 1.6650640782257715
2018 20840986 1107 5.3 836 4.011326527449325 454 382 75.5 41.1 34.5 4.7 1.7
<!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>
body {
margin: 5px;
padding: 0px;
text-align: center;
}
text {
font-family: 'Fira Sans', sans-serif;
font-weight: 400;
font-size: 12pt;
}
#area path {
fill: #970008;
fill-opacity: 0.85;
stroke: none;
}
#line path {
fill: none;
stroke: black;
stroke-width: 4;
}
#points circle {
fill: black;
stroke: white;
stroke-width: 1.5;
}
#y path,
#x path {
fill: none;
stroke: none;
}
#y line {
stroke: #A6A2A1;
stroke-width: 2.25;
stoke-opacity: 0.25;
}
#x line,
#bars {
stroke: black;
stroke-width: 2.25;
}
text.title {
font-weight: 700;
font-size: 36pt;
}
text.subtitle {
font-weight: 300;
font-size: 18pt;
}
#note text {
font-weight: 300;
font-size: 18pt;
fill: white;
}
#note #note-3 {
font-weight: 500;
}
#note line {
stroke: white;
stroke-width: 2px;
stroke-opacity: 1;
}
<!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>
<svg width="600" height="750">
<g id="x"></g>
<g id="y"></g>
<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>
<g id="footer">
<text x="0" y="750" dx="5px" dy="-22pt">
Source: Florida Department of Law Enforcement
</text>
<a href="https://tinyurl.com/theex4x">
<text x="0" y="750" dx="5px" dy="-6pt">
Inspiration: https://tinyurl.com/theex4x
</text>
</a>
</g>
<g id="plot">
<g id="area"></g>
<g id="note"></g>
<g id="line"></g>
<g id="points"></g>
<g id="bars"></g>
</g>
</svg>
<script>
const svg = d3.select('body').select('svg');
const size = {
width: parseInt(svg.attr('width')),
height: parseInt(svg.attr('height'))
};
const pad = {top: 100, right: 45, bottom: 75, left: 45};
const plot = svg.select('g#plot');
plot.attr("transform", translate(pad.left, pad.top));
const width = size.width - pad.left - pad.right;
const height = size.height - pad.top - pad.bottom;
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([0, height]);
d3.csv("data.csv", convert).then(draw);
function draw(data) {
const filtered = data.filter(row => row.Year >= 1989 && row.Year < 2017);
console.table(filtered);
x.domain(d3.extent(filtered, row => row.Year));
y.domain([0, 1000]);
drawArea(filtered);
drawLine(filtered);
drawAxis(filtered);
drawNote(filtered);
}
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;
}
function drawArea(data) {
// TODO
}
function drawLine(data) {
// TODO
}
function drawAxis(data) {
const xaxis = d3.axisBottom(x)
.tickSize(20, 0)
.tickValues([1990, 2000, 2010])
.tickFormat(year => String(year) + "s");
// TODO
const yaxis = d3.axisLeft(y)
.ticks(6)
.tickSizeInner(-width);
// TODO
}
function drawNote(data) {
const note = plot.select('g#note');
const lines = ['2005', 'Florida enacted', 'its "Stand Your', 'Ground" law'];
// TODO
}
function translate(x, y) {
return "translate(" + String(x) + "," + String(y) + ")";
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment