Skip to content

Commit

Permalink
Pivoting and Charting (#25)
Browse files Browse the repository at this point in the history
Closes #21, #25, #26
  • Loading branch information
akshaisarma authored May 11, 2017
1 parent 423d595 commit 548d675
Show file tree
Hide file tree
Showing 57 changed files with 1,249 additions and 179 deletions.
24 changes: 12 additions & 12 deletions app/components/output-data-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,26 @@ export default Ember.Component.extend({
}),

// Helper equalities for template
isRawAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('RAW')),
isGroupAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('GROUP')),
isCountDistinctAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('COUNT_DISTINCT')),
isDistributionAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('DISTRIBUTION')),
isTopKAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('TOP_K')),
isRawAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('RAW')).readOnly(),
isGroupAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('GROUP')).readOnly(),
isCountDistinctAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('COUNT_DISTINCT')).readOnly(),
isDistributionAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('DISTRIBUTION')).readOnly(),
isTopKAggregation: Ember.computed.equal('outputDataType', AGGREGATIONS.get('TOP_K')).readOnly(),

isSelectType: Ember.computed.equal('rawType', RAWS.get('SELECT')),
showRawSelections: Ember.computed.and('isRawAggregation', 'isSelectType'),
isSelectType: Ember.computed.equal('rawType', RAWS.get('SELECT')).readOnly(),
showRawSelections: Ember.computed.and('isRawAggregation', 'isSelectType').readOnly(),

isNumberOfPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('NUMBER')),
isPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('POINTS')),
isGeneratedPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('GENERATED')),
isNumberOfPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('NUMBER')).readOnly(),
isPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('POINTS')).readOnly(),
isGeneratedPoints: Ember.computed.equal('pointType', DISTRIBUTION_POINTS.get('GENERATED')).readOnly(),

canDeleteProjections: Ember.computed('query.projections.[]', function() {
return this.get('query.projections.length') > 1;
}),
}).readOnly(),

canDeleteField: Ember.computed('query.aggregation.groups.[]', function() {
return this.get('query.aggregation.groups.length') > 1;
}),
}).readOnly(),

