cartodb/lib/assets/javascripts/deep-insights/widgets/time-series/torque-time-slider-view.js
2020-06-15 10:58:47 +08:00

325 lines
9.8 KiB
JavaScript

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');
}
});