Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pivoting and Charting #25

Merged
merged 9 commits into from
May 11, 2017
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