findOrDefault(valuePath, defaultValue) {
let value = this.get(valuePath);
Expand Down
40 changes: 40 additions & 0 deletions app/components/pivot-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Ember from 'ember';

export default Ember.Component.extend({
rows: null,
columns: null,
initialOptions: null,
defaultOptions: {
unusedAttrsVertical: true,
menuLimit: 200,
renderers: Ember.$.extend(
Ember.$.pivotUtilities.renderers,
Ember.$.pivotUtilities.c3_renderers
)
},

options: Ember.computed('initialOptions', 'defaultOptions', function() {
let deserialized = this.get('initialOptions');
let options = this.get('defaultOptions');
// Attach refresh handler
return Ember.$.extend({ onRefresh: this.refreshHandler(this) }, deserialized, options);
}),

didInsertElement() {
this._super(...arguments);
let { rows, options } = this.getProperties('rows', 'options');
this.$('.pivot-table-container').pivotUI(rows, options);
},

refreshHandler(context) {
return (configuration) => {
let copy = JSON.parse(JSON.stringify(configuration));
// Deletes functions and defaults: http://nicolas.kruchten.com/pivottable/examples/onrefresh.html
delete copy.aggregators;
delete copy.renderers;
delete copy.rendererOptions;
delete copy.localeStrings;
context.sendAction('onRefresh', copy);
};
}
});
2 changes: 1 addition & 1 deletion app/components/query-blurb.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import Ember from 'ember';

export default Ember.Component.extend({
classNames: ['query-blurb'],
query: null
summary: null
});
165 changes: 165 additions & 0 deletions app/components/records-charter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2016, Yahoo Inc.
* Licensed under the terms of the Apache License, Version 2.0.
* See the LICENSE file associated with the project for terms.
*/
import Ember from 'ember';

export default Ember.Component.extend({
classNames: ['records-charter'],
model: null,
columns: null,
rows: null,
chartType: 'bar',
simpleMode: true,

cannotModeSwitch: Ember.computed.alias('model.isRaw').readOnly(),
canModeSwitch: Ember.computed.not('cannotModeSwitch').readOnly(),
notSimpleMode: Ember.computed.not('simpleMode').readOnly(),
pivotMode: Ember.computed.or('notSimpleMode', 'cannotModeSwitch').readOnly(),
pivotOptions: Ember.computed('model.pivotOptions', function() {
return JSON.parse(this.get('model.pivotOptions'));
}).readOnly(),

sampleRow: Ember.computed('rows', 'columns', function() {
let typicalRow = { };
let rows = this.get('rows');
this.get('columns').forEach(column => {
for (let row of rows) {
let value = row[column];
if (!Ember.isEmpty(value)) {
typicalRow[column] = value;
break;
}
}
});
return typicalRow;
}),

independentColumns: Ember.computed('model', 'sampleRow', 'columns', function() {
let { columns, sampleRow } = this.getProperties('columns', 'sampleRow');
let isDistribution = this.get('model.isDistribution');
if (isDistribution) {
return Ember.A(columns.filter(c => this.isAny(c, 'Quantile', 'Range')));
}
// Pick all string columns
return Ember.A(columns.filter(c => this.isType(sampleRow, c, 'string')));
}),

dependentColumns: Ember.computed('model', 'sampleRow', 'columns', function() {
let { columns, sampleRow } = this.getProperties('columns', 'sampleRow');
let isDistribution = this.get('model.isDistribution');
if (isDistribution) {
return Ember.A(columns.filter(c => this.isAny(c, 'Count', 'Value', 'Probability')));
}
// Pick all number columns
return Ember.A(columns.filter(c => this.isType(sampleRow, c, 'number')));
}),

options: Ember.computed('dependentColumns', function() {
let numberOfColumns = this.get('dependentColumns.length');
if (numberOfColumns === 1) {
return { };
}
// Everything else, 2 axes
return {
scales: {
yAxes: [{
position: 'left',
id: '0'
}, {
position: 'right',
id: '1'
}]
}
};
}),

labels: Ember.computed('independentColumns', 'rows', function() {
// Only one independent column for now
let rows = this.get('rows');
// [ [field1 values...], [field2 values...], ...]
let valuesList = this.get('independentColumns').map(field => this.getFieldValues(field, rows));
// valuesList won't be empty because all non-Raw aggregations will have at least one string field
return this.zip(valuesList);
}),

datasets: Ember.computed('dependentColumns', 'rows', function() {
let dependentColumns = this.get('dependentColumns');
let rows = this.get('rows');
return dependentColumns.map((c, i) => this.dataset(c, rows, i));
}),

data: Ember.computed('labels', 'datasets', function() {
return {
labels: this.get('labels'),
datasets: this.get('datasets')
};
}),

dataset(column, rows, index) {
let values = this.getFieldValues(column, rows);
let dataset = {
label: column,
data: values,
backgroundColor: this.randomColors(values.length)
};
// Add yAxisID only if we have more than one dataset. More than 2 => Add the first y-axis
if (index === 1) {
dataset.yAxisID = '1';
} else if (index > 1) {
dataset.yAxisID = '0';
}
return dataset;
},

randomUpto(size) {
return Math.floor(Math.random() * size);
},

randomColor() {
let red = this.randomUpto(255);
let green = this.randomUpto(255);
let blue = this.randomUpto(255);
return `rgb(${red},${green},${blue})`;
},

randomColors(size) {
let color = this.randomColor();
return new Array(size).fill(color);
},

isType(row, field, type) {
return Ember.isEqual(Ember.typeOf(row[field]), type);
},

isAny(field, ...values) {
for (let value of values) {
if (Ember.isEqual(field, value)) {
return true;
}
}
return false;
},

zip(arrayOfArrays, delimiter = '/') {
let zipped = arrayOfArrays[0].map((_, i) => arrayOfArrays.map(a => a[i]));
return zipped.map(a => a.reduce((p, c) => `${p}${delimiter}${c}`), '');
},

getFieldValues(field, rows) {
return rows.map(row => row[field]);
},

actions: {
toggleMode() {
this.toggleProperty('simpleMode');
},

saveOptions(options) {
let model = this.get('model');
model.set('pivotOptions', JSON.stringify(options));
model.save();
}
}
});
26 changes: 22 additions & 4 deletions app/components/records-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import Ember from 'ember';
export default Ember.Component.extend({
fileSaver: Ember.inject.service(),
classNames: ['records-viewer'],
showRawData: false,
showTable: false,
showChart: false,
// Copy of the model
model: null,
metadata: null,
records: null,
fileName: 'results',

enableCharting: Ember.computed.not('model.isSingleRow').readOnly(),

columns: Ember.computed('records', function() {
return Ember.A(this.extractUniqueColumns(this.get('records')));
}).readOnly(),
Expand Down Expand Up @@ -98,13 +105,24 @@ export default Ember.Component.extend({
return flattened;
},

flipTo(field) {
this.set('showRawData', false);
this.set('showTable', false);
this.set('showChart', false);
this.set(field, true);
},

actions: {
showRawData() {
this.set('showTable', false);
rawDataMode() {
this.flipTo('showRawData');
},

tableMode() {
this.flipTo('showTable');
},

showTable() {
this.set('showTable', true);
chartMode() {
this.flipTo('showChart');
},

downloadAsJSON() {
Expand Down
7 changes: 0 additions & 7 deletions app/components/results-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,5 @@ export default Ember.Component.extend(PaginatedTable, {
this._super(...arguments);
this.set('table', new Table(this.get('columns')));
this.addPages(1);
},

actions: {
resultClick(result) {
this.sendAction('resultClick', result);
}
}
});

31 changes: 11 additions & 20 deletions app/initializers/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ export default {
decodedSettings = JSON.parse(decodeURIComponent(metaSettings));
}
// Merge into default settings, overriding them
let settings = { };
Ember.merge(settings, ENV.APP.SETTINGS);
Ember.merge(settings, decodedSettings);
let settings = this.deepMergeSettings(decodedSettings);

application.register('settings:main', Ember.Object.create(settings), { instantiate: false });
application.inject('service', 'settings', 'settings:main');
Expand All @@ -27,24 +25,17 @@ export default {
application.inject('model', 'settings', 'settings:main');
application.inject('controller', 'settings', 'settings:main');
application.inject('component', 'settings', 'settings:main');

let version = settings.modelVersion;
this.applyMigrations(version);
localStorage.modelVersion = version;
},

/**
* Applies any forced migrations for local storage. Currently, only wipes localStorage
* if version is greater than the stored version or if stored version is not present.
* @param {Number} version A numeric version to compare the current stored version against.
* @return {Boolean} Denoting whether local storage was modified.
*/
applyMigrations(version) {
let currentVersion = localStorage.modelVersion;
if (!currentVersion || version > currentVersion) {
localStorage.clear();
return true;
}
return false;
deepMergeSettings(overrides) {
let settings = JSON.parse(JSON.stringify(ENV.APP.SETTINGS));
Ember.$.extend(true, settings, overrides);

// Handle arrays manually
let helpLinks = [];
Ember.$.merge(helpLinks, ENV.APP.SETTINGS.helpLinks || []);
Ember.$.merge(helpLinks, overrides.helpLinks || []);
settings.helpLinks = helpLinks;
return settings;
}
};
Loading

0 comments on commit 548d675

Please sign in to comment.