morris.donut.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # Donut charts.
  2. #
  3. # @example
  4. # Morris.Donut({
  5. # el: $('#donut-container'),
  6. # data: [
  7. # { label: 'yin', value: 50 },
  8. # { label: 'yang', value: 50 }
  9. # ]
  10. # });
  11. class Morris.Donut extends Morris.EventEmitter
  12. defaults:
  13. colors: [
  14. '#0B62A4'
  15. '#3980B5'
  16. '#679DC6'
  17. '#95BBD7'
  18. '#B0CCE1'
  19. '#095791'
  20. '#095085'
  21. '#083E67'
  22. '#052C48'
  23. '#042135'
  24. ],
  25. backgroundColor: '#FFFFFF',
  26. labelColor: '#000000',
  27. formatter: Morris.commas
  28. resize: false
  29. # Create and render a donut chart.
  30. #
  31. constructor: (options) ->
  32. return new Morris.Donut(options) unless (@ instanceof Morris.Donut)
  33. @options = $.extend {}, @defaults, options
  34. if typeof options.element is 'string'
  35. @el = $ document.getElementById(options.element)
  36. else
  37. @el = $ options.element
  38. if @el == null || @el.length == 0
  39. throw new Error("Graph placeholder not found.")
  40. # bail if there's no data
  41. if options.data is undefined or options.data.length is 0
  42. return
  43. @raphael = new Raphael(@el[0])
  44. if @options.resize
  45. $(window).bind 'resize', (evt) =>
  46. if @timeoutId?
  47. window.clearTimeout @timeoutId
  48. @timeoutId = window.setTimeout @resizeHandler, 100
  49. @setData options.data
  50. # Clear and redraw the chart.
  51. redraw: ->
  52. @raphael.clear()
  53. cx = @el.width() / 2
  54. cy = @el.height() / 2
  55. w = (Math.min(cx, cy) - 10) / 3
  56. total = 0
  57. total += value for value in @values
  58. min = 5 / (2 * w)
  59. C = 1.9999 * Math.PI - min * @data.length
  60. last = 0
  61. idx = 0
  62. @segments = []
  63. for value, i in @values
  64. next = last + min + C * (value / total)
  65. seg = new Morris.DonutSegment(
  66. cx, cy, w*2, w, last, next,
  67. @data[i].color || @options.colors[idx % @options.colors.length],
  68. @options.backgroundColor, idx, @raphael)
  69. seg.render()
  70. @segments.push seg
  71. seg.on 'hover', @select
  72. seg.on 'click', @click
  73. last = next
  74. idx += 1
  75. @text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800)
  76. @text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14)
  77. max_value = Math.max @values...
  78. idx = 0
  79. for value in @values
  80. if value == max_value
  81. @select idx
  82. break
  83. idx += 1
  84. setData: (data) ->
  85. @data = data
  86. @values = (parseFloat(row.value) for row in @data)
  87. @redraw()
  88. # @private
  89. click: (idx) =>
  90. @fire 'click', idx, @data[idx]
  91. # Select the segment at the given index.
  92. select: (idx) =>
  93. s.deselect() for s in @segments
  94. segment = @segments[idx]
  95. segment.select()
  96. row = @data[idx]
  97. @setLabels(row.label, @options.formatter(row.value, row))
  98. # @private
  99. setLabels: (label1, label2) ->
  100. inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3
  101. maxWidth = 1.8 * inner
  102. maxHeightTop = inner / 2
  103. maxHeightBottom = inner / 3
  104. @text1.attr(text: label1, transform: '')
  105. text1bbox = @text1.getBBox()
  106. text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height)
  107. @text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}")
  108. @text2.attr(text: label2, transform: '')
  109. text2bbox = @text2.getBBox()
  110. text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height)
  111. @text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}")
  112. drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) ->
  113. text = @raphael.text(xPos, yPos, '')
  114. .attr('font-size', fontSize)
  115. .attr('fill', color)
  116. text.attr('font-weight', fontWeight) if fontWeight?
  117. return text
  118. resizeHandler: =>
  119. @timeoutId = null
  120. @raphael.setSize @el.width(), @el.height()
  121. @redraw()
  122. # A segment within a donut chart.
  123. #
  124. # @private
  125. class Morris.DonutSegment extends Morris.EventEmitter
  126. constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @index, @raphael) ->
  127. @sin_p0 = Math.sin(p0)
  128. @cos_p0 = Math.cos(p0)
  129. @sin_p1 = Math.sin(p1)
  130. @cos_p1 = Math.cos(p1)
  131. @is_long = if (p1 - p0) > Math.PI then 1 else 0
  132. @path = @calcSegment(@inner + 3, @inner + @outer - 5)
  133. @selectedPath = @calcSegment(@inner + 3, @inner + @outer)
  134. @hilight = @calcArc(@inner)
  135. calcArcPoints: (r) ->
  136. return [
  137. @cx + r * @sin_p0,
  138. @cy + r * @cos_p0,
  139. @cx + r * @sin_p1,
  140. @cy + r * @cos_p1]
  141. calcSegment: (r1, r2) ->
  142. [ix0, iy0, ix1, iy1] = @calcArcPoints(r1)
  143. [ox0, oy0, ox1, oy1] = @calcArcPoints(r2)
  144. return (
  145. "M#{ix0},#{iy0}" +
  146. "A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" +
  147. "L#{ox1},#{oy1}" +
  148. "A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" +
  149. "Z")
  150. calcArc: (r) ->
  151. [ix0, iy0, ix1, iy1] = @calcArcPoints(r)
  152. return (
  153. "M#{ix0},#{iy0}" +
  154. "A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}")
  155. render: ->
  156. @arc = @drawDonutArc(@hilight, @color)
  157. @seg = @drawDonutSegment(
  158. @path,
  159. @color,
  160. @backgroundColor,
  161. => @fire('hover', @index),
  162. => @fire('click', @index)
  163. )
  164. drawDonutArc: (path, color) ->
  165. @raphael.path(path)
  166. .attr(stroke: color, 'stroke-width': 2, opacity: 0)
  167. drawDonutSegment: (path, fillColor, strokeColor, hoverFunction, clickFunction) ->
  168. @raphael.path(path)
  169. .attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3)
  170. .hover(hoverFunction)
  171. .click(clickFunction)
  172. select: =>
  173. unless @selected
  174. @seg.animate(path: @selectedPath, 150, '<>')
  175. @arc.animate(opacity: 1, 150, '<>')
  176. @selected = true
  177. deselect: =>
  178. if @selected
  179. @seg.animate(path: @path, 150, '<>')
  180. @arc.animate(opacity: 0, 150, '<>')
  181. @selected = false