Playing around with the idea of a bump chart comparison with two overlapping areas. Based on Farmers Markets data from data.gov.
Another iteration with step interpolation instead of cardinal.
Playing around with the idea of a bump chart comparison with two overlapping areas. Based on Farmers Markets data from data.gov.
Another iteration with step interpolation instead of cardinal.
const svg = d3.select('svg') | |
svg.append('text') | |
.attr('class', 'title') | |
.attr('x', 960/2) | |
.attr('y', 35) | |
.text("Farmers' Markets Goods Comparison") | |
d3.json('https://gist.githubusercontent.com/susielu/3d194b8660ec6ab214a3/raw/38a2cdc96efaaaeb4849c86b600de5dfecea2dec/farmers-markets-lat-long.json', (error, data) => { | |
const h = 480 | |
const w = 800 | |
const padding = 20 | |
const xScale = d3.scaleLinear().range([padding, w - padding]).domain([-130, -65]) | |
const xBarScale = d3.scaleLinear().range([padding, w - padding]) | |
const yScale = d3.scaleLinear().range([h - padding, padding]).domain([20, 50]) | |
let comp1 = "maple" | |
let comp2 = "seafood" | |
let selected = 'comp1' | |
const offset = 'translate(60, 40)' | |
//Filtering out states outside of the contiguous US for simplicity | |
data = data.filter(d => d.x >= -130 && d.x <= -65 && d.y >= 20 && d.y <=50) | |
//Making a legend w00t http://d3-legend.susielu.com/ | |
const colors = d3.scaleOrdinal().domain([`${comp1}` , `${comp2}`, 'both']).range([ | |
"rgba(0, 200, 200, .5)", | |
"rgba(200, 0, 200, .5)", | |
"#ac8cdc" | |
]) | |
const colorLegend = d3.legendColor() | |
.shapeHeight(8) | |
.shapePadding(5) | |
.scale(colors) | |
svg.append('g') | |
.attr('class', 'legend') | |
.attr('transform', 'translate(200, 390)') | |
.call(colorLegend) | |
const map = svg.append('g') | |
.attr('class', 'map') | |
.attr('transform', offset) | |
map.selectAll('circle') | |
.data(data) | |
.enter() | |
.append('circle') | |
.attr('r', 1) | |
.attr('cx', d => xScale(d.x)) | |
.attr('cy', d => yScale(d.y)) | |
const rollup = leaves => { | |
let first = 0 | |
let second = 0 | |
let both = 0 | |
leaves.forEach(l => { | |
if (l[comp1] === "Y"){ first++ } | |
if (l[comp2] === "Y"){ second++} | |
if (l[comp1] === "Y" && l[comp2] === "Y"){ both++ } | |
}) | |
return { | |
length: leaves.length, | |
comp1: first, | |
comp2: second, | |
both: both | |
} | |
} | |
const lat = svg.append('g') | |
.attr('class', 'lat') | |
.attr('transform', offset) | |
let latArea = d3.area() | |
.x(d => xScale(parseInt(d.key))) | |
.y1(d => yLatScale(d.value.length)) | |
.y0(d => yLatScale(0)) | |
.curve(d3.curveCardinal), | |
latNested = d3.nest() | |
.key(d => Math.round(d.x)) | |
.rollup(rollup) | |
.entries(data) | |
.sort((a,b) => parseInt(a.key) - parseInt(b.key)); | |
const yLatMax = d3.max(latNested, d => d.value.length) | |
const yLatScale = d3.scaleLinear().range([h -40, h - 140]).domain([0, yLatMax]) | |
//Makes a horizontal bar chart then rotates it for the longitudinal graph | |
const long = svg.append('g') | |
.attr('class', 'long') | |
.attr('transform', `rotate(90, ${w + 60}, 40) ${offset}`) | |
const xLongScale = d3.scaleLinear().range([w + padding, w + h - padding]).domain([50, 20]) | |
let longArea = d3.area() | |
.x(d => xLongScale(parseInt(d.key))) | |
.y1(d => yLongScale(d.value.length)) | |
.y0(d => yLongScale(0)) | |
.curve(d3.curveCardinal), | |
longNested = d3.nest() | |
.key(d => Math.round(d.y)) | |
.rollup(rollup) | |
.entries(data) | |
.sort((a,b) => parseInt(a.key) - parseInt(b.key)) | |
const yLongMax = d3.max(longNested, d => d.value.length) | |
const yLongScale = d3.scaleLinear().range([ padding , padding - 100]).domain([0, yLongMax]) | |
const transition = d3.transition() | |
.ease(d3.easePolyInOut) | |
const createHistogram = (group, area, nest) => { | |
group.append('path') | |
.attr('fill', 'none') | |
.attr('stroke', 'grey') | |
.attr('d', area(nest)) | |
group.append('path') | |
.attr('class', 'comp1') | |
group.append('path') | |
.attr('class', 'comp2') | |
} | |
const updateMap = () => { | |
map.selectAll('circle') | |
.attr('class', d => d[comp1] === "Y" && d[comp2] === "Y" ? | |
'compBoth' : | |
d[comp1] === "Y" ? | |
'comp1' : d[comp2] === "Y" ? | |
'comp2' : '') | |
} | |
const updateHistogram = (type, group, area, nest, scale) => { | |
const nestKey = type === "lat" ? 'x' : 'y' | |
nest = d3.nest() | |
.key(d => Math.round(d[nestKey])) | |
.rollup(rollup) | |
.entries(data) | |
.sort((a,b) => parseInt(a.key) - parseInt(b.key)) | |
//Overlapping bump area logic | |
area.y1(d => { | |
if (d.value.comp1 > d.value.comp2){ | |
return scale(d.value.comp1) | |
} else { | |
return scale(d.value.comp1 + d.value.comp2 - d.value.both) | |
} | |
}) | |
area.y0(d => { | |
if (d.value.comp1 > d.value.comp2){ | |
return scale(0) | |
} else { | |
return scale(d.value.comp2 - d.value.both) | |
} | |
}) | |
group.select('path.comp1') | |
.transition(transition) | |
.attr('d', area(nest)) | |
//Overlapping bump area logic | |
area.y1(d => { | |
if (d.value.comp2 > d.value.comp1){ | |
return scale(d.value.comp2) | |
} else { | |
return scale(d.value.comp1 + d.value.comp2 - d.value.both) | |
} | |
}) | |
area.y0(d => { | |
if (d.value.comp2 > d.value.comp1){ | |
return scale(0) | |
} else { | |
return scale(d.value.comp1 - d.value.both) | |
} | |
}) | |
group.select('path.comp2') | |
.transition(transition) | |
.attr('d', area(nest)) | |
} | |
const update = ()=> { | |
updateMap() | |
updateHistogram('lat', lat, latArea, latNested, yLatScale) | |
updateHistogram('long', long, longArea, longNested, yLongScale) | |
//Update text colors in Goods selector | |
svg.selectAll('.types text') | |
.attr('class', d => d.key === comp1 ? 'comp1' : d.key === comp2 ? 'comp2' : '') | |
//Update legend key | |
colors.domain([`${comp1}` , `${comp2}`, 'both']) | |
colorLegend.scale(colors) | |
svg.select('g.legend').call(colorLegend) | |
} | |
//Initial render of graphs and map | |
createHistogram(lat, latArea, latNested) | |
createHistogram(long, longArea, longNested) | |
update() | |
const variables = [ | |
{ "key": "vegetables", "label": "Vegetables 96%", "percent": .96}, | |
{ "key": "bakedgoods", "label": "Baked Goods 88%", "percent": .88}, | |
{ "key": "honey", "label": "Honey 81%", "percent": .81}, | |
{ "key": "jams", "label": "Jams 80%", "percent": .80}, | |
{ "key": "fruits", "label": "Fruits 80%", "percent": .80}, | |
{ "key": "herbs", "label": "Herbs 79%", "percent": .79}, | |
{ "key": "eggs", "label": "Eggs 74%", "percent": .74}, | |
{ "key": "flower", "label": "Flowers 69%", "percent": .69}, | |
{ "key": "soap", "label": "Soap 67%", "percent": .67 }, | |
{ "key": "plants", "label": "Plants 66%", "percent": .66}, | |
{ "key": "crafts", "label": "Crafts 61%", "percent": .61}, | |
{ "key": "prepared", "label": "Prepared Food 61%", "percent": .61}, | |
{ "key": "meat", "label": "Meat 55%", "percent": .55}, | |
{ "key": "cheese", "label": "Cheese 50%", "percent": .50}, | |
{ "key": "poultry", "label": "Poultry 45%", "percent": .45}, | |
{ "key": "coffee", "label": "Coffee 33%", "percent": .33}, | |
{ "key": "maple", "label": "Maple 32%", "percent": .32}, | |
{ "key": "nuts", "label": "Nuts 29%", "percent": .29}, | |
{ "key": "trees", "label": "Trees 29%", "percent": .29}, | |
{ "key": "seafood", "label": "Seafood 24%", "percent": .24}, | |
{ "key": "juices", "label": "Juices 22%", "percent": .22}, | |
{ "key": "mushrooms", "label": "Mushrooms 22%", "percent": .22}, | |
{ "key": "petfood", "label": "Pet Food 18%", "percent": .18}, | |
{ "key": "wine", "label": "Wine 17%", "percent": .17}, | |
{ "key": "beans", "label": "Beans 14%", "percent": .14}, | |
{ "key": "grains", "label": "Grains 14%", "percent": .14}, | |
{ "key": "wildharvest", "label": "Wild Harvest 13%", "percent": .13}, | |
{ "key": "nursery", "label": "Nursery 6%", "percent": .06}, | |
{ "key": "tofu", "label": "Tofu 4%", "percent": .04}, | |
] | |
svg.append('text') | |
.attr('class', '.controlTitle') | |
.attr('x', 20) | |
.attr('y', 40) | |
.text('Goods selector') | |
svg.selectAll('rect.control') | |
.data(['comp1', 'comp2']) | |
.enter() | |
.append('rect') | |
.attr('x', (d, i) => 20 + i*20) | |
.attr('y', 50) | |
.attr('width', 15) | |
.attr('height', 15) | |
.attr('class', d => `control ${d} ${selected === d ? 'selected' : ''}`) | |
.on('click', d => { | |
if (selected === "comp1"){ | |
selected = "comp2" | |
} else { | |
selected = "comp1" | |
} | |
svg.selectAll('rect.control') | |
.attr('class', d => `control ${d} ${selected === d ? 'selected' : ''}`) | |
}) | |
const types = svg.append('g') | |
.attr('class', 'types') | |
let changeComp = (d) => { | |
if (selected === "comp1"){ | |
comp1 = d.key | |
} else { | |
comp2 = d.key | |
} | |
update() | |
} | |
types.selectAll('text') | |
.data(variables) | |
.enter() | |
.append('text') | |
.attr('x', 20) | |
.attr('y', (d, i) => i*14 + 80) | |
.text(d => d.label) | |
.attr('class', d => d.key === comp1 ? 'comp1' : d.key === comp2 ? 'comp2' : '') | |
.on('click', changeComp) | |
}); |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'> | |
<style> | |
body{ | |
background-color: whitesmoke; | |
} | |
svg { | |
background-color: white; | |
font-family: 'Lato'; | |
} | |
text.title { | |
text-anchor: middle; | |
font-size: 20px; | |
} | |
.legend text { | |
font-size: 12px; | |
} | |
path { | |
fill-opacity: .8; | |
} | |
circle { | |
fill: grey; | |
opacity: .7; | |
} | |
.comp1 { | |
fill: rgb(0, 200, 200); | |
} | |
.comp2 { | |
fill: rgb(200, 0, 200); | |
} | |
.compBoth { | |
fill: #ac8cdc; | |
} | |
path.comp1, path.comp2 { | |
opacity: .5; | |
} | |
rect { | |
opacity: .8; | |
cursor: pointer; | |
} | |
rect.comp1 { | |
stroke: rgb(0, 200, 200); | |
} | |
rect.comp2 { | |
stroke: rgb(200, 0, 200); | |
} | |
rect:not(.selected) { | |
fill: white; | |
} | |
.types { | |
font-size: 8px; | |
text-transform: uppercase; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
</style> | |
</head> | |
<body> | |
<svg width="960" height="500"></svg> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.9.0/d3-legend.min.js"></script> | |
<script src="index.js"></script> | |
</body> | |
</html> |
'use strict'; | |
var svg = d3.select('svg'); | |
svg.append('text').attr('class', 'title').attr('x', 960 / 2).attr('y', 35).text("Farmers' Markets Goods Comparison"); | |
d3.json('https://gist.githubusercontent.com/susielu/3d194b8660ec6ab214a3/raw/38a2cdc96efaaaeb4849c86b600de5dfecea2dec/farmers-markets-lat-long.json', function (error, data) { | |
var h = 480; | |
var w = 800; | |
var padding = 20; | |
var xScale = d3.scaleLinear().range([padding, w - padding]).domain([-130, -65]); | |
var xBarScale = d3.scaleLinear().range([padding, w - padding]); | |
var yScale = d3.scaleLinear().range([h - padding, padding]).domain([20, 50]); | |
var comp1 = "maple"; | |
var comp2 = "seafood"; | |
var selected = 'comp1'; | |
var offset = 'translate(60, 40)'; | |
//Filtering out states outside of the contiguous US for simplicity | |
data = data.filter(function (d) { | |
return d.x >= -130 && d.x <= -65 && d.y >= 20 && d.y <= 50; | |
}); | |
//Making a legend w00t http://d3-legend.susielu.com/ | |
var colors = d3.scaleOrdinal().domain(['' + comp1, '' + comp2, 'both']).range(["rgba(0, 200, 200, .5)", "rgba(200, 0, 200, .5)", "#ac8cdc"]); | |
var colorLegend = d3.legendColor().shapeHeight(8).shapePadding(5).scale(colors); | |
svg.append('g').attr('class', 'legend').attr('transform', 'translate(200, 390)').call(colorLegend); | |
var map = svg.append('g').attr('class', 'map').attr('transform', offset); | |
map.selectAll('circle').data(data).enter().append('circle').attr('r', 1).attr('cx', function (d) { | |
return xScale(d.x); | |
}).attr('cy', function (d) { | |
return yScale(d.y); | |
}); | |
var rollup = function rollup(leaves) { | |
var first = 0; | |
var second = 0; | |
var both = 0; | |
leaves.forEach(function (l) { | |
if (l[comp1] === "Y") { | |
first++; | |
} | |
if (l[comp2] === "Y") { | |
second++; | |
} | |
if (l[comp1] === "Y" && l[comp2] === "Y") { | |
both++; | |
} | |
}); | |
return { | |
length: leaves.length, | |
comp1: first, | |
comp2: second, | |
both: both | |
}; | |
}; | |
var lat = svg.append('g').attr('class', 'lat').attr('transform', offset); | |
var latArea = d3.area().x(function (d) { | |
return xScale(parseInt(d.key)); | |
}).y1(function (d) { | |
return yLatScale(d.value.length); | |
}).y0(function (d) { | |
return yLatScale(0); | |
}).curve(d3.curveCardinal), | |
latNested = d3.nest().key(function (d) { | |
return Math.round(d.x); | |
}).rollup(rollup).entries(data).sort(function (a, b) { | |
return parseInt(a.key) - parseInt(b.key); | |
}); | |
var yLatMax = d3.max(latNested, function (d) { | |
return d.value.length; | |
}); | |
var yLatScale = d3.scaleLinear().range([h - 40, h - 140]).domain([0, yLatMax]); | |
//Makes a horizontal bar chart then rotates it for the longitudinal graph | |
var long = svg.append('g').attr('class', 'long').attr('transform', 'rotate(90, ' + (w + 60) + ', 40) ' + offset); | |
var xLongScale = d3.scaleLinear().range([w + padding, w + h - padding]).domain([50, 20]); | |
var longArea = d3.area().x(function (d) { | |
return xLongScale(parseInt(d.key)); | |
}).y1(function (d) { | |
return yLongScale(d.value.length); | |
}).y0(function (d) { | |
return yLongScale(0); | |
}).curve(d3.curveCardinal), | |
longNested = d3.nest().key(function (d) { | |
return Math.round(d.y); | |
}).rollup(rollup).entries(data).sort(function (a, b) { | |
return parseInt(a.key) - parseInt(b.key); | |
}); | |
var yLongMax = d3.max(longNested, function (d) { | |
return d.value.length; | |
}); | |
var yLongScale = d3.scaleLinear().range([padding, padding - 100]).domain([0, yLongMax]); | |
var transition = d3.transition().ease(d3.easePolyInOut); | |
var createHistogram = function createHistogram(group, area, nest) { | |
group.append('path').attr('fill', 'none').attr('stroke', 'grey').attr('d', area(nest)); | |
group.append('path').attr('class', 'comp1'); | |
group.append('path').attr('class', 'comp2'); | |
}; | |
var updateMap = function updateMap() { | |
map.selectAll('circle').attr('class', function (d) { | |
return d[comp1] === "Y" && d[comp2] === "Y" ? 'compBoth' : d[comp1] === "Y" ? 'comp1' : d[comp2] === "Y" ? 'comp2' : ''; | |
}); | |
}; | |
var updateHistogram = function updateHistogram(type, group, area, nest, scale) { | |
var nestKey = type === "lat" ? 'x' : 'y'; | |
nest = d3.nest().key(function (d) { | |
return Math.round(d[nestKey]); | |
}).rollup(rollup).entries(data).sort(function (a, b) { | |
return parseInt(a.key) - parseInt(b.key); | |
}); | |
//Overlapping bump area logic | |
area.y1(function (d) { | |
if (d.value.comp1 > d.value.comp2) { | |
return scale(d.value.comp1); | |
} else { | |
return scale(d.value.comp1 + d.value.comp2 - d.value.both); | |
} | |
}); | |
area.y0(function (d) { | |
if (d.value.comp1 > d.value.comp2) { | |
return scale(0); | |
} else { | |
return scale(d.value.comp2 - d.value.both); | |
} | |
}); | |
group.select('path.comp1').transition(transition).attr('d', area(nest)); | |
//Overlapping bump area logic | |
area.y1(function (d) { | |
if (d.value.comp2 > d.value.comp1) { | |
return scale(d.value.comp2); | |
} else { | |
return scale(d.value.comp1 + d.value.comp2 - d.value.both); | |
} | |
}); | |
area.y0(function (d) { | |
if (d.value.comp2 > d.value.comp1) { | |
return scale(0); | |
} else { | |
return scale(d.value.comp1 - d.value.both); | |
} | |
}); | |
group.select('path.comp2').transition(transition).attr('d', area(nest)); | |
}; | |
var update = function update() { | |
updateMap(); | |
updateHistogram('lat', lat, latArea, latNested, yLatScale); | |
updateHistogram('long', long, longArea, longNested, yLongScale); | |
//Update text colors in Goods selector | |
svg.selectAll('.types text').attr('class', function (d) { | |
return d.key === comp1 ? 'comp1' : d.key === comp2 ? 'comp2' : ''; | |
}); | |
//Update legend key | |
colors.domain(['' + comp1, '' + comp2, 'both']); | |
colorLegend.scale(colors); | |
svg.select('g.legend').call(colorLegend); | |
}; | |
//Initial render of graphs and map | |
createHistogram(lat, latArea, latNested); | |
createHistogram(long, longArea, longNested); | |
update(); | |
var variables = [{ "key": "vegetables", "label": "Vegetables 96%", "percent": .96 }, { "key": "bakedgoods", "label": "Baked Goods 88%", "percent": .88 }, { "key": "honey", "label": "Honey 81%", "percent": .81 }, { "key": "jams", "label": "Jams 80%", "percent": .80 }, { "key": "fruits", "label": "Fruits 80%", "percent": .80 }, { "key": "herbs", "label": "Herbs 79%", "percent": .79 }, { "key": "eggs", "label": "Eggs 74%", "percent": .74 }, { "key": "flower", "label": "Flowers 69%", "percent": .69 }, { "key": "soap", "label": "Soap 67%", "percent": .67 }, { "key": "plants", "label": "Plants 66%", "percent": .66 }, { "key": "crafts", "label": "Crafts 61%", "percent": .61 }, { "key": "prepared", "label": "Prepared Food 61%", "percent": .61 }, { "key": "meat", "label": "Meat 55%", "percent": .55 }, { "key": "cheese", "label": "Cheese 50%", "percent": .50 }, { "key": "poultry", "label": "Poultry 45%", "percent": .45 }, { "key": "coffee", "label": "Coffee 33%", "percent": .33 }, { "key": "maple", "label": "Maple 32%", "percent": .32 }, { "key": "nuts", "label": "Nuts 29%", "percent": .29 }, { "key": "trees", "label": "Trees 29%", "percent": .29 }, { "key": "seafood", "label": "Seafood 24%", "percent": .24 }, { "key": "juices", "label": "Juices 22%", "percent": .22 }, { "key": "mushrooms", "label": "Mushrooms 22%", "percent": .22 }, { "key": "petfood", "label": "Pet Food 18%", "percent": .18 }, { "key": "wine", "label": "Wine 17%", "percent": .17 }, { "key": "beans", "label": "Beans 14%", "percent": .14 }, { "key": "grains", "label": "Grains 14%", "percent": .14 }, { "key": "wildharvest", "label": "Wild Harvest 13%", "percent": .13 }, { "key": "nursery", "label": "Nursery 6%", "percent": .06 }, { "key": "tofu", "label": "Tofu 4%", "percent": .04 }]; | |
svg.append('text').attr('class', '.controlTitle').attr('x', 20).attr('y', 40).text('Goods selector'); | |
svg.selectAll('rect.control').data(['comp1', 'comp2']).enter().append('rect').attr('x', function (d, i) { | |
return 20 + i * 20; | |
}).attr('y', 50).attr('width', 15).attr('height', 15).attr('class', function (d) { | |
return 'control ' + d + ' ' + (selected === d ? 'selected' : ''); | |
}).on('click', function (d) { | |
if (selected === "comp1") { | |
selected = "comp2"; | |
} else { | |
selected = "comp1"; | |
} | |
svg.selectAll('rect.control').attr('class', function (d) { | |
return 'control ' + d + ' ' + (selected === d ? 'selected' : ''); | |
}); | |
}); | |
var types = svg.append('g').attr('class', 'types'); | |
var changeComp = function changeComp(d) { | |
if (selected === "comp1") { | |
comp1 = d.key; | |
} else { | |
comp2 = d.key; | |
} | |
update(); | |
}; | |
types.selectAll('text').data(variables).enter().append('text').attr('x', 20).attr('y', function (d, i) { | |
return i * 14 + 80; | |
}).text(function (d) { | |
return d.label; | |
}).attr('class', function (d) { | |
return d.key === comp1 ? 'comp1' : d.key === comp2 ? 'comp2' : ''; | |
}).on('click', changeComp); | |
}); |