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