From a73eea6f6438cb770a145cd4b588fe9b20255156 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Thu, 2 Jun 2016 22:09:54 -0700 Subject: [PATCH 1/9] Implement crosstalk filtering --- R/normalize.R | 16 ++++ R/utils.R | 13 +++- inst/htmlwidgets/leaflet.js | 131 +++++++++++++++++++++++--------- javascript/src/dataframe.js | 10 ++- javascript/src/layer-manager.js | 50 +++++++++++- javascript/src/methods.js | 63 +++++++++------ 6 files changed, 213 insertions(+), 70 deletions(-) diff --git a/R/normalize.R b/R/normalize.R index c2089ac83..33f069c95 100644 --- a/R/normalize.R +++ b/R/normalize.R @@ -36,6 +36,10 @@ doResolveFormula.data.frame = function(data, f) { eval(f[[2]], data, environment(f)) } +doResolveFormula.SharedData = function(data, f) { + doResolveFormula(data$data(withSelection = TRUE, withFilter = FALSE, withKey = TRUE), f) +} + doResolveFormula.map = function(data, f) { eval(f[[2]], data, environment(f)) } @@ -160,6 +164,12 @@ pointData.SpatialPointsDataFrame = function(obj) { ) } +#' @export +pointData.SharedData = function(obj) { + pointData(obj$data(withSelection = FALSE, + withFilter = FALSE, withKey = FALSE)) +} + # A simple polygon is a list(lng=numeric(), lat=numeric()). A compound polygon # is a list of simple polygons. This function returns a list of compound # polygons, so list(list(list(lng=..., lat=...))). There is also a bbox @@ -221,6 +231,12 @@ polygonData.SpatialLinesDataFrame = function(obj) { polygonData(sp::SpatialLines(obj@lines)) } +#' @export +polygonData.SharedData = function(obj) { + polygonData(obj$data(withSelection = FALSE, + withFilter = FALSE, withKey = FALSE)) +} + dfbbox = function(df) { suppressWarnings(rbind( lng = range(df$lng, na.rm = TRUE), diff --git a/R/utils.R b/R/utils.R index 025bc42d4..b2887a757 100644 --- a/R/utils.R +++ b/R/utils.R @@ -44,7 +44,18 @@ filterNULL = function(x) { #' @rdname dispatch #' @export invokeMethod = function(map, data, method, ...) { - args = evalFormula(list(...), data) + crosstalkOptions <- if (crosstalk::is.SharedData(data)) { + sd <- data + data <- sd$data() + list( + ctKey = sd$key(), + ctGroup = sd$groupName() + ) + } else { + NULL + } + + args = c(evalFormula(list(...), data), list(crosstalkOptions)) dispatch(map, method, diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 47dbaeb4c..318f9cbd1 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -208,7 +208,7 @@ var DataFrame = function () { } }, { key: "get", - value: function get(row, col) { + value: function get(row, col, missingOK) { var _this3 = this; if (row > this.effectiveLength) throw new Error("Row argument was out of bounds: " + row + " > " + this.effectiveLength); @@ -231,7 +231,9 @@ var DataFrame = function () { } else if (typeof col === "number") { colIndex = col; } - if (colIndex < 0 || colIndex > this.columns.length) throw new Error("Unknown column index: " + col); + if (colIndex < 0 || colIndex > this.columns.length) { + if (missingOK) return void 0;else throw new Error("Unknown column index: " + col); + } return this.columns[colIndex][row % this.columns[colIndex].length]; } @@ -582,6 +584,7 @@ if (_htmlwidgets2.default.shinyMode) { },{"./control-store":2,"./fixup-default-icon":4,"./global/htmlwidgets":5,"./global/jquery":6,"./global/leaflet":7,"./global/shiny":8,"./layer-manager":10,"./methods":11,"./util":13}],10:[function(require,module,exports){ +(function (global){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -626,6 +629,8 @@ var LayerManager = function () { // } // } this._byStamp = {}; + // {: {: }} + this._byCrosstalkGroup = {}; // END layer indices @@ -637,7 +642,9 @@ var LayerManager = function () { _createClass(LayerManager, [{ key: "addLayer", - value: function addLayer(layer, category, layerId, group) { + value: function addLayer(layer, category, layerId, group, ctGroup, ctKey) { + var _this = this; + // Was a group provided? var hasId = typeof layerId === "string"; var grouped = typeof group === "string"; @@ -683,16 +690,57 @@ var LayerManager = function () { this._byStamp[stamp] = { layer: layer, group: group, + ctGroup: ctGroup, + ctKey: ctKey, layerId: layerId, category: category, - container: container + container: container, + hidden: false }; + // Update crosstalk group index + if (ctGroup) { + (function () { + var ctg = _this._byCrosstalkGroup[ctGroup]; + if (!ctg) { + ctg = _this._byCrosstalkGroup[ctGroup] = {}; + var crosstalk = global.crosstalk; + var fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + fs.on("change", function (e) { + var selectedKeys = {}; + for (var i = 0; i < e.value.length; i++) { + selectedKeys[e.value[i]] = true; + } + var groupKeys = Object.keys(ctg); + for (var _i = 0; _i < groupKeys.length; _i++) { + var key = groupKeys[_i]; + var layerInfo = _this._byStamp[ctg[key]]; + _this._setVisibility(layerInfo, selectedKeys[groupKeys[_i]]); + } + }); + } + ctg[ctKey] = stamp; + })(); + } + // Add to container container.addLayer(layer); return oldLayer; } + }, { + key: "_setVisibility", + value: function _setVisibility(layerInfo, visible) { + if (layerInfo.hidden ^ visible) { + return; + } else if (visible) { + layerInfo.container.addLayer(layerInfo.layer); + layerInfo.hidden = false; + } else { + layerInfo.container.removeLayer(layerInfo.layer); + layerInfo.hidden = true; + } + } }, { key: "getLayer", value: function getLayer(category, layerId) { @@ -701,20 +749,20 @@ var LayerManager = function () { }, { key: "removeLayer", value: function removeLayer(category, layerIds) { - var _this = this; + var _this2 = this; // Find layer info _jquery2.default.each((0, _util.asArray)(layerIds), function (i, layerId) { - var layer = _this._byLayerId[_this._layerIdKey(category, layerId)]; + var layer = _this2._byLayerId[_this2._layerIdKey(category, layerId)]; if (layer) { - _this._removeLayer(layer); + _this2._removeLayer(layer); } }); } }, { key: "clearLayers", value: function clearLayers(category) { - var _this2 = this; + var _this3 = this; // Find all layers in _byCategory[category] var catTable = this._byCategory[category]; @@ -729,7 +777,7 @@ var LayerManager = function () { stamps.push(k); }); _jquery2.default.each(stamps, function (i, stamp) { - _this2._removeLayer(stamp); + _this3._removeLayer(stamp); }); } }, { @@ -752,11 +800,11 @@ var LayerManager = function () { }, { key: "getVisibleGroups", value: function getVisibleGroups() { - var _this3 = this; + var _this4 = this; var result = []; _jquery2.default.each(this._groupContainers, function (k, v) { - if (_this3._map.hasLayer(v)) { + if (_this4._map.hasLayer(v)) { result.push(k); } }); @@ -765,7 +813,7 @@ var LayerManager = function () { }, { key: "clearGroup", value: function clearGroup(group) { - var _this4 = this; + var _this5 = this; // Find all layers in _byGroup[group] var groupTable = this._byGroup[group]; @@ -780,7 +828,7 @@ var LayerManager = function () { stamps.push(k); }); _jquery2.default.each(stamps, function (i, stamp) { - _this4._removeLayer(stamp); + _this5._removeLayer(stamp); }); } }, { @@ -794,6 +842,7 @@ var LayerManager = function () { this._byCategory = {}; this._byLayerId = {}; this._byStamp = {}; + this._byCrosstalkGroup = {}; _jquery2.default.each(this._categoryContainers, clearLayerGroup); this._categoryContainers = {}; _jquery2.default.each(this._groupContainers, clearLayerGroup); @@ -823,6 +872,9 @@ var LayerManager = function () { } delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; + if (layerInfo.ctGroup) { + delete this._byCrosstalkGroup[layerInfo.ctGroup][layerInfo.ctKey]; + } } }, { key: "_layerIdKey", @@ -837,6 +889,7 @@ var LayerManager = function () { exports.default = LayerManager; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./global/jquery":6,"./global/leaflet":7,"./util":13}],11:[function(require,module,exports){ (function (global){ "use strict"; @@ -916,17 +969,17 @@ methods.setMaxBounds = function (lat1, lng1, lat2, lng2) { this.setMaxBounds([[lat1, lng1], [lat2, lng2]]); }; -methods.addPopups = function (lat, lng, popup, layerId, group, options) { +methods.addPopups = function (lat, lng, popup, layerId, group, options, crosstalkOptions) { var _this2 = this; - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("popup", popup).col("layerId", layerId).col("group", group).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("popup", popup).col("layerId", layerId).col("group", group).cbind(options).cbind(crosstalkOptions || {}); var _loop = function _loop(i) { (function () { var popup = _leaflet2.default.popup(df.get(i)).setLatLng([df.get(i, "lat"), df.get(i, "lng")]).setContent(df.get(i, "popup")); var thisId = df.get(i, "layerId"); var thisGroup = df.get(i, "group"); - this.layerManager.addLayer(popup, "popup", thisId, thisGroup); + this.layerManager.addLayer(popup, "popup", thisId, thisGroup, df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); }).call(_this2); }; @@ -980,6 +1033,7 @@ function unpackStrings(iconset) { } function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { + (function () { var _this3 = this; @@ -994,17 +1048,18 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { var _loop2 = function _loop2(i) { (function () { var marker = markerFunc(df, i); - var thisId = df.get(i, "layerId"); - var thisGroup = cluster ? null : df.get(i, "group"); + var row = df.get(i); + var thisId = row.layerId; + var thisGroup = cluster ? null : row.group; if (cluster) { clusterGroup.clusterLayerStore.add(marker, thisId); } else { - this.layerManager.addLayer(marker, "marker", thisId, thisGroup); + this.layerManager.addLayer(marker, "marker", thisId, thisGroup, row.ctGroup, row.ctKey); } - var popup = df.get(i, "popup"); + var popup = row.popup; if (popup !== null) marker.bindPopup(popup); - var label = df.get(i, "label"); - var labelOptions = df.get(i, "labelOptions"); + var label = row.label; + var labelOptions = row.labelOptions; if (label !== null) { if (labelOptions !== null) { if (labelOptions.noHide) { @@ -1032,7 +1087,7 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { }).call(map); } -methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions) { +methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { var icondf = void 0; var getIcon = void 0; @@ -1077,7 +1132,7 @@ methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, c }; } - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -1088,7 +1143,7 @@ methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, c }); }; -methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions) { +methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { var icondf = void 0; var getIcon = void 0; if (icon) { @@ -1108,7 +1163,7 @@ methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, p }; } - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -1116,7 +1171,7 @@ methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, p var options = df.get(i); if (icon) options.icon = getIcon(i); return _leaflet2.default.marker([df.get(i, "lat"), df.get(i, "lng")], options); - }); + }, crosstalkOptions); }; function addLayers(map, category, df, layerFunc) { @@ -1125,7 +1180,7 @@ function addLayers(map, category, df, layerFunc) { var layer = layerFunc(df, i); var thisId = df.get(i, "layerId"); var thisGroup = df.get(i, "group"); - this.layerManager.addLayer(layer, category, thisId, thisGroup); + this.layerManager.addLayer(layer, category, thisId, thisGroup, df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); if (layer.bindPopup) { var popup = df.get(i, "popup"); if (popup !== null) layer.bindPopup(popup); @@ -1152,16 +1207,16 @@ function addLayers(map, category, df, layerFunc) { } } -methods.addCircles = function (lat, lng, radius, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addCircles = function (lat, lng, radius, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { return _leaflet2.default.circle([df.get(i, "lat"), df.get(i, "lng")], df.get(i, "radius"), df.get(i)); }); }; -methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addMarkers(this, df, group, clusterOptions, clusterId, function (df, i) { return _leaflet2.default.circleMarker([df.get(i, "lat"), df.get(i, "lng")], df.get(i)); @@ -1172,8 +1227,8 @@ methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, * @param lat Array of arrays of latitude coordinates for polylines * @param lng Array of arrays of longitude coordinates for polylines */ -methods.addPolylines = function (polygons, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addPolylines = function (polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { var shape = df.get(i, "shapes")[0]; @@ -1212,8 +1267,8 @@ methods.clearShapes = function () { this.layerManager.clearLayers("shape"); }; -methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat1", lat1).col("lng1", lng1).col("lat2", lat2).col("lng2", lng2).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat1", lat1).col("lng1", lng1).col("lat2", lat2).col("lng2", lng2).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { return _leaflet2.default.rectangle([[df.get(i, "lat1"), df.get(i, "lng1")], [df.get(i, "lat2"), df.get(i, "lng2")]], df.get(i)); @@ -1224,8 +1279,8 @@ methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, option * @param lat Array of arrays of latitude coordinates for polygons * @param lng Array of arrays of longitude coordinates for polygons */ -methods.addPolygons = function (polygons, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addPolygons = function (polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { var shapes = df.get(i, "shapes"); diff --git a/javascript/src/dataframe.js b/javascript/src/dataframe.js index c56cde367..7272d9cee 100644 --- a/javascript/src/dataframe.js +++ b/javascript/src/dataframe.js @@ -62,7 +62,7 @@ export default class DataFrame { return this; } - get(row, col) { + get(row, col, missingOK) { if (row > this.effectiveLength) throw new Error("Row argument was out of bounds: " + row + " > " + this.effectiveLength); @@ -78,8 +78,12 @@ export default class DataFrame { } else if (typeof(col) === "number") { colIndex = col; } - if (colIndex < 0 || colIndex > this.columns.length) - throw new Error("Unknown column index: " + col); + if (colIndex < 0 || colIndex > this.columns.length) { + if (missingOK) + return void(0); + else + throw new Error("Unknown column index: " + col); + } return this.columns[colIndex][row % this.columns[colIndex].length]; } diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index a773c34d4..db59da435 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -23,6 +23,8 @@ export default class LayerManager { // } // } this._byStamp = {}; + // {: {: }} + this._byCrosstalkGroup = {}; // END layer indices @@ -32,7 +34,7 @@ export default class LayerManager { this._groupContainers = {}; } - addLayer(layer, category, layerId, group) { + addLayer(layer, category, layerId, group, ctGroup, ctKey) { // Was a group provided? let hasId = typeof(layerId) === "string"; let grouped = typeof(group) === "string"; @@ -79,17 +81,55 @@ export default class LayerManager { this._byStamp[stamp] = { layer: layer, group: group, + ctGroup: ctGroup, + ctKey: ctKey, layerId: layerId, category: category, - container: container + container: container, + hidden: false }; + // Update crosstalk group index + if (ctGroup) { + let ctg = this._byCrosstalkGroup[ctGroup]; + if (!ctg) { + ctg = this._byCrosstalkGroup[ctGroup] = {}; + let crosstalk = global.crosstalk; + let fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + fs.on("change", (e) => { + let selectedKeys = {}; + for (let i = 0; i < e.value.length; i++) { + selectedKeys[e.value[i]] = true; + } + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setVisibility(layerInfo, selectedKeys[groupKeys[i]]); + } + }); + } + ctg[ctKey] = stamp; + } + // Add to container container.addLayer(layer); return oldLayer; } + _setVisibility(layerInfo, visible) { + if (layerInfo.hidden ^ visible) { + return; + } else if (visible) { + layerInfo.container.addLayer(layerInfo.layer); + layerInfo.hidden = false; + } else { + layerInfo.container.removeLayer(layerInfo.layer); + layerInfo.hidden = true; + } + } + getLayer(category, layerId) { return this._byLayerId[this._layerIdKey(category, layerId)]; } @@ -174,6 +214,7 @@ export default class LayerManager { this._byCategory = {}; this._byLayerId = {}; this._byStamp = {}; + this._byCrosstalkGroup = {}; $.each(this._categoryContainers, clearLayerGroup); this._categoryContainers = {}; $.each(this._groupContainers, clearLayerGroup); @@ -202,9 +243,12 @@ export default class LayerManager { } delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; + if (layerInfo.ctGroup) { + delete this._byCrosstalkGroup[layerInfo.ctGroup][layerInfo.ctKey]; + } } _layerIdKey(category, layerId) { return category + "\n" + layerId; } -} \ No newline at end of file +} diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 827106a80..87f2e1285 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -53,14 +53,15 @@ methods.setMaxBounds = function(lat1, lng1, lat2, lng2) { ]); }; -methods.addPopups = function(lat, lng, popup, layerId, group, options) { +methods.addPopups = function(lat, lng, popup, layerId, group, options, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) .col("popup", popup) .col("layerId", layerId) .col("group", group) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); for (let i = 0; i < df.nrow(); i++) { (function() { @@ -69,7 +70,8 @@ methods.addPopups = function(lat, lng, popup, layerId, group, options) { .setContent(df.get(i, "popup")); let thisId = df.get(i, "layerId"); let thisGroup = df.get(i, "group"); - this.layerManager.addLayer(popup, "popup", thisId, thisGroup); + this.layerManager.addLayer(popup, "popup", thisId, thisGroup, + df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); }).call(this); } }; @@ -119,6 +121,7 @@ function unpackStrings(iconset) { } function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { + (function() { let clusterGroup = this.layerManager.getLayer("cluster", clusterId), cluster = clusterOptions !== null; @@ -131,17 +134,18 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { for (let i = 0; i < df.nrow(); i++) { (function() { let marker = markerFunc(df, i); - let thisId = df.get(i, "layerId"); - let thisGroup = cluster ? null : df.get(i, "group"); + let row = df.get(i); + let thisId = row.layerId; + let thisGroup = cluster ? null : row.group; if (cluster) { clusterGroup.clusterLayerStore.add(marker, thisId); } else { - this.layerManager.addLayer(marker, "marker", thisId, thisGroup); + this.layerManager.addLayer(marker, "marker", thisId, thisGroup, row.ctGroup, row.ctKey); } - let popup = df.get(i, "popup"); + let popup = row.popup; if (popup !== null) marker.bindPopup(popup); - let label = df.get(i, "label"); - let labelOptions = df.get(i, "labelOptions"); + let label = row.label; + let labelOptions = row.labelOptions; if (label !== null) { if (labelOptions !== null) { if(labelOptions.noHide) { @@ -166,7 +170,8 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { } methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, - clusterOptions, clusterId, label, labelOptions) { + clusterOptions, clusterId, label, labelOptions, + crosstalkOptions) { let icondf; let getIcon; @@ -219,7 +224,8 @@ methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -231,7 +237,7 @@ methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, }; methods.addAwesomeMarkers = function(lat, lng, icon, layerId, group, options, popup, -clusterOptions, clusterId, label, labelOptions) { +clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { let icondf; let getIcon; if (icon) { @@ -259,7 +265,8 @@ clusterOptions, clusterId, label, labelOptions) { .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -267,7 +274,7 @@ clusterOptions, clusterId, label, labelOptions) { let options = df.get(i); if (icon) options.icon = getIcon(i); return L.marker([df.get(i, "lat"), df.get(i, "lng")], options); - }); + }, crosstalkOptions); }; function addLayers(map, category, df, layerFunc) { @@ -276,7 +283,8 @@ function addLayers(map, category, df, layerFunc) { let layer = layerFunc(df, i); let thisId = df.get(i, "layerId"); let thisGroup = df.get(i, "group"); - this.layerManager.addLayer(layer, category, thisId, thisGroup); + this.layerManager.addLayer(layer, category, thisId, thisGroup, + df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); if (layer.bindPopup) { let popup = df.get(i, "popup"); if (popup !== null) layer.bindPopup(popup); @@ -299,7 +307,7 @@ function addLayers(map, category, df, layerFunc) { } } -methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, label, labelOptions) { +methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) @@ -309,14 +317,15 @@ methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { return L.circle([df.get(i, "lat"), df.get(i, "lng")], df.get(i, "radius"), df.get(i)); }); }; -methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions) { +methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) @@ -326,7 +335,8 @@ methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, c .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addMarkers(this, df, group, clusterOptions, clusterId, function(df, i) { return L.circleMarker([df.get(i, "lat"), df.get(i, "lng")], df.get(i)); @@ -337,7 +347,7 @@ methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, c * @param lat Array of arrays of latitude coordinates for polylines * @param lng Array of arrays of longitude coordinates for polylines */ -methods.addPolylines = function(polygons, layerId, group, options, popup, label, labelOptions) { +methods.addPolylines = function(polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("shapes", polygons) .col("layerId", layerId) @@ -345,7 +355,8 @@ methods.addPolylines = function(polygons, layerId, group, options, popup, label, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { let shape = df.get(i, "shapes")[0]; @@ -384,7 +395,7 @@ methods.clearShapes = function() { this.layerManager.clearLayers("shape"); }; -methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions) { +methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat1", lat1) .col("lng1", lng1) @@ -395,7 +406,8 @@ methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { return L.rectangle( @@ -411,7 +423,7 @@ methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options * @param lat Array of arrays of latitude coordinates for polygons * @param lng Array of arrays of longitude coordinates for polygons */ -methods.addPolygons = function(polygons, layerId, group, options, popup, label, labelOptions) { +methods.addPolygons = function(polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("shapes", polygons) .col("layerId", layerId) @@ -419,7 +431,8 @@ methods.addPolygons = function(polygons, layerId, group, options, popup, label, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { let shapes = df.get(i, "shapes"); From b6192e3662f293fa0045518e2aa9cf5242a7b5ec Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Wed, 8 Jun 2016 11:57:35 -0700 Subject: [PATCH 2/9] Support multiple layers per crosstalk key; support selection highlighting --- inst/htmlwidgets/leaflet.js | 45 ++++++++++++++++++++++++++++++--- javascript/src/layer-manager.js | 45 ++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 318f9cbd1..a246ffd88 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -629,7 +629,7 @@ var LayerManager = function () { // } // } this._byStamp = {}; - // {: {: }} + // {: {: [, , ...], ...}} this._byCrosstalkGroup = {}; // END layer indices @@ -706,6 +706,9 @@ var LayerManager = function () { ctg = _this._byCrosstalkGroup[ctGroup] = {}; var crosstalk = global.crosstalk; var fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + if (!ctg[ctKey]) ctg[ctKey] = []; + ctg[ctKey].push(stamp); + fs.on("change", function (e) { var selectedKeys = {}; for (var i = 0; i < e.value.length; i++) { @@ -718,8 +721,28 @@ var LayerManager = function () { _this._setVisibility(layerInfo, selectedKeys[groupKeys[_i]]); } }); + crosstalk.group(ctGroup).var("selection").on("change", function (e) { + if (!e.value) { + var groupKeys = Object.keys(ctg); + for (var i = 0; i < groupKeys.length; i++) { + var key = groupKeys[i]; + var layerInfo = _this._byStamp[ctg[key]]; + _this._setOpacity(layerInfo, 1.0); + } + } else { + var selectedKeys = {}; + for (var _i2 = 0; _i2 < e.value.length; _i2++) { + selectedKeys[e.value[_i2]] = true; + } + var _groupKeys = Object.keys(ctg); + for (var _i3 = 0; _i3 < _groupKeys.length; _i3++) { + var _key = _groupKeys[_i3]; + var _layerInfo = _this._byStamp[ctg[_key]]; + _this._setOpacity(_layerInfo, selectedKeys[_groupKeys[_i3]] ? 1.0 : 0.2); + } + } + }); } - ctg[ctKey] = stamp; })(); } @@ -741,6 +764,13 @@ var LayerManager = function () { layerInfo.hidden = true; } } + }, { + key: "_setOpacity", + value: function _setOpacity(layerInfo, opacity) { + if (layerInfo.layer.setOpacity) { + layerInfo.layer.setOpacity(opacity); + } + } }, { key: "getLayer", value: function getLayer(category, layerId) { @@ -873,7 +903,16 @@ var LayerManager = function () { delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; if (layerInfo.ctGroup) { - delete this._byCrosstalkGroup[layerInfo.ctGroup][layerInfo.ctKey]; + var ctGroup = this._byCrosstalkGroup[layerInfo.ctGroup]; + var layersForKey = ctGroup[layerInfo.ctKey]; + var idx = layersForKey ? layersForKey.indexOf(stamp) : -1; + if (idx >= 0) { + if (layersForKey.length === 1) { + delete ctGroup[layerInfo.ctKey]; + } else { + layersForKey.splice(idx, 1); + } + } } } }, { diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index db59da435..4a8bb99a4 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -23,7 +23,7 @@ export default class LayerManager { // } // } this._byStamp = {}; - // {: {: }} + // {: {: [, , ...], ...}} this._byCrosstalkGroup = {}; // END layer indices @@ -96,6 +96,10 @@ export default class LayerManager { ctg = this._byCrosstalkGroup[ctGroup] = {}; let crosstalk = global.crosstalk; let fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + if (!ctg[ctKey]) + ctg[ctKey] = []; + ctg[ctKey].push(stamp); + fs.on("change", (e) => { let selectedKeys = {}; for (let i = 0; i < e.value.length; i++) { @@ -108,8 +112,28 @@ export default class LayerManager { this._setVisibility(layerInfo, selectedKeys[groupKeys[i]]); } }); + crosstalk.group(ctGroup).var("selection").on("change", (e) => { + if (!e.value) { + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setOpacity(layerInfo, 1.0); + } + } else { + let selectedKeys = {}; + for (let i = 0; i < e.value.length; i++) { + selectedKeys[e.value[i]] = true; + } + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setOpacity(layerInfo, selectedKeys[groupKeys[i]] ? 1.0 : 0.2); + } + } + }); } - ctg[ctKey] = stamp; } // Add to container @@ -130,6 +154,12 @@ export default class LayerManager { } } + _setOpacity(layerInfo, opacity) { + if (layerInfo.layer.setOpacity) { + layerInfo.layer.setOpacity(opacity); + } + } + getLayer(category, layerId) { return this._byLayerId[this._layerIdKey(category, layerId)]; } @@ -244,7 +274,16 @@ export default class LayerManager { delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; if (layerInfo.ctGroup) { - delete this._byCrosstalkGroup[layerInfo.ctGroup][layerInfo.ctKey]; + let ctGroup = this._byCrosstalkGroup[layerInfo.ctGroup]; + let layersForKey = ctGroup[layerInfo.ctKey]; + let idx = layersForKey ? layersForKey.indexOf(stamp) : -1; + if (idx >= 0) { + if (layersForKey.length === 1) { + delete ctGroup[layerInfo.ctKey]; + } else { + layersForKey.splice(idx, 1); + } + } } } From 54c2e36c7857ff1e5ea10874d5a0969916d705b2 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Wed, 8 Jun 2016 11:57:47 -0700 Subject: [PATCH 3/9] Add crosstalk dependencies on the fly --- R/utils.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/utils.R b/R/utils.R index b2887a757..b5dfcf144 100644 --- a/R/utils.R +++ b/R/utils.R @@ -45,6 +45,7 @@ filterNULL = function(x) { #' @export invokeMethod = function(map, data, method, ...) { crosstalkOptions <- if (crosstalk::is.SharedData(data)) { + map$dependencies <- c(map$dependencies, crosstalk::dependencies()) sd <- data data <- sd$data() list( From bd79aaa8173c66e4ab2a12eb446a4e68cb136078 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Wed, 8 Jun 2016 16:44:59 -0700 Subject: [PATCH 4/9] Fix crosstalk bugs; implement path highlighting --- inst/htmlwidgets/leaflet.js | 109 ++++++++++++++++++++------------ javascript/src/layer-manager.js | 68 ++++++++++++++------ 2 files changed, 119 insertions(+), 58 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index a246ffd88..80ea7bc02 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -687,7 +687,7 @@ var LayerManager = function () { this._byCategory[category][stamp] = layer; // Update stamp index - this._byStamp[stamp] = { + var layerInfo = this._byStamp[stamp] = { layer: layer, group: group, ctGroup: ctGroup, @@ -701,53 +701,79 @@ var LayerManager = function () { // Update crosstalk group index if (ctGroup) { (function () { + if (layer.setStyle) { + // Need to save this info so we know what to set opacity to later + layer.options.origOpacity = typeof layer.options.opacity !== "undefined" ? layer.options.opacity : 0.5; + layer.options.origFillOpacity = typeof layer.options.fillOpacity !== "undefined" ? layer.options.fillOpacity : 0.2; + } + var ctg = _this._byCrosstalkGroup[ctGroup]; if (!ctg) { - ctg = _this._byCrosstalkGroup[ctGroup] = {}; - var crosstalk = global.crosstalk; - var fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); - if (!ctg[ctKey]) ctg[ctKey] = []; - ctg[ctKey].push(stamp); - - fs.on("change", function (e) { - var selectedKeys = {}; - for (var i = 0; i < e.value.length; i++) { - selectedKeys[e.value[i]] = true; - } - var groupKeys = Object.keys(ctg); - for (var _i = 0; _i < groupKeys.length; _i++) { - var key = groupKeys[_i]; - var layerInfo = _this._byStamp[ctg[key]]; - _this._setVisibility(layerInfo, selectedKeys[groupKeys[_i]]); - } - }); - crosstalk.group(ctGroup).var("selection").on("change", function (e) { - if (!e.value) { - var groupKeys = Object.keys(ctg); - for (var i = 0; i < groupKeys.length; i++) { - var key = groupKeys[i]; - var layerInfo = _this._byStamp[ctg[key]]; - _this._setOpacity(layerInfo, 1.0); + (function () { + ctg = _this._byCrosstalkGroup[ctGroup] = {}; + var crosstalk = global.crosstalk; + var fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + + var handleFilter = function handleFilter(e) { + if (!e.value) { + var groupKeys = Object.keys(ctg); + for (var i = 0; i < groupKeys.length; i++) { + var key = groupKeys[i]; + var _layerInfo = _this._byStamp[ctg[key]]; + _this._setVisibility(_layerInfo, true); + } + } else { + var selectedKeys = {}; + for (var _i = 0; _i < e.value.length; _i++) { + selectedKeys[e.value[_i]] = true; + } + var _groupKeys = Object.keys(ctg); + for (var _i2 = 0; _i2 < _groupKeys.length; _i2++) { + var _key = _groupKeys[_i2]; + var _layerInfo2 = _this._byStamp[ctg[_key]]; + _this._setVisibility(_layerInfo2, selectedKeys[_groupKeys[_i2]]); + } } - } else { - var selectedKeys = {}; - for (var _i2 = 0; _i2 < e.value.length; _i2++) { - selectedKeys[e.value[_i2]] = true; + }; + fs.on("change", handleFilter); + + var handleSelection = function handleSelection(e) { + if (!e.value) { + var groupKeys = Object.keys(ctg); + for (var i = 0; i < groupKeys.length; i++) { + var key = groupKeys[i]; + var _layerInfo3 = _this._byStamp[ctg[key]]; + _this._setOpacity(_layerInfo3, 1.0); + } + } else { + var selectedKeys = {}; + for (var _i3 = 0; _i3 < e.value.length; _i3++) { + selectedKeys[e.value[_i3]] = true; + } + var _groupKeys2 = Object.keys(ctg); + for (var _i4 = 0; _i4 < _groupKeys2.length; _i4++) { + var _key2 = _groupKeys2[_i4]; + var _layerInfo4 = _this._byStamp[ctg[_key2]]; + _this._setOpacity(_layerInfo4, selectedKeys[_groupKeys2[_i4]] ? 1.0 : 0.2); + } } - var _groupKeys = Object.keys(ctg); - for (var _i3 = 0; _i3 < _groupKeys.length; _i3++) { - var _key = _groupKeys[_i3]; - var _layerInfo = _this._byStamp[ctg[_key]]; - _this._setOpacity(_layerInfo, selectedKeys[_groupKeys[_i3]] ? 1.0 : 0.2); - } - } - }); + }; + crosstalk.group(ctGroup).var("selection").on("change", handleSelection); + + setTimeout(function () { + handleFilter({ value: fs.filteredKeys }); + handleSelection({ value: crosstalk.group(ctGroup).var("selection").get() }); + }, 100); + })(); } + + if (!ctg[ctKey]) ctg[ctKey] = []; + ctg[ctKey].push(stamp); })(); } // Add to container - container.addLayer(layer); + if (!layerInfo.hidden) container.addLayer(layer); return oldLayer; } @@ -769,6 +795,11 @@ var LayerManager = function () { value: function _setOpacity(layerInfo, opacity) { if (layerInfo.layer.setOpacity) { layerInfo.layer.setOpacity(opacity); + } else if (layerInfo.layer.setStyle) { + layerInfo.layer.setStyle({ + opacity: opacity * layerInfo.layer.options.origOpacity, + fillOpacity: opacity * layerInfo.layer.options.origFillOpacity + }); } } }, { diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index 4a8bb99a4..52aa8b53d 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -78,7 +78,7 @@ export default class LayerManager { this._byCategory[category][stamp] = layer; // Update stamp index - this._byStamp[stamp] = { + let layerInfo = this._byStamp[stamp] = { layer: layer, group: group, ctGroup: ctGroup, @@ -91,28 +91,42 @@ export default class LayerManager { // Update crosstalk group index if (ctGroup) { + if (layer.setStyle) { + // Need to save this info so we know what to set opacity to later + layer.options.origOpacity = typeof(layer.options.opacity) !== "undefined" ? layer.options.opacity : 0.5; + layer.options.origFillOpacity = typeof(layer.options.fillOpacity) !== "undefined" ? layer.options.fillOpacity : 0.2; + } + let ctg = this._byCrosstalkGroup[ctGroup]; if (!ctg) { ctg = this._byCrosstalkGroup[ctGroup] = {}; let crosstalk = global.crosstalk; let fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); - if (!ctg[ctKey]) - ctg[ctKey] = []; - ctg[ctKey].push(stamp); - - fs.on("change", (e) => { - let selectedKeys = {}; - for (let i = 0; i < e.value.length; i++) { - selectedKeys[e.value[i]] = true; - } - let groupKeys = Object.keys(ctg); - for (let i = 0; i < groupKeys.length; i++) { - let key = groupKeys[i]; - let layerInfo = this._byStamp[ctg[key]]; - this._setVisibility(layerInfo, selectedKeys[groupKeys[i]]); + + let handleFilter = (e) => { + if (!e.value) { + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setVisibility(layerInfo, true); + } + } else { + let selectedKeys = {}; + for (let i = 0; i < e.value.length; i++) { + selectedKeys[e.value[i]] = true; + } + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setVisibility(layerInfo, selectedKeys[groupKeys[i]]); + } } - }); - crosstalk.group(ctGroup).var("selection").on("change", (e) => { + }; + fs.on("change", handleFilter); + + let handleSelection = (e) => { if (!e.value) { let groupKeys = Object.keys(ctg); for (let i = 0; i < groupKeys.length; i++) { @@ -132,12 +146,23 @@ export default class LayerManager { this._setOpacity(layerInfo, selectedKeys[groupKeys[i]] ? 1.0 : 0.2); } } - }); + }; + crosstalk.group(ctGroup).var("selection").on("change", handleSelection); + + setTimeout(() => { + handleFilter({value: fs.filteredKeys}); + handleSelection({value: crosstalk.group(ctGroup).var("selection").get()}); + }, 100); } + + if (!ctg[ctKey]) + ctg[ctKey] = []; + ctg[ctKey].push(stamp); } // Add to container - container.addLayer(layer); + if (!layerInfo.hidden) + container.addLayer(layer); return oldLayer; } @@ -157,6 +182,11 @@ export default class LayerManager { _setOpacity(layerInfo, opacity) { if (layerInfo.layer.setOpacity) { layerInfo.layer.setOpacity(opacity); + } else if (layerInfo.layer.setStyle) { + layerInfo.layer.setStyle({ + opacity: opacity * layerInfo.layer.options.origOpacity, + fillOpacity: opacity * layerInfo.layer.options.origFillOpacity + }); } } From d58c369cea07f9d3a554544504a24456ec9c523e Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Wed, 15 Jun 2016 17:18:00 -0700 Subject: [PATCH 5/9] Fix crosstalk breakage - crosstalk::dependencies() was renamed to crosstalk::crosstalkLibs() - Also fix tests and metadata --- DESCRIPTION | 3 +++ NAMESPACE | 3 +++ R/utils.R | 3 ++- tests/testit/test-remote.R | 6 +++--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b038843b8..a4fc7f14f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,6 +33,7 @@ URL: http://rstudio.github.io/leaflet/ BugReports: https://github.com/rstudio/leaflet/issues Imports: base64enc, + crosstalk, htmlwidgets, htmltools, magrittr, @@ -50,4 +51,6 @@ Suggests: rgdal, R6, RJSONIO +Remotes: + rstudio/crosstalk RoxygenNote: 5.0.1 diff --git a/NAMESPACE b/NAMESPACE index c0fd033c6..b992df8b0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,11 +2,13 @@ S3method("[",leaflet_awesome_icon_set) S3method("[",leaflet_icon_set) +S3method(pointData,SharedData) S3method(pointData,SpatialPoints) S3method(pointData,SpatialPointsDataFrame) S3method(pointData,data.frame) S3method(pointData,default) S3method(pointData,matrix) +S3method(polygonData,SharedData) export("%>%") export(JS) export(WMSTileOptions) @@ -93,5 +95,6 @@ export(setMaxBounds) export(setView) export(showGroup) export(tileOptions) +import(crosstalk) importFrom(htmlwidgets,JS) importFrom(magrittr,"%>%") diff --git a/R/utils.R b/R/utils.R index b5dfcf144..fa350f11e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -42,10 +42,11 @@ filterNULL = function(x) { #' @param method the name of the JavaScript method to invoke #' @param ... unnamed arguments to be passed to the JavaScript method #' @rdname dispatch +#' @import crosstalk #' @export invokeMethod = function(map, data, method, ...) { crosstalkOptions <- if (crosstalk::is.SharedData(data)) { - map$dependencies <- c(map$dependencies, crosstalk::dependencies()) + map$dependencies <- c(map$dependencies, crosstalk::crosstalkLibs()) sd <- data data <- sd$data() list( diff --git a/tests/testit/test-remote.R b/tests/testit/test-remote.R index 5e8b6557f..019d22fc8 100644 --- a/tests/testit/test-remote.R +++ b/tests/testit/test-remote.R @@ -151,7 +151,7 @@ assert( ) mockSession$.flush() expected <- list( - list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addPolygons\",\"args\":[[[{\"lng\":[1,2,3,4,5],\"lat\":[1,2,3,4,5]}]],null,null,{\"lineCap\":null,\"lineJoin\":null,\"clickable\":true,\"pointerEvents\":null,\"className\":\"\",\"stroke\":true,\"color\":\"#03F\",\"weight\":5,\"opacity\":0.5,\"fill\":true,\"fillColor\":\"#03F\",\"fillOpacity\":0.2,\"dashArray\":null,\"smoothFactor\":1,\"noClip\":false},null,null,null]}]}", class = "json")) + list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addPolygons\",\"args\":[[[{\"lng\":[1,2,3,4,5],\"lat\":[1,2,3,4,5]}]],null,null,{\"lineCap\":null,\"lineJoin\":null,\"clickable\":true,\"pointerEvents\":null,\"className\":\"\",\"stroke\":true,\"color\":\"#03F\",\"weight\":5,\"opacity\":0.5,\"fill\":true,\"fillColor\":\"#03F\",\"fillOpacity\":0.2,\"dashArray\":null,\"smoothFactor\":1,\"noClip\":false},null,null,null,null]}]}", class = "json")) ) # cat(deparse(mockSession$.calls), "\n") assert(identical(mockSession$.calls, expected)) @@ -167,7 +167,7 @@ remote2 <- leafletProxy("map", mockSession, ) # Check that addMarkers() takes effect immediately, no flush required remote2 %>% addMarkers() -expected2 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) +expected2 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) # cat(deparse(mockSession$.calls), "\n") assert(identical(mockSession$.calls, expected2)) # Flushing should do nothing @@ -184,6 +184,6 @@ remote3 <- leafletProxy("map", mockSession, remote3 %>% clearShapes() %>% addMarkers() assert(identical(mockSession$.calls, list())) mockSession$.flush() -expected3 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"clearShapes\",\"args\":[]}]}", class = "json")), .Names = c("type", "message")), structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) +expected3 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"clearShapes\",\"args\":[null]}]}", class = "json")), .Names = c("type", "message")), structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) # Check that multiple calls are invoked in order assert(identical(mockSession$.calls, expected3)) From 786f1b1fdfa298ca1275782b544d710256a063d9 Mon Sep 17 00:00:00 2001 From: Joe Cheng Date: Wed, 22 Jun 2016 22:53:46 -0700 Subject: [PATCH 6/9] Crosstalk selection can have zero length when inactive --- inst/htmlwidgets/leaflet.js | 2 +- javascript/src/layer-manager.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 80ea7bc02..b12378767 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -738,7 +738,7 @@ var LayerManager = function () { fs.on("change", handleFilter); var handleSelection = function handleSelection(e) { - if (!e.value) { + if (!e.value || !e.value.length) { var groupKeys = Object.keys(ctg); for (var i = 0; i < groupKeys.length; i++) { var key = groupKeys[i]; diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index 52aa8b53d..404272f03 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -127,7 +127,7 @@ export default class LayerManager { fs.on("change", handleFilter); let handleSelection = (e) => { - if (!e.value) { + if (!e.value || !e.value.length) { let groupKeys = Object.keys(ctg); for (let i = 0; i < groupKeys.length; i++) { let key = groupKeys[i]; From 5f2efaf6426679a1efa6352060a58bf8dd1da211 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 25 Aug 2016 20:17:46 -0500 Subject: [PATCH 7/9] coloring of transient selections is working --- inst/htmlwidgets/leaflet.js | 20 +++++++++++++++++--- javascript/src/layer-manager.js | 22 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index b12378767..b9642f088 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -705,6 +705,8 @@ var LayerManager = function () { // Need to save this info so we know what to set opacity to later layer.options.origOpacity = typeof layer.options.opacity !== "undefined" ? layer.options.opacity : 0.5; layer.options.origFillOpacity = typeof layer.options.fillOpacity !== "undefined" ? layer.options.fillOpacity : 0.2; + layer.options.origColor = typeof layer.options.color !== "undefined" ? layer.options.color : "#03F"; + layer.options.origFillColor = typeof layer.options.fillColor !== "undefined" ? layer.options.fillColor : layer.options.origColor; } var ctg = _this._byCrosstalkGroup[ctGroup]; @@ -743,6 +745,9 @@ var LayerManager = function () { for (var i = 0; i < groupKeys.length; i++) { var key = groupKeys[i]; var _layerInfo3 = _this._byStamp[ctg[key]]; + var opts = _layerInfo3.layer.options; + _layerInfo3.layer.options.ctColor = opts.origColor; + _layerInfo3.layer.options.ctFillColor = opts.origFillColor; _this._setOpacity(_layerInfo3, 1.0); } } else { @@ -751,10 +756,18 @@ var LayerManager = function () { selectedKeys[e.value[_i3]] = true; } var _groupKeys2 = Object.keys(ctg); + // for compatability with plotly's ability to colour selections + // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 + var selectionColour = crosstalk.var("selectionColour").get(); + console.log(selectionColour); for (var _i4 = 0; _i4 < _groupKeys2.length; _i4++) { var _key2 = _groupKeys2[_i4]; var _layerInfo4 = _this._byStamp[ctg[_key2]]; - _this._setOpacity(_layerInfo4, selectedKeys[_groupKeys2[_i4]] ? 1.0 : 0.2); + var _opts = _layerInfo4.layer.options; + var selected = selectedKeys[_groupKeys2[_i4]]; + _layerInfo4.layer.options.ctColor = selected ? selectionColour || _opts.origColor : _opts.origColor; + _layerInfo4.layer.options.ctFillColor = selected ? selectionColour || _opts.origFillColor : _opts.origFillColor; + _this._setOpacity(_layerInfo4, selected ? 1.0 : 0.2); } } }; @@ -798,7 +811,9 @@ var LayerManager = function () { } else if (layerInfo.layer.setStyle) { layerInfo.layer.setStyle({ opacity: opacity * layerInfo.layer.options.origOpacity, - fillOpacity: opacity * layerInfo.layer.options.origFillOpacity + fillOpacity: opacity * layerInfo.layer.options.origFillOpacity, + color: layerInfo.layer.options.ctColor, + fillColor: layerInfo.layer.options.ctFillColor }); } } @@ -1971,7 +1986,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // pixel of the original image has some contribution to the downscaled image) // as opposed to a single-step downscaling which will discard a lot of data // (and with sparse images at small scales can give very surprising results). - var Mipmapper = function () { function Mipmapper(img) { _classCallCheck(this, Mipmapper); diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index 404272f03..361ea70d6 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -95,6 +95,8 @@ export default class LayerManager { // Need to save this info so we know what to set opacity to later layer.options.origOpacity = typeof(layer.options.opacity) !== "undefined" ? layer.options.opacity : 0.5; layer.options.origFillOpacity = typeof(layer.options.fillOpacity) !== "undefined" ? layer.options.fillOpacity : 0.2; + layer.options.origColor = typeof layer.options.color !== "undefined" ? layer.options.color : "#03F"; + layer.options.origFillColor = typeof layer.options.fillColor !== "undefined" ? layer.options.fillColor : layer.options.origColor; } let ctg = this._byCrosstalkGroup[ctGroup]; @@ -132,7 +134,11 @@ export default class LayerManager { for (let i = 0; i < groupKeys.length; i++) { let key = groupKeys[i]; let layerInfo = this._byStamp[ctg[key]]; + let opts = layerInfo.layer.options; + layerInfo.layer.options.ctColor = opts.origColor; + layerInfo.layer.options.ctFillColor = opts.origFillColor; this._setOpacity(layerInfo, 1.0); + } } else { let selectedKeys = {}; @@ -140,10 +146,20 @@ export default class LayerManager { selectedKeys[e.value[i]] = true; } let groupKeys = Object.keys(ctg); + // for compatability with plotly's ability to colour selections + // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 + let selectionColour = crosstalk.var("selectionColour").get(); + console.log(selectionColour); for (let i = 0; i < groupKeys.length; i++) { let key = groupKeys[i]; let layerInfo = this._byStamp[ctg[key]]; - this._setOpacity(layerInfo, selectedKeys[groupKeys[i]] ? 1.0 : 0.2); + let opts = layerInfo.layer.options; + let selected = selectedKeys[groupKeys[i]]; + layerInfo.layer.options.ctColor = + selected ? selectionColour || opts.origColor : opts.origColor; + layerInfo.layer.options.ctFillColor = + selected ? selectionColour || opts.origFillColor : opts.origFillColor; + this._setOpacity(layerInfo, selected ? 1.0 : 0.2); } } }; @@ -185,7 +201,9 @@ export default class LayerManager { } else if (layerInfo.layer.setStyle) { layerInfo.layer.setStyle({ opacity: opacity * layerInfo.layer.options.origOpacity, - fillOpacity: opacity * layerInfo.layer.options.origFillOpacity + fillOpacity: opacity * layerInfo.layer.options.origFillOpacity, + color: layerInfo.layer.options.ctColor, + fillColor: layerInfo.layer.options.ctFillColor }); } } From 56eb3ecbb25ddc195c1cc6f530246dbb565f99ee Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 26 Aug 2016 10:29:17 -0500 Subject: [PATCH 8/9] get persistent selection working --- DESCRIPTION | 3 +- inst/htmlwidgets/leaflet.js | 50 ++++++++++++++++------------ javascript/src/layer-manager.js | 58 ++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a4fc7f14f..8a8dc3fdc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -52,5 +52,6 @@ Suggests: R6, RJSONIO Remotes: - rstudio/crosstalk + rstudio/crosstalk, + jcheng5/plotly@joe/feature/crosstalk RoxygenNote: 5.0.1 diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index b9642f088..8e52ee68c 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -745,10 +745,12 @@ var LayerManager = function () { for (var i = 0; i < groupKeys.length; i++) { var key = groupKeys[i]; var _layerInfo3 = _this._byStamp[ctg[key]]; - var opts = _layerInfo3.layer.options; - _layerInfo3.layer.options.ctColor = opts.origColor; - _layerInfo3.layer.options.ctFillColor = opts.origFillColor; - _this._setOpacity(_layerInfo3, 1.0); + // reset the crosstalk style params + _layerInfo3.layer.options.ctOpacity = undefined; + _layerInfo3.layer.options.ctFillOpacity = undefined; + _layerInfo3.layer.options.ctColor = undefined; + _layerInfo3.layer.options.ctFillColor = undefined; + _this._setStyle(_layerInfo3); } } else { var selectedKeys = {}; @@ -758,16 +760,22 @@ var LayerManager = function () { var _groupKeys2 = Object.keys(ctg); // for compatability with plotly's ability to colour selections // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 - var selectionColour = crosstalk.var("selectionColour").get(); - console.log(selectionColour); + var selectionColour = crosstalk.var("plotlySelectionColour").get(); + var ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || { opacityDim: 0.2 }; + var persist = ctOpts.persistent === true; for (var _i4 = 0; _i4 < _groupKeys2.length; _i4++) { var _key2 = _groupKeys2[_i4]; var _layerInfo4 = _this._byStamp[ctg[_key2]]; - var _opts = _layerInfo4.layer.options; var selected = selectedKeys[_groupKeys2[_i4]]; - _layerInfo4.layer.options.ctColor = selected ? selectionColour || _opts.origColor : _opts.origColor; - _layerInfo4.layer.options.ctFillColor = selected ? selectionColour || _opts.origFillColor : _opts.origFillColor; - _this._setOpacity(_layerInfo4, selected ? 1.0 : 0.2); + var opts = _layerInfo4.layer.options; + + // remember "old" selection colors if this is persistent selection + _layerInfo4.layer.options.ctColor = selected ? selectionColour : persist ? opts.ctColor : opts.origColor; + _layerInfo4.layer.options.ctFillColor = selected ? selectionColour : persist ? opts.ctFillColor : opts.origFillColor; + + _layerInfo4.layer.options.ctOpacity = selected ? opts.origOpacity : persist && opts.origOpacity == opts.ctOpacity ? opts.origOpacity : ctOpts.opacityDim * opts.origOpacity; + + _this._setStyle(_layerInfo4); } } }; @@ -804,18 +812,18 @@ var LayerManager = function () { } } }, { - key: "_setOpacity", - value: function _setOpacity(layerInfo, opacity) { - if (layerInfo.layer.setOpacity) { - layerInfo.layer.setOpacity(opacity); - } else if (layerInfo.layer.setStyle) { - layerInfo.layer.setStyle({ - opacity: opacity * layerInfo.layer.options.origOpacity, - fillOpacity: opacity * layerInfo.layer.options.origFillOpacity, - color: layerInfo.layer.options.ctColor, - fillColor: layerInfo.layer.options.ctFillColor - }); + key: "_setStyle", + value: function _setStyle(layerInfo) { + var opts = layerInfo.layer.options; + if (!layerInfo.layer.setStyle) { + return; } + layerInfo.layer.setStyle({ + opacity: opts.ctOpacity || opts.origOpacity, + fillOpacity: opts.ctFillOpacity || opts.origFillOpacity, + color: opts.ctColor || opts.origColor, + fillColor: opts.ctFillColor || opts.origFillColor + }); } }, { key: "getLayer", diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index 361ea70d6..a96b2094a 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -95,8 +95,8 @@ export default class LayerManager { // Need to save this info so we know what to set opacity to later layer.options.origOpacity = typeof(layer.options.opacity) !== "undefined" ? layer.options.opacity : 0.5; layer.options.origFillOpacity = typeof(layer.options.fillOpacity) !== "undefined" ? layer.options.fillOpacity : 0.2; - layer.options.origColor = typeof layer.options.color !== "undefined" ? layer.options.color : "#03F"; - layer.options.origFillColor = typeof layer.options.fillColor !== "undefined" ? layer.options.fillColor : layer.options.origColor; + layer.options.origColor = typeof(layer.options.color) !== "undefined" ? layer.options.color : "#03F"; + layer.options.origFillColor = typeof(layer.options.fillColor) !== "undefined" ? layer.options.fillColor : layer.options.origColor; } let ctg = this._byCrosstalkGroup[ctGroup]; @@ -134,11 +134,12 @@ export default class LayerManager { for (let i = 0; i < groupKeys.length; i++) { let key = groupKeys[i]; let layerInfo = this._byStamp[ctg[key]]; - let opts = layerInfo.layer.options; - layerInfo.layer.options.ctColor = opts.origColor; - layerInfo.layer.options.ctFillColor = opts.origFillColor; - this._setOpacity(layerInfo, 1.0); - + // reset the crosstalk style params + layerInfo.layer.options.ctOpacity = undefined; + layerInfo.layer.options.ctFillOpacity = undefined; + layerInfo.layer.options.ctColor = undefined; + layerInfo.layer.options.ctFillColor = undefined; + this._setStyle(layerInfo); } } else { let selectedKeys = {}; @@ -148,18 +149,29 @@ export default class LayerManager { let groupKeys = Object.keys(ctg); // for compatability with plotly's ability to colour selections // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 - let selectionColour = crosstalk.var("selectionColour").get(); - console.log(selectionColour); + let selectionColour = crosstalk.var("plotlySelectionColour").get(); + let ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || {opacityDim: 0.2}; + let persist = ctOpts.persistent === true; for (let i = 0; i < groupKeys.length; i++) { let key = groupKeys[i]; let layerInfo = this._byStamp[ctg[key]]; - let opts = layerInfo.layer.options; let selected = selectedKeys[groupKeys[i]]; + let opts = layerInfo.layer.options; + + // remember "old" selection colors if this is persistent selection layerInfo.layer.options.ctColor = - selected ? selectionColour || opts.origColor : opts.origColor; + selected ? selectionColour : + persist ? opts.ctColor : opts.origColor; layerInfo.layer.options.ctFillColor = - selected ? selectionColour || opts.origFillColor : opts.origFillColor; - this._setOpacity(layerInfo, selected ? 1.0 : 0.2); + selected ? selectionColour : + persist ? opts.ctFillColor : opts.origFillColor; + + layerInfo.layer.options.ctOpacity = + selected ? opts.origOpacity : + (persist && opts.origOpacity == opts.ctOpacity) ? opts.origOpacity : + ctOpts.opacityDim * opts.origOpacity; + + this._setStyle(layerInfo); } } }; @@ -195,17 +207,17 @@ export default class LayerManager { } } - _setOpacity(layerInfo, opacity) { - if (layerInfo.layer.setOpacity) { - layerInfo.layer.setOpacity(opacity); - } else if (layerInfo.layer.setStyle) { - layerInfo.layer.setStyle({ - opacity: opacity * layerInfo.layer.options.origOpacity, - fillOpacity: opacity * layerInfo.layer.options.origFillOpacity, - color: layerInfo.layer.options.ctColor, - fillColor: layerInfo.layer.options.ctFillColor - }); + _setStyle(layerInfo) { + let opts = layerInfo.layer.options; + if (!layerInfo.layer.setStyle) { + return; } + layerInfo.layer.setStyle({ + opacity: opts.ctOpacity || opts.origOpacity, + fillOpacity: opts.ctFillOpacity || opts.origFillOpacity, + color: opts.ctColor || opts.origColor, + fillColor: opts.ctFillColor || opts.origFillColor + }); } getLayer(category, layerId) { From da91b64b8b107197e872bab7c59e485c50e16878 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 31 Oct 2016 10:50:17 -0500 Subject: [PATCH 9/9] handle 2D array selections (nested keys) --- inst/htmlwidgets/leaflet.js | 9 ++++++++- javascript/src/layer-manager.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 8e52ee68c..8ee03906c 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -755,7 +755,14 @@ var LayerManager = function () { } else { var selectedKeys = {}; for (var _i3 = 0; _i3 < e.value.length; _i3++) { - selectedKeys[e.value[_i3]] = true; + var val = e.value[_i3]; + // support 2D arrays (nested keys) + if (Array.isArray(val)) { + for (var j = 0; j < val.length; j++) { + selectedKeys[val[j]] = true; + } + } + selectedKeys[val] = true; } var _groupKeys2 = Object.keys(ctg); // for compatability with plotly's ability to colour selections diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index a96b2094a..cb6c7ce9a 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -144,7 +144,14 @@ export default class LayerManager { } else { let selectedKeys = {}; for (let i = 0; i < e.value.length; i++) { - selectedKeys[e.value[i]] = true; + let val = e.value[i]; + // support 2D arrays (nested keys) + if (Array.isArray(val)) { + for (let j = 0; j < val.length; j++) { + selectedKeys[val[j]] = true; + } + } + selectedKeys[val] = true; } let groupKeys = Object.keys(ctg); // for compatability with plotly's ability to colour selections