@carto/airship-bridge
Version:
Airship bridge to other libs (CARTO VL, CARTO.js)
347 lines (346 loc) • 15.4 kB
JavaScript
import semver from 'semver';
import { getColumnName, getExpression } from '../util/Utils';
import { AnimationControls } from './animation-controls/AnimationControls';
import { CategoryFilter } from './category/CategoryFilter';
import { CategoricalHistogramFilter } from './histogram/CategoricalHistogramFilter';
import { NumericalHistogramFilter } from './histogram/NumericalHistogramFilter';
import { GlobalRangeFilter } from './range/GlobalRangeFilter';
import { TimeSeries } from './time-series/TimeSeries';
var VL_VERSION = '^1.2.3';
/**
* This class is the main interface to bind a VL layer to one or more Airship widgets.
*
* The normal usage is create an instance and use its public methods to generate filters for
* different widgets.
*
* After you have specified all the required filters, simply call the method `build` and all will be
* handled for you. Internally, a new layer will be created with an invisible Viz, as a source for all
* the widget's data.
*
* Some caveats:
* - You can create as many filters for a column you want, but only one per widget.
* - If you enable non-read-only capabilities, it is recommended that the Viz filter property not to
* be changed, as it will be changed internally by each filter.
*
* @export
* @class VLBridge
*/
var VLBridge = /** @class */ (function () {
/**
* Creates an instance of VLBridge.
* @param {VLBridgeOptions} { carto, map, layer, source }
* - carto: CARTO VL namespace
* - map: CARTO VL map instance (Mapbox GL)
* - layer: CARTO VL layer
* - source: CARTO VL source
* @memberof VLBridge
*/
function VLBridge(_a) {
var carto = _a.carto, map = _a.map, layer = _a.layer, source = _a.source;
this._vizFilters = [];
this._carto = carto;
this._map = map;
this._layer = layer;
this._source = source;
this._id = this._layer.id;
this._rebuildFilters = this._rebuildFilters.bind(this);
this._updateDataLayerVariables = this._updateDataLayerVariables.bind(this);
if (!this._carto.expressions.globalHistogram) {
throw new Error("Provided VL version " + this._carto.version + " lacks globalHistogram support.");
}
if (!semver.satisfies(this._carto.version, VL_VERSION)) {
throw new Error("Provided VL version " + this._carto.version + " not supported. Must satisfy " + VL_VERSION);
}
}
/**
* Create a numerical histogram filter. See {@link NumericalHistogramOptions} for more details
*
* @param {(any | string)} widget Your widget or a
* selector to locate it
* @param {string} column The column you want to pull data from
* @param {NumericalHistogramOptions} [options={}] Options for the bridge
* @returns {NumericalHistogramFilter}
* @memberof VLBridge
*/
VLBridge.prototype.numericalHistogram = function (widget, column, options) {
if (options === void 0) { options = {}; }
var buckets = options.buckets, bucketRanges = options.bucketRanges, weight = options.weight, readOnly = options.readOnly, totals = options.totals;
var colName = getColumnName(column);
var expression = getExpression(column);
var histogram = new NumericalHistogramFilter(this._carto, this._layer, widget, colName, buckets, weight, this._source, bucketRanges, readOnly, totals, expression);
this._addFilter(histogram);
return histogram;
};
/**
* Create a categorical histogram filter. See {@link CategoricalHistogramOptions} for more details
*
* @param {(any | string)} widget Your widget or a selector to locate it
* @param {string} column The column to pull data from
* @param {CategoricalHistogramOptions} [options={}] Options for this particular filter
* @returns {CategoricalHistogramFilter}
* @memberof VLBridge
*/
VLBridge.prototype.categoricalHistogram = function (widget, column, options) {
if (options === void 0) { options = {}; }
var readOnly = options.readOnly, totals = options.totals, weight = options.weight;
var colName = getColumnName(column);
var expression = getExpression(column);
var histogram = new CategoricalHistogramFilter(this._carto, this._layer, widget, colName, this._source, readOnly, weight, totals, expression);
this._addFilter(histogram);
return histogram;
};
/**
* Creates a numerical or categorical histogram, depending on the arguments.
*
* If neither buckets or bucketRanges are provided, a categorical one will be created. A numerical one otherwise
*
* @param {(any | string)} widget Your widget or a selector
* @param {string} column The column to pull data from
* @param {NumericalHistogramOptions} options Options for the Histogram
* @returns {NumericalHistogramFilter | CategoricalHistogramFilter}
* @memberof VLBridge
*/
VLBridge.prototype.histogram = function (widget, column, options) {
if (options === void 0) { options = {}; }
var buckets = options.buckets, bucketRanges = options.bucketRanges, readOnly = options.readOnly, weight = options.weight, totals = options.totals;
if (buckets === undefined && bucketRanges === undefined) {
var histogramWidget = widget;
return this.categoricalHistogram(histogramWidget, column, {
readOnly: readOnly,
totals: totals,
weight: weight
});
}
return this.numericalHistogram(widget, column, {
bucketRanges: bucketRanges,
buckets: buckets,
readOnly: readOnly,
totals: totals,
weight: weight
});
};
/**
* Creates a category widget filter.
*
* You can provide an HTML element (like a button) on the options, so the filtering takes place
* after the user clicks on it. This option is ignored if readOnly
*
* @param {any | string} widget An airship category widget, or a selector
* @param {string} column Column to pull data from
* @param {CategoryOptions} [options={}]
* @returns
* @memberof VLBridge
*/
VLBridge.prototype.category = function (widget, column, options) {
if (options === void 0) { options = {}; }
var readOnly = options.readOnly, button = options.button, weight = options.weight;
var colName = getColumnName(column);
var expression = getExpression(column);
var category = new CategoryFilter(this._carto, this._layer, widget, colName, this._source, weight, readOnly, button, expression);
this._addFilter(category);
return category;
};
/**
* Creates a time series widget filter.
*
* Internally this creates a {@link NumericalHistogramFilter} and instances a {@link TimeSeries}.
*
* One will take care of the histogram part and the other of the animation parts.
*
* There can only be one animation per layer (per VLBridge instance)
*
* @param {(any | string)} widget The Time series widget, or a selector
* @param {string} column The string to pull data from it
* @param {AnimationOptions} [options={}]
* @memberof VLBridge
*/
VLBridge.prototype.timeSeries = function (widget, column, options) {
var _this = this;
if (options === void 0) { options = {}; }
if (this._animation) {
throw new Error('There can only be one animation');
}
var buckets = options.buckets, bucketRanges = options.bucketRanges, readOnly = options.readOnly, totals = options.totals, weight = options.weight, duration = options.duration, fade = options.fade, variableName = options.variableName, propertyName = options.propertyName, autoplay = options.autoplay;
this._animation = new TimeSeries(this._carto, this._layer, column, widget, function () {
if (propertyName === 'filter') {
_this._rebuildFilters();
}
}, duration, fade, variableName, propertyName, autoplay);
var histogram = this.numericalHistogram(widget, column, {
bucketRanges: bucketRanges,
buckets: buckets,
readOnly: readOnly,
totals: totals,
weight: weight
});
histogram.setTimeSeries(true);
histogram.on('rangeChanged', function (range) {
_this._animation.setRange(range);
});
return this._animation;
};
/**
* Creates an animation controls widget
*
* By default, the animation is set to the 'filter' property,
* but it is possible to animate any style property using the 'propertyName' option.
*
* There can only be one animation per layer (per VLBridge instance)
*
* @param {(any | string)} widget The Animation Controls widget, or a selector
* @param {string} column The name of the column in the dataset used in the animation
* @param {AnimationControlsOptions} [options={}]
* @param {number} [options.duration] Animation duration in seconds. It is 10 by default
* @param {[number, number]} [options.fade] Animation fade in and out.
* @param {string} [options.variableName] Name of the viz variable that has the animation assigned
* @param {string} [options.propertyName] Name of the style property to apply the animation, 'filter' by default.
* @memberof VLBridge
*/
VLBridge.prototype.animationControls = function (widget, column, options) {
var _this = this;
if (options === void 0) { options = {}; }
var duration = options.duration, fade = options.fade, variableName = options.variableName, _a = options.propertyName, propertyName = _a === void 0 ? 'filter' : _a, autoplay = options.autoplay;
this._animation = new AnimationControls(widget, this._carto, column, variableName, propertyName, duration, fade, autoplay, this._layer, function () {
if (propertyName === 'filter') {
_this._rebuildFilters();
}
}, null);
return this._animation;
};
/**
* Creates a global range slider filter.
*
* @param {(any | string)} widget A range slider widget or a selector
* @param {string} column The column to pull data from
* @returns {GlobalRangeFilter}
* @memberof VLBridge
*/
VLBridge.prototype.globalRange = function (widget, column) {
var range = new GlobalRangeFilter(this._carto, this._layer, widget, column, this._source);
this._addFilter(range);
return range;
};
/**
* Call this method after creating all the different filters you require.
*
* It will internally do the following:
* - Add new variables to your Viz, with the columns of all the non-read-only filters
* - Create a new layer as the filters' data source
* @returns
* @memberof VLBridge
*/
VLBridge.prototype.build = function () {
var _this = this;
if (this._vizFilters.length === 0) {
return;
}
var onLoaded = function () {
_this._appendVariables();
_this._buildDataLayer();
};
if (!this._layer.viz) {
this._layer.on('loaded', onLoaded);
}
else {
onLoaded();
}
};
VLBridge.prototype._addFilter = function (filter) {
filter.on('filterChanged', this._rebuildFilters);
filter.on('expressionReady', this._updateDataLayerVariables);
this._vizFilters.push(filter);
};
/**
* This will append extra variables with all the columns of non-read-only filters.
*
* This is required so that whenever the filter is changed, the original viz layer
* can be filtered by it.
*
* @private
* @memberof VLBridge
*/
VLBridge.prototype._appendVariables = function () {
var _this = this;
var s = this._carto.expressions;
this._vizFilters.forEach(function (filter) { return _this._layer.viz.variables[filter.columnPropName] = s.prop(filter.column); });
};
/**
* This will create a new Layer using the same source as the original, add it to the map, and
* pass it to all the filters so they can hook up to read the data
*
* It has style properties to make it invisible, plus all the expressions created by each filter.
*
* @private
* @memberof VLBridge
*/
VLBridge.prototype._buildDataLayer = function () {
var _this = this;
var variables = this._getVariables();
var s = this._carto.expressions;
var viz = new this._carto.Viz({
color: s.rgba(0, 0, 0, 0),
strokeWidth: 0,
variables: variables,
});
this._readOnlyLayer = new this._carto.Layer("asbind_ro_" + this._id, this._source, viz);
this._readOnlyLayer.addTo(this._map);
this._vizFilters.forEach(function (filter) { return filter.setDataLayer(_this._readOnlyLayer); });
};
VLBridge.prototype._getVariables = function () {
var variables = this._readOnlyLayer !== undefined ? this._readOnlyLayer.viz.variables : {};
for (var _i = 0, _a = this._vizFilters; _i < _a.length; _i++) {
var filter = _a[_i];
var name_1 = filter.name;
if (filter.globalExpression) {
variables[name_1 + "_global"] = filter.globalExpression;
}
if (filter.expression) {
variables[name_1] = filter.expression;
}
variables[filter.columnPropName] = this._carto.expressions.prop(filter.column);
}
return variables;
};
VLBridge.prototype._updateDataLayerVariables = function (payload) {
if (!this._readOnlyLayer.viz) {
return;
}
this._readOnlyLayer.viz.variables[payload.name] = payload.expression;
};
/**
* Gather all the VL filters from each filter, combine them and filter both the data layer and
* the original layer with it.
*
* If there is an animation involved, it uses @animation and all the filters.
*
* @private
* @memberof VLBridge
*/
VLBridge.prototype._rebuildFilters = function () {
var newFilter = this._combineFilters(this._vizFilters
.filter(function (hasFilter) { return hasFilter.filter !== null; })
.map(function (hasFilter) { return hasFilter.filter; }));
// Update (if required) the readonly layer
if (this._readOnlyLayer) {
this._readOnlyLayer.viz.filter.blendTo(newFilter, 0);
}
if (this._layer.viz.filter.isAnimated() && this._animation) {
if (this._layer.viz.variables[this._animation.variableName]) {
newFilter = "@" + this._animation.variableName + " and " + newFilter;
}
}
// Update the Visualization filter
this._layer.viz.filter.blendTo(newFilter, 0);
if (this._animation) {
this._animation.restart();
}
};
VLBridge.prototype._combineFilters = function (filters) {
if (filters.length === 0) {
return '1';
}
return filters.join(' and ');
};
return VLBridge;
}());
export default VLBridge;