Initial commit
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user