Initial commit
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
var _ = require('underscore');
|
||||
var cdb = require('internal-carto.js');
|
||||
var Dashboard = require('deep-insights/api/dashboard.js');
|
||||
var DashboardView = require('deep-insights/dashboard-view');
|
||||
var WidgetsCollection = require('deep-insights/widgets/widgets-collection');
|
||||
var WidgetsService = require('deep-insights/widgets-service');
|
||||
var URLHelper = require('deep-insights/api/url-helper');
|
||||
|
||||
/**
|
||||
* Translates a vizJSON v3 datastructure into a working dashboard which will be rendered in given selector.
|
||||
*
|
||||
* @param {String} selector e.g. "#foobar-id", ".some-class"
|
||||
* @param {Object} vizJSON JSON datastructure
|
||||
* @param {Object} opts (Optional) flags, see 3rd param for cdb.createVis for available ones. Keys used here:
|
||||
* renderMenu: {Boolean} If true (default), render a top-level menu on the left side.
|
||||
* @return {Object} with keys:
|
||||
* dashboardView: root (backbone) view of the dashboard
|
||||
* vis: the instantiated vis map, same result as given from cdb.createVis()
|
||||
*/
|
||||
var createDashboard = function (selector, vizJSON, opts, callback) {
|
||||
var dashboardEl = document.querySelector(selector);
|
||||
if (!dashboardEl) throw new Error('no element found with selector ' + selector);
|
||||
|
||||
// Default options
|
||||
opts = opts || {};
|
||||
opts.renderMenu = _.isBoolean(opts.renderMenu)
|
||||
? opts.renderMenu
|
||||
: true;
|
||||
opts.autoStyle = _.isBoolean(opts.autoStyle)
|
||||
? opts.autoStyle
|
||||
: false;
|
||||
|
||||
var widgets = new WidgetsCollection();
|
||||
|
||||
var model = new cdb.core.Model({
|
||||
title: vizJSON.title,
|
||||
description: vizJSON.description,
|
||||
updatedAt: vizJSON.updated_at,
|
||||
userName: vizJSON.user.fullname,
|
||||
userProfileURL: vizJSON.user.profile_url,
|
||||
userAvatarURL: vizJSON.user.avatar_url,
|
||||
renderMenu: opts.renderMenu,
|
||||
autoStyle: opts.autoStyle,
|
||||
showLogo: opts.cartodb_logo,
|
||||
initialPosition: {
|
||||
bounds: vizJSON.bounds
|
||||
}
|
||||
});
|
||||
var dashboardView = new DashboardView({
|
||||
el: dashboardEl,
|
||||
widgets: widgets,
|
||||
model: model
|
||||
});
|
||||
var dashboardState = opts.state || URLHelper.getStateFromCurrentURL();
|
||||
if (dashboardState && !_.isEmpty(dashboardState.map)) {
|
||||
if (_.isArray(dashboardState.map.center)) {
|
||||
vizJSON.center = dashboardState.map.center;
|
||||
}
|
||||
if (_.isNumber(dashboardState.map.zoom)) {
|
||||
vizJSON.zoom = dashboardState.map.zoom;
|
||||
}
|
||||
if (dashboardState.map.ne && dashboardState.map.sw) {
|
||||
vizJSON.bounds = [dashboardState.map.ne, dashboardState.map.sw];
|
||||
}
|
||||
}
|
||||
|
||||
var vis = cdb.createVis(dashboardView.$('#map'), vizJSON, _.extend(opts, {
|
||||
skipMapInstantiation: true
|
||||
}));
|
||||
|
||||
vis.once('load', function (vis) {
|
||||
var widgetsState = (dashboardState && dashboardState.widgets) || {};
|
||||
|
||||
// Create widgets
|
||||
var widgetsService = new WidgetsService(widgets, vis.dataviews);
|
||||
|
||||
var widgetModelsMap = {
|
||||
formula: widgetsService.createFormulaModel.bind(widgetsService),
|
||||
histogram: widgetsService.createHistogramModel.bind(widgetsService),
|
||||
'time-series': widgetsService.createTimeSeriesModel.bind(widgetsService),
|
||||
category: widgetsService.createCategoryModel.bind(widgetsService)
|
||||
};
|
||||
vizJSON.widgets.forEach(function (widget) {
|
||||
// Flatten the data structure given in vizJSON, the widgetsService will use whatever it needs and ignore the rest
|
||||
var attrs = _.extend({}, widget, widget.options);
|
||||
var newWidgetModel = widgetModelsMap[widget.type];
|
||||
var state = widgetsState[widget.id];
|
||||
|
||||
if (_.isFunction(newWidgetModel)) {
|
||||
// Find the Layer that the Widget should be created for.
|
||||
var layer;
|
||||
var source;
|
||||
if (widget.layer_id) {
|
||||
layer = vis.map.layers.get(widget.layer_id);
|
||||
} else if (Number.isInteger(widget.layerIndex)) {
|
||||
// TODO Since namedmap doesn't have ids we need to map in another way, here using index
|
||||
// should we solve this in another way?
|
||||
layer = vis.map.layers.at(widget.layerIndex);
|
||||
}
|
||||
if (widget.source && widget.source.id) {
|
||||
source = vis.analysis.findNodeById(widget.source.id);
|
||||
attrs.source = source;
|
||||
}
|
||||
|
||||
newWidgetModel(attrs, layer, state);
|
||||
} else {
|
||||
cdb.log.error('No widget found for type ' + widget.type);
|
||||
}
|
||||
});
|
||||
|
||||
dashboardView.render();
|
||||
|
||||
var callbackObj = {
|
||||
dashboardView: dashboardView,
|
||||
widgets: widgetsService,
|
||||
areWidgetsInitialised: function () {
|
||||
var widgetsCollection = widgetsService.getCollection();
|
||||
if (widgetsCollection.size() > 0) {
|
||||
return widgetsCollection.hasInitialState();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
vis: vis
|
||||
};
|
||||
|
||||
vis.instantiateMap({
|
||||
success: function () {
|
||||
callback && callback(null, callbackObj);
|
||||
},
|
||||
error: function (errorMessage) {
|
||||
callback && callback(new Error(errorMessage), callbackObj);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function (selector, vizJSON, opts, callback) {
|
||||
var args = arguments;
|
||||
var fn = args[args.length - 1];
|
||||
|
||||
if (_.isFunction(fn)) {
|
||||
callback = fn;
|
||||
}
|
||||
|
||||
function _load (vizJSON) {
|
||||
createDashboard(selector, vizJSON, opts, function (error, dashboard) {
|
||||
var _dashboard = new Dashboard(dashboard);
|
||||
|
||||
if (opts.share_urls) {
|
||||
_dashboard.onStateChanged(
|
||||
_.debounce(
|
||||
function (state, url) {
|
||||
window.history.replaceState('Object', 'Title', url);
|
||||
},
|
||||
500
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
callback && callback(error, _dashboard);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof vizJSON === 'string') {
|
||||
cdb.core.Loader.get(vizJSON, function (data) {
|
||||
if (data) {
|
||||
_load(data, opts);
|
||||
} else {
|
||||
callback && callback(new Error('Error fetching viz.json file: ' + vizJSON));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_load(vizJSON, opts);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
var URLHelper = require('./url-helper');
|
||||
var _ = require('underscore');
|
||||
|
||||
function Dashboard (dashboard) {
|
||||
this._dashboard = dashboard;
|
||||
|
||||
this.onDataviewsFetched(function () {
|
||||
dashboard.widgets._widgetsCollection.initialState();
|
||||
dashboard.widgets._widgetsCollection.each(function (m) {
|
||||
m && m.applyInitialState && m.applyInitialState();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Dashboard.prototype = {
|
||||
|
||||
/**
|
||||
* @return {View} used in the dashboard
|
||||
*/
|
||||
getView: function () {
|
||||
return this._dashboard.dashboardView;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Map} the map used in the dashboard
|
||||
*/
|
||||
getMap: function () {
|
||||
return this._dashboard.vis;
|
||||
},
|
||||
|
||||
reloadMap: function () {
|
||||
this.getMap().reload();
|
||||
},
|
||||
|
||||
forceResize: function () {
|
||||
this._dashboard.widgets.getCollection()
|
||||
.each(function (widget) {
|
||||
widget.forceResize();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Array} of widgets in the dashboard
|
||||
*/
|
||||
getWidgets: function () {
|
||||
return this._dashboard.widgets.getList();
|
||||
},
|
||||
|
||||
getDashboardURL: function () {
|
||||
return URLHelper.getURLFromState(this.getState());
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
var state = {};
|
||||
var mapState = this.getMapState();
|
||||
if (!_.isEmpty(mapState)) state.map = mapState;
|
||||
|
||||
var widgetsCollection = this._dashboard.widgets.getCollection();
|
||||
var widgetsState = widgetsCollection.getStates();
|
||||
if (!_.isEmpty(widgetsState)) state.widgets = widgetsState;
|
||||
return state;
|
||||
},
|
||||
|
||||
getMapState: function () {
|
||||
var map = this._dashboard.vis.map;
|
||||
var currentBoundingBox = map.getViewBounds();
|
||||
var center = map.get('center');
|
||||
var zoom = map.get('zoom');
|
||||
|
||||
return {
|
||||
ne: currentBoundingBox[0],
|
||||
sw: currentBoundingBox[1],
|
||||
center: center,
|
||||
zoom: zoom
|
||||
};
|
||||
},
|
||||
|
||||
setState: function (state) {
|
||||
// todo: set map state
|
||||
this._dashboard.widgets.setWidgetsState(state.widgets);
|
||||
this._dashboard.vis.mapvis.map.setBounds([state.map.ne, state.map.sw]);
|
||||
},
|
||||
|
||||
onDataviewsFetched: function (callback) {
|
||||
var areWidgetsInitialized = this._dashboard.areWidgetsInitialised();
|
||||
if (areWidgetsInitialized) {
|
||||
callback && callback();
|
||||
} else {
|
||||
this._dashboard.vis.once('dataviewsFetched', function () {
|
||||
callback && callback();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onStateChanged: function (callback) {
|
||||
this.onDataviewsFetched(function () {
|
||||
callback && this._bindChange(callback);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_bindChange: function (callback) {
|
||||
var widgetsCollection = this._dashboard.widgets.getCollection();
|
||||
widgetsCollection.bind('change', function () {
|
||||
callback(this.getState(), this.getDashboardURL());
|
||||
}, this);
|
||||
|
||||
this._dashboard.vis.map.bind('change', function () {
|
||||
callback(this.getState(), this.getDashboardURL());
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Integer} id - widget id
|
||||
* @return a widget object
|
||||
*/
|
||||
getWidget: function (id) {
|
||||
return this._dashboard.widgets.get(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a category widget.
|
||||
* @param {Object} widgetAttrs - attributes for the new widget
|
||||
* @param {string} widgetAttrs.id - id (required)
|
||||
* @param {string} widgetAttrs.title - title (required)
|
||||
* @param {number} widgetAttrs.order - index of the widget (optional)
|
||||
* @param ...
|
||||
* @return {CategoryWidget} The new widget
|
||||
*/
|
||||
createCategoryWidget: function (widgetAttrs, layer) {
|
||||
return this._dashboard.widgets.createCategoryModel(widgetAttrs, layer, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a histogram widget
|
||||
* @param {Object} widgetAttrs - attributes for the new widget
|
||||
* @param {string} widgetAttrs.id - id (required)
|
||||
* @param {string} widgetAttrs.title - title (required)
|
||||
* @param {number} widgetAttrs.order - index of the widget (optional)
|
||||
* @param ...
|
||||
* @return {HistogramWidget} The new widget
|
||||
*/
|
||||
createHistogramWidget: function (widgetAttrs, layer) {
|
||||
return this._dashboard.widgets.createHistogramModel(widgetAttrs, layer, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a formula widget
|
||||
* @param {Object} widgetAttrs - attributes for the new widget
|
||||
* @param {string} widgetAttrs.id - id (required)
|
||||
* @param {string} widgetAttrs.title - title (required)
|
||||
* @param {number} widgetAttrs.order - index of the widget (optional)
|
||||
* @param ...
|
||||
* @return {FormulaWidget} The new widget
|
||||
*/
|
||||
createFormulaWidget: function (widgetAttrs, layer) {
|
||||
return this._dashboard.widgets.createFormulaModel(widgetAttrs, layer, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a timesier es widget
|
||||
* @param {Object} widgetAttrs - attributes for the new widget
|
||||
* @param {string} widgetAttrs.id - id (required)
|
||||
* @param {string} widgetAttrs.title - title (required)
|
||||
* @param {number} widgetAttrs.order - index of the widget (optional)
|
||||
* @param ...
|
||||
* @return {TimeSeriesWidget} The new widget
|
||||
*/
|
||||
createTimeSeriesWidget: function (widgetAttrs, layer) {
|
||||
return this._dashboard.widgets.createTimeSeriesModel(widgetAttrs, layer, null);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Dashboard;
|
||||
@@ -0,0 +1,31 @@
|
||||
var URI = require('urijs');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = {
|
||||
getURLFromState: function (state) {
|
||||
var thisURL = new URI(this.getLocalURL());
|
||||
thisURL.removeQuery('state');
|
||||
if (!_.isEmpty(state)) {
|
||||
var statesString = JSON.stringify(state);
|
||||
thisURL.setQuery('state', statesString);
|
||||
}
|
||||
return thisURL.toString();
|
||||
},
|
||||
getStateFromCurrentURL: function () {
|
||||
var currentURL = this.getLocalURL();
|
||||
return this.getStateFromURL(currentURL);
|
||||
},
|
||||
getStateFromURL: function (url) {
|
||||
var uri = new URI(url);
|
||||
var state;
|
||||
var result = {};
|
||||
if (uri.hasQuery('state')) {
|
||||
state = uri.query(true)['state'];
|
||||
if (!_.isEmpty(state)) {
|
||||
result = JSON.parse(decodeURIComponent(state));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getLocalURL: _.constant(window.location.href)
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
WIDGETS: {
|
||||
TOOLTIP_OFFSET_Y: 10
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var WidgetViewFactory = require('./widgets/widget-view-factory');
|
||||
var TimeSeriesContentView = require('./widgets/time-series/content-view');
|
||||
var TorqueTimeSeriesContentView = require('./widgets/time-series/torque-content-view');
|
||||
|
||||
var TIME_SERIES_TYPE = 'time-series';
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Dashboard-belowMap',
|
||||
|
||||
initialize: function (options) {
|
||||
this._widgetViewFactory = new WidgetViewFactory([
|
||||
{
|
||||
// same type as below, but also check if the associated layer is a a torque layer
|
||||
match: function (widgetModel) {
|
||||
if (widgetModel.get('type') === TIME_SERIES_TYPE) {
|
||||
var dataviewModel = widgetModel.dataviewModel;
|
||||
var layerModel = widgetModel.layerModel;
|
||||
return dataviewModel && layerModel.get('type') === 'torque' && widgetModel.get('animated') === true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
createContentView: function (widgetModel) {
|
||||
return new TorqueTimeSeriesContentView({
|
||||
model: widgetModel
|
||||
});
|
||||
},
|
||||
customizeWidgetAttrs: function (attrs) {
|
||||
attrs.className += ' CDB-Widget--timeSeries';
|
||||
return attrs;
|
||||
}
|
||||
}, {
|
||||
type: TIME_SERIES_TYPE,
|
||||
createContentView: function (widgetModel) {
|
||||
return new TimeSeriesContentView({
|
||||
model: widgetModel
|
||||
});
|
||||
},
|
||||
customizeWidgetAttrs: function (attrs) {
|
||||
attrs.className += ' CDB-Widget--timeSeries';
|
||||
return attrs;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this._widgets = options.widgets;
|
||||
this._widgets.bind('add', this._maybeRenderWidgetView, this);
|
||||
this._widgets.bind('reset', this.render, this);
|
||||
this._widgets.bind('add reset remove', this._onWidgetsChange, this);
|
||||
this.add_related_model(this._widgets);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this._widgets.each(this._maybeRenderWidgetView, this);
|
||||
this._toggleVisiblity();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_maybeRenderWidgetView: function (widgetModel) {
|
||||
var view = this._widgetViewFactory.createWidgetView(widgetModel);
|
||||
if (view) {
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
}
|
||||
},
|
||||
|
||||
_toggleVisiblity: function () {
|
||||
this.$el.toggle(!_.isEmpty(this._subviews));
|
||||
},
|
||||
|
||||
_onWidgetsChange: function () {
|
||||
this._widgets.each(function (widget) {
|
||||
widget.forceResize();
|
||||
});
|
||||
this._toggleVisiblity();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
var cdb = require('internal-carto.js');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var template = require('./dashboard-menu-view.tpl');
|
||||
var moment = require('moment');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
|
||||
var BREAKPOINT = 1201;
|
||||
|
||||
var DashboardMenuView = CoreView.extend({
|
||||
className: 'CDB-Dashboard-menu',
|
||||
|
||||
events: {
|
||||
'click .js-toggle-view': '_toogleView'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.viewModel = new cdb.core.Model({
|
||||
open: false,
|
||||
footer: $(window).width() < BREAKPOINT
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.viewModel, 'change', this.render);
|
||||
this._resize = _.debounce(this._onResizeWindow.bind(this), 20);
|
||||
$(window).on('resize', this._resize);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var shortTitle = this.model.get('title');
|
||||
var isOpen = this.viewModel.get('open');
|
||||
var isFooter = this.viewModel.get('footer');
|
||||
|
||||
if (shortTitle && shortTitle.length > 120) {
|
||||
shortTitle = shortTitle.slice(0, 110) + '...' + ' %23 map';
|
||||
}
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
hasTranslation: isOpen || isFooter,
|
||||
showLogo: this.model.get('showLogo'),
|
||||
title: this.model.get('title'),
|
||||
description: this.model.get('description'),
|
||||
updatedAt: moment(this.model.get('updatedAt')).fromNow(),
|
||||
userName: this.model.get('userName'),
|
||||
url: window.location.href,
|
||||
urlWithoutParams: window.location.protocol + '//' + window.location.host + window.location.pathname,
|
||||
inIframe: (window.location !== window.parent.location),
|
||||
shortTitle: shortTitle,
|
||||
userAvatarURL: this.model.get('userAvatarURL'),
|
||||
userProfileURL: this.model.get('userProfileURL')
|
||||
})
|
||||
);
|
||||
|
||||
this.$el.toggleClass('is-active', isOpen);
|
||||
|
||||
var content = this._getDescription().get(0);
|
||||
Ps.initialize(content, {
|
||||
wheelSpeed: 2,
|
||||
suppressScrollX: true
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_getDescription: function () {
|
||||
return this.$('.js-scroll-wrapper');
|
||||
},
|
||||
|
||||
_toogleView: function () {
|
||||
var open = this.viewModel.get('open');
|
||||
this.viewModel.set({open: !open});
|
||||
},
|
||||
|
||||
_onResizeWindow: function () {
|
||||
this.viewModel.set({
|
||||
footer: $(window).width() < BREAKPOINT
|
||||
});
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
$(window).off('resize', this._resize);
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DashboardMenuView;
|
||||
@@ -0,0 +1,114 @@
|
||||
<div class="CDB-Dashboard-menuContainer">
|
||||
<div class="CDB-Dashboard-menuInner">
|
||||
<div class="CDB-Dashboard-menuHeader">
|
||||
<% if (showLogo === true) { %>
|
||||
<div class="CDB-Dashboard-menuLogo">
|
||||
<a href="http://www.onenetcom.cn" target="_blank">
|
||||
<svg viewBox="0 0 62 24" height="24" width="62">
|
||||
<g class="CDB-EmbedLogo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="m1.30478,8.40541l5.77211,0l0,1.64848l-3.42718,0l0,1.33971l2.92728,0l0,1.54904l-2.92728,0l0,3.13472l-2.34493,0l0,-7.67195l0,0.00001zm15.19816,6.40549l-2.65928,0l-0.36592,1.26646l-2.38614,0l2.83966,-7.67195l2.54591,0l2.83967,7.67195l-2.44284,0l-0.37106,-1.26646l0,0zm-0.4896,-1.65894l-0.83489,-2.75792l-0.82974,2.75792l1.66463,0zm7.16361,0.38725l2.22122,-0.14128c0.04809,0.36633 0.14602,0.64544 0.29375,0.83732c0.24052,0.31051 0.5841,0.46576 1.03074,0.46576c0.33327,0 0.59008,-0.07936 0.77047,-0.23811c0.18038,-0.15874 0.27055,-0.34277 0.27055,-0.55211c0,-0.19886 -0.08588,-0.37679 -0.25767,-0.53379c-0.17177,-0.15699 -0.57033,-0.30528 -1.19565,-0.44483c-1.02387,-0.23374 -1.75398,-0.54426 -2.19031,-0.93152c-0.43978,-0.38725 -0.65968,-0.88092 -0.65968,-1.481c0,-0.39425 0.11253,-0.76667 0.33758,-1.1173c0.22505,-0.35063 0.56347,-0.62625 1.01527,-0.82686c0.45181,-0.20061 1.07111,-0.30091 1.85791,-0.30091c0.96546,0 1.70157,0.1823 2.20836,0.54687c0.50677,0.36457 0.80826,0.9446 0.90446,1.74005l-2.20061,0.13083c-0.0584,-0.34539 -0.18124,-0.59659 -0.36848,-0.75359c-0.18726,-0.157 -0.4458,-0.2355 -0.77563,-0.2355c-0.27143,0 -0.47587,0.05843 -0.61328,0.17531c-0.13745,0.11687 -0.20615,0.25904 -0.20615,0.42651c0,0.12211 0.05669,0.232 0.17007,0.32969c0.10994,0.10119 0.37105,0.19538 0.78336,0.2826c1.02043,0.22328 1.75138,0.4492 2.19288,0.67771c0.4415,0.22851 0.76274,0.51199 0.96374,0.8504c0.201,0.33841 0.30149,0.71695 0.30149,1.13562c0,0.49192 -0.13398,0.94547 -0.40197,1.36064c-0.268,0.41517 -0.64249,0.73003 -1.12351,0.94459c-0.48101,0.21457 -1.08743,0.32185 -1.81925,0.32185c-1.28501,0 -2.17486,-0.2512 -2.66961,-0.75359c-0.49476,-0.50239 -0.77477,-1.14085 -0.84004,-1.91537l-0.00002,0.00003l0,-0.00002l-0.00001,0zm11.01853,-5.13382l7.09662,0l0,1.89444l-2.38099,0l0,5.7775l-2.3346,0l0,-5.7775l-2.38101,0l0,-1.89444l-0.00001,0l-0.00002,0z" class="CDB-EmbedLogo-text" fill="#FFFFFF"></path> <g class="CDB-EmbedLogo-image"
|
||||
<% if (hasTranslation) { %>
|
||||
transform="translate(37.000000, 0.000000)"
|
||||
<% } %>
|
||||
>
|
||||
<circle class="CDB-EmbedLogo-halo" fill="rgba(255, 255, 255, 0.20)" cx="12.3333333" cy="12" r="12"></circle>
|
||||
<path d="M12.3333333,16.5 C14.8186147,16.5 16.8333333,14.4852814 16.8333333,12 C16.8333333,9.51471863 14.8186147,7.5 12.3333333,7.5 C9.84805196,7.5 7.83333333,9.51471863 7.83333333,12 C7.83333333,14.4852814 9.84805196,16.5 12.3333333,16.5 L12.3333333,16.5 Z" class="CDB-EmbedLogo-point" fill="#FFFFFF" ></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<ul class="CDB-Dashboard-menuActions">
|
||||
<!--
|
||||
<li class="CDB-Dashboard-menuActionsItem">
|
||||
<a href="https://twitter.com/share?url=<%- urlWithoutParams %>&text=<%- shortTitle %>" target="_blank" class="u-hintTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-twitter CDB-Size-large"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="CDB-Dashboard-menuActionsItem">
|
||||
<a href="http://www.facebook.com/sharer.php?u=<%- urlWithoutParams %>&text=<%- shortTitle %>" target="_blank" class="u-hintTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-facebook CDB-Size-medium"></i>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
<% if (inIframe) { %>
|
||||
<li class="CDB-Dashboard-menuActionsItem">
|
||||
<a href="<%- url %>" target="_blank" class="u-hintTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-anchor CDB-Size-medium"></i>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="CDB-Dashboard-menuInfo">
|
||||
<button class="js-toggle-view">
|
||||
<i class="CDB-IconFont CDB-Size-medium CDB-IconFont-rArrowLight Size-large"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Dashboard-menuTexts CDB-Dashboard-hideMobile">
|
||||
<div class="CDB-Dashboard-menuTextInner js-content">
|
||||
<button class="js-toggle-view u-actionTextColor CDB-Dashboard-menuTextActions">
|
||||
<svg width="10px" height="7px" viewBox="12 13 10 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M12,13.5 C12,13.2238576 12.1908338,13 12.4169561,13 L21.5830439,13 C21.8133224,13 22,13.2319336 22,13.5 C22,13.7761424 21.8091662,14 21.5830439,14 L12.4169561,14 C12.1866776,14 12,13.7680664 12,13.5 L12,13.5 L12,13.5 Z M12,16.5 C12,16.2238576 12.1908338,16 12.4169561,16 L21.5830439,16 C21.8133224,16 22,16.2319336 22,16.5 C22,16.7761424 21.8091662,17 21.5830439,17 L12.4169561,17 C12.1866776,17 12,16.7680664 12,16.5 L12,16.5 L12,16.5 Z M12,19.5 C12,19.2238576 12.1908338,19 12.4169561,19 L21.5830439,19 C21.8133224,19 22,19.2319336 22,19.5 C22,19.7761424 21.8091662,20 21.5830439,20 L12.4169561,20 C12.1866776,20 12,19.7680664 12,19.5 L12,19.5 L12,19.5 Z" id="Combined-Shape" stroke="none" fill="#1785fB" fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="CDB-Dashboard-menuTime CDB-Text CDB-Size-small u-upperCase u-altTextColor u-bSpace--m js-timeAgo">更新 <%- updatedAt %></p>
|
||||
<div class="CDB-Dashboard-metadata">
|
||||
<h1 class="CDB-Dashboard-menuTitle CDB-Dashboard-menuTitle--mobile CDB-Text CDB-Size-huge u-ellipsis js-title" title="<%- title %>"><%- title %></h1>
|
||||
|
||||
<div class="CDB-Dashboard-scrollWrapper js-scroll-wrapper">
|
||||
<div class="CDB-Dashboard-scrollContent js-scroll-content">
|
||||
<h2 class="CDB-Dashboard-menuDescription CDB-Text CDB-Size-large is-light u-secondaryTextColor js-description"><%= cdb.core.sanitize.html(description) %></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="CDB-Dashboard-menuFooter">
|
||||
<ul>
|
||||
<li class="CDB-Dashboard-menuFooterItem ">
|
||||
<div class="CDB-Dashboard-menuMedia CDB-Dashboard-menuAvatar">
|
||||
<img src="<%- userAvatarURL %>" alt="avatar" class="inline-block"/>
|
||||
</div>
|
||||
<p class="CDB-Text CDB-Size-medium CDB-Dashboard-menuFooterTxt">版权 <a href="<%- userProfileURL %>" target="_blank"><%- userName %></a></p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="CDB-Dashboard-bg js-toggle-view"></div>
|
||||
|
||||
<div class="CDB-Dashboard-menuHeaderMobile">
|
||||
<div class="u-flex u-alignStart CDB-Dashboard-menuHeaderMobileInner">
|
||||
<button class="js-toggle-view u-actionTextColor CDB-Dashboard-menuHeaderMobileActions">
|
||||
<i class="CDB-IconFont CDB-Size-medium CDB-IconFont-rArrowLight Size-large"></i>
|
||||
</button>
|
||||
<div class="CDB-Dashboard-menuHeaderMobileText">
|
||||
<% if (showLogo === true) { %>
|
||||
<div class="CDB-Dashboard-menuLogo">
|
||||
<a href="http://onenetcom.cn" target="_blank">
|
||||
<svg width="62px" height="24px" viewBox="35 63 62 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g class="CDB-EmbedLogo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(35.000000, 63.000000)">
|
||||
<path d="m1.30478,8.40541l5.77211,0l0,1.64848l-3.42718,0l0,1.33971l2.92728,0l0,1.54904l-2.92728,0l0,3.13472l-2.34493,0l0,-7.67195l0,0.00001zm15.19816,6.40549l-2.65928,0l-0.36592,1.26646l-2.38614,0l2.83966,-7.67195l2.54591,0l2.83967,7.67195l-2.44284,0l-0.37106,-1.26646l0,0zm-0.4896,-1.65894l-0.83489,-2.75792l-0.82974,2.75792l1.66463,0zm7.16361,0.38725l2.22122,-0.14128c0.04809,0.36633 0.14602,0.64544 0.29375,0.83732c0.24052,0.31051 0.5841,0.46576 1.03074,0.46576c0.33327,0 0.59008,-0.07936 0.77047,-0.23811c0.18038,-0.15874 0.27055,-0.34277 0.27055,-0.55211c0,-0.19886 -0.08588,-0.37679 -0.25767,-0.53379c-0.17177,-0.15699 -0.57033,-0.30528 -1.19565,-0.44483c-1.02387,-0.23374 -1.75398,-0.54426 -2.19031,-0.93152c-0.43978,-0.38725 -0.65968,-0.88092 -0.65968,-1.481c0,-0.39425 0.11253,-0.76667 0.33758,-1.1173c0.22505,-0.35063 0.56347,-0.62625 1.01527,-0.82686c0.45181,-0.20061 1.07111,-0.30091 1.85791,-0.30091c0.96546,0 1.70157,0.1823 2.20836,0.54687c0.50677,0.36457 0.80826,0.9446 0.90446,1.74005l-2.20061,0.13083c-0.0584,-0.34539 -0.18124,-0.59659 -0.36848,-0.75359c-0.18726,-0.157 -0.4458,-0.2355 -0.77563,-0.2355c-0.27143,0 -0.47587,0.05843 -0.61328,0.17531c-0.13745,0.11687 -0.20615,0.25904 -0.20615,0.42651c0,0.12211 0.05669,0.232 0.17007,0.32969c0.10994,0.10119 0.37105,0.19538 0.78336,0.2826c1.02043,0.22328 1.75138,0.4492 2.19288,0.67771c0.4415,0.22851 0.76274,0.51199 0.96374,0.8504c0.201,0.33841 0.30149,0.71695 0.30149,1.13562c0,0.49192 -0.13398,0.94547 -0.40197,1.36064c-0.268,0.41517 -0.64249,0.73003 -1.12351,0.94459c-0.48101,0.21457 -1.08743,0.32185 -1.81925,0.32185c-1.28501,0 -2.17486,-0.2512 -2.66961,-0.75359c-0.49476,-0.50239 -0.77477,-1.14085 -0.84004,-1.91537l-0.00002,0.00003l0,-0.00002l-0.00001,0zm11.01853,-5.13382l7.09662,0l0,1.89444l-2.38099,0l0,5.7775l-2.3346,0l0,-5.7775l-2.38101,0l0,-1.89444l-0.00001,0l-0.00002,0z" class="CDB-EmbedLogo-text" fill="#FFFFFF"></path> <g class="CDB-EmbedLogo-image"
|
||||
<g class="CDB-EmbedLogo-image" transform="translate(37.000000, 0.000000)">
|
||||
<circle class="CDB-EmbedLogo-halo" fill="rgba(255, 255, 255, 0.20)" cx="12.3333333" cy="12" r="12"></circle>
|
||||
<path d="M12.3333333,16.5 C14.8186147,16.5 16.8333333,14.4852814 16.8333333,12 C16.8333333,9.51471863 14.8186147,7.5 12.3333333,7.5 C9.84805196,7.5 7.83333333,9.51471863 7.83333333,12 C7.83333333,14.4852814 9.84805196,16.5 12.3333333,16.5 L12.3333333,16.5 Z" class="CDB-EmbedLogo-point" fill="#FFFFFF" ></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
<p class="CDB-Dashboard-menuTime CDB-Text CDB-Size-small u-upperCase u-altTextColor u-bSpace--m js-timeAgo">更新 <%- updatedAt %></p>
|
||||
<h1 class="CDB-Text CDB-Size-large u-ellipsis js-title u-bSpace--xl u-ellipsis"><%- title %></h1>
|
||||
<h2 class="CDB-Text CDB-Size-medium u-secondaryTextColor js-description"><%= cdb.core.sanitize.html(description) %></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,196 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Ps = require('perfect-scrollbar');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var CategoryContentView = require('./widgets/category/content-view');
|
||||
var FormulaContentView = require('./widgets/formula/content-view');
|
||||
var HistogramContentView = require('./widgets/histogram/content-view');
|
||||
var WidgetViewFactory = require('./widgets/widget-view-factory');
|
||||
var template = require('./dashboard-sidebar.tpl');
|
||||
var matchMedia = window.matchMedia;
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-canvas',
|
||||
|
||||
initialize: function (options) {
|
||||
this._widgetViewFactory = new WidgetViewFactory([
|
||||
{
|
||||
type: 'formula',
|
||||
createContentView: function (widgetModel) {
|
||||
return new FormulaContentView({
|
||||
model: widgetModel
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: 'histogram',
|
||||
createContentView: function (widgetModel) {
|
||||
return new HistogramContentView({
|
||||
model: widgetModel
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: 'category',
|
||||
createContentView: function (widgetModel) {
|
||||
return new CategoryContentView({
|
||||
model: widgetModel
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this._widgets = options.widgets;
|
||||
|
||||
this._widgets.bind('add', this._maybeRenderWidgetView, this);
|
||||
this._widgets.bind('reset', this.render, this);
|
||||
this._widgets.bind('orderChanged', this.render, this);
|
||||
this._widgets.bind('change:collapsed', this._onWidgetUpdate, this);
|
||||
this._widgets.bind('add remove reset', this._onWidgetsChange, this);
|
||||
this.add_related_model(this._widgets);
|
||||
|
||||
this._resizeHandler = this._onResize.bind(this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._cleanScroll();
|
||||
this._observer && this._observer.disconnect();
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(template());
|
||||
this.$el.toggleClass('CDB-Widget-canvas--withMenu', this.model.get('renderMenu'));
|
||||
this._widgets.each(this._maybeRenderWidgetView, this);
|
||||
this._toggleVisiblity();
|
||||
|
||||
this._renderScroll();
|
||||
this._renderShadows();
|
||||
this._bindScroll();
|
||||
this._initResize();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initResize: function () {
|
||||
var breakpoints = ['(max-width: 1600px)', '(max-width: 1280px)', '(max-width: 759px)'];
|
||||
breakpoints.forEach(function (breakpoint) {
|
||||
var mq = matchMedia(breakpoint);
|
||||
mq.addListener(this._resizeHandler);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_updateScrollCss: function () {
|
||||
this._onWidgetUpdate();
|
||||
|
||||
var element = this._container();
|
||||
if (!element) return;
|
||||
|
||||
var containerWidth = element.clientWidth;
|
||||
var containerHeight = element.clientHeight;
|
||||
var contentWidth = element.scrollWidth;
|
||||
var contentHeight = element.scrollHeight;
|
||||
var xScroll = containerWidth < contentWidth;
|
||||
var yScroll = containerHeight < contentHeight;
|
||||
|
||||
this._$container().toggleClass('hasXScroll', xScroll);
|
||||
this._$container().toggleClass('hasYScroll', yScroll);
|
||||
},
|
||||
|
||||
_$container: function () {
|
||||
return $(this._container());
|
||||
},
|
||||
|
||||
_container: function () {
|
||||
return this.el.querySelector('.js-container');
|
||||
},
|
||||
|
||||
_maybeRenderWidgetView: function (widgetModel) {
|
||||
var view = this._widgetViewFactory.createWidgetView(widgetModel);
|
||||
|
||||
if (view) {
|
||||
this.addView(view);
|
||||
this._$container().append(view.render().el);
|
||||
}
|
||||
},
|
||||
|
||||
_bindScroll: function () {
|
||||
this._$container()
|
||||
.on('ps-y-reach-start', _.bind(this._onScrollTop, this))
|
||||
.on('ps-y-reach-end', _.bind(this._onScrollBottom, this))
|
||||
.on('ps-scroll-y', _.bind(this._onScroll, this));
|
||||
},
|
||||
|
||||
_renderScroll: function () {
|
||||
Ps.initialize(this._container(), {
|
||||
wheelSpeed: 1,
|
||||
wheelPropagation: false,
|
||||
swipePropagation: true,
|
||||
stopPropagationOnClick: false,
|
||||
minScrollbarLength: 20,
|
||||
useBothWheelAxes: true
|
||||
});
|
||||
},
|
||||
|
||||
_onWidgetUpdate: function () {
|
||||
Ps.update(this._container());
|
||||
},
|
||||
|
||||
_renderShadows: function () {
|
||||
this.$shadowTop = $('<div>').addClass('CDB-Widget-canvasShadow CDB-Widget-canvasShadow--top');
|
||||
this.$shadowBottom = $('<div>').addClass('CDB-Widget-canvasShadow CDB-Widget-canvasShadow--bottom');
|
||||
this.$el.append(this.$shadowTop);
|
||||
this.$el.append(this.$shadowBottom);
|
||||
},
|
||||
|
||||
_onScrollTop: function () {
|
||||
this.$shadowTop.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_onScroll: function () {
|
||||
var $el = this._$container();
|
||||
var currentPos = $el.scrollTop();
|
||||
var max = $el.get(0).scrollHeight;
|
||||
var height = $el.outerHeight();
|
||||
var maxPos = max - height;
|
||||
|
||||
this.$shadowTop.toggleClass('is-visible', currentPos > 0);
|
||||
this.$shadowBottom.toggleClass('is-visible', currentPos < maxPos);
|
||||
},
|
||||
|
||||
_updateScroll: function () {
|
||||
this._$container().scrollLeft = 0;
|
||||
this._$container().scrollTop = 0;
|
||||
},
|
||||
|
||||
_onResize: function (mediaQuery) {
|
||||
// we don't use mediaQuery.matches
|
||||
// trigger actions always if breakpoints changes
|
||||
this._updateScroll();
|
||||
this._onScroll();
|
||||
},
|
||||
|
||||
_onScrollBottom: function () {
|
||||
this.$shadowBottom.removeClass('is-visible');
|
||||
},
|
||||
|
||||
_cleanScroll: function () {
|
||||
$(window).off('resize', this._resizeHandler);
|
||||
if (this._container()) {
|
||||
this._$container().off('ps-scroll-y');
|
||||
Ps.destroy(this._container());
|
||||
}
|
||||
},
|
||||
|
||||
_toggleVisiblity: function () {
|
||||
this.$el.toggle(!_.isEmpty(this._subviews));
|
||||
},
|
||||
|
||||
_onWidgetsChange: function () {
|
||||
this._toggleVisiblity();
|
||||
this.render();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._cleanScroll();
|
||||
this._observer && this._observer.disconnect();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<div class="CDB-Widget-canvasInner js-container"></div>
|
||||
@@ -0,0 +1,89 @@
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./dashboard.tpl');
|
||||
var DashboardBelowMapView = require('./dashboard-below-map-view');
|
||||
var DashboardMenuView = require('./dashboard-menu-view');
|
||||
var DashboardSidebarView = require('./dashboard-sidebar-view');
|
||||
|
||||
/**
|
||||
* Dashboard is a wrapper around the map canvas, which contains widget views for the map contdxt
|
||||
* Widgets may be rendered in two areas, in the "sidebar" or "below-map".
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Dashboard-canvas',
|
||||
|
||||
initialize: function (options) {
|
||||
this._widgets = options.widgets;
|
||||
this._infoView = options.infoView;
|
||||
|
||||
// TODO parent context requires some markup to be present already, but NOT the other views
|
||||
this.el.classList.add(this.className);
|
||||
this.$el.html(template());
|
||||
|
||||
this._onWindowResize = this._onWindowResize.bind(this);
|
||||
$(window).bind('resize', this._onWindowResize);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
var view;
|
||||
var doRenderMenu = this.model.get('renderMenu');
|
||||
|
||||
this.$el.toggleClass('CDB-Dashboard-canvas--withMenu', doRenderMenu);
|
||||
|
||||
if (doRenderMenu) {
|
||||
view = new DashboardMenuView({
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
}
|
||||
|
||||
const hasTimeSeries = this._widgets.some(function (model) {
|
||||
return model.get('type') === 'time-series';
|
||||
});
|
||||
|
||||
if (hasTimeSeries) {
|
||||
this.$el.toggleClass('CDB-Dashboard-canvas--withTimeSeries');
|
||||
}
|
||||
|
||||
view = new DashboardBelowMapView({
|
||||
widgets: this._widgets
|
||||
});
|
||||
this.addView(view);
|
||||
this.$('.js-map-wrapper')
|
||||
.toggleClass('CDB-Dashboard-mapWrapper--withMenu', doRenderMenu)
|
||||
.append(view.render().el);
|
||||
|
||||
if (this._widgets && this._widgets.length) {
|
||||
this.$el.toggleClass('CDB-Dashboard-canvas--withWidgets');
|
||||
}
|
||||
|
||||
view = new DashboardSidebarView({
|
||||
widgets: this._widgets,
|
||||
model: this.model
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getInitialMapState: function () {
|
||||
return {
|
||||
bounds: this.model.get('initialPosition').bounds
|
||||
};
|
||||
},
|
||||
|
||||
_onWindowResize: function () {
|
||||
this._widgets.each(function (widget) {
|
||||
widget.forceResize();
|
||||
});
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
$(window).unbind('resize', this._onWindowResize);
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="CDB-Dashboard-mapWrapper js-map-wrapper">
|
||||
<div class="CDB-Map-canvas">
|
||||
<div class="CDB-Map" id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,131 @@
|
||||
var ACTIVE_LOCALE = 'zh-cn';
|
||||
var Locale = require('locale/index');
|
||||
var Polyglot = require('node-polyglot');
|
||||
var polyglot = new Polyglot({
|
||||
locale: ACTIVE_LOCALE, // Needed for pluralize behaviour
|
||||
phrases: Locale[ACTIVE_LOCALE]
|
||||
});
|
||||
var _t = polyglot.t.bind(polyglot);
|
||||
|
||||
var MAP = {
|
||||
'aggregate-intersection': {
|
||||
title: _t('analyses.aggregate-intersection.short-title')
|
||||
},
|
||||
'bounding-box': {
|
||||
title: _t('analyses.bounding-box.short-title')
|
||||
},
|
||||
'bounding-circle': {
|
||||
title: _t('analyses.bounding-circle.short-title')
|
||||
},
|
||||
'buffer': {
|
||||
title: _t('analyses.area-of-influence.short-title')
|
||||
},
|
||||
'centroid': {
|
||||
title: _t('analyses.centroid.short-title')
|
||||
},
|
||||
'convex-hull': {
|
||||
title: _t('analyses.convex-hull.short-title')
|
||||
},
|
||||
'concave-hull': {
|
||||
title: _t('analyses.concave-hull.short-title')
|
||||
},
|
||||
'data-observatory-measure': {
|
||||
title: _t('analyses.data-observatory-measure.short-title')
|
||||
},
|
||||
'data-observatory-multiple-measures': {
|
||||
title: _t('analyses.data-observatory-multiple-measures.short-title')
|
||||
},
|
||||
'filter-by-node-column': {
|
||||
title: _t('analyses.filter-by-node-column.short-title')
|
||||
},
|
||||
'filter-category': {
|
||||
title: _t('analyses.filter.short-title')
|
||||
},
|
||||
'filter-range': {
|
||||
title: _t('analyses.filter.short-title')
|
||||
},
|
||||
'georeference-city': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-ip-address': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-country': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-long-lat': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-postal-code': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-street-address': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'georeference-admin-region': {
|
||||
title: _t('analyses.georeference.short-title')
|
||||
},
|
||||
'intersection': {
|
||||
title: _t('analyses.filter-intersection.short-title')
|
||||
},
|
||||
'kmeans': {
|
||||
title: _t('analyses.kmeans.short-title')
|
||||
},
|
||||
'line-to-single-point': {
|
||||
title: _t('analyses.connect-with-lines.short-title')
|
||||
},
|
||||
'line-source-to-target': {
|
||||
title: _t('analyses.connect-with-lines.short-title')
|
||||
},
|
||||
'line-sequential': {
|
||||
title: _t('analyses.connect-with-lines.short-title')
|
||||
},
|
||||
'merge': {
|
||||
title: _t('analyses.merge.short-title')
|
||||
},
|
||||
'moran': {
|
||||
title: _t('analyses.moran-cluster.short-title')
|
||||
},
|
||||
'routing-sequential': {
|
||||
title: _t('analyses.routing.short-title')
|
||||
},
|
||||
'routing-to-layer-all-to-all': {
|
||||
title: _t('analyses.routing.short-title')
|
||||
},
|
||||
'routing-to-single-point': {
|
||||
title: _t('analyses.routing.short-title')
|
||||
},
|
||||
'sampling': {
|
||||
title: _t('analyses.sampling.short-title')
|
||||
},
|
||||
'spatial-markov-trend': {
|
||||
title: _t('analyses.spatial-markov-trend.short-title')
|
||||
},
|
||||
'trade-area': {
|
||||
title: _t('analyses.area-of-influence.short-title')
|
||||
},
|
||||
'weighted-centroid': {
|
||||
title: _t('analyses.centroid.short-title')
|
||||
},
|
||||
'closest': {
|
||||
title: _t('analyses.find-nearest.short-title')
|
||||
},
|
||||
'deprecated-sql-function': {
|
||||
title: _t('analyses.deprecated-sql-function.short-title')
|
||||
}
|
||||
};
|
||||
|
||||
var getAnalysisByType = function (type) {
|
||||
var safeType = type === '' ? 'unknown' : type;
|
||||
return MAP[safeType] || {
|
||||
title: _t('analyses.' + safeType)
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
MAP: MAP,
|
||||
|
||||
title: function (type) {
|
||||
return getAnalysisByType(type).title;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
var _ = require('underscore');
|
||||
var d3 = require('d3');
|
||||
var moment = require('moment');
|
||||
|
||||
var AGGREGATION_FORMATS = {
|
||||
second: {
|
||||
display: 'HH:mm:ss',
|
||||
unit: 's'
|
||||
},
|
||||
minute: {
|
||||
display: 'HH:mm - MMM Do, YYYY',
|
||||
unit: 'm'
|
||||
},
|
||||
hour: {
|
||||
display: 'HH:00 - MMM Do, YYYY',
|
||||
unit: 'h'
|
||||
},
|
||||
day: {
|
||||
display: 'MMM Do, YYYY',
|
||||
unit: 'd'
|
||||
},
|
||||
week: {
|
||||
display: 'MMM Do, YYYY',
|
||||
unit: 'w'
|
||||
},
|
||||
month: {
|
||||
display: 'MMM YYYY',
|
||||
unit: 'M'
|
||||
},
|
||||
quarter: {
|
||||
display: '[Q]Q YYYY',
|
||||
unit: 'Q'
|
||||
},
|
||||
year: {
|
||||
display: 'YYYY',
|
||||
unit: 'y'
|
||||
},
|
||||
decade: {
|
||||
display: 'YYYY',
|
||||
unit: 'y'
|
||||
}
|
||||
};
|
||||
|
||||
var format = {};
|
||||
|
||||
format.formatNumber = function (value, unit) {
|
||||
if (!_.isNumber(value) || value === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var format = d3.format('.2s');
|
||||
var p = 0;
|
||||
var absV = Math.abs(value);
|
||||
|
||||
if (value > 1000) {
|
||||
value = format(value) + (unit ? ' ' + unit : '');
|
||||
return value;
|
||||
}
|
||||
|
||||
if (absV > 100) {
|
||||
p = 0;
|
||||
} else if (absV > 10) {
|
||||
p = 1;
|
||||
} else if (absV > 1) {
|
||||
p = 2;
|
||||
} else if (absV > 0) {
|
||||
p = Math.max(Math.ceil(Math.abs(Math.log(absV) / Math.log(10))) + 2, 3);
|
||||
}
|
||||
|
||||
value = value.toFixed(p);
|
||||
var m = value.match(/(\.0+)$/);
|
||||
if (m) {
|
||||
value = value.replace(m[0], '');
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
format.formatDate = function (value) {
|
||||
return d3.time.format('%Y-%m-%d')(value);
|
||||
};
|
||||
|
||||
format.formatTime = function (value) {
|
||||
return d3.time.format('%H:%M:%S %d/%m/%Y')(value);
|
||||
};
|
||||
|
||||
format.timeFactory = function (format) {
|
||||
return d3.time.format(format);
|
||||
};
|
||||
|
||||
format.formatValue = function (value) {
|
||||
if (_.isNumber(value)) {
|
||||
return format.formatNumber(value);
|
||||
}
|
||||
if (_.isDate(value)) {
|
||||
return format.formatDate(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
format.timestampFactory = function (aggregation, offset) {
|
||||
return function (timestamp) {
|
||||
if (!_.has(AGGREGATION_FORMATS, aggregation)) {
|
||||
return '-';
|
||||
}
|
||||
var format = AGGREGATION_FORMATS[aggregation];
|
||||
var date = moment.unix(timestamp).utc();
|
||||
if (_.isFinite(offset)) {
|
||||
date.utcOffset(offset / 60);
|
||||
}
|
||||
var formatted = date.format(format.display);
|
||||
return formatted;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = format;
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
VERSION: '',
|
||||
createDashboard: require('./api/create-dashboard')
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
var cdb = require('internal-carto.js');
|
||||
cdb.deepInsights = {
|
||||
VERSION: require('../package.json').version,
|
||||
createDashboard: require('./api/create-dashboard')
|
||||
};
|
||||
module.exports = cdb;
|
||||
@@ -0,0 +1,37 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
var ERRORS = {
|
||||
generic: {
|
||||
level: 'error',
|
||||
refresh: true
|
||||
},
|
||||
limit: {
|
||||
level: 'error',
|
||||
error: 'Timeout',
|
||||
message: 'The server is taking too long to respond, due to poor conectivity or a temporary error with our servers. Please try again soon.',
|
||||
refresh: true
|
||||
},
|
||||
no_data_available: {
|
||||
level: 'alert',
|
||||
error: 'No data available',
|
||||
message: 'There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust the Map View.',
|
||||
refresh: false
|
||||
},
|
||||
too_many_bins: {
|
||||
level: 'error',
|
||||
error: 'Too many bins returned',
|
||||
message: 'There are too many bins returned. Try selecting a less granular aggregation or filtering the data source.',
|
||||
refresh: false
|
||||
},
|
||||
dataview: {
|
||||
level: 'error',
|
||||
refresh: false
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = function (error) {
|
||||
var type = error && error.type && _.has(ERRORS, error.type)
|
||||
? error.type
|
||||
: 'generic';
|
||||
return _.extend({}, error, ERRORS[type]);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = function escapeHTML (str) {
|
||||
return _.escape(str);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.
|
||||
* @param {Object} object The object to query.
|
||||
* @param {String} path The path of the property to get.
|
||||
* @param {Any} defaultValue The value returned for undefined resolved values.
|
||||
* @return {Any} Returns the resolved value.
|
||||
*/
|
||||
module.exports = function (object, path, defaultValue) {
|
||||
var keys = path.split('.');
|
||||
var value = keys.reduce(function (a, b) {
|
||||
return (a || {})[b];
|
||||
}, object);
|
||||
|
||||
return value || defaultValue;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = {
|
||||
COLORS: [
|
||||
'#11A579',
|
||||
'#E83f74',
|
||||
'#9F5DC8',
|
||||
'#EF8205',
|
||||
'#29B3B2',
|
||||
'#E54F3B',
|
||||
'#59BB12',
|
||||
'#EF57BA',
|
||||
'#E2AF00',
|
||||
'#12AADE'
|
||||
],
|
||||
|
||||
/**
|
||||
* Get the letter representation.
|
||||
* @param {String, Object} sourceId or backbone model that have id. e.g. 'c2'
|
||||
* @return {String} e.g. 'c' or an empty string if there is no letter in the given id
|
||||
*/
|
||||
letter: function (sourceId) {
|
||||
if (!sourceId || !_.isString(sourceId)) return '';
|
||||
var match = sourceId.match(/^([a-z]+)/);
|
||||
return (_.isArray(match) && match[0]) || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a color given a letter
|
||||
* @param {String} Letter. eg: 'a', 'b', 'c', etc.
|
||||
* @return {String} Hex color code: eg: '#7F3C8D'
|
||||
*/
|
||||
getColorForLetter: function (letter) {
|
||||
if (!letter) {
|
||||
return this.COLORS[0];
|
||||
}
|
||||
|
||||
var letterNumber = letter.charCodeAt(0) - 97;
|
||||
var colorIndex = ((letterNumber / this.COLORS.length % 1) * 10).toFixed();
|
||||
return this.COLORS[colorIndex] || this.COLORS[0];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
var moment = require('moment');
|
||||
var _ = require('underscore');
|
||||
var helper = {};
|
||||
|
||||
helper.substractOneUnit = function (timestamp, aggregation) {
|
||||
if (!_.isNumber(timestamp)) {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
var result = moment.unix(timestamp).utc();
|
||||
aggregation && aggregation === 'week'
|
||||
? result.subtract(1, 'day')
|
||||
: result.subtract(1, aggregation);
|
||||
|
||||
return result.unix();
|
||||
};
|
||||
|
||||
module.exports = helper;
|
||||
@@ -0,0 +1,5 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = function unescapeHTML (str) {
|
||||
return _.unescape(str);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
var $ = require('jquery');
|
||||
|
||||
var TABLET_WIDTH = 760;
|
||||
var MOBILE_WIDTH = 480;
|
||||
|
||||
var utils = {};
|
||||
|
||||
utils._isViewport = function (vp) {
|
||||
return $(window).width() < vp;
|
||||
};
|
||||
|
||||
utils.isMobileViewport = function () {
|
||||
return this._isViewport(MOBILE_WIDTH);
|
||||
};
|
||||
|
||||
utils.isTabletViewport = function () {
|
||||
return this._isViewport(TABLET_WIDTH);
|
||||
};
|
||||
|
||||
module.exports = utils;
|
||||
@@ -0,0 +1,192 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetModel = require('./widgets/widget-model');
|
||||
var CategoryWidgetModel = require('./widgets/category/category-widget-model');
|
||||
var HistogramWidgetModel = require('./widgets/histogram/histogram-widget-model');
|
||||
var TimeSeriesWidgetModel = require('./widgets/time-series/time-series-widget-model');
|
||||
|
||||
var WIDGETSTYLEPARAMS = {
|
||||
auto_style_allowed: 'autoStyleEnabled'
|
||||
};
|
||||
|
||||
// We create an object with options off the attributes
|
||||
var makeWidgetStyleOptions = function (attrs) {
|
||||
return _.reduce(WIDGETSTYLEPARAMS, function (memo, value, key) {
|
||||
if (attrs[key] !== undefined) {
|
||||
memo[value] = attrs[key];
|
||||
return memo;
|
||||
}
|
||||
}, {});
|
||||
};
|
||||
|
||||
var _checkProperties = function (obj, propertiesArray) {
|
||||
_.each(propertiesArray, function (prop) {
|
||||
if (obj[prop] === undefined) {
|
||||
throw new Error(prop + ' is required');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var extendAttrs = function (attrs, state, hasInitialState) {
|
||||
return _.extend(attrs, state, { hasInitialState: hasInitialState }); // Will overwrite preset attributes with the ones passed on the state
|
||||
};
|
||||
|
||||
var checkAnalysisModel = function (attrs) {
|
||||
if (!(attrs.source instanceof Object) || !attrs.source.cid) {
|
||||
throw new Error('Source must be defined and be an instance of AnalysisModel.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Public API to interact with dashboard widgets.
|
||||
*/
|
||||
var WidgetsService = function (widgetsCollection, dataviews) {
|
||||
this._widgetsCollection = widgetsCollection;
|
||||
this._dataviews = dataviews;
|
||||
};
|
||||
|
||||
WidgetsService.prototype.getCollection = function () {
|
||||
return this._widgetsCollection;
|
||||
};
|
||||
|
||||
WidgetsService.prototype.get = function (id) {
|
||||
return this._widgetsCollection.get(id);
|
||||
};
|
||||
|
||||
WidgetsService.prototype.getList = function () {
|
||||
return this._widgetsCollection.models;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
* @param {String} attrs.title Title rendered on the widget view
|
||||
* @param {String} attrs.column Name of column to use to aggregate
|
||||
* @param {String} attrs.aggregation Name of aggregation operation to apply to get categories
|
||||
* can be any of ['sum', 'count']. Default is 'count'
|
||||
* @param {String} attrs.aggregation_column column to be used for the aggregation operation
|
||||
* it only applies for sum operations.
|
||||
* @param {Object} attrs.source Object with the id of the source analysis node that the widget points to
|
||||
* @param {Object} layer Instance of a layer model (cartodb.js)
|
||||
* @return {CategoryWidgetModel}
|
||||
*/
|
||||
WidgetsService.prototype.createCategoryModel = function (attrs, layer, state) {
|
||||
_checkProperties(attrs, ['title']);
|
||||
var extendedAttrs = extendAttrs(attrs, state, this._widgetsCollection.hasInitialState());
|
||||
checkAnalysisModel(extendedAttrs);
|
||||
|
||||
var dataviewModel = this._dataviews.createCategoryModel(extendedAttrs);
|
||||
|
||||
var ATTRS_NAMES = ['id', 'title', 'order', 'collapsed', 'prefix', 'suffix', 'show_stats', 'show_source', 'style', 'hasInitialState'];
|
||||
var widgetAttrs = _.pick(extendedAttrs, ATTRS_NAMES);
|
||||
var options = makeWidgetStyleOptions(extendedAttrs);
|
||||
|
||||
widgetAttrs.attrsNames = ATTRS_NAMES;
|
||||
|
||||
var widgetModel = new CategoryWidgetModel(widgetAttrs, {
|
||||
dataviewModel: dataviewModel,
|
||||
layerModel: layer
|
||||
}, options);
|
||||
widgetModel.setInitialState(state);
|
||||
this._widgetsCollection.add(widgetModel);
|
||||
|
||||
return widgetModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
* @param {String} attrs.title Title rendered on the widget view
|
||||
* @param {String} attrs.column Name of column
|
||||
* @param {Number} attrs.bins Count of bins
|
||||
* @param {Object} layer Instance of a layer model (cartodb.js)
|
||||
* @return {WidgetModel}
|
||||
*/
|
||||
WidgetsService.prototype.createHistogramModel = function (attrs, layer, state, opts) {
|
||||
_checkProperties(attrs, ['title']);
|
||||
var extendedAttrs = extendAttrs(attrs, state, this._widgetsCollection.hasInitialState());
|
||||
checkAnalysisModel(extendedAttrs);
|
||||
var dataviewModel = this._dataviews.createHistogramModel(extendedAttrs);
|
||||
|
||||
// Default bins attribute was removed from dataViewModel because of time-series aggregation.
|
||||
// Just in case it's needed for histogram models we added it here.
|
||||
if (!dataviewModel.has('bins')) {
|
||||
dataviewModel.set('bins', 10, { silent: true });
|
||||
}
|
||||
|
||||
var attrsNames = ['id', 'title', 'order', 'collapsed', 'bins', 'show_stats', 'show_source', 'normalized', 'style', 'hasInitialState', 'table_name'];
|
||||
var widgetAttrs = _.pick(extendedAttrs, attrsNames);
|
||||
var options = makeWidgetStyleOptions(extendedAttrs);
|
||||
|
||||
widgetAttrs.type = 'histogram';
|
||||
widgetAttrs.attrsNames = attrsNames;
|
||||
|
||||
var widgetModel = new HistogramWidgetModel(widgetAttrs, {
|
||||
dataviewModel: dataviewModel,
|
||||
layerModel: layer
|
||||
}, options);
|
||||
widgetModel.setInitialState(state);
|
||||
this._widgetsCollection.add(widgetModel);
|
||||
|
||||
return widgetModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
* @param {String} attrs.title Title rendered on the widget view
|
||||
* @param {String} attrs.column Name of column
|
||||
* @param {String} attrs.operation Name of operation to use, can be any of ['min', 'max', 'avg', 'sum']
|
||||
* @param {Object} layer Instance of a layer model (cartodb.js)
|
||||
* @return {CategoryWidgetModel}
|
||||
*/
|
||||
WidgetsService.prototype.createFormulaModel = function (attrs, layer, state) {
|
||||
_checkProperties(attrs, ['title']);
|
||||
var extendedAttrs = extendAttrs(attrs, state, this._widgetsCollection.hasInitialState());
|
||||
checkAnalysisModel(extendedAttrs);
|
||||
var dataviewModel = this._dataviews.createFormulaModel(extendedAttrs);
|
||||
|
||||
var ATTRS_NAMES = ['id', 'title', 'order', 'collapsed', 'prefix', 'suffix', 'show_stats', 'show_source', 'description', 'hasInitialState'];
|
||||
var widgetAttrs = _.pick(extendedAttrs, ATTRS_NAMES);
|
||||
widgetAttrs.type = 'formula';
|
||||
widgetAttrs.attrsNames = ATTRS_NAMES;
|
||||
|
||||
var widgetModel = new WidgetModel(widgetAttrs, {
|
||||
dataviewModel: dataviewModel,
|
||||
layerModel: layer
|
||||
});
|
||||
widgetModel.setInitialState(state);
|
||||
this._widgetsCollection.add(widgetModel);
|
||||
|
||||
return widgetModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
* @param {String} attrs.column Name of column that contains
|
||||
* @param {Object} layer Instance of a layer model (cartodb.js)
|
||||
* @param {Number} bins
|
||||
* @return {WidgetModel}
|
||||
*/
|
||||
WidgetsService.prototype.createTimeSeriesModel = function (attrs, layer, state, opts) {
|
||||
// TODO will other kind really work for a time-series?
|
||||
attrs.column_type = attrs.column_type || 'date';
|
||||
checkAnalysisModel(attrs);
|
||||
var dataviewModel = this._dataviews.createHistogramModel(attrs);
|
||||
|
||||
var ATTRS_NAMES = ['id', 'style', 'title', 'normalized', 'animated', 'timezone'];
|
||||
var widgetAttrs = _.pick(attrs, ATTRS_NAMES);
|
||||
widgetAttrs.type = 'time-series';
|
||||
widgetAttrs.attrsNames = ATTRS_NAMES;
|
||||
|
||||
var widgetModel = new TimeSeriesWidgetModel(widgetAttrs, {
|
||||
dataviewModel: dataviewModel,
|
||||
layerModel: layer
|
||||
}, opts);
|
||||
widgetModel.setInitialState(state);
|
||||
this._widgetsCollection.add(widgetModel);
|
||||
|
||||
return widgetModel;
|
||||
};
|
||||
|
||||
WidgetsService.prototype.setWidgetsState = function (state) {
|
||||
this._widgetsCollection.setStates(state);
|
||||
};
|
||||
|
||||
module.exports = WidgetsService;
|
||||
@@ -0,0 +1,120 @@
|
||||
var _ = require('underscore');
|
||||
var d3 = require('d3');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
/**
|
||||
* Animate between two values
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
animateFromValues: function (from, to, className, template, opts) {
|
||||
var $el = this.$(className);
|
||||
var options = opts || {};
|
||||
var formatter = options.formatter || d3.format('0,000');
|
||||
var templateData = options.templateData || {};
|
||||
var debounceWait = options.debounceWait || 500;
|
||||
|
||||
var hasDecimals = (to % 1 === 0);
|
||||
|
||||
var stepValue = function (i) {
|
||||
i = hasDecimals ? Math.round(i) : i;
|
||||
var value = (_.isNaN(i) || i === undefined) ? (options.defaultValue || 0) : formatter(i);
|
||||
var data = _.extend({ value: value }, templateData);
|
||||
$el.text(template(data));
|
||||
};
|
||||
|
||||
if (options.animationSpeed === 0) {
|
||||
stepValue(to);
|
||||
return;
|
||||
}
|
||||
|
||||
stepValue(from);
|
||||
|
||||
var animate = _.debounce(function () {
|
||||
$el.prop('counter', from).stop().animate({ counter: to }, {
|
||||
duration: options.animationSpeed || 500,
|
||||
easing: options.easingMethod || 'swing',
|
||||
step: stepValue
|
||||
});
|
||||
}, debounceWait);
|
||||
|
||||
animate();
|
||||
},
|
||||
|
||||
animateFromCurrentValue: function (value, className, template, opts) {
|
||||
var $el = this.$(className);
|
||||
|
||||
var options = opts || {};
|
||||
var debounceWait = options.debounceWait || 500;
|
||||
|
||||
var to = value;
|
||||
var from = +this.$(className).text();
|
||||
|
||||
var formatter = options.formatter || d3.format('0,000');
|
||||
var templateData = options.templateData || {};
|
||||
|
||||
var hasDecimals = (to % 1 === 0);
|
||||
|
||||
var stepValue = function (i) {
|
||||
i = hasDecimals ? Math.round(i) : i;
|
||||
value = (_.isNaN(i) || i === undefined) ? (options.defaultValue || 0) : formatter(i);
|
||||
var data = _.extend({ value: value }, templateData);
|
||||
$el.text(template(data));
|
||||
};
|
||||
|
||||
if (options.animationSpeed === 0) {
|
||||
stepValue(to);
|
||||
return;
|
||||
}
|
||||
|
||||
stepValue(from);
|
||||
|
||||
var animate = _.debounce(function () {
|
||||
$el.prop('counter', from).stop().animate({ counter: to }, {
|
||||
duration: options.animationSpeed || 500,
|
||||
easing: options.easingMethod || 'swing',
|
||||
step: stepValue
|
||||
});
|
||||
}, debounceWait);
|
||||
|
||||
animate();
|
||||
},
|
||||
|
||||
animateValue: function (model, what, className, template, opts) {
|
||||
var $el = this.$(className);
|
||||
|
||||
var options = opts || {};
|
||||
var debounceWait = options.debounceWait || 500;
|
||||
|
||||
var to = model.get(what);
|
||||
var from = model.previous(what) || 0;
|
||||
|
||||
var formatter = options.formatter || d3.format('0,000');
|
||||
var templateData = options.templateData || {};
|
||||
|
||||
var hasDecimals = (to % 1 === 0);
|
||||
|
||||
var stepValue = function (i) {
|
||||
i = hasDecimals ? Math.round(i) : i;
|
||||
var value = (_.isNaN(i) || i === undefined) ? (options.defaultValue || 0) : formatter(i);
|
||||
var data = _.extend({ value: value }, templateData);
|
||||
$el.text(template(data));
|
||||
};
|
||||
|
||||
if (options.animationSpeed === 0) {
|
||||
stepValue(to);
|
||||
return;
|
||||
}
|
||||
|
||||
stepValue(from);
|
||||
|
||||
var animate = _.debounce(function () {
|
||||
$el.prop('counter', from).stop().animate({ counter: to }, {
|
||||
duration: options.animationSpeed || 500,
|
||||
easing: options.easingMethod || 'swing',
|
||||
step: stepValue
|
||||
});
|
||||
}, debounceWait);
|
||||
|
||||
animate();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
var cdb = require('internal-carto.js');
|
||||
var StyleUtils = require('./style-utils');
|
||||
var CategoryColors = require('./category-colors');
|
||||
var getValue = require('../../util/get-object-value');
|
||||
|
||||
var AutoStyler = cdb.core.Model.extend({
|
||||
initialize: function (dataviewModel, layerModel, options) {
|
||||
this.options = options || {};
|
||||
this.styles = options && options.auto_style;
|
||||
this.dataviewModel = dataviewModel;
|
||||
this.colors = new CategoryColors(this.styles);
|
||||
this.layer = layerModel;
|
||||
},
|
||||
|
||||
getStyle: function () {
|
||||
var style = this.layer.get('initialStyle');
|
||||
if (!style) return;
|
||||
|
||||
AutoStyler.FILL_SELECTORS.forEach(function (item) {
|
||||
style = StyleUtils.changeStyle(style, item, this._getFillColor(item));
|
||||
}.bind(this));
|
||||
|
||||
AutoStyler.OPACITY_SELECTORS.forEach(function (item) {
|
||||
style = StyleUtils.changeStyle(style, item, this._getOpacity());
|
||||
}.bind(this));
|
||||
|
||||
return StyleUtils.replaceWrongSpaceChar(style);
|
||||
},
|
||||
|
||||
_getColor: function () {
|
||||
return getValue(this.styles, 'definition.color');
|
||||
},
|
||||
|
||||
_getOpacity: function () {
|
||||
return getValue(this.styles, 'definition.color.opacity');
|
||||
}
|
||||
});
|
||||
|
||||
AutoStyler.FILL_SELECTORS = ['marker-fill', 'polygon-fill', 'line-color'];
|
||||
AutoStyler.OPACITY_SELECTORS = ['marker-fill-opacity', 'polygon-opacity', 'line-opacity'];
|
||||
|
||||
module.exports = AutoStyler;
|
||||
@@ -0,0 +1,79 @@
|
||||
var _ = require('underscore');
|
||||
var colorScales = [
|
||||
'#7F3C8D',
|
||||
'#11A579',
|
||||
'#3969AC',
|
||||
'#F2B701',
|
||||
'#E73F74'
|
||||
]; // Demo colors
|
||||
|
||||
function getColorRange (styles) {
|
||||
var colorRange = styles && styles.definition &&
|
||||
styles.definition.color &&
|
||||
styles.definition.color.range;
|
||||
|
||||
return colorRange || colorScales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to set categories to each color
|
||||
* - Right now, there is a relation 1 color to 1 category.
|
||||
* - If that category is not available in the new data, that
|
||||
* color will be freed.
|
||||
*
|
||||
*/
|
||||
|
||||
function CategoryColors (styles) {
|
||||
this.updateColors(styles);
|
||||
}
|
||||
|
||||
CategoryColors.prototype.updateColors = function (styles) {
|
||||
var colorRange = getColorRange(styles);
|
||||
this.colors = {};
|
||||
_.each(colorRange, function (c) {
|
||||
this.colors[c] = null;
|
||||
}, this);
|
||||
};
|
||||
|
||||
CategoryColors.prototype.updateData = function (d) {
|
||||
// Remove categories from colors where they are not present anymore
|
||||
_.each(this.colors, function (value, key) {
|
||||
if (!_.contains(d, value)) {
|
||||
this.colors[key] = null;
|
||||
} else {
|
||||
d = _.without(d, value);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Set colors by new categories
|
||||
_.each(d, function (category) {
|
||||
var nextFreeColor = this.getNextAvailableColor();
|
||||
if (nextFreeColor) {
|
||||
this.colors[nextFreeColor] = category;
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
CategoryColors.prototype.getNextAvailableColor = function () {
|
||||
for (var i in this.colors) {
|
||||
if (this.colors[i] === null) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
CategoryColors.prototype.getColorByCategory = function (category) {
|
||||
for (var i in this.colors) {
|
||||
if (this.colors[i] === category) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return '#A5AA99';
|
||||
};
|
||||
|
||||
CategoryColors.prototype.getCategoryByColor = function (color) {
|
||||
return this.colors[color];
|
||||
};
|
||||
|
||||
module.exports = CategoryColors;
|
||||
@@ -0,0 +1,67 @@
|
||||
var _ = require('underscore');
|
||||
var AutoStyler = require('./auto-styler');
|
||||
|
||||
module.exports = AutoStyler.extend({
|
||||
updateStyle: function (style) {
|
||||
this.styles = style.auto_style;
|
||||
this.colors.updateColors(style.auto_style);
|
||||
this.colors.updateData(_.pluck(this.dataviewModel.get('data'), 'name'));
|
||||
},
|
||||
|
||||
_getRange: function () {
|
||||
return _.map(this.dataviewModel.get('data'), function (category) {
|
||||
return this.colors.getColorByCategory(category.name);
|
||||
}, this);
|
||||
},
|
||||
|
||||
getDef: function () {
|
||||
var model = this.dataviewModel;
|
||||
var categories = model.get('data');
|
||||
var range = this._getRange();
|
||||
var definitions = {};
|
||||
|
||||
AutoStyler.FILL_SELECTORS.forEach(function (item) {
|
||||
var definition = {};
|
||||
var geom = item.substring(0, item.indexOf('-'));
|
||||
definition = { color:
|
||||
{ domain: _.pluck(categories, 'name'), range: range, attribute: model.get('column') }
|
||||
};
|
||||
definitions[geom === 'marker' ? 'point' : geom] = definition;
|
||||
});
|
||||
|
||||
return definitions;
|
||||
},
|
||||
|
||||
_getFillColor: function () {
|
||||
var model = this.dataviewModel;
|
||||
var categories = model.get('data');
|
||||
var column = model.get('column');
|
||||
|
||||
return this._getCategoryRamp(categories, column);
|
||||
},
|
||||
|
||||
_getCategoryRamp: function (categories, column) {
|
||||
var ramp = 'ramp([' + column + '], ';
|
||||
|
||||
var catListColors = '';
|
||||
var catListValues = '';
|
||||
|
||||
for (var i = 0; i < categories.length; i++) {
|
||||
var cat = categories[i];
|
||||
var next = i !== categories.length - 1 ? ', ' : '';
|
||||
|
||||
catListColors += '"' + this.colors.getColorByCategory(cat.name) + '"' + next;
|
||||
if (!cat.agg) {
|
||||
if (typeof cat.name !== 'string') {
|
||||
catListValues += cat.name + next;
|
||||
} else {
|
||||
catListValues += '"' + String(cat.name).replace(/"/g, '\\"') + '"' + next;
|
||||
}
|
||||
} else if (i === categories.length - 1) {
|
||||
catListValues = catListValues.substring(0, catListValues.length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
return ramp + '(' + catListColors + '), (' + catListValues + '), \'=\')';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
var Stylers = {
|
||||
category: require('./category'),
|
||||
histogram: require('./histogram')
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
get: function (dataviewModel, layerModel, style) {
|
||||
var AutoStyler = Stylers[dataviewModel.get('type')];
|
||||
if (AutoStyler) {
|
||||
return new AutoStyler(dataviewModel, layerModel, style);
|
||||
} else {
|
||||
throw new Error('dataview type not supported');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
var _ = require('underscore');
|
||||
var cartocolor = require('cartocolor');
|
||||
var AutoStyler = require('./auto-styler');
|
||||
var StyleUtils = require('./style-utils');
|
||||
|
||||
var HistogramAutoStyler = AutoStyler.extend({
|
||||
updateStyle: function (style) {
|
||||
this.styles = style.auto_style;
|
||||
},
|
||||
|
||||
_getFillColor: function (sym) {
|
||||
var custom = this._getColor();
|
||||
var scales = custom || {};
|
||||
|
||||
if (!custom) {
|
||||
var shape = this.dataviewModel.getDistributionType(
|
||||
this.dataviewModel.getUnfilteredDataModel().get('data')
|
||||
);
|
||||
|
||||
scales = HistogramAutoStyler.SCALES_MAP[sym][shape];
|
||||
}
|
||||
|
||||
var ramp = 'ramp([' + this.dataviewModel.get('column') + '], ';
|
||||
var colors = custom
|
||||
? "('" + scales.range.join("', '") + "'), "
|
||||
: 'cartocolor(' + scales.palette + ', ' + this.dataviewModel.get('bins') + '), ';
|
||||
var cuantification = scales.quantification + ')';
|
||||
|
||||
return ramp + colors + cuantification;
|
||||
},
|
||||
|
||||
getDef: function (cartocss) {
|
||||
var definitions = {};
|
||||
var shape = this.dataviewModel.getDistributionType(
|
||||
this.dataviewModel.getUnfilteredDataModel().get('data')
|
||||
);
|
||||
var bins = this.dataviewModel.get('bins');
|
||||
var attr = this.dataviewModel.get('column');
|
||||
var styles = this.styles;
|
||||
var isCustomDefinition = (this.styles && this.styles.custom) || false;
|
||||
|
||||
AutoStyler.FILL_SELECTORS.forEach(function (item) {
|
||||
if (StyleUtils.isPropertyIncluded(cartocss, item)) {
|
||||
var scales = HistogramAutoStyler.SCALES_MAP[item][shape];
|
||||
var geom = item.substring(0, item.indexOf('-'));
|
||||
var definition = {};
|
||||
|
||||
if (scales) {
|
||||
if (isCustomDefinition === true) {
|
||||
definition = _.extend(definition, styles.definition);
|
||||
} else {
|
||||
definition = {
|
||||
color: {
|
||||
range: cartocolor[scales.palette][bins] || cartocolor[scales.palette][Object.keys(cartocolor[scales.palette]).length],
|
||||
quantification: scales.quantification,
|
||||
attribute: attr
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
definitions[geom === 'marker' ? 'point' : geom] = definition;
|
||||
}
|
||||
});
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
HistogramAutoStyler.SCALES_MAP = {
|
||||
'polygon-fill': {
|
||||
'F': {
|
||||
palette: 'PinkYl',
|
||||
quantification: 'equal'
|
||||
},
|
||||
'L': {
|
||||
palette: 'Emrld',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'J': {
|
||||
palette: 'Emrld',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'A': {
|
||||
palette: 'Geyser',
|
||||
quantification: 'quantiles'
|
||||
},
|
||||
'C': {
|
||||
palette: 'Sunset',
|
||||
quantification: 'jenks'
|
||||
},
|
||||
'U': {
|
||||
palette: 'Sunset',
|
||||
quantification: 'jenks'
|
||||
}
|
||||
},
|
||||
'line-color': {
|
||||
'F': {
|
||||
palette: 'PinkYl',
|
||||
quantification: 'equal'
|
||||
},
|
||||
'L': {
|
||||
palette: 'Emrld',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'J': {
|
||||
palette: 'Emrld',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'A': {
|
||||
palette: 'Geyser',
|
||||
quantification: 'quantiles'
|
||||
},
|
||||
'C': {
|
||||
palette: 'Sunset',
|
||||
quantification: 'jenks'
|
||||
},
|
||||
'U': {
|
||||
palette: 'Sunset',
|
||||
quantification: 'jenks'
|
||||
}
|
||||
},
|
||||
'marker-fill': {
|
||||
'F': {
|
||||
palette: 'RedOr',
|
||||
quantification: 'equal'
|
||||
},
|
||||
'L': {
|
||||
palette: 'BluYl',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'J': {
|
||||
palette: 'BluYl',
|
||||
quantification: 'headtails'
|
||||
},
|
||||
'A': {
|
||||
palette: 'Geyser',
|
||||
quantification: 'quantiles'
|
||||
},
|
||||
'C': {
|
||||
palette: 'SunsetDark',
|
||||
quantification: 'jenks'
|
||||
},
|
||||
'U': {
|
||||
palette: 'SunsetDark',
|
||||
quantification: 'jenks'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = HistogramAutoStyler;
|
||||
@@ -0,0 +1,91 @@
|
||||
var _ = require('underscore');
|
||||
var postcss = require('postcss');
|
||||
var stripInlineComments = require('postcss-strip-inline-comments');
|
||||
var SCSSsyntax = require('postcss-scss');
|
||||
|
||||
var OUTLINE_ATTRS = ['line-color', 'line-opacity'];
|
||||
|
||||
function generateCSSTreeFromCartoCSS (cartocss) {
|
||||
return postcss()
|
||||
.use(stripInlineComments)
|
||||
.process(cartocss, { syntax: SCSSsyntax });
|
||||
}
|
||||
|
||||
function isPropertyIncluded (cartocss, attr) {
|
||||
var cssTree = generateCSSTreeFromCartoCSS(cartocss);
|
||||
var root = cssTree.result.root;
|
||||
var propertyIncluded = false;
|
||||
|
||||
if (root) {
|
||||
root.walkDecls(attr, function (node) {
|
||||
var parentNode = node.parent;
|
||||
|
||||
if (!isSelectorRule(parentNode) || isMapnikGeometrySelectorRule(parentNode)) {
|
||||
propertyIncluded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return propertyIncluded;
|
||||
}
|
||||
|
||||
function isSelectorRule (node) {
|
||||
return node.type === 'rule' && node.selector.search(/\[(.)+\]/g) !== -1;
|
||||
}
|
||||
|
||||
function isMapnikGeometrySelectorRule (node) {
|
||||
return isSelectorRule(node) && node.selector.search('mapnik::geometry_type') !== -1;
|
||||
}
|
||||
|
||||
function isOutlineRule (node) {
|
||||
return node.type === 'rule' && node.selector.search('::outline') !== -1;
|
||||
}
|
||||
|
||||
function replaceWrongSpaceChar (cartocss) {
|
||||
return cartocss.replace(new RegExp(String.fromCharCode(160), 'g'), ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change attr style and remove all the duplicates
|
||||
* @param {String} cartocss cartocss original String
|
||||
* @param {String} attr CSS Attribute ex, polygon-fill
|
||||
* @param {String} newStyle New style value ex, red;
|
||||
* @return {String} Cartocss modified String
|
||||
*/
|
||||
function changeStyle (cartocss, attr, newStyle) {
|
||||
if (_.isUndefined(newStyle)) return cartocss;
|
||||
|
||||
var cssTree = generateCSSTreeFromCartoCSS(cartocss);
|
||||
var root = cssTree.result.root;
|
||||
var attributeAlreadyChanged = false;
|
||||
|
||||
if (root) {
|
||||
root.walkDecls(attr, function (node) {
|
||||
var parentNode = node.parent;
|
||||
|
||||
if (!(isOutlineRule(parentNode) && _.contains(OUTLINE_ATTRS, attr))) {
|
||||
if (isSelectorRule(parentNode) || attributeAlreadyChanged) {
|
||||
// If the attribute is inside a conditional selection, it has to be removed
|
||||
node.remove();
|
||||
} else {
|
||||
// If the attribute is inside a regular root (or symbolizer), it just
|
||||
// changes the value
|
||||
node.value = newStyle;
|
||||
attributeAlreadyChanged = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cssTree.css;
|
||||
}
|
||||
|
||||
return cartocss;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
changeStyle: _.memoize(changeStyle, function (css, attr, style) {
|
||||
return css + attr + style;
|
||||
}),
|
||||
isPropertyIncluded: isPropertyIncluded,
|
||||
replaceWrongSpaceChar: replaceWrongSpaceChar
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetModel = require('../widget-model');
|
||||
var LockedCategoriesCollection = require('./locked-categories-collection');
|
||||
|
||||
/**
|
||||
* Model for a category widget
|
||||
*/
|
||||
module.exports = WidgetModel.extend({
|
||||
|
||||
defaults: _.extend({
|
||||
type: 'category',
|
||||
search: false,
|
||||
locked: false
|
||||
},
|
||||
WidgetModel.prototype.defaults
|
||||
),
|
||||
|
||||
defaultState: _.extend({
|
||||
acceptedCategories: [],
|
||||
locked: false,
|
||||
autoStyle: false
|
||||
},
|
||||
WidgetModel.prototype.defaultState
|
||||
),
|
||||
|
||||
initialize: function () {
|
||||
WidgetModel.prototype.initialize.apply(this, arguments);
|
||||
this.lockedCategories = new LockedCategoriesCollection();
|
||||
|
||||
this.listenTo(this.dataviewModel, 'change:allCategoryNames', this._onDataviewAllCategoryNamesChange);
|
||||
|
||||
this.on('change:locked', this._onLockedChange, this);
|
||||
this.on('change:collapsed', this._onCollapsedChange, this);
|
||||
|
||||
if (this.isAutoStyleEnabled()) {
|
||||
this.on('change:style', this._updateAutoStyle, this);
|
||||
}
|
||||
|
||||
this.dataviewModel.filter.on('change', function () {
|
||||
this.set('acceptedCategories', this._acceptedCategories().pluck('name'));
|
||||
}, this);
|
||||
this.dataviewModel.once('change:allCategoryNames', function () {
|
||||
if (this.get('autoStyle')) {
|
||||
this.autoStyle();
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
setupSearch: function () {
|
||||
this.dataviewModel.setupSearch();
|
||||
this.lockedCategories.addItems(this._acceptedCategories().toJSON());
|
||||
this.toggleSearch();
|
||||
},
|
||||
|
||||
toggleSearch: function () {
|
||||
this.set('search', !this.get('search'));
|
||||
},
|
||||
|
||||
enableSearch: function () {
|
||||
this.set('search', true);
|
||||
},
|
||||
|
||||
disableSearch: function () {
|
||||
this.set('search', false);
|
||||
},
|
||||
|
||||
isSearchEnabled: function () {
|
||||
return this.get('search');
|
||||
},
|
||||
|
||||
cleanSearch: function () {
|
||||
this.dataviewModel.cleanSearch();
|
||||
this.lockedCategories.reset([]);
|
||||
},
|
||||
|
||||
autoStyle: function () {
|
||||
// NOTE: maybe not pre-assing colors to categories?
|
||||
this.autoStyler.colors.updateData(this.dataviewModel.get('allCategoryNames'));
|
||||
WidgetModel.prototype.autoStyle.call(this);
|
||||
},
|
||||
|
||||
isLocked: function () {
|
||||
return this.get('locked');
|
||||
},
|
||||
|
||||
canBeLocked: function () {
|
||||
return this.isLocked() || this._acceptedCategories().size() > 0;
|
||||
},
|
||||
|
||||
canApplyLocked: function () {
|
||||
if (this._acceptedCategories().size() !== this.lockedCategories.size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._acceptedCategories().any(function (m) {
|
||||
return !this.lockedCategories.isItemLocked(m.get('name'));
|
||||
}, this);
|
||||
},
|
||||
|
||||
applyLocked: function () {
|
||||
var currentLocked = this.lockedCategories.getItemsName();
|
||||
if (!currentLocked.length) {
|
||||
this.unlockCategories();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.set('locked', true);
|
||||
|
||||
var f = this.dataviewModel.filter;
|
||||
f.cleanFilter(false);
|
||||
f.accept(currentLocked);
|
||||
f.applyFilter();
|
||||
|
||||
this.cleanSearch();
|
||||
},
|
||||
|
||||
lockCategories: function () {
|
||||
this.set('locked', true);
|
||||
this.dataviewModel.fetch();
|
||||
},
|
||||
|
||||
unlockCategories: function () {
|
||||
this.set('locked', false);
|
||||
this.dataviewModel.filter.acceptAll();
|
||||
},
|
||||
|
||||
_onDataviewAllCategoryNamesChange: function (m, names) {
|
||||
if (this.isAutoStyleEnabled()) {
|
||||
if (!this.isAutoStyle()) {
|
||||
this.autoStyler.colors.updateData(names);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onLockedChange: function (m, isLocked) {
|
||||
if (isLocked) {
|
||||
this.dataviewModel.enableFilter();
|
||||
} else {
|
||||
this.dataviewModel.disableFilter();
|
||||
}
|
||||
},
|
||||
|
||||
_acceptedCategories: function () {
|
||||
return this.dataviewModel.filter.acceptedCategories;
|
||||
},
|
||||
|
||||
_onCollapsedChange: function (m, isCollapsed) {
|
||||
this.dataviewModel.set('enabled', !isCollapsed);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="CDB-Widget-header js-header"></div>
|
||||
<div class="CDB-Widget-content CDB-Widget-content--noSidesMargin">
|
||||
<div class="CDB-Widget-listWrapper js-content"></div>
|
||||
</div>
|
||||
<div class="CDB-Widget-footer js-footer"></div>
|
||||
@@ -0,0 +1,125 @@
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var SearchTitleView = require('./title/search-title-view');
|
||||
var CategoryOptionsView = require('./options/options-view');
|
||||
var CategoryItemsView = require('./list/items-view');
|
||||
var CategoryStatsView = require('./stats/stats-view');
|
||||
var CategoryPaginatorView = require('./paginator/paginator-view');
|
||||
var SearchCategoryItemsView = require('./list/search-items-view');
|
||||
var SearchCategoryPaginatorView = require('./paginator/search-paginator-view');
|
||||
var template = require('./content-template.tpl');
|
||||
|
||||
var STARTING_PAGE = 1;
|
||||
|
||||
/**
|
||||
* Content view for category widget
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body',
|
||||
|
||||
_ITEMS_PER_PAGE: 6,
|
||||
|
||||
initialize: function () {
|
||||
this._dataviewModel = this.model.dataviewModel;
|
||||
this._layerModel = this.model.layerModel;
|
||||
|
||||
this._paginatorModel = new Backbone.Model({
|
||||
page: STARTING_PAGE
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.toggleClass('is-collapsed', !!this.model.get('collapsed'));
|
||||
|
||||
this.$el.html(template());
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
if (this.model.get('hasInitialState') === true) {
|
||||
this._onInitialState();
|
||||
} else {
|
||||
this.model.bind('change:hasInitialState', this._onInitialState, this);
|
||||
}
|
||||
this.model.bind('change:collapsed', function (mdl, isCollapsed) {
|
||||
this.$el.toggleClass('is-collapsed', !!isCollapsed);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_onInitialState: function () {
|
||||
this.render();
|
||||
|
||||
if (this.model.get('autoStyle') === true) {
|
||||
this.model.autoStyle();
|
||||
}
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var searchTitle = new SearchTitleView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel
|
||||
});
|
||||
this.$('.js-header').append(searchTitle.render().el);
|
||||
this.addView(searchTitle);
|
||||
|
||||
var stats = new CategoryStatsView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel
|
||||
});
|
||||
this.$('.js-header').append(stats.render().el);
|
||||
this.addView(stats);
|
||||
|
||||
var options = new CategoryOptionsView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel
|
||||
});
|
||||
this.$('.js-content').html(options.render().el);
|
||||
this.addView(options);
|
||||
|
||||
var dataList = new CategoryItemsView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
itemsPerPage: this._ITEMS_PER_PAGE,
|
||||
paginatorModel: this._paginatorModel
|
||||
});
|
||||
this.$('.js-content').append(dataList.render().el);
|
||||
this.addView(dataList);
|
||||
|
||||
var pagination = new CategoryPaginatorView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
itemsPerPage: this._ITEMS_PER_PAGE,
|
||||
paginatorModel: this._paginatorModel
|
||||
});
|
||||
this.$('.js-footer').append(pagination.render().el);
|
||||
this.addView(pagination);
|
||||
|
||||
var searchList = new SearchCategoryItemsView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
itemsPerPage: this._ITEMS_PER_PAGE,
|
||||
paginator: true,
|
||||
paginatorModel: this._paginatorModel
|
||||
});
|
||||
this.$('.js-content').append(searchList.render().el);
|
||||
this.addView(searchList);
|
||||
|
||||
var searchPagination = new SearchCategoryPaginatorView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
itemsPerPage: this._ITEMS_PER_PAGE,
|
||||
paginator: true,
|
||||
paginatorModel: this._paginatorModel
|
||||
});
|
||||
this.$('.js-footer').append(searchPagination.render().el);
|
||||
this.addView(searchPagination);
|
||||
}
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<button type="button" class="CDB-Widget-listItemInner CDB-Widget-listButton js-button <%- isDisabled ? 'is-disabled' : '' %>">
|
||||
<div class="CDB-Widget-contentSpaced">
|
||||
<p class="CDB-Text is-semibold u-upperCase CDB-Size-medium u-ellipsis u-rSpace--xl" title="<%- name %>"><%- name %></p>
|
||||
<p class="CDB-Text CDB-Size-small u-secondaryTextColor" title="<%- value %>"><%- prefix %><%- formattedValue %><%- suffix %></p>
|
||||
</div>
|
||||
<div class="CDB-Widget-progressBar">
|
||||
<div class="CDB-Widget-progressState <%- isAggregated ? 'CDB-Widget-progressState--pattern' : '' %> <%- isAccepted ? 'is-accepted' : '' %>" style="width: <%- percentage %>%; background-color: <%- color %>"></div>
|
||||
</div>
|
||||
</button>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<div class="CDB-Widget-listItemInner <%- isDisabled ? 'is-disabled' : '' %>">
|
||||
<div class="CDB-Widget-contentSpaced">
|
||||
<p class="CDB-Text is-semibold u-upperCase CDB-Size-medium u-ellipsis u-rSpace--xl" title="<%- name %>"><%- name %></p>
|
||||
<p class="CDB-Text CDB-Size-small u-secondaryTextColor" title="<%- value %>"><%- prefix %><%- formattedValue %><%- suffix %></p>
|
||||
</div>
|
||||
<div class="CDB-Widget-progressBar">
|
||||
<div class="CDB-Widget-progressState CDB-Widget-progressState--pattern <%- isAggregated ? 'CDB-Widget-progressState--inactive' : '' %>"
|
||||
style="width: <%- percentage %>%; background-color: <%- color %>"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var formatter = require('../../../../formatter');
|
||||
var _ = require('underscore');
|
||||
var clickableTemplate = require('./item-clickable-template.tpl');
|
||||
var unclickableTemplate = require('./item-unclickable-template.tpl');
|
||||
|
||||
/**
|
||||
* Category list item view
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'li',
|
||||
className: 'CDB-Widget-listItem',
|
||||
|
||||
events: {
|
||||
'click .js-button': '_onItemClick'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.widgetModel = this.options.widgetModel;
|
||||
this.dataviewModel = this.options.dataviewModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var name = this.model.get('name');
|
||||
var value = this.model.get('value');
|
||||
var template = this.model.get('agg') || this.widgetModel.isLocked()
|
||||
? unclickableTemplate
|
||||
: clickableTemplate;
|
||||
var acceptedCategories = this.dataviewModel.filter.getAcceptedCategoryNames();
|
||||
var isAccepted = _.contains(acceptedCategories, name);
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
isAggregated: this.model.get('agg'),
|
||||
name: name,
|
||||
value: value,
|
||||
formattedValue: formatter.formatNumber(value),
|
||||
percentage: ((value / this.dataviewModel.get('max')) * 100),
|
||||
color: this.widgetModel.getColor(name),
|
||||
isDisabled: !this.model.get('selected') ? 'is-disabled' : '',
|
||||
isAccepted: isAccepted,
|
||||
prefix: this.widgetModel.get('prefix'),
|
||||
suffix: this.widgetModel.get('suffix')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change', this.render, this);
|
||||
this.widgetModel.bind('change:search change:prefix change:suffix change:autoStyle', this.render, this);
|
||||
this.add_related_model(this.widgetModel);
|
||||
},
|
||||
|
||||
_onItemClick: function () {
|
||||
this.trigger('itemClicked', this.model, this);
|
||||
}
|
||||
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<button type="button" class="CDB-Widget-listItemInner CDB-Widget-listItemInner--fullSpace CDB-Widget-listButton js-button <%- isDisabled ? 'is-disabled' : '' %>">
|
||||
<span class="CDB-Widget-checkbox <%- isDisabled ? '' : 'is-checked' %>"></span>
|
||||
<div class="u-lSpace u-ellipsis">
|
||||
<div class="CDB-Widget-contentSpaced">
|
||||
<p class="CDB-Text is-semibold u-upperCase CDB-Size-medium u-ellipsis u-rSpace--xl" title="<%- name %>"><%- name %></p>
|
||||
<p class="CDB-Text CDB-Size-small u-secondaryTextColor" title="<%- value %>"><%- prefix %><%- formattedValue %><%- suffix %></p>
|
||||
</div>
|
||||
<div class="CDB-Widget-progressBar">
|
||||
<div class="CDB-Widget-progressState is-accepted" style="width: <%- percentage %>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -0,0 +1,54 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var formatter = require('../../../../formatter');
|
||||
var template = require('./search-item-clickable-template.tpl');
|
||||
|
||||
/**
|
||||
* Category search list view
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
tagName: 'li',
|
||||
className: 'CDB-Widget-listItem',
|
||||
|
||||
events: {
|
||||
'click .js-button': '_onItemClick'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
// This data model comes from the original data in order to get
|
||||
// the max value and set properly the progress bar and add the
|
||||
// necessary suffix and prefix for the item.
|
||||
this.dataviewModel = this.options.dataviewModel;
|
||||
this.widgetModel = this.options.widgetModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var value = this.model.get('value');
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
name: this.model.get('name'),
|
||||
value: value,
|
||||
formattedValue: formatter.formatNumber(value),
|
||||
percentage: ((value / this.dataviewModel.get('max')) * 100),
|
||||
isDisabled: !this.model.get('selected'),
|
||||
prefix: this.widgetModel.get('prefix'),
|
||||
suffix: this.widgetModel.get('suffix')
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:selected', this.render, this);
|
||||
|
||||
this.widgetModel.bind('change:prefix change:suffix', this.render, this);
|
||||
this.add_related_model(this.widgetModel);
|
||||
},
|
||||
|
||||
_onItemClick: function () {
|
||||
this.model.set('selected', !this.model.get('selected'));
|
||||
}
|
||||
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<li class="CDB-Widget-listItem CDB-Widget-listItem--fake"></li>
|
||||
<li class="CDB-Widget-listItem CDB-Widget-listItem--fake"></li>
|
||||
<li class="CDB-Widget-listItem CDB-Widget-listItem--fake"></li>
|
||||
<li class="CDB-Widget-listItem CDB-Widget-listItem--fake"></li>
|
||||
@@ -0,0 +1,175 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var CategoryItemView = require('./item/item-view');
|
||||
var placeholder = require('./items-placeholder-template.tpl');
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'dataviewModel',
|
||||
'widgetModel',
|
||||
'paginatorModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Category list view
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
options: {
|
||||
paginator: false,
|
||||
itemsPerPage: 6
|
||||
},
|
||||
|
||||
className: 'CDB-Widget-list js-list',
|
||||
|
||||
tagName: 'ul',
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
if (!this._dataviewModel.get('sync_on_bbox_change')) {
|
||||
this.$el.addClass('CDB-Widget-list--nodynamic');
|
||||
}
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
var data = this._dataviewModel.getData();
|
||||
var isDataEmpty = _.isEmpty(data) || _.size(data) === 0;
|
||||
|
||||
if (isDataEmpty) {
|
||||
this._renderPlaceholder();
|
||||
} else {
|
||||
this._renderList();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._paginatorModel, 'change:page', this.render);
|
||||
|
||||
this.listenTo(this._widgetModel, 'change:search', this.toggle);
|
||||
this.listenTo(this._widgetModel, 'change:style change:autoStyle', this.render);
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:data', this.render);
|
||||
this.listenTo(this._dataviewModel, 'change:sync_on_bbox_change', this.blockFiltering);
|
||||
},
|
||||
|
||||
_renderPlaceholder: function () {
|
||||
// Change view classes
|
||||
this.$el.addClass('CDB-Widget-list--withBorders CDB-Widget-list--fake');
|
||||
this.$el.append(placeholder());
|
||||
},
|
||||
|
||||
_renderList: function () {
|
||||
this._createList();
|
||||
|
||||
this.$el.removeClass('CDB-Widget-list--withBorders CDB-Widget-list--fake CDB-Widget-list--noresults');
|
||||
|
||||
this._renderItems();
|
||||
},
|
||||
|
||||
_renderItems: function () {
|
||||
var currentPage = this._paginatorModel.get('page');
|
||||
var pageIndex = currentPage - 1; // Transform from 1-based index to zero-based index
|
||||
var items = this.pagesData[pageIndex];
|
||||
|
||||
_.each(items, function (model) {
|
||||
this._addItem(model);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_createList: function () {
|
||||
this.pagesData = {};
|
||||
|
||||
var data = this._getData();
|
||||
var currentIndex = 0;
|
||||
|
||||
data.each(function (model, index) {
|
||||
if (index % this.options.itemsPerPage === 0) {
|
||||
if (index !== 0) {
|
||||
currentIndex += 1;
|
||||
}
|
||||
this.pagesData[currentIndex] = [];
|
||||
}
|
||||
|
||||
this.pagesData[currentIndex].push(model);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_getData: function () {
|
||||
return this._dataviewModel.getData();
|
||||
},
|
||||
|
||||
_addItem: function (model) {
|
||||
var view = new CategoryItemView({
|
||||
model: model,
|
||||
widgetModel: this._widgetModel,
|
||||
dataviewModel: this._dataviewModel
|
||||
});
|
||||
|
||||
view.bind('itemClicked', this._setFilters, this);
|
||||
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_setFilters: function (mdl) {
|
||||
var isSelected = mdl.get('selected');
|
||||
var filter = this._dataviewModel.filter;
|
||||
var clickedName = mdl.get('name');
|
||||
|
||||
if (isSelected) {
|
||||
// If there isn't any filter applied,
|
||||
// clicking over one will turn rest into as "unselected"
|
||||
if (filter.rejectedCategories.size() === 0 &&
|
||||
filter.acceptedCategories.size() === 0
|
||||
) {
|
||||
var data = this._dataviewModel.getData();
|
||||
// Make elements "unselected"
|
||||
data.each(function (m) {
|
||||
var name = m.get('name');
|
||||
if (name !== clickedName) {
|
||||
m.set('selected', false);
|
||||
}
|
||||
});
|
||||
filter.accept(mdl.get('name'));
|
||||
} else {
|
||||
mdl.set('selected', false);
|
||||
filter.reject(clickedName);
|
||||
}
|
||||
} else {
|
||||
mdl.set('selected', true);
|
||||
filter.accept(clickedName);
|
||||
}
|
||||
this._widgetModel.set('acceptedCategories', this._widgetModel._acceptedCategories().pluck('name'));
|
||||
},
|
||||
|
||||
blockFiltering: function (e) {
|
||||
if (e.changed['sync_on_bbox_change']) { // Is dynamic
|
||||
this.$el.removeClass('CDB-Widget-list--nodynamic');
|
||||
} else { // It is not
|
||||
this.$el.addClass('CDB-Widget-list--nodynamic');
|
||||
this._dataviewModel.filter.acceptAll();
|
||||
}
|
||||
},
|
||||
|
||||
removeSelections: function () {
|
||||
this._dataviewModel.filter.acceptAll();
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this[!this._widgetModel.isSearchEnabled() ? 'show' : 'hide']();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$el.removeClass('is-hidden');
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.$el.addClass('is-hidden');
|
||||
}
|
||||
|
||||
});
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<li class="CDB-Widget-listItem">
|
||||
<h4 class="CDB-Text CDB-Size-large">No results</h4>
|
||||
<p class="CDB-Text CDB-Size-medium u-tSpace-xl u-altTextColor">Your search "<%- q %>" didn't match<br/>with any value.</p>
|
||||
<p class="CDB-Text CDB-Size-medium u-tSpace-xl u-altTextColor">Try searching again.</p>
|
||||
</li>
|
||||
@@ -0,0 +1,63 @@
|
||||
var CategoryItemsView = require('./items-view');
|
||||
var WidgetSearchCategoryItemView = require('./item/search-item-view');
|
||||
var placeholder = require('./search-items-no-results-template.tpl');
|
||||
|
||||
/**
|
||||
* Category list view
|
||||
*/
|
||||
module.exports = CategoryItemsView.extend({
|
||||
className: 'CDB-Widget-list is-hidden js-list',
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
var data = this._searchResultsCollection;
|
||||
var isDataEmpty = data.isEmpty() || data.size() === 0;
|
||||
|
||||
if (isDataEmpty) {
|
||||
this._renderPlaceholder();
|
||||
} else {
|
||||
this._renderList();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
CategoryItemsView.prototype._initBinds.apply(this, arguments);
|
||||
this._searchResultsCollection = this._dataviewModel.getSearchResult();
|
||||
this.listenTo(this._searchResultsCollection, 'change:selected', this._onSelectedItemChange);
|
||||
this.listenTo(this._searchResultsCollection, 'reset', this.render);
|
||||
},
|
||||
|
||||
_getData: function () {
|
||||
return this._searchResultsCollection;
|
||||
},
|
||||
|
||||
_renderPlaceholder: function () {
|
||||
this.$el.addClass('CDB-Widget-list--noresults');
|
||||
|
||||
this.$el.html(
|
||||
placeholder({
|
||||
q: this._dataviewModel.getSearchQuery()
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_addItem: function (model) {
|
||||
var view = new WidgetSearchCategoryItemView({
|
||||
model: model,
|
||||
widgetModel: this._widgetModel,
|
||||
dataviewModel: this._dataviewModel
|
||||
});
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this[this._widgetModel.isSearchEnabled() ? 'show' : 'hide']();
|
||||
},
|
||||
|
||||
_onSelectedItemChange: function (model, isSelected) {
|
||||
this._widgetModel.lockedCategories[isSelected ? 'addItem' : 'removeItem'](model);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Locked categories collection
|
||||
*
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
addItem: function (mdl) {
|
||||
if (!this.isItemLocked(mdl.get('name'))) {
|
||||
this.add(mdl);
|
||||
}
|
||||
},
|
||||
|
||||
addItems: function (mdls) {
|
||||
_.each(mdls, function (m) {
|
||||
if (!this.isItemLocked(m.name)) {
|
||||
this.add(m);
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
resetItems: function (mdls) {
|
||||
this.reset(mdls);
|
||||
},
|
||||
|
||||
removeItem: function (mdl) {
|
||||
var lockedItem = this.isItemLocked(mdl.get('name'));
|
||||
if (lockedItem) {
|
||||
this.remove(lockedItem);
|
||||
}
|
||||
},
|
||||
|
||||
removeItems: function () {
|
||||
this.reset([]);
|
||||
},
|
||||
|
||||
isItemLocked: function (name) {
|
||||
return this.find(function (d) {
|
||||
return d.get('name') === name;
|
||||
});
|
||||
},
|
||||
|
||||
getItemsName: function () {
|
||||
return this.pluck('name');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
<% if (isSearchEnabled) { %>
|
||||
<p class="CDB-Text is-semibold CDB-Size-small u-upperCase js-lockCategories"><%- totalLocked %> selected</p>
|
||||
<% } else { %>
|
||||
<p class="CDB-Text is-semibold CDB-Size-small u-upperCase js-textInfo">
|
||||
<% if (isLocked) { %>
|
||||
<%- totalCats %> blocked <button class="CDB-Text CDB-Size-small u-upperCase u-actionTextColor CDB-Widget-link u-lSpace js-unlock">unlock</button>
|
||||
<% } else { %>
|
||||
<% if (noneSelected) { %>
|
||||
None selected
|
||||
<% } else { %>
|
||||
<%- allSelected ? "All selected" : acceptedCats + " selected" %>
|
||||
<% if (canBeLocked) { %>
|
||||
<button class="CDB-Text CDB-Size-small u-upperCase u-actionTextColor CDB-Widget-link u-lSpace js-lock">lock</button>
|
||||
<% }%>
|
||||
<% }%>
|
||||
<% } %>
|
||||
</p>
|
||||
<% if (canSelectAll) { %>
|
||||
<div class="CDB-Widget-filterButtons">
|
||||
<button class="CDB-Text CDB-Size-small u-upperCase u-actionTextColor CDB-Widget-link CDB-Widget-filterButton js-all">all</button>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,75 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./options-template.tpl');
|
||||
|
||||
/**
|
||||
* Category filter view
|
||||
*
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-filter CDB-Widget-contentSpaced CDB-Widget-contentSpaced--sideMargins',
|
||||
|
||||
events: {
|
||||
'click .js-all': '_onSelectAll',
|
||||
'click .js-lock': '_lockCategories',
|
||||
'click .js-unlock': '_unlockCategories'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.dataviewModel = this.options.dataviewModel;
|
||||
this.widgetModel = this.options.widgetModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var acceptedCats = this.dataviewModel.filter.acceptedCategories.size();
|
||||
var rejectedCats = this.dataviewModel.filter.rejectedCategories.size();
|
||||
var areAllRejected = this.dataviewModel.filter.areAllRejected();
|
||||
var totalCats = this.dataviewModel.getData().size();
|
||||
var isLocked = this.widgetModel.isLocked();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
isSearchEnabled: this.widgetModel.isSearchEnabled(),
|
||||
isSearchApplied: this.dataviewModel.isSearchApplied(),
|
||||
isLocked: isLocked,
|
||||
canBeLocked: this.widgetModel.canBeLocked(),
|
||||
allSelected: (rejectedCats === 0 && acceptedCats === 0 && !areAllRejected),
|
||||
canSelectAll: !isLocked && (rejectedCats > 0 || acceptedCats > 0 || areAllRejected),
|
||||
noneSelected: areAllRejected || (!totalCats && !acceptedCats),
|
||||
acceptedCats: acceptedCats,
|
||||
totalLocked: this.widgetModel.lockedCategories.size(),
|
||||
totalCats: totalCats
|
||||
})
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.widgetModel.bind('change:search change:locked', this.render, this);
|
||||
this.widgetModel.lockedCategories.bind('change add remove', this.render, this);
|
||||
this.add_related_model(this.widgetModel);
|
||||
this.add_related_model(this.widgetModel.lockedCategories);
|
||||
|
||||
this.dataviewModel.bind('change:data', this.render, this);
|
||||
this.add_related_model(this.dataviewModel);
|
||||
|
||||
var f = this.dataviewModel.filter;
|
||||
f.acceptedCategories.bind('add remove reset', this.render, this);
|
||||
f.rejectedCategories.bind('add remove reset', this.render, this);
|
||||
this.add_related_model(f.rejectedCategories);
|
||||
this.add_related_model(f.acceptedCategories);
|
||||
},
|
||||
|
||||
_lockCategories: function () {
|
||||
this.widgetModel.lockCategories();
|
||||
},
|
||||
|
||||
_unlockCategories: function () {
|
||||
this.widgetModel.unlockCategories();
|
||||
},
|
||||
|
||||
_onSelectAll: function () {
|
||||
this.dataviewModel.filter.acceptAll();
|
||||
}
|
||||
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<section class="CDB-Widget-pagination">
|
||||
<button class="CDB-Widget-paginationButton u-rSpace--m js-prev">
|
||||
<svg width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g fill="#1785FB">
|
||||
<path d="M5.09530663,4.3528203 C5.12374481,4.37148693 5.15072296,4.39333837 5.17574487,4.41836028 C5.20217765,4.44479306 5.22503325,4.47380933 5.24426546,4.50470442 C5.4081023,4.70181793 5.39325947,4.99960853 5.21412378,5.17874423 C5.15044967,5.24241833 5.07410755,5.28556158 4.99327465,5.30793729 L0.85159735,9.44961458 C0.653891927,9.64732 0.339500325,9.64512912 0.144238179,9.44986697 C-0.0453133946,9.2603154 -0.0509109683,8.93790934 0.144490568,8.7425078 L4.10763557,4.7793628 L0.182869475,0.85459671 C-0.012532062,0.659195174 -0.00693448836,0.336789113 0.182617085,0.14723754 C0.377879231,-0.0480246061 0.692270834,-0.050215493 0.889976256,0.147489929 L5.09530663,4.3528203 Z" transform="translate(2.679181, 4.798552) scale(-1, 1) translate(-2.679181, -4.798552) "></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="CDB-Text CDB-Size-small is-semibold u-upperCase">
|
||||
<span class="u-mainTextColor"><%- currentPage %></span>
|
||||
<span class="u-altTextColor u-lSpace u-rSpace">of</span>
|
||||
<span class="u-mainTextColor"><%- pages %></span>
|
||||
</p>
|
||||
<button class="CDB-Widget-paginationButton u-lSpace--m js-next">
|
||||
<svg width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g fill="#1785FB">
|
||||
<path d="M5.28600662,4.78918286 C5.3144448,4.80784949 5.34142295,4.82970094 5.36644486,4.85472284 C5.39287764,4.88115562 5.41573324,4.91017189 5.43496545,4.94106698 C5.59880229,5.13818049 5.58395946,5.4359711 5.40482376,5.61510679 C5.34114966,5.6787809 5.26480754,5.72192414 5.18397463,5.74429985 L1.04229734,9.88597714 C0.844591917,10.0836826 0.530200314,10.0814917 0.334938168,9.88622953 C0.145386595,9.69667796 0.139789021,9.3742719 0.335190558,9.17887036 L4.29833556,5.21572536 L0.373569464,1.29095927 C0.178167927,1.09555773 0.183765501,0.773151675 0.373317075,0.583600101 C0.56857922,0.388337955 0.882970823,0.386147068 1.08067625,0.58385249 L5.28600662,4.78918286 Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</section>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<div class="CDB-Widget-contentFlex">
|
||||
<button class="u-rSpace--m CDB-Text is-semibold u-upperCase CDB-Size-small js-searchToggle">
|
||||
<div class="CDB-Shape u-iBlock">
|
||||
<span class="CDB-Shape-magnify is-small is-blue"></span>
|
||||
</div>
|
||||
<span class="u-iBlock u-actionTextColor">
|
||||
search in <%- categoriesCount %> categor<%- categoriesCount === 1 ? 'y' : 'ies' %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var defaultTemplate = require('./paginator-template.tpl');
|
||||
var paginationTemplate = require('./pagination-template.tpl');
|
||||
|
||||
var MIN_CATEGORIES = 5;
|
||||
var STARTING_PAGE = 1;
|
||||
var PAGINATION_STEP = 1;
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'dataviewModel',
|
||||
'widgetModel',
|
||||
'paginatorModel'
|
||||
];
|
||||
|
||||
/**
|
||||
* Display paginator for category widget
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
options: {
|
||||
itemsPerPage: 6,
|
||||
template: defaultTemplate,
|
||||
paginator: false
|
||||
},
|
||||
|
||||
className: 'CDB-Widget-nav CDB-Widget-contentSpaced',
|
||||
|
||||
events: {
|
||||
'click .js-searchToggle': '_onSearchClicked',
|
||||
'click .js-next': '_onNextPage',
|
||||
'click .js-prev': '_onPrevPage'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
checkAndBuildOpts(options, REQUIRED_OPTS, this);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._paginatorModel, 'change:page', this.render);
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:categoriesCount', this.render);
|
||||
this.listenTo(this._dataviewModel, 'change:data change:searchData', this._onDataChanged);
|
||||
|
||||
this.listenTo(this._widgetModel, 'change:search', this.toggle);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var categoriesCount = this._dataviewModel.getCount();
|
||||
|
||||
if (categoriesCount > MIN_CATEGORIES) {
|
||||
var template = this.options.template;
|
||||
|
||||
this.$el.html(template({
|
||||
categoriesCount: categoriesCount
|
||||
}));
|
||||
|
||||
if (this.options.paginator) {
|
||||
this.$el.append(paginationTemplate({
|
||||
currentPage: this._paginatorModel.get('page'),
|
||||
pages: this._totalPages()
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
this._paginatorModel.set('page', STARTING_PAGE);
|
||||
}
|
||||
},
|
||||
|
||||
// If current page doesn't exist due to a data change, we should reset it
|
||||
_setPage: function () {
|
||||
var pages = this._totalPages();
|
||||
|
||||
if (this._paginatorModel.get('page') > pages || this._paginatorModel.get('page') < STARTING_PAGE) {
|
||||
this._paginatorModel.set({ page: STARTING_PAGE }, { silent: true });
|
||||
}
|
||||
},
|
||||
|
||||
_onSearchClicked: function () {
|
||||
this._widgetModel.setupSearch();
|
||||
},
|
||||
|
||||
_onDataChanged: function () {
|
||||
this._setPage();
|
||||
this.render();
|
||||
},
|
||||
|
||||
_onPrevPage: function () {
|
||||
this._changePage(-PAGINATION_STEP);
|
||||
},
|
||||
|
||||
_onNextPage: function () {
|
||||
this._changePage(PAGINATION_STEP);
|
||||
},
|
||||
|
||||
_changePage: function (step) {
|
||||
var totalPages = this._totalPages();
|
||||
var currentPage = this._paginatorModel.get('page');
|
||||
var nextPage = currentPage + step;
|
||||
|
||||
if (nextPage > totalPages) {
|
||||
nextPage = STARTING_PAGE;
|
||||
}
|
||||
|
||||
if (nextPage < STARTING_PAGE) {
|
||||
nextPage = totalPages;
|
||||
}
|
||||
|
||||
this._paginatorModel.set('page', nextPage);
|
||||
},
|
||||
|
||||
_totalPages: function () {
|
||||
return Math.ceil(this._dataviewModel.getSize() / this.options.itemsPerPage);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this[ this._widgetModel.isSearchEnabled() ? 'hide' : 'show' ]();
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.$el.addClass('is-hidden');
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$el.removeClass('is-hidden');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
$(window).unbind('resize.' + this.cid);
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<div class="CDB-Widget-contentFlex u-rSpace--xl">
|
||||
<button class="CDB-Text is-semibold u-upperCase CDB-Size-small u-rSpace--m js-searchToggle u-actionTextColor">cancel</button>
|
||||
</div>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
var PaginatorView = require('./paginator-view');
|
||||
var searchTemplate = require('./search-paginator-template.tpl');
|
||||
var paginationTemplate = require('./pagination-template.tpl');
|
||||
|
||||
module.exports = PaginatorView.extend({
|
||||
className: 'CDB-Widget-nav is-hidden CDB-Widget-contentSpaced',
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
var pages = this._totalPages();
|
||||
|
||||
this.$el.html(searchTemplate());
|
||||
|
||||
if (pages > 1) {
|
||||
this.$el.append(paginationTemplate({
|
||||
currentPage: this._paginatorModel.get('page'),
|
||||
pages: pages
|
||||
}));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_totalPages: function () {
|
||||
return Math.ceil(this._dataviewModel.getSearchCount() / this.options.itemsPerPage);
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
this[ !this._widgetModel.isSearchEnabled() ? 'hide' : 'show' ]();
|
||||
},
|
||||
|
||||
_onSearchClicked: function () {
|
||||
this._widgetModel.cleanSearch();
|
||||
this._widgetModel.toggleSearch();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<%- value %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<% if (isSearchEnabled) { %>
|
||||
<% if (isSearchApplied) { %>
|
||||
<dt class="CDB-Widget-infoCount"><%- resultsCount %></dt><dd class="CDB-Widget-infoDescription">found</dd>
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
</dt>
|
||||
<% } else { %>
|
||||
<dt class="CDB-Widget-infoCount"><%- nullsPer %>%</dt><dd class="CDB-Widget-infoDescription"> null rows</dd>
|
||||
<dt class="CDB-Widget-infoCount"><span class="js-cats"><%- catsPer %></span>%</dt><dd class="CDB-Widget-infoDescription">of total</dd>
|
||||
<% } %>
|
||||
@@ -0,0 +1,103 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var formatter = require('../../../formatter');
|
||||
var template = require('./stats-template.tpl');
|
||||
var animationTemplate = require('./cats-template.tpl');
|
||||
var AnimateValues = require('../../animate-values');
|
||||
|
||||
/**
|
||||
* Category stats info view
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-info CDB-Text CDB-Size-small u-secondaryTextColor u-upperCase u-tSpace',
|
||||
tagName: 'dl',
|
||||
|
||||
initialize: function () {
|
||||
this.widgetModel = this.options.widgetModel;
|
||||
this.dataviewModel = this.options.dataviewModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
isSearchEnabled: this.widgetModel.isSearchEnabled(),
|
||||
isSearchApplied: this.dataviewModel.isSearchApplied(),
|
||||
isLocked: this.widgetModel.isLocked(),
|
||||
isOtherAvailable: this.dataviewModel.isOtherAvailable(),
|
||||
resultsCount: this.dataviewModel.getSearchCount(),
|
||||
totalCats: this._getCategoriesSize(),
|
||||
nullsPer: this._getNullPercentage(),
|
||||
catsPer: this._getCurrentCategoriesPercentage()
|
||||
})
|
||||
);
|
||||
|
||||
var animator = new AnimateValues({
|
||||
el: this.$el
|
||||
});
|
||||
|
||||
animator.animateFromValues(this._getPreviousCategoriesPercentage(), this._getCurrentCategoriesPercentage(), '.js-cats',
|
||||
animationTemplate, { defaultValue: '-', animationSpeed: 700, formatter: formatter.formatValue }
|
||||
);
|
||||
|
||||
this._checkVisibility();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.dataviewModel.bind('change:data change:totalCount', this.render, this);
|
||||
this.widgetModel.bind('change:search change:locked', this.render, this);
|
||||
this.widgetModel.bind('change:show_stats change:collapsed', this._checkVisibility, this);
|
||||
this.add_related_model(this.dataviewModel);
|
||||
this.add_related_model(this.widgetModel);
|
||||
},
|
||||
|
||||
_getNullPercentage: function () {
|
||||
var nulls = this.dataviewModel.get('nulls');
|
||||
var total = this.dataviewModel.get('totalCount') || 0;
|
||||
return !nulls ? 0 : ((nulls / total) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
_getPreviousCategoriesPercentage: function () {
|
||||
var total = this.dataviewModel.previous('totalCount') || 0;
|
||||
var data = this.dataviewModel.getPreviousData();
|
||||
return this._getCategoriesPercentage(data, total);
|
||||
},
|
||||
|
||||
_getCurrentCategoriesPercentage: function () {
|
||||
var total = this.dataviewModel.get('totalCount') || 0;
|
||||
var data = this.dataviewModel.getData().toJSON();
|
||||
return this._getCategoriesPercentage(data, total);
|
||||
},
|
||||
|
||||
_getCategoriesPercentage: function (data, total) {
|
||||
if (!total) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var currentTotal = data.reduce(function (memo, mdl) {
|
||||
return !mdl.agg ? (memo + parseFloat(mdl.value)) : memo;
|
||||
}, 0);
|
||||
|
||||
if (!currentTotal) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ((currentTotal / total) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
_getCategoriesSize: function () {
|
||||
return _.pluck(
|
||||
this.dataviewModel.getData().reject(function (mdl) {
|
||||
return mdl.get('agg');
|
||||
}), 'name').length;
|
||||
},
|
||||
|
||||
_checkVisibility: function () {
|
||||
var isVisible = !!this.widgetModel.get('show_stats') && !this.widgetModel.get('collapsed');
|
||||
this.$el.toggle(isVisible);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
<% if (isSearchEnabled) { %>
|
||||
<form class="CDB-Widget-search js-form">
|
||||
<div class="CDB-Shape CDB-Widget-searchLens u-iBlock u-rSpace js-searchIcon">
|
||||
<span class="CDB-Shape-magnify is-small is-blue"></span>
|
||||
</div>
|
||||
<input type="text" class="CDB-Text CDB-Size-large CDB-Widget-textInput CDB-Widget-searchTextInput js-textInput" value="<%- q %>" placeholder="Search by <%- columnName %>"/>
|
||||
<% if (canShowApply) { %>
|
||||
<button type="button" class="CDB-Text is-semibold u-upperCase CDB-Size-small CDB-Widget-searchApply js-applyLocked u-actionTextColor">apply</button>
|
||||
<% } %>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<div class="CDB-Widget-title CDB-Widget-contentSpaced js-title">
|
||||
<h3 class="CDB-Text CDB-Size-large u-ellipsis js-titleText" title="<%- title %>"><%- title %></h3>
|
||||
<div class="CDB-Widget-options CDB-Widget-contentSpaced">
|
||||
<% if (isAutoStyleEnabled) { %>
|
||||
<button class="CDB-Widget-buttonIcon CDB-Widget-buttonIcon--circle js-colors
|
||||
<%- isAutoStyle ? 'is-selected' : '' %>
|
||||
<%- isAutoStyle ? 'js-cancelAutoStyle' : 'js-autoStyle' %>
|
||||
" data-tooltip="
|
||||
<%- isAutoStyle ? 'Remove Auto style' : 'Apply Auto Style' %>
|
||||
">
|
||||
<i class="CDB-IconFont CDB-IconFont-drop CDB-IconFont--small CDB-IconFont--top"></i>
|
||||
</button>
|
||||
<% } %>
|
||||
<button class="CDB-Shape CDB-Widget-actions js-actions u-lSpace" data-tooltip="More options">
|
||||
<div class="CDB-Shape-threePoints is-blue is-small">
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% if (showSource) { %>
|
||||
<dl class="CDB-Widget-info u-tSpace">
|
||||
<div class="u-flex u-alignCenter u-ellipsis">
|
||||
<span class="CDB-Text CDB-Size-small is-semibold u-upperCase" style="color: <%- sourceColor %>;">
|
||||
<%- sourceId %>
|
||||
</span>
|
||||
|
||||
<% if (!isSourceType) { %>
|
||||
<span class="CDB-Text CDB-Size-small u-lSpace--s u-flex" style="color: <%- sourceColor %>;">
|
||||
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-mainTextColor u-lSpace">
|
||||
<%= sourceType %>
|
||||
</span>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-altTextColor u-ellipsis u-lSpace" title="<%= layerName %>">
|
||||
<%= layerName %>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
<% } %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,186 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var DropdownView = require('../../dropdown/widget-dropdown-view');
|
||||
var TipsyTooltipView = require('../../../../builder/components/tipsy-tooltip-view');
|
||||
var template = require('./search-title-template.tpl');
|
||||
var layerColors = require('../../../util/layer-colors');
|
||||
var analyses = require('../../../data/analyses');
|
||||
var escapeHTML = require('../../../util/escape-html');
|
||||
|
||||
/**
|
||||
* Show category title or search any category
|
||||
* + another options for this widget, as in,
|
||||
* colorize categories, lock defined categories...
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'keyup .js-textInput': '_onKeyupInput',
|
||||
'submit .js-form': '_onSubmitForm',
|
||||
'click .js-applyLocked': '_applyLocked',
|
||||
'click .js-autoStyle': '_autoStyle',
|
||||
'click .js-cancelAutoStyle': '_cancelAutoStyle'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
if (!this.options.widgetModel) throw new Error('widgetModel is required');
|
||||
if (!this.options.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!this.options.layerModel) throw new Error('layerModel is required');
|
||||
|
||||
this.model = this.options.widgetModel;
|
||||
this.dataviewModel = this.options.dataviewModel;
|
||||
this.layerModel = this.options.layerModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
var sourceId = this.dataviewModel.get('source').id;
|
||||
var letter = layerColors.letter(sourceId);
|
||||
var sourceColor = layerColors.getColorForLetter(letter);
|
||||
var sourceType = this.dataviewModel.getSourceType() || '';
|
||||
var isSourceType = this.dataviewModel.isSourceType();
|
||||
var layerName = isSourceType
|
||||
? this.model.get('table_name')
|
||||
: this.layerModel.get('layer_name');
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
isCollapsed: this.model.get('collapsed'),
|
||||
isAutoStyleEnabled: this._isAutoStyleButtonVisible(),
|
||||
isAutoStyle: this.model.isAutoStyle(),
|
||||
title: this.model.get('title'),
|
||||
sourceId: sourceId,
|
||||
sourceType: analyses.title(sourceType),
|
||||
isSourceType: isSourceType,
|
||||
showSource: this.model.get('show_source') && letter !== '',
|
||||
sourceColor: sourceColor,
|
||||
layerName: escapeHTML(layerName),
|
||||
columnName: this.dataviewModel.get('column'),
|
||||
q: this.dataviewModel.getSearchQuery(),
|
||||
isLocked: this.model.isLocked(),
|
||||
canBeLocked: this.model.canBeLocked(),
|
||||
isSearchEnabled: this.model.isSearchEnabled(),
|
||||
canShowApply: this.model.canApplyLocked()
|
||||
})
|
||||
);
|
||||
this._initViews();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:search', this._onSearchToggled);
|
||||
this.listenTo(this.model, 'change:title change:collapsed change:autoStyle change:style', this.render);
|
||||
this.listenTo(this.model.lockedCategories, 'change add remove', this.render);
|
||||
this.listenTo(this.dataviewModel, 'change:column', this.render);
|
||||
this.listenTo(this.dataviewModel.filter, 'change', this.render);
|
||||
this.listenTo(this.layerModel, 'change:visible change:cartocss change:layer_name', this.render);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var dropdown = new DropdownView({
|
||||
model: this.model,
|
||||
target: '.js-actions',
|
||||
container: this.$el
|
||||
});
|
||||
this.addView(dropdown);
|
||||
|
||||
var colorsTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-colors'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(colorsTooltip);
|
||||
|
||||
var actionsTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-actions'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(actionsTooltip);
|
||||
},
|
||||
|
||||
_isAutoStyleButtonVisible: function () {
|
||||
return this.model.isAutoStyleEnabled() &&
|
||||
this.layerModel.get('visible') &&
|
||||
this.model.hasColorsAutoStyle();
|
||||
},
|
||||
|
||||
_onSearchToggled: function () {
|
||||
var isSearchEnabled = this.model.isSearchEnabled();
|
||||
this[isSearchEnabled ? '_bindESC' : '_unbindESC']();
|
||||
this.render();
|
||||
if (isSearchEnabled) {
|
||||
this._focusOnInput();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmitForm: function (ev) {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
var q = this.$('.js-textInput').val();
|
||||
if (this.dataviewModel.getSearchQuery() !== q) {
|
||||
this.dataviewModel.setSearchQuery(q);
|
||||
if (this.dataviewModel.isSearchValid()) {
|
||||
this.dataviewModel.applySearch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_focusOnInput: function () {
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self.$('.js-textInput').focus();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_onKeyupInput: _.debounce(
|
||||
function (ev) {
|
||||
var q = this.$('.js-textInput').val();
|
||||
if (ev.keyCode !== 13 && ev.keyCode !== 27 && q !== '') {
|
||||
this._onSubmitForm();
|
||||
}
|
||||
}, 250
|
||||
),
|
||||
|
||||
_bindESC: function () {
|
||||
$(document).bind('keyup.' + this.cid, _.bind(this._onKeyUp, this));
|
||||
},
|
||||
|
||||
_unbindESC: function () {
|
||||
$(document).unbind('keyup.' + this.cid);
|
||||
},
|
||||
|
||||
_onKeyUp: function (ev) {
|
||||
if (ev.keyCode === 27) {
|
||||
this._cancelSearch();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_applyLocked: function () {
|
||||
this.model.toggleSearch();
|
||||
this.model.applyLocked();
|
||||
},
|
||||
|
||||
_autoStyle: function () {
|
||||
this.model.autoStyle();
|
||||
},
|
||||
|
||||
_cancelAutoStyle: function () {
|
||||
this.model.cancelAutoStyle();
|
||||
},
|
||||
|
||||
_cancelSearch: function () {
|
||||
this.model.cleanSearch();
|
||||
this.model.disableSearch();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._unbindESC();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
<ul class="CDB-Dropdown-list CDB-Text CDB-Size-medium">
|
||||
|
||||
<% if (flags.canCollapse) { %>
|
||||
<li class="CDB-Dropdown-item">
|
||||
<button class="CDB-Dropdown-link js-toggleCollapsed">
|
||||
Toggle widget
|
||||
<div class="CDB-Dropdown-toggle">
|
||||
<% if (collapsed) { %>
|
||||
<input class="CDB-Toggle js-inputCollapsed" checked="checked" type="checkbox" name="collapsed">
|
||||
<% } else { %>
|
||||
<input class="CDB-Toggle js-inputCollapsed" type="checkbox" name="collapsed">
|
||||
<% } %>
|
||||
<span class="u-iBlock CDB-ToggleFace"></span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (flags.localTimezone) { %>
|
||||
<li class="CDB-Dropdown-item">
|
||||
<button class="CDB-Dropdown-link js-toggleLocalTimezone">
|
||||
Local Time Zone
|
||||
<div class="CDB-Dropdown-toggle">
|
||||
<% if (local_timezone) { %>
|
||||
<input class="CDB-Toggle u-iBlock js-localTimezone" checked="checked" type="checkbox" name="localtimezone">
|
||||
<% } else { %>
|
||||
<input class="CDB-Toggle u-iBlock js-localTimezone" type="checkbox" name="localtimezone">
|
||||
<% } %>
|
||||
<span class="u-iBlock CDB-ToggleFace"></span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (flags.normalizeHistogram) { %>
|
||||
<li class="CDB-Dropdown-item">
|
||||
<button class="CDB-Dropdown-link js-toggleNormalized">
|
||||
Show totals
|
||||
<div class="CDB-Dropdown-toggle">
|
||||
<% if (normalized) { %>
|
||||
<input class="CDB-Toggle u-iBlock js-inputNormalized" type="checkbox" name="normalized">
|
||||
<% } else { %>
|
||||
<input class="CDB-Toggle u-iBlock js-inputNormalized" checked="checked" type="checkbox" name="normalized">
|
||||
<% } %>
|
||||
<span class="u-iBlock CDB-ToggleFace"></span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (show_options) { %>
|
||||
<li class="CDB-Dropdown-item">
|
||||
<button type="button" class="CDB-Dropdown-link u-ellipsis u-actionTextColor js-editWidget" title="Edit">Edit</button>
|
||||
</li>
|
||||
<li class="CDB-Dropdown-item">
|
||||
<button type="button" class="CDB-Dropdown-link u-ellipsis u-errorTextColor js-removeWidget" title="Delete...">Delete...</button>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
@@ -0,0 +1,168 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./template.tpl');
|
||||
var DropdownOverlay = require('../../../builder/components/dropdown-overlay/dropdown-overlay-view');
|
||||
|
||||
/**
|
||||
* Standard widget dropdown view
|
||||
*
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'CDB-Dropdown',
|
||||
|
||||
events: {
|
||||
'click .js-toggleLocalTimezone': '_toggleLocalTimezone',
|
||||
'click .js-toggleNormalized': '_toggleNormalized',
|
||||
'click .js-toggleCollapsed': '_toggleCollapsed',
|
||||
'click .js-removeWidget': '_removeWidget',
|
||||
'click .js-editWidget': '_editWidget'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.target) {
|
||||
throw new Error('target is not defined');
|
||||
}
|
||||
|
||||
this._target = this.options.target;
|
||||
this._$container = this.options.container;
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var flags = _.defaults(
|
||||
this.options.flags || {}, {
|
||||
canCollapse: true
|
||||
}
|
||||
);
|
||||
|
||||
var templateData = _.defaults({},
|
||||
this.model.attributes, {
|
||||
flags: flags
|
||||
}, {
|
||||
'local_timezone': false,
|
||||
'normalized': false,
|
||||
'collapsed': false,
|
||||
'show_options': false
|
||||
}
|
||||
);
|
||||
|
||||
this.$el.html(template(templateData));
|
||||
|
||||
this._dropdownOverlay = new DropdownOverlay({
|
||||
visible: true,
|
||||
onClickAction: this._onGlobalClick.bind(this)
|
||||
});
|
||||
this.addView(this._dropdownOverlay);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.add_related_model(this.model);
|
||||
|
||||
this.model.bind('change:widget_dropdown_open', this._onChangeOpen, this);
|
||||
|
||||
this._$container.delegate(this._target, 'click', _.bind(this._toggle, this));
|
||||
},
|
||||
|
||||
_removeWidget: function () {
|
||||
this.model.trigger('removeWidget', this.model);
|
||||
this._toggle();
|
||||
},
|
||||
|
||||
_editWidget: function () {
|
||||
this.model.trigger('editWidget', this.model);
|
||||
this._toggle();
|
||||
},
|
||||
|
||||
_bindESC: function () {
|
||||
$(document).bind('keyup.' + this.cid, _.bind(this._onKeyUp, this));
|
||||
},
|
||||
|
||||
_unbindESC: function () {
|
||||
$(document).unbind('keyup.' + this.cid);
|
||||
},
|
||||
|
||||
_onGlobalClick: function (ev) {
|
||||
this.model.set('widget_dropdown_open', false);
|
||||
},
|
||||
|
||||
_onKeyUp: function (ev) {
|
||||
if (ev.keyCode === 27) {
|
||||
this.model.set('widget_dropdown_open', false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeOpen: function () {
|
||||
if (this.model.get('widget_dropdown_open')) {
|
||||
this._open();
|
||||
} else {
|
||||
this._close();
|
||||
}
|
||||
},
|
||||
|
||||
_toggleCollapsed: function () {
|
||||
var collapsed = !this.model.get('collapsed');
|
||||
this.model.set('collapsed', collapsed);
|
||||
this._toggle();
|
||||
},
|
||||
|
||||
_toggleNormalized: function () {
|
||||
var normalized = !this.model.get('normalized');
|
||||
this.model.set('normalized', normalized);
|
||||
this._toggle();
|
||||
},
|
||||
|
||||
_toggleLocalTimezone: function () {
|
||||
var localTimezone = !this.model.get('local_timezone');
|
||||
this.model.set('local_timezone', localTimezone);
|
||||
this._toggle();
|
||||
},
|
||||
|
||||
_open: function () {
|
||||
this._bindESC();
|
||||
|
||||
this.render();
|
||||
this._$container.append(this.$el);
|
||||
this.$el.show();
|
||||
|
||||
this._adjustVerticalPosition();
|
||||
},
|
||||
|
||||
_adjustVerticalPosition: function () {
|
||||
if (this._getDropdownBottom() > this._getBodyHeight()) {
|
||||
this.$el.addClass('has-top-position');
|
||||
}
|
||||
},
|
||||
|
||||
_getDropdownBottom: function () {
|
||||
return this.$el.offset().top + this.$el.height();
|
||||
},
|
||||
|
||||
_getBodyHeight: function () {
|
||||
return $('body').height();
|
||||
},
|
||||
|
||||
_close: function () {
|
||||
this._unbindESC();
|
||||
this._dropdownOverlay && this._dropdownOverlay.clean();
|
||||
this.$el.hide();
|
||||
this.$el.removeClass('has-top-position');
|
||||
},
|
||||
|
||||
_toggle: function () {
|
||||
this.model.set('widget_dropdown_open', !this.model.get('widget_dropdown_open'));
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.model.set('widget_dropdown_open', false);
|
||||
this._unbindESC();
|
||||
this._dropdownOverlay && this._dropdownOverlay.clean();
|
||||
this._$container.undelegate(this._target, 'click');
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<%= prefix %><%= value %><%= suffix %>
|
||||
@@ -0,0 +1,132 @@
|
||||
var _ = require('underscore');
|
||||
var d3 = require('d3');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var formatter = require('../../formatter');
|
||||
var template = require('./template.tpl');
|
||||
var DropdownView = require('../dropdown/widget-dropdown-view');
|
||||
var animationTemplate = require('./animation-template.tpl');
|
||||
var AnimateValues = require('../animate-values.js');
|
||||
var layerColors = require('../../util/layer-colors');
|
||||
var Analyses = require('../../data/analyses');
|
||||
var escapeHTML = require('../../util/escape-html');
|
||||
var unescapeHTML = require('../../util/unescape-html');
|
||||
var TipsyTooltipView = require('../../../builder/components/tipsy-tooltip-view');
|
||||
|
||||
/**
|
||||
* Default widget content view:
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body',
|
||||
|
||||
initialize: function () {
|
||||
this._dataviewModel = this.model.dataviewModel;
|
||||
this._layerModel = this.model.layerModel;
|
||||
|
||||
if (this.model.get('hasInitialState') === true) {
|
||||
this._initBinds();
|
||||
} else {
|
||||
this.listenToOnce(this.model, 'change:hasInitialState', this._onInitialState);
|
||||
}
|
||||
},
|
||||
|
||||
_onInitialState: function () {
|
||||
this._initBinds();
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
var value = this._dataviewModel.get('data');
|
||||
|
||||
var format = function (value) {
|
||||
var formatter = d3.format('0,000');
|
||||
|
||||
if (_.isNumber(value)) {
|
||||
return formatter(value.toFixed(2));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
var nulls = (!_.isUndefined(this._dataviewModel.get('nulls')) && formatter.formatNumber(this._dataviewModel.get('nulls'))) || '-';
|
||||
var isCollapsed = this.model.get('collapsed');
|
||||
|
||||
var prefix = this.model.get('prefix');
|
||||
var suffix = this.model.get('suffix');
|
||||
|
||||
var sourceId = this._dataviewModel.get('source').id;
|
||||
var letter = layerColors.letter(sourceId);
|
||||
var sourceColor = layerColors.getColorForLetter(letter);
|
||||
var sourceType = this._dataviewModel.getSourceType() || '';
|
||||
var isSourceType = this._dataviewModel.isSourceType();
|
||||
var layerName = isSourceType
|
||||
? this.model.get('table_name')
|
||||
: this._layerModel.get('layer_name');
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
title: this.model.get('title'),
|
||||
sourceId: sourceId,
|
||||
sourceType: Analyses.title(sourceType),
|
||||
isSourceType: isSourceType,
|
||||
showStats: this.model.get('show_stats'),
|
||||
showSource: this.model.get('show_source') && letter !== '',
|
||||
operation: this._dataviewModel.get('operation'),
|
||||
value: value,
|
||||
formatedValue: format(value),
|
||||
description: this.model.get('description'),
|
||||
nulls: nulls,
|
||||
prefix: unescapeHTML(prefix),
|
||||
suffix: unescapeHTML(suffix),
|
||||
isCollapsed: isCollapsed,
|
||||
sourceColor: sourceColor,
|
||||
layerName: escapeHTML(layerName)
|
||||
})
|
||||
);
|
||||
|
||||
var animator = new AnimateValues({
|
||||
el: this.$el
|
||||
});
|
||||
|
||||
animator.animateValue(
|
||||
this._dataviewModel,
|
||||
'data',
|
||||
'.js-value',
|
||||
animationTemplate,
|
||||
{
|
||||
animationSpeed: 700,
|
||||
formatter: format,
|
||||
templateData: {
|
||||
prefix: unescapeHTML(prefix),
|
||||
suffix: unescapeHTML(suffix)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.$el.toggleClass('is-collapsed', !!isCollapsed);
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:title change:description change:collapsed change:prefix change:suffix', this.render);
|
||||
this.listenTo(this._dataviewModel, 'change:data', this.render);
|
||||
this.listenTo(this._layerModel, 'change:layer_name', this.render);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var dropdown = new DropdownView({
|
||||
model: this.model,
|
||||
target: '.js-actions',
|
||||
container: this.$('.js-header')
|
||||
});
|
||||
this.addView(dropdown);
|
||||
|
||||
var actionsTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-actions'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(actionsTooltip);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<div class="CDB-Widget-listItem--fake"></div>
|
||||
@@ -0,0 +1,54 @@
|
||||
<div class="CDB-Widget-header js-header">
|
||||
<div class="CDB-Widget-title CDB-Widget-contentSpaced">
|
||||
<h3 class="CDB-Text CDB-Size-large u-ellipsis js-title <%- isCollapsed ? 'is-collapsed' : '' %>"><%- title %></h3>
|
||||
<div class="CDB-Widget-options">
|
||||
<button class="CDB-Shape CDB-Widget-actions js-actions" data-tooltip="More options">
|
||||
<div class="CDB-Shape-threePoints is-blue is-small">
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% if (showSource) { %>
|
||||
<dl class="CDB-Widget-info u-tSpace">
|
||||
<div class="u-flex u-alignCenter u-ellipsis">
|
||||
<span class="CDB-Text CDB-Size-small is-semibold u-upperCase" style="color: <%- sourceColor %>;">
|
||||
<%- sourceId %>
|
||||
</span>
|
||||
|
||||
<% if (!isSourceType) { %>
|
||||
<span class="CDB-Text CDB-Size-small u-lSpace--s u-flex" style="color: <%- sourceColor %>;">
|
||||
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-mainTextColor u-lSpace">
|
||||
<%= sourceType %>
|
||||
</span>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-altTextColor u-ellipsis u-lSpace" title="<%= layerName %>">
|
||||
<%= layerName %>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
<% } %>
|
||||
<% if (showStats) { %>
|
||||
<dl class="CDB-Widget-info CDB-Text CDB-Size-small u-secondaryTextColor u-upperCase u-tSpace">
|
||||
<dt class="CDB-Widget-infoCount"><%- nulls %></dt><dd class="CDB-Widget-infoDescription">null rows</dd>
|
||||
</dl>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="CDB-Widget-content CDB-Widget-content--formula">
|
||||
<% if (_.isNumber(value)) { %>
|
||||
<h4 class="CDB-Text CDB-Size-huge <%- !isCollapsed ? 'js-value' : '' %>" title="<%- value %>">
|
||||
<%= prefix %><%- value %><%= suffix %>
|
||||
</h4>
|
||||
<% if (description) { %>
|
||||
<p class="CDB-Text CDB-Size-small u-tSpace js-description"><%- description %></p>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="CDB-Widget-listItem--fake"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%- value %> <%- suffix %>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,699 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var formatter = require('../../formatter');
|
||||
var HistogramTitleView = require('./histogram-title-view');
|
||||
var HistogramChartView = require('./chart');
|
||||
var placeholder = require('./placeholder.tpl');
|
||||
var template = require('./content.tpl');
|
||||
var DropdownView = require('../dropdown/widget-dropdown-view');
|
||||
var TipsyTooltipView = require('../../../builder/components/tipsy-tooltip-view');
|
||||
var AnimateValues = require('../animate-values.js');
|
||||
var animationTemplate = require('./animation-template.tpl');
|
||||
var layerColors = require('../../util/layer-colors');
|
||||
var analyses = require('../../data/analyses');
|
||||
var escapeHTML = require('../../util/escape-html');
|
||||
|
||||
var TABLET_VIEWPORT_BREAKPOINT = 1200;
|
||||
|
||||
/**
|
||||
* Widget content view for a histogram
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body',
|
||||
|
||||
defaults: {
|
||||
chartHeight: 48 + 20 + 4
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-clear': '_resetWidget',
|
||||
'click .js-zoom': '_zoom'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this._dataviewModel = this.model.dataviewModel;
|
||||
this._layerModel = this.model.layerModel;
|
||||
this._originalData = this._dataviewModel.getUnfilteredDataModel();
|
||||
this.filter = this._dataviewModel.filter;
|
||||
this.lockedByUser = false;
|
||||
this._numberOfFilters = 0;
|
||||
this._initStateApplied = false;
|
||||
|
||||
if (this._originalData.get('hasBeenFetched')) {
|
||||
this._initBinds();
|
||||
} else {
|
||||
this.listenToOnce(this._originalData, 'change:hasBeenFetched', function () {
|
||||
this._initBinds();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this._onWindowResized.bind(this), { passive: true });
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this._unbinds();
|
||||
|
||||
this.$el.toggleClass('is-collapsed', !!this.model.get('collapsed'));
|
||||
|
||||
var data = this._dataviewModel.getData();
|
||||
var hasNulls = this._dataviewModel.hasNulls();
|
||||
var originalData = this._originalData.getData();
|
||||
var isDataEmpty = !_.size(data) && !_.size(originalData);
|
||||
|
||||
var sourceId = this._dataviewModel.get('source').id;
|
||||
var letter = layerColors.letter(sourceId);
|
||||
var sourceColor = layerColors.getColorForLetter(letter);
|
||||
var sourceType = this._dataviewModel.getSourceType() || '';
|
||||
var isSourceType = this._dataviewModel.isSourceType();
|
||||
var layerName = isSourceType
|
||||
? this.model.get('table_name')
|
||||
: this._layerModel.get('layer_name');
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
title: this.model.get('title'),
|
||||
sourceId: sourceId,
|
||||
sourceType: analyses.title(sourceType),
|
||||
isSourceType: isSourceType,
|
||||
showStats: this.model.get('show_stats'),
|
||||
showNulls: hasNulls,
|
||||
showSource: this.model.get('show_source') && letter !== '',
|
||||
itemsCount: !isDataEmpty ? data.length : '-',
|
||||
isCollapsed: !!this.model.get('collapsed'),
|
||||
sourceColor: sourceColor,
|
||||
layerName: escapeHTML(layerName)
|
||||
})
|
||||
);
|
||||
|
||||
if (isDataEmpty) {
|
||||
this._addPlaceholder();
|
||||
this._initTitleView();
|
||||
} else {
|
||||
this._setupBindings();
|
||||
this._initViews();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._initTitleView();
|
||||
this._initDropdownView();
|
||||
this._renderMiniChart();
|
||||
this._renderMainChart();
|
||||
this._renderAllValues();
|
||||
},
|
||||
|
||||
_onWindowResized: function () {
|
||||
if (window.innerWidth < TABLET_VIEWPORT_BREAKPOINT) {
|
||||
this.histogramChartView && this.histogramChartView.forceResize();
|
||||
}
|
||||
},
|
||||
|
||||
_initDropdownView: function () {
|
||||
var dropdown = new DropdownView({
|
||||
model: this.model,
|
||||
target: '.js-actions',
|
||||
container: this.$('.js-header'),
|
||||
flags: {
|
||||
normalizeHistogram: true
|
||||
}
|
||||
});
|
||||
|
||||
this.addView(dropdown);
|
||||
},
|
||||
|
||||
_toggleTooltip: function (event) {
|
||||
if (!event || !event.target) {
|
||||
return this._clearTooltip();
|
||||
}
|
||||
|
||||
this._showTooltip(event);
|
||||
},
|
||||
|
||||
_showTooltip: function (event) {
|
||||
if (this.tooltip && event && this.tooltip.getElement() === event.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearTooltip();
|
||||
|
||||
this.tooltip = new TipsyTooltipView({
|
||||
el: event.target,
|
||||
title: 'data-tooltip',
|
||||
trigger: 'manual'
|
||||
});
|
||||
|
||||
this.tooltip.showTipsy();
|
||||
this.addView(this.tooltip);
|
||||
},
|
||||
|
||||
_clearTooltip: function () {
|
||||
if (!this.tooltip) return;
|
||||
|
||||
this.tooltip.hideTipsy();
|
||||
this.removeView(this.tooltip);
|
||||
this.tooltip = undefined;
|
||||
},
|
||||
|
||||
_initTitleView: function () {
|
||||
var titleView = new HistogramTitleView({
|
||||
widgetModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel
|
||||
});
|
||||
|
||||
this.$('.js-title').append(titleView.render().el);
|
||||
this.addView(titleView);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:collapsed', this.render);
|
||||
this.listenTo(this.model, 'change:normalized', this._onNormalizedChanged);
|
||||
|
||||
if (this.model.get('hasInitialState') === true) {
|
||||
this._setInitialState();
|
||||
} else {
|
||||
this.listenTo(this.model, 'change:hasInitialState', this._setInitialState);
|
||||
}
|
||||
|
||||
this.listenTo(this._layerModel, 'change:layer_name', this.render);
|
||||
},
|
||||
|
||||
_setInitialState: function () {
|
||||
var data = this._dataviewModel.getData();
|
||||
if (data.length === 0) {
|
||||
this._dataviewModel.once('change:data', this._onInitialState, this);
|
||||
} else {
|
||||
this._onInitialState();
|
||||
}
|
||||
},
|
||||
|
||||
_onNormalizedChanged: function () {
|
||||
var normalized = this.model.get('normalized');
|
||||
this.histogramChartView.setNormalized(normalized);
|
||||
this.miniHistogramChartView.setNormalized(normalized);
|
||||
},
|
||||
|
||||
_onInitialState: function () {
|
||||
this.add_related_model(this._dataviewModel);
|
||||
this.render();
|
||||
|
||||
if (this.model.get('autoStyle') === true) {
|
||||
this.model.autoStyle();
|
||||
}
|
||||
|
||||
if (this._hasRange()) {
|
||||
this._numberOfFilters = 1;
|
||||
}
|
||||
|
||||
// in order to calculate the stats right, we simulate the full zommmed range
|
||||
if (this._isZoomed()) {
|
||||
this._numberOfFilters = 2;
|
||||
}
|
||||
|
||||
if (this._numberOfFilters !== 0) {
|
||||
this._setInitialRange();
|
||||
} else {
|
||||
this._completeInitialState();
|
||||
}
|
||||
},
|
||||
|
||||
_setupRange: function (data, min, max, updateCallback) {
|
||||
var lo = 0;
|
||||
var hi = data.length;
|
||||
var startMin;
|
||||
var startMax;
|
||||
|
||||
if (_.isNumber(min)) {
|
||||
startMin = _.findWhere(data, {start: min});
|
||||
lo = startMin ? startMin.bin : 0;
|
||||
}
|
||||
|
||||
if (_.isNumber(max)) {
|
||||
startMax = _.findWhere(data, {end: max});
|
||||
hi = startMax ? startMax.bin + 1 : data.length;
|
||||
}
|
||||
|
||||
if ((lo && lo !== 0) || (hi && hi !== data.length)) {
|
||||
this.filter.setRange(
|
||||
data[lo].start,
|
||||
data[hi - 1].end
|
||||
);
|
||||
this._onChangeFilterEnabled();
|
||||
this.histogramChartView.selectRange(lo, hi);
|
||||
}
|
||||
|
||||
updateCallback && updateCallback(lo, hi);
|
||||
},
|
||||
|
||||
_setInitialRange: function () {
|
||||
var data = this._dataviewModel.getData();
|
||||
var min = this.model.get('min');
|
||||
var max = this.model.get('max');
|
||||
var filterEnabled = (min !== undefined || max !== undefined);
|
||||
|
||||
this._setupRange(data, min, max, function (lo, hi) {
|
||||
this.model.set({ filter_enabled: filterEnabled, lo_index: lo, hi_index: hi });
|
||||
this._dataviewModel.bind('change:data', this._onHistogramDataChanged, this);
|
||||
this._initStateApplied = true;
|
||||
|
||||
// If zoomed, we open the zoom level. Internally it does a fetch, so a change:data is triggered
|
||||
// and the _onHistogramDataChanged callback will do the _updateStats call.
|
||||
if (this._isZoomed()) {
|
||||
this._onChangeZoomEnabled();
|
||||
this._onZoomIn();
|
||||
} else {
|
||||
this._updateStats();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_completeInitialState: function () {
|
||||
this._dataviewModel.bind('change:data', this._onHistogramDataChanged, this);
|
||||
this._initStateApplied = true;
|
||||
this._updateStats();
|
||||
},
|
||||
|
||||
_isZoomed: function () {
|
||||
return this.model.get('zoomed');
|
||||
},
|
||||
|
||||
_hasRange: function () {
|
||||
var min = this.model.get('min');
|
||||
var max = this.model.get('max');
|
||||
|
||||
return (min != null || max != null);
|
||||
},
|
||||
|
||||
_onHistogramDataChanged: function () {
|
||||
// When the histogram is zoomed, we don't need to rely
|
||||
// on the change url to update the histogram
|
||||
// TODO the widget should not know about the URL… could this state be got from the dataview model somehow?
|
||||
if (this._dataviewModel.changed.url && this._isZoomed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the action was initiated by the user
|
||||
// don't replace the stored data
|
||||
if (this.lockedByUser) {
|
||||
this.lockedByUser = false;
|
||||
} else {
|
||||
if (!this._isZoomed()) {
|
||||
this.histogramChartView.showShadowBars();
|
||||
this.miniHistogramChartView.replaceData(this._dataviewModel.getData());
|
||||
} else {
|
||||
this._filteredData = this._dataviewModel.getData();
|
||||
}
|
||||
this.histogramChartView.replaceData(this._dataviewModel.getData());
|
||||
}
|
||||
|
||||
if (this.unsettingRange) {
|
||||
this._unsetRange();
|
||||
}
|
||||
|
||||
this._updateStats();
|
||||
},
|
||||
|
||||
_unsetRange: function () {
|
||||
this.unsettingRange = false;
|
||||
this.histogramChartView.replaceData(this._dataviewModel.getData());
|
||||
this.model.set({ lo_index: null, hi_index: null });
|
||||
|
||||
if (!this._isZoomed()) {
|
||||
this.histogramChartView.showShadowBars();
|
||||
}
|
||||
},
|
||||
|
||||
_addPlaceholder: function () {
|
||||
this.$('.js-content').append(placeholder());
|
||||
},
|
||||
|
||||
_renderMainChart: function () {
|
||||
this.histogramChartView = new HistogramChartView(({
|
||||
type: 'histogram',
|
||||
margin: { top: 4, right: 4, bottom: 4, left: 4 },
|
||||
hasHandles: true,
|
||||
hasAxisTip: true,
|
||||
chartBarColor: this.model.getColor() || '#9DE0AD',
|
||||
width: this.canvasWidth,
|
||||
height: this.defaults.chartHeight,
|
||||
data: this._dataviewModel.getData(),
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel,
|
||||
originalData: this._originalData,
|
||||
displayShadowBars: !this.model.get('normalized'),
|
||||
normalized: this.model.get('normalized'),
|
||||
widgetModel: this.model
|
||||
}));
|
||||
|
||||
this.$('.js-chart').append(this.histogramChartView.el);
|
||||
this.addView(this.histogramChartView);
|
||||
|
||||
this.histogramChartView.bind('on_brush_end', this._onBrushEnd, this);
|
||||
this.histogramChartView.bind('hover', this._toggleTooltip, this);
|
||||
this.histogramChartView.render().show();
|
||||
|
||||
this._updateStats();
|
||||
},
|
||||
|
||||
_renderMiniChart: function () {
|
||||
this.miniHistogramChartView = new HistogramChartView(({
|
||||
type: 'histogram',
|
||||
className: 'CDB-Chart--mini',
|
||||
mini: true,
|
||||
margin: { top: 0, right: 4, bottom: 4, left: 4 },
|
||||
height: 40,
|
||||
showOnWidthChange: false,
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel,
|
||||
data: this._dataviewModel.getData(),
|
||||
normalized: this.model.get('normalized'),
|
||||
chartBarColor: this.model.getColor() || '#9DE0AD',
|
||||
originalData: this._originalData,
|
||||
widgetModel: this.model
|
||||
}));
|
||||
|
||||
this.addView(this.miniHistogramChartView);
|
||||
this.$('.js-mini-chart').append(this.miniHistogramChartView.el);
|
||||
this.miniHistogramChartView.bind('on_brush_end', this._onMiniRangeUpdated, this);
|
||||
this.miniHistogramChartView.render();
|
||||
},
|
||||
|
||||
_setupBindings: function () {
|
||||
this._dataviewModel.bind('change:bins', this._onChangeBins, this);
|
||||
this.model.bind('change:zoomed', this._onChangeZoomed, this);
|
||||
this.model.bind('change:zoom_enabled', this._onChangeZoomEnabled, this);
|
||||
this.model.bind('change:filter_enabled', this._onChangeFilterEnabled, this);
|
||||
this.model.bind('change:total', this._onChangeTotal, this);
|
||||
this.model.bind('change:nulls', this._onChangeNulls, this);
|
||||
this.model.bind('change:max', this._onChangeMax, this);
|
||||
this.model.bind('change:min', this._onChangeMin, this);
|
||||
this.model.bind('change:avg', this._onChangeAvg, this);
|
||||
},
|
||||
|
||||
_unbinds: function () {
|
||||
this._dataviewModel.off('change:bins', this._onChangeBins, this);
|
||||
this.model.off('change:zoomed', this._onChangeZoomed, this);
|
||||
this.model.off('change:zoom_enabled', this._onChangeZoomEnabled, this);
|
||||
this.model.off('change:filter_enabled', this._onChangeFilterEnabled, this);
|
||||
this.model.off('change:total', this._onChangeTotal, this);
|
||||
this.model.off('change:nulls', this._onChangeNulls, this);
|
||||
this.model.off('change:max', this._onChangeMax, this);
|
||||
this.model.off('change:min', this._onChangeMin, this);
|
||||
this.model.off('change:avg', this._onChangeAvg, this);
|
||||
},
|
||||
|
||||
_onMiniRangeUpdated: function (loBarIndex, hiBarIndex) {
|
||||
this.lockedByUser = false;
|
||||
|
||||
this._clearTooltip();
|
||||
this.histogramChartView.removeSelection();
|
||||
|
||||
var data = this._originalData.getData();
|
||||
|
||||
this.model.set({lo_index: loBarIndex, hi_index: hiBarIndex, zlo_index: null, zhi_index: null});
|
||||
this._applyBrushFilter(data, loBarIndex, hiBarIndex);
|
||||
},
|
||||
|
||||
_onBrushEnd: function (loBarIndex, hiBarIndex) {
|
||||
if (this._isZoomed()) {
|
||||
this._onBrushEndFiltered(loBarIndex, hiBarIndex);
|
||||
} else {
|
||||
this._onBrushEndUnfiltered(loBarIndex, hiBarIndex);
|
||||
}
|
||||
},
|
||||
|
||||
_onBrushEndFiltered: function (loBarIndex, hiBarIndex) {
|
||||
var data = this._filteredData;
|
||||
if ((!data || !data.length) || (this.model.get('zlo_index') === loBarIndex && this.model.get('zhi_index') === hiBarIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._numberOfFilters = 2;
|
||||
this.lockedByUser = true;
|
||||
this.model.set({filter_enabled: true, zlo_index: loBarIndex, zhi_index: hiBarIndex});
|
||||
this._applyBrushFilter(data, loBarIndex, hiBarIndex);
|
||||
},
|
||||
|
||||
_onBrushEndUnfiltered: function (loBarIndex, hiBarIndex) {
|
||||
var data = this._dataviewModel.getData();
|
||||
if ((!data || !data.length) || (this.model.get('lo_index') === loBarIndex && this.model.get('hi_index') === hiBarIndex)) {
|
||||
return;
|
||||
}
|
||||
this._numberOfFilters = 1;
|
||||
this.model.set({filter_enabled: true, zoom_enabled: true, lo_index: loBarIndex, hi_index: hiBarIndex});
|
||||
this._applyBrushFilter(data, loBarIndex, hiBarIndex);
|
||||
},
|
||||
|
||||
_applyBrushFilter: function (data, loBarIndex, hiBarIndex) {
|
||||
if (loBarIndex !== hiBarIndex && loBarIndex >= 0 && loBarIndex < data.length && (hiBarIndex - 1) >= 0 && (hiBarIndex - 1) < data.length) {
|
||||
this.filter.setRange(
|
||||
data[loBarIndex].start,
|
||||
data[hiBarIndex - 1].end
|
||||
);
|
||||
this._updateStats();
|
||||
} else {
|
||||
console.error('Error accessing array bounds', loBarIndex, hiBarIndex, data);
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeFilterEnabled: function () {
|
||||
this.$('.js-filter').toggleClass('is-hidden', !this.model.get('filter_enabled'));
|
||||
},
|
||||
|
||||
_onChangeBins: function (mdl, bins) {
|
||||
this._resetWidget();
|
||||
},
|
||||
|
||||
_onChangeZoomEnabled: function () {
|
||||
this.$('.js-zoom').toggleClass('is-hidden', !this.model.get('zoom_enabled'));
|
||||
},
|
||||
|
||||
_renderAllValues: function () {
|
||||
this._changeHeaderValue('.js-nulls', 'nulls', '');
|
||||
this._changeHeaderValue('.js-val', 'total', 'SELECTED');
|
||||
this._changeHeaderValue('.js-max', 'max', '');
|
||||
this._changeHeaderValue('.js-min', 'min', '');
|
||||
this._changeHeaderValue('.js-avg', 'avg', '');
|
||||
},
|
||||
|
||||
_changeHeaderValue: function (className, what, suffix) {
|
||||
if (this.model.get(what) == null) {
|
||||
this.$(className).text('0 ' + suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
this._addTitleForValue(className, what, suffix);
|
||||
|
||||
var animator = new AnimateValues({
|
||||
el: this.$el
|
||||
});
|
||||
|
||||
animator.animateValue(this.model, what, className, animationTemplate, {
|
||||
formatter: formatter.formatNumber,
|
||||
templateData: { suffix: ' ' + suffix }
|
||||
});
|
||||
},
|
||||
|
||||
_onChangeNulls: function () {
|
||||
this._changeHeaderValue('.js-nulls', 'nulls', '');
|
||||
},
|
||||
|
||||
_onChangeTotal: function () {
|
||||
this._changeHeaderValue('.js-val', 'total', 'SELECTED');
|
||||
},
|
||||
|
||||
_onChangeMax: function () {
|
||||
this._changeHeaderValue('.js-max', 'max', '');
|
||||
},
|
||||
|
||||
_onChangeMin: function () {
|
||||
this._changeHeaderValue('.js-min', 'min', '');
|
||||
},
|
||||
|
||||
_onChangeAvg: function () {
|
||||
this._changeHeaderValue('.js-avg', 'avg', '');
|
||||
},
|
||||
|
||||
_addTitleForValue: function (className, what, unit) {
|
||||
this.$(className).attr('title', this._formatNumberWithCommas(this.model.get(what).toFixed(2)) + ' ' + unit);
|
||||
},
|
||||
|
||||
_formatNumberWithCommas: function (x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
},
|
||||
|
||||
_calculateBars: function (data) {
|
||||
var min;
|
||||
var max;
|
||||
var loBarIndex;
|
||||
var hiBarIndex;
|
||||
var startMin;
|
||||
var startMax;
|
||||
|
||||
if (this._isZoomed() && this._numberOfFilters === 2) {
|
||||
min = this.model.get('zmin');
|
||||
max = this.model.get('zmax');
|
||||
loBarIndex = this.model.get('zlo_index');
|
||||
hiBarIndex = this.model.get('zhi_index');
|
||||
} else if (this._numberOfFilters === 1) {
|
||||
min = this.model.get('min');
|
||||
max = this.model.get('max');
|
||||
loBarIndex = this.model.get('lo_index');
|
||||
hiBarIndex = this.model.get('hi_index');
|
||||
}
|
||||
|
||||
if (data.length > 0) {
|
||||
if (!_.isNumber(min) && !_.isNumber(loBarIndex)) {
|
||||
loBarIndex = 0;
|
||||
} else if (_.isNumber(min) && !_.isNumber(loBarIndex)) {
|
||||
startMin = _.findWhere(data, {start: min});
|
||||
loBarIndex = (startMin && startMin.bin) || 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(max) && !_.isNumber(hiBarIndex)) {
|
||||
hiBarIndex = data.length;
|
||||
} else if (_.isNumber(max) && !_.isNumber(hiBarIndex)) {
|
||||
startMax = _.findWhere(data, {end: max});
|
||||
hiBarIndex = (startMax && startMax.bin + 1) || data.length;
|
||||
}
|
||||
} else {
|
||||
loBarIndex = 0;
|
||||
hiBarIndex = data.length;
|
||||
}
|
||||
|
||||
return {
|
||||
loBarIndex: loBarIndex,
|
||||
hiBarIndex: hiBarIndex
|
||||
};
|
||||
},
|
||||
|
||||
_updateStats: function () {
|
||||
if (!this._initStateApplied) return;
|
||||
var data;
|
||||
|
||||
if (!this._isZoomed() || (this._isZoomed() && this._numberOfFilters === 2)) {
|
||||
data = this.histogramChartView.model.get('data');
|
||||
} else {
|
||||
data = this.miniHistogramChartView.model.get('data');
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nulls = this._dataviewModel.get('nulls');
|
||||
var bars = this._calculateBars(data);
|
||||
var loBarIndex = bars.loBarIndex;
|
||||
var hiBarIndex = bars.hiBarIndex;
|
||||
var sum, avg, min, max;
|
||||
var attrs;
|
||||
|
||||
if (data && data.length) {
|
||||
sum = this._calcSum(data, loBarIndex, hiBarIndex);
|
||||
avg = this._calcAvg(data, loBarIndex, hiBarIndex);
|
||||
|
||||
if (loBarIndex >= 0 && loBarIndex < data.length) {
|
||||
min = data[loBarIndex].start;
|
||||
}
|
||||
|
||||
if (hiBarIndex >= 0 && hiBarIndex - 1 < data.length) {
|
||||
max = data[Math.max(0, hiBarIndex - 1)].end;
|
||||
}
|
||||
|
||||
if (this._isZoomed() && this._numberOfFilters === 2) {
|
||||
attrs = {zmin: min, zmax: max, zlo_index: loBarIndex, zhi_index: hiBarIndex};
|
||||
} else if (!this._isZoomed() && this._numberOfFilters === 1) {
|
||||
attrs = {min: min, max: max, lo_index: loBarIndex, hi_index: hiBarIndex};
|
||||
}
|
||||
|
||||
attrs = _.extend(
|
||||
{ total: sum, nulls: nulls, avg: avg },
|
||||
attrs);
|
||||
|
||||
this.model.set(attrs);
|
||||
}
|
||||
},
|
||||
|
||||
_calcAvg: function (data, start, end) {
|
||||
var selectedData = data.slice(start, end);
|
||||
|
||||
var total = this._calcSum(data, start, end);
|
||||
|
||||
if (!total) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var area = _.reduce(selectedData, function (memo, d) {
|
||||
return (d.avg && d.freq) ? (d.avg * d.freq) + memo : memo;
|
||||
}, 0);
|
||||
|
||||
return area / total;
|
||||
},
|
||||
|
||||
_calcSum: function (data, start, end) {
|
||||
return _.reduce(data.slice(start, end), function (memo, d) {
|
||||
return d.freq + memo;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_onChangeZoomed: function () {
|
||||
if (this.model.get('zoomed')) {
|
||||
this._onZoomIn();
|
||||
} else {
|
||||
this._resetWidget();
|
||||
this._dataviewModel.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
_showMiniRange: function () {
|
||||
var loBarIndex = this.model.get('lo_index');
|
||||
var hiBarIndex = this.model.get('hi_index');
|
||||
|
||||
this.miniHistogramChartView.selectRange(loBarIndex, hiBarIndex);
|
||||
this.miniHistogramChartView.show();
|
||||
},
|
||||
|
||||
_zoom: function () {
|
||||
this.model.set({ zoomed: true, zoom_enabled: false });
|
||||
this.histogramChartView.removeSelection();
|
||||
},
|
||||
|
||||
_onZoomIn: function () {
|
||||
this.lockedByUser = false;
|
||||
this._showMiniRange();
|
||||
this.histogramChartView.setBounds();
|
||||
this._dataviewModel.enableFilter();
|
||||
this._dataviewModel.fetch();
|
||||
},
|
||||
|
||||
_resetWidget: function () {
|
||||
this._numberOfFilters = 0;
|
||||
this.model.set({
|
||||
zoomed: false,
|
||||
zoom_enabled: false,
|
||||
filter_enabled: false,
|
||||
lo_index: null,
|
||||
hi_index: null,
|
||||
min: null,
|
||||
max: null,
|
||||
zlo_index: null,
|
||||
zhi_index: null,
|
||||
zmin: null,
|
||||
zmax: null
|
||||
});
|
||||
this.filter.unsetRange();
|
||||
this._dataviewModel.disableFilter();
|
||||
this.histogramChartView.unsetBounds();
|
||||
this.miniHistogramChartView.hide();
|
||||
this._clearTooltip();
|
||||
this._updateStats();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
window.removeEventListener('resize', this._onWindowResized.bind(this));
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="CDB-Widget-header js-header">
|
||||
<div class="js-title"></div>
|
||||
<% if (showSource) { %>
|
||||
<dl class="CDB-Widget-info u-tSpace">
|
||||
<div class="u-flex u-alignCenter u-ellipsis">
|
||||
<span class="CDB-Text CDB-Size-small is-semibold u-upperCase" style="color: <%- sourceColor %>;">
|
||||
<%- sourceId %>
|
||||
</span>
|
||||
|
||||
<% if (!isSourceType) { %>
|
||||
<span class="CDB-Text CDB-Size-small u-lSpace--s u-flex" style="color: <%- sourceColor %>;">
|
||||
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-mainTextColor u-lSpace">
|
||||
<%= sourceType %>
|
||||
</span>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-altTextColor u-ellipsis u-lSpace" title="<%= layerName %>">
|
||||
<%= layerName %>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
<% } %>
|
||||
<% if (showStats) { %>
|
||||
<dl class="CDB-Widget-info CDB-Text CDB-Size-small u-secondaryTextColor u-upperCase u-tSpace">
|
||||
<% if (showNulls) { %>
|
||||
<dt class="CDB-Widget-infoCount js-nulls">0</dt><dd class="CDB-Widget-infoDescription">NULL ROWS</dd>
|
||||
<% } %>
|
||||
<dt class="CDB-Widget-infoCount js-min">0</dt><dd class="CDB-Widget-infoDescription">MIN</dd>
|
||||
<dt class="CDB-Widget-infoCount js-avg">0</dt><dd class="CDB-Widget-infoDescription">AVG</dd>
|
||||
<dt class="CDB-Widget-infoCount js-max">0</dt><dd class="CDB-Widget-infoDescription">MAX</dd>
|
||||
</dl>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Widget-content CDB-Widget-content--histogram js-content">
|
||||
<div class="CDB-Widget-filter CDB-Widget-contentSpaced ">
|
||||
<p class="CDB-Text CDB-Size-small is-semibold u-upperCase js-val">…</p>
|
||||
<div class="CDB-Widget-filterButtons js-filter is-hidden">
|
||||
<button class="CDB-Text CDB-Size-small u-upperCase u-actionTextColor CDB-Widget-filterButton js-zoom">zoom</button>
|
||||
<button class="CDB-Text CDB-Size-small u-upperCase u-actionTextColor CDB-Widget-filterButton js-clear">clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="js-mini-chart"></div>
|
||||
<div class="CDB-Chart--histogram js-chart"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="CDB-Widget-title CDB-Widget-contentSpaced">
|
||||
<h3 class="CDB-Text CDB-Size-large u-ellipsis" title="<%- title %>"><%- title %></h3>
|
||||
<div class="CDB-Widget-options CDB-Widget-contentSpaced">
|
||||
<% if (isAutoStyleEnabled) { %>
|
||||
<button class="CDB-Widget-buttonIcon CDB-Widget-buttonIcon--circle js-sizes
|
||||
<%- isAutoStyle ? 'is-selected' : '' %>
|
||||
<%- isAutoStyle ? 'js-cancelAutoStyle' : 'js-autoStyle' %>
|
||||
" data-tooltip="<%- isAutoStyle ? 'Remove Auto style' : 'Apply Auto Style' %>">
|
||||
<i class="CDB-IconFont CDB-IconFont-drop CDB-IconFont--small CDB-IconFont--top"></i>
|
||||
</button>
|
||||
<% } %>
|
||||
<button class="CDB-Shape CDB-Widget-actions js-actions u-lSpace" data-tooltip="More options">
|
||||
<div class="CDB-Shape-threePoints is-blue is-small">
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var TipsyTooltipView = require('../../../builder/components/tipsy-tooltip-view');
|
||||
var template = require('./histogram-title-template.tpl');
|
||||
|
||||
/**
|
||||
* Show title + show if histogram sizes are applied or not
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-title CDB-Widget-contentSpaced',
|
||||
|
||||
events: {
|
||||
'click .js-autoStyle': '_autoStyle',
|
||||
'click .js-cancelAutoStyle': '_cancelAutoStyle'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.widgetModel) throw new Error('widgetModel is required');
|
||||
if (!opts.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!opts.layerModel) throw new Error('layerModel is required');
|
||||
|
||||
this.widgetModel = opts.widgetModel;
|
||||
this.dataviewModel = opts.dataviewModel;
|
||||
this._layerModel = opts.layerModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(
|
||||
template({
|
||||
title: this.widgetModel.get('title'),
|
||||
isAutoStyleEnabled: this._isAutoStyleButtonVisible(),
|
||||
isAutoStyle: this.widgetModel.get('autoStyle'),
|
||||
isCollapsed: this.widgetModel.get('collapsed')
|
||||
})
|
||||
);
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.widgetModel.bind('change:title change:collapsed change:autoStyle change:style', this.render, this);
|
||||
this.add_related_model(this.widgetModel);
|
||||
|
||||
this._layerModel.bind('change:visible change:cartocss', this.render, this);
|
||||
this.add_related_model(this._layerModel);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var sizesTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-sizes'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(sizesTooltip);
|
||||
|
||||
var actionsTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-actions'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(actionsTooltip);
|
||||
},
|
||||
|
||||
_isAutoStyleButtonVisible: function () {
|
||||
var layerModelMeta = this._layerModel.get('meta');
|
||||
var cartocss = this._layerModel.get('cartocss') || (layerModelMeta && layerModelMeta.cartocss);
|
||||
var hasColorsAutoStyle = cartocss && this.widgetModel.hasColorsAutoStyle();
|
||||
|
||||
return this.widgetModel.isAutoStyleEnabled() &&
|
||||
this._layerModel.get('visible') &&
|
||||
hasColorsAutoStyle;
|
||||
},
|
||||
|
||||
_autoStyle: function () {
|
||||
this.widgetModel.autoStyle();
|
||||
},
|
||||
|
||||
_cancelAutoStyle: function () {
|
||||
this.widgetModel.cancelAutoStyle();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetModel = require('../widget-model');
|
||||
|
||||
/**
|
||||
* Model for a histogram widget
|
||||
*/
|
||||
module.exports = WidgetModel.extend({
|
||||
defaults: {
|
||||
normalized: true
|
||||
},
|
||||
|
||||
defaultState: _.extend(
|
||||
{
|
||||
autoStyle: false,
|
||||
normalized: false
|
||||
},
|
||||
WidgetModel.prototype.defaultState
|
||||
),
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
WidgetModel.prototype.initialize.apply(this, arguments);
|
||||
this.on('change:collapsed', this._onCollapsedChange, this);
|
||||
this.on('change:style', this._updateAutoStyle, this);
|
||||
this.dataviewModel.once('change', function () {
|
||||
if (this.get('autoStyle')) {
|
||||
this.autoStyle();
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
_onCollapsedChange: function (m, isCollapsed) {
|
||||
this.dataviewModel.set('enabled', !isCollapsed);
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
var state = WidgetModel.prototype.getState.call(this);
|
||||
var start = this.dataviewModel.get('start');
|
||||
var end = this.dataviewModel.get('end');
|
||||
var min = this.get('min');
|
||||
var max = this.get('max');
|
||||
|
||||
var checkRoughEqual = function (a, b) {
|
||||
if (_.isNumber(a) && _.isNumber(b) && (a !== b) && Math.abs(a - b) > Math.abs(start - end) * 0.01) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (checkRoughEqual(start, min)) {
|
||||
state.min = min;
|
||||
} else {
|
||||
delete state.min;
|
||||
}
|
||||
|
||||
if (checkRoughEqual(end, max)) {
|
||||
state.max = max;
|
||||
} else {
|
||||
delete state.max;
|
||||
}
|
||||
|
||||
if (this.get('zoomed') === true) {
|
||||
state.zoomed = true;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<ul class="CDB-Widget-chart CDB-Widget-chart--fake">
|
||||
<% for (var i = 0; i < 15; i++) { %>
|
||||
<li class="CDB-Widget-chartItem CDB-Widget-chartItem--<%- _.sample(['small', 'medium', 'big'], 1)[0] %> CDB-Widget-chartItem--fake"></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
@@ -0,0 +1 @@
|
||||
<%- value %> <%- suffix %>
|
||||
@@ -0,0 +1,190 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var placeholderTemplate = require('./placeholder.tpl');
|
||||
var contentTemplate = require('./content.tpl');
|
||||
var HistogramView = require('./histogram-view');
|
||||
var TimeSeriesHeaderView = require('./time-series-header-view');
|
||||
var DropdownView = require('../dropdown/widget-dropdown-view');
|
||||
var layerColors = require('../../util/layer-colors');
|
||||
var analyses = require('../../data/analyses');
|
||||
var escapeHTML = require('../../util/escape-html');
|
||||
|
||||
/**
|
||||
* Widget content view for a time-series
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body CDB-Widget-body--timeSeries',
|
||||
|
||||
initialize: function () {
|
||||
if (!this.model.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!this.model.layerModel) throw new Error('layerModel is required');
|
||||
|
||||
this._dataviewModel = this.model.dataviewModel;
|
||||
this._layerModel = this.model.layerModel;
|
||||
this._selectedAmount = 0;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
var sourceId = this._dataviewModel.get('source').id;
|
||||
var letter = layerColors.letter(sourceId);
|
||||
var sourceColor = layerColors.getColorForLetter(letter);
|
||||
var sourceType = this._dataviewModel.getSourceType() || '';
|
||||
var isSourceType = this._dataviewModel.isSourceType();
|
||||
var layerName = isSourceType
|
||||
? this.model.get('table_name')
|
||||
: this._layerModel.get('layer_name');
|
||||
|
||||
if (this._isDataEmpty() || this._hasError()) {
|
||||
this.$el.append(placeholderTemplate({
|
||||
hasTorqueLayer: false
|
||||
}));
|
||||
} else {
|
||||
this.$el.append(contentTemplate({
|
||||
sourceId: sourceId,
|
||||
sourceType: analyses.title(sourceType),
|
||||
isSourceType: isSourceType,
|
||||
showSource: this.model.get('show_source') && letter !== '',
|
||||
sourceColor: sourceColor,
|
||||
layerName: escapeHTML(layerName)
|
||||
}));
|
||||
this._createHistogramView();
|
||||
this._createHeaderView();
|
||||
this._createDropdownView();
|
||||
this._updateRange();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._dataviewModel.once('error', function () {
|
||||
console.log('the tiler does not support non-torque layers just yet…');
|
||||
});
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:data', this.render);
|
||||
this.listenToOnce(this.model, 'change:hasInitialState', this.render);
|
||||
|
||||
this.listenTo(this._layerModel, 'change:layer_name', this.render);
|
||||
this.add_related_model(this._layerModel);
|
||||
},
|
||||
|
||||
_createHistogramView: function () {
|
||||
if (this._histogramView) {
|
||||
this._histogramView.remove();
|
||||
}
|
||||
|
||||
this._histogramView = new HistogramView({
|
||||
timeSeriesModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel,
|
||||
rangeFilter: this._dataviewModel.filter,
|
||||
displayShadowBars: !this.model.get('normalized'),
|
||||
normalized: !!this.model.get('normalized')
|
||||
});
|
||||
|
||||
this.addView(this._histogramView);
|
||||
this.$('.js-content').append(this._histogramView.render().el);
|
||||
},
|
||||
|
||||
_createHeaderView: function () {
|
||||
if (this._headerView) {
|
||||
this._headerView.remove();
|
||||
}
|
||||
|
||||
this._headerView = new TimeSeriesHeaderView({
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel,
|
||||
rangeFilter: this._dataviewModel.filter,
|
||||
timeSeriesModel: this.model,
|
||||
showClearButton: true,
|
||||
selectedAmount: this._selectedAmount
|
||||
});
|
||||
|
||||
if (!this._histogramView) {
|
||||
throw new Error('Histogram view must be instantiated before the header view');
|
||||
}
|
||||
this._headerView.bind('resetFilter', this._histogramView.resetFilter, this._histogramView);
|
||||
this.addView(this._headerView);
|
||||
this.$('.js-title').append(this._headerView.render().el);
|
||||
},
|
||||
|
||||
_createDropdownView: function () {
|
||||
if (this._dropdownView) {
|
||||
this._dropdownView.remove();
|
||||
}
|
||||
|
||||
this._dropdownView = new DropdownView({
|
||||
model: this.model,
|
||||
target: '.js-actions',
|
||||
container: this.$('.js-header'),
|
||||
flags: {
|
||||
localTimezone: this._dataviewModel.getColumnType() === 'date',
|
||||
normalizeHistogram: true,
|
||||
canCollapse: false
|
||||
}
|
||||
});
|
||||
|
||||
this.addView(this._dropdownView);
|
||||
},
|
||||
|
||||
_updateRange: function () {
|
||||
var bars = this._calculateBars();
|
||||
var bins = this._dataviewModel.get('bins');
|
||||
var lo = Math.max(bars.loBarIndex, 0);
|
||||
var hi = Math.min(bars.hiBarIndex, bins);
|
||||
if (lo > 0 || hi < bins) {
|
||||
this._histogramView.selectRange(lo, hi);
|
||||
}
|
||||
},
|
||||
|
||||
_calculateBars: function () {
|
||||
var data = this._dataviewModel.getData();
|
||||
var min = this.model.get('min');
|
||||
var max = this.model.get('max');
|
||||
var loBarIndex = this.model.get('lo_index');
|
||||
var hiBarIndex = this.model.get('hi_index');
|
||||
var startMin;
|
||||
var startMax;
|
||||
|
||||
if (data.length > 0) {
|
||||
if (!_.isNumber(min) && !_.isNumber(loBarIndex)) {
|
||||
loBarIndex = 0;
|
||||
} else if (_.isNumber(min) && !_.isNumber(loBarIndex)) {
|
||||
startMin = _.findWhere(data, {start: min});
|
||||
loBarIndex = (startMin && startMin.bin) || 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(max) && !_.isNumber(hiBarIndex)) {
|
||||
hiBarIndex = data.length;
|
||||
} else if (_.isNumber(max) && !_.isNumber(hiBarIndex)) {
|
||||
startMax = _.findWhere(data, {end: max});
|
||||
hiBarIndex = (startMax && startMax.bin + 1) || data.length;
|
||||
}
|
||||
} else {
|
||||
loBarIndex = 0;
|
||||
hiBarIndex = data.length;
|
||||
}
|
||||
|
||||
return {
|
||||
loBarIndex: loBarIndex,
|
||||
hiBarIndex: hiBarIndex
|
||||
};
|
||||
},
|
||||
|
||||
_appendView: function (view) {
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
},
|
||||
|
||||
_isDataEmpty: function () {
|
||||
var data = this._dataviewModel.getUnfilteredData();
|
||||
return _.isEmpty(data) || _.size(data) === 0;
|
||||
},
|
||||
|
||||
_hasError: function () {
|
||||
return this._dataviewModel.has('error');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="CDB-Widget-header js-header">
|
||||
<div class="js-title"></div>
|
||||
|
||||
<% if (showSource) { %>
|
||||
<div class="CDB-Widget-contentSpaced CDB-Widget-contentFull">
|
||||
<dl class="CDB-Widget-info u-tSpace">
|
||||
<div class="u-flex u-alignCenter u-ellipsis">
|
||||
<span class="CDB-Text CDB-Size-small is-semibold u-upperCase" style="color: <%- sourceColor %>;">
|
||||
<%- sourceId %>
|
||||
</span>
|
||||
|
||||
<% if (!isSourceType) { %>
|
||||
<span class="CDB-Text CDB-Size-small u-lSpace--s u-flex" style="color: <%- sourceColor %>;">
|
||||
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-mainTextColor u-lSpace">
|
||||
<%= sourceType %>
|
||||
</span>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-altTextColor u-ellipsis u-lSpace" title="<%= layerName %>">
|
||||
<%= layerName %>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="CDB-Widget-content CDB-Widget-content--timeSeries js-content"></div>
|
||||
@@ -0,0 +1,215 @@
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var HistogramChartView = require('../histogram/chart');
|
||||
var viewportUtils = require('../../viewport-utils');
|
||||
var TipsyTooltipView = require('../../../builder/components/tipsy-tooltip-view');
|
||||
|
||||
/**
|
||||
* Time-series histogram view.
|
||||
*/
|
||||
|
||||
var INLINE_BARS_HEIGHT_PX = 48;
|
||||
var BOTTOM_MARGIN_PX = 4;
|
||||
var LABELS_PX = 16;
|
||||
var MARGINS_PX = 4;
|
||||
var INLINE_BARS_HEIGHT_WITHOUT_BOTTOM_PX = 16;
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Chart--histogram',
|
||||
|
||||
options: {
|
||||
tooltipDataAttribute: 'data-tooltip'
|
||||
},
|
||||
|
||||
defaults: {
|
||||
histogramChartHeight: INLINE_BARS_HEIGHT_PX +
|
||||
BOTTOM_MARGIN_PX +
|
||||
LABELS_PX +
|
||||
MARGINS_PX,
|
||||
histogramChartMobileHeight: INLINE_BARS_HEIGHT_WITHOUT_BOTTOM_PX
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
if (!this.options.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!this.options.layerModel) throw new Error('layerModel is required');
|
||||
|
||||
this._timeSeriesModel = this.options.timeSeriesModel;
|
||||
this._dataviewModel = this.options.dataviewModel;
|
||||
this._layerModel = this.options.layerModel;
|
||||
this._rangeFilter = this.options.rangeFilter;
|
||||
this._originalData = this._dataviewModel.getUnfilteredDataModel();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this._createHistogramView();
|
||||
return this;
|
||||
},
|
||||
|
||||
selectRange: function (loBarIndex, hiBarIndex) {
|
||||
this._chartView.selectRange(loBarIndex, hiBarIndex);
|
||||
},
|
||||
|
||||
resetFilter: function () {
|
||||
this._rangeFilter.unsetRange();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._dataviewModel, 'change:data', this._onChangeData, this);
|
||||
this.listenTo(this._dataviewModel, 'change:column', this.resetFilter, this);
|
||||
this.listenTo(this._timeSeriesModel, 'change:normalized', this._onNormalizedChanged);
|
||||
this.listenTo(this._timeSeriesModel, 'change:local_timezone', this._onChangeLocalTimezone);
|
||||
this.listenTo(this._timeSeriesModel, 'forceResize', this._onForceResize);
|
||||
this.listenTo(this._rangeFilter, 'change', this._onFilterChanged);
|
||||
},
|
||||
|
||||
_toggleTooltip: function (event) {
|
||||
if (!event || !event.target) {
|
||||
return this._clearTooltip();
|
||||
}
|
||||
|
||||
this._showTooltip(event);
|
||||
},
|
||||
|
||||
_showTooltip: function (event) {
|
||||
if (this.tooltip && event && this.tooltip.getElement() === event.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearTooltip();
|
||||
|
||||
this.tooltip = new TipsyTooltipView({
|
||||
el: event.target,
|
||||
title: function () {
|
||||
return this._getTooltipTitle(event);
|
||||
}.bind(this),
|
||||
trigger: 'manual'
|
||||
});
|
||||
|
||||
this.tooltip.showTipsy();
|
||||
this.addView(this.tooltip);
|
||||
},
|
||||
|
||||
_getTooltipTitle: function (event) {
|
||||
return $(event.target).attr(this.options.tooltipDataAttribute);
|
||||
},
|
||||
|
||||
_clearTooltip: function () {
|
||||
if (!this.tooltip) return;
|
||||
|
||||
this.tooltip.hideTipsy();
|
||||
this.removeView(this.tooltip);
|
||||
this.tooltip = undefined;
|
||||
},
|
||||
|
||||
_createHistogramView: function () {
|
||||
this._chartView = this._instantiateChartView();
|
||||
this.addView(this._chartView);
|
||||
this.$el.append(this._chartView.render().el);
|
||||
this._chartView.show();
|
||||
|
||||
this.listenTo(this._chartView, 'on_brush_end', this._onBrushEnd, this);
|
||||
this.listenTo(this._chartView, 'on_reset_filter', this.resetFilter, this);
|
||||
this.listenTo(this._chartView, 'hover', this._toggleTooltip, this);
|
||||
this.listenTo(this._chartView.model, 'change:width', this._onChangeChartWidth, this);
|
||||
},
|
||||
|
||||
_instantiateChartView: function () {
|
||||
return new HistogramChartView({
|
||||
type: this._getChartType(),
|
||||
chartBarColor: this._timeSeriesModel.getWidgetColor() || '#F2CC8F',
|
||||
animationSpeed: 100,
|
||||
margin: {
|
||||
top: this._getMarginTop(),
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
left: this._getMarginLeft()
|
||||
},
|
||||
hasHandles: true,
|
||||
handleWidth: 8,
|
||||
hasAxisTip: true,
|
||||
animationBarDelay: function (d, i) {
|
||||
return (i * 3);
|
||||
},
|
||||
height: this.defaults.histogramChartHeight,
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._layerModel,
|
||||
data: this._dataviewModel.getData(),
|
||||
originalData: this._originalData,
|
||||
displayShadowBars: !this._timeSeriesModel.get('normalized'),
|
||||
normalized: !!this._timeSeriesModel.get('normalized'),
|
||||
widgetModel: this._timeSeriesModel,
|
||||
local_timezone: !!this._timeSeriesModel.get('local_timezone')
|
||||
});
|
||||
},
|
||||
|
||||
_getChartType: function () {
|
||||
return 'time-' + this._dataviewModel.getColumnType();
|
||||
},
|
||||
|
||||
_onChangeData: function () {
|
||||
if (this._chartView) {
|
||||
this._chartView.replaceData(this._dataviewModel.getData());
|
||||
this._chartView.updateXScale();
|
||||
this._chartView.updateYScale();
|
||||
}
|
||||
},
|
||||
|
||||
_onBrushEnd: function (loBarIndex, hiBarIndex) {
|
||||
var data = this._dataviewModel.getData();
|
||||
this._rangeFilter.setRange(
|
||||
data[loBarIndex].start,
|
||||
data[hiBarIndex - 1].end
|
||||
);
|
||||
this._timeSeriesModel.set({lo_index: loBarIndex, hi_index: hiBarIndex});
|
||||
},
|
||||
|
||||
_onChangeChartWidth: function () {
|
||||
var isTablet = viewportUtils.isTabletViewport();
|
||||
this._chartView.toggleLabels(!isTablet);
|
||||
|
||||
var height = isTablet
|
||||
? this.defaults.histogramChartMobileHeight : this.defaults.histogramChartHeight;
|
||||
|
||||
this._chartView.model.set('height', height);
|
||||
},
|
||||
|
||||
_onNormalizedChanged: function () {
|
||||
if (this._chartView) {
|
||||
this._chartView.setNormalized(this._timeSeriesModel.get('normalized'));
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeLocalTimezone: function () {
|
||||
this._dataviewModel.set('localTimezone', this._timeSeriesModel.get('local_timezone'));
|
||||
},
|
||||
|
||||
_onForceResize: function () {
|
||||
this._chartView.forceResize();
|
||||
},
|
||||
|
||||
_resetFilterInDI: function () {
|
||||
this._timeSeriesModel.set({
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
lo_index: undefined,
|
||||
hi_index: undefined
|
||||
});
|
||||
this._chartView.removeSelection();
|
||||
},
|
||||
|
||||
_onFilterChanged: function () {
|
||||
if (!this._rangeFilter.has('min') && !this._rangeFilter.has('max')) {
|
||||
this._resetFilterInDI();
|
||||
}
|
||||
},
|
||||
|
||||
_getMarginLeft: function () {
|
||||
return 4;
|
||||
},
|
||||
|
||||
_getMarginTop: function () {
|
||||
return viewportUtils.isTabletViewport() ? 0 : 4;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="CDB-Widget-header CDB-Widget-header--timeSeries">
|
||||
<div class="CDB-Widget-timeSeriesTimeInfo CDB-Widget-timeSeriesTimeInfo--fake"></div>
|
||||
</div>
|
||||
<div class="CDB-Widget-content CDB-Widget-content--<% if (hasTorqueLayer) { %>torqueTimeSeries<% } else { %>timeSeries<% } %> u-flex u-alignCenter">
|
||||
<% if (hasTorqueLayer) { %>
|
||||
<div class="CDB-Widget-timeSeriesFakeControl"></div>
|
||||
<% } %>
|
||||
<div class="CDB-Widget-timeSeriesFakeChart <% if (hasTorqueLayer) { %>CDB-Widget-timeSeriesFakeChart--torque<% } %>">
|
||||
<% for (var i = 0; i < 50; i++) { %>
|
||||
<div class="CDB-Widget-timeSeriesFakeChartItem" style="height: <%- Math.floor(Math.random() * 100) %>%"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,130 @@
|
||||
var cdb = require('internal-carto.js');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var d3 = require('d3');
|
||||
var template = require('./time-series-header.tpl');
|
||||
var formatter = require('../../formatter');
|
||||
var AnimateValues = require('../animate-values.js');
|
||||
var animationTemplate = require('./animation-template.tpl');
|
||||
var TipsyTooltipView = require('../../../builder/components/tipsy-tooltip-view');
|
||||
|
||||
/**
|
||||
* View to reset render range.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-contentSpaced CDB-Widget-contentFull',
|
||||
|
||||
events: {
|
||||
'click .js-clear': '_onClick'
|
||||
},
|
||||
|
||||
options: {
|
||||
showClearButton: true
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!opts.layerModel) throw new Error('layerModel is required');
|
||||
if (!opts.rangeFilter) throw new Error('rangeFilter is required');
|
||||
if (!opts.timeSeriesModel) throw new Error('timeSeriesModel is required');
|
||||
if (opts.selectedAmount === void 0) throw new Error('selectedAmount is required');
|
||||
|
||||
this._timeSeriesModel = opts.timeSeriesModel;
|
||||
this._dataviewModel = opts.dataviewModel;
|
||||
this._rangeFilter = opts.rangeFilter;
|
||||
this._selectedAmount = opts.selectedAmount;
|
||||
this._layerModel = opts.layerModel;
|
||||
|
||||
this.model = new cdb.core.Model();
|
||||
|
||||
this._createFormatter();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var title = this._timeSeriesModel.get('title');
|
||||
var showSelection = !this._rangeFilter.isEmpty();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
start: this.model.get('left_axis_tip') || this.formatter(this._rangeFilter.get('min')),
|
||||
end: this.model.get('right_axis_tip') || this.formatter(this._rangeFilter.get('max')),
|
||||
title: title,
|
||||
showClearButton: this.options.showClearButton && showSelection,
|
||||
showSelection: showSelection
|
||||
})
|
||||
);
|
||||
|
||||
this._animateValue();
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var actionsTooltip = new TipsyTooltipView({
|
||||
el: this.$el.find('.js-actions'),
|
||||
gravity: 'auto'
|
||||
});
|
||||
this.addView(actionsTooltip);
|
||||
},
|
||||
|
||||
_createFormatter: function () {
|
||||
this.formatter = formatter.formatNumber;
|
||||
|
||||
if (this._dataviewModel.getColumnType() === 'date') {
|
||||
this.formatter = formatter.timestampFactory(this._dataviewModel.get('aggregation'), this._dataviewModel.getCurrentOffset());
|
||||
}
|
||||
},
|
||||
|
||||
_onLocalTimezoneChanged: function () {
|
||||
this._createFormatter();
|
||||
this.render();
|
||||
},
|
||||
|
||||
_animateValue: function () {
|
||||
var animator = new AnimateValues({
|
||||
el: this.$el
|
||||
});
|
||||
var property = this._rangeFilter.isEmpty() ? 'totalAmount' : 'filteredAmount';
|
||||
var to = this._dataviewModel.get(property);
|
||||
|
||||
animator.animateFromValues.call(this, this._selectedAmount, to, '.js-val', animationTemplate, {
|
||||
formatter: formatter.formatNumber,
|
||||
templateData: { suffix: ' Selected' }
|
||||
});
|
||||
|
||||
this._selectedAmount = to;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:left_axis_tip change:right_axis_tip', this.render);
|
||||
this.listenTo(this._timeSeriesModel, 'change:title', this.render);
|
||||
this.listenTo(this._timeSeriesModel, 'change:local_timezone', this._onLocalTimezoneChanged);
|
||||
this.listenTo(this._dataviewModel, 'change:totalAmount', this._animateValue);
|
||||
this.listenTo(this._dataviewModel, 'on_update_axis_tip', this._onUpdateAxisTip);
|
||||
this.listenTo(this._rangeFilter, 'change', this.render);
|
||||
},
|
||||
|
||||
_onUpdateAxisTip: function (axisTip) {
|
||||
this.model.set(axisTip.attr, axisTip.text);
|
||||
},
|
||||
|
||||
_setupScales: function () {
|
||||
var data = this._dataviewModel.get('data');
|
||||
var columnType = this._dataviewModel.getColumnType();
|
||||
|
||||
if (columnType === 'date') {
|
||||
this._scale = d3.time.scale()
|
||||
.domain([data[0].start * 1000, data[data.length - 1].end * 1000])
|
||||
.range([this._dataviewModel.get('start'), this._dataviewModel.get('end')]);
|
||||
} else {
|
||||
this._scale = d3.scale.linear()
|
||||
.domain([data[0].start, data[data.length - 1].end])
|
||||
.range([this._dataviewModel.get('start'), this._dataviewModel.get('end')]);
|
||||
}
|
||||
},
|
||||
|
||||
_onClick: function () {
|
||||
this.trigger('resetFilter', this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<p class="CDB-Text CDB-Size-large js-widget-title u-iBlock u-ellipsis u-rSpace"><%- title %></p>
|
||||
<div class="CDB-Widget-contentSpaced--end">
|
||||
<div class="CDB-Widget-options CDB-Widget-contentSpaced">
|
||||
<p class="CDB-Text CDB-Size-small is-semibold u-upperCase u-rSpace--m js-val">…</p>
|
||||
|
||||
<% if (showSelection && start && end) { %>
|
||||
<div class="CDB-Chart-staticTips">
|
||||
<div class="CDB-Chart-staticTip u-iBlock u-rSpace">
|
||||
<p class="CDB-Text CDB-Size-small CDB-Chart-staticTipText"><%= start %></p>
|
||||
</div>
|
||||
|
||||
<p class="CDB-Text CDB-Size-small is-semibold u-upperCase u-iBlock u-rSpace">to</p>
|
||||
|
||||
<div class="CDB-Chart-staticTip u-iBlock">
|
||||
<p class="CDB-Text CDB-Size-small CDB-Chart-staticTipText"><%= end %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (showClearButton) { %>
|
||||
<button class="CDB-Text CDB-Size-small is-semibold u-upperCase u-actionTextColor CDB-Widget-filterButton js-clear">Clear</button>
|
||||
<% } %>
|
||||
<button class="CDB-Shape CDB-Widget-actions js-actions u-lSpace" data-tooltip="More options">
|
||||
<div class="CDB-Shape-threePoints is-blue is-small">
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
<div class="CDB-Shape-threePointsItem"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
var WidgetModel = require('../widget-model');
|
||||
var _ = require('underscore');
|
||||
|
||||
/**
|
||||
* Model for a time-series widget
|
||||
*/
|
||||
module.exports = WidgetModel.extend({
|
||||
defaults: {
|
||||
normalized: true,
|
||||
animated: false
|
||||
},
|
||||
|
||||
defaultState: _.extend(
|
||||
{
|
||||
normalized: false
|
||||
},
|
||||
WidgetModel.prototype.defaultState
|
||||
),
|
||||
|
||||
getState: function () {
|
||||
var state = WidgetModel.prototype.getState.call(this);
|
||||
var start = this.dataviewModel.get('start');
|
||||
var end = this.dataviewModel.get('end');
|
||||
var data = this.dataviewModel.get('data');
|
||||
var lo = this.get('lo_index');
|
||||
var hi = this.get('hi_index');
|
||||
var l;
|
||||
var m;
|
||||
|
||||
var checkRoughEqual = function (a, b) {
|
||||
if (_.isNumber(a) && _.isNumber(b) && (a !== b) && Math.abs(a - b) > Math.abs(start - end) * 0.01) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (_.isNumber(lo) && _.isNumber(hi) && lo < data.length && (hi - 1) < data.length) {
|
||||
l = data[lo].start;
|
||||
m = data[hi - 1].end;
|
||||
} else {
|
||||
l = start;
|
||||
m = end;
|
||||
}
|
||||
|
||||
if (checkRoughEqual(start, l)) {
|
||||
state.min = l;
|
||||
}
|
||||
if (checkRoughEqual(end, m)) {
|
||||
state.max = m;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var torqueTemplate = require('./torque-template.tpl');
|
||||
var placeholderTemplate = require('./placeholder.tpl');
|
||||
var TorqueHistogramView = require('./torque-histogram-view');
|
||||
var TorqueHeaderView = require('./torque-header-view');
|
||||
var DropdownView = require('../dropdown/widget-dropdown-view');
|
||||
var layerColors = require('../../util/layer-colors');
|
||||
var analyses = require('../../data/analyses');
|
||||
var escapeHTML = require('../../util/escape-html');
|
||||
|
||||
/**
|
||||
* Widget content view for a Torque time-series
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body CDB-Widget-body--timeSeries',
|
||||
|
||||
initialize: function () {
|
||||
if (!this.model.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!this.model.layerModel) throw new Error('layerModel is required');
|
||||
|
||||
this._dataviewModel = this.model.dataviewModel;
|
||||
this._layerModel = this.model.layerModel;
|
||||
this._originalData = this._dataviewModel.getUnfilteredDataModel();
|
||||
this._selectedAmount = 0;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
var sourceId = this._dataviewModel.get('source').id;
|
||||
var letter = layerColors.letter(sourceId);
|
||||
var sourceColor = layerColors.getColorForLetter(letter);
|
||||
var sourceType = this._dataviewModel.getSourceType() || '';
|
||||
var isSourceType = this._dataviewModel.isSourceType();
|
||||
var layerName = isSourceType
|
||||
? this.model.get('table_name')
|
||||
: this._layerModel.get('layer_name');
|
||||
|
||||
if (this._isDataEmpty()) {
|
||||
this.$el.html(placeholderTemplate({
|
||||
hasTorqueLayer: true
|
||||
}));
|
||||
} else {
|
||||
this.$el.html(torqueTemplate({
|
||||
sourceId: sourceId,
|
||||
sourceType: analyses.title(sourceType),
|
||||
isSourceType: isSourceType,
|
||||
showSource: this.model.get('show_source') && letter !== '',
|
||||
sourceColor: sourceColor,
|
||||
layerName: escapeHTML(layerName)
|
||||
}));
|
||||
this._createHeaderView();
|
||||
this._createTorqueHistogramView();
|
||||
this._createDropdownView();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_createHeaderView: function () {
|
||||
if (this._headerView) {
|
||||
this._headerView.remove();
|
||||
}
|
||||
|
||||
this._headerView = new TorqueHeaderView({
|
||||
el: this.$('.js-torque-header'),
|
||||
dataviewModel: this._dataviewModel,
|
||||
torqueLayerModel: this._layerModel,
|
||||
timeSeriesModel: this.model,
|
||||
selectedAmount: this._selectedAmount
|
||||
});
|
||||
|
||||
this.addView(this._headerView);
|
||||
this._headerView.render();
|
||||
},
|
||||
|
||||
_createTorqueHistogramView: function () {
|
||||
if (this._histogramView) {
|
||||
this._histogramView.remove();
|
||||
}
|
||||
|
||||
this._histogramView = new TorqueHistogramView({
|
||||
timeSeriesModel: this.model,
|
||||
dataviewModel: this._dataviewModel,
|
||||
rangeFilter: this._dataviewModel.filter,
|
||||
torqueLayerModel: this._layerModel,
|
||||
layerModel: this._layerModel,
|
||||
displayShadowBars: !this.model.get('normalized'),
|
||||
normalized: !!this.model.get('normalized')
|
||||
});
|
||||
this.addView(this._histogramView);
|
||||
this.$el.append(this._histogramView.render().el);
|
||||
},
|
||||
|
||||
_createDropdownView: function () {
|
||||
if (this._dropdownView) {
|
||||
this._dropdownView.remove();
|
||||
}
|
||||
|
||||
this._dropdownView = new DropdownView({
|
||||
model: this.model,
|
||||
target: '.js-actions',
|
||||
container: this.$('.js-header'),
|
||||
flags: {
|
||||
localTimezone: this._dataviewModel.getColumnType() === 'date',
|
||||
normalizeHistogram: !!this.model.get('normalized'),
|
||||
canCollapse: false
|
||||
}
|
||||
});
|
||||
this.addView(this._dropdownView);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._originalData, 'change:data', this._onOriginalDataChange);
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:data', this.render);
|
||||
this.listenTo(this._dataviewModel, 'change:bins', this._onChangeBins);
|
||||
|
||||
this.listenTo(this._layerModel, 'change:layer_name', this.render);
|
||||
this.add_related_model(this._layerModel);
|
||||
},
|
||||
|
||||
_isDataEmpty: function () {
|
||||
var data = this._dataviewModel.getData();
|
||||
return _.isEmpty(data) || _.size(data) === 0;
|
||||
},
|
||||
|
||||
_onOriginalDataChange: function () {
|
||||
// do an explicit fetch in order to get actual data
|
||||
// with the filters applied (e.g. bbox)
|
||||
this._dataviewModel.fetch();
|
||||
},
|
||||
|
||||
_onChangeBins: function (mdl, bins) {
|
||||
this._originalData.setBins(bins);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./torque-controls.tpl');
|
||||
|
||||
/**
|
||||
* Torque animation controls, to manage run state
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
events: {
|
||||
'click .CDB-Widget-controlButton': '_onClick'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this._torqueLayerModel = this.options.torqueLayerModel;
|
||||
this._rangeFilter = this.options.rangeFilter;
|
||||
this.listenTo(this._torqueLayerModel, 'change:isRunning', this.render);
|
||||
this.listenTo(this._torqueLayerModel, 'change:start change.end', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
running: this._torqueLayerModel.get('isRunning'),
|
||||
disabled: !this._rangeFilter.isEmpty()
|
||||
})
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onClick: function () {
|
||||
if (this._torqueLayerModel.get('isRunning')) {
|
||||
this._torqueLayerModel.pause();
|
||||
} else {
|
||||
this._torqueLayerModel.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
<button class="CDB-Widget-controlButton <% if (disabled) { %>is-disabled <% } %>" <% if (disabled) { %> disabled <% } %>>
|
||||
<div class="CDB-Widget-controlButtonContent">
|
||||
|
||||
<% if (running) { %>
|
||||
<svg width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Time-series" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Time-Series" transform="translate(-201.000000, -543.000000)" fill="#FFFFFF">
|
||||
<g id="TS---01" transform="translate(168.000000, 432.000000)">
|
||||
<g id="Torque-bar" transform="translate(24.000000, 100.000000)">
|
||||
<g id="Button-Pause" transform="translate(0.000000, 4.000000)">
|
||||
<path d="M9,7 L10,7 L10,17 L9,17 L9,7 Z M14,7 L15,7 L15,17 L14,17 L14,7 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<%} else { %>
|
||||
<svg width="9px" height="11px" viewBox="0 0 9 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Time-series" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Time-Series" transform="translate(-200.000000, -279.000000)" fill-rule="nonzero" fill="#FFFFFF">
|
||||
<g id="TS---01" transform="translate(168.000000, 168.000000)">
|
||||
<g id="Torque-bar" transform="translate(24.000000, 72.000000)">
|
||||
<g id="Button-Play" transform="translate(0.000000, 32.000000)">
|
||||
<path d="M15.766443,12.044867 L9.20456553,8.10774058 C8.92171794,7.93803203 9,7.89347968 9,8.22734108 L9,16.2102807 C9,16.5418859 8.92367044,16.4984182 9.20456553,16.3298812 L15.766443,12.3927547 C16.0692969,12.2110424 16.0693051,12.2265843 15.766443,12.044867 Z M16.2809387,11.1873741 C17.23035,11.7570209 17.2319213,12.6796581 16.2809387,13.2502477 L9.71906129,17.1873741 C8.76964995,17.7570209 8,17.3168605 8,16.2102807 L8,8.22734108 C8,7.11806049 8.76807871,6.67965811 9.71906129,7.25024766 L16.2809387,11.1873741 Z" id="Triangle"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<% } %>
|
||||
</div>
|
||||
</button>
|
||||
@@ -0,0 +1,66 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var TorqueTimeInfoView = require('./torque-time-info-view');
|
||||
var TimeSeriesHeaderView = require('./time-series-header-view');
|
||||
var template = require('./torque-header-view.tpl');
|
||||
|
||||
/**
|
||||
* View for the header in the torque time-series view
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-contentSpaced',
|
||||
|
||||
initialize: function () {
|
||||
this._dataviewModel = this.options.dataviewModel;
|
||||
this._torqueLayerModel = this.options.torqueLayerModel;
|
||||
this._rangeFilter = this._dataviewModel.filter;
|
||||
this._selectedAmount = this.options.selectedAmount;
|
||||
this._timeSeriesModel = this.options.timeSeriesModel;
|
||||
|
||||
this.listenTo(this._rangeFilter, 'change', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var showClearButton = true;
|
||||
this.clearSubViews();
|
||||
this.$el.addClass(this.className);
|
||||
this.$el.html(template());
|
||||
|
||||
if (this._rangeFilter.isEmpty()) {
|
||||
this._appendView('.js-torque-controls',
|
||||
new TorqueTimeInfoView({
|
||||
dataviewModel: this._dataviewModel,
|
||||
torqueLayerModel: this._torqueLayerModel,
|
||||
timeSeriesModel: this._timeSeriesModel
|
||||
})
|
||||
);
|
||||
showClearButton = false;
|
||||
}
|
||||
this._createTimeSeriesHeaderView(showClearButton);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_createTimeSeriesHeaderView: function (showClearButton) {
|
||||
var headerView = new TimeSeriesHeaderView({
|
||||
dataviewModel: this._dataviewModel,
|
||||
layerModel: this._torqueLayerModel,
|
||||
rangeFilter: this._dataviewModel.filter,
|
||||
showClearButton: showClearButton,
|
||||
timeSeriesModel: this._timeSeriesModel,
|
||||
selectedAmount: this._selectedAmount
|
||||
});
|
||||
this._appendView('.js-time-series-header', headerView);
|
||||
headerView.on('resetFilter', this._resetFilter, this);
|
||||
},
|
||||
|
||||
_resetFilter: function () {
|
||||
// Move it to 0 so it doesn't stutter as much
|
||||
this._torqueLayerModel.set({ step: 0 });
|
||||
this._torqueLayerModel.resetRenderRange();
|
||||
},
|
||||
|
||||
_appendView: function (selector, view) {
|
||||
this.addView(view);
|
||||
this.$(selector).append(view.render().el);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="CDB-Widget-contentSpaced CDB-Widget-contentSpaced--end js-torque-controls"></div>
|
||||
<div class="CDB-Widget-contentFull">
|
||||
<div class="js-time-series-header"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,170 @@
|
||||
var _ = require('underscore');
|
||||
var HistogramView = require('./histogram-view');
|
||||
var TorqueTimeSliderView = require('./torque-time-slider-view');
|
||||
var TorqueControlsView = require('./torque-controls-view');
|
||||
|
||||
/**
|
||||
* Torque time-series histogram view.
|
||||
* Extends the common histogram chart view with time-control
|
||||
* this.dataviewModel is a histogram model
|
||||
*/
|
||||
module.exports = HistogramView.extend({
|
||||
defaults: _.extend({}, HistogramView.prototype.defaults, {offsetLeft: 34}),
|
||||
|
||||
className: function () {
|
||||
return HistogramView.prototype.className + ' CDB-Widget-content CDB-Widget-content--torqueTimeSeries u-flex';
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
if (!this.options.torqueLayerModel) throw new Error('torqeLayerModel is required');
|
||||
if (!this.options.rangeFilter) throw new Error('rangeFilter is required');
|
||||
if (!this.options.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!this.options.timeSeriesModel) throw new Error('timeSeriesModel is required');
|
||||
|
||||
this._torqueLayerModel = this.options.torqueLayerModel;
|
||||
this._dataviewModel = this.options.dataviewModel;
|
||||
this._timeSeriesModel = this.options.timeSeriesModel;
|
||||
HistogramView.prototype.initialize.call(this);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
HistogramView.prototype._initBinds.call(this);
|
||||
|
||||
this.listenTo(this._torqueLayerModel, 'change:renderRange', this._onRenderRangeChanged);
|
||||
this.listenTo(this._torqueLayerModel, 'change:steps change:start change:end', this._reSelectRange);
|
||||
this.listenTo(this._torqueLayerModel, 'change:cartocss', this._onUpdateCartocss);
|
||||
},
|
||||
|
||||
_createHistogramView: function () {
|
||||
this._chartType = this._torqueLayerModel.get('column_type') === 'date' ? 'time' : 'number';
|
||||
HistogramView.prototype._createHistogramView.call(this);
|
||||
|
||||
this._torqueControls = new TorqueControlsView({
|
||||
torqueLayerModel: this._torqueLayerModel,
|
||||
rangeFilter: this._rangeFilter
|
||||
});
|
||||
this.addView(this._torqueControls);
|
||||
|
||||
this.$el.prepend(this._torqueControls.render().el);
|
||||
|
||||
this._chartView.setAnimated();
|
||||
this._chartView.bind('on_brush_click', this._onBrushClick, this);
|
||||
|
||||
this._timeSliderView = new TorqueTimeSliderView({
|
||||
dataviewModel: this._dataviewModel, // a histogram model
|
||||
chartView: this._chartView,
|
||||
torqueLayerModel: this._torqueLayerModel,
|
||||
timeSeriesModel: this._timeSeriesModel,
|
||||
rangeFilter: this._rangeFilter
|
||||
});
|
||||
this.addView(this._timeSliderView);
|
||||
this._timeSliderView.render();
|
||||
},
|
||||
|
||||
_onChangeData: function () {
|
||||
HistogramView.prototype._onChangeData.call(this);
|
||||
|
||||
if (this._chartView) {
|
||||
this._reSelectRange();
|
||||
}
|
||||
},
|
||||
|
||||
_onRenderRangeChanged: function (_model, range) {
|
||||
if (range.start === undefined && range.end === undefined) {
|
||||
this._chartView.removeSelection();
|
||||
this._rangeFilter.unsetRange();
|
||||
}
|
||||
},
|
||||
|
||||
_onBrushClick: function (indexPct) {
|
||||
var steps = this._torqueLayerModel.get('steps');
|
||||
var step = Math.round(steps * indexPct);
|
||||
|
||||
// Going to the last step causes a jump to the beginning immediately
|
||||
if (step === steps) step -= 1;
|
||||
|
||||
HistogramView.prototype.resetFilter.apply(this);
|
||||
|
||||
this._torqueLayerModel.set({ step: step });
|
||||
},
|
||||
|
||||
_onBrushEnd: function () {
|
||||
HistogramView.prototype._onBrushEnd.apply(this, arguments);
|
||||
this._reSelectRange();
|
||||
},
|
||||
|
||||
_timeToStep: function (timestamp, place) {
|
||||
var steps = this._torqueLayerModel.get('steps');
|
||||
var start = this._torqueLayerModel.get('start');
|
||||
var end = this._torqueLayerModel.get('end');
|
||||
var step = (end - start) === 0
|
||||
? place === 'max'
|
||||
? steps
|
||||
: 0
|
||||
: (steps * (1000 * timestamp - start)) / (end - start);
|
||||
step = Number(step.toFixed(2));
|
||||
|
||||
return step;
|
||||
},
|
||||
|
||||
_reSelectRange: function (model) {
|
||||
if (!this._rangeFilter.isEmpty()) {
|
||||
this._torqueLayerModel.pause();
|
||||
|
||||
var min = this._rangeFilter.get('min');
|
||||
var max = this._rangeFilter.get('max');
|
||||
var loStep = this._timeToStep(min, 'min');
|
||||
var hiStep = this._timeToStep(max, 'max');
|
||||
|
||||
// -- HACK: Reset filter if the min/max values are out of the scope
|
||||
var data = this._dataviewModel.get('data');
|
||||
var loBar = _.findWhere(data, { start: min });
|
||||
var hiBar = _.findWhere(data, { end: max });
|
||||
if (!loBar || !hiBar) {
|
||||
return this._torqueLayerModel.resetRenderRange();
|
||||
}
|
||||
|
||||
// clamp values since the range can be outside of the current torque thing
|
||||
var steps = this._torqueLayerModel.get('steps');
|
||||
var ratio = this._chartView.getSelectionExtent() / 100;
|
||||
this._updateDuration(ratio);
|
||||
this._torqueLayerModel.renderRange(
|
||||
this._clampRangeVal(0, steps, loStep), // start
|
||||
this._clampRangeVal(0, steps, hiStep) // end
|
||||
);
|
||||
} else {
|
||||
this._torqueLayerModel.play();
|
||||
this._updateDuration(1);
|
||||
}
|
||||
},
|
||||
|
||||
_updateDuration: function (ratio, cartocss) {
|
||||
if (!this._torqueLayerModel.getAnimationDuration) return;
|
||||
var duration = this._torqueLayerModel.getAnimationDuration(cartocss || this._torqueLayerModel.get('cartocss'));
|
||||
|
||||
this._torqueLayerModel.set('customDuration', Math.round(duration * ratio));
|
||||
},
|
||||
|
||||
_onUpdateCartocss: function (m, cartocss) {
|
||||
var ratio;
|
||||
if (!this._rangeFilter.isEmpty()) {
|
||||
var loStep = this._timeToStep(this._rangeFilter.get('min'));
|
||||
var hiStep = this._timeToStep(this._rangeFilter.get('max'));
|
||||
var steps = this._torqueLayerModel.get('steps');
|
||||
ratio = (hiStep - loStep) / steps;
|
||||
} else {
|
||||
ratio = 1;
|
||||
}
|
||||
|
||||
// Update silently, when carto.js updates the cartoCSS for torque, it will apply the new duration.
|
||||
this._updateDuration(ratio, cartocss, { silent: true });
|
||||
},
|
||||
|
||||
_clampRangeVal: function (a, b, t) {
|
||||
return Math.max(a, Math.min(b, t));
|
||||
},
|
||||
|
||||
_getMarginLeft: function () {
|
||||
return 16;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<p class="CDB-Text CDB-Size-large u-iBlock">
|
||||
Selected from
|
||||
</p>
|
||||
<p class="CDB-Text CDB-Size-large u-secondaryTextColor u-iBlock">
|
||||
<%- timeFormatter(startDate) %>
|
||||
<%- dateFormatter(startDate) %>
|
||||
</p>
|
||||
<p class="CDB-Text CDB-Size-large u-iBlock">
|
||||
to
|
||||
</p>
|
||||
<p class="CDB-Text CDB-Size-large u-secondaryTextColor u-iBlock">
|
||||
<%- timeFormatter(endDate) %>
|
||||
<%- dateFormatter(endDate) %>
|
||||
</p>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="CDB-Widget-header js-header">
|
||||
<div class="js-torque-header"></div>
|
||||
|
||||
<% if (showSource) { %>
|
||||
<div class="CDB-Widget-contentSpaced CDB-Widget-contentFull">
|
||||
<dl class="CDB-Widget-info u-tSpace">
|
||||
<div class="u-flex u-alignCenter u-ellipsis">
|
||||
<span class="CDB-Text CDB-Size-small is-semibold u-upperCase" style="color: <%- sourceColor %>;">
|
||||
<%- sourceId %>
|
||||
</span>
|
||||
|
||||
<% if (!isSourceType) { %>
|
||||
<span class="CDB-Text CDB-Size-small u-lSpace--s u-flex" style="color: <%- sourceColor %>;">
|
||||
<i class="CDB-IconFont CDB-IconFont-ray CDB-Size-small"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-mainTextColor u-lSpace">
|
||||
<%= sourceType %>
|
||||
</span>
|
||||
|
||||
<span class="CDB-Text CDB-Size-small u-altTextColor u-ellipsis u-lSpace" title="<%= layerName %>">
|
||||
<%= layerName %>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
var d3 = require('d3');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var moment = require('moment');
|
||||
var formatter = require('../../formatter');
|
||||
var template = require('./torque-time-info.tpl');
|
||||
|
||||
/**
|
||||
* View rendering the current step time
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-timeSeriesTimeInfo',
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.torqueLayerModel) throw new Error('torqueLayerModel is required');
|
||||
if (!opts.dataviewModel) throw new Error('dataviewModel is required');
|
||||
if (!opts.timeSeriesModel) throw new Error('timeSeriesModel is required');
|
||||
|
||||
this._torqueLayerModel = this.options.torqueLayerModel;
|
||||
this._dataviewModel = this.options.dataviewModel;
|
||||
this._timeSeriesModel = this.options.timeSeriesModel;
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var time = this._torqueLayerModel.get('time');
|
||||
var columnType = this._torqueLayerModel.get('column_type');
|
||||
var scale = d3.scale.linear()
|
||||
.domain([0, this._dataviewModel.get('data').length])
|
||||
.range([this._dataviewModel.get('start'), this._dataviewModel.get('end')]);
|
||||
var html = '';
|
||||
var timeFormatter = formatter.formatNumber;
|
||||
|
||||
if (columnType === 'number') {
|
||||
html = template({
|
||||
time: timeFormatter(scale(this._torqueLayerModel.get('step')))
|
||||
});
|
||||
} else if (columnType === 'date' && !isNaN(time && time.getTime())) {
|
||||
timeFormatter = formatter.timestampFactory(this._dataviewModel.get('aggregation'), this._dataviewModel.getCurrentOffset());
|
||||
|
||||
html = template({
|
||||
time: timeFormatter(moment(time).unix())
|
||||
});
|
||||
}
|
||||
|
||||
this.$el.html(html);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._torqueLayerModel, 'change:step', this.render);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<p class="CDB-Text CDB-Size-large u-secondaryTextColor u-iBlock">
|
||||
<%- time %>
|
||||
</p>
|
||||
@@ -0,0 +1,324 @@
|
||||
var d3 = require('d3');
|
||||
var cdb = require('internal-carto.js');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var moment = require('moment');
|
||||
var formatter = require('../../formatter');
|
||||
var viewportUtils = require('../../viewport-utils');
|
||||
|
||||
var TIP_RECT_HEIGHT = 17;
|
||||
var TIP_H_PADDING = 6;
|
||||
var CHART_MARGIN = 16;
|
||||
var MOBILE_BAR_HEIGHT = 3;
|
||||
var TOOLTIP_MARGIN = 2;
|
||||
|
||||
/**
|
||||
* Time-slider, expected to be used in a histogram view
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
defaults: {
|
||||
width: 6,
|
||||
height: 8
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
if (!this.options.chartView) throw new Error('chartView is required');
|
||||
if (!this.options.torqueLayerModel) throw new Error('torqeLayerModel is required');
|
||||
if (!this.options.timeSeriesModel) throw new Error('timeSeriesModel is required');
|
||||
|
||||
this.model = new cdb.core.Model();
|
||||
|
||||
this._dataviewModel = this.options.dataviewModel;
|
||||
this._chartView = this.options.chartView;
|
||||
this._torqueLayerModel = this.options.torqueLayerModel;
|
||||
this._timeSeriesModel = this.options.timeSeriesModel;
|
||||
|
||||
this._chartMargins = this._chartView.model.get('margin');
|
||||
|
||||
this._initBinds();
|
||||
this._updateXScale();
|
||||
this._createFormatter();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// Make the render call idempotent; only create time slider once
|
||||
if (!this.timeSlider) {
|
||||
var dragBehavior = d3.behavior.drag()
|
||||
.on('dragstart', this._onDragStart.bind(this))
|
||||
.on('drag', this._onDrag.bind(this))
|
||||
.on('dragend', this._onDragEnd.bind(this));
|
||||
|
||||
var d3el = this._chartView.canvas.append('rect');
|
||||
this.timeSlider = d3el
|
||||
.attr('class', 'CDB-TimeSlider')
|
||||
.attr('width', this.defaults.width)
|
||||
.attr('height', this._calcHeight())
|
||||
.attr('rx', 3)
|
||||
.attr('ry', 3)
|
||||
.data([{ x: 0, y: 0 }])
|
||||
.attr('transform', this._translateXY)
|
||||
.call(dragBehavior);
|
||||
|
||||
this.setElement(d3el.node());
|
||||
}
|
||||
|
||||
if (this._isTabletViewport()) {
|
||||
this._generateTimeSliderTip();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_isTabletViewport: function () {
|
||||
return viewportUtils.isTabletViewport();
|
||||
},
|
||||
|
||||
_generateTimeSliderTip: function () {
|
||||
var yPos = this._calcHeight() / 2 + MOBILE_BAR_HEIGHT + TOOLTIP_MARGIN;
|
||||
yPos = Math.floor(yPos);
|
||||
|
||||
this.timeSliderTip = this._chartView.canvas.select('.CDB-WidgetCanvas').append('g')
|
||||
.attr('class', 'CDB-Chart-timeSliderTip')
|
||||
.data([{ x: CHART_MARGIN, y: yPos }])
|
||||
.attr('transform', this._translateXY);
|
||||
|
||||
this.timeSliderTip.append('rect')
|
||||
.attr('class', 'CDB-Chart-timeSliderTipRect')
|
||||
.attr('rx', '2')
|
||||
.attr('ry', '2')
|
||||
.attr('height', TIP_RECT_HEIGHT);
|
||||
|
||||
this.timeSliderTip.append('text')
|
||||
.attr('class', 'CDB-Text CDB-Size-small CDB-Chart-timeSliderTipText')
|
||||
.attr('dy', '11')
|
||||
.attr('dx', '0');
|
||||
},
|
||||
|
||||
_onLocalTimezoneChanged: function () {
|
||||
this._createFormatter();
|
||||
this._updateTimeSliderTip();
|
||||
},
|
||||
|
||||
_updateTimeSliderTip: function () {
|
||||
var self = this;
|
||||
|
||||
var textLabelData = this._isDateTimeSeries() ? this._torqueLayerModel.get('time') : this._torqueLayerModel.get('step');
|
||||
|
||||
if (textLabelData === void 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = this._chartView.canvas;
|
||||
var textLabel = chart.select('.CDB-Chart-timeSliderTipText');
|
||||
|
||||
var scale = d3.scale.linear()
|
||||
.domain([0, this._dataviewModel.get('data').length])
|
||||
.range([this._dataviewModel.get('start'), this._dataviewModel.get('end')]);
|
||||
|
||||
textLabel
|
||||
.data([textLabelData])
|
||||
.text(function (d) {
|
||||
return self._isDateTimeSeries() ? this.formatter(moment(d).unix()) : this.formatter(scale(d));
|
||||
}.bind(this));
|
||||
|
||||
if (!textLabel.node()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rectLabel = chart.select('.CDB-Chart-timeSliderTipRect');
|
||||
var textBBox = textLabel.node().getBBox();
|
||||
var width = textBBox.width;
|
||||
var rectWidth = width + TIP_H_PADDING;
|
||||
var chartWidth = this._chartView.chartWidth() + CHART_MARGIN;
|
||||
|
||||
rectLabel.attr('width', rectWidth);
|
||||
textLabel.attr('dx', TIP_H_PADDING / 2);
|
||||
textLabel.attr('dy', textBBox.height - Math.abs((textBBox.height - TIP_RECT_HEIGHT) / 2));
|
||||
|
||||
var timeSliderX = this._xScale(this._torqueLayerModel.get('step'));
|
||||
var xPos = timeSliderX + this.defaults.width - rectWidth / 2;
|
||||
var yPos = this._calcHeight() / 2 + MOBILE_BAR_HEIGHT + TOOLTIP_MARGIN;
|
||||
yPos = Math.floor(yPos);
|
||||
|
||||
var timeSliderTipData = this.timeSliderTip.data();
|
||||
timeSliderTipData[0].y = yPos;
|
||||
|
||||
var newX = xPos;
|
||||
|
||||
if (xPos < CHART_MARGIN) {
|
||||
newX = CHART_MARGIN;
|
||||
} else if ((xPos + rectWidth) >= chartWidth) {
|
||||
newX = chartWidth - rectWidth;
|
||||
}
|
||||
|
||||
if (!isNaN(newX)) {
|
||||
timeSliderTipData[0].x = newX;
|
||||
|
||||
this.timeSliderTip
|
||||
.data(timeSliderTipData)
|
||||
.transition()
|
||||
.ease('linear')
|
||||
.attr('transform', this._translateXY);
|
||||
}
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._torqueLayerModel, 'change:start change:end', this._updateChartandTimeslider);
|
||||
this.listenTo(this._torqueLayerModel, 'change:step', this._onChangeStep);
|
||||
this.listenTo(this._torqueLayerModel, 'change:time', this._onChangeTime);
|
||||
this.listenTo(this._torqueLayerModel, 'change:steps', this._updateChartandTimeslider);
|
||||
|
||||
this.listenTo(this._chartView.model, 'change:width', this._updateChartandTimeslider);
|
||||
this.listenTo(this._chartView.model, 'change:height', this._onChangeChartHeight);
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:bins', this._updateChartandTimeslider);
|
||||
this.listenTo(this._dataviewModel, 'change:column_type', this._createFormatter);
|
||||
this.listenTo(this._dataviewModel.filter, 'change:min change:max', this._onFilterMinMaxChange);
|
||||
|
||||
this.listenTo(this._timeSeriesModel, 'change:local_timezone', this._onLocalTimezoneChanged);
|
||||
|
||||
this.listenTo(this._dataviewModel, 'change:start change:end', this._updateChartandTimeslider);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
if (this.timeSlider) {
|
||||
this.timeSlider.remove();
|
||||
}
|
||||
CoreView.prototype.clean.call(this);
|
||||
},
|
||||
|
||||
_onFilterMinMaxChange: function (m, isFiltering) {
|
||||
this.$el.toggle(!isFiltering);
|
||||
},
|
||||
|
||||
_onDragStart: function () {
|
||||
var isRunning = this._torqueLayerModel.get('isRunning');
|
||||
if (isRunning) {
|
||||
this._torqueLayerModel.pause();
|
||||
}
|
||||
this.model.set({
|
||||
isDragging: true,
|
||||
wasRunning: isRunning
|
||||
});
|
||||
},
|
||||
|
||||
_onDrag: function (d, i) {
|
||||
var nextX = d.x + d3.event.dx;
|
||||
if (this._isWithinRange(nextX)) {
|
||||
d.x = nextX;
|
||||
this.timeSlider.attr('transform', this._translateXY);
|
||||
|
||||
var step = Math.round(this._xScale.invert(d.x));
|
||||
this._torqueLayerModel.setStep(step);
|
||||
}
|
||||
},
|
||||
|
||||
_onDragEnd: function () {
|
||||
this.model.set('isDragging', false);
|
||||
if (this.model.get('wasRunning')) {
|
||||
this._torqueLayerModel.play();
|
||||
}
|
||||
},
|
||||
|
||||
_translateXY: function (d) {
|
||||
return 'translate(' + [d.x, d.y] + ')';
|
||||
},
|
||||
|
||||
_isWithinRange: function (x) {
|
||||
return x >= this._chartMargins.left && x <= this._width() - this._chartMargins.right;
|
||||
},
|
||||
|
||||
_onChangeStep: function () {
|
||||
// Time slider might not be created when this method is first called
|
||||
if (this.timeSlider && !this.model.get('isDragging')) {
|
||||
var data = this.timeSlider.data();
|
||||
var newX = this._xScale(this._torqueLayerModel.get('step'));
|
||||
|
||||
if (!isNaN(newX)) {
|
||||
data[0].x = newX;
|
||||
|
||||
this.timeSlider
|
||||
.data(data)
|
||||
.transition()
|
||||
.ease('linear')
|
||||
.attr('transform', this._translateXY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeChartHeight: function () {
|
||||
var height = this._isTabletViewport() ? this._calcHeight() / 2 + MOBILE_BAR_HEIGHT : this._calcHeight();
|
||||
|
||||
this.timeSlider.attr('height', height);
|
||||
},
|
||||
|
||||
_onChangeTime: function () {
|
||||
if (this._dataviewModel.filter.isEmpty() && this._isTabletViewport()) {
|
||||
var timeSliderTip = this._chartView.canvas.select('.CDB-Chart-timeSliderTip');
|
||||
|
||||
if (!timeSliderTip.node()) {
|
||||
this._generateTimeSliderTip();
|
||||
}
|
||||
|
||||
this._updateTimeSliderTip();
|
||||
} else {
|
||||
this._removeTimeSliderTip();
|
||||
}
|
||||
},
|
||||
|
||||
_removeTimeSliderTip: function () {
|
||||
var timeSliderTip = this._chartView.canvas.select('.CDB-Chart-timeSliderTip');
|
||||
|
||||
if (timeSliderTip.node()) {
|
||||
timeSliderTip.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_updateChartandTimeslider: function () {
|
||||
this._updateXScale();
|
||||
this._onChangeStep();
|
||||
},
|
||||
|
||||
_calcHeight: function () {
|
||||
return this._chartView.chartHeight() + this.defaults.height;
|
||||
},
|
||||
|
||||
_createFormatter: function () {
|
||||
this.formatter = formatter.formatNumber;
|
||||
|
||||
if (this._isDateTimeSeries()) {
|
||||
this.formatter = formatter.timestampFactory(this._dataviewModel.get('aggregation'), this._dataviewModel.getCurrentOffset());
|
||||
}
|
||||
},
|
||||
|
||||
_isDateTimeSeries: function () {
|
||||
return this._dataviewModel.getColumnType() === 'date';
|
||||
},
|
||||
|
||||
_updateXScale: function () {
|
||||
// calculate range based on the torque layer bounds (that are not the same than the histogram ones)
|
||||
var range = 1000 * (this._dataviewModel.get('end') - this._dataviewModel.get('start'));
|
||||
// get normalized start and end
|
||||
var start = (this._torqueLayerModel.get('start') - 1000 * this._dataviewModel.get('start')) / range;
|
||||
var end = (this._torqueLayerModel.get('end') - 1000 * this._dataviewModel.get('start')) / range;
|
||||
|
||||
// This function might be called in-between state changes, so just to be safe let's keep the range sane
|
||||
var scaleRangeMin = (start * this._width()) + this._chartMargins.left;
|
||||
var scaleRangeMax = (end * this._width()) - this._chartMargins.right;
|
||||
|
||||
scaleRangeMin = scaleRangeMin < 0
|
||||
? this._chartMargins.left
|
||||
: scaleRangeMin;
|
||||
|
||||
scaleRangeMax = scaleRangeMax > this._width()
|
||||
? this._width() - this._chartMargins.right
|
||||
: scaleRangeMax;
|
||||
|
||||
this._xScale = d3.scale.linear()
|
||||
.domain([0, this._torqueLayerModel.get('steps')])
|
||||
.range([scaleRangeMin, scaleRangeMax]);
|
||||
},
|
||||
|
||||
_width: function () {
|
||||
return this._chartView.model.get('width');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<%= placeholder %>
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--medium CDB-Widget-errorButton js-refresh">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium">REFRESH</span>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="CDB-Widget-title CDB-Widget-contentSpaced">
|
||||
<h3 class="CDB-Text CDB-Size-large u-ellipsis" title="<%- title %>"><%- title %></h3>
|
||||
</div>
|
||||
|
||||
<h2 class="CDB-Text CDB-Size-small is-semibold u-upperCase u-tSpace"><%- error %></h2>
|
||||
|
||||
<div class="CDB-Text CDB-Size-medium u-tSpace-xl"><%- message %></div>
|
||||
|
||||
<% if (refresh) { %>
|
||||
<button class="CDB-Button CDB-Button--secondary CDB-Button--medium u-tSpace-xl js-refresh">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium">REFRESH</span>
|
||||
</button>
|
||||
<% } else { %>
|
||||
<%= placeholder %>
|
||||
<% } %>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var errorButtonTemplate = require('./widget-error-button-template.tpl');
|
||||
var errorTextTemplate = require('./widget-error-text-template.tpl');
|
||||
|
||||
/**
|
||||
* Default widget error view:
|
||||
*
|
||||
* It will listen or not to dataviewModel changes when first load is done.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-body is-hidden',
|
||||
|
||||
events: {
|
||||
'click .js-refresh': '_onRefreshClick'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
this._title = opts.title;
|
||||
this._errorModel = opts.errorModel;
|
||||
|
||||
this.listenTo(this._errorModel, 'change', this._onErrorModelChanged);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var error = this._errorModel.get('error');
|
||||
|
||||
if (error) {
|
||||
var placeholderTemplate = this._errorModel.get('placeholder');
|
||||
var placeholder = _.isFunction(placeholderTemplate) ? placeholderTemplate() : '';
|
||||
this.$el.addClass('CDB-Widget--' + error.level);
|
||||
|
||||
var body = error.type
|
||||
? errorTextTemplate({
|
||||
placeholder: placeholder,
|
||||
error: error.error,
|
||||
title: this._title,
|
||||
message: error.message,
|
||||
refresh: error.refresh
|
||||
})
|
||||
: errorButtonTemplate({ placeholder: placeholder });
|
||||
|
||||
this.$el.html(body);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onErrorModelChanged: function () {
|
||||
this._reset();
|
||||
|
||||
_.isEmpty(this._errorModel.get('error'))
|
||||
? this.$el.addClass('is-hidden')
|
||||
: this.$el.removeClass('is-hidden');
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
_onRefreshClick: function () {
|
||||
this._errorModel.get('model').refresh();
|
||||
},
|
||||
|
||||
_reset: function () {
|
||||
this.$el.removeClass('CDB-Widget--alert CDB-Widget--error');
|
||||
this.$el.html('');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
/**
|
||||
* Default widget loader view:
|
||||
*
|
||||
* It will listen or not to dataviewModel changes when
|
||||
* first load is done.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget-loader',
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.append('<div class="CDB-Loader js-loader"></div>');
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function () {
|
||||
this.$('.js-loader').addClass('is-visible');
|
||||
clearTimeout(this._timeout);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
var self = this;
|
||||
this._timeout = setTimeout(function () {
|
||||
self.$('.js-loader').removeClass('is-visible');
|
||||
}, 500);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('loading', this.show, this);
|
||||
this.model.bind('error', function (mdl, err) {
|
||||
if (!err || (err && err.statusText !== 'abort')) {
|
||||
this.hide();
|
||||
}
|
||||
}, this);
|
||||
this.model.bind('loaded', this.hide, this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
var _ = require('underscore');
|
||||
var cdb = require('internal-carto.js');
|
||||
var AutoStylerFactory = require('./auto-style/factory');
|
||||
|
||||
var TIME_SERIES_TYPE = 'time-series';
|
||||
var HISTOGRAM_TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
* Default widget model
|
||||
*
|
||||
* Note: Currently all widgets have a dependency on a dataview, why it makes sense to have it here.
|
||||
* If you need a widget model that's backed up by a dataview model please implement your own model and adhere to the
|
||||
* public interface instead of extending/hacking this one.
|
||||
*/
|
||||
module.exports = cdb.core.Model.extend({
|
||||
defaults: {
|
||||
attrsNames: [],
|
||||
show_stats: false,
|
||||
show_source: false
|
||||
},
|
||||
|
||||
defaultState: {
|
||||
'collapsed': false
|
||||
},
|
||||
|
||||
initialize: function (attrs, models, opts) {
|
||||
opts = opts || {};
|
||||
if (!models.dataviewModel) throw new Error('dataviewModel is required.');
|
||||
if (!models.layerModel) throw new Error('layerModel is required.');
|
||||
|
||||
this.dataviewModel = models.dataviewModel;
|
||||
this.layerModel = models.layerModel;
|
||||
|
||||
// Autostyle could be disabled initially if the styles have an aggregation
|
||||
// If no option, autoStyleEnabled by default
|
||||
this._autoStyleEnabledWhenCreated = opts.autoStyleEnabled === undefined ? true : opts.autoStyleEnabled;
|
||||
|
||||
this.activeAutoStyler();
|
||||
this.listenTo(this, 'change:style', this.activeAutoStyler);
|
||||
},
|
||||
|
||||
activeAutoStyler: function () {
|
||||
if (this.isAutoStyleEnabled() && !this.autoStyler) {
|
||||
this.autoStyler = AutoStylerFactory.get(this.dataviewModel, this.layerModel, this.get('style'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {Object} attrs, not that it should be
|
||||
* @return {Boolean} true if at least one attribute was changed
|
||||
* @throws {Error} Should throw an error if the attrs are invalid or inconsistent
|
||||
*/
|
||||
update: function (attrs) {
|
||||
var wAttrs = _.pick(attrs, this.get('attrsNames'));
|
||||
this.set(wAttrs);
|
||||
this.dataviewModel.update(attrs);
|
||||
this._triggerChangesInAutoStyle();
|
||||
return !!(this.changedAttributes() || this.dataviewModel.changedAttributes());
|
||||
},
|
||||
|
||||
_triggerChangesInAutoStyle: function () {
|
||||
var changed = this.changed && this.changed.style && this.changed.style.auto_style && this.changed.style.auto_style.definition;
|
||||
var previous = this.previousAttributes();
|
||||
var former = previous.style && previous.style.auto_style && previous.style.auto_style.definition;
|
||||
|
||||
if (!_.isEqual(changed, former)) {
|
||||
this.trigger('customAutoStyle', this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
remove: function () {
|
||||
this.dataviewModel.remove();
|
||||
this.trigger('destroy', this);
|
||||
this.stopListening();
|
||||
},
|
||||
|
||||
isAutoStyleEnabled: function () {
|
||||
var styles = this.get('style');
|
||||
|
||||
if (this.get('type') === 'category' || this.get('type') === 'histogram') {
|
||||
if (!styles || !styles.auto_style) {
|
||||
// Only when styles are undefined we check the autostyle option
|
||||
return this._autoStyleEnabledWhenCreated;
|
||||
}
|
||||
|
||||
return styles && styles.auto_style && styles.auto_style.allowed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getWidgetColor: function () {
|
||||
var styles = this.get('style');
|
||||
var widgetStyle = styles && styles.widget_style;
|
||||
var widgetColor = widgetStyle && widgetStyle.definition &&
|
||||
widgetStyle.definition.color &&
|
||||
widgetStyle.definition.color.fixed;
|
||||
var widgetColorChanged = (widgetStyle && widgetStyle.widget_color_changed) ||
|
||||
(widgetStyle && !widgetStyle.widget_color_changed && widgetColor !== '#9DE0AD');
|
||||
|
||||
return widgetColorChanged && widgetColor;
|
||||
},
|
||||
|
||||
hasColorsAutoStyle: function () {
|
||||
var autoStyle = this.getAutoStyle();
|
||||
var hasDefinedColors = false;
|
||||
|
||||
if (!autoStyle || _.isEmpty(autoStyle) || _.isEmpty(autoStyle.definition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check colors in all geometries
|
||||
_.each(autoStyle.definition, function (geometryStyle) {
|
||||
if (geometryStyle.color && geometryStyle.color.range && geometryStyle.color.range.length > 0) {
|
||||
hasDefinedColors = true;
|
||||
}
|
||||
}, this);
|
||||
|
||||
return hasDefinedColors;
|
||||
},
|
||||
|
||||
getColor: function (name) {
|
||||
if (this.isAutoStyleEnabled() && this.isAutoStyle() && this.get('type') === 'category') {
|
||||
return this.autoStyler.colors.getColorByCategory(name);
|
||||
} else {
|
||||
return this.getWidgetColor();
|
||||
}
|
||||
},
|
||||
|
||||
isAutoStyle: function () {
|
||||
return this.get('autoStyle');
|
||||
},
|
||||
|
||||
autoStyle: function () {
|
||||
if (!this.isAutoStyleEnabled()) return;
|
||||
if (!this.dataForAutoStyle()) return;
|
||||
|
||||
var layer = this.layerModel;
|
||||
var initialStyle = layer.get('cartocss');
|
||||
if (!initialStyle && layer.get('meta')) {
|
||||
initialStyle = layer.get('meta').cartocss;
|
||||
}
|
||||
layer.set('initialStyle', initialStyle);
|
||||
|
||||
var style = this.autoStyler.getStyle();
|
||||
layer.set('cartocss', style);
|
||||
this.set('autoStyle', true);
|
||||
},
|
||||
|
||||
dataForAutoStyle: function () {
|
||||
return this.dataviewModel.get('data').length > 0;
|
||||
},
|
||||
|
||||
reapplyAutoStyle: function () {
|
||||
var style = this.autoStyler.getStyle();
|
||||
this.layerModel.set('cartocss', style);
|
||||
this.set('autoStyle', true);
|
||||
},
|
||||
|
||||
cancelAutoStyle: function (noRestore) {
|
||||
if (!noRestore) {
|
||||
this.layerModel.restoreCartoCSS();
|
||||
}
|
||||
this.set('autoStyle', false);
|
||||
},
|
||||
|
||||
getAutoStyle: function () {
|
||||
var style = this.get('style');
|
||||
var layerModel = this.layerModel;
|
||||
var cartocss = layerModel.get('cartocss') || (layerModel.get('meta') && layerModel.get('meta').cartocss);
|
||||
|
||||
if (this.isAutoStyleEnabled() && this.autoStyler) {
|
||||
if (style && style.auto_style && style.auto_style.definition) {
|
||||
var toRet = _.extend(style.auto_style, {cartocss: cartocss});
|
||||
return _.extend({}, toRet, {definition: this.autoStyler.getDef(cartocss)});
|
||||
} else {
|
||||
return {
|
||||
definition: this.autoStyler.getDef(cartocss),
|
||||
cartocss: cartocss
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
_updateAutoStyle: function (_model, style) {
|
||||
if (this.autoStyler) {
|
||||
this.autoStyler.updateStyle(style);
|
||||
}
|
||||
if (this.isAutoStyle()) {
|
||||
this.reapplyAutoStyle();
|
||||
}
|
||||
},
|
||||
|
||||
setInitialState: function (state) {
|
||||
this.initialState = state || {};
|
||||
},
|
||||
|
||||
applyInitialState: function () {
|
||||
var attrs = _.extend(
|
||||
this.initialState,
|
||||
{hasInitialState: true}
|
||||
);
|
||||
|
||||
this.setState(attrs);
|
||||
},
|
||||
|
||||
setState: function (state) {
|
||||
this.set(state);
|
||||
},
|
||||
|
||||
getState: function () {
|
||||
var state = {};
|
||||
for (var key in this.defaultState) {
|
||||
var attribute = this.get(key);
|
||||
var defaultValue = this.defaultState[key];
|
||||
if (typeof defaultValue !== 'undefined' && typeof attribute !== 'undefined' && !_.isEqual(attribute, defaultValue)) {
|
||||
state[key] = attribute;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
forceResize: function () {
|
||||
var type = this.get('type');
|
||||
if (type === TIME_SERIES_TYPE ||
|
||||
type === HISTOGRAM_TYPE) {
|
||||
this.trigger('forceResize');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
var _ = require('underscore');
|
||||
var WidgetView = require('./widget-view');
|
||||
|
||||
var WidgetViewFactory = function (defs) {
|
||||
this.defs = [];
|
||||
_.each(defs, function (def) {
|
||||
this.addType(def);
|
||||
}, this);
|
||||
};
|
||||
|
||||
WidgetViewFactory.prototype.DEFAULT_CLASS_NAMES = 'CDB-Widget CDB-Widget--light';
|
||||
|
||||
WidgetViewFactory.prototype.addType = function (def) {
|
||||
if (!def.match) {
|
||||
if (def.type) {
|
||||
def.match = function (widget) {
|
||||
return widget.get('type') === this.type;
|
||||
};
|
||||
} else {
|
||||
throw new Error('def.type or def.match must be provided for createContentView to work');
|
||||
}
|
||||
}
|
||||
if (!_.isFunction(def.createContentView)) throw new Error('def.createContentView must be a function');
|
||||
this.defs.push(def);
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Object, undefined} Returns nothing if there is not matching view for the given model
|
||||
*/
|
||||
WidgetViewFactory.prototype.createWidgetView = function (widget) {
|
||||
var def = _.find(this.defs, function (def) {
|
||||
return def.match(widget);
|
||||
});
|
||||
|
||||
if (def) {
|
||||
var attrs = {
|
||||
className: this.DEFAULT_CLASS_NAMES,
|
||||
model: widget,
|
||||
contentView: def.createContentView(widget)
|
||||
};
|
||||
|
||||
return new WidgetView(
|
||||
_.isFunction(def.customizeWidgetAttrs)
|
||||
? def.customizeWidgetAttrs(attrs)
|
||||
: attrs
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = WidgetViewFactory;
|
||||
@@ -0,0 +1,149 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var WidgetLoaderView = require('./widget-loader-view');
|
||||
var WidgetErrorView = require('./widget-error-view');
|
||||
var errorEnhancer = require('../util/error-enhancer');
|
||||
var getValue = require('../util/get-object-value');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
|
||||
var PLACEHOLDER_TEMPLATES = {
|
||||
category: require('./category/list/items-placeholder-template.tpl'),
|
||||
formula: require('./formula/placeholder.tpl'),
|
||||
histogram: require('./histogram/placeholder.tpl')
|
||||
};
|
||||
|
||||
var MAX_BUCKETS = 367;
|
||||
|
||||
/**
|
||||
* Default widget view
|
||||
* The model is a expected to be widget model
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
className: 'CDB-Widget CDB-Widget--light',
|
||||
|
||||
options: {
|
||||
columns_title: []
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.errorModel = new Backbone.Model({});
|
||||
var dataviewModel = this.model.dataviewModel;
|
||||
|
||||
this.listenTo(this.model, 'destroy', this.clean);
|
||||
this.listenTo(this.model, 'error', this._onError);
|
||||
this.listenTo(this.model, 'setDisabled', this._setDisabled);
|
||||
this.listenTo(dataviewModel, 'statusError', this._onError);
|
||||
this.listenTo(dataviewModel, 'sync change:data', this._onDataChanged);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.empty();
|
||||
|
||||
this._appendView(new WidgetLoaderView({
|
||||
model: this.model.dataviewModel
|
||||
}));
|
||||
|
||||
this._appendView(new WidgetErrorView({
|
||||
title: this.model.get('title'),
|
||||
errorModel: this.errorModel
|
||||
}));
|
||||
|
||||
this._appendView(this.options.contentView);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onDataChanged: function (model) {
|
||||
if (this._noDataAvailable()) {
|
||||
this.options.contentView.$el.addClass('is-hidden');
|
||||
return this.errorModel.set({
|
||||
model: model,
|
||||
error: errorEnhancer({ type: 'no_data_available' }),
|
||||
placeholder: this._getPlaceholder()
|
||||
});
|
||||
}
|
||||
|
||||
if (this._dataHasTooManyBins()) {
|
||||
this.options.contentView.$el.addClass('is-hidden');
|
||||
return this.errorModel.set({
|
||||
model: model,
|
||||
error: errorEnhancer({ type: 'too_many_bins' }),
|
||||
placeholder: this._getPlaceholder()
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isEmpty(this.errorModel.get('error'))) {
|
||||
this.errorModel.clear();
|
||||
this.options.contentView.render();
|
||||
this.options.contentView.$el.removeClass('is-hidden');
|
||||
}
|
||||
},
|
||||
|
||||
_onError: function (model, error) {
|
||||
if (error && error.message === 'abort') {
|
||||
return;
|
||||
}
|
||||
|
||||
var enhancedError = errorEnhancer(error);
|
||||
|
||||
this.options.contentView.$el.addClass('is-hidden');
|
||||
this.errorModel.set({
|
||||
model: model,
|
||||
error: enhancedError,
|
||||
placeholder: this._getPlaceholder()
|
||||
});
|
||||
},
|
||||
|
||||
_appendView: function (view) {
|
||||
this.$el.append(view.render().el);
|
||||
this.addView(view);
|
||||
},
|
||||
|
||||
_noDataAvailable: function () {
|
||||
var valueToCheck = this._isHistogram() ? 'totalAmount' : 'data';
|
||||
var data = this.model.dataviewModel.get(valueToCheck);
|
||||
|
||||
return !Utils.hasValue(data) || (_.isArray(data) && _.isEmpty(data));
|
||||
},
|
||||
|
||||
_dataHasTooManyBins: function () {
|
||||
if (this._isHistogram()) {
|
||||
var data = this.model.dataviewModel.getUnfilteredData && this.model.dataviewModel.getUnfilteredData();
|
||||
return !_.isEmpty(data) && _.size(data) > MAX_BUCKETS;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_isHistogram: function () {
|
||||
return this.model.dataviewModel.get('type') === 'histogram';
|
||||
},
|
||||
|
||||
_extractError: function (response) {
|
||||
// XmlHttpRequest error?
|
||||
var errors = getValue(response, 'responseJSON.errors_with_context', []);
|
||||
if (errors.length > 0) {
|
||||
return errors[0];
|
||||
} else if (response && response.message) {
|
||||
response.type = response.type || 'generic';
|
||||
return response;
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
_getPlaceholder: function () {
|
||||
var widgetType = this.model.dataviewModel.get('type');
|
||||
return PLACEHOLDER_TEMPLATES[widgetType];
|
||||
},
|
||||
|
||||
_setDisabled: function (model, selectedWidgetId) {
|
||||
var differentWidget = model.get('id') !== selectedWidgetId;
|
||||
|
||||
if (selectedWidgetId && !differentWidget) {
|
||||
this.el.scrollIntoView && this.el.scrollIntoView();
|
||||
}
|
||||
|
||||
this.$el.toggleClass('is-disabled', selectedWidgetId !== null && differentWidget);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
|
||||
/**
|
||||
* Collection of Dataviews
|
||||
*/
|
||||
module.exports = Backbone.Collection.extend({
|
||||
comparator: 'order',
|
||||
|
||||
initialize: function () {
|
||||
this._allDataviewsFetched = false;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:order', _.debounce(this._onChangeOrder.bind(this), 10), this);
|
||||
// If a category model applies the category colors, rest should remove/disable
|
||||
// the category colors applied before.
|
||||
this.bind('change:autoStyle', function (changedWidgetModel, autoStyle) {
|
||||
if (autoStyle) {
|
||||
this.each(function (widgetModel) {
|
||||
// Only set if model actually has the attr (i.e. it's a category model)
|
||||
if (widgetModel.layerModel && widgetModel !== changedWidgetModel && widgetModel.layerModel.get('layer_name') === changedWidgetModel.layerModel.get('layer_name') && widgetModel.get('autoStyle')) {
|
||||
widgetModel.set('autoStyle', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
_onChangeOrder: function () {
|
||||
this.sort();
|
||||
this.trigger('orderChanged', this);
|
||||
},
|
||||
|
||||
getStates: function () {
|
||||
var state = {};
|
||||
this.each(function (widgetModel) {
|
||||
var widgetState = widgetModel.getState();
|
||||
if (!_.isEmpty(widgetState)) {
|
||||
state[widgetModel.get('id')] = widgetState;
|
||||
}
|
||||
});
|
||||
return state;
|
||||
},
|
||||
|
||||
setStates: function (states) {
|
||||
for (var i in states) {
|
||||
var widget = this.at(i);
|
||||
widget.setState(states[i]);
|
||||
}
|
||||
},
|
||||
|
||||
hasInitialState: function () {
|
||||
return this._allDataviewsFetched;
|
||||
},
|
||||
|
||||
initialState: function () {
|
||||
this._allDataviewsFetched = true;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user