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)