|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>MRI and Alzheimers</title> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<style> |
|
body { |
|
margin: 0px; |
|
background-color: #000000; |
|
} |
|
body svg { |
|
background-color: #000000; |
|
} |
|
.tick text { |
|
fill: white; |
|
font-size: 10pt; |
|
font-family: sans-serif; |
|
} |
|
.tick line { |
|
stroke: #3d3d3d; |
|
} |
|
.axis-label { |
|
fill: white; |
|
font-size: 16pt; |
|
font-family: sans-serif; |
|
} |
|
.legend-label { |
|
fill: white; |
|
font-size: 12pt; |
|
font-family: sans-serif; |
|
} |
|
.legend-text { |
|
fill: white; |
|
font-size: 8pt; |
|
font-family: sans-serif; |
|
} |
|
.marker { |
|
opacity: 0.75; |
|
} |
|
#marksG { |
|
clip-path: url(#viewClip); |
|
} |
|
#zoomCatcher { |
|
fill: transparent; |
|
stroke: none; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<div id="chart"> |
|
<svg width="960" height="500"> |
|
<defs> |
|
<clipPath id="viewClip"> |
|
<rect id="viewBox"/> |
|
</clipPath> |
|
</defs> |
|
<g id="mainG"> |
|
<g id="xAxisG"/> |
|
<g id="yAxisG"/> |
|
<g id="clippingG" clip-path="url(#viewClip)"> |
|
<g id="marksG"/> |
|
</g> |
|
<g id="legendG"/> |
|
</g> |
|
<rect id="zoomCatcher"/> |
|
</svg> |
|
</div> |
|
<script> |
|
|
|
const xValue = d => d.Age; |
|
const xLabel = 'Age'; |
|
const yValue = d => d.nWBV; |
|
const yLabel = 'Brain Volume (normalized)'; |
|
const margin = { left: 60, right: 120, top: 90, bottom: 30 }; |
|
|
|
const xTicks = 8; |
|
const yTicks = 5; |
|
const vRatio = 500 / 960; |
|
|
|
const colorValue = d => d["CDR"]; |
|
const colorLabel = "Dementia"; |
|
|
|
const sizeValue = d => d["Educ"]; |
|
const sizeLabel = "Education"; |
|
|
|
const chartDiv = document.getElementById("chart"); |
|
const svg = d3.select('svg'); |
|
const g = d3.select("#mainG"); |
|
const xAxisG = d3.select("#xAxisG"); |
|
const yAxisG = d3.select("#yAxisG"); |
|
const marksG = d3.select("#marksG"); |
|
const legendG = d3.select("#legendG"); |
|
const viewBox = d3.select("#viewBox"); |
|
const zoomCatcher = d3.select("#zoomCatcher"); |
|
|
|
const xScale = d3.scaleLinear().domain([9.9, 100]); |
|
const yScale = d3.scaleLinear().domain([0.60, 0.90]); |
|
|
|
const colorScale = d3.scaleOrdinal() |
|
.domain(["Unassessed", "None", "Very Mild", "Mild", "Moderate"]) |
|
.range(["#01665e", "#5ab4ac", "#f6e8c3", "#d8b365", "#8c510a"]); |
|
|
|
const sizeScale = d3.scaleOrdinal() |
|
.domain(["Unknown", "< High School", "High School Grad", "Some College", "College Grad", "Beyond College"]) |
|
.range([50, 25, 50, 100, 150, 200]); |
|
|
|
const xAxis = d3.axisTop() |
|
.scale(xScale) |
|
.ticks(xTicks) |
|
.tickPadding(15); |
|
|
|
const yAxis = d3.axisRight() |
|
.scale(yScale) |
|
.ticks(yTicks) |
|
.tickPadding(15); |
|
|
|
const xAxisText = xAxisG.append('text'); |
|
const yAxisText = yAxisG.append('text') |
|
|
|
function drawLegend() { |
|
|
|
legendG.append('rect') |
|
.attr('x', -50) |
|
.attr('y', -50) |
|
.attr('width', 350) |
|
.attr('height', 300) |
|
.attr('fill', 'black') |
|
.attr('opacity', 0.75); |
|
|
|
legendG.append('line') |
|
.attr('x1', -20) |
|
.attr('y1', -12) |
|
.attr('x2', 260) |
|
.attr('y2', -12) |
|
.attr('stroke', 'white'); |
|
|
|
const colorLegendG = legendG.append('g') |
|
.attr('transform', `translate(160, 0)`); |
|
|
|
const sizeLegendG = legendG.append('g') |
|
.attr('transform', `translate(0, 0)`); |
|
|
|
colorLegendG.append('text') |
|
.attr('class', 'legend-label') |
|
.attr('x', -2) |
|
.attr('y', -18) |
|
.text(colorLabel); |
|
|
|
sizeLegendG.append('text') |
|
.attr('class', 'legend-label') |
|
.attr('x', -2) |
|
.attr('y', -18) |
|
.text(sizeLabel); |
|
|
|
for(i in colorScale.range()) { |
|
const label = colorScale.domain()[i]; |
|
const color = colorScale.range()[i]; |
|
const area = 150; |
|
var item = colorLegendG.append('circle'); |
|
updateCircleMark(item, 10, i*18, area, color); |
|
if(i == 0) { updateGhostMark(item, color); } |
|
colorLegendG.append('text') |
|
.attr('x', 25) |
|
.attr('y', i*18 + 3) |
|
.attr('class', 'legend-text') |
|
.text(label); |
|
} |
|
|
|
for(i in sizeScale.range()) { |
|
const label = sizeScale.domain()[i]; |
|
const area = sizeScale.range()[i]; |
|
const color = '#01665e'; |
|
var item1 = sizeLegendG.append('rect'); |
|
var item2 = sizeLegendG.append('circle'); |
|
updateSquareMark(item1, 10, i*18, area, color); |
|
updateCircleMark(item2, 30, i*18, area, color); |
|
if(i == 0) { |
|
updateGhostMark(item1, color); |
|
updateGhostMark(item2, color); |
|
} |
|
sizeLegendG.append('text') |
|
.attr('x', 50) |
|
.attr('y', i*18 + 3) |
|
.attr('class', 'legend-text') |
|
.text(label); |
|
} |
|
sizeLegendG |
|
.append('text') |
|
.attr('x', 5) |
|
.attr('y', i*18 + 21) |
|
.attr('class', 'legend-text') |
|
.text('M'); |
|
sizeLegendG |
|
.append('text') |
|
.attr('x', 27) |
|
.attr('y', i*18 + 21) |
|
.attr('class', 'legend-text') |
|
.text('F'); |
|
} |
|
|
|
function updateCircleMark(item, cx, cy, area, color) { |
|
const r = Math.sqrt(area / Math.PI); |
|
return item |
|
.attr('cx', cx) |
|
.attr('cy', cy) |
|
.attr('r', r) |
|
.attr('fill', color); |
|
} |
|
|
|
function updateSquareMark(item, cx, cy, area, color) { |
|
const r = Math.sqrt(area / 4); |
|
return item |
|
.attr('x', cx - r) |
|
.attr('y', cy - r) |
|
.attr('width', 2*r) |
|
.attr('height', 2*r) |
|
.attr('fill', color); |
|
} |
|
|
|
function updateGhostMark(item, color) { |
|
return item |
|
.attr('fill', 'None') |
|
.attr('stroke', color) |
|
.attr('stroke-width', 1.5); |
|
} |
|
|
|
// customize appearance of axis |
|
function customAxis(g, axis) { |
|
g.call(axis); |
|
g.select(".domain").remove(); |
|
} |
|
|
|
const row = d => { |
|
d.Age = +d.Age; |
|
d.nWBV = +d.nWBV; |
|
const cdr = +d.CDR; |
|
if(d.CDR == "") { d.CDR = "Unrated" } |
|
else if(cdr == 0) { d.CDR = "None" } |
|
else if(cdr == 0.5) { d.CDR = "Very Mild" } |
|
else if(cdr == 1.0) { d.CDR = "Mild" } |
|
else if(cdr == 2.0) { d.CDR = "Moderate" } |
|
|
|
const educ = +d.Educ; |
|
if(d.Educ == "") { d.Educ = "Unknown" } |
|
else if(educ == 1) { d.Educ = "< High School" } |
|
else if(educ == 2) { d.Educ = "High School" } |
|
else if(educ == 3) { d.Educ = "Some College" } |
|
else if(educ == 4) { d.Educ = "College Grad" } |
|
else if(educ == 5) { d.Educ = "Beyond College" } |
|
else { d.Educ = "Unknown" } |
|
|
|
return d; |
|
}; |
|
|
|
const zoom = d3.zoom() |
|
.scaleExtent([1, 10]); |
|
|
|
function redraw(data) { |
|
|
|
const width = Math.max(800, chartDiv.clientWidth); |
|
const height = width * vRatio; |
|
|
|
svg |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
const innerWidth = width - margin.left - margin.right; |
|
const innerHeight = height - margin.top - margin.bottom; |
|
|
|
const xoff = margin.left; |
|
const yoff = margin.top; |
|
|
|
zoom |
|
.extent([[0, 0], [innerWidth, innerHeight]]) |
|
.translateExtent([[0, 0], [innerWidth, innerHeight]]) |
|
.on("zoom", zoomed); |
|
|
|
g.attr('transform', `translate(${margin.left},${margin.top})`); |
|
xAxisG.attr('transform', `translate(0, ${innerHeight})`); |
|
legendG.attr('transform', `translate(20, ${innerHeight - 100})`); |
|
|
|
xAxisText.attr('class', 'axis-label') |
|
.attr('x', innerWidth / 2) |
|
.attr('y', -(innerHeight + 50)) |
|
.text(xLabel); |
|
|
|
yAxisText |
|
.attr('class', 'axis-label') |
|
.attr('x', innerHeight / 2) |
|
.attr('y', -innerWidth - 70) |
|
.attr('transform', `rotate(90)`) |
|
.style('text-anchor', 'middle') |
|
.text(yLabel); |
|
|
|
xScale |
|
.range([0, innerWidth]); |
|
|
|
yScale |
|
.range([innerHeight, 0]); |
|
|
|
xAxis.tickSize(innerHeight); |
|
yAxis.tickSize(innerWidth); |
|
|
|
function updateMarkers(xScale, yScale) { |
|
const squareMarkers = marksG.selectAll('rect.marker') |
|
.data(data.filter(d => d["M/F"] == "M")); |
|
|
|
const circleMarkers = marksG.selectAll('circle.marker') |
|
.data(data.filter(d => d["M/F"] != "M")); |
|
|
|
squareMarkers.enter() |
|
.append('rect') |
|
.attr('class', 'marker') |
|
.merge(squareMarkers) |
|
.each(function(d) { |
|
var item = d3.select(this); |
|
var cx = xScale(xValue(d)); |
|
var cy = yScale(yValue(d)); |
|
var area = sizeScale(sizeValue(d)); |
|
var color = colorScale(colorValue(d)); |
|
updateSquareMark(item, cx, cy, area, color); |
|
if(sizeValue(d) == "Unknown") { |
|
updateGhostMark(item, color); |
|
} |
|
}); |
|
|
|
circleMarkers.enter() |
|
.append('circle') |
|
.attr('class', 'marker') |
|
.merge(circleMarkers) |
|
.each(function(d) { |
|
var item = d3.select(this); |
|
var cx = xScale(xValue(d)); |
|
var cy = yScale(yValue(d)); |
|
var area = sizeScale(sizeValue(d)); |
|
var color = colorScale(colorValue(d)); |
|
updateCircleMark(item, cx, cy, area, color); |
|
if(sizeValue(d) == "Unknown") { |
|
updateGhostMark(item, color); |
|
} |
|
}); |
|
} |
|
|
|
updateMarkers(xScale, yScale); |
|
|
|
function zoomed() { |
|
const t = d3.event.transform; |
|
const zoomXscale = t.rescaleX(xScale); |
|
const zoomYscale = t.rescaleY(yScale); |
|
|
|
xAxisG.call(xAxis.scale(zoomXscale)); |
|
yAxisG.call(yAxis.scale(zoomYscale)); |
|
updateMarkers(zoomXscale, zoomYscale); |
|
} |
|
|
|
xAxisG.call(g => customAxis(g, xAxis)); |
|
yAxisG.call(g => customAxis(g, yAxis)); |
|
|
|
viewBox |
|
.attr('x', 0) |
|
.attr('y', 0) |
|
.attr('width', innerWidth) |
|
.attr('height', innerHeight); |
|
|
|
zoomCatcher |
|
.attr('transform', `translate(${margin.left}, ${margin.top})`) |
|
.attr('width', innerWidth) |
|
.attr('height', innerHeight) |
|
.call(zoom); |
|
} |
|
|
|
drawLegend(); |
|
|
|
var theData = null; |
|
|
|
d3.csv('oasis_cross-sectional.csv', row, data => { |
|
theData = data; |
|
redraw(data); |
|
}); |
|
|
|
// Redraw based on the new size whenever the browser window is resized. |
|
window.addEventListener("resize", function () { |
|
if(theData != null) { redraw(theData); } |
|
}); |
|
|
|
</script> |
|
|
|
</body> |
|
</html> |