Example of d3.brush to select items in a scattergram and synchronization with table.
forked from boeric's block: Internet vs. GDP
xxxxxxxxxx
<html lang="en">
<meta charset="utf-8">
<!--<link href="/assets/css/bootstrap.css" rel="stylesheet">-->
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<style>
div {
margin-left: 10px;
margin-top: 10px;
}
#table tr td:nth-child(3), #table tr td:nth-child(4), #table tr td:nth-child(5),
#table tr th:nth-child(3), #table tr th:nth-child(4), #table tr th:nth-child(5) {
text-align: right;
}
#table td, #table th {
font-size: 12px;
line-height: 17px;
}
.background {
fill: #4B9E9E;
fill-opacity: 0.1;
}
.extent {
fill: rgb(180, 180, 180);
fill-opacity: 0.4;
}
.resize rect {
fill: #276C86;
}
.axis text {
/*font-family: sans-serif;*/
font-size: 12px;
}
input[type="radio"] {
vertical-align: top;
font-size: 12px;
line-height: 17px;
}
label {
font-size: 13px;
font-weight: normal;
line-height: 17px;
display: inline;
margin-left: 20px;
}
form {
position: absolute;
left: 75px;
top: 55px;
}
#table thead th:hover {
background-color: rgb(240, 240, 240) ! important;
}
.btn {
background-image: linear-gradient(to bottom, rgb(255, 134, 26), #E6E6E6);
}
</style>
<body>
<table id="mainLayout" style="margin-top: 12px">
<tr>
<td style="width: 500px; vertical-align: top">
<div id="chartDiv" style="border1: 1px solid black">
<h4 style="margin-top: 0px">Alan mate drag the rectangle around <br/> Internet/Cellular Penetration vs. GDP/Capita 2012 (World Bank)</h4>
<svg id="svgDiv"></svg>
</div>
</td>
<td style="vertical-align: top">
<div id="tableContainerDiv" style="height: 467px; border1: 1px solid black;">
<p style="font-size: 14px"><b>Source:</b> World Bank, World Development Indicators <br>(extracted March 2014)</p>
<div id="tableDiv">
</div>
</div>
</td>
</tr>
<tr>
<td style="width: 500px">
<div style="margin-top: 0px; margin-left: 40px">
<button type="button" id="helpBtn" class="btn"><b>?</b></button>
</div>
<div id="helpDiv" style="margin-left: 40px; display: none">
<p style="font-size: 13px;">
<b>Note:</b> The X-axis (GDP/capita) is drawn with a logaritmic scale<br>
<b>Note:</b> Only countries with cellular, internet and GDP data are included (174 out of 214 countries)<br>
<b>Usage:</b> a) Drag the gray rectangle around the chart to view different data points, b) Grab the sides of the rectangle to change its size, c) Click the headers in the table to change the sort order, d) Hover over countries in the table to see the corresponding location in the scatter chart, e) Double click in the chart to remove the rectangle, and begin dragging to re-create
</p>
</div>
</td>
</tr>
</table>
<form>
<label><input type="radio" name="mode" value="internet" checked> Internet penetration</label>
<label><input type="radio" name="mode" value="cellular"> Cellular penetration</label>
</form>
<!--<script src="../d3/d3.v3.js"></script>-->
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
'use strict';
var margin = {top: 30, right: 30, bottom: 2, left: 30},
data = [],
table, chart,
commasFormatter = d3.format(",.0f"),
mode = "showInternet",
svgGroup;
// get the raw data tsv file
d3.tsv("internetCellularGDP.txt", function(error, json) {
var nest = d3.nest()
.key(function(d) { return d["Country Code"]; })
.key(function(d) { return d["Indicator Code"]; })
.entries(json);
// create usable data structure, process country by country
nest.forEach(function(d) {
var include = true;
var item = {};
item.key = d.key;
item.countryCode = d.key;
item.countryName = d.values[0].values[0]["Country Name"];
["GDP", "Cellular", "Internet"].forEach(function(c, i) {
var value = d.values[i].values[0]["2012 [YR2012]"];
if (value === "..") { include = false; return; } // exclude countries with any null values
item["indicatorName" + c] = d.values[i].values[0]["Indicator Name"];
item["indicatorCode" + c] = d.values[i].values[0]["Indicator Code"];
item["value" + c] = +value;
});
if (include) data.push(item);
});
// select the chart div
var chartDiv = d3.select("#chartDiv")
.style("overflow", "scroll");
// select the svg element
var svg = d3.select("#svgDiv")
.attr("width", "580px")
.attr("height", "430px")
.style("margin-bottom", "10px")
.style("margin-right", "10px")
.style("background-color", "white");
svgGroup = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
// create chart
chart = scatterView()
.accessor(mode)
.data(data);
chart(svgGroup);
// select table div
var tableDiv = d3.select("#tableDiv")
.style("overflow", "scroll")
.style("width", "330px")
.style("height", "400px")
.style("margin", "0px")
.style("padding-left", "0px");
// create table
table = tableView()
.data(data);
table(tableDiv);
// define handlers after chart and table are constructed
chart.on("filter", function(filtered) {
table.data(filtered);
table(tableDiv);
});
table.on("hover", function(item) {
chart.highlight(item)
});
chart.forceBrushEvent(); //TODO: upon initial load, table is not synced to brush
// chart function -----------------------------------------------------------------------------
function scatterView() {
var dispatch = d3.dispatch(chart, "filter");
var data,
dataYmax, dataXmax,
saveG,
radius = 7, radiusSelected = 9,
strokeWidth = 1,
fillOpacity = 0.5,
colorFill,
colorStroke,
mode = "internet",
colors = {
red: 'rgb(216, 50, 51)',
green: 'rgb(51, 164, 51)',
blue: 'rgb(42, 126, 184)',
orange: 'rgb(255, 134, 26)'
}
function x(g) {
// save reference to svg group (for highlight method)
saveG = g;
// get dimensions of parent
var container = d3.select(g.node().parentNode);
var width = +container.attr("width").replace("px", "");
var height = +container.attr("height").replace("px", "");
// remove all previous elements in group
g.selectAll("*").remove();
// set dimensions
var chartWidth = width - 60;
var chartHeight = height - 60;
var xField = function(d) { return d.valueGDP; };
var yField = function(d) { return yAccessor(d); };
var yAccessor = function(d) {
if (mode == "internet") return d.valueInternet;
else return d.valueCellular;
}
dataYmax = d3.max(data, yField);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, yField)])
.range([chartHeight, 0]);
dataXmax = d3.max(data, xField) + 10000;
var xScale = d3.scale.log()
.domain([d3.min(data, xField), d3.max(data, xField) + 10000])
.range([0, chartWidth - 10]);
// set colors for this mode
colorFill = (mode === "internet" ? colors.orange : colors.green);
colorStroke = (mode === "internet" ? colors.orange : colors.green);
// set color scale for circle color --> not used at the moment
var colorScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.valueGDP })])
.range(["#17D84D", "#FF1D1D"])
.interpolate(d3.interpolateHsl);
// create circles for scatter chart, and bind data
var circles = g
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d, i) { return xScale(xField(d)); })
.attr("cy", function(d, i) { return yScale(yField(d)); })
.attr("r", radius)
.style("fill", colorFill)
.style("stroke", colorStroke)
.style("stroke-width", strokeWidth)
.style("fill-opacity", fillOpacity)
.on("click", function(d) { console.log(d.key); });
// setup y axis
var yaxis = d3.svg.axis()
.scale(yScale)
.orient("left") //left, right, top
.ticks(4) //best guess
// call y axis
var yg = g.append("g")
.call(yaxis)
.attr("class", "axis");
yg
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 10)
.attr("x", -10)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Penetration per 100 people");
// format y axis
yg.selectAll("path").style({ fill: "none", stroke: "gray"})
//yg.selectAll(".tick text").attr("transform", "rotate(0)")
yg.selectAll("line").style({ stroke: "#000"});
// setup x axis
var xaxis = d3.svg.axis()
.scale(xScale)
.orient("bottom") //left, right, top
.ticks(5) // doesn't really work on a log scale
//.tickFormat(function(d) { return commasFormatter(d); })
.tickFormat(function(d) { return xScale.tickFormat(4,d3.format(",d"))(d) });
// call x axis
var xg = g.append("g")
.call(xaxis)
.attr("class", "axis");
xg.append("text")
//.attr("transform", "rotate(-90)")
.attr("y", -20)
.attr("x", chartWidth - 20)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("GDP per capita (curr $)");
// format x axis
xg.attr("transform", "translate(" + [0, chartHeight] + ")")
xg.selectAll("path").style({ fill: "none", stroke: "gray"})
xg.selectAll("line").style({ stroke: "#000"})
//xg.selectAll(".tick text").attr("transform", "rotate(0)")
// create brush
var brush = d3.svg.brush()
.x(xScale)
.y(yScale)
.extent([ [2000, dataYmax * 0.3], [20000, dataYmax * 0.7] ])
.on("brush", brushed);
// create brush group
var brushg = g.append("g")
.classed("brush", true)
.call(brush);
// set brush styles (after brush is created)
//brushg.selectAll(".background").style({visibility: "visible"})
brushg.selectAll(".background").style({visibility: "none"})
brushg.selectAll(".extent").style({visibility: "visible"})
//brushg.selectAll(".resize rect").style({visibility: "visible"})
// initial invokation
brushed();
// brush handler
function brushed() {
var minext = brush.extent()[0];
var xmin = minext[0];
var ymin = minext[1];
var maxext = brush.extent()[1];
var xmax = maxext[0];
var ymax = maxext[1];
var filtered = data.filter(function(d) {
var x = xField(d);
var y = yField(d);
return x >= xmin
&& x <= xmax
&& y >= ymin
&& y <= ymax;
})
g.selectAll("circle")
.style("stroke", colorStroke)
.style("fill-opacity", fillOpacity)
g.selectAll("circle")
.data(filtered, function(d) { return d.key })
//.style("stroke", "#FFFFFF")
.style("stroke", "black")
.style("fill-opacity", 0.8)
if (filtered.length == 0) filtered = data;
dispatch.filter(filtered);
} // end brush handler
} // end x function
x.data = function(_) {
if(!arguments.length) return data;
data = _;
return x;
}
x.highlight = function(_) {
saveG.selectAll("circle")
.attr("r", radius)
.style("stroke-width", strokeWidth)
.style("fill", colorFill)
.style("fill-opacity", fillOpacity)
// matching data only
.data(_, function(d) { return d.key })
.attr("r", radiusSelected)
.style("stroke-width", 1)
.style("fill", colors.red)
.style("fill-opacity", 1);
return x;
}
x.accessor = function(_) {
if(!arguments.length) return mode;
if (_ === "showInternet") mode = "internet";
else mode = "cellular";
return x;
}
x.forceBrushEvent = function() {
//console.log('x.forceBrushEvent');
return x;
}
return d3.rebind(x, dispatch, "on");
} // end scatterView function
// table function -----------------------------------------------------------------------------
function tableView() {
var dispatch = d3.dispatch(x, "hover");
var data,
sortColumn = "countryCode";
function x(div) {
div.select("#table").remove();
var table = div.append("table")
.attr("class", "table table-condensed table-bordered table-striped")
.attr("id", "table")
.style("background-color", "white");
var columnWidths = ["10%", "30%", "20%", "20%", "20%"];
table.selectAll("col")
.data(columnWidths)
.enter()
.append("col")
.style("width", function(d) { return d; });
var columnHead = ["Code", "Country", "Cellular use per 100 people", "Internet use per 100 people", "GDP per capita ($)"];
var columnData = ["countryCode", "countryName", "valueCellular", "valueInternet", "valueGDP"];
var thead = table.append("thead");
var tbody = table.append("tbody");
thead.append("tr")
.selectAll("th")
.data(columnHead)
.enter()
.append("th")
.attr("id", function(d, i) { return "head" + i ; })
.attr("class", "colHead")
.style("background-color", function(d, i) {
return (columnData[i] === sortColumn) ? "rgb(220, 220, 220)" : "white"; })
.text(function(d) { return d; })
.on("click", function(d, i) {
sortColumn = columnData[i];
x.sort();
x(div);
});
// create table rows
var rows = tbody.selectAll("tr")
.data(data)
.enter()
.append("tr");
// create cells
rows.selectAll("td")
.data(function(d) {
return columnData.map(function(column) {
var value = d[column];
if (typeof value == "number") {
if (column === "valueGDP") value = commasFormatter(value)
else value = value.toFixed(2);
}
return { value: value };
});
})
.enter()
.append("td")
.html(function(d) { return d.value; });
//rows.exit().remove();
// handle hover events
rows.on("mouseover", function(d, i) { dispatch.hover([d]) })
rows.on("mouseout", function(d, i) { dispatch.hover([]) })
}
x.data = function(_) {
if(!arguments.length) return data;
data = _;
x.sort();
return x;
}
x.sort = function() {
var result = 0;
data.sort(function(a, b) {
if (a[sortColumn] > b[sortColumn]) result = -1;
if (a[sortColumn] < b[sortColumn]) result = 1;
if (sortColumn === "countryCode" || sortColumn === "countryName") result *= -1;
return result;
});
}
return d3.rebind(x, dispatch, "on");
} // end tableView function
// controls -----------------------------------------------------------------------------------
d3.selectAll("input").on("change", change);
function change() {
if (this.value === "internet") mode = "showInternet";
else mode = "showCellular";
//console.log(mode);
// regenerate the chart
chart
.accessor(mode)
.data(data);
chart(svgGroup);
}
var helpBtn = d3.select("#helpBtn")
.on("click", function(){
// get current state of help div
var helpDiv = d3.select("#helpDiv");
if (helpDiv.style("display") == "none") helpDiv.style("display", "block");
else helpDiv.style("display", "none");
helpBtn[0][0].blur(); // remove focus
});
}); // end d3.tsv ajax
</script>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js