Initial commit
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var createTuplesItems = require('./create-tuples-items');
|
||||
var widgetsTypes = require('./widgets-types');
|
||||
var BodyView = require('./body-view');
|
||||
var template = require('./add-widgets.tpl');
|
||||
var renderLoading = require('builder/components/loading/render-loading');
|
||||
var TableStats = require('./tablestats.js');
|
||||
|
||||
var ENTER_KEY_CODE = 13;
|
||||
|
||||
/**
|
||||
* View to add new widgets.
|
||||
* Expected to be rendered in a modal.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components/modals/add-widgets/add-widgets-view',
|
||||
|
||||
className: 'Dialog-content Dialog-content--expanded',
|
||||
|
||||
events: {
|
||||
'click .js-continue': '_onContinue'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.userActions) throw new Error('userActions is required');
|
||||
if (!opts.modalModel) throw new Error('modalModel is required');
|
||||
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
||||
if (!opts.widgetDefinitionsCollection) throw new Error('widgetDefinitionsCollection is required');
|
||||
if (!opts.analysisDefinitionNodesCollection) throw new Error('analysisDefinitionNodesCollection is required');
|
||||
|
||||
this._userActions = opts.userActions;
|
||||
this._modalModel = opts.modalModel;
|
||||
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
|
||||
this._widgetDefinitionsCollection = opts.widgetDefinitionsCollection;
|
||||
this._analysisDefinitionNodesCollection = opts.analysisDefinitionNodesCollection;
|
||||
|
||||
this._optionsCollection = new Backbone.Collection();
|
||||
this.tableStats = new TableStats({
|
||||
configModel: opts.configModel,
|
||||
userModel: opts.userModel
|
||||
});
|
||||
|
||||
this._analysisDefinitionNodesCollection.each(function (analysisDefinitionNode) {
|
||||
analysisDefinitionNode = analysisDefinitionNode.querySchemaModel;
|
||||
analysisDefinitionNode.on('change', this._onQuerySchemaChange, this);
|
||||
this.add_related_model(analysisDefinitionNode);
|
||||
analysisDefinitionNode.fetch();
|
||||
}, this);
|
||||
|
||||
this.listenTo(this._optionsCollection, 'change:selected', this._updateContinueButtonState);
|
||||
|
||||
this.add_related_model(this._optionsCollection);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(template());
|
||||
|
||||
if (this._hasFetchedAllQuerySchemas()) {
|
||||
this._renderBodyView();
|
||||
} else {
|
||||
this._renderLoadingView();
|
||||
}
|
||||
|
||||
this._updateContinueButtonState();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
$(document).bind('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
_disableBinds: function () {
|
||||
$(document).unbind('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
_onKeyDown: function (event) {
|
||||
event.preventDefault();
|
||||
if (event.which === ENTER_KEY_CODE) {
|
||||
this._onContinue();
|
||||
}
|
||||
},
|
||||
|
||||
_onQuerySchemaChange: function () {
|
||||
if (this._hasFetchedAllQuerySchemas()) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
_hasFetchedAllQuerySchemas: function () {
|
||||
return this._analysisDefinitionNodesCollection.all(function (analysisDefinitionNode) {
|
||||
return analysisDefinitionNode.querySchemaModel.get('status') !== 'fetching';
|
||||
});
|
||||
},
|
||||
|
||||
_onContinue: function () {
|
||||
var self = this;
|
||||
var selectedOptionModels = this._optionsCollection.filter(this._isSelected);
|
||||
|
||||
if (selectedOptionModels.length > 0) {
|
||||
this._saveSelectedWidgets(selectedOptionModels)
|
||||
.then(function (widgets) {
|
||||
self._userActions.goToEditWidget(widgets);
|
||||
self._userActions.updateWidgetsOrder(selectedOptionModels);
|
||||
})
|
||||
.catch(function () {
|
||||
self._userActions.updateWidgetsOrder(selectedOptionModels);
|
||||
});
|
||||
|
||||
this._modalModel.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
_saveSelectedWidgets: function (selectedOptionModels) {
|
||||
var saveWidgetOptionsPromises = [];
|
||||
|
||||
_.map(selectedOptionModels, function (selectedOptionModel) {
|
||||
saveWidgetOptionsPromises.push(this._userActions.saveWidgetOption(selectedOptionModel));
|
||||
}, this);
|
||||
|
||||
return Promise.all(saveWidgetOptionsPromises);
|
||||
},
|
||||
|
||||
_renderBodyView: function () {
|
||||
this._createOptionsModels();
|
||||
var view = new BodyView({
|
||||
el: this._$body(),
|
||||
optionsCollection: this._optionsCollection,
|
||||
widgetsTypes: widgetsTypes
|
||||
});
|
||||
this.addView(view.render());
|
||||
},
|
||||
|
||||
_renderLoadingView: function () {
|
||||
this._$body().html(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-widgets.loading-title')
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_$body: function () {
|
||||
return this.$('.js-body');
|
||||
},
|
||||
|
||||
_updateContinueButtonState: function () {
|
||||
this.$('.js-continue').toggleClass('is-disabled', !this._optionsCollection.any(this._isSelected));
|
||||
},
|
||||
|
||||
_isSelected: function (model) {
|
||||
return !!model.get('selected');
|
||||
},
|
||||
|
||||
_createOptionsModels: function () {
|
||||
var self = this;
|
||||
this._optionsCollection.reset();
|
||||
var tuplesItems = createTuplesItems(this._analysisDefinitionNodesCollection, this._layerDefinitionsCollection);
|
||||
|
||||
_.each(widgetsTypes, function (widgetType) {
|
||||
var models = widgetType.createOptionModels(tuplesItems, this._widgetDefinitionsCollection);
|
||||
models = models.map(function (model) { model.stats = self.tableStats; return model; });
|
||||
this._optionsCollection.add(models);
|
||||
}, this);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._disableBinds();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<div class="Modal">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-headerContainer">
|
||||
<h2 class="CDB-Text CDB-Size-huge is-light u-mainTextColor u-bSpace"><%- _t('components.modals.add-widgets.modal-title') %></h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-widgets.modal-desc') %></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Modal-container js-body"></div>
|
||||
|
||||
<div class="Modal-footer">
|
||||
<div class="Modal-footerContainer u-flex u-justifyEnd">
|
||||
<button class="CDB-Button CDB-Button--primary is-disabled js-continue">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-widgets.continue-btn') %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var createTemplateTabPane = require('builder/components/tab-pane/create-template-tab-pane');
|
||||
var tabPaneButtonTemplate = require('./tab-pane-button-template.tpl');
|
||||
var tabPaneTemplate = require('./tab-pane-template.tpl');
|
||||
|
||||
/**
|
||||
* View to select widget options to create.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components/modals/add-widgets/body-view',
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.optionsCollection) throw new Error('optionsCollection is required');
|
||||
if (!opts.widgetsTypes) throw new Error('widgetsTypes is required');
|
||||
|
||||
// Only render tab items for types that have option models available
|
||||
var availableTypes = _.unique(opts.optionsCollection.pluck('type'));
|
||||
this._tabPaneItems = _
|
||||
.reduce(opts.widgetsTypes, function (memo, d) {
|
||||
if (_.contains(availableTypes, d.type)) {
|
||||
var tabPaneItem = d.createTabPaneItem(opts.optionsCollection);
|
||||
memo.push(tabPaneItem);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
var options = {
|
||||
tabPaneOptions: {
|
||||
template: tabPaneTemplate,
|
||||
tabPaneItemOptions: {
|
||||
tagName: 'li',
|
||||
klassName: 'CDB-NavMenu-item'
|
||||
}
|
||||
},
|
||||
tabPaneTemplateOptions: {
|
||||
tagName: 'button',
|
||||
className: 'CDB-NavMenu-link u-upperCase',
|
||||
template: tabPaneButtonTemplate
|
||||
}
|
||||
};
|
||||
|
||||
var view = createTemplateTabPane(this._tabPaneItems, options);
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<p class="Widget-categoryFakeText"></p>
|
||||
<div class="Widget-categoryFake"></div>
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||||
var WidgetDefinitionModel = require('builder/data/widget-definition-model');
|
||||
|
||||
var CATEGORY_TYPE = 'category';
|
||||
|
||||
module.exports = WidgetOptionModel.extend({
|
||||
defaults: _.defaults({type: CATEGORY_TYPE}, WidgetOptionModel.defaults),
|
||||
|
||||
save: function (widgetDefinitionsCollection) {
|
||||
var model = this;
|
||||
var columnName = this.columnName();
|
||||
var styleModel = this.layerDefinitionModel() && this.layerDefinitionModel().styleModel;
|
||||
var isAllowed = styleModel && styleModel.canApplyAutoStyle() || false;
|
||||
|
||||
var attrs = {
|
||||
type: CATEGORY_TYPE,
|
||||
layer_id: this.layerDefinitionModel().id,
|
||||
source: {
|
||||
id: this.analysisDefinitionNodeModel().id
|
||||
},
|
||||
options: {
|
||||
column: columnName,
|
||||
aggregation_column: columnName,
|
||||
aggregation: this.get('aggregation'),
|
||||
title: this.get('title')
|
||||
},
|
||||
style: {
|
||||
widget_style: {
|
||||
definition: WidgetDefinitionModel.getDefaultWidgetStyle(CATEGORY_TYPE)
|
||||
},
|
||||
auto_style: {
|
||||
allowed: isAllowed,
|
||||
custom: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return widgetDefinitionsCollection.addWidget(model, attrs);
|
||||
}
|
||||
});
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var LayerSelectorView = require('builder/components/modals/add-widgets/layer-selector-view');
|
||||
var template = require('./category-option.tpl');
|
||||
var categoryFake = require('./category-fake.tpl');
|
||||
|
||||
/**
|
||||
* View for an individual category option.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components/modals/add-widgets/category/category-option-view',
|
||||
|
||||
events: {
|
||||
'click': '_onSelect'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(this._html());
|
||||
this._renderLayerSelector();
|
||||
var tableStats = this.options.model.stats;
|
||||
if (tableStats) {
|
||||
this._drawGraph();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var isSelected = !!this.model.get('selected');
|
||||
|
||||
this.$el.toggleClass('is-selected', isSelected);
|
||||
|
||||
return template({
|
||||
columnName: this.model.columnName(),
|
||||
isSelected: isSelected
|
||||
});
|
||||
},
|
||||
|
||||
_renderLayerSelector: function () {
|
||||
var view = new LayerSelectorView({
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$('.js-inner').append(view.render().el);
|
||||
},
|
||||
|
||||
_onSelect: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
},
|
||||
|
||||
_drawGraph: function () {
|
||||
var self = this;
|
||||
this.options.model.stats.graphFor(
|
||||
this.model.analysisDefinitionNodeModel().get('table_name'),
|
||||
this.model.get('name'), function (graph) {
|
||||
if (graph.stats && graph.stats.freqs) {
|
||||
self.$('.js-Category-bar').append(graph.getCategory({
|
||||
color: '#9DE0AD',
|
||||
width: 240,
|
||||
height: 10
|
||||
}));
|
||||
var stats = self.$('.js-catstats').children();
|
||||
self.$(stats[0]).text(graph.getNullsPercentage() + '% null');
|
||||
self.$(stats[1]).text((graph.getPercentageInTopCategories() * 100).toFixed(2) + _t('components.modals.add-widgets.percentage-in-top-cats'));
|
||||
self.$('.js-catstats').css('display', 'flex');
|
||||
} else {
|
||||
self.$('.js-Category-bar').append(categoryFake());
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:layer_index', this.render);
|
||||
this.listenTo(this.model, 'change:selected', this.render);
|
||||
}
|
||||
});
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
<div class="WidgetList-option">
|
||||
<input class="CDB-Checkbox js-checkbox" type="checkbox" <% if (isSelected) { %>checked="checked"<% } %> />
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</div>
|
||||
|
||||
<div class="WidgetList-inner js-inner">
|
||||
<h3 class="u-ellipsis CDB-Text CDB-Size-large u-bSpace--m"><%- columnName %></h3>
|
||||
<ul class="js-catstats u-flex CDB-Text CDB-Size-small u-secondaryTextColor u-upperCase">
|
||||
<li class='u-rSpace--m'></li>
|
||||
<li class='u-rSpace--m'></li>
|
||||
</ul>
|
||||
<div class="u-bSpace--m js-Category-bar">
|
||||
</div>
|
||||
</div>
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CategoryOptionView = require('./category-option-view.js');
|
||||
|
||||
/**
|
||||
* View to select category widget options
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components/modals/add-widgets/category/category-options-view',
|
||||
|
||||
tagName: 'ul',
|
||||
className: 'WidgetList',
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this.collection
|
||||
.chain()
|
||||
.filter(this._isCategory)
|
||||
.each(this._renderOption, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderOption: function (m) {
|
||||
var view = new CategoryOptionView({
|
||||
tagName: 'li',
|
||||
className: 'WidgetList-item js-WidgetList-item',
|
||||
model: m
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_isCategory: function (m) {
|
||||
return m.get('type') === 'category';
|
||||
}
|
||||
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
var BLACKLISTED_COLUMNS = ['created_at', 'the_geom', 'the_geom_webmercator', 'updated_at'];
|
||||
|
||||
/**
|
||||
* Tmp data structure with column and layer-def models for each unique name+type tuple.
|
||||
* Each bucket contains a columnModel and layerDefinitionModel tuple,
|
||||
* since they are unique even if the columns' name+type happen to be the same
|
||||
* From this widget definition options for specific needs
|
||||
*
|
||||
* @param {Object} layerDefinitionsCollection
|
||||
* @return {Object} e.g.
|
||||
* e.g.
|
||||
* {
|
||||
* foobar-string: [{
|
||||
* columnModel: M({name: 'foobar', type: 'string', …}),
|
||||
* layerDefinitionModel: M({ id: 'abc-123', … })
|
||||
* }]
|
||||
* }
|
||||
*/
|
||||
module.exports = function (analysisDefinitionNodesCollection, layerDefinitionsCollection) {
|
||||
return analysisDefinitionNodesCollection
|
||||
.reduce(function (tuplesItems, nodeDefModel) {
|
||||
var layerDefinitionModel = layerDefinitionsCollection.find(function (l) { return l.isOwnerOfAnalysisNode(nodeDefModel); });
|
||||
if (!layerDefinitionModel) return tuplesItems; // e.g. if the node is one of those temporary tables
|
||||
|
||||
var querySchemaModel = nodeDefModel.querySchemaModel;
|
||||
var queryGeometryModel = nodeDefModel.queryGeometryModel;
|
||||
if (!querySchemaModel || !queryGeometryModel) return tuplesItems;
|
||||
if (!queryGeometryModel.isDone() || !queryGeometryModel.get('simple_geom')) {
|
||||
return tuplesItems;
|
||||
}
|
||||
|
||||
querySchemaModel.columnsCollection.each(function (m) {
|
||||
var columnName = m.get('name');
|
||||
|
||||
if (!_.contains(BLACKLISTED_COLUMNS, columnName)) {
|
||||
var key = columnName + '-' + m.get('type');
|
||||
var tuples = tuplesItems[key] = tuplesItems[key] || [];
|
||||
tuples.push({
|
||||
analysisDefinitionNodeModel: nodeDefModel,
|
||||
layerDefinitionModel: layerDefinitionModel,
|
||||
columnModel: m
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tuplesItems;
|
||||
}, {});
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<div class='Widget-formulaFakeContainer'>
|
||||
<p class='Widget-categoryFakeText'></p>
|
||||
<div class='Widget-formulaFake'></div>
|
||||
</div>
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||||
|
||||
var FORMULA_TYPE = 'formula';
|
||||
|
||||
module.exports = WidgetOptionModel.extend({
|
||||
defaults: _.defaults({type: FORMULA_TYPE}, WidgetOptionModel.defaults),
|
||||
|
||||
save: function (widgetDefinitionsCollection) {
|
||||
var model = this;
|
||||
var columnName = this.columnName();
|
||||
|
||||
var attrs = {
|
||||
type: FORMULA_TYPE,
|
||||
layer_id: this.layerDefinitionModel().id,
|
||||
source: {
|
||||
id: this.analysisDefinitionNodeModel().id
|
||||
},
|
||||
options: {
|
||||
column: columnName,
|
||||
title: this.get('title'),
|
||||
operation: this.get('operation')
|
||||
}
|
||||
};
|
||||
|
||||
return widgetDefinitionsCollection.addWidget(model, attrs);
|
||||
}
|
||||
});
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var LayerSelectorView = require('builder/components/modals/add-widgets/layer-selector-view');
|
||||
var template = require('./formula-option.tpl');
|
||||
var formulaFake = require('./formula-fake.tpl');
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* View for an individual formula option.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click': '_onSelect'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.listenTo(this.model, 'change:layer_index', this.render);
|
||||
this.listenTo(this.model, 'change:selected', this.render);
|
||||
this.aggregation = this.model.get('operation');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(this._html());
|
||||
this._renderLayerSelector();
|
||||
var tableStats = this.options.model.stats;
|
||||
if (tableStats) {
|
||||
this._drawGraph();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var isSelected = !!this.model.get('selected');
|
||||
|
||||
this.$el.toggleClass('is-selected', isSelected);
|
||||
|
||||
return template({
|
||||
columnName: this.model.get('title'),
|
||||
isSelected: isSelected
|
||||
});
|
||||
},
|
||||
|
||||
_renderLayerSelector: function () {
|
||||
var view = new LayerSelectorView({
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$('.js-inner').append(view.render().el);
|
||||
},
|
||||
|
||||
_onSelect: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
},
|
||||
|
||||
_drawGraph: function () {
|
||||
var self = this;
|
||||
this.options.model.stats.graphFor(
|
||||
this.model.analysisDefinitionNodeModel().get('table_name'),
|
||||
this.model.get('name'), function (graph) {
|
||||
if (graph.stats) {
|
||||
var stats = self.$('.js-formulastats').children();
|
||||
var aggregation = graph[{ // eslint-disable-line
|
||||
'avg': 'getAverage',
|
||||
'sum': 'getSum',
|
||||
'min': 'getMin',
|
||||
'max': 'getMax',
|
||||
'count': 'getCount'
|
||||
}[self.aggregation]]();
|
||||
if (_.isNumber(aggregation)) {
|
||||
self.$(stats[0]).text(graph.getNullsPercentage() + '% null');
|
||||
if (self.aggregation === 'count') {
|
||||
self.$(stats[1]).text(aggregation.toFixed().toString());
|
||||
} else {
|
||||
self.$(stats[1]).text(aggregation.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','));
|
||||
}
|
||||
self.$('.js-formulastats').show();
|
||||
} else {
|
||||
self.$('.js-formulastats').append(formulaFake());
|
||||
self.$('.js-formulastats').show();
|
||||
}
|
||||
} else {
|
||||
self.$('.js-formulastats').append(formulaFake());
|
||||
self.$('.js-formulastats').show();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<div class="WidgetList-option">
|
||||
<input class="CDB-Checkbox js-checkbox" type="checkbox" <% if (isSelected) { %>checked="checked"<% } %> />
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</div>
|
||||
|
||||
<div class="WidgetList-inner js-inner">
|
||||
<h3 class="u-ellipsis CDB-Text CDB-Size-large u-bSpace--m"><%- columnName %></h3>
|
||||
<div class="js-formulastats" style="display: none;">
|
||||
<ul class="u-flex CDB-Text CDB-Size-small u-secondaryTextColor u-upperCase">
|
||||
<li class='u-rSpace'></li>
|
||||
</ul>
|
||||
<h4 class="CDB-Text CDB-Size-huge u-bSpace--m"></h4>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var FormulaOptionView = require('./formula-option-view.js');
|
||||
|
||||
/**
|
||||
* View to select formula widget options
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'WidgetList',
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this.collection
|
||||
.chain()
|
||||
.filter(this._isFormula)
|
||||
.each(this._renderOption, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderOption: function (m) {
|
||||
var view = new FormulaOptionView({
|
||||
className: 'WidgetList-item js-WidgetList-item',
|
||||
model: m
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_isFormula: function (m) {
|
||||
return m.get('type') === 'formula';
|
||||
}
|
||||
|
||||
});
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
<div class="Widget-histogramFakeContainer">
|
||||
<p class="Widget-categoryFakeText"></p>
|
||||
<div class="Widget-histogramFake"></div>
|
||||
</div>
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||||
var WidgetDefinitionModel = require('builder/data/widget-definition-model');
|
||||
|
||||
var HISTOGRAM_TYPE = 'histogram';
|
||||
|
||||
module.exports = WidgetOptionModel.extend({
|
||||
defaults: _.defaults({type: HISTOGRAM_TYPE}, WidgetOptionModel.defaults),
|
||||
|
||||
save: function (widgetDefinitionsCollection) {
|
||||
var model = this;
|
||||
var columnName = this.columnName();
|
||||
var styleModel = this.layerDefinitionModel() && this.layerDefinitionModel().styleModel;
|
||||
var isAllowed = styleModel && styleModel.canApplyAutoStyle() || false;
|
||||
|
||||
var attrs = {
|
||||
type: HISTOGRAM_TYPE,
|
||||
layer_id: this.layerDefinitionModel().id,
|
||||
source: {
|
||||
id: this.analysisDefinitionNodeModel().id
|
||||
},
|
||||
options: {
|
||||
column: columnName,
|
||||
title: this.get('title'),
|
||||
bins: this.get('bins')
|
||||
},
|
||||
style: {
|
||||
widget_style: {
|
||||
definition: WidgetDefinitionModel.getDefaultWidgetStyle(HISTOGRAM_TYPE)
|
||||
},
|
||||
auto_style: {
|
||||
allowed: isAllowed,
|
||||
custom: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return widgetDefinitionsCollection.addWidget(model, attrs);
|
||||
}
|
||||
});
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var LayerSelectorView = require('builder/components/modals/add-widgets/layer-selector-view');
|
||||
var template = require('./histogram-option.tpl');
|
||||
var histogramFake = require('./histogram-fake.tpl');
|
||||
|
||||
/**
|
||||
* View for an individual histogram option.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click': '_onSelect'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.listenTo(this.model, 'change:layer_index', this.render);
|
||||
this.listenTo(this.model, 'change:selected', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(this._html());
|
||||
this._renderLayerSelector();
|
||||
var tableStats = this.options.model.stats;
|
||||
if (tableStats) {
|
||||
this._drawGraph();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var isSelected = !!this.model.get('selected');
|
||||
|
||||
this.$el.toggleClass('is-selected', isSelected);
|
||||
|
||||
return template({
|
||||
columnName: this.model.columnName(),
|
||||
isSelected: isSelected
|
||||
});
|
||||
},
|
||||
|
||||
_renderLayerSelector: function () {
|
||||
var view = new LayerSelectorView({
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$('.js-inner').append(view.render().el);
|
||||
},
|
||||
|
||||
_onSelect: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
},
|
||||
|
||||
_drawGraph: function () {
|
||||
var self = this;
|
||||
this.options.model.stats.graphFor(
|
||||
this.model.analysisDefinitionNodeModel().get('table_name'),
|
||||
this.model.get('name'), function (graph) {
|
||||
if (graph.stats) {
|
||||
self.$('.js-Histogram').append(graph.getHistogram({
|
||||
color: '#9DE0AD',
|
||||
width: 240,
|
||||
height: 20,
|
||||
bins: 20
|
||||
}));
|
||||
var stats = self.$('.js-histstats').children();
|
||||
self.$(stats[0]).text(graph.getNullsPercentage() + '% null');
|
||||
self.$('.js-histstats').css('display', 'flex');
|
||||
} else {
|
||||
self.$('.js-Histogram').append(histogramFake());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
<div class="WidgetList-option">
|
||||
<input class="CDB-Checkbox js-checkbox" type="checkbox" <% if (isSelected) { %>checked="checked"<% } %> />
|
||||
<span class="u-iBlock CDB-Checkbox-face"></span>
|
||||
</div>
|
||||
|
||||
<div class="WidgetList-inner js-inner">
|
||||
<h3 class="u-ellipsis CDB-Text CDB-Size-large u-bSpace--m"><%- columnName %></h3>
|
||||
<ul class="js-histstats u-flex CDB-Text CDB-Size-small u-upperCase" style="display: none;">
|
||||
<li class='u-rSpace'></li>
|
||||
</ul>
|
||||
<div class="u-tSpace--m u-bSpace--m js-Histogram"></div>
|
||||
</div>
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var HistogramOptionView = require('./histogram-option-view.js');
|
||||
|
||||
/**
|
||||
* View to select histogram widget options
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'WidgetList',
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
this.collection
|
||||
.chain()
|
||||
.filter(this._isHistogram)
|
||||
.each(this._renderOption, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderOption: function (m) {
|
||||
var view = new HistogramOptionView({
|
||||
className: 'WidgetList-item js-WidgetList-item',
|
||||
model: m
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_isHistogram: function (m) {
|
||||
return m.get('type') === 'histogram';
|
||||
}
|
||||
|
||||
});
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var analyses = require('builder/data/analyses');
|
||||
require('builder/components/form-components/index');
|
||||
var CustomListItemView = require('builder/components/form-components/editors/select/select-layer-list-item-view');
|
||||
var itemListTemplate = require('builder/components/form-components/editors/select/select-layer-item.tpl');
|
||||
var selectedItemTemplate = require('builder/components/form-components/editors/select/select-layer-list-item.tpl');
|
||||
|
||||
/**
|
||||
* View for selecting the layer through which to load the column data.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'CDB-SelectorLayer',
|
||||
|
||||
events: {
|
||||
'click': '_onClick',
|
||||
'change': '_onChange'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this._stateModel = new Backbone.Model({
|
||||
highlighted: false
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._unbindEvents();
|
||||
this._initViews();
|
||||
this._bindEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._stateModel, 'change:highlighted', this._toggleHover);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var self = this;
|
||||
var options = this._buildOptions();
|
||||
|
||||
this._selectView = new Backbone.Form.editors.Select({
|
||||
className: 'Widget-select u-flex u-alignCenter',
|
||||
key: 'name',
|
||||
schema: {
|
||||
options: options
|
||||
},
|
||||
model: self.model,
|
||||
showSearch: false,
|
||||
template: require('./select.tpl'),
|
||||
selectedItemTemplate: selectedItemTemplate,
|
||||
customListItemView: CustomListItemView,
|
||||
itemListTemplate: itemListTemplate,
|
||||
mouseOverAction: this._onMouseOver.bind(this),
|
||||
mouseOutAction: this._onMouseOut.bind(this)
|
||||
});
|
||||
|
||||
this._selectView.setValue(this.model.get('layer_index') || 0);
|
||||
this.$el.html(this._selectView.render().el);
|
||||
},
|
||||
|
||||
_onMouseOver: function () {
|
||||
this._stateModel.set('highlighted', true);
|
||||
},
|
||||
|
||||
_onMouseOut: function () {
|
||||
this._stateModel.set('highlighted', false);
|
||||
},
|
||||
|
||||
_toggleHover: function () {
|
||||
var $widget = this.$el.closest('.js-WidgetList-item');
|
||||
|
||||
$widget.toggleClass('is-hover', this._stateModel.get('highlighted'));
|
||||
},
|
||||
|
||||
_buildOptions: function () {
|
||||
return _.map(this.model.get('tuples'), function (item, index) {
|
||||
var layerDefModel = item.layerDefinitionModel;
|
||||
var nodeDefModel = item.analysisDefinitionNodeModel;
|
||||
var layerName = nodeDefModel.isSourceType()
|
||||
? layerDefModel.getTableName()
|
||||
: layerDefModel.getName();
|
||||
|
||||
return {
|
||||
val: index,
|
||||
layerName: layerName,
|
||||
nodeTitle: analyses.short_title(nodeDefModel),
|
||||
layer_id: nodeDefModel.id,
|
||||
color: layerDefModel.getColor(),
|
||||
isSourceType: nodeDefModel.isSourceType()
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
_bindEvents: function () {
|
||||
if (this._selectView) {
|
||||
this._selectView.on('change', this._onChange, this);
|
||||
}
|
||||
},
|
||||
|
||||
_unbindEvents: function () {
|
||||
if (this._selectView) {
|
||||
this._selectView.off('change', this._onChange, this);
|
||||
}
|
||||
},
|
||||
|
||||
_onClick: function (event) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
this.model.set('layer_index', this._selectView.getValue());
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._selectView.remove();
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
|
||||
});
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<% layerNames.forEach(function (name, i) { %>
|
||||
<option value="<%- i %>" <% if (i === layerIndex) { %>selected="selected"<% } %>>
|
||||
<%- name %>
|
||||
</option>
|
||||
<% }) %>
|
||||
@@ -0,0 +1,20 @@
|
||||
<p class="CDB-Text CDB-Size-small u-upperCase u-altTextColor Widget-selectTitle">Select Layer</p>
|
||||
<div class="CDB-InputText CDB-Text is-cursor js-button u-ellipsis
|
||||
<% if (isDisabled) { %> is-disabled <% } %>
|
||||
<% if (!label) { %> is-empty <% } %>"
|
||||
tabindex="0">
|
||||
<% if (isLoading) { %>
|
||||
<div class="CDB-LoaderIcon CDB-LoaderIcon--small is-dark">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewBox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="u-lSpace u-secondaryTextColor"><%- _t('components.backbone-forms.select.loading') %></span>
|
||||
<% } else { %>
|
||||
<% if (isEmpty) { %>
|
||||
<%- _t('components.backbone-forms.select.empty') %>
|
||||
<% } else { %>
|
||||
<%- label || _t('components.backbone-forms.select.placeholder', { keyAttr: keyAttr }) %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
<%- label %>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<div class="Modal-navigation">
|
||||
<ul class="Modal-navigationInner CDB-Text is-semibold CDB-Size-medium js-menu"></ul>
|
||||
</div>
|
||||
<div class="Modal-inner Modal-inner--with-navigation js-content">
|
||||
</div>
|
||||
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
var CDB = require('internal-carto.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
function TableStats (opts) {
|
||||
if (opts.user && opts.api_key) {
|
||||
this.user = opts.username;
|
||||
this.api_key = opts.api_key;
|
||||
} else if (opts.configModel) {
|
||||
this.configModel = opts.configModel;
|
||||
this.userModel = opts.userModel;
|
||||
}
|
||||
this.tables = {};
|
||||
this.queue = {};
|
||||
}
|
||||
|
||||
TableStats.prototype = {
|
||||
|
||||
graphFor: function (tableName, column, callback) {
|
||||
this._getPgStats(tableName, function (stats) {
|
||||
var graph = new ColumnGraph(stats[column]);
|
||||
callback(graph);
|
||||
});
|
||||
},
|
||||
|
||||
_getPgStats: function (table, callback) {
|
||||
var userModel = this.userModel;
|
||||
var self = this;
|
||||
if (this.tables[table]) {
|
||||
if (!_.isEmpty(this.tables[table])) {
|
||||
callback(this.tables[table]);
|
||||
} else {
|
||||
this.queue[table].push(callback);
|
||||
}
|
||||
} else {
|
||||
this.tables[table] = {};
|
||||
this.queue[table] = [callback];
|
||||
|
||||
var schema = userModel.getSchemaName();
|
||||
var sql = new CDB.SQL({
|
||||
user: this.configModel.get('user_name'),
|
||||
sql_api_template: this.configModel.get('sql_api_template'),
|
||||
api_key: this.configModel.get('api_key')
|
||||
});
|
||||
|
||||
sql.execute(
|
||||
'with a as (select reltuples from pg_class where relname = \'' + table + '\'), b as (select * from pg_stats where tablename = \'' + table + '\' and schemaname = \'' + schema + '\') select * from a,b',
|
||||
null,
|
||||
{
|
||||
rows_per_page: 40,
|
||||
page: 0
|
||||
}
|
||||
).done(function (data) {
|
||||
data = data || {};
|
||||
var rows = data.rows.map(function (r) {
|
||||
var count = r.reltuples;
|
||||
var bounds = self._reformatToJSON(r.histogram_bounds);
|
||||
if (bounds) {
|
||||
var avgs = bounds.reduce(function (p, c, i) {
|
||||
if (i < bounds.length - 1) {
|
||||
p.push((bounds[i + 1] + c) / 2);
|
||||
}
|
||||
return p;
|
||||
}, []);
|
||||
var binsize = count / (bounds.length - 1);
|
||||
var sum = avgs.reduce(function (p, c) {
|
||||
return p + c * binsize;
|
||||
}, 0);
|
||||
var average = sum / count;
|
||||
var min = bounds[0];
|
||||
var max = _.last(bounds);
|
||||
}
|
||||
var mostCommon = self._reformatToJSON(r.most_common_vals);
|
||||
var trues;
|
||||
if (mostCommon) {
|
||||
var trueIndex = mostCommon.indexOf('t');
|
||||
var falseIndex = mostCommon.indexOf('f');
|
||||
if (trueIndex > -1 && falseIndex > -1) {
|
||||
trues = r.most_common_freqs[trueIndex];
|
||||
}
|
||||
}
|
||||
return {
|
||||
column: r.attname,
|
||||
histogram_bounds: bounds,
|
||||
freqs: r.most_common_freqs,
|
||||
mostcommon: mostCommon,
|
||||
nulls: r.null_frac,
|
||||
count: count,
|
||||
sum: sum,
|
||||
min: min,
|
||||
max: max,
|
||||
avg: average,
|
||||
trues: trues
|
||||
};
|
||||
});
|
||||
|
||||
var stats = rows.reduce(function (p, c) {
|
||||
var column = c.column;
|
||||
delete c.column;
|
||||
p[column] = c;
|
||||
return p;
|
||||
}, {});
|
||||
self.tables[table] = stats;
|
||||
self._processQueue(table);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_processQueue: function (table) {
|
||||
var self = this;
|
||||
this.queue[table].forEach(function (callback) {
|
||||
callback(self.tables[table]);
|
||||
});
|
||||
},
|
||||
|
||||
_reformatToJSON: function (sqlData) {
|
||||
if (!sqlData) return null;
|
||||
sqlData = sqlData.replace('{', '').replace('}', '').split(',');
|
||||
if (!sqlData.some(isNaN)) {
|
||||
sqlData = sqlData.map(function (n) {
|
||||
return parseInt(n, 10);
|
||||
});
|
||||
}
|
||||
return sqlData;
|
||||
}
|
||||
};
|
||||
|
||||
function ColumnGraph (stats, options) {
|
||||
options = options || {};
|
||||
this.stats = stats;
|
||||
this.normalize = typeof options.normalize === 'undefined' ? true : options.normalize;
|
||||
}
|
||||
|
||||
ColumnGraph.prototype = {
|
||||
getNullsPercentage: function () {
|
||||
return this.stats.nulls;
|
||||
},
|
||||
|
||||
getPercentageInTopCategories: function (topx) {
|
||||
return this.stats.freqs.slice(0, topx || 10).reduce(function (p, c) { return p + c; });
|
||||
},
|
||||
|
||||
getTrues: function () {
|
||||
return this.stats.trues;
|
||||
},
|
||||
|
||||
getHistogram: function (options) {
|
||||
return this._generateHistogram(options.width, options.height, options.color, options.bins);
|
||||
},
|
||||
|
||||
getCategory: function (options) {
|
||||
return this._generateCategory(options.width, options.height, options.color);
|
||||
},
|
||||
|
||||
getMin: function () {
|
||||
return this.stats.min;
|
||||
},
|
||||
|
||||
getMax: function () {
|
||||
return this.stats.max;
|
||||
},
|
||||
|
||||
getAverage: function () {
|
||||
return this.stats.avg;
|
||||
},
|
||||
|
||||
getCount: function () {
|
||||
return this.stats.count;
|
||||
},
|
||||
|
||||
getSum: function () {
|
||||
return this.stats.sum;
|
||||
},
|
||||
|
||||
_generateCategory: function (width, height, color) {
|
||||
if (!this.stats.freqs) return;
|
||||
var canvas = document.createElement('canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
var proportion;
|
||||
if (_.isNumber(this.getTrues())) {
|
||||
proportion = this.getTrues();
|
||||
} else {
|
||||
proportion = this.getPercentageInTopCategories();
|
||||
}
|
||||
canvas.height = height || 10;
|
||||
canvas.width = width || 400;
|
||||
var spacing = 2;
|
||||
var begin = spacing;
|
||||
var end = canvas.width - spacing * 2;
|
||||
var greenPosition = (end - begin) * proportion + begin;
|
||||
context.lineCap = 'round';
|
||||
context.strokeStyle = '#EDEDED';
|
||||
context.lineWidth = 4;
|
||||
context.beginPath();
|
||||
context.moveTo(begin, canvas.height / 2);
|
||||
context.lineTo(end, canvas.height / 2);
|
||||
context.stroke();
|
||||
context.strokeStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(begin, canvas.height / 2);
|
||||
context.lineTo(greenPosition, canvas.height / 2);
|
||||
context.stroke();
|
||||
return canvas;
|
||||
},
|
||||
|
||||
_generateHistogram: function (width, height, color, bins) {
|
||||
var histogram;
|
||||
|
||||
if (_.isEmpty(this.stats)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stats.freqs && this.stats.freqs.length < 60) {
|
||||
histogram = this._getHistogramFromFreqs(bins);
|
||||
} else if (this.stats.histogram_bounds) {
|
||||
histogram = this._getHistogramFromBounds(bins);
|
||||
}
|
||||
|
||||
if (!histogram || _.isEmpty(histogram)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.normalize) {
|
||||
var max = _.max(histogram);
|
||||
histogram = histogram.map(function (bin) {
|
||||
return bin / max;
|
||||
});
|
||||
}
|
||||
|
||||
bins = histogram.length;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
canvas.height = height || 90;
|
||||
canvas.width = width || 400;
|
||||
|
||||
var barSeparation = 2;
|
||||
var barWidth = (canvas.width - barSeparation * (bins - 1)) / bins;
|
||||
context.strokeStyle = '#EDEDED';
|
||||
context.lineWidth = 1;
|
||||
|
||||
// Guides
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(0, 0);
|
||||
context.lineTo(canvas.width, 0);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height / 2);
|
||||
context.lineTo(canvas.width, canvas.height / 2);
|
||||
context.stroke();
|
||||
|
||||
// Bars
|
||||
|
||||
histogram.forEach(function (bar, index) {
|
||||
var x = index * (barWidth + barSeparation);
|
||||
var barHeight = canvas.height * bar;
|
||||
context.beginPath();
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||
});
|
||||
|
||||
// Base line
|
||||
|
||||
context.strokeStyle = '#A1BBA7';
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height);
|
||||
context.lineTo(canvas.width, canvas.height);
|
||||
context.stroke();
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
||||
_getHistogramFromBounds: function (bins) {
|
||||
var bounds = this.stats.histogram_bounds;
|
||||
bounds = bounds.sort(function (a, b) { return a - b; });
|
||||
var min = bounds[0];
|
||||
var max = bounds[bounds.length - 1];
|
||||
var width = (max - min) / bins;
|
||||
var histogram = new Array(bins + 1).join('0').split('').map(parseFloat);
|
||||
function scaleBin (value) {
|
||||
if (max === min) return min;
|
||||
return Math.floor((bins - 1) * (value - min) / (max - min));
|
||||
}
|
||||
|
||||
var boundProportion = 1 / (bounds.length - 1);
|
||||
for (var i = 0; i < bounds.length - 1; ++i) {
|
||||
var binMin = scaleBin(bounds[i]);
|
||||
var binMax = scaleBin(bounds[i + 1]);
|
||||
if (binMin === binMax) {
|
||||
histogram[binMin] += boundProportion;
|
||||
} else {
|
||||
var proportionForFirstBin = ((min + width * (binMin + 1)) - bounds[i]) / (bounds[i + 1] - bounds[i]);
|
||||
histogram[binMin] += proportionForFirstBin * boundProportion;
|
||||
var proportionForLastBin = (bounds[i + 1] - (min + width * (binMax))) / (bounds[i + 1] - bounds[i]);
|
||||
histogram[binMax] += proportionForLastBin * boundProportion;
|
||||
var remaining = boundProportion - proportionForFirstBin - proportionForLastBin;
|
||||
_.range(binMin + 1, binMax).forEach(function (binIndex, i, array) {
|
||||
histogram[binIndex] = remaining / array.length;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return histogram;
|
||||
},
|
||||
|
||||
_getHistogramFromFreqs: function (bins) {
|
||||
var values = this.stats.mostcommon;
|
||||
var freqs = this.stats.freqs;
|
||||
bins = Math.min(bins, values.length);
|
||||
var min = _.min(values);
|
||||
var max = _.max(values);
|
||||
|
||||
// If there is only one value, histogram should not be rendered
|
||||
if (min === max) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var histogram = new Array(bins + 1).join('0').split('').map(parseFloat);
|
||||
function scale (value) {
|
||||
if (max === min) return min;
|
||||
return (bins - 1) * (value - min) / (max - min);
|
||||
}
|
||||
values.forEach(function (v, i) {
|
||||
histogram[Math.floor(scale(v))] += freqs[i];
|
||||
});
|
||||
return histogram;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = TableStats;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||||
|
||||
/**
|
||||
* Special case for the time-series type, when if selected it deletes an existing time-series if there is one,
|
||||
* otherwise it does nothing.
|
||||
*/
|
||||
module.exports = WidgetOptionModel.extend({
|
||||
|
||||
defaults: _.defaults({type: 'time-series'}, WidgetOptionModel.defaults),
|
||||
|
||||
save: function (widgetDefinitionsCollection) {
|
||||
var m = widgetDefinitionsCollection.find(this._isTimesSeries);
|
||||
if (m) {
|
||||
m.destroy({ wait: true });
|
||||
}
|
||||
},
|
||||
|
||||
_isTimesSeries: function (m) {
|
||||
return m.get('type') === 'time-series';
|
||||
}
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./time-series-none-option.tpl');
|
||||
|
||||
/**
|
||||
* Represents the (default) no-option view, i.e. since time-series options only can have one selection,
|
||||
* this serves as the "unselect" option.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click': '_onSelect'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.listenTo(this.model, 'change:selected', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this._html());
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var isSelected = !!this.model.get('selected');
|
||||
|
||||
this.$el.toggleClass('is-selected', isSelected);
|
||||
|
||||
return template({
|
||||
isSelected: isSelected
|
||||
});
|
||||
},
|
||||
|
||||
_onSelect: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
}
|
||||
|
||||
});
|
||||
lib/assets/javascripts/builder/components/modals/add-widgets/time-series/time-series-none-option.tpl
Executable
+9
@@ -0,0 +1,9 @@
|
||||
<div class="WidgetList-option">
|
||||
<input class="CDB-Radio u-iBlock js-radio" type="radio" <% if (isSelected) { %>checked="checked"<% } %> />
|
||||
<span class="u-iBlock CDB-Radio-face"></span>
|
||||
</div>
|
||||
|
||||
<div class="WidgetList-inner js-inner">
|
||||
<h3 class="WidgetList-title CDB-Text CDB-Size-large u-bSpace--xl"><%- _t('components.modals.add-widgets.time-series-no-option-title') %></h3>
|
||||
<p class="CDB-Text CDB-Size-small u-altTextColor"><%- _t('components.modals.add-widgets.time-series-no-option-desc') %></p>
|
||||
</div>
|
||||
lib/assets/javascripts/builder/components/modals/add-widgets/time-series/time-series-option-model.js
Executable
+81
@@ -0,0 +1,81 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetOptionModel = require('builder/components/modals/add-widgets/widget-option-model');
|
||||
var WidgetDefinitionModel = require('builder/data/widget-definition-model');
|
||||
|
||||
var TIME_SERIES_TYPE = 'time-series';
|
||||
|
||||
module.exports = WidgetOptionModel.extend({
|
||||
defaults: _.defaults({type: TIME_SERIES_TYPE}, WidgetOptionModel.defaults),
|
||||
|
||||
save: function (widgetDefinitionsCollection) {
|
||||
var model = this;
|
||||
var columnName = this.columnName();
|
||||
var layerId = this.layerDefinitionModel().id;
|
||||
|
||||
var attrs = {
|
||||
type: TIME_SERIES_TYPE,
|
||||
layer_id: layerId,
|
||||
source: {
|
||||
id: this.analysisDefinitionNodeModel().id
|
||||
},
|
||||
options: {
|
||||
column: columnName,
|
||||
title: this.get('title')
|
||||
},
|
||||
style: {
|
||||
widget_style: {
|
||||
definition: WidgetDefinitionModel.getDefaultWidgetStyle(TIME_SERIES_TYPE)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var attrsSave = {
|
||||
type: TIME_SERIES_TYPE,
|
||||
layer_id: layerId,
|
||||
source: this.analysisDefinitionNodeModel().id,
|
||||
column: columnName,
|
||||
title: this.get('title'),
|
||||
widget_style_definition: WidgetDefinitionModel.getDefaultWidgetStyle(TIME_SERIES_TYPE)
|
||||
};
|
||||
|
||||
// Depending on column type, widget should have bins or aggregation
|
||||
if (this.get('aggregation')) {
|
||||
attrs.options.aggregation = this.get('aggregation');
|
||||
attrsSave.aggregation = this.get('aggregation');
|
||||
} else if (this.get('bins')) {
|
||||
attrs.options.bins = this.get('bins');
|
||||
attrsSave.bins = this.get('bins');
|
||||
}
|
||||
|
||||
var successReplaceHandler = function () {
|
||||
widgetDefinitionsCollection.trigger('successReplace', model);
|
||||
};
|
||||
|
||||
var errorHandler = function (model, e) {
|
||||
widgetDefinitionsCollection.trigger('error', model, e);
|
||||
};
|
||||
|
||||
var createUpdatingNotification = function () {
|
||||
widgetDefinitionsCollection.trigger('updating', model);
|
||||
};
|
||||
|
||||
var existingModel = widgetDefinitionsCollection.find(this._isTimesSeries);
|
||||
if (existingModel) {
|
||||
// Update existing widget, but only if the column or layer differs
|
||||
if (existingModel.get('column') !== columnName || existingModel.get('layer_id') !== layerId) {
|
||||
createUpdatingNotification();
|
||||
return existingModel.save(attrsSave, {
|
||||
wait: true,
|
||||
success: successReplaceHandler,
|
||||
error: errorHandler
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return widgetDefinitionsCollection.addWidget(model, attrs);
|
||||
}
|
||||
},
|
||||
|
||||
_isTimesSeries: function (model) {
|
||||
return model.get('type') === TIME_SERIES_TYPE;
|
||||
}
|
||||
});
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var LayerSelectorView = require('builder/components/modals/add-widgets/layer-selector-view');
|
||||
var template = require('./time-series-option.tpl');
|
||||
|
||||
/**
|
||||
* View for an individual time-series option.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click': '_onSelect'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this.listenTo(this.model, 'change:layer_index', this.render);
|
||||
this.listenTo(this.model, 'change:selected', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(this._html());
|
||||
this._renderLayerSelector();
|
||||
return this;
|
||||
},
|
||||
|
||||
_html: function () {
|
||||
var isSelected = !!this.model.get('selected');
|
||||
|
||||
this.$el.toggleClass('is-selected', isSelected);
|
||||
|
||||
return template({
|
||||
columnName: this.model.columnName(),
|
||||
isSelected: isSelected
|
||||
});
|
||||
},
|
||||
|
||||
_renderLayerSelector: function () {
|
||||
var view = new LayerSelectorView({
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$('.js-inner').append(view.render().el);
|
||||
},
|
||||
|
||||
_onSelect: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
}
|
||||
|
||||
});
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
<div class="WidgetList-option">
|
||||
<input class="CDB-Radio u-iBlock js-radio" type="radio" <% if (isSelected) { %>checked="checked"<% } %> />
|
||||
<span class="u-iBlock CDB-Radio-face"></span>
|
||||
</div>
|
||||
|
||||
<div class="WidgetList-inner js-inner">
|
||||
<h3 class="u-ellipsis CDB-Text CDB-Size-large u-bSpace--m"><%- columnName %></h3>
|
||||
</div>
|
||||
lib/assets/javascripts/builder/components/modals/add-widgets/time-series/time-series-options-view.js
Executable
+56
@@ -0,0 +1,56 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var TimeSeriesOptionView = require('./time-series-option-view.js');
|
||||
var TimeSeriesNoneOptionView = require('./time-series-none-option-view.js');
|
||||
|
||||
/**
|
||||
* View to select time-series widget options
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
module: 'components/modals/add-widgets/time-series/time-series-options-view',
|
||||
|
||||
className: 'WidgetList',
|
||||
|
||||
initialize: function () {
|
||||
this.listenTo(this.collection, 'change:selected', this._onSelectedChange);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this.collection
|
||||
.chain()
|
||||
.filter(this._isTimeSeries)
|
||||
.each(this._renderOption, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderOption: function (m) {
|
||||
var ViewClass = m.has('tuples')
|
||||
? TimeSeriesOptionView
|
||||
: TimeSeriesNoneOptionView;
|
||||
|
||||
var view = new ViewClass({
|
||||
className: 'WidgetList-item js-WidgetList-item',
|
||||
model: m
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_isTimeSeries: function (m) {
|
||||
return m.get('type') === 'time-series';
|
||||
},
|
||||
|
||||
_onSelectedChange: function (selectedModel, isSelected) {
|
||||
// Make sure there can only be one selected time-series widget option
|
||||
if (isSelected) {
|
||||
this.collection.each(function (m) {
|
||||
if (this._isTimeSeries(m) && m !== selectedModel) {
|
||||
m.set('selected', false);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
layer_index: 0,
|
||||
tuples: []
|
||||
},
|
||||
|
||||
analysisDefinitionNodeModel: function () {
|
||||
return this._selectedItem().analysisDefinitionNodeModel;
|
||||
},
|
||||
|
||||
layerDefinitionModel: function () {
|
||||
return this._selectedItem().layerDefinitionModel;
|
||||
},
|
||||
|
||||
columnName: function () {
|
||||
return this._selectedItem().columnModel.get('name');
|
||||
},
|
||||
|
||||
save: function () {
|
||||
throw new Error('save should be implemented by child');
|
||||
},
|
||||
|
||||
_selectedItem: function () {
|
||||
var i = this.get('layer_index') || 0;
|
||||
var tuples = this.get('tuples') || [];
|
||||
return tuples[i] || {};
|
||||
}
|
||||
|
||||
});
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
var _ = require('underscore');
|
||||
var CategoryOptionModel = require('./category/category-option-model');
|
||||
var CategoryOptionsView = require('./category/category-options-view');
|
||||
var HistogramOptionModel = require('./histogram/histogram-option-model');
|
||||
var HistogramOptionsView = require('./histogram/histogram-options-view');
|
||||
var FormulaOptionModel = require('./formula/formula-option-model');
|
||||
var FormulaOptionsView = require('./formula/formula-options-view');
|
||||
var TimeSeriesOptionModel = require('./time-series/time-series-option-model');
|
||||
var TimeSeriesNoneOptionModel = require('./time-series/time-series-none-option-model');
|
||||
var TimeSeriesOptionsView = require('./time-series/time-series-options-view');
|
||||
var ScrollView = require('builder/components/scroll/scroll-view');
|
||||
|
||||
var CARTODB_ID = 'cartodb_id';
|
||||
|
||||
// Order is the same in how things will be presented in the UI
|
||||
module.exports = [
|
||||
{
|
||||
type: 'category',
|
||||
createOptionModels: function (tuplesItems) {
|
||||
return _.reduce(tuplesItems, function (memo, tuples) {
|
||||
var columnModel = tuples[0].columnModel;
|
||||
var type = columnModel.get('type');
|
||||
var m;
|
||||
if (type === 'string' || type === 'boolean') {
|
||||
m = new CategoryOptionModel({
|
||||
tuples: tuples,
|
||||
title: columnModel.get('name'),
|
||||
name: columnModel.get('name'),
|
||||
aggregation: 'count' // or sum
|
||||
});
|
||||
memo.push(m);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
},
|
||||
createTabPaneItem: function (optionsCollection) {
|
||||
return {
|
||||
label: _t('components.modals.add-widgets.tab-pane.category-label'),
|
||||
name: 'category',
|
||||
createContentView: function () {
|
||||
return new ScrollView({
|
||||
createContentView: function () {
|
||||
return new CategoryOptionsView({
|
||||
collection: optionsCollection
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'histogram',
|
||||
createOptionModels: function (tuplesItems) {
|
||||
return _.reduce(tuplesItems, function (memo, tuples) {
|
||||
var columnModel = tuples[0].columnModel;
|
||||
if (columnModel.get('type') === 'number') {
|
||||
var m = new HistogramOptionModel({
|
||||
tuples: tuples,
|
||||
title: columnModel.get('name'),
|
||||
name: columnModel.get('name'),
|
||||
bins: 10
|
||||
});
|
||||
memo.push(m);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
},
|
||||
createTabPaneItem: function (optionsCollection) {
|
||||
return {
|
||||
label: _t('components.modals.add-widgets.tab-pane.histogram-label'),
|
||||
name: 'histogram',
|
||||
createContentView: function () {
|
||||
return new ScrollView({
|
||||
createContentView: function () {
|
||||
return new HistogramOptionsView({
|
||||
collection: optionsCollection
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'formula',
|
||||
createOptionModels: function (tuplesItems) {
|
||||
return _.reduce(tuplesItems, function (memo, tuples) {
|
||||
var columnModel = tuples[0].columnModel;
|
||||
var columnName = columnModel.get('name');
|
||||
var operation = 'avg';
|
||||
var title = columnName;
|
||||
|
||||
if (columnModel.get('type') === 'number') {
|
||||
if (columnName === CARTODB_ID) {
|
||||
operation = 'count';
|
||||
title = _t('editor.data.stats.feature-count');
|
||||
}
|
||||
|
||||
var m = new FormulaOptionModel({
|
||||
tuples: tuples,
|
||||
title: title,
|
||||
operation: operation,
|
||||
name: columnModel.get('name')
|
||||
});
|
||||
memo.push(m);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
},
|
||||
createTabPaneItem: function (optionsCollection) {
|
||||
return {
|
||||
label: _t('components.modals.add-widgets.tab-pane.formula-label'),
|
||||
name: 'formula',
|
||||
createContentView: function () {
|
||||
return new ScrollView({
|
||||
createContentView: function () {
|
||||
return new FormulaOptionsView({
|
||||
collection: optionsCollection
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'time-series',
|
||||
createOptionModels: function (tuplesItems, widgetDefinitionsCollection) {
|
||||
var defaults = {
|
||||
bins: 256
|
||||
};
|
||||
var existingDefModel = widgetDefinitionsCollection.find(function (m) {
|
||||
return m.get('type') === 'time-series';
|
||||
});
|
||||
|
||||
var models = _.reduce(tuplesItems, function (memo, tuples) {
|
||||
var columnModel = tuples[0].columnModel;
|
||||
var columnName = columnModel.get('name');
|
||||
var columnType = columnModel.get('type');
|
||||
if (columnType === 'date' || columnType === 'number') {
|
||||
var attrs = {
|
||||
tuples: tuples,
|
||||
title: columnName
|
||||
};
|
||||
if (columnType === 'date') {
|
||||
attrs.aggregation = defaults.aggregation;
|
||||
} else if (columnType === 'number') {
|
||||
attrs.bins = defaults.bins;
|
||||
}
|
||||
|
||||
// Preselect the correct tuple, if there already exists a widget definition
|
||||
if (existingDefModel && existingDefModel.get('column') === columnName) {
|
||||
_.find(tuples, function (d, i) {
|
||||
if (d.layerDefinitionModel.id === existingDefModel.get('layer_id')) {
|
||||
attrs.selected = true;
|
||||
attrs.layer_index = i;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var m = new TimeSeriesOptionModel(attrs);
|
||||
memo.push(m);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
// Special-case; preprend the none-model since it should appear first
|
||||
var noneOptionModel = new TimeSeriesNoneOptionModel();
|
||||
return [noneOptionModel].concat(models);
|
||||
},
|
||||
createTabPaneItem: function (optionsCollection) {
|
||||
return {
|
||||
label: _t('components.modals.add-widgets.tab-pane.time-series-label'),
|
||||
name: 'time-series',
|
||||
createContentView: function () {
|
||||
return new ScrollView({
|
||||
createContentView: function () {
|
||||
return new TimeSeriesOptionsView({
|
||||
collection: optionsCollection
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user