console.log("d3.version", d3.version) console.log("crossfilter.version", crossfilter.version) var layers = [], chart, chartDiv; // formats var fmt = d3.format("02d"), fmt1dec = d3.format(".1f"), fmtM = d3.format(",d"); // ensure at least 700px height if running in an iframe (bl.ocks) // http://bl.ocks.org/mbostock/1093025 d3.select(self.frameElement).transition().duration(500).style("height", "600px"); // build the map var mapId = "mapbox.streets"; // dark map L.mapbox.accessToken = 'pk.eyJ1IjoiYmNhbzYiLCJhIjoiY2phazdzdnZjMmZwaDJ3cGRhbHYzbDJlbyJ9.W6eoGsJU4BbJoz4yglvRcw'; var map = L.mapbox.map("mapDiv", mapId).setView([33, -100], 4); // create layer to hold shooting events var shootingsLayer = L.geoJson(null, { pointToLayer: scaledPoint }).addTo(map); // helper functions to set map marker attributes function pointColor(feature) { return feature.properties.Dead > 0 ? "red" : "orange"; } function pointRadius(feature) { return Math.max(feature.properties.Dead * 2, 6); } function scaledPoint(feature, latlng) { // generate html for popup var html = "
" + feature.properties.Summary + "
"; html += "
"; html += "Location: " + feature.properties.Location + '
'; html += "Date: " + feature.properties.displayDate + "
"; html += "Dead: " + feature.properties.Dead + "
"; html += "Injured: " + feature.properties.Injured + "
"; /*html += "References:
"; // generate an anchor element for each reference feature.properties.References.forEach(function(d, i) { // http://stackoverflow.com/questions/8498592/extract-root-domain-name-from-string var matches = d.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); var domain = matches && matches[1]; html += "  " + domain + "
"; })*/ // return the marker return L.circleMarker(latlng, { radius: pointRadius(feature), fillColor: pointColor(feature), fillOpacity: 0.6, weight: 0.5, color: "lightred" }).bindPopup(html); } // get reference to chart div chartDiv = d3.select(".chart"); // add a reset anchor element chartDiv.select(".title") .append("a") .attr("href", "javascript:reset()") .attr("class", "reset") .text("Reset filter") .style("margin-left", "10px") .style("display", "none"); // get the data d3.json("shootings3.GeoJSON", function(error, data) { var featureCount = data.features.length; // process the data data.features.forEach(function(d) { var c = d.properties.Date.split("\/"), year = +c[2], month = +c[0] - 1, day = +c[1], date = new Date(year, month, day), week = d3.time.week(date), weekOfYear = d3.time.weekOfYear(week); d.properties.date = date; d.properties.displayDate = date.toString().substring(0, 16); d.properties.week = week; d.properties.weekNumber = fmt(weekOfYear); d.properties.year = week.getYear() + 1900; d.properties.weekBin = (week.getYear() + 1900) + "-" + fmt(weekOfYear); }) // get extent (earliest/latest) dates var dateExtent = d3.extent(data.features, function(d) { return d.properties.date; }) // add one week to extent dateExtent[1].setDate(dateExtent[1].getDate() + 7) console.log(dateExtent); // sort the features in date order data.features.sort(function(a, b) { if (a.properties.date > b.properties.date) return 1; if (a.properties.date < b.properties.date) return -1; return 0; }) // init crossfilter var cf = crossfilter(data.features) // add week dimension var week = cf.dimension(function(d) { return d.properties.week; }); // create groups from the week dimension var countGroup = week.group(d3.time.week); var deadGroup = week.group().reduceSum(function(d) { return d.properties.Dead }) var injuredGroup = week.group().reduceSum(function(d) { return d.properties.Injured }) //console.log("deadGroup.top()", deadGroup.top(Infinity)) //console.log("deadGroup.all())", deadGroup.all()) //console.log("injuredGroup.top()", injuredGroup.top(Infinity)) // add tracking dimension and group var weekTracker = cf.dimension(function(d) { return d.properties.week; }); var countTracker = weekTracker.group(d3.time.week); // add event handlers to toggle data series view (based on the groups defined above) d3.selectAll("input[type=radio][name=view]") .on("change", function() { var elem = d3.select(this); var value = elem.property("value"); var group = value == "count" ? countGroup : value == "dead" ? deadGroup : injuredGroup; // rebuild the svg bar chart with the new data series chart .removeContents() .brushDirty(true) .group(group); // refresh viz renderAll(); }) // create the bar chart chart = barChart() .dimension(week) .group(countGroup) .round(d3.time.week.round) // ensures whole week .x(d3.time.scale() .domain(dateExtent) .rangeRound([0, 5 * countGroup.all().length + 1])) .filter([new Date(1966, 8, 1), new Date(2017, 11, 12)]) //.filter([new Date(2013, 1, 1), new Date(2013, 5, 1)]) //.filter(null) .on("brush", function() { renderAll(); }) .on("brushend", function() { renderAll(); }) .enableBrush(true); // init animator var anim = animator() .speed(1000) .chart(chart) .container(d3.select(".animControls")); // start the animator anim(countGroup.all()) //window.anim = anim; // uncomment this to get access to the animator object from the browser console // called at init, by the brush event handlers and by filter reset "a" elem function renderAll() { // run the chart function (these three are equivalent functionally) //d3.select(".chart").call(chart); chartDiv.call(chart); // chart(chartDiv) // get the filtered data var selected = week.top(Infinity); //console.log("selected", selected) // create geojson structure (needed for mapbox api) var geoJson = { type: "FeatureCollection", features: selected } // clear the shootings layer and add the new filtered data shootingsLayer .clearLayers() .addData(geoJson); // update info window var totalEvents = selected.length, groups = countTracker.all(), weeks = groups.reduce(function(p, v, i) { return groups[i].value > 0 ? p + 1 : p + 0 }, 0), deadInjuredCount = selected.reduce(function(p, c, i) { return { dead: selected[i].properties.Dead + p.dead, injured: selected[i].properties.Injured + p.injured } }, { dead: 0, injured: 0 }), avgDead = deadInjuredCount.dead / weeks, avgInjured = deadInjuredCount.injured / weeks; // create html for info window var html = ""; html += ""; html += " "; html += " "; html += " "; html += " "; if (weeks != 1) { html += " "; html += " "; } html += "
Number of Weeks" + weeks + "
Total Shootings" + fmtM(totalEvents) + "
Total Dead" + fmtM(deadInjuredCount.dead) + "
Total Injured" + fmtM(deadInjuredCount.injured) + "
Dead per Week" + fmt1dec(avgDead) + "
Injured per Week" + fmt1dec(avgInjured) + "
" // set the html d3.select(".infoDiv").html(html) } // initial render renderAll(); // resets the week filter (driven by the bar chart brush) window.reset = function() { if (anim) anim.exit(); chart.filter(null) renderAll(); }; // setup the bar chart (used to filter weeks) function barChart() { //TODO: remove this instance counter (in this viz, there is only one instance) if (!barChart.id) barChart.id = 0; var margin = {top: 10, right: 10, bottom: 20, left: 50}, x, y = d3.scale.linear().range([150, 0]), id = barChart.id++, axis = d3.svg.axis().orient("bottom"), brush = d3.svg.brush(), brushDirty, dimension, group, round, svg, gBrush, enableBrush, theDiv; // main bar chart function; will be called repeatedly by renderAll function chart(div) { // determine dimensions of svg var width = x.range()[1], height = y.range()[0]; // update y scale domain y.domain([0, group.top(1)[0].value]); // set y domain to max value in this group //TODO: inefficient code; div is an array of one div.each(function() { var div = theDiv = d3.select(this); var g = div.select("g"); // if g is empty, reubild the svg if (g.empty()) { // create svg and group g = div.append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")") .on("click", function() { // exit the animator if (anim) anim.exit(); //TODO: need to handle the case of mouse drag as well... }) // reset the clip path to full width g.append("clipPath") .attr("id", "clip-" + id) .append("rect") .attr("width", width) .attr("height", height); // generate two paths, one background, one foreground g.selectAll(".bar") .data(["background", "foreground"]) .enter().append("path") .attr("class", function(d) { return d + " bar"; }) // assign all the data in the group to the path .datum(group.all()); // assign the clip path to the foreground bars g.selectAll(".foreground.bar") .attr("clip-path", "url(#clip-" + id + ")"); // Initialize the brush component with pretty resize handles. if (enableBrush) { gBrush = g.append("g").attr("class", "brush").call(brush); gBrush.selectAll("rect").attr("height", height); gBrush.selectAll(".resize").append("path").attr("d", resizePath); } // add the x-axis last (so it's drawn on top of brush) g.append("g") .attr("class", "axis") .attr("transform", "translate(0," + height + ")") .call(axis); } // Only redraw the brush if set externally. // at init, the date chart has an externally set brush if (brushDirty) { brushDirty = false; g.selectAll(".brush").call(brush); div.select(".title a").style("display", brush.empty() ? "none" : null); if (brush.empty()) { g.selectAll("#clip-" + id + " rect") .attr("x", 0) .attr("width", width); } else { var extent = brush.extent(); g.selectAll("#clip-" + id + " rect") .attr("x", x(extent[0])) .attr("width", x(extent[1]) - x(extent[0])); } } // set the d attribute on the path g.selectAll(".bar").attr("d", barPath); }); // generate the bar chart path item function barPath(groups, j, a) { var path = [], i = -1, n = groups.length, d; while (++i < n) { d = groups[i]; path.push("M", x(d.key), ",", height, "V", y(d.value), "h4V", height); } return path.join(""); } // generate pretty brush left and right "handles" function resizePath(d) { var e = +(d == "e"), x = e ? 1 : -1, y = height / 3; return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) + "M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8); } } // brush handlers brush.on("brushstart.chart", function() { // get the containing div var div = d3.select(this.parentNode.parentNode.parentNode); // remove the display property from the reset anchor elem div.select(".title a").style("display", null); }); brush.on("brush.chart", function() { var g = d3.select(this.parentNode), extent = brush.extent(); // handle rounding of extent (only integers) if (round) g.select(".brush") .call(brush.extent(extent = extent.map(round))) // set a rounded brush extent .selectAll(".resize") .style("display", null); // remove the resize handles (why?) // update clip rectangle g.select("#clip-" + id + " rect") .attr("x", x(extent[0])) .attr("width", x(extent[1]) - x(extent[0])); // update the filter dimension.filterRange(extent); }); brush.on("brushend.chart", function() { if (brush.empty()) { emptyBrush.call(this); } }); // function to sync UI and crossfilter to an empty brush function emptyBrush() { //console.log("emptyBrush", this); if (!brush.empty()) { console.error("brush not empty") return; } // get reference to containing div var div = d3.select(this.parentNode.parentNode.parentNode); // hide the reset anchor element div.select(".title a").style("display", "none"); // remove the clip rectangle div.select("#clip-" + id + " rect") .attr("x", null) // remove the x attribute which will render the clipRect invalid .attr("width", "100%"); // reset the filter dimension.filterAll(); } // chart configuration functions chart.margin = function(_) { if (!arguments.length) return margin; margin = _; return chart; }; chart.x = function(_) { if (!arguments.length) return x; x = _; axis.scale(x); brush.x(x); return chart; }; chart.y = function(_) { if (!arguments.length) return y; y = _; return chart; }; chart.dimension = function(_) { //console.log("chart.dimension..." + _) if (!arguments.length) return dimension; dimension = _; return chart; }; chart.filter = function(_) { if (_) { brush.extent(_); dimension.filterRange(_); } else { brush.clear(); dimension.filterAll(); } brushDirty = true; return chart; }; chart.group = function(_) { if (!arguments.length) return group; group = _; return chart; }; chart.round = function(_) { if (!arguments.length) return round; round = _; return chart; }; chart.removeContents = function() { theDiv.selectAll("svg").remove(); //theDiv.selectAll("a").remove(); //console.log("theDiv", theDiv) return chart; } chart.brushDirty = function(_) { if (!arguments.length) return brushDirty; brushDirty = _; return chart; } chart.enableBrush = function(_) { if (!arguments.length) return enableBrush; enableBrush = _; return chart; } chart.clearBrush = function() { //console.log("---", d3.select(".brush")) d3.select(".brush").call(brush.clear()) emptyBrush.call(d3.select(".brush").node()) return chart; } chart.brushExtent = function(_) { if (!arguments.length) return brush.extent(); //console.log("setting brush extent...", _, "current", brush.extent()) brush.extent(_); d3.select(".brush").call(brush); brush.event(d3.select(".brush")) return chart; } // copy "on" event handlers from "brush" to "chart" return d3.rebind(chart, brush, "on"); } }) // animator object/function function animator() { var speed = 1000, currPos = 0, started = false, animating = false, data, ready = false, intHandle, toHandle, inTimeout = false, chart, container; function main(_) { data = _; if (data.length > 0) ready = true; // data good? if (!ready) { console.error("bad data") return; } // container good? if (!container) { console.error("No animation control container provided") return; } // event handlers function startStop() { this.blur(); var elem = d3.select(this) var value = elem.attr("value") //console.log("startStop...", this, value) if (value == "start") { // start the animation... started = true; //main.resume(); // change this button text d3.select("#anim_startStop").style("width", "130px") elem.text("Not Implemented").attr("value", "start") //elem.text("Exit Animation").attr("value", "stop") // show buttons /*d3.selectAll(".anim2").style("display", "inline-block") d3.selectAll(".anim4").style("display", "inline-block")*/ } else { // stop the animation started = false; main.stop(); // change this button text elem.text("Start Animation").attr("value", "start") // hide buttons d3.selectAll(".anim").style("display", "none") // ensure the halt/resume button says halt d3.select(".anim2").text("Halt").attr("value", "halt") } } function haltResume() { this.blur(); var elem = d3.select(this) var value = elem.attr("value") //console.log("haltResume...", this, value) if (value == "halt") { // halt the animation main.stop(); // change this button text elem.text("Resume").attr("value", "resume") // hide buttons d3.selectAll(".anim4").style("display", "none") // show buttons d3.selectAll(".anim3").style("display", "inline-block") } else { // resume the animation main.resume(); // change this button text elem.text("Halt").attr("value", "halt") // hide buttons d3.selectAll(".anim3").style("display", "none") // show buttons d3.selectAll(".anim4").style("display", "inline-block") } } function fasterSlower() { this.blur(); var elem = d3.select(this) var value = elem.attr("value") //console.log("fasterSlower...", this, value) if (value == "faster") main.faster() else main.slower(); } function forwardBackward() { this.blur(); var elem = d3.select(this) var value = elem.attr("value") //console.log("forwardBackward...", this, value, this.value) if (value == "forward") main.forward() else main.backward(); } function reset() { this.blur(); main.reset(); } // animation controls var controls = [ { text: "Start Animation", handler: startStop, id: "startStop", display: "inline-block", value: "start", class: "anim1", width: "110px" }, { text: "Halt", handler: haltResume, display: "none", value: "halt", class: "anim anim2", width: "60px" }, { text: "Backward", handler: forwardBackward, display: "none", value: "backward", class: "anim anim3" }, { text: "Forward", handler: forwardBackward, display: "none", value: "forward", class: "anim anim3" }, { text: "Faster", handler: fasterSlower, display: "none", value: "faster", class: "anim anim4" }, { text: "Slower", handler: fasterSlower, display: "none", value: "slower", class: "anim anim4" }, { text: "Reset", handler: reset, display: "none", class: "anim anim3 anim4" } ] container.selectAll("button") .data(controls) .enter().append("button") .attr("id", function(d) { return d.id ? "anim_" + d.id : null }) .style("display", "inline-block") .style("cursor", "default") .style("width", function(d) { return d.width ? d.width : "70px" }) .style("text-align", "center") .attr("class", function(d) { return d.class }) .style("display", function(d) { /*return "inline-block";*/ return d.display }) .attr("value", function(d) { return d.value ? d.value : null }) //.attr("class", "reset") .text(function(d) { return d.text }) .on("click", function(d) { /*console.log("adfads", d.handler);*/ d.handler.call(this) }) // set index currPos = data.length - 1; intHandle = setInterval(function() { if (animating && !inTimeout) go(0); }, 100) function go(useSpeed) { inTimeout = true; toHandle = setTimeout(function() { if (animating) { currPos++; if (currPos == data.length) currPos = 0; oneCycle(); } if (animating) go(speed); else inTimeout = false; }, useSpeed) } } function oneCycle() { //console.log("oneCycle") var dateExtent = []; dateExtent[0] = new Date(data[currPos].key); dateExtent[1] = new Date(data[currPos].key); dateExtent[1].setDate(dateExtent[1].getDate() + 7) chart.brushExtent(dateExtent) } // action functions main.reset = function() { speed = 1000; currPos = 0; oneCycle(); //animating = false; } main.resume = function() { animating = true; } main.stop = function() { animating = false; } main.exit = function() { animating = false; // change this button text d3.select("#anim_startStop").text("Start Animation").attr("value", "start") // hide buttons d3.selectAll(".anim").style("display", "none") // ensure the halt/resume button says halt d3.select(".anim2").text("Halt").attr("value", "halt") } main.slower = function() { speed = Math.min(4000, speed * 2); } main.faster = function() { speed = Math.max(250, speed / 2); } main.forward = function() { if (ready && !animating) { currPos++; if (currPos == data.length) currPos = 0; oneCycle(); } else console.log("not ready"); } main.backward = function() { if (ready && !animating) { currPos--; if (currPos < 0) currPos = data.length - 1; oneCycle(); } else console.log("not ready"); } // configuration functions main.speed = function(_) { if (!arguments.length) return speed; speed = _; return main; } main.chart = function(_) { if (!arguments.length) return chart; chart = _; return main; } main.container = function(_) { if (!arguments.length) return container; container = _; return main; } return main; } // end animator