| 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 /** | |
| 12 * Transforms the ChartData base on the specified dimension columns and facts | |
| 13 * columns indices. The values in the facts columns will be aggregated by the | |
| 14 * tree hierarchy generated by the dimension columns. Expand and Collapse | |
| 15 * methods may be called to display different levels of aggregation. | |
| 16 * | |
| 17 * The output ChartData produced by transform() will contain only columns in the | |
| 18 * original ChartData that were specified in dimensions or facts column indices. | |
| 19 * The output column will be re-ordered first by the indices specified in the | |
| 20 * dimension column indices then by the facts column indices. The data in the | |
| 21 * cells of each row will also follow this rule. | |
| 22 */ | |
| 23 class AggregationTransformer extends ChangeNotifier | |
| 24 implements ChartDataTransform, ChartData { | |
| 25 | |
| 26 static const String AGGREGATION_TYPE_SUM = 'sum'; | |
| 27 static const String AGGREGATION_TYPE_MIN = 'min'; | |
| 28 static const String AGGREGATION_TYPE_MAX = 'max'; | |
| 29 static const String AGGREGATION_TYPE_VALID = 'valid'; | |
| 30 final SubscriptionsDisposer _dataSubscriptions = new SubscriptionsDisposer(); | |
| 31 final Set<List> _expandedSet = new Set(); | |
| 32 Iterable<ChartColumnSpec> columns; | |
| 33 ObservableList<Iterable> rows = new ObservableList(); | |
| 34 List<int> _dimensionColumnIndices; | |
| 35 List<int> _factsColumnIndices; | |
| 36 String _aggregationType; | |
| 37 AggregationModel _model; | |
| 38 bool _expandAllDimension = false; | |
| 39 List _selectedColumns = []; | |
| 40 FieldAccessor _indexFieldAccessor = (List row, int index) => row[index]; | |
| 41 ChartData _data; | |
| 42 | |
| 43 AggregationTransformer(this._dimensionColumnIndices, | |
| 44 this._factsColumnIndices, | |
| 45 [String aggregationType = AGGREGATION_TYPE_SUM]) { | |
| 46 _aggregationType = aggregationType; | |
| 47 } | |
| 48 | |
| 49 /** | |
| 50 * Transforms the ChartData base on the specified dimension columns and facts | |
| 51 * columns, aggregation type and currently expanded dimensions. | |
| 52 */ | |
| 53 ChartData transform(ChartData data) { | |
| 54 assert(data.columns.length > max(_dimensionColumnIndices)); | |
| 55 assert(data.columns.length > max(_factsColumnIndices)); | |
| 56 _data = data; | |
| 57 _registerListeners(); | |
| 58 _transform(); | |
| 59 return this; | |
| 60 } | |
| 61 | |
| 62 /** Registers listeners if data.rows or data.columns are Observable. */ | |
| 63 _registerListeners() { | |
| 64 _dataSubscriptions.dispose(); | |
| 65 | |
| 66 if(_data is Observable) { | |
| 67 var observable = (_data as Observable); | |
| 68 _dataSubscriptions.add(observable.changes.listen((records) { | |
| 69 _transform(); | |
| 70 | |
| 71 // NOTE: Currently we're only passing the first change because the chart | |
| 72 // area just draw with the updated data. When we add partial update | |
| 73 // to chart area, we'll need to handle this better. | |
| 74 notifyChange(records.first); | |
| 75 })); | |
| 76 } | |
| 77 } | |
| 78 | |
| 79 /** | |
| 80 * Performs the filter transform with _data. This is called on transform and | |
| 81 * onChange if the input ChartData is Observable. | |
| 82 */ | |
| 83 _transform() { | |
| 84 _model = new AggregationModel(_data.rows, _dimensionColumnIndices, | |
| 85 _factsColumnIndices, aggregationTypes: [_aggregationType], | |
| 86 dimensionAccessor: _indexFieldAccessor, | |
| 87 factsAccessor: _indexFieldAccessor); | |
| 88 _model.compute(); | |
| 89 | |
| 90 // If user called expandAll prior to model initiation, do it now. | |
| 91 if (_expandAllDimension) { | |
| 92 expandAll(); | |
| 93 } | |
| 94 | |
| 95 _selectedColumns.clear(); | |
| 96 _selectedColumns.addAll(_dimensionColumnIndices); | |
| 97 _selectedColumns.addAll(_factsColumnIndices); | |
| 98 | |
| 99 // Process rows. | |
| 100 rows.clear(); | |
| 101 var transformedRows = <Iterable>[]; | |
| 102 for (var value in _model.valuesForDimension(_dimensionColumnIndices[0])) { | |
| 103 _generateAggregatedRow(transformedRows, [value]); | |
| 104 } | |
| 105 rows.addAll(transformedRows); | |
| 106 | |
| 107 // Process columns. | |
| 108 columns = new List<ChartColumnSpec>.generate(_selectedColumns.length, (index
) => | |
| 109 _data.columns.elementAt(_selectedColumns[index])); | |
| 110 } | |
| 111 | |
| 112 /** | |
| 113 * Fills the aggregatedRows List with data base on the set of expanded values | |
| 114 * recursively. Currently when a dimension is expanded, rows are | |
| 115 * generated for its children but not for itself. If we want to change the | |
| 116 * logic to include itself, just move the expand check around the else clause | |
| 117 * and always write a row of data whether it's expanded or not. | |
| 118 */ | |
| 119 _generateAggregatedRow(List<Iterable> aggregatedRows, List dimensionValues) { | |
| 120 var entity = _model.facts(dimensionValues); | |
| 121 var dimensionLevel = dimensionValues.length - 1; | |
| 122 | |
| 123 // Dimension is not expanded at this level. Generate data rows and fill int | |
| 124 // value base on whether the column is dimension column or facts column. | |
| 125 if (!_isExpanded(dimensionValues) || | |
| 126 dimensionValues.length == _dimensionColumnIndices.length) { | |
| 127 aggregatedRows.add(new List.generate(_selectedColumns.length, (index) { | |
| 128 | |
| 129 // Dimension column. | |
| 130 if (index < _dimensionColumnIndices.length) { | |
| 131 if (index < dimensionLevel) { | |
| 132 // If column index is in a higher level, write parent value. | |
| 133 return dimensionValues[0]; | |
| 134 } else if (index == dimensionLevel) { | |
| 135 // If column index is at current level, write value. | |
| 136 return dimensionValues.last; | |
| 137 } else { | |
| 138 // If column Index is in a lower level, write empty string. | |
| 139 return ''; | |
| 140 } | |
| 141 } else { | |
| 142 // Write aggregated value for facts column. | |
| 143 return entity['${_aggregationType}(${_selectedColumns[index]})']; | |
| 144 } | |
| 145 })); | |
| 146 } else { | |
| 147 // Dimension is expanded, process each child dimension in the expanded | |
| 148 // dimension. | |
| 149 for (AggregationItem childAggregation in entity['aggregations']) { | |
| 150 _generateAggregatedRow(aggregatedRows, childAggregation.dimensions); | |
| 151 } | |
| 152 } | |
| 153 } | |
| 154 | |
| 155 /** | |
| 156 * Expands a specific dimension and optionally expands all of its parent | |
| 157 * dimensions. | |
| 158 */ | |
| 159 void expand(List dimension, [bool expandParent = true]) { | |
| 160 _expandAllDimension = false; | |
| 161 _expandedSet.add(dimension); | |
| 162 if (expandParent && dimension.length > 1) { | |
| 163 Function eq = const ListEquality().equals; | |
| 164 var dim = dimension.take(dimension.length - 1).toList(); | |
| 165 if (!_expandedSet.any((e) => eq(e, dim))) { | |
| 166 expand(dim); | |
| 167 } | |
| 168 } | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * Collapses a specific dimension and optionally collapse all of its | |
| 173 * Children dimensions. | |
| 174 */ | |
| 175 void collapse(List dimension, [bool collapseChildren = true]) { | |
| 176 _expandAllDimension = false; | |
| 177 if (collapseChildren) { | |
| 178 Function eq = const ListEquality().equals; | |
| 179 // Doing this because _expandedSet.where doesn't work. | |
| 180 var collapseList = []; | |
| 181 for (List dim in _expandedSet) { | |
| 182 if (eq(dim.take(dimension.length).toList(), dimension)) { | |
| 183 collapseList.add(dim); | |
| 184 } | |
| 185 } | |
| 186 _expandedSet.removeAll(collapseList); | |
| 187 } else { | |
| 188 _expandedSet.remove(dimension); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 /** Expands all dimensions. */ | |
| 193 void expandAll() { | |
| 194 if (_model != null) { | |
| 195 for (var value in _model.valuesForDimension(_dimensionColumnIndices[0])) { | |
| 196 _expandAll([value]); | |
| 197 } | |
| 198 _expandAllDimension = false; | |
| 199 } else { | |
| 200 _expandAllDimension = true; | |
| 201 } | |
| 202 } | |
| 203 | |
| 204 void _expandAll(value) { | |
| 205 var entity = _model.facts(value); | |
| 206 _expandedSet.add(value); | |
| 207 for (AggregationItem childAggregation in entity['aggregations']) { | |
| 208 _expandAll(childAggregation.dimensions); | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 /** Collapses all dimensions. */ | |
| 213 void collapseAll() { | |
| 214 _expandAllDimension = false; | |
| 215 _expandedSet.clear(); | |
| 216 } | |
| 217 | |
| 218 /** Tests if specific dimension is expanded. */ | |
| 219 bool _isExpanded(List dimension) { | |
| 220 Function eq = const ListEquality().equals; | |
| 221 return _expandedSet.any((e) => eq(e, dimension)); | |
| 222 } | |
| 223 } | |
| OLD | NEW |