# Donut charts. # # @example # Morris.Donut({ # el: $('#donut-container'), # data: [ # { label: 'yin', value: 50 }, # { label: 'yang', value: 50 } # ] # }); class Morris.Donut extends Morris.EventEmitter defaults: colors: [ '#0B62A4' '#3980B5' '#679DC6' '#95BBD7' '#B0CCE1' '#095791' '#095085' '#083E67' '#052C48' '#042135' ], backgroundColor: '#FFFFFF', labelColor: '#000000', formatter: Morris.commas # Create and render a donut chart. # constructor: (options) -> if not (this instanceof Morris.Donut) return new Morris.Donut(options) if typeof options.element is 'string' @el = $ document.getElementById(options.element) else @el = $ options.element @options = $.extend {}, @defaults, options if @el == null || @el.length == 0 throw new Error("Graph placeholder not found.") # bail if there's no data if options.data is undefined or options.data.length is 0 return @data = options.data @values = (parseFloat(row.value) for row in @data) @redraw() # Clear and redraw the chart. # # If you need to re-size your charts, call this method after changing the # size of the container element. redraw: -> @el.empty() @raphael = new Raphael(@el[0]) cx = @el.width() / 2 cy = @el.height() / 2 w = (Math.min(cx, cy) - 10) / 3 total = 0 total += value for value in @values min = 5 / (2 * w) C = 1.9999 * Math.PI - min * @data.length last = 0 idx = 0 @segments = [] for value, i in @values next = last + min + C * (value / total) seg = new Morris.DonutSegment( cx, cy, w*2, w, last, next, @options.colors[idx % @options.colors.length], @options.backgroundColor, @data[i], idx, @raphael) seg.render() @segments.push seg seg.on 'hover', @select seg.on 'click', @click last = next idx += 1 @text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800) @text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14) max_value = Math.max.apply(null, value for value in @values) idx = 0 for value in @values if value == max_value @select idx break idx += 1 # @private click: (idx) => if typeof idx is 'number' then segment = @segments[idx] else segment = idx @fire 'click', segment.idx, segment.data # Select the segment at the given index. select: (idx) => s.deselect() for s in @segments segment = @segments[idx] segment.select() row = @data[idx] @setLabels(row.label, @options.formatter(row.value, row)) # @private setLabels: (label1, label2) -> inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3 maxWidth = 1.8 * inner maxHeightTop = inner / 2 maxHeightBottom = inner / 3 @text1.attr(text: label1, transform: '') text1bbox = @text1.getBBox() text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height) @text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}") @text2.attr(text: label2, transform: '') text2bbox = @text2.getBBox() text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height) @text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}") drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) -> text = @raphael.text(xPos, yPos, '') .attr('font-size', fontSize) .attr('fill', color) text.attr('font-weight', fontWeight) if fontWeight? return text # A segment within a donut chart. # # @private class Morris.DonutSegment extends Morris.EventEmitter constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @data, @index, @raphael) -> @sin_p0 = Math.sin(p0) @cos_p0 = Math.cos(p0) @sin_p1 = Math.sin(p1) @cos_p1 = Math.cos(p1) @is_long = if (p1 - p0) > Math.PI then 1 else 0 @path = @calcSegment(@inner + 3, @inner + @outer - 5) @selectedPath = @calcSegment(@inner + 3, @inner + @outer) @hilight = @calcArc(@inner) calcArcPoints: (r) -> return [ @cx + r * @sin_p0, @cy + r * @cos_p0, @cx + r * @sin_p1, @cy + r * @cos_p1] calcSegment: (r1, r2) -> [ix0, iy0, ix1, iy1] = @calcArcPoints(r1) [ox0, oy0, ox1, oy1] = @calcArcPoints(r2) return ( "M#{ix0},#{iy0}" + "A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" + "L#{ox1},#{oy1}" + "A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" + "Z") calcArc: (r) -> [ix0, iy0, ix1, iy1] = @calcArcPoints(r) return ( "M#{ix0},#{iy0}" + "A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}") render: -> @arc = @drawDonutArc(@hilight, @color) @seg = @drawDonutSegment( @path, @color, @backgroundColor, => @fire('hover', @index), => @fire('click', @index) ) drawDonutArc: (path, color) -> @raphael.path(path) .attr(stroke: color, 'stroke-width': 2, opacity: 0) drawDonutSegment: (path, fillColor, strokeColor, hoverFunction, clickFunction) -> @raphael.path(path) .attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3) .hover(hoverFunction) .click(clickFunction) select: => unless @selected @seg.animate(path: @selectedPath, 150, '<>') @arc.animate(opacity: 1, 150, '<>') @selected = true deselect: => if @selected @seg.animate(path: @path, 150, '<>') @arc.animate(opacity: 0, 150, '<>') @selected = false