Last active
March 21, 2018 02:48
-
-
Save anbnyc/6788c595fd95a3b9605245941ac73299 to your computer and use it in GitHub Desktop.
Infinite Bubbles prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<style> | |
circle{ | |
stroke-width: 1px; | |
stroke: #333; | |
} | |
rect.white-background{ | |
fill: #fff; | |
opacity: .75; | |
} | |
.label text{ | |
font-size: 24px; | |
font-family: Helvetica; | |
font-weight: bold; | |
fill: #111; | |
} | |
.tooltip{ | |
font-size: 20px; | |
alignment-baseline: middle; | |
font-family: Helvetica; | |
fill: #111; | |
} | |
.legend-entry text{ | |
text-anchor: end; | |
font-size: 12px; | |
font-family: Helvetica; | |
} | |
</style> | |
</head> | |
<body> | |
</body> | |
<script> | |
const dc = o => JSON.parse(JSON.stringify(o)) | |
const viewer = r => [r.x, r.y, r.r*2] | |
const body = d3.select("body") | |
const nutrientNames = { | |
"IRON": "Iron", | |
"POTASSIUM": "Potassium", | |
"CALCIUM": "Calcium", | |
"FIBER": "Dietary Fiber", | |
"SODIUM": "Sodium", | |
"VITA": "Vitamin A", | |
"VITC": "Vitamin C", | |
"CARB": "Total Carb", | |
"FAT": "Total Fat", | |
} | |
const { IRON, POTASSIUM, CALCIUM, FIBER, SODIUM, VITA, VITC, CARB, FAT } = nutrientNames | |
let state = { | |
height: 700,//window.innerHeight, | |
width: 1400,//window.innerWidth, | |
view: null, | |
level: 0, | |
D: 600 | |
} | |
const pack = d3.pack() | |
.size([state.D, state.D]) | |
const foodColor = d3.scaleOrdinal() | |
.domain(["Seafood","Fruits","Vegetables"]) | |
.range(["#22f", "#f82", "#2f2"]) | |
const nutrientColor = d3.scaleOrdinal(d3.schemeSet2).domain(Object.keys(nutrientNames)); | |
const metaColor = ({ name, type, category }) => | |
type === 'foods' | |
? foodColor(category) | |
: type === 'nutrients' | |
? nutrientColor(name) | |
: '#ddd' | |
const labelMaker = ({ name, type, children }) => ({ | |
foods: `${children.length} nutrition facts for ${name} sized by % daily value in a serving`, | |
nutrients: `${children.length} foods with ${nutrientNames[name]} sized by % daily value in a serving` | |
})[type] || `Nine nutrition facts for 61 foods` | |
d3.tsv('./nutrition_data.tsv').then(function(raw){ | |
let nutrients = Object.keys(nutrientNames).reduce((t,v) => ({ ...t, [v]: { type: 'nutrients', name: v, sum: 0, children: [] } }), {}) | |
let foods = {} | |
raw.forEach(e => { | |
foods[e.Food] = { | |
type: 'foods', | |
name: e.Food, | |
category: e.Category, | |
sum: 0, | |
children: [] | |
} | |
Object.keys(nutrientNames).forEach(n => { | |
if(+e[nutrientNames[n]]){ | |
foods[e.Food][n] = +e[nutrientNames[n]] | |
foods[e.Food].sum += +e[nutrientNames[n]] | |
foods[e.Food].children.push({ ...nutrients[n], children: [] }) // no children to prevent infinite loop | |
nutrients[n][e.Food] = +e[nutrientNames[n]] | |
nutrients[n].sum += +e[nutrientNames[n]] | |
nutrients[n].children.push({ ...foods[e.Food], children: [] }) | |
} | |
}) | |
}) | |
const root = d3.hierarchy({ children: Object.values(foods) }).sum(d => d.sum) | |
pack(root) | |
setState({ foods, nutrients, data: { name: 'root', children: Object.values(foods), oldParent: {} }, view: viewer(root), zooming: false }) | |
}) | |
function setState(nextState){ | |
const prevState = state | |
state = Object.assign({}, state, nextState) | |
update(prevState) | |
} | |
function update(prevState){ | |
const { height, width, data, level, D, zooming } = state | |
const root = d3.hierarchy(data).sum(d => d.sum) | |
pack(root) | |
const view = state.view || viewer(root) | |
const k = D / view[2] | |
let svg = body.selectAll("svg") | |
.data([null]) | |
svg = svg | |
.enter().append("svg") | |
.merge(svg) | |
.attr('height', height) | |
.attr('width', width) | |
let nodes = svg.selectAll("g.nodes") | |
.data([null]) | |
nodes = nodes | |
.enter().append('g') | |
.attr('class','nodes') | |
.merge(nodes) | |
.attr("transform", `translate(${width/2},${height/2})`) | |
nodes.selectAll('circle.exit') | |
.transition() | |
.duration(50) | |
.style('opacity',0) | |
.remove(); | |
let node = nodes.selectAll('circle.node') | |
.data(root.descendants(), d => (d.parent ? d.parent.data.name : d.data.oldParent.name)+d.data.name) | |
node.exit() | |
.classed('exit', true) | |
.on("click", null) | |
let nodeEnter = node | |
.enter().append('circle') | |
.attr('class','node') | |
.attr('r', 0) | |
nodeEnter | |
.attr("transform", d => `translate(${(d.x - view[0])*k}, ${(d.y - view[1])*k})`) | |
.transition() | |
.delay(100) | |
.duration(250) | |
.attr('r', d => d.r * k) | |
node | |
.transition() | |
.delay(zooming ? 0 : 100) | |
.duration(zooming ? 0 : 250) | |
.attr("transform", d => `translate(${(d.x - view[0])*k}, ${(d.y - view[1])*k})`) | |
.attr('r', d => d.r * k) | |
node = nodeEnter | |
.merge(node) | |
.attr("fill", d => metaColor(d.data)) | |
.on("mousemove", function(d){ | |
tooltip | |
.attr('x', d3.event.x + 10) | |
.attr('y', d3.event.y) | |
.style('visibility','visible') | |
.text((d.data.type === 'foods' | |
? d.data.name | |
: d.data.type === 'nutrients' | |
? nutrientNames[d.data.name] | |
: '') | |
+ (d.parent && d.parent.data[d.data.name] ? " "+d.parent.data[d.data.name]+"%" : "")) | |
}) | |
.on("mouseout", function(){ | |
tooltip.style('visibility','hidden') | |
}) | |
.on("click", function(d){ | |
d3.event.stopPropagation(); | |
d3.transition() | |
.duration(750) | |
.delay(100) | |
.tween("zoom", function(){ | |
return function(t) { | |
setState({ view: d3.interpolateZoom(view, viewer(d))(t) }); | |
}; | |
}) | |
.on("start", function(){ | |
setState({ zooming: true }) | |
}) | |
.on("end", function(){ | |
if(d.depth !== level){ | |
let newData = { | |
...d.data, | |
oldParent: { ...d.parent.data, children: null, oldParent: null }, | |
...state[d.data.type][d.data.name], | |
// ...(state[d.data.type][d.data.name].children.reduce((t,v) => ({...t, [v.name]: state[d.data.type] }) ,{})) | |
} | |
setState({ data: newData, level: 0, view: null, zooming: false }) | |
} | |
}) | |
}) | |
let legend = svg.selectAll(".legend") | |
.data([null]) | |
.enter().append('g') | |
.attr('class','legend') | |
.attr('transform', `translate(${width - 30},${height - 50})`) | |
legend = legend | |
.enter().append('g') | |
.attr('class','legend') | |
.merge(legend) | |
let legendRow = legend.selectAll('.legend-row') | |
.data([foodColor.domain(), nutrientColor.domain()]) | |
let legendRowEnter = legendRow | |
.enter().append('g') | |
.attr('class','legend-row') | |
legendRowEnter.append('rect') | |
.attr('class','white-background') | |
.attr("height", 20) | |
.attr("y", -15) | |
.attr("width", d => 85*d.length) | |
.attr("x", d => -85*d.length + 20) | |
legendRow = legendRowEnter | |
.merge(legendRow) | |
.attr('transform', (d,i) => `translate(0,${i === 0 ? 0 : 30})`) | |
legendRow | |
let legendEntry = legendRow.selectAll('.legend-entry') | |
.data((d,i) => d.map(e => ({ datum: e, index: i }))) | |
.enter().append('g') | |
.attr('class','legend-entry') | |
.attr('transform', (d,i) => `translate(${-85*i},0)`) | |
legendEntry.append("circle") | |
.attr('r', 5) | |
.attr('cx', 7) | |
.attr('cy', -4) | |
.attr('fill', d => d.index === 0 ? foodColor(d.datum) : nutrientColor(d.datum)) | |
legendEntry.append("text") | |
.text(d => d.index === 0 ? d.datum : nutrientNames[d.datum]) | |
let tooltip = svg.selectAll('.tooltip') | |
.data([null]) | |
tooltip = tooltip | |
.enter().append('text') | |
.attr('class','tooltip') | |
.merge(tooltip) | |
.style('visibility', 'hidden') | |
let label = svg.selectAll(".label") | |
.data([labelMaker(data)]) | |
labelEnter = label | |
.enter().append("g") | |
.attr("class", "label") | |
.attr("transform", "translate(10, 10)") | |
labelEnter.append("rect") | |
.attr('class','white-background') | |
.attr("height", 32) | |
label = labelEnter | |
.merge(label) | |
let labelText = label.selectAll("text") | |
.data(d => [d]) | |
labelText = labelText.enter().append("text") | |
.merge(labelText) | |
.text(text => text) | |
.attr("x", 6) | |
.attr("y", 25) | |
label.selectAll("rect") | |
.attr("width", function(d){ | |
return this.nextSibling.getBBox().width + 12 | |
}) | |
} | |
</script> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Food | Serving Size | Category | Total Fat | Sodium | Potassium | Total Carb | Dietary Fiber | Vitamin A | Vitamin C | Calcium | Iron | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Apple | 1 large (242 g/8 oz) | Fruits | 0 | 0 | 7 | 11 | 20 | 2 | 8 | 2 | 2 | |
Avocado California | 1/5 medium (30 g/1.1 oz) | Fruits | 7 | 0 | 4 | 1 | 4 | 0 | 4 | 0 | 2 | |
Banana | 1 medium (126 g/4.5 oz) | Fruits | 0 | 0 | 13 | 10 | 12 | 2 | 15 | 0 | 2 | |
Cantaloupe | 1/4 medium (134 g/4.8 oz) | Fruits | 0 | 1 | 7 | 4 | 4 | 120 | 80 | 2 | 2 | |
Grapefruit | 1/2 medium (154 g/5.5 oz) | Fruits | 0 | 0 | 5 | 5 | 8 | 35 | 100 | 4 | 0 | |
Grapes | 3/4 cup (126 g/4.5 oz) | Fruits | 0 | 1 | 7 | 8 | 4 | 0 | 2 | 2 | 0 | |
Honeydew Melon | 1/10 medium melon (134 g/4.8 oz) | Fruits | 0 | 1 | 6 | 4 | 4 | 2 | 45 | 2 | 2 | |
Kiwifruit | 2 medium (148 g/5.3 oz) | Fruits | 2 | 0 | 13 | 7 | 16 | 2 | 240 | 4 | 2 | |
Lemon | 1 medium (58 g/2.1 oz) | Fruits | 0 | 0 | 2 | 2 | 8 | 0 | 40 | 2 | 0 | |
Lime | 1 medium (67 g/2.4 oz) | Fruits | 0 | 0 | 2 | 2 | 8 | 0 | 35 | 0 | 0 | |
Nectarine | 1 medium (140 g/5.0 oz) | Fruits | 1 | 0 | 7 | 5 | 8 | 8 | 15 | 0 | 2 | |
Orange | 1 medium (154 g/5.5 oz) | Fruits | 0 | 0 | 7 | 6 | 12 | 2 | 130 | 6 | 0 | |
Peach | 1 medium (147 g/5.3 oz) | Fruits | 1 | 0 | 7 | 5 | 8 | 6 | 15 | 0 | 2 | |
Pear | 1 medium (166 g/5.9 oz) | Fruits | 0 | 0 | 5 | 9 | 24 | 0 | 10 | 2 | 0 | |
Pineapple | 2 slices, 3in diameter, 3/4 thick (112 g/4 oz) | Fruits | 0 | 0 | 3 | 4 | 4 | 2 | 50 | 2 | 2 | |
Plums | 2 medium (151 g/5.4 oz) | Fruits | 0 | 0 | 7 | 6 | 8 | 8 | 10 | 0 | 2 | |
Strawberries | 8 medium (147 g/5.3 oz) | Fruits | 0 | 0 | 5 | 4 | 8 | 0 | 160 | 2 | 2 | |
Sweet Cherries | 21 cherries; 1 cup (140 g/5.0 oz) | Fruits | 0 | 0 | 10 | 9 | 4 | 2 | 15 | 2 | 2 | |
Tangerine | 1 medium (109 g/3.9 oz) | Fruits | 0 | 0 | 5 | 4 | 8 | 6 | 45 | 4 | 0 | |
Watermelon | 1/18 medium melon; 2 cups diced pieces (280 g/10.0 oz) | Fruits | 0 | 0 | 8 | 7 | 4 | 30 | 25 | 2 | 4 | |
Asparagus | 5 spears (93 g/3.3 oz) | Vegetables | 0 | 0 | 7 | 1 | 8 | 10 | 15 | 2 | 2 | |
Bell Pepper | 1 medium (148 g/5.3 oz) | Vegetables | 0 | 2 | 6 | 2 | 8 | 4 | 190 | 2 | 4 | |
Broccoli | 1 medium stalk (148 g/5.3 oz) | Vegetables | 1 | 3 | 13 | 3 | 12 | 6 | 220 | 6 | 6 | |
Carrot | 1 carrot, 7in long, 1 1/4 diameter (78 g/2.8 oz) | Vegetables | 0 | 3 | 7 | 2 | 8 | 110 | 10 | 2 | 2 | |
Cauliflower | 1/6 medium head (99 g/3.5 oz) | Vegetables | 0 | 1 | 8 | 2 | 8 | 0 | 100 | 2 | 2 | |
Celery | 2 medium stalks (110 g/3.9 oz) | Vegetables | 0 | 5 | 7 | 1 | 8 | 10 | 15 | 4 | 2 | |
Cucumber | 1/3 medium (99 g/3.5 oz) | Vegetables | 0 | 0 | 4 | 1 | 4 | 4 | 10 | 2 | 2 | |
Green (Snap) Beans | 3/4 cup cut (83 g/3.0 oz) | Vegetables | 0 | 0 | 6 | 2 | 12 | 4 | 10 | 4 | 2 | |
Green Cabbage | 1/12 medium head (84 g/3.0 oz) | Vegetables | 0 | 1 | 5 | 2 | 8 | 0 | 70 | 4 | 2 | |
Green Onion | 1/4 cup chopped (25 g/0.9 oz) | Vegetables | 0 | 0 | 2 | 1 | 4 | 2 | 8 | 2 | 2 | |
Iceberg Lettuce | 1/6 medium head (89 g/3.2 oz) | Vegetables | 0 | 0 | 4 | 1 | 4 | 6 | 6 | 2 | 2 | |
Leaf Lettuce | 1 1/2 cups shredded (85 g/3.0 oz) | Vegetables | 0 | 1 | 5 | 1 | 4 | 130 | 6 | 2 | 4 | |
Mushrooms | 5 medium (84 g/3.0 oz) | Vegetables | 0 | 0 | 9 | 1 | 4 | 0 | 2 | 0 | 2 | |
Onion | 1 medium (148 g/5.3 oz) | Vegetables | 0 | 0 | 5 | 4 | 12 | 0 | 20 | 4 | 4 | |
Potato | 1 medium (148 g/5.3 oz) | Vegetables | 0 | 0 | 18 | 9 | 8 | 0 | 45 | 2 | 6 | |
Radishes | 7 radishes (85 g/3.0 oz) | Vegetables | 0 | 2 | 5 | 1 | 4 | 0 | 30 | 2 | 2 | |
Summer Squash | 1/2 medium (98 g/3.5 oz) | Vegetables | 0 | 0 | 7 | 1 | 8 | 6 | 30 | 2 | 2 | |
Sweet Corn | kernels from 1 medium ear (90 g/3.2 oz) | Vegetables | 4 | 0 | 7 | 6 | 8 | 2 | 10 | 0 | 2 | |
Sweet Potato | 1 medium, 5in long, 2 diameter (130 g/4.6 oz) | Vegetables | 0 | 3 | 13 | 8 | 16 | 120 | 30 | 4 | 4 | |
Tomato | 1 medium (148 g/5.3 oz) | Vegetables | 0 | 1 | 10 | 2 | 4 | 20 | 40 | 2 | 4 | |
Blue Crab | (84 g/3 oz) | Seafood | 2 | 14 | 9 | 0 | 0 | 0 | 4 | 10 | 4 | |
Catfish | (84 g/3 oz) | Seafood | 9 | 2 | 7 | 0 | 0 | 0 | 0 | 0 | 0 | |
Clams | about 12 small | Seafood | 2 | 4 | 13 | 2 | 0 | 10 | 0 | 8 | 30 | |
Cod | (84 g/3 oz) | Seafood | 2 | 3 | 13 | 0 | 0 | 0 | 2 | 2 | 2 | |
Flounder/Sole | (84 g/3 oz) | Seafood | 2 | 4 | 11 | 0 | 0 | 0 | 0 | 2 | 0 | |
Haddock | (84 g/3 oz) | Seafood | 2 | 4 | 10 | 0 | 0 | 2 | 0 | 2 | 6 | |
Halibut | (84 g/3 oz) | Seafood | 3 | 3 | 14 | 0 | 0 | 4 | 0 | 2 | 6 | |
Lobster | (84 g/3 oz) | Seafood | 1 | 13 | 9 | 0 | 0 | 2 | 0 | 6 | 2 | |
Ocean Perch | (84 g/3 oz) | Seafood | 3 | 4 | 8 | 0 | 0 | 0 | 2 | 10 | 4 | |
Orange Roughy | (84 g/3 oz) | Seafood | 2 | 3 | 10 | 0 | 0 | 2 | 0 | 4 | 2 | |
Oysters | about 12 medium | Seafood | 6 | 13 | 6 | 2 | 0 | 0 | 6 | 6 | 45 | |
Pollock | (84 g/3 oz) | Seafood | 2 | 5 | 11 | 0 | 0 | 2 | 0 | 0 | 2 | |
Rainbow Trout | (84 g/3 oz) | Seafood | 9 | 1 | 11 | 0 | 0 | 4 | 4 | 8 | 2 | |
Rockfish | (84 g/3 oz) | Seafood | 3 | 3 | 13 | 0 | 0 | 4 | 0 | 2 | 2 | |
Atlantic Salmon/Coho/Sockeye/Chinook | (84 g/3 oz) | Seafood | 15 | 2 | 12 | 0 | 0 | 4 | 4 | 2 | 2 | |
Chum Salmon/Pink | (84 g/3 oz) | Seafood | 6 | 3 | 12 | 0 | 0 | 2 | 0 | 2 | 4 | |
Scallops | about 6 large or 14 small | Seafood | 2 | 13 | 12 | 2 | 0 | 2 | 0 | 4 | 14 | |
Shrimp | (84 g/3 oz) | Seafood | 2 | 10 | 6 | 0 | 0 | 4 | 4 | 6 | 10 | |
Swordfish | (84 g/3 oz) | Seafood | 9 | 4 | 9 | 0 | 0 | 2 | 2 | 0 | 6 | |
Tilapia | (84 g/3 oz) | Seafood | 4 | 1 | 10 | 0 | 0 | 0 | 2 | 0 | 2 | |
Tuna | (84 g/3 oz) | Seafood | 2 | 2 | 14 | 0 | 0 | 2 | 2 | 2 | 4 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment