| // |
| // Copyright 2014 Google Inc. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd |
| // |
| |
| part of charted.charts; |
| |
| /// AggregationItem is created by [AggregationModel] to make access to facts |
| /// observable. Users must use AggregationItem.isValid before trying to access |
| /// the aggregations. |
| abstract class AggregationItem extends Observable { |
| /// List of dimension fields in effect |
| List<String> dimensions; |
| |
| /// Check if this entity is valid. |
| /// Currently the only case where an entity becomes invalid |
| /// is when a groupBy is called on the model. |
| bool get isValid; |
| |
| /// Fetch the fact from AggregationModel and return it |
| /// Currently takes keys in the form of "sum(spend)", where sum is |
| /// the aggregation type and spend is fact's field name. |
| /// |
| /// Currently, "sum", "count", "min", "max", "avg", "valid" and "avgOfValid" |
| /// are supported as the operators. |
| operator [](String key); |
| |
| /// List of lower aggregations. |
| List<AggregationItem> lowerAggregations(); |
| |
| /// Check if we support a given key. |
| bool containsKey(String key); |
| |
| /// List of valid field names for this entity. |
| /// It's the combined list of accessors for individual items, items in |
| /// the next dimension and all possible facts defined on the view. |
| Iterable<String> get fieldNames; |
| } |
| |
| /// Implementation of AggregationItem |
| /// Instances of _AggregationItemImpl are created only by AggregationModel |
| class _AggregationItemImpl extends Observable implements AggregationItem { |
| static final List<String> derivedAggregationTypes = ['count', 'avg']; |
| |
| AggregationModel model; |
| List<String> dimensions; |
| |
| String _key; |
| |
| int _factsOffset; |
| |
| /// Currently entities are created only when they have valid aggregations |
| _AggregationItemImpl(this.model, this.dimensions, this._key) { |
| if (model == null) { |
| throw new ArgumentError('Model cannot be null'); |
| } |
| if (_key == null) { |
| _key = ''; |
| } |
| |
| // facts + list of items + list of children (drilldown) |
| _factsOffset = model._dimToAggrMap[_key]; |
| } |
| |
| /// _dimToAggrMap got updated on the model, update ourselves accordingly |
| void update() { |
| _factsOffset = model._dimToAggrMap[_key]; |
| } |
| |
| /// Mark this entity as invalid. |
| void clear() { |
| _factsOffset = null; |
| } |
| |
| bool get isValid => _factsOffset != null; |
| |
| dynamic operator [](String key) { |
| if (!isValid) { |
| throw new StateError('Entity is not valid anymore'); |
| } |
| |
| int argPos = key.indexOf('('); |
| if (argPos == -1) { |
| return _nonAggregationMember(key); |
| } |
| |
| String aggrFunc = key.substring(0, argPos); |
| int aggrFuncIndex = model.computedAggregationTypes.indexOf(aggrFunc); |
| if (aggrFuncIndex == -1 && !derivedAggregationTypes.contains(aggrFunc)) { |
| throw new ArgumentError('Unknown aggregation method: ${aggrFunc}'); |
| } |
| |
| String factName = key.substring(argPos + 1, key.lastIndexOf(')')); |
| int factIndex = model._factFields.indexOf(factName); |
| |
| // Try parsing int if every element in factFields is int. |
| if (model._factFields.every((e) => e is int)) { |
| factIndex = model._factFields.indexOf(int.parse(factName, onError: (e) { |
| throw new ArgumentError('Type of factFields are int but factName' + |
| 'contains non int value'); |
| })); |
| } |
| if (factIndex == -1) { |
| throw new ArgumentError('Model not configured for ${factName}'); |
| } |
| |
| int offset = _factsOffset + factIndex * model._aggregationTypesCount; |
| // No items for the corresponding fact, so return null. |
| if (aggrFunc != 'count' && |
| aggrFunc != 'avg' && |
| model._aggregations[offset + model._offsetCnt].toInt() == 0) { |
| return null; |
| } |
| |
| if (aggrFuncIndex != -1) { |
| return model._aggregations[offset + aggrFuncIndex]; |
| } else if (aggrFunc == 'count') { |
| return model._aggregations[_factsOffset + model._offsetFilteredCount] |
| .toInt(); |
| } else if (aggrFunc == 'avg') { |
| return model._aggregations[offset + model._offsetSum] / |
| model._aggregations[_factsOffset + model._offsetFilteredCount] |
| .toInt(); |
| } else if (aggrFunc == 'avgOfValid') { |
| return model._aggregations[offset + model._offsetSum] / |
| model._aggregations[offset + model._offsetCnt].toInt(); |
| } |
| return null; |
| } |
| |
| dynamic _nonAggregationMember(String key) { |
| if (key == 'items') { |
| return new _AggregationItemsIterator(model, dimensions, _key); |
| } |
| return null; |
| } |
| |
| List<AggregationItem> lowerAggregations() { |
| List<AggregationItem> aggregations = new List<AggregationItem>(); |
| if (dimensions.length == model._dimFields.length) { |
| return aggregations; |
| } |
| |
| var lowerDimensionField = model._dimFields[dimensions.length]; |
| List<String> lowerVals = model.valuesForDimension(lowerDimensionField); |
| |
| lowerVals.forEach((String name) { |
| List<String> lowerDims = new List.from(dimensions)..add(name); |
| AggregationItem entity = model.facts(lowerDims); |
| if (entity != null) { |
| aggregations.add(entity); |
| } |
| }); |
| |
| return aggregations; |
| } |
| |
| bool containsKey(String key) => fieldNames.contains(key); |
| |
| Iterable<String> get fieldNames { |
| if (!isValid) { |
| throw new StateError('Entity is not valid anymore'); |
| } |
| |
| if (model._itemFieldNamesCache == null) { |
| List<String> cache = new List<String>.from(['items', 'children']); |
| model._factFields.forEach((var name) { |
| AggregationModel.supportedAggregationTypes.forEach((String aggrType) { |
| cache.add('${aggrType}(${name})'); |
| }); |
| }); |
| model._itemFieldNamesCache = cache; |
| } |
| return model._itemFieldNamesCache; |
| } |
| |
| // TODO(prsd): Implementation of [Observable] |
| Stream<List<ChangeRecord>> get changes { |
| throw new UnimplementedError(); |
| } |
| } |
| |
| class _AggregationItemsIterator implements Iterator { |
| final AggregationModel model; |
| List<String> dimensions; |
| String key; |
| |
| int _current; |
| int _counter = 0; |
| |
| int _start; |
| int _count; |
| int _endOfRows; |
| |
| _AggregationItemsIterator( |
| this.model, List<String> this.dimensions, String this.key) { |
| int offset = model._dimToAggrMap[key]; |
| if (offset != null) { |
| int factsEndOffset = |
| offset + model._factFields.length * model._aggregationTypesCount; |
| _start = model._aggregations[factsEndOffset].toInt(); |
| _count = model._aggregations[factsEndOffset + 1].toInt(); |
| _endOfRows = model._rows.length; |
| } |
| } |
| |
| bool moveNext() { |
| if (_current == null) { |
| _current = _start; |
| } else { |
| ++_current; |
| } |
| |
| if (++_counter > _count) { |
| return false; |
| } |
| |
| // If model had a filter applied, then check if _current points to a |
| // filtered-in row, else skip till we find one. |
| // Also, make sure (even if something else went wrong) we don't go |
| // beyond the number of items in the model. |
| if (this.model._filterResults != null) { |
| while ((this.model._filterResults[_current ~/ AggregationModel.SMI_BITS] & |
| (1 << _current % AggregationModel.SMI_BITS)) == |
| 0 && |
| _current <= _endOfRows) { |
| ++_current; |
| } |
| } |
| return (_current < _endOfRows); |
| } |
| |
| get current { |
| if (_current == null || _counter > _count) { |
| return null; |
| } |
| return model._rows[model._sorted[_current]]; |
| } |
| } |