Skip to content

Instantly share code, notes, and snippets.

@anbnyc
Last active March 21, 2018 02:48
Show Gist options
  • Save anbnyc/6788c595fd95a3b9605245941ac73299 to your computer and use it in GitHub Desktop.
Save anbnyc/6788c595fd95a3b9605245941ac73299 to your computer and use it in GitHub Desktop.
Infinite Bubbles prototype
<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>
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