Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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);
}
});
@@ -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>
@@ -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);
}
});
@@ -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'));
}
});
@@ -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');
}
});
@@ -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();
}
});
@@ -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>
@@ -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);
}
});
@@ -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>
@@ -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 { %>
&nbsp;
<% } %>
</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;
}
});