| OLD | NEW |
| (Empty) |
| 1 // | |
| 2 // Copyright 2014 Google Inc. All rights reserved. | |
| 3 // | |
| 4 // Use of this source code is governed by a BSD-style | |
| 5 // license that can be found in the LICENSE file or at | |
| 6 // https://developers.google.com/open-source/licenses/bsd | |
| 7 // | |
| 8 | |
| 9 part of charted.charts; | |
| 10 | |
| 11 class PieChartRenderer extends LayoutRendererBase { | |
| 12 static const STATS_PERCENTAGE = 'percentage-only'; | |
| 13 static const STATS_VALUE = 'value-only'; | |
| 14 static const STATS_VALUE_PERCENTAGE = 'value-percentage'; | |
| 15 | |
| 16 final Iterable<int> dimensionsUsingBand = const[]; | |
| 17 final String statsMode; | |
| 18 final num innerRadiusRatio; | |
| 19 final int maxSliceCount; | |
| 20 final String otherItemsLabel; | |
| 21 final String otherItemsColor; | |
| 22 final showLabels; | |
| 23 final sortDataByValue; | |
| 24 | |
| 25 @override | |
| 26 final String name = "pie-rdr"; | |
| 27 | |
| 28 final List<ChartLegendItem> _legend = []; | |
| 29 | |
| 30 Iterable otherRow; | |
| 31 | |
| 32 PieChartRenderer({ | |
| 33 num innerRadiusRatio: 0, | |
| 34 bool showLabels, | |
| 35 this.sortDataByValue: true, | |
| 36 this.statsMode: STATS_PERCENTAGE, | |
| 37 this.maxSliceCount: SMALL_INT_MAX, | |
| 38 this.otherItemsLabel: 'Other', | |
| 39 this.otherItemsColor: '#EEEEEE'}) | |
| 40 : showLabels = showLabels == null ? innerRadiusRatio == 0 : showLabels, | |
| 41 innerRadiusRatio = innerRadiusRatio; | |
| 42 | |
| 43 /// Returns false if the number of dimension axes != 0. Pie chart can only | |
| 44 /// be rendered on areas with no axes. | |
| 45 @override | |
| 46 bool prepare(ChartArea area, ChartSeries series) { | |
| 47 _ensureAreaAndSeries(area, series); | |
| 48 return area is LayoutArea; | |
| 49 } | |
| 50 | |
| 51 @override | |
| 52 Iterable<ChartLegendItem> layout( | |
| 53 Element element, {Future schedulePostRender}) { | |
| 54 _ensureReadyToDraw(element); | |
| 55 | |
| 56 var radius = math.min(rect.width, rect.height) / 2; | |
| 57 root.attr('transform', 'translate(${rect.width / 2}, ${rect.height / 2})'); | |
| 58 | |
| 59 // Pick only items that are valid - non-null and don't have null value | |
| 60 var measure = series.measures.first, | |
| 61 dimension = area.config.dimensions.first, | |
| 62 indices = new List.generate(area.data.rows.length, (i) => i); | |
| 63 | |
| 64 // Sort row indices by value. | |
| 65 if (sortDataByValue) { | |
| 66 indices.sort((int a, int b) { | |
| 67 var aRow = area.data.rows.elementAt(a), | |
| 68 bRow = area.data.rows.elementAt(b), | |
| 69 aVal = (aRow == null || aRow.elementAt(measure) == null) | |
| 70 ? 0 | |
| 71 : aRow.elementAt(measure), | |
| 72 bVal = (bRow == null || bRow.elementAt(measure) == null) | |
| 73 ? 0 | |
| 74 : bRow.elementAt(measure); | |
| 75 return bVal.compareTo(aVal); | |
| 76 }); | |
| 77 } | |
| 78 | |
| 79 // Limit items to the passed maxSliceCount | |
| 80 if (indices.length > maxSliceCount) { | |
| 81 var displayed = indices.take(maxSliceCount).toList(); | |
| 82 var otherItemsValue = 0; | |
| 83 for (int i = displayed.length; i < indices.length; ++i) { | |
| 84 var index = indices.elementAt(i), | |
| 85 row = area.data.rows.elementAt(index); | |
| 86 otherItemsValue += row == null || row.elementAt(measure) == null | |
| 87 ? 0 | |
| 88 : row.elementAt(measure); | |
| 89 } | |
| 90 otherRow = new List(max([dimension, measure]) + 1) | |
| 91 ..[dimension] = otherItemsLabel | |
| 92 ..[measure] = otherItemsValue; | |
| 93 indices = displayed..add(SMALL_INT_MAX); | |
| 94 } else { | |
| 95 otherRow = null; | |
| 96 } | |
| 97 | |
| 98 if (area.config.isRTL) { | |
| 99 indices = indices.reversed.toList(); | |
| 100 } | |
| 101 | |
| 102 var accessor = (d, i) { | |
| 103 var row = d == SMALL_INT_MAX ? otherRow : area.data.rows.elementAt(d); | |
| 104 return row == null || row.elementAt(measure) == null | |
| 105 ? 0 | |
| 106 : row.elementAt(measure); | |
| 107 }; | |
| 108 var data = (new PieLayout()..accessor = accessor).layout(indices), | |
| 109 arc = new SvgArc( | |
| 110 innerRadiusCallback: (d, i, e) => innerRadiusRatio * radius, | |
| 111 outerRadiusCallback: (d, i, e) => radius), | |
| 112 pie = root.selectAll('.pie-path').data(data); | |
| 113 | |
| 114 pie.enter.append('path').classed('pie-path'); | |
| 115 pie | |
| 116 ..each((d, i, e) { | |
| 117 var styles = stylesForData(d.data, i); | |
| 118 e.classes.removeAll(ChartState.VALUE_CLASS_NAMES); | |
| 119 if (!isNullOrEmpty(styles)) { | |
| 120 e.classes.addAll(styles); | |
| 121 } | |
| 122 e.attributes | |
| 123 ..['fill'] = colorForData(d.data, i) | |
| 124 ..['d'] = arc.path(d, i, host) | |
| 125 ..['stroke-width'] = '1px' | |
| 126 ..['stroke'] = '#ffffff'; | |
| 127 | |
| 128 e.append( | |
| 129 Namespace.createChildElement('text', e) | |
| 130 ..classes.add('pie-label')); | |
| 131 }) | |
| 132 ..on('click', (d, i, e) => _event(mouseClickController, d, i, e)) | |
| 133 ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e)) | |
| 134 ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e)); | |
| 135 | |
| 136 pie.exit.remove(); | |
| 137 | |
| 138 _legend.clear(); | |
| 139 var items = new List.generate(data.length, (i) { | |
| 140 SvgArcData d = data.elementAt(i); | |
| 141 Iterable row = d.data == SMALL_INT_MAX | |
| 142 ? otherRow | |
| 143 : area.data.rows.elementAt(d.data); | |
| 144 | |
| 145 return new ChartLegendItem(index: d.data, color: colorForData(d.data, i), | |
| 146 label: row.elementAt(dimension), series: [series], | |
| 147 value: '${(((d.endAngle - d.startAngle) * 50) / math.PI).toStringAsFix
ed(2)}%'); | |
| 148 }); | |
| 149 return _legend..addAll(area.config.isRTL ? items.reversed : items); | |
| 150 } | |
| 151 | |
| 152 String colorForData(int row, int index) => | |
| 153 colorForValue(row, isTail: row == SMALL_INT_MAX); | |
| 154 | |
| 155 Iterable<String> stylesForData(int row, int i) => | |
| 156 stylesForValue(row, isTail: row == SMALL_INT_MAX); | |
| 157 | |
| 158 @override | |
| 159 handleStateChanges(List<ChangeRecord> changes) { | |
| 160 root.selectAll('.pie-path').each((d, i, e) { | |
| 161 var styles = stylesForData(d.data, i); | |
| 162 e.classes.removeAll(ChartState.VALUE_CLASS_NAMES); | |
| 163 if (!isNullOrEmpty(styles)) { | |
| 164 e.classes.addAll(styles); | |
| 165 } | |
| 166 e.attributes['fill'] = colorForData(d.data, i); | |
| 167 }); | |
| 168 } | |
| 169 | |
| 170 @override | |
| 171 void dispose() { | |
| 172 if (root == null) return; | |
| 173 root.selectAll('.pie-path').remove(); | |
| 174 } | |
| 175 | |
| 176 void _event(StreamController controller, data, int index, Element e) { | |
| 177 // Currently, events are not supported on "Other" pie | |
| 178 if (controller == null || data.data == SMALL_INT_MAX) return; | |
| 179 controller.add(new DefaultChartEventImpl( | |
| 180 scope.event, area, series, data.data, series.measures.first, data.value
)); | |
| 181 } | |
| 182 } | |
| OLD | NEW |