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); | |
| }); |