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 |