Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | espaco | 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 | |||
| 30 | # Create and render a donut chart. |
||
| 31 | # |
||
| 32 | constructor: (options) -> |
||
| 33 | return new Morris.Donut(options) unless (@ instanceof Morris.Donut) |
||
| 34 | @options = $.extend {}, @defaults, options |
||
| 35 | |||
| 36 | if typeof options.element is 'string' |
||
| 37 | @el = $ document.getElementById(options.element) |
||
| 38 | else |
||
| 39 | @el = $ options.element |
||
| 40 | |||
| 41 | if @el == null || @el.length == 0 |
||
| 42 | throw new Error("Graph placeholder not found.") |
||
| 43 | |||
| 44 | # bail if there's no data |
||
| 45 | if options.data is undefined or options.data.length is 0 |
||
| 46 | return |
||
| 47 | |||
| 48 | @raphael = new Raphael(@el[0]) |
||
| 49 | |||
| 50 | if @options.resize |
||
| 51 | $(window).bind 'resize', (evt) => |
||
| 52 | if @timeoutId? |
||
| 53 | window.clearTimeout @timeoutId |
||
| 54 | @timeoutId = window.setTimeout @resizeHandler, 100 |
||
| 55 | |||
| 56 | @setData options.data |
||
| 57 | |||
| 58 | # Clear and redraw the chart. |
||
| 59 | redraw: -> |
||
| 60 | @raphael.clear() |
||
| 61 | |||
| 62 | cx = @el.width() / 2 |
||
| 63 | cy = @el.height() / 2 |
||
| 64 | w = (Math.min(cx, cy) - 10) / 3 |
||
| 65 | |||
| 66 | total = 0 |
||
| 67 | total += value for value in @values |
||
| 68 | |||
| 69 | min = 5 / (2 * w) |
||
| 70 | C = 1.9999 * Math.PI - min * @data.length |
||
| 71 | |||
| 72 | last = 0 |
||
| 73 | idx = 0 |
||
| 74 | @segments = [] |
||
| 75 | for value, i in @values |
||
| 76 | next = last + min + C * (value / total) |
||
| 77 | seg = new Morris.DonutSegment( |
||
| 78 | cx, cy, w*2, w, last, next, |
||
| 79 | @data[i].color || @options.colors[idx % @options.colors.length], |
||
| 80 | @options.backgroundColor, idx, @raphael) |
||
| 81 | seg.render() |
||
| 82 | @segments.push seg |
||
| 83 | seg.on 'hover', @select |
||
| 84 | seg.on 'click', @click |
||
| 85 | last = next |
||
| 86 | idx += 1 |
||
| 87 | |||
| 88 | @text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800) |
||
| 89 | @text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14) |
||
| 90 | |||
| 91 | max_value = Math.max @values... |
||
| 92 | idx = 0 |
||
| 93 | for value in @values |
||
| 94 | if value == max_value |
||
| 95 | @select idx |
||
| 96 | break |
||
| 97 | idx += 1 |
||
| 98 | |||
| 99 | setData: (data) -> |
||
| 100 | @data = data |
||
| 101 | @values = (parseFloat(row.value) for row in @data) |
||
| 102 | @redraw() |
||
| 103 | |||
| 104 | # @private |
||
| 105 | click: (idx) => |
||
| 106 | @fire 'click', idx, @data[idx] |
||
| 107 | |||
| 108 | # Select the segment at the given index. |
||
| 109 | select: (idx) => |
||
| 110 | s.deselect() for s in @segments |
||
| 111 | segment = @segments[idx] |
||
| 112 | segment.select() |
||
| 113 | row = @data[idx] |
||
| 114 | @setLabels(row.label, @options.formatter(row.value, row)) |
||
| 115 | |||
| 116 | |||
| 117 | |||
| 118 | # @private |
||
| 119 | setLabels: (label1, label2) -> |
||
| 120 | inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3 |
||
| 121 | maxWidth = 1.8 * inner |
||
| 122 | maxHeightTop = inner / 2 |
||
| 123 | maxHeightBottom = inner / 3 |
||
| 124 | @text1.attr(text: label1, transform: '') |
||
| 125 | text1bbox = @text1.getBBox() |
||
| 126 | text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height) |
||
| 127 | @text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}") |
||
| 128 | @text2.attr(text: label2, transform: '') |
||
| 129 | text2bbox = @text2.getBBox() |
||
| 130 | text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height) |
||
| 131 | @text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}") |
||
| 132 | |||
| 133 | drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) -> |
||
| 134 | text = @raphael.text(xPos, yPos, '') |
||
| 135 | .attr('font-size', fontSize) |
||
| 136 | .attr('fill', color) |
||
| 137 | text.attr('font-weight', fontWeight) if fontWeight? |
||
| 138 | return text |
||
| 139 | |||
| 140 | resizeHandler: => |
||
| 141 | @timeoutId = null |
||
| 142 | @raphael.setSize @el.width(), @el.height() |
||
| 143 | @redraw() |
||
| 144 | |||
| 145 | |||
| 146 | # A segment within a donut chart. |
||
| 147 | # |
||
| 148 | # @private |
||
| 149 | class Morris.DonutSegment extends Morris.EventEmitter |
||
| 150 | constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @index, @raphael) -> |
||
| 151 | @sin_p0 = Math.sin(p0) |
||
| 152 | @cos_p0 = Math.cos(p0) |
||
| 153 | @sin_p1 = Math.sin(p1) |
||
| 154 | @cos_p1 = Math.cos(p1) |
||
| 155 | @is_long = if (p1 - p0) > Math.PI then 1 else 0 |
||
| 156 | @path = @calcSegment(@inner + 3, @inner + @outer - 5) |
||
| 157 | @selectedPath = @calcSegment(@inner + 3, @inner + @outer) |
||
| 158 | @hilight = @calcArc(@inner) |
||
| 159 | |||
| 160 | calcArcPoints: (r) -> |
||
| 161 | return [ |
||
| 162 | @cx + r * @sin_p0, |
||
| 163 | @cy + r * @cos_p0, |
||
| 164 | @cx + r * @sin_p1, |
||
| 165 | @cy + r * @cos_p1] |
||
| 166 | |||
| 167 | calcSegment: (r1, r2) -> |
||
| 168 | [ix0, iy0, ix1, iy1] = @calcArcPoints(r1) |
||
| 169 | [ox0, oy0, ox1, oy1] = @calcArcPoints(r2) |
||
| 170 | return ( |
||
| 171 | "M#{ix0},#{iy0}" + |
||
| 172 | "A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" + |
||
| 173 | "L#{ox1},#{oy1}" + |
||
| 174 | "A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" + |
||
| 175 | "Z") |
||
| 176 | |||
| 177 | calcArc: (r) -> |
||
| 178 | [ix0, iy0, ix1, iy1] = @calcArcPoints(r) |
||
| 179 | return ( |
||
| 180 | "M#{ix0},#{iy0}" + |
||
| 181 | "A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}") |
||
| 182 | |||
| 183 | render: -> |
||
| 184 | @arc = @drawDonutArc(@hilight, @color) |
||
| 185 | @seg = @drawDonutSegment( |
||
| 186 | @path, |
||
| 187 | @color, |
||
| 188 | @backgroundColor, |
||
| 189 | => @fire('hover', @index), |
||
| 190 | => @fire('click', @index) |
||
| 191 | ) |
||
| 192 | |||
| 193 | drawDonutArc: (path, color) -> |
||
| 194 | @raphael.path(path) |
||
| 195 | .attr(stroke: color, 'stroke-width': 2, opacity: 0) |
||
| 196 | |||
| 197 | drawDonutSegment: (path, fillColor, strokeColor, hoverFunction, clickFunction) -> |
||
| 198 | @raphael.path(path) |
||
| 199 | .attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3) |
||
| 200 | .hover(hoverFunction) |
||
| 201 | .click(clickFunction) |
||
| 202 | |||
| 203 | select: => |
||
| 204 | unless @selected |
||
| 205 | @seg.animate(path: @selectedPath, 150, '<>') |
||
| 206 | @arc.animate(opacity: 1, 150, '<>') |
||
| 207 | @selected = true |
||
| 208 | |||
| 209 | deselect: => |
||
| 210 | if @selected |
||
| 211 | @seg.animate(path: @path, 150, '<>') |
||
| 212 | @arc.animate(opacity: 0, 150, '<>') |
||
| 213 | @selected = false |