getViewport = ->
e = window
a = "inner"
#unless "innerWidth" of window
# a = "client"
# e = document.documentElement or document.body
if e[a + "Width"] > 1000
w = e[a + "Width"] / 2.5
else
w = e[a + "Width"]
width: w
height: e[a + "Height"]
root = exports ? this
root.Bubbles = () ->
# standard variables accessible to
# the rest of the functions inside Bubbles
width = getViewport().width
height = 400
data = []
node = null
label = null
margin = {top: 5, right: 0, bottom: 0, left: 0}
# largest size for our bubbles
maxRadius = 55
# this scale will be used to size our bubbles
rScale = d3.scale.sqrt().range([0,maxRadius])
# I've abstracted the data value used to size each
# into its own function. This should make it easy
# to switch out the underlying dataset
rValue = (d) -> parseFloat(d.count)
# function to define the 'id' of a data element
# - used to bind the data uniquely to the force nodes
# and for url creation
# - should make it easier to switch out dataset
# for your own
idValue = (d) -> d.name
# function to define what to display in each bubble
# again, abstracted to ease migration to
# a different dataset if desired
textValue = (d) -> d.name
# constants to control how
# collision look and act
collisionPadding = 4
minCollisionRadius = 12
# variables that can be changed
# to tweak how the force layout
# acts
# - jitter controls the 'jumpiness'
# of the collisions
jitter = 1.2
# ---
# tweaks our dataset to get it into the
# format we want
# - for this dataset, we just need to
# ensure the count is a number
# - for your own dataset, you might want
# to tweak a bit more
# ---
transformData = (rawData) ->
rawData.forEach (d) ->
d.count = parseFloat(d.count)
rawData.sort(() -> 0.5 - Math.random())
rawData
# ---
# tick callback function will be executed for every
# iteration of the force simulation
# - moves force nodes towards their destinations
# - deals with collisions of force nodes
# - updates visual bubbles to reflect new force node locations
# ---
tick = (e) ->
dampenedAlpha = e.alpha * 0.1
# Most of the work is done by the gravity and collide
# functions.
node
.each(gravity(dampenedAlpha))
.each(collide(jitter))
.attr("transform", (d) -> "translate(#{d.x},#{d.y})")
# As the labels are created in raw html and not svg, we need
# to ensure we specify the 'px' for moving based on pixels
label
.style("left", (d) -> ((margin.left + d.x) - d.dx / 2) + "px")
.style("top", (d) -> ((margin.top + d.y) - d.dy / 2) + "px")
# The force variable is the force layout controlling the bubbles
# here we disable gravity and charge as we implement custom versions
# of gravity and collisions for this visualization
force = d3.layout.force()
.gravity(0)
.charge(0)
.size([width, height])
.on("tick", tick)
root.force_sup = force;
# ---
# Creates new chart function. This is the 'constructor' of our
# visualization
# Check out http://bost.ocks.org/mike/chart/
# for a explanation and rational behind this function design
# ---
chart = (selection) ->
selection.each (rawData) ->
# first, get the data in the right format
data = transformData(rawData)
# setup the radius scale's domain now that
# we have some data
maxDomainValue = d3.max(data, (d) -> rValue(d))
rScale.domain([0, maxDomainValue])
# a fancy way to setup svg element
svg = d3.select(this).selectAll("svg").data([data])
svgEnter = svg.enter().append("svg")
svg.attr("width", width + margin.left + margin.right )
svg.attr("height", height + margin.top + margin.bottom )
# node will be used to group the bubbles
node = svgEnter.append("g").attr("id", "bubble-nodes")
.attr("transform", "translate(#{margin.left},#{margin.top})")
# clickable background rect to clear the current selection
node.append("rect")
.attr("id", "bubble-background")
.attr("width", width)
.attr("height", height)
.on("click", clear)
# label is the container div for all the labels that sit on top of
# the bubbles
# - remember that we are keeping the labels in plain html and
# the bubbles in svg
label = d3.select(this).selectAll("#bubble-labels").data([data])
.enter()
.append("div")
.attr("id", "bubble-labels")
update()
# ---
# update starts up the force directed layout and then
# updates the nodes and labels
# ---
update = () ->
# add a radius to our data nodes that will serve to determine
# when a collision has occurred. This uses the same scale as
# the one used to size our bubbles, but it kicks up the minimum
# size to make it so smaller bubbles have a slightly larger
# collision 'sphere'
data.forEach (d,i) ->
d.forceR = Math.max(minCollisionRadius, rScale(rValue(d)))
# start up the force layout
force.nodes(data).start()
# call our update methods to do the creation and layout work
updateNodes()
updateLabels()
# ---
# updateNodes creates a new bubble for each node in our dataset
# ---
updateNodes = () ->
# here we are using the idValue function to uniquely bind our
# data to the (currently) empty 'bubble-node selection'.
# if you want to use your own data, you just need to modify what
# idValue returns
node = node.selectAll(".bubble-node").data(data, (d) -> idValue(d))
# we don't actually remove any nodes from our data in this example
# but if we did, this line of code would remove them from the
# visualization as well
node.exit().remove()
# nodes are just links with circles inside.
# the styling comes from the css
node.enter()
.append("a")
.attr("class", (d) -> "bubble-node " + toggleColor(d))
.attr("xlink:href", (d) -> "##{encodeURIComponent(idValue(d))}")
.call(force.drag)
.call(connectEvents)
.append("circle")
.attr("r", (d) -> rScale(rValue(d)))
# ---
# updateLabels is more involved as we need to deal with getting the sizing
# to work well with the font size
# ---
updateLabels = () ->
# as in updateNodes, we use idValue to define what the unique id for each data
# point is
label = label.selectAll(".bubble-label").data(data, (d) -> idValue(d))
label.exit().remove()
# labels are anchors with div's inside them
# labelEnter holds our enter selection so it
# is easier to append multiple elements to this selection
labelEnter = label.enter().append("a")
.attr("class", "bubble-label")
.attr("href", (d) -> "##{encodeURIComponent(idValue(d))}")
.call(force.drag)
.call(connectEvents)
labelEnter.append("div")
.attr("class", "bubble-label-name")
.attr("data-value", (d) -> rValue(d))
.text((d) -> textValue(d))
#labelEnter.append("div")
# .attr("class", "bubble-label-value")
# .text((d) -> rValue(d))
# label font size is determined based on the size of the bubble
# this sizing allows for a bit of overhang outside of the bubble
# - remember to add the 'px' at the end as we are dealing with
# styling divs
label
.style("font-size", (d) -> Math.min(Math.max(8, rScale(rValue(d) / 2)),40) + "px")
.style("width", (d) -> 2.5 * rScale(rValue(d)) + "px")
# interesting hack to get the 'true' text width
# - create a span inside the label
# - add the text to this span
# - use the span to compute the nodes 'dx' value
# which is how much to adjust the label by when
# positioning it
# - remove the extra span
label.append("span")
.text((d) -> textValue(d))
.each((d) -> d.dx = Math.max(2.5 * rScale(rValue(d)), this.getBoundingClientRect().width))
.remove()
# reset the width of the label to the actual width
label
.style("width", (d) -> d.dx + "px")
# compute and store each nodes 'dy' value - the
# amount to shift the label down
# 'this' inside of D3's each refers to the actual DOM element
# connected to the data node
label.each((d) -> d.dy = this.getBoundingClientRect().height)
# ---
# custom gravity to skew the bubble placement
# ---
gravity = (alpha) ->
# start with the center of the display
cx = width / 2
cy = height / 2
# use alpha to affect how much to push
# towards the horizontal or vertical
ax = alpha / 4
ay = alpha / 4
# return a function that will modify the
# node's x and y values
(d) ->
d.x += (cx - d.x) * ax
d.y += (cy - d.y) * ay
# ---
# custom collision function to prevent
# nodes from touching
# This version is brute force
# we could use quadtree to speed up implementation
# (which is what Mike's original version does)
# ---
collide = (jitter) ->
# return a function that modifies
# the x and y of a node
(d) ->
data.forEach (d2) ->
# check that we aren't comparing a node
# with itself
if d != d2
# use distance formula to find distance
# between two nodes
x = d.x - d2.x
y = d.y - d2.y
distance = Math.sqrt(x * x + y * y)
# find current minimum space between two nodes
# using the forceR that was set to match the
# visible radius of the nodes
minDistance = d.forceR + d2.forceR + collisionPadding
# if the current distance is less then the minimum
# allowed then we need to push both nodes away from one another
if distance < minDistance
# scale the distance based on the jitter variable
distance = (distance - minDistance) / distance * jitter
# move our two nodes
moveX = x * distance
moveY = y * distance
d.x -= moveX
d.y -= moveY
d2.x += moveX
d2.y += moveY
# ---
# adds mouse events to element
# ---
connectEvents = (d) ->
d.on("click", click)
d.on("mouseover", mouseover)
d.on("mouseout", mouseout)
# ---
# clears currently selected bubble
# ---
clear = () ->
location.replace("#")
# ---
# changes clicked bubble by modifying url
# ---
click = (d) ->
updateActive(idValue(d))
d3.event.preventDefault()
# ---
# activates new node
# ---
updateActive = (id) ->
node.classed("bubble-selected", (d) -> id == idValue(d))
# if no node is selected, id will be empty
prop = '';
article = ''
if id.length > 0
size = parseFloat($('[href="#'+id.replace(" ","%20")+'"]').children().attr('data-value')).toLocaleString("it-IT");
if id == 'Malta' or id == 'Cipro'
article = ''
if id == 'Lussemburgo' or id == 'Regno Unito' or id == 'Portogallo' or id == 'Belgio'
article = ''
if id == 'Paesi Bassi'
article = ''
prop = ''
if id == 'Estonia' or id == 'Austria' or id == 'Irlanda' or id == 'Italia' or id == 'Ungheria'
article = ""
d3.select("#status").html("#{article}#{id} has#{prop} a surface area of #{size} km2")
else
d3.select("#status").html("Please, select a country to know its surface area")
# ---
# hover event
# ---
mouseover = (d) ->
node.classed("bubble-hover", (p) -> p == d)
# ---
# remove hover class
# ---
mouseout = (d) ->
node.classed("bubble-hover", false)
# ---
# public getter/setter for jitter variable
# ---
chart.jitter = (_) ->
if !arguments.length
return jitter
jitter = _
force.start()
chart
# ---
# public getter/setter for height variable
# ---
chart.height = (_) ->
if !arguments.length
return height
height = _
chart
# ---
# public getter/setter for width variable
# ---
chart.width = (_) ->
if !arguments.length
return width
width = _
chart
# ---
# public getter/setter for radius function
# ---
chart.r = (_) ->
if !arguments.length
return rValue
rValue = _
chart
# final act of our main function is to
# return the chart function we have created
return chart
root.Bubbles_pop = () ->
# standard variables accessible to
# the rest of the functions inside Bubbles
width = getViewport().width
height = 400
data = []
node = null
label = null
margin = {top: 5, right: 0, bottom: 0, left: 0}
# largest size for our bubbles
maxRadius = 55
# this scale will be used to size our bubbles
rScale = d3.scale.sqrt().range([0,maxRadius])
# I've abstracted the data value used to size each
# into its own function. This should make it easy
# to switch out the underlying dataset
rValue = (d) -> parseFloat(d.count)
# function to define the 'id' of a data element
# - used to bind the data uniquely to the force nodes
# and for url creation
# - should make it easier to switch out dataset
# for your own
idValue = (d) -> d.name
# function to define what to display in each bubble
# again, abstracted to ease migration to
# a different dataset if desired
textValue = (d) -> d.name
# constants to control how
# collision look and act
collisionPadding = 4
minCollisionRadius = 12
# variables that can be changed
# to tweak how the force layout
# acts
# - jitter controls the 'jumpiness'
# of the collisions
jitter = 1.2
# ---
# tweaks our dataset to get it into the
# format we want
# - for this dataset, we just need to
# ensure the count is a number
# - for your own dataset, you might want
# to tweak a bit more
# ---
transformData = (rawData) ->
rawData.forEach (d) ->
d.count = parseFloat(d.count)
rawData.sort(() -> 0.5 - Math.random())
rawData
# ---
# tick callback function will be executed for every
# iteration of the force simulation
# - moves force nodes towards their destinations
# - deals with collisions of force nodes
# - updates visual bubbles to reflect new force node locations
# ---
tick = (e) ->
dampenedAlpha = e.alpha * 0.1
# Most of the work is done by the gravity and collide
# functions.
node
.each(gravity(dampenedAlpha))
.each(collide(jitter))
.attr("transform", (d) -> "translate(#{d.x},#{d.y})")
# As the labels are created in raw html and not svg, we need
# to ensure we specify the 'px' for moving based on pixels
label
.style("left", (d) -> ((margin.left + d.x) - d.dx / 2) + "px")
.style("top", (d) -> ((margin.top + d.y) - d.dy / 2) + "px")
# The force variable is the force layout controlling the bubbles
# here we disable gravity and charge as we implement custom versions
# of gravity and collisions for this visualization
force = d3.layout.force()
.gravity(0)
.charge(0)
.size([width, height])
.on("tick", tick)
root.force_pop = force;
# ---
# Creates new chart function. This is the 'constructor' of our
# visualization
# Check out http://bost.ocks.org/mike/chart/
# for a explanation and rational behind this function design
# ---
chart = (selection) ->
selection.each (rawData) ->
# first, get the data in the right format
data = transformData(rawData)
# setup the radius scale's domain now that
# we have some data
maxDomainValue = d3.max(data, (d) -> rValue(d))
rScale.domain([0, maxDomainValue])
# a fancy way to setup svg element
svg = d3.select(this).selectAll("svg").data([data])
svgEnter = svg.enter().append("svg")
svg.attr("width", width + margin.left + margin.right )
svg.attr("height", height + margin.top + margin.bottom )
# node will be used to group the bubbles
node = svgEnter.append("g").attr("id", "bubble-nodes")
.attr("transform", "translate(#{margin.left},#{margin.top})")
# clickable background rect to clear the current selection
node.append("rect")
.attr("id", "bubble-background")
.attr("width", width)
.attr("height", height)
.on("click", clear)
# label is the container div for all the labels that sit on top of
# the bubbles
# - remember that we are keeping the labels in plain html and
# the bubbles in svg
label = d3.select(this).selectAll("#bubble-labels").data([data])
.enter()
.append("div")
.attr("id", "bubble-labels")
update()
# ---
# update starts up the force directed layout and then
# updates the nodes and labels
# ---
update = () ->
# add a radius to our data nodes that will serve to determine
# when a collision has occurred. This uses the same scale as
# the one used to size our bubbles, but it kicks up the minimum
# size to make it so smaller bubbles have a slightly larger
# collision 'sphere'
data.forEach (d,i) ->
d.forceR = Math.max(minCollisionRadius, rScale(rValue(d)))
# start up the force layout
force.nodes(data).start()
# call our update methods to do the creation and layout work
updateNodes()
updateLabels()
# ---
# updateNodes creates a new bubble for each node in our dataset
# ---
updateNodes = () ->
# here we are using the idValue function to uniquely bind our
# data to the (currently) empty 'bubble-node selection'.
# if you want to use your own data, you just need to modify what
# idValue returns
node = node.selectAll(".bubble-node").data(data, (d) -> idValue(d))
# we don't actually remove any nodes from our data in this example
# but if we did, this line of code would remove them from the
# visualization as well
node.exit().remove()
# nodes are just links with circles inside.
# the styling comes from the css
node.enter()
.append("a")
.attr("class", (d) -> "bubble-node " + toggleColor(d))
.attr("xlink:href", (d) -> "##{encodeURIComponent(idValue(d))}-popolazione")
.call(force.drag)
.call(connectEvents)
.append("circle")
.attr("r", (d) -> rScale(rValue(d)))
# ---
# updateLabels is more involved as we need to deal with getting the sizing
# to work well with the font size
# ---
updateLabels = () ->
# as in updateNodes, we use idValue to define what the unique id for each data
# point is
label = label.selectAll(".bubble-label").data(data, (d) -> idValue(d))
label.exit().remove()
# labels are anchors with div's inside them
# labelEnter holds our enter selection so it
# is easier to append multiple elements to this selection
labelEnter = label.enter().append("a")
.attr("class", "bubble-label")
.attr("href", (d) -> "##{encodeURIComponent(idValue(d))}-popolazione")
.call(force.drag)
.call(connectEvents)
labelEnter.append("div")
.attr("class", "bubble-label-name")
.attr("data-value", (d) -> rValue(d))
.text((d) -> textValue(d))
#labelEnter.append("div")
# .attr("class", "bubble-label-value")
# .text((d) -> rValue(d))
# label font size is determined based on the size of the bubble
# this sizing allows for a bit of overhang outside of the bubble
# - remember to add the 'px' at the end as we are dealing with
# styling divs
label
.style("font-size", (d) -> Math.min(Math.max(8, rScale(rValue(d) / 2)),40) + "px")
.style("width", (d) -> 2.5 * rScale(rValue(d)) + "px")
# interesting hack to get the 'true' text width
# - create a span inside the label
# - add the text to this span
# - use the span to compute the nodes 'dx' value
# which is how much to adjust the label by when
# positioning it
# - remove the extra span
label.append("span")
.text((d) -> textValue(d))
.each((d) -> d.dx = Math.max(2.5 * rScale(rValue(d)), this.getBoundingClientRect().width))
.remove()
# reset the width of the label to the actual width
label
.style("width", (d) -> d.dx + "px")
# compute and store each nodes 'dy' value - the
# amount to shift the label down
# 'this' inside of D3's each refers to the actual DOM element
# connected to the data node
label.each((d) -> d.dy = this.getBoundingClientRect().height)
# ---
# custom gravity to skew the bubble placement
# ---
gravity = (alpha) ->
# start with the center of the display
cx = width / 2
cy = height / 2
# use alpha to affect how much to push
# towards the horizontal or vertical
ax = alpha / 4
ay = alpha / 4
# return a function that will modify the
# node's x and y values
(d) ->
d.x += (cx - d.x) * ax
d.y += (cy - d.y) * ay
# ---
# custom collision function to prevent
# nodes from touching
# This version is brute force
# we could use quadtree to speed up implementation
# (which is what Mike's original version does)
# ---
collide = (jitter) ->
# return a function that modifies
# the x and y of a node
(d) ->
data.forEach (d2) ->
# check that we aren't comparing a node
# with itself
if d != d2
# use distance formula to find distance
# between two nodes
x = d.x - d2.x
y = d.y - d2.y
distance = Math.sqrt(x * x + y * y)
# find current minimum space between two nodes
# using the forceR that was set to match the
# visible radius of the nodes
minDistance = d.forceR + d2.forceR + collisionPadding
# if the current distance is less then the minimum
# allowed then we need to push both nodes away from one another
if distance < minDistance
# scale the distance based on the jitter variable
distance = (distance - minDistance) / distance * jitter
# move our two nodes
moveX = x * distance
moveY = y * distance
d.x -= moveX
d.y -= moveY
d2.x += moveX
d2.y += moveY
# ---
# adds mouse events to element
# ---
connectEvents = (d) ->
d.on("click", click)
d.on("mouseover", mouseover)
d.on("mouseout", mouseout)
# ---
# clears currently selected bubble
# ---
clear = () ->
location.replace("#")
# ---
# changes clicked bubble by modifying url
# ---
click = (d) ->
updateActive(idValue(d))
d3.event.preventDefault()
# ---
# activates new node
# ---
updateActive = (id) ->
node.classed("bubble-selected", (d) -> id == idValue(d))
# if no node is selected, id will be empty
prop = '';
article = ''
if id.length > 0
size = parseFloat($('[href="#'+id.replace(" ","%20")+'-popolazione"]').children().attr('data-value')).toLocaleString("it-IT");
if id == 'Malta' or id == 'Cipro'
article = ''
if id == 'Lussemburgo' or id == 'Regno Unito' or id == 'Portogallo' or id == 'Belgio'
article = ''
if id == 'Paesi Bassi'
article = ''
prop = 'hanno'
if id == 'Estonia' or id == 'Austria' or id == 'Irlanda' or id == 'Italia' or id == 'Ungheria'
article = ""
d3.select("#status").html("#{article}#{id}#{prop} has a popluation size of #{size} INHABITANTS")
else
d3.select("#status").html("Please, select a country to know its population size")
# ---
# hover event
# ---
mouseover = (d) ->
node.classed("bubble-hover", (p) -> p == d)
# ---
# remove hover class
# ---
mouseout = (d) ->
node.classed("bubble-hover", false)
# ---
# public getter/setter for jitter variable
# ---
chart.jitter = (_) ->
if !arguments.length
return jitter
jitter = _
force.start()
chart
# ---
# public getter/setter for height variable
# ---
chart.height = (_) ->
if !arguments.length
return height
height = _
chart
# ---
# public getter/setter for width variable
# ---
chart.width = (_) ->
if !arguments.length
return width
width = _
chart
# ---
# public getter/setter for radius function
# ---
chart.r = (_) ->
if !arguments.length
return rValue
rValue = _
chart
# final act of our main function is to
# return the chart function we have created
return chart
root.Bubbles_dens = () ->
# standard variables accessible to
# the rest of the functions inside Bubbles
width = getViewport().width
height = 400
data = []
node = null
label = null
margin = {top: 5, right: 0, bottom: 0, left: 0}
# largest size for our bubbles
maxRadius = 70
# this scale will be used to size our bubbles
rScale = d3.scale.sqrt().range([0,maxRadius])
# I've abstracted the data value used to size each
# into its own function. This should make it easy
# to switch out the underlying dataset
rValue = (d) -> parseFloat(d.count)
# function to define the 'id' of a data element
# - used to bind the data uniquely to the force nodes
# and for url creation
# - should make it easier to switch out dataset
# for your own
idValue = (d) -> d.name
# function to define what to display in each bubble
# again, abstracted to ease migration to
# a different dataset if desired
textValue = (d) -> d.name
# constants to control how
# collision look and act
collisionPadding = 4
minCollisionRadius = 12
# variables that can be changed
# to tweak how the force layout
# acts
# - jitter controls the 'jumpiness'
# of the collisions
jitter = 1.2
# ---
# tweaks our dataset to get it into the
# format we want
# - for this dataset, we just need to
# ensure the count is a number
# - for your own dataset, you might want
# to tweak a bit more
# ---
transformData = (rawData) ->
rawData.forEach (d) ->
d.count = parseFloat(d.count)
rawData.sort(() -> 0.5 - Math.random())
rawData
# ---
# tick callback function will be executed for every
# iteration of the force simulation
# - moves force nodes towards their destinations
# - deals with collisions of force nodes
# - updates visual bubbles to reflect new force node locations
# ---
tick = (e) ->
dampenedAlpha = e.alpha * 0.1
# Most of the work is done by the gravity and collide
# functions.
node
.each(gravity(dampenedAlpha))
.each(collide(jitter))
.attr("transform", (d) -> "translate(#{d.x},#{d.y})")
# As the labels are created in raw html and not svg, we need
# to ensure we specify the 'px' for moving based on pixels
label
.style("left", (d) -> ((margin.left + d.x) - d.dx / 2) + "px")
.style("top", (d) -> ((margin.top + d.y) - d.dy / 2) + "px")
# The force variable is the force layout controlling the bubbles
# here we disable gravity and charge as we implement custom versions
# of gravity and collisions for this visualization
force = d3.layout.force()
.gravity(0)
.charge(0)
.size([width, height])
.on("tick", tick)
root.force_dens = force;
# ---
# Creates new chart function. This is the 'constructor' of our
# visualization
# Check out http://bost.ocks.org/mike/chart/
# for a explanation and rational behind this function design
# ---
chart = (selection) ->
selection.each (rawData) ->
# first, get the data in the right format
data = transformData(rawData)
# setup the radius scale's domain now that
# we have some data
maxDomainValue = d3.max(data, (d) -> rValue(d))
rScale.domain([0, maxDomainValue])
# a fancy way to setup svg element
svg = d3.select(this).selectAll("svg").data([data])
svgEnter = svg.enter().append("svg")
svg.attr("width", width + margin.left + margin.right )
svg.attr("height", height + margin.top + margin.bottom )
# node will be used to group the bubbles
node = svgEnter.append("g").attr("id", "bubble-nodes")
.attr("transform", "translate(#{margin.left},#{margin.top})")
# clickable background rect to clear the current selection
node.append("rect")
.attr("id", "bubble-background")
.attr("width", width)
.attr("height", height)
.on("click", clear)
# label is the container div for all the labels that sit on top of
# the bubbles
# - remember that we are keeping the labels in plain html and
# the bubbles in svg
label = d3.select(this).selectAll("#bubble-labels").data([data])
.enter()
.append("div")
.attr("id", "bubble-labels")
update()
# ---
# update starts up the force directed layout and then
# updates the nodes and labels
# ---
update = () ->
# add a radius to our data nodes that will serve to determine
# when a collision has occurred. This uses the same scale as
# the one used to size our bubbles, but it kicks up the minimum
# size to make it so smaller bubbles have a slightly larger
# collision 'sphere'
data.forEach (d,i) ->
d.forceR = Math.max(minCollisionRadius, rScale(rValue(d)))
# start up the force layout
force.nodes(data).start()
# call our update methods to do the creation and layout work
updateNodes()
updateLabels()
# ---
# updateNodes creates a new bubble for each node in our dataset
# ---
updateNodes = () ->
# here we are using the idValue function to uniquely bind our
# data to the (currently) empty 'bubble-node selection'.
# if you want to use your own data, you just need to modify what
# idValue returns
node = node.selectAll(".bubble-node").data(data, (d) -> idValue(d))
# we don't actually remove any nodes from our data in this example
# but if we did, this line of code would remove them from the
# visualization as well
node.exit().remove()
# nodes are just links with circles inside.
# the styling comes from the css
node.enter()
.append("a")
.attr("class", (d) -> "bubble-node " + toggleColor(d))
.attr("xlink:href", (d) -> "##{encodeURIComponent(idValue(d))}-densita")
.call(force.drag)
.call(connectEvents)
.append("circle")
.attr("r", (d) -> rScale(rValue(d)))
# ---
# updateLabels is more involved as we need to deal with getting the sizing
# to work well with the font size
# ---
updateLabels = () ->
# as in updateNodes, we use idValue to define what the unique id for each data
# point is
label = label.selectAll(".bubble-label").data(data, (d) -> idValue(d))
label.exit().remove()
# labels are anchors with div's inside them
# labelEnter holds our enter selection so it
# is easier to append multiple elements to this selection
labelEnter = label.enter().append("a")
.attr("class", "bubble-label")
.attr("href", (d) -> "##{encodeURIComponent(idValue(d))}-densita")
.call(force.drag)
.call(connectEvents)
labelEnter.append("div")
.attr("class", "bubble-label-name")
.attr("data-value", (d) -> rValue(d))
.text((d) -> textValue(d))
#labelEnter.append("div")
# .attr("class", "bubble-label-value")
# .text((d) -> rValue(d))
# label font size is determined based on the size of the bubble
# this sizing allows for a bit of overhang outside of the bubble
# - remember to add the 'px' at the end as we are dealing with
# styling divs
label
.style("font-size", (d) -> Math.min(Math.max(8, rScale(rValue(d) / 2)),40) + "px")
.style("width", (d) -> 2.5 * rScale(rValue(d)) + "px")
# interesting hack to get the 'true' text width
# - create a span inside the label
# - add the text to this span
# - use the span to compute the nodes 'dx' value
# which is how much to adjust the label by when
# positioning it
# - remove the extra span
label.append("span")
.text((d) -> textValue(d))
.each((d) -> d.dx = Math.max(2.5 * rScale(rValue(d)), this.getBoundingClientRect().width))
.remove()
# reset the width of the label to the actual width
label
.style("width", (d) -> d.dx + "px")
# compute and store each nodes 'dy' value - the
# amount to shift the label down
# 'this' inside of D3's each refers to the actual DOM element
# connected to the data node
label.each((d) -> d.dy = this.getBoundingClientRect().height)
# ---
# custom gravity to skew the bubble placement
# ---
gravity = (alpha) ->
# start with the center of the display
cx = width / 2
cy = height / 2
# use alpha to affect how much to push
# towards the horizontal or vertical
ax = alpha / 4
ay = alpha / 4
# return a function that will modify the
# node's x and y values
(d) ->
d.x += (cx - d.x) * ax
d.y += (cy - d.y) * ay
# ---
# custom collision function to prevent
# nodes from touching
# This version is brute force
# we could use quadtree to speed up implementation
# (which is what Mike's original version does)
# ---
collide = (jitter) ->
# return a function that modifies
# the x and y of a node
(d) ->
data.forEach (d2) ->
# check that we aren't comparing a node
# with itself
if d != d2
# use distance formula to find distance
# between two nodes
x = d.x - d2.x
y = d.y - d2.y
distance = Math.sqrt(x * x + y * y)
# find current minimum space between two nodes
# using the forceR that was set to match the
# visible radius of the nodes
minDistance = d.forceR + d2.forceR + collisionPadding
# if the current distance is less then the minimum
# allowed then we need to push both nodes away from one another
if distance < minDistance
# scale the distance based on the jitter variable
distance = (distance - minDistance) / distance * jitter
# move our two nodes
moveX = x * distance
moveY = y * distance
d.x -= moveX
d.y -= moveY
d2.x += moveX
d2.y += moveY
# ---
# adds mouse events to element
# ---
connectEvents = (d) ->
d.on("click", click)
d.on("mouseover", mouseover)
d.on("mouseout", mouseout)
# ---
# clears currently selected bubble
# ---
clear = () ->
location.replace("#")
# ---
# changes clicked bubble by modifying url
# ---
click = (d) ->
updateActive(idValue(d))
d3.event.preventDefault()
# ---
# activates new node
# ---
updateActive = (id) ->
node.classed("bubble-selected", (d) -> id == idValue(d))
# if no node is selected, id will be empty
prop = '';
article = ''
if id.length > 0
size = parseFloat($('[href="#'+id.replace(" ","%20")+'-densita"]').children().attr('data-value')).toLocaleString("it-IT");
if id == 'Malta' or id == 'Cipro'
article = ''
if id == 'Ungheria'
article = ""
if id == 'Lussemburgo' or id == 'Regno Unito' or id == 'Portogallo' or id == 'Belgio'
article = ''
if id == 'Paesi Bassi'
article = ''
prop = ''
if id == 'Estonia' or id == 'Austria' or id == 'Irlanda' or id == 'Italia'
article = ""
d3.select("#status").html("#{article}#{id}#{prop} has a density of #{size} people per km2")
else
d3.select("#status").html("Please, select a country to know its population density")
# ---
# hover event
# ---
mouseover = (d) ->
node.classed("bubble-hover", (p) -> p == d)
# ---
# remove hover class
# ---
mouseout = (d) ->
node.classed("bubble-hover", false)
# ---
# public getter/setter for jitter variable
# ---
chart.jitter = (_) ->
if !arguments.length
return jitter
jitter = _
force.start()
chart
# ---
# public getter/setter for height variable
# ---
chart.height = (_) ->
if !arguments.length
return height
height = _
chart
# ---
# public getter/setter for width variable
# ---
chart.width = (_) ->
if !arguments.length
return width
width = _
chart
# ---
# public getter/setter for radius function
# ---
chart.r = (_) ->
if !arguments.length
return rValue
rValue = _
chart
# final act of our main function is to
# return the chart function we have created
return chart
# ---
# Helper function that simplifies the calling
# of our chart with it's data and div selector
# specified
# ---
root.plotData = (selector, data, plot) ->
d3.select(selector)
.datum(data)
.call(plot)
texts = [
{key:"superfici",file:"superfici.csv",name:"Superfici"},
{key:"abitanti",file:"popolazione.csv",name:"Abitanti"}
{key:"densita",file:"densita.csv",name:"Densità "}
]
# ---
# jQuery document ready.
# ---
$ ->
# create a new Bubbles chart
plot = root.Bubbles()
plot_pop = root.Bubbles_pop()
plot_dens = root.Bubbles_dens()
# ---
# function that is called when
# data is loaded
# ---
display = (data) ->
plotData("#bubbleContainer", data, plot)
console.log(data)
display_pop = (data) ->
plotData("#bubbleContainer_pop", data, plot_pop)
display_dens = (data) ->
plotData("#bubbleContainer_dens", data, plot_dens)
#kept for historical reason, just one dataset is available
# default to the first text if something gets messed up
if !text
text = texts[1]
# load our data
d3.csv("lib/data/#{texts[0].file}", display)
d3.csv("lib/data/#{texts[1].file}", display_pop)
d3.csv("lib/data/#{texts[2].file}", display_dens)