| // |
| // Copyright 2014 Google Inc. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd |
| // |
| |
| part of charted.charts; |
| |
| class PieChartRenderer extends LayoutRendererBase { |
| static const STATS_PERCENTAGE = 'percentage-only'; |
| static const STATS_VALUE = 'value-only'; |
| static const STATS_VALUE_PERCENTAGE = 'value-percentage'; |
| |
| final Iterable<int> dimensionsUsingBand = const []; |
| final String statsMode; |
| final num innerRadiusRatio; |
| final int maxSliceCount; |
| final String otherItemsLabel; |
| final String otherItemsColor; |
| final showLabels; |
| final bool sortDataByValue; |
| |
| @override |
| final String name = "pie-rdr"; |
| |
| final List<ChartLegendItem> _legend = []; |
| |
| Iterable otherRow; |
| |
| PieChartRenderer( |
| {num innerRadiusRatio: 0, |
| bool showLabels, |
| this.sortDataByValue: true, |
| this.statsMode: STATS_PERCENTAGE, |
| this.maxSliceCount: SMALL_INT_MAX, |
| this.otherItemsLabel: 'Other', |
| this.otherItemsColor: '#EEEEEE'}) |
| : showLabels = showLabels == null ? innerRadiusRatio == 0 : showLabels, |
| innerRadiusRatio = innerRadiusRatio; |
| |
| /// Returns false if the number of dimension axes != 0. Pie chart can only |
| /// be rendered on areas with no axes. |
| @override |
| bool prepare(ChartArea area, ChartSeries series) { |
| _ensureAreaAndSeries(area, series); |
| return area is LayoutArea; |
| } |
| |
| @override |
| Iterable<ChartLegendItem> layout(Element element, |
| {Future schedulePostRender}) { |
| _ensureReadyToDraw(element); |
| |
| var radius = math.min(rect.width, rect.height) / 2; |
| root.attr('transform', 'translate(${rect.width / 2}, ${rect.height / 2})'); |
| |
| // Pick only items that are valid - non-null and don't have null value |
| var measure = series.measures.first, |
| dimension = area.config.dimensions.first, |
| indices = new List.generate(area.data.rows.length, (i) => i); |
| |
| // Sort row indices by value. |
| if (sortDataByValue) { |
| indices.sort((int a, int b) { |
| var aRow = area.data.rows.elementAt(a), |
| bRow = area.data.rows.elementAt(b); |
| num aVal = (aRow == null || aRow.elementAt(measure) == null) |
| ? 0 |
| : aRow.elementAt(measure), |
| bVal = (bRow == null || bRow.elementAt(measure) == null) |
| ? 0 |
| : bRow.elementAt(measure); |
| return bVal.compareTo(aVal); |
| }); |
| } |
| |
| // Limit items to the passed maxSliceCount |
| if (indices.length > maxSliceCount) { |
| var displayed = indices.take(maxSliceCount).toList(); |
| num otherItemsValue = 0; |
| for (int i = displayed.length; i < indices.length; ++i) { |
| var index = indices.elementAt(i), row = area.data.rows.elementAt(index); |
| otherItemsValue += row == null || row.elementAt(measure) == null |
| ? 0 |
| : row.elementAt(measure) as num; |
| } |
| otherRow = new List(max([dimension, measure]).toInt() + 1) |
| ..[dimension] = otherItemsLabel |
| ..[measure] = otherItemsValue; |
| indices = displayed..add(SMALL_INT_MAX); |
| } else { |
| otherRow = null; |
| } |
| |
| if (area.config.isRTL) { |
| indices = indices.reversed.toList(); |
| } |
| |
| num accessor(dynamic _d, int i) { |
| var d = _d as int; |
| var row = d == SMALL_INT_MAX ? otherRow : area.data.rows.elementAt(d); |
| return row == null || row.elementAt(measure) == null |
| ? 0 |
| : row.elementAt(measure) as num; |
| } |
| |
| var data = (new PieLayout()..accessor = accessor).layout(indices); |
| var arc = new SvgArc( |
| innerRadiusCallback: (d, i, e) => innerRadiusRatio * radius, |
| outerRadiusCallback: (d, i, e) => radius); |
| var pie = root.selectAll('.pie-path').data(data); |
| |
| pie.enter.appendWithCallback((d, i, e) { |
| var pieSector = Namespace.createChildElement('path', e) |
| ..classes.add('pie-path'); |
| var styles = stylesForData(d.data as int, i); |
| if (!isNullOrEmpty(styles)) { |
| pieSector.classes.addAll(styles); |
| } |
| pieSector.attributes |
| ..['fill'] = colorForData(d.data as int, i) |
| ..['d'] = arc.path(d, i, host) |
| ..['stroke-width'] = '1px' |
| ..['stroke'] = '#ffffff'; |
| |
| pieSector.append(Namespace.createChildElement('text', pieSector) |
| ..classes.add('pie-label')); |
| return pieSector; |
| }) |
| ..on('click', (d, i, e) => _event(mouseClickController, d, i, e)) |
| ..on('mouseover', (d, i, e) => _event(mouseOverController, d, i, e)) |
| ..on('mouseout', (d, i, e) => _event(mouseOutController, d, i, e)); |
| |
| pie.each((d, i, e) { |
| var styles = stylesForData(d.data as int, i); |
| e.classes.removeAll(ChartState.VALUE_CLASS_NAMES); |
| if (!isNullOrEmpty(styles)) { |
| e.classes.addAll(styles); |
| } |
| e.attributes |
| ..['fill'] = colorForData(d.data as int, i) |
| ..['d'] = arc.path(d, i, host) |
| ..['stroke-width'] = '1px' |
| ..['stroke'] = '#ffffff'; |
| }); |
| |
| pie.exit.remove(); |
| |
| _legend.clear(); |
| var items = new List<ChartLegendItem>.generate(data.length, (i) { |
| SvgArcData d = data.elementAt(i); |
| Iterable row = d.data == SMALL_INT_MAX |
| ? otherRow |
| : area.data.rows.elementAt(d.data as int); |
| |
| return new ChartLegendItem( |
| index: d.data as int, |
| color: colorForData(d.data as int, i), |
| label: row.elementAt(dimension) as String, |
| series: [series], |
| value: |
| '${(((d.endAngle - d.startAngle) * 50) / math.pi).toStringAsFixed(2)}%'); |
| }); |
| return _legend..addAll(area.config.isRTL ? items.reversed : items); |
| } |
| |
| String colorForData(int row, int index) => |
| colorForValue(row, isTail: row == SMALL_INT_MAX); |
| |
| Iterable<String> stylesForData(int row, int i) => |
| stylesForValue(row, isTail: row == SMALL_INT_MAX); |
| |
| @override |
| handleStateChanges(List<ChangeRecord> changes) { |
| root.selectAll('.pie-path').each((d, i, e) { |
| var styles = stylesForData(d.data as int, i); |
| e.classes.removeAll(ChartState.VALUE_CLASS_NAMES); |
| if (!isNullOrEmpty(styles)) { |
| e.classes.addAll(styles); |
| } |
| e.attributes['fill'] = colorForData(d.data as int, i); |
| }); |
| } |
| |
| @override |
| void dispose() { |
| if (root == null) return; |
| root.selectAll('.pie-path').remove(); |
| } |
| |
| void _event(StreamController controller, data, int index, Element e) { |
| // Currently, events are not supported on "Other" pie |
| if (controller == null || data.data == SMALL_INT_MAX) return; |
| controller.add(new DefaultChartEventImpl(scope.event, area, series, |
| data.data as int, series.measures.first, data.value as num)); |
| } |
| } |