Notes:
Sources:
Data: data.json
Code:
fish-eye.ejs
<% demo = {category: "d3js", key: "fish-eye", files: files}; %>
<script type="text/javascript">$(document).ready(function() {
window.Charts['<%= demo.category %>']['<%= demo.key %>'].ready();
})</script>
<%- partial("../mixins/_demo_title", demo) %>
<div id="chart" class="<%= demo.key %>-chart"></div>
<script type="text/javascript" src="/vendors/bower/d3/d3.min.js"></script>
<script type="text/javascript" src="/vendors/bower/d3-plugins/fisheye/fisheye.js "></script>
<script type="text/javascript" src="/js/d3js-utils.js"></script>
<script type="text/javascript" src="/js/d3js/<%= demo.key %>.js"></script>
<%- partial("../mixins/_notes", demo) %>
<%- partial("../mixins/_sources_data", demo) %>
<%- partial("../mixins/_code", demo) %>
fish-eye.coffee
window.Charts = window.Charts || {}
window.Charts.d3js = window.Charts.d3js || {}
window.Charts.d3js['fish-eye'] = window.Charts.d3js['fish-eye'] || {}
ch = window.Charts.d3js['fish-eye']
ch.ready = ->
ch.setCg()
ch.setDom()
ch.setVars()
ch.getData(ch.render)
ch.render = ->
ch.setChartTitle()
ch.setBackground()
ch.setPointer()
ch.setFilter()
ch.setAxis()
ch.setLabels()
ch.setDots()
ch.setTitles()
ch.bindMousemove()
ch.bindClick()
ch.setCg = ->
ch.cg =
margin: {top: 80, right: 50, bottom: 70, left: 70}
ch.cg.height = 700 - ch.cg.margin.top - ch.cg.margin.bottom
ch.cg.width = $('#chart').innerWidth() - ch.cg.margin.left - ch.cg.margin.right
ch.setDom = -> ch.dom =
svg: d3.select('#chart').append('svg')\
.attr('width', ch.cg.width + ch.cg.margin.left + ch.cg.margin.right)\
.attr('height', ch.cg.height + ch.cg.margin.top + ch.cg.margin.bottom)\
.append('g')\
.attr('transform', 'translate(' + ch.cg.margin.left + ',' + ch.cg.margin.top + ')')
ch.getData = (cb)->
d3.json('/data/d3js/fish-eye/data.json', (nations)->
ch.data = nations
cb()
)
ch.setChartTitle = ->
d3utils.middleTitle(ch.dom.svg, ch.cg.width, 'Income Per Capita vs ' + \
'Life Expectancy vs Population vs Region - 180 Countries', -40)
ch.setVars = -> ch.vars =
xScale: d3.fisheye.scale(d3.scale.log).domain([300, 1e5]).range([0, ch.cg.width])
yScale: d3.fisheye.scale(d3.scale.linear).domain([20, 90]).range([ch.cg.height, 0])
radiusScale: d3.scale.sqrt().domain([0, 5e8]).range([5, 60])
colorScale: d3.scale.category10().domain(['Sub-Saharan Africa', 'South Asia', \
'Middle East & North Africa', 'America', 'Europe & Central Asia', 'East Asia & Pacific'])
focused: false
ch.setAxis = ->
ch.dom.xAxis = d3.svg.axis().orient('bottom').scale(ch.vars.xScale)
.tickFormat(d3.format(',d')).tickSize(-ch.cg.height)
ch.dom.yAxis = d3.svg.axis().scale(ch.vars.yScale).orient('left').tickSize(-ch.cg.width)
ch.dom.svg.append('g').attr('class', 'x axis')
.attr('transform', 'translate(0,' + ch.cg.height + ')').call(ch.dom.xAxis)
ch.dom.svg.append('g').attr('class', 'y axis').call(ch.dom.yAxis)
ch.setBackground = ->
ch.dom.svg.append('rect').attr('class', 'background').attr('width', ch.cg.width)
.attr('height', ch.cg.height)
ch.setLabels = ->
ch.dom.svg.append('text').attr('class', 'x label').attr('text-anchor', 'end')
.attr('x', ch.cg.width - 26).attr('y', ch.cg.height + 26)
.text('income per capita, inflation-adjusted (dollars)')
ch.dom.svg.append('text').attr('class', 'y label').attr('text-anchor', 'end')
.attr('x', -26).attr('y', -40).attr('dy', '.75em').attr('transform', 'rotate(-90)')
.text('life expectancy (years)');
ch.setFilter = ->
d3utils.filterColor('circles', ch.dom.svg, 1.5, .6, true)
ch.position = (dot)->
dot.attr('cx', (d)-> ch.vars.xScale(d.income))
.attr('cy', (d)-> ch.vars.yScale(d.lifeExpectancy))
.attr('r', (d)-> ch.vars.radiusScale(d.population))
ch.setDots = ->
ch.dom.dot = ch.dom.svg.append('g').attr('class', 'dots').selectAll('.dot')
.data(ch.data).enter().append('circle').attr('class', 'dot')
.style({
fill: (d)-> ch.vars.colorScale(d.region)
filter: 'url(#drop-shadow-circles)'
stroke: 'black', 'stroke-width': '1px'
}).call(ch.position).sort((a, b)-> b.population - a.population)
ch.setTitles = ->
ch.dom.dot.append('title').text((d)->
"#{d.name}:\n- Income: #{ch.humanizeNumber(d.income)} $/P.C.\n" + \
"- Population: #{ch.humanizeNumber(d.population)}\n" + \
"- Life expectancy: #{d.lifeExpectancy} years"
)
ch.zoom = ->
mouse = d3.mouse(this)
ch.vars.xScale.distortion(2.5).focus(mouse[0])
ch.vars.yScale.distortion(2.5).focus(mouse[1])
ch.dom.dot.call(ch.position)
ch.dom.svg.select('.x.axis').call(ch.dom.xAxis)
ch.dom.svg.select('.y.axis').call(ch.dom.yAxis)
ch.setPointer = ->
ch.dom.pointer = ch.dom.svg.append('text').text('+').attr('class', 'pointer')
ch.bindMousemove = ->
ch.dom.svg.on('mousemove', ->
if not ch.vars.focused
ch.zoom.call(this)
)
ch.bindClick = ->
ch.dom.svg.on('click', ->
ch.vars.focused = !ch.vars.focused
if ch.vars.focused
mouse = d3.mouse(this)
ch.dom.pointer.attr({x: mouse[0], y: mouse[1]}).style({opacity: 1})
else
ch.dom.pointer.style({opacity: 0})
ch.zoom.call(this)
)
ch.humanizeNumber = (n) -> # http://stackoverflow.com/a/25194011/3244654
n = n.toString()
while true
n2 = n.replace /(\d)(\d{3})($|,|\.)/g, '$1,$2$3'
if n == n2 then break else n = n2
n
_fish-eye.styl
.fish-eye-chart
text
font 10px sans-serif
text-shadow 1px 1px 1px #ccc
.axis path, .axis line
fill none
stroke #eee
shape-rendering crispEdges
.background
fill none
pointer-events all
.chart-title
font-size 14px
.pointer
fill #7AAE61
font-size 15px
opacity 0
d3js-utils.coffee
d3utils = {
middleTitle: ((svg, width, text, top = -15)->
element = svg.append('text').attr({class: 'chart-title', 'text-anchor': 'middle', \
transform: 'translate(' + String(width / 2) + ',' + top + ')'})
.text(text).style({'font-weight': 'bold'})
)
svg: ((selector, width, height, margin)->
d3.select(selector).text('').append('svg')
.attr({width: width + margin.left + margin.right, \
height: height + margin.top + margin.bottom})
.append('g').attr({transform: 'translate(' + margin.left + ',' + margin.top + ')'})
)
filterBlackOpacity: ((id, svg, deviation, slope)->
defs = svg.append('defs')
filter = defs.append('filter').attr({id: 'drop-shadow-' + id, width: '500%', height: '500%', \
x: '-200%', y: '-200%'})
filter.append('feGaussianBlur').attr({in: 'SourceAlpha', stdDeviation: deviation})
filter.append('feOffset').attr({dx: 1, dy: 1})
filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
feMerge = filter.append('feMerge')
feMerge.append('feMergeNode')
feMerge.append('feMergeNode').attr('in', 'SourceGraphic')
)
filterColor: ((id, svg, deviation, slope, extra = false)->
defs = svg.append('defs')
filter = defs.append('filter').attr({id:'drop-shadow-' + id})
filter.attr({width: '500%', height: '500%', x: '-200%', y: '-200%'}) if extra
filter.append('feOffset').attr({result: 'offOut', in: 'SourceGraphic', dx: .5, dy: .5})
filter.append('feGaussianBlur')
.attr({result: 'blurOut', in: 'offOut', stdDeviation: deviation})
filter.append('feBlend').attr({in: 'SourceGraphic', in2: 'blurOut', mode: 'normal'})
filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
)
tooltip: ((selector, customOpts = {})->
defaultOpts = {
followMouse: false, followElement: false, elementSelector: ''
leftOffst: 60, topOffst: 40
tOpts: {container: 'body', viewport: {selector: '#chart svg'}}
}
opts = _.merge(defaultOpts, customOpts)
# Bootstrap tooltip.
$(selector).tooltip(opts.tOpts)
# For SVG forms, it is better to position the tooltip manually.
if opts.followMouse
$(selector).hover((e)->
$('.tooltip')
.css({top: String(e.pageY - opts.topOffst) + 'px', \
left: String(e.pageX - opts.leftOffst) + 'px'})
)
else if opts.followElement
$(selector).hover((e)->
$('.tooltip')
.css({top: String($(opts.elementSelector).position().top - opts.topOffst) + 'px', \
left: String($(opts.elementSelector).position().left - opts.leftOffst) + 'px'})
)
)
colorsScale: ((colors, extent)->
c = d3.scale.linear().domain(extent).range([0,1])
colorScale = d3.scale.linear()
.domain(d3.range(0, 1, 1.0 / (colors.length))).range(colors)
return ((p)-> colorScale(c(p)))
)
}
window.d3utils = d3utils