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 |