| // |
| // 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; |
| |
| /// Transforms the ChartData based on the specified dimension columns and facts |
| /// columns indices. The values in the facts columns will be aggregated by the |
| /// tree hierarchy generated by the dimension columns. Expand and Collapse |
| /// methods may be called to display different levels of aggregation. |
| /// |
| /// The output ChartData produced by transform() will contain only columns in the |
| /// original ChartData that were specified in dimensions or facts column indices. |
| /// The output column will be re-ordered first by the indices specified in the |
| /// dimension column indices then by the facts column indices. The data in the |
| /// cells of each row will also follow this rule. |
| class AggregationTransformer extends Observable |
| implements ChartDataTransform, ChartData { |
| static const String AGGREGATION_TYPE_SUM = 'sum'; |
| static const String AGGREGATION_TYPE_MIN = 'min'; |
| static const String AGGREGATION_TYPE_MAX = 'max'; |
| static const String AGGREGATION_TYPE_VALID = 'valid'; |
| final SubscriptionsDisposer _dataSubscriptions = new SubscriptionsDisposer(); |
| final Set<List> _expandedSet = new Set(); |
| List<ChartColumnSpec> columns; |
| ObservableList<List> rows = new ObservableList<List>(); |
| List<int> _dimensionColumnIndices; |
| List<int> _factsColumnIndices; |
| String _aggregationType; |
| AggregationModel _model; |
| bool _expandAllDimension = false; |
| List<int> _selectedColumns = []; |
| FieldAccessor _indexFieldAccessor = |
| (row, index) => (row as List)[index as int]; |
| ChartData _data; |
| |
| AggregationTransformer(this._dimensionColumnIndices, this._factsColumnIndices, |
| [String aggregationType = AGGREGATION_TYPE_SUM]) { |
| _aggregationType = aggregationType; |
| } |
| |
| /// Transforms the ChartData base on the specified dimension columns and facts |
| /// columns, aggregation type and currently expanded dimensions. |
| ChartData transform(ChartData data) { |
| assert(data.columns.length > max(_dimensionColumnIndices)); |
| assert(data.columns.length > max(_factsColumnIndices)); |
| _data = data; |
| _registerListeners(); |
| _transform(); |
| return this; |
| } |
| |
| /// Registers listeners if data.rows or data.columns are Observable. |
| _registerListeners() { |
| _dataSubscriptions.dispose(); |
| |
| if (_data is Observable) { |
| var observable = (_data as Observable); |
| _dataSubscriptions.add(observable.changes.listen((records) { |
| _transform(); |
| |
| // NOTE: Currently we're only passing the first change because the chart |
| // area just draw with the updated data. When we add partial update |
| // to chart area, we'll need to handle this better. |
| notifyChange(records.first); |
| })); |
| } |
| } |
| |
| /// Performs the filter transform with _data. This is called on transform and |
| /// onChange if the input ChartData is Observable. |
| _transform() { |
| _model = new AggregationModel( |
| _data.rows, _dimensionColumnIndices, _factsColumnIndices, |
| aggregationTypes: [_aggregationType], |
| dimensionAccessor: _indexFieldAccessor, |
| factsAccessor: _indexFieldAccessor); |
| _model.compute(); |
| |
| // If user called expandAll prior to model initiation, do it now. |
| if (_expandAllDimension) { |
| expandAll(); |
| } |
| |
| _selectedColumns.clear(); |
| _selectedColumns.addAll(_dimensionColumnIndices); |
| _selectedColumns.addAll(_factsColumnIndices); |
| |
| // Process rows. |
| rows.clear(); |
| var transformedRows = <List>[]; |
| for (String value |
| in _model.valuesForDimension(_dimensionColumnIndices[0])) { |
| _generateAggregatedRow(transformedRows, [value]); |
| } |
| rows.addAll(transformedRows); |
| |
| // Process columns. |
| columns = new List<ChartColumnSpec>.generate(_selectedColumns.length, |
| (int index) => _data.columns.elementAt(_selectedColumns[index])); |
| } |
| |
| /// Fills the aggregatedRows List with data base on the set of expanded values |
| /// recursively. Currently when a dimension is expanded, rows are |
| /// generated for its children but not for itself. If we want to change the |
| /// logic to include itself, just move the expand check around the else clause |
| /// and always write a row of data whether it's expanded or not. |
| _generateAggregatedRow( |
| List<List> aggregatedRows, List<String> dimensionValues) { |
| var entity = _model.facts(dimensionValues); |
| var dimensionLevel = dimensionValues.length - 1; |
| |
| // Dimension is not expanded at this level. Generate data rows and fill int |
| // value base on whether the column is dimension column or facts column. |
| if (!_isExpanded(dimensionValues) || |
| dimensionValues.length == _dimensionColumnIndices.length) { |
| aggregatedRows.add(new List.generate(_selectedColumns.length, (index) { |
| // Dimension column. |
| if (index < _dimensionColumnIndices.length) { |
| if (index < dimensionLevel) { |
| // If column index is in a higher level, write parent value. |
| return dimensionValues[0]; |
| } else if (index == dimensionLevel) { |
| // If column index is at current level, write value. |
| return dimensionValues.last; |
| } else { |
| // If column Index is in a lower level, write empty string. |
| return ''; |
| } |
| } else { |
| // Write aggregated value for facts column. |
| return entity['${_aggregationType}(${_selectedColumns[index]})']; |
| } |
| })); |
| } else { |
| // Dimension is expanded, process each child dimension in the expanded |
| // dimension. |
| for (AggregationItem childAggregation in entity.lowerAggregations()) { |
| _generateAggregatedRow(aggregatedRows, childAggregation.dimensions); |
| } |
| } |
| } |
| |
| /// Expands a specific dimension and optionally expands all of its parent |
| /// dimensions. |
| void expand(List dimension, [bool expandParent = true]) { |
| _expandAllDimension = false; |
| _expandedSet.add(dimension); |
| if (expandParent && dimension.length > 1) { |
| var eq = const ListEquality().equals; |
| var dim = dimension.take(dimension.length - 1).toList(); |
| if (!_expandedSet.any((e) => eq(e, dim))) { |
| expand(dim); |
| } |
| } |
| } |
| |
| /// Collapses a specific dimension and optionally collapse all of its |
| /// Children dimensions. |
| void collapse(List dimension, [bool collapseChildren = true]) { |
| _expandAllDimension = false; |
| if (collapseChildren) { |
| var eq = const ListEquality().equals; |
| // Doing this because _expandedSet.where doesn't work. |
| var collapseList = []; |
| for (List dim in _expandedSet) { |
| if (eq(dim.take(dimension.length).toList(), dimension)) { |
| collapseList.add(dim); |
| } |
| } |
| _expandedSet.removeAll(collapseList); |
| } else { |
| _expandedSet.remove(dimension); |
| } |
| } |
| |
| /// Expands all dimensions. |
| void expandAll() { |
| if (_model != null) { |
| for (String value |
| in _model.valuesForDimension(_dimensionColumnIndices[0])) { |
| _expandAll([value]); |
| } |
| _expandAllDimension = false; |
| } else { |
| _expandAllDimension = true; |
| } |
| } |
| |
| void _expandAll(List<String> value) { |
| var entity = _model.facts(value); |
| _expandedSet.add(value); |
| for (AggregationItem childAggregation in entity.lowerAggregations()) { |
| _expandAll(childAggregation.dimensions); |
| } |
| } |
| |
| /// Collapses all dimensions. |
| void collapseAll() { |
| _expandAllDimension = false; |
| _expandedSet.clear(); |
| } |
| |
| /// Tests if specific dimension is expanded. |
| bool _isExpanded(List dimension) { |
| var eq = const ListEquality().equals; |
| return _expandedSet.any((e) => eq(e, dimension)); |
| } |
| } |