Fish Eye Chart main source: ··· Home

Notes:

  • Uses the Fish Eye plugin
  • Changed style
  • Click to stop and show a pointer
  • Extended the title information

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

Home