| 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 * AggregationItem is created by [AggregationModel] to make access to facts | |
| 13 * observable. Users must use AggregationItem.isValid before trying to access | |
| 14 * the aggregations. | |
| 15 */ | |
| 16 abstract class AggregationItem extends ChangeNotifier { | |
| 17 /** | |
| 18 * List of dimension fields in effect | |
| 19 */ | |
| 20 List<String> dimensions; | |
| 21 | |
| 22 /** | |
| 23 * Check if this entity is valid. | |
| 24 * Currently the only case where an entity becomes invalid | |
| 25 * is when a groupBy is called on the model. | |
| 26 */ | |
| 27 bool get isValid; | |
| 28 | |
| 29 /** | |
| 30 * Fetch the fact from AggregationModel and return it | |
| 31 * Currently takes keys in the form of "sum(spend)", where sum is | |
| 32 * the aggregation type and spend is fact's field name. | |
| 33 * | |
| 34 * Currently, "sum", "count", "min", "max", "avg", "valid" and "avgOfValid" | |
| 35 * are supported as the operators. | |
| 36 */ | |
| 37 | |
| 38 operator[](String key); | |
| 39 | |
| 40 /** | |
| 41 * Check if we support a given key. | |
| 42 */ | |
| 43 bool containsKey(String key); | |
| 44 | |
| 45 /** | |
| 46 * List of valid field names for this entity. | |
| 47 * It's the combined list of accessors for individual items, items in | |
| 48 * the next dimension and all possible facts defined on the view. | |
| 49 */ | |
| 50 Iterable<String> get fieldNames; | |
| 51 } | |
| 52 | |
| 53 /* | |
| 54 * Implementation of AggregationItem | |
| 55 * Instances of _AggregationItemImpl are created only by AggregationModel | |
| 56 */ | |
| 57 class _AggregationItemImpl extends ChangeNotifier implements AggregationItem { | |
| 58 static final List<String> derivedAggregationTypes = ['count', 'avg']; | |
| 59 | |
| 60 AggregationModel model; | |
| 61 List<String> dimensions; | |
| 62 | |
| 63 String _key; | |
| 64 | |
| 65 int _factsOffset; | |
| 66 | |
| 67 /* | |
| 68 * Currently entities are created only when they have valid aggregations | |
| 69 */ | |
| 70 _AggregationItemImpl(this.model, this.dimensions, this._key) { | |
| 71 if (model == null) { | |
| 72 throw new ArgumentError('Model cannot be null'); | |
| 73 } | |
| 74 if (_key == null) { | |
| 75 _key = ''; | |
| 76 } | |
| 77 | |
| 78 // facts + list of items + list of children (drilldown) | |
| 79 _factsOffset = model._dimToAggrMap[_key]; | |
| 80 } | |
| 81 | |
| 82 /** | |
| 83 * _dimToAggrMap got updated on the model, update ourselves accordingly | |
| 84 */ | |
| 85 void update() { | |
| 86 _factsOffset = model._dimToAggrMap[_key]; | |
| 87 } | |
| 88 | |
| 89 /* | |
| 90 * Mark this entity as invalid. | |
| 91 */ | |
| 92 void clear() { | |
| 93 _factsOffset = null; | |
| 94 } | |
| 95 | |
| 96 bool get isValid => _factsOffset != null; | |
| 97 | |
| 98 dynamic operator[](String key) { | |
| 99 if (!isValid) { | |
| 100 throw new StateError('Entity is not valid anymore'); | |
| 101 } | |
| 102 | |
| 103 int argPos = key.indexOf('('); | |
| 104 if (argPos == -1) { | |
| 105 return _nonAggregationMember(key); | |
| 106 } | |
| 107 | |
| 108 String aggrFunc = key.substring(0, argPos); | |
| 109 int aggrFuncIndex = model.computedAggregationTypes.indexOf(aggrFunc); | |
| 110 if (aggrFuncIndex == -1 && !derivedAggregationTypes.contains(aggrFunc)) { | |
| 111 throw new ArgumentError('Unknown aggregation method: ${aggrFunc}'); | |
| 112 } | |
| 113 | |
| 114 String factName = key.substring(argPos + 1, key.lastIndexOf(')')); | |
| 115 int factIndex = model._factFields.indexOf(factName); | |
| 116 | |
| 117 // Try parsing int if every element in factFields is int. | |
| 118 if (model._factFields.every((e) => e is int)) { | |
| 119 factIndex = model._factFields.indexOf(int.parse(factName, | |
| 120 onError: (e) { | |
| 121 throw new ArgumentError('Type of factFields are int but factName' + | |
| 122 'contains non int value'); | |
| 123 })); | |
| 124 } | |
| 125 if (factIndex == -1) { | |
| 126 throw new ArgumentError('Model not configured for ${factName}'); | |
| 127 } | |
| 128 | |
| 129 int offset = _factsOffset + factIndex * model._aggregationTypesCount; | |
| 130 // No items for the corresponding fact, so return null. | |
| 131 if (aggrFunc != 'count' && aggrFunc != 'avg' && | |
| 132 model._aggregations[offset + model._offsetCnt].toInt() == 0) { | |
| 133 return null; | |
| 134 } | |
| 135 | |
| 136 if (aggrFuncIndex != -1) { | |
| 137 return model._aggregations[offset + aggrFuncIndex]; | |
| 138 } else if (aggrFunc == 'count') { | |
| 139 return model._aggregations[_factsOffset + | |
| 140 model._offsetFilteredCount].toInt(); | |
| 141 } else if (aggrFunc == 'avg') { | |
| 142 return model._aggregations[offset + model._offsetSum] / | |
| 143 model._aggregations[_factsOffset + model._offsetFilteredCount]. | |
| 144 toInt(); | |
| 145 } else if (aggrFunc == 'avgOfValid') { | |
| 146 return model._aggregations[offset + model._offsetSum] / | |
| 147 model._aggregations[offset + model._offsetCnt].toInt(); | |
| 148 } | |
| 149 return null; | |
| 150 } | |
| 151 | |
| 152 dynamic _nonAggregationMember(String key) { | |
| 153 if (key == 'items') { | |
| 154 return new _AggregationItemsIterator(model, dimensions, _key); | |
| 155 } | |
| 156 if (key == 'aggregations') { | |
| 157 return _lowerAggregations(); | |
| 158 } | |
| 159 return null; | |
| 160 } | |
| 161 | |
| 162 List<AggregationItem> _lowerAggregations() { | |
| 163 List<AggregationItem> aggregations = new List<AggregationItem>(); | |
| 164 if (dimensions.length == model._dimFields.length) { | |
| 165 return aggregations; | |
| 166 } | |
| 167 | |
| 168 var lowerDimensionField = model._dimFields[dimensions.length]; | |
| 169 List lowerVals = model.valuesForDimension(lowerDimensionField); | |
| 170 | |
| 171 lowerVals.forEach((name) { | |
| 172 List lowerDims = new List.from(dimensions)..add(name); | |
| 173 AggregationItem entity = model.facts(lowerDims); | |
| 174 if (entity != null) { | |
| 175 aggregations.add(entity); | |
| 176 } | |
| 177 }); | |
| 178 | |
| 179 return aggregations; | |
| 180 } | |
| 181 | |
| 182 bool containsKey(String key) => fieldNames.contains(key); | |
| 183 | |
| 184 Iterable<String> get fieldNames { | |
| 185 if (!isValid) { | |
| 186 throw new StateError('Entity is not valid anymore'); | |
| 187 } | |
| 188 | |
| 189 if (model._itemFieldNamesCache == null) { | |
| 190 List<String> cache = new List<String>.from(['items', 'children']); | |
| 191 model._factFields.forEach((var name) { | |
| 192 AggregationModel.supportedAggregationTypes.forEach((String aggrType) { | |
| 193 cache.add('${aggrType}(${name})'); | |
| 194 }); | |
| 195 }); | |
| 196 model._itemFieldNamesCache = cache; | |
| 197 } | |
| 198 return model._itemFieldNamesCache; | |
| 199 } | |
| 200 | |
| 201 /* | |
| 202 * TODO(prsd): Implementation of [Observable] | |
| 203 */ | |
| 204 Stream<List<ChangeRecord>> get changes { | |
| 205 throw new UnimplementedError(); | |
| 206 } | |
| 207 } | |
| 208 | |
| 209 class _AggregationItemsIterator implements Iterator { | |
| 210 final AggregationModel model; | |
| 211 List<String> dimensions; | |
| 212 String key; | |
| 213 | |
| 214 int _current; | |
| 215 int _counter = 0; | |
| 216 | |
| 217 int _start; | |
| 218 int _count; | |
| 219 int _endOfRows; | |
| 220 | |
| 221 _AggregationItemsIterator(this.model, List<String> this.dimensions, | |
| 222 String this.key) { | |
| 223 int offset = model._dimToAggrMap[key]; | |
| 224 if (offset != null) { | |
| 225 int factsEndOffset = offset + | |
| 226 model._factFields.length * model._aggregationTypesCount; | |
| 227 _start = model._aggregations[factsEndOffset].toInt(); | |
| 228 _count = model._aggregations[factsEndOffset + 1].toInt(); | |
| 229 _endOfRows = model._rows.length; | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 bool moveNext() { | |
| 234 if (_current == null) { | |
| 235 _current = _start; | |
| 236 } else { | |
| 237 ++_current; | |
| 238 } | |
| 239 | |
| 240 if (++_counter > _count) { | |
| 241 return false; | |
| 242 } | |
| 243 | |
| 244 /* | |
| 245 * If model had a filter applied, then check if _current points to a | |
| 246 * filtered-in row, else skip till we find one. | |
| 247 * Also, make sure (even if something else went wrong) we don't go | |
| 248 * beyond the number of items in the model. | |
| 249 */ | |
| 250 if (this.model._filterResults != null) { | |
| 251 while ((this.model._filterResults[_current ~/ AggregationModel.SMI_BITS] & | |
| 252 (1 << _current % AggregationModel.SMI_BITS)) == 0 && | |
| 253 _current <= _endOfRows) { | |
| 254 ++_current; | |
| 255 } | |
| 256 } | |
| 257 return (_current < _endOfRows); | |
| 258 } | |
| 259 | |
| 260 get current { | |
| 261 if (_current == null || _counter > _count) { | |
| 262 return null; | |
| 263 } | |
| 264 return model._rows[model._sorted[_current]]; | |
| 265 } | |
| 266 } | |
| OLD | NEW |