blob: 652877714be4d89e3f103b660176bc8365de865d [file] [log] [blame]
//
// 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));
}
}