class BubbleChart
constructor: (data) ->
@data = data
@width = 960
@height = @width/2
@tooltip = CustomTooltip("tooltip", 200)
# locations the nodes will move towards
# depending on which view is currently being
# used
@center = {x: @width / 2, y: @height / 2}
@year_centers = {
"Digitalizzazione, innovazione, competitivita' e cultura": {x: @width / 5+10, y: @height / 2},
"Rivoluzione verde e transizione ecologica": {x: @width / 4 + 90, y: @height / 2},
"Infrastrutture per una mobilita' sostenibile": {x: @width / 2-50, y: @height / 2},
"Istruzione e ricerca": {x: @width/2+50, y: @height / 2},
"Coesione e inclusione": {x: @width - 280, y: @height / 2},
"Salute": {x: @width - 200, y: @height / 2},
}
# used when setting up force and
# moving around nodes
@layout_gravity = -0.01
@damper = 0.1
# these will be set in create_nodes and create_vis
@vis = null
@nodes = []
@force = null
@circles = null
# nice looking colors - no reason to buck the trend
@fill_color = d3.scale.ordinal()
.domain(["0","1","3","4","5","6"])
.range(["#7aa25c", "#FFa500", "#7aa2FF","#f80000","#f002a0","#619fdc"])
# use the max total_amount in the data as the max in the scale's domain
max_amount = d3.max(@data, (d) -> parseInt(d.total_amount))
@radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
this.create_nodes()
this.create_vis()
# create node objects from original data
# that will serve as the data behind each
# bubble in the vis, then add each node
# to @nodes to be used later
create_nodes: () =>
@data.forEach (d) =>
node = {
id: d.id
radius: @radius_scale(parseInt(d.total_amount)*0.6)
value: d.total_amount
name: d.grant_title
org: d.organization
group: d.group
year: d.start_year
x: Math.random() * 900
y: Math.random() * 800
}
@nodes.push node
@nodes.sort (a,b) -> b.value - a.value
# create svg at #vis and then
# create circle representation for each node
create_vis: () =>
@vis = d3.select("#vis")
.append("div")
.classed("svg-container", true)
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 960 540")
.classed("svg-content-responsive", true)
.attr("id", "svg_vis")
@circles = @vis.selectAll("circle")
.data(@nodes, (d) -> d.id)
# used because we need 'this' in the
# mouse callbacks
that = this
# radius will be set to 0 initially.
# see transition below
@circles.enter().append("circle")
.attr("r", 0)
.attr("fill", (d) => @fill_color(d.group))
.attr("stroke-width", 2)
.attr("stroke", (d) => d3.rgb(@fill_color(d.group)).darker())
.attr("id", (d) -> "bubble_#{d.id}")
.on("mouseover", (d,i) -> that.show_details(d,i,this))
.on("mouseout", (d,i) -> that.hide_details(d,i,this))
# Fancy transition to make bubbles appear, ending with the
# correct radius
@circles.transition().duration(2000).attr("r", (d) -> d.radius)
# Charge function that is called for each node.
# Charge is proportional to the diameter of the
# circle (which is stored in the radius attribute
# of the circle's associated data.
# This is done to allow for accurate collision
# detection with nodes of different sizes.
# Charge is negative because we want nodes to
# repel.
# Dividing by 8 scales down the charge to be
# appropriate for the visualization dimensions.
charge: (d) ->
-Math.pow(d.radius, 2.0) / 13
# Starts up the force layout with
# the default values
start: () =>
@force = d3.layout.force()
.nodes(@nodes)
.size([@width, @height])
# Sets up force layout to display
# all nodes in one circle.
display_group_all: () =>
@force.gravity(@layout_gravity)
.charge(this.charge)
.friction(0.9)
.on "tick", (e) =>
@circles.each(this.move_towards_center(e.alpha))
.attr("cx", (d) -> d.x)
.attr("cy", (d) -> d.y)
@force.start()
this.hide_years()
# Moves all circles towards the @center
# of the visualization
move_towards_center: (alpha) =>
(d) =>
d.x = d.x + (@center.x - d.x) * (@damper + 0.02) * alpha
d.y = d.y + (@center.y - d.y) * (@damper + 0.02) * alpha
# sets the display of bubbles to be separated
# into each year. Does this by calling move_towards_year
display_by_year: () =>
@force.gravity(@layout_gravity)
.charge(this.charge)
.friction(0.9)
.on "tick", (e) =>
@circles.each(this.move_towards_year(e.alpha))
.attr("cx", (d) -> d.x)
.attr("cy", (d) -> d.y)
@force.start()
this.display_years()
# move all circles to their associated @year_centers
move_towards_year: (alpha) =>
(d) =>
target = @year_centers[d.year]
d.x = d.x + (target.x - d.x) * (@damper + 0.02) * alpha * 1.1
d.y = d.y + (target.y - d.y) * (@damper + 0.02) * alpha * 1.1
# Method to display year titles
display_years: () =>
years_x = {"Digitalizzazione": @width / 5-90, "Rivoluzione verde": @width / 4 +40, "Infrastrutture per mobilita'": @width / 2-30, "Istruzione e ricerca": @width/2+110,"Coesione e inclusione": @width - 220,"Salute": @width - 100}
years_data = d3.keys(years_x)
years_group = {"Digitalizzazione": "#FFa500","Rivoluzione verde":"#7aa25c","Infrastrutture per mobilita'":"#619fdc","Istruzione e ricerca":"#f80000","Coesione e inclusione":"#ed72e7","Salute":"#0000FF"}
years = @vis.selectAll(".years")
.data(years_data)
years.enter().append("text")
.attr("class", "year")
.attr("x", (d) => years_x[d] )
.attr("y", 40)
.attr("text-anchor", "middle")
.attr("fill",(d) => years_group[d])
.text((d) -> d)
# Method to hide year titiles
hide_years: () =>
years = @vis.selectAll(".year").remove()
show_details: (data, i, element) =>
d3.select(element).attr("stroke", "#f80000")
content = "COMPONENTE: #{data.name}
"
content +="IMPORTO #{data.value}
"
content +="MISSIONE #{data.year}"
@tooltip.showTooltip(content,d3.event)
hide_details: (data, i, element) =>
d3.select(element).attr("stroke", (d) => d3.rgb(@fill_color(d.group)).darker())
@tooltip.hideTooltip()
root = exports ? this
$ ->
chart = null
render_vis = (csv) ->
chart = new BubbleChart csv
chart.start()
root.display_all()
root.display_all = () =>
chart.display_group_all()
root.display_year = () =>
chart.display_by_year()
root.toggle_view = (view_type) =>
if view_type == 'year'
root.display_year()
else
root.display_all()
d3.csv "https://docs.google.com/spreadsheets/d/e/2PACX-1vSjMFew4pG83xZSPKbktSZj47GaBLY1PAFqNvT3RjBod82OWH2bgeJtSN4Rutf_c5SoUpYtsIePw3al/pub?gid=44335948&single=true&output=csv", render_vis