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 |