• main.coffee

  • ¶
    'use strict'
  • ¶

    그리기 영역 정보

    margin = new Margin(50)
    [width, height] = [1500, 600]
    
    color = d3.scale.category20()
    bounce = "이탈"
    nodeId = 0
  • ¶

  • ¶

    drawFlow

    플로우를 그린다

    • @param target selection 시각화할 selection
    • @param tree 중첩오브젝트 데이터배열
    drawFlow = (target, tree)->
  • ¶

    중첩구조에서 노드 배열과 링크 배열을 취득하기 위해 이용

      cluster = d3.layout.cluster().children((d)-> d?.values or [])
        .value((d)-> d.values.cnt)
      nodes = cluster.nodes(tree)
    
  • ¶

    노드는 유니크한 ID를 붙여둔다(삭제・갱신을 위해)

      for node, idx in nodes
        node.idx = node.idx or nodeId++
      links = cluster.links(nodes)
  • ¶

    링크의 값은 목표 노드의 값

      links.forEach (link)->
        link.value = link.target.value
  • ¶

    sankey 레이아웃을 생성レイアウトを生

      sankey = d3.sankey().size([width, height])
        .nodeWidth(120)
        .nodePadding(0)
        .nodes(nodes)
        .links(links)
        .layout(32)
  • ¶

    경로 데이터 생성용 함수

      path = sankey.link()
  • ¶

    링크 그리기

    1. 기존 링크 갱신
      linkSelection = target.selectAll('.link').data(links, (d)-> "#{d.source.idx}-#{d.target.idx}")
      linkSelection.transition().attr(
        opacity: (d)-> if d.target.key is bounce then 0 else .5
      )
      linkSelection.select('path').transition().attr(
        'stroke-width': (d)-> d.dy
        d: (d)->if d.key is bounce then "" else path(d)
      )
    
  • ¶
    1. 삭제 링크(부모)의 제거
      linkExit = linkSelection.exit().transition().attr(opacity: 1e-16)
  • ¶
    1. 신규 링크 추가
      linkEnter = linkSelection.enter().append('g').attr(class: 'link')
      linkEnter.append('path').attr(
        stroke: 'grey'
        fill: 'none'
        'stroke-width': (d)-> d.dy
        d: (d)-> if d.key is bounce then "" else path(d)
        opacity: (d)-> .5
      )
  • ¶

    노드 그리기

      nodeSelection = target.selectAll('.node').data(nodes, (d)-> d.idx)
    
  • ¶
    • 기존 노드 갱신
      nodeSelection.transition().attr(
        transform: (d)-> "translate(#{d.x},#{d.y})"
        opacity: (d)-> if d.key is bounce then 0 else .5
      )
      nodeSelection.select('rect').transition().attr(
        width: (d)->d.dx
        height: (d)-> Math.max 1, d.dy
        fill: (d)-> color(d.key)
      )
      nodeSelection.select('text').transition().style(
        opacity: (d)-> if d.dy < 12 or d.key is bounce then 0 else 1
      )
  • ¶
    • 삭제 노드(부모)제거
      nodeExit = nodeSelection.exit().transition().attr(
        opacity: 1e-16
      )
  • ¶
    • 신규 노드 추가
      nodeEnter = nodeSelection.enter().append('g').attr(
        class: 'node'
        transform: (d)-> "translate(#{d.x},#{d.y})"
        cursor: 'pointer'
        opacity: (d)-> if d.key is bounce then 0 else .5
      )
      nodeEnter.append('rect').attr(
        width: (d)->d.dx
        height: (d)-> Math.max 1, d.dy
        fill: (d)-> color(d.key)
      )
      nodeEnter.append('text').text((d)-> "#{d.key} (#{d.value})").attr(dy: 12).style(
        opacity: (d)-> if d.dy < 12 or d.key is bounce then 0 else 1
      )
  • ¶

    노드 클릭 시에 깊이 탐색을 한다

      nodeEnter.on('click', (d)->
        depth = Math.min d.depth, 5
        if d.parent
    
  • ¶

    openLeaves(d, depth)

          drawFlow(target, d.parent)
      )
  • ¶

  • ¶

    openLeaves

    잎을 전개한다

    • @param tree 오브젝트(중첩구조)
    • @param depth 깊이
    openLeaves = (tree, depth)->
      if tree.children and tree.children instanceof Array
        for child in tree.children
          openLeaves child, depth
      else
        nest = d3.nest()
        [0...depth].forEach (i)->
          nest.key((d)-> d.values[i]?.path or bounce)
        data = nest.rollup((values)->
          cnt: values.length
          children: values
        ).entries(tree.values.children)
    
        tree._values = tree.values
        tree.values = data
    
    
    
  • ¶

  • ¶

    플로우를 그린다

    • @param target 문자열 SVG 추가하는 요소
    • @param sessions 배열 세션 배열
    drawFlowChart = (target, sessions)->
      initDepth = 5
    
      svg = d3.select('body').append('svg').attr(
        width: width + margin.width
        height: height + margin.height
      )
      main = svg.append('g').attr(
        width: width
        height: height
        transform: "translate(#{margin.left},#{margin.top})"
      )
  • ¶

    세션으로부터 액세스한 경로에 기반해 중첩구조 생성(깊이지정)

      nest = d3.nest()
      [0...initDepth].forEach (i)->
        nest = nest.key((d)-> d[i]?.path or bounce)
      data = nest.rollup((values)->
        cnt: values.length
        children: values
      ).entries sessions
    
    
  • ¶

    플로우를 그린다

      drawFlow main, data[0]
  • ¶

  • ¶

    セッション

    class Session
      constructor: (@sid, @bounceRate = .2)->
        @accesses = []
        @begin = new Date()
    
      start: (path)->
        @accesses.push
          referer: ""
          path: path
          time: @begin
    
        @next(path)
    
      next: (referer)->
        lastAccess = @accesses[@accesses.length - 1]
    
        category = if Math.random() < .7 then "/cate#{parseInt(Math.random() * 3)}" else ""
        path = "#{category}/item#{0|Math.random() * 6}"
    
        @accesses.push
          referer: referer
          path: path
          time: lastAccess.time + parseInt(Math.random() * 1000 * 60 * 5)
  • ¶

    이탈률에 따른 세션 종료

        if Math.random() < @bounceRate
          return @accesses.map (hist)=>
            sid:     @sid
            time:    hist.time
            path:    hist.path
            referer: hist.referer
    
        @next(path)
  • ¶

  • ¶

    데모데이터 작성

    data = []
    d3.range(0, 1000).map (i)->
      session = new Session(i)
    
      data.push session.start("/")
  • ¶

    액세스 플로우 그리기

    drawFlowChart('body', data)