|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta name="viewport" content="width=device-width"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script> |
|
<title>Basic Scatter Plot</title> |
|
<style> |
|
body { |
|
margin: 0px; |
|
} |
|
|
|
.domain { |
|
display: none; |
|
} |
|
|
|
.tick line { |
|
stroke: #C0C0BB; |
|
} |
|
|
|
.tick text, .legendCells text { |
|
fill: #8E8883; |
|
font-size: 13pt; |
|
font-family: sans-serif; |
|
} |
|
|
|
.axis-label, .legend-label { |
|
fill: #635F5D; |
|
font-size: 15pt; |
|
font-family: sans-serif; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg width="960" height="500"></svg> |
|
<script> |
|
|
|
const xValue = d => d.income; |
|
const xLabel = 'Income Per Person, ($/year)'; |
|
const yValue = d => d.health; |
|
const yLabel = 'Health Expectancy, (years)'; |
|
const rValue = d => d.population; |
|
const rLabel = 'Size of spots stand for population'; |
|
const colorLabel = 'region'; |
|
const margin = {left: 120, right: 250, top: 30, bottom: 100}; |
|
|
|
const svg = d3.select('svg'); |
|
const width = svg.attr('width'); |
|
const height = svg.attr('height'); |
|
const innerWidth = width - margin.left - margin.right; |
|
const innerHeight = height - margin.top - margin.bottom; |
|
|
|
let hoveredColorValue; |
|
const hoveredStrokeColor = "black"; |
|
|
|
const g = svg.append('g') |
|
.attr('transform', `translate(${margin.left},${margin.top})`); |
|
|
|
// This is the layer where the bars are drawn. |
|
const baseBarLayer = g.append("g"); |
|
|
|
const xAxisG = g.append('g') |
|
.attr('transform', `translate(0, ${innerHeight})`); |
|
const yAxisG = g.append('g'); |
|
const colorLegendG = g.append('g') |
|
.attr("class", "color-legend") |
|
.attr('transform', `translate(${innerWidth + 60}, 150)`); |
|
|
|
xAxisG.append('text') |
|
.attr('class', 'axis-label') |
|
.attr('x', innerWidth / 2) |
|
.attr('y', 70) |
|
.text(xLabel); |
|
|
|
yAxisG.append('text') |
|
.attr('class', 'axis-label') |
|
.attr('x', -innerHeight / 2) |
|
.attr('y', -60) |
|
.attr('transform', `rotate(-90)`) |
|
.style('text-anchor', 'middle') |
|
.text(yLabel); |
|
|
|
colorLegendG.append('text') |
|
.attr('class', 'legend-label') |
|
.attr('x', -30) |
|
.attr('y', -40); |
|
|
|
// This layer contains a semi-transparent overlay |
|
// that fades out the base bars. |
|
const overlayRect = g.append("g") |
|
.append("circle") |
|
.attr("width", innerWidth) |
|
.attr("height", innerHeight) |
|
.attr("fill", "none") |
|
.style("pointer-events", "none"); |
|
|
|
// This contains the subset of bars rendered on top |
|
// when you hover over the entries in the color legend. |
|
const foregroundBarLayer = g.append("g"); |
|
|
|
const xScale = d3.scaleLog(); |
|
const yScale = d3.scaleLinear(); |
|
const rScale = d3.scaleSqrt(); |
|
const colorScale = d3.scaleOrdinal() |
|
.range(d3.schemeCategory10); |
|
|
|
const xAxis = d3.axisBottom() |
|
.scale(xScale) |
|
.ticks(11, ",.1s") |
|
.tickPadding(15) |
|
.tickSize(-innerHeight); |
|
|
|
const yAxis = d3.axisLeft() |
|
.scale(yScale) |
|
.ticks(5) |
|
.tickPadding(15) |
|
.tickSize(-innerWidth); |
|
|
|
const colorLegend = d3.legendColor() |
|
.scale(colorScale) |
|
.shape('circle'); |
|
|
|
const row = d => { |
|
d.income = +d.income; |
|
d.health = +d.health; |
|
d.population = +d.population; |
|
return d; |
|
}; |
|
|
|
function render(data) { |
|
|
|
const nested = d3.nest() |
|
.key(function (d) { |
|
return d[colorLabel]; |
|
}) |
|
.entries(data); |
|
|
|
xScale |
|
.domain(d3.extent(data, xValue)) |
|
.range([0, innerWidth]) |
|
.nice; |
|
|
|
yScale |
|
.domain(d3.extent(data, yValue)) |
|
.range([innerHeight, 0]) |
|
.nice(); |
|
|
|
rScale |
|
.domain(d3.extent(data, rValue)) |
|
.range([4, 18]); |
|
|
|
colorScale.domain(nested.map(function (nested) { |
|
return nested.key; |
|
})); |
|
|
|
xAxisG.call(xAxis); |
|
yAxisG.call(yAxis); |
|
|
|
renderBars(foregroundBarLayer, nested); |
|
|
|
if (hoveredColorValue) { |
|
setOverlayTransparency(0.8); |
|
console.log(hoveredColorValue); |
|
renderBars(foregroundBarLayer, nested.filter(function (layer) { |
|
return layer.key === hoveredColorValue; |
|
})); |
|
console.log(nested.filter(function (layer) { |
|
return layer.key === hoveredColorValue |
|
})); |
|
} else { |
|
setOverlayTransparency(0.0); |
|
renderBars(foregroundBarLayer, nested); |
|
} |
|
|
|
colorLegendG.call(colorLegend) |
|
.selectAll('.cell text') |
|
.attr('dy', '0.1em'); |
|
|
|
listenForHover(colorLegendG.selectAll("circle"), data); |
|
listenForHover(colorLegendG.selectAll("text"), data); |
|
} |
|
|
|
function renderBars(g, layers) { |
|
const layerGs = g.selectAll(".layer").data(layers); |
|
layerGs.enter().append("g").attr("class", "layer"); |
|
layerGs.exit().remove(); |
|
|
|
layerGs.style("fill", function (d) { |
|
return colorScale(d.key); |
|
}); |
|
|
|
const circles = layerGs.selectAll('.mark').data(function (d) { |
|
return d.values; |
|
}); |
|
|
|
circles.enter().append('circle') |
|
.attr('class', 'mark') |
|
.merge(circles) |
|
.attr('cx', d => xScale(xValue(d))) |
|
.attr('cy', d => yScale(yValue(d))) |
|
.attr('fill-opacity', 0.7) |
|
.attr('r', d => rScale(rValue(d))); |
|
|
|
circles.exit().remove(); |
|
} |
|
|
|
function listenForHover(selection, data) { |
|
selection |
|
.on("mouseover", function (d) { |
|
hoveredColorValue = d; |
|
render(data); |
|
}) |
|
.on("mouseout", function (d) { |
|
hoveredColorValue = null; |
|
render(data); |
|
}) |
|
.style("cursor", "pointer"); |
|
} |
|
|
|
function setOverlayTransparency(alpha) { |
|
overlayRect |
|
.transition().duration(400) |
|
.attr("fill", "rgba(255, 255, 255, " + alpha + ")"); |
|
} |
|
|
|
d3.csv('income_health.csv', row, render); |
|
</script> |
|
</body> |
|
</html> |