diff --git a/.travis.yml b/.travis.yml
index cfa1e5be59..9fc70ea623 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,36 +1,9 @@
-language: R
-cache: packages
-warnings_are_errors: false
-
-r:
- - devel
- - oldrel
- - release
-
-addons:
- apt:
- sources:
- - sourceline: 'ppa:ubuntugis/ppa'
- packages:
- - libudunits2-dev
- - libproj-dev
- - libgeos-dev
- - libgdal-dev
-
-env:
- global:
- # don't treat missing suggested packages as error
- - _R_CHECK_FORCE_SUGGESTS_=false
- # plotly_api_key (for posting to plot.ly)
- - secure: "WsvmMHN4YVhnk0bLRE04APcLbs5s4vWKSHjEdU0bPXd0xdMTzZeP5D7pxyF1983C+P5LpSnGHv4dgwLMBkNzxJwBR7/Ta7lfO1akYILWwxib+1DVbCqUH5Z4Ba1FSCQptIrLNGR3P7+0Lem4hEhqKdPKltFnxhnXO0Y+MeG71IQ="
- # MAPBOX_TOKEN (for testing `plot_mapbox()`)
- - secure: "QPBEqtLRdwb4ablJORzD0JdCT9ESe3nNdIehM1oxfcNKfpSdf2OFxH3TkeYY1nMpv0mLxiMNTy6xcj9Yk5MaaBCIA0P7q6OdZv9ruzQD1j3g84gP45KwBilbPGjb+/EvOS0fM25vR/pAmA8IyoUfPC2J8HwiNnW8DYdt/hJOJ9A="
- # GITHUB_PAT
- - secure: "lZf7GBt1+ogux5WAXmIZ6/VBTmR1G7rv+8Aevogfan7GWb32K3IVatsrKr1pSvz0hysP0MDueQqTv0GcNOvrFnYvobyo6fFpx+n33WkXQCGybUOHyfJpPdw1L2wtAy88ugNJDl+n9fdXr4yL2cWvRqq4WBKeeSi6hpdiKOaoB3Y="
-
+language: node_js
+node_js:
+ - "12"
+sudo: required
+dist: xenial
before_install:
- - echo "Sys.setenv('plotly_username' = 'cpsievert')" > ~/.Rprofile
-
-before_script:
- - if [[ "$TRAVIS_R_VERSION_STRING" == "release" ]]; then docker run -e GITHUB_PAT=$GITHUB_PAT -e MAPBOX_TOKEN=$MAPBOX_TOKEN -e VMODE="ci" -v $(pwd):/home/plotly --privileged cpsievert/plotly-orca; fi
-
+ - sudo apt-get update
+script:
+ - jest
\ No newline at end of file
diff --git a/README.md b/README.md
index 0621bf783e..80f626f616 100644
--- a/README.md
+++ b/README.md
@@ -1,114 +1,3 @@
+This is a fork of the ropensci/plotly HtmlWidget for use in Displayr. The repository has been modified to be an node package and use the rhtmlBuildUtils framework. This was done in order to run unit tests on the widget js code, which has been modified by Displayr. The integration with rhtmlBuildUtils is incomplete, and only the building and unit testing of widget js code is supported. Features from rhtmlBuildUtils such as visual regression testing, linting and the internal web server are absent.
-
-
-
-
-[](https://travis-ci.org/ropensci/plotly)
-[](https://cran.r-project.org/package=plotly)
-[](https://www.rpackages.io/package/plotly)
-[](https://www.rpackages.io/package/plotly)
-
-An R package for creating interactive web graphics via the open source
-JavaScript graphing library
-[plotly.js](https://github.com/plotly/plotly.js).
-
-## Installation
-
-Install from CRAN:
-
-``` r
-install.packages("plotly")
-```
-
-Or install the latest development version (on GitHub) via devtools:
-
-``` r
-devtools::install_github("ropensci/plotly")
-```
-
-## Getting started
-
-### Web-based ggplot2 graphics
-
-If you use [ggplot2](https://github.com/tidyverse/ggplot2), `ggplotly()`
-converts your static plots to an interactive web-based version\!
-
-``` r
-library(plotly)
-g <- ggplot(faithful, aes(x = eruptions, y = waiting)) +
- stat_density_2d(aes(fill = ..level..), geom = "polygon") +
- xlim(1, 6) + ylim(40, 100)
-ggplotly(g)
-```
-
-
-
-By default, `ggplotly()` tries to replicate the static ggplot2 version
-exactly (before any interaction occurs), but sometimes you need greater
-control over the interactive behavior. The `ggplotly()` function itself
-has some convenient “high-level” arguments, such as `dynamicTicks`,
-which tells plotly.js to dynamically recompute axes, when appropriate.
-The `style()` function also comes in handy for *modifying* the
-underlying trace
-attributes (e.g. [hoveron](https://plotly.com/r/reference/#scatter-hoveron)) used to generate the plot:
-
-``` r
-gg <- ggplotly(g, dynamicTicks = "y")
-style(gg, hoveron = "points", hoverinfo = "x+y+text", hoverlabel = list(bgcolor = "white"))
-```
-
-
-
-Moreover, since `ggplotly()` returns a plotly object, you can apply
-essentially any function from the R package on that object. Some useful
-ones include `layout()` (for [customizing the
-layout](https://plotly-r.com/improving-ggplotly.html#modifying-layout)),
-`add_traces()` (and its higher-level `add_*()` siblings, for example
-`add_polygons()`, for [adding new
-traces/data](https://plotly-r.com/improving-ggplotly.html#leveraging-statistical-output)),
-`subplot()` (for [combining multiple plotly
-objects](https://plotly-r.com/arranging-views.html#arranging-plotly-objects)),
-and `plotly_json()` (for inspecting the underlying JSON sent to
-plotly.js).
-
-The `ggplotly()` function will also respect some “unofficial”
-**ggplot2** aesthetics, namely `text` (for [customizing the
-tooltip](https://plotly-r.com/controlling-tooltips.html#tooltip-text-ggplotly)),
-`frame` (for [creating
-animations](https://plotly-r.com/animating-views.html)),
-and `ids` (for ensuring sensible smooth transitions).
-
-### Using plotly without ggplot2
-
-The `plot_ly()` function provides a more direct interface to plotly.js
-so you can leverage more specialized chart types (e.g., [parallel
-coordinates](https://plotly.com/r/parallel-coordinates-plot/) or
-[maps](https://plotly.com/r/maps/)) or even some visualization that the
-ggplot2 API won’t ever support (e.g., surface,
-[mesh](https://plotly.com/r/3d-mesh/),
-[trisurf](https://plotly.com/r/trisurf/), etc).
-
-``` r
-plot_ly(z = ~volcano, type = "surface")
-```
-
-
-
-## Learn more
-
-To learn more about special features that the plotly R package provides (e.g., [client-side linking](https://plotly-r.com/client-side-linking.html), [**shiny** integration](https://plotly-r.com/linking-views-with-shiny.html), [editing and generating static images](https://plotly-r.com/publish.html), [custom events in JavaScript](https://plotly-r.com/javascript.html), and more), see . You may already be familiar with existing plotly documentation (e.g., ), which is essentially a language-agnostic how-to guide for learning plotly.js, whereas is meant to be more wholistic tutorial written by and for the R user. The package itself ships with a number of demos (list them by running `demo(package = "plotly")`) and shiny/rmarkdown examples (list them by running `plotly_example("shiny")` or `plotly_example("rmd")`). [Carson](https://cpsievert.me) also keeps numerous [slide decks](https://talks.cpsievert.me) with useful examples and concepts.
-
-## Contributing
-
-Please read through our [contributing
-guidelines](https://github.com/ropensci/plotly/blob/master/CONTRIBUTING.md).
-Included are directions for opening issues, asking questions,
-contributing changes to plotly, and our code of
-conduct.
-
------
-
-
+Changes to widget js code should be made in [theSrc/tasks](theSrc/tasks) and `gulp compileWidgetEntryPoint` needs to be called to compile the code and write it to [inst/htmlwidgets](inst/htmlwidgets).
\ No newline at end of file
diff --git a/build/config/widget.config.js b/build/config/widget.config.js
new file mode 100644
index 0000000000..5ea3388f01
--- /dev/null
+++ b/build/config/widget.config.js
@@ -0,0 +1,11 @@
+const cliArgs = require('yargs').argv
+const _ = require('lodash')
+
+const config = {
+ widgetEntryPoint: 'theSrc/scripts/plotly.js',
+ widgetFactory: 'theSrc/scripts/plotly.factory.js',
+ widgetName: 'plotly',
+}
+
+const commandLineOverides = _.omit(cliArgs, ['_', '$0'])
+module.exports = _.merge(config, commandLineOverides)
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000000..fb77040bff
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,8 @@
+const gulp = require('gulp')
+const rhtmlBuildUtils = require('rhtmlBuildUtils')
+
+const dontRegisterTheseTasks = []
+rhtmlBuildUtils.registerGulpTasks({
+ gulp: gulp,
+ exclusions: dontRegisterTheseTasks
+})
\ No newline at end of file
diff --git a/inst/htmlwidgets/plotly.js b/inst/htmlwidgets/plotly.js
index abe689973c..d637157815 100644
--- a/inst/htmlwidgets/plotly.js
+++ b/inst/htmlwidgets/plotly.js
@@ -1,934 +1,2 @@
-HTMLWidgets.widget({
- name: "plotly",
- type: "output",
-
- initialize: function(el, width, height) {
- return {};
- },
-
- resize: function(el, width, height, instance) {
- if (instance.autosize) {
- var width = instance.width || width;
- var height = instance.height || height;
- Plotly.relayout(el, {width: width, height: height});
- }
- },
-
- renderValue: function(el, x, instance) {
-
- // Plotly.relayout() mutates the plot input object, so make sure to
- // keep a reference to the user-supplied width/height *before*
- // we call Plotly.plot();
- var lay = x.layout || {};
- instance.width = lay.width;
- instance.height = lay.height;
- instance.autosize = lay.autosize || true;
-
- /*
- / 'inform the world' about highlighting options this is so other
- / crosstalk libraries have a chance to respond to special settings
- / such as persistent selection.
- / AFAIK, leaflet is the only library with such intergration
- / https://github.com/rstudio/leaflet/pull/346/files#diff-ad0c2d51ce5fdf8c90c7395b102f4265R154
- */
- var ctConfig = crosstalk.var('plotlyCrosstalkOpts').set(x.highlight);
-
- if (typeof(window) !== "undefined") {
- // make sure plots don't get created outside the network (for on-prem)
- window.PLOTLYENV = window.PLOTLYENV || {};
- window.PLOTLYENV.BASE_URL = x.base_url;
-
- // Enable persistent selection when shift key is down
- // https://stackoverflow.com/questions/1828613/check-if-a-key-is-down
- var persistOnShift = function(e) {
- if (!e) window.event;
- if (e.shiftKey) {
- x.highlight.persistent = true;
- x.highlight.persistentShift = true;
- } else {
- x.highlight.persistent = false;
- x.highlight.persistentShift = false;
- }
- };
-
- // Only relevant if we haven't forced persistent mode at command line
- if (!x.highlight.persistent) {
- window.onmousemove = persistOnShift;
- }
- }
-
- var graphDiv = document.getElementById(el.id);
-
- // TODO: move the control panel injection strategy inside here...
- HTMLWidgets.addPostRenderHandler(function() {
-
- // lower the z-index of the modebar to prevent it from highjacking hover
- // (TODO: do this via CSS?)
- // https://github.com/ropensci/plotly/issues/956
- // https://www.w3schools.com/jsref/prop_style_zindex.asp
- var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar");
- for (var i = 0; i < modebars.length; i++) {
- modebars[i].style.zIndex = 1;
- }
- });
-
- // inject a "control panel" holding selectize/dynamic color widget(s)
- if ((x.selectize || x.highlight.dynamic) && !instance.plotly) {
- var flex = document.createElement("div");
- flex.class = "plotly-crosstalk-control-panel";
- flex.style = "display: flex; flex-wrap: wrap";
-
- // inject the colourpicker HTML container into the flexbox
- if (x.highlight.dynamic) {
- var pickerDiv = document.createElement("div");
-
- var pickerInput = document.createElement("input");
- pickerInput.id = el.id + "-colourpicker";
- pickerInput.placeholder = "asdasd";
-
- var pickerLabel = document.createElement("label");
- pickerLabel.for = pickerInput.id;
- pickerLabel.innerHTML = "Brush color ";
-
- pickerDiv.appendChild(pickerLabel);
- pickerDiv.appendChild(pickerInput);
- flex.appendChild(pickerDiv);
- }
-
- // inject selectize HTML containers (one for every crosstalk group)
- if (x.selectize) {
- var ids = Object.keys(x.selectize);
-
- for (var i = 0; i < ids.length; i++) {
- var container = document.createElement("div");
- container.id = ids[i];
- container.style = "width: 80%; height: 10%";
- container.class = "form-group crosstalk-input-plotly-highlight";
-
- var label = document.createElement("label");
- label.for = ids[i];
- label.innerHTML = x.selectize[ids[i]].group;
- label.class = "control-label";
-
- var selectDiv = document.createElement("div");
- var select = document.createElement("select");
- select.multiple = true;
-
- selectDiv.appendChild(select);
- container.appendChild(label);
- container.appendChild(selectDiv);
- flex.appendChild(container);
- }
- }
-
- // finally, insert the flexbox inside the htmlwidget container,
- // but before the plotly graph div
- graphDiv.parentElement.insertBefore(flex, graphDiv);
-
- if (x.highlight.dynamic) {
- var picker = $("#" + pickerInput.id);
- var colors = x.highlight.color || [];
- // TODO: let users specify options?
- var opts = {
- value: colors[0],
- showColour: "both",
- palette: "limited",
- allowedCols: colors.join(" "),
- width: "20%",
- height: "10%"
- };
- picker.colourpicker({changeDelay: 0});
- picker.colourpicker("settings", opts);
- picker.colourpicker("value", opts.value);
- // inform crosstalk about a change in the current selection colour
- var grps = x.highlight.ctGroups || [];
- for (var i = 0; i < grps.length; i++) {
- crosstalk.group(grps[i]).var('plotlySelectionColour')
- .set(picker.colourpicker('value'));
- }
- picker.on("change", function() {
- for (var i = 0; i < grps.length; i++) {
- crosstalk.group(grps[i]).var('plotlySelectionColour')
- .set(picker.colourpicker('value'));
- }
- });
- }
- }
-
- // if no plot exists yet, create one with a particular configuration
- if (!instance.plotly) {
-
- var plot = Plotly.plot(graphDiv, x);
- instance.plotly = true;
-
- } else {
-
- // this is essentially equivalent to Plotly.newPlot(), but avoids creating
- // a new webgl context
- // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532
-
- // TODO: restore crosstalk selections?
- Plotly.purge(graphDiv);
- // TODO: why is this necessary to get crosstalk working?
- graphDiv.data = undefined;
- graphDiv.layout = undefined;
- var plot = Plotly.plot(graphDiv, x);
- }
-
- // Trigger plotly.js calls defined via `plotlyProxy()`
- plot.then(function() {
- if (HTMLWidgets.shinyMode) {
- Shiny.addCustomMessageHandler("plotly-calls", function(msg) {
- var gd = document.getElementById(msg.id);
- if (!gd) {
- throw new Error("Couldn't find plotly graph with id: " + msg.id);
- }
- // This isn't an official plotly.js method, but it's the only current way to
- // change just the configuration of a plot
- // https://community.plot.ly/t/update-config-function/9057
- if (msg.method == "reconfig") {
- Plotly.react(gd, gd.data, gd.layout, msg.args);
- return;
- }
- if (!Plotly[msg.method]) {
- throw new Error("Unknown method " + msg.method);
- }
- var args = [gd].concat(msg.args);
- Plotly[msg.method].apply(null, args);
- });
- }
-
- // plotly's mapbox API doesn't currently support setting bounding boxes
- // https://www.mapbox.com/mapbox-gl-js/example/fitbounds/
- // so we do this manually...
- // TODO: make sure this triggers on a redraw and relayout as well as on initial draw
- var mapboxIDs = graphDiv._fullLayout._subplots.mapbox || [];
- for (var i = 0; i < mapboxIDs.length; i++) {
- var id = mapboxIDs[i];
- var mapOpts = x.layout[id] || {};
- var args = mapOpts._fitBounds || {};
- if (!args) {
- continue;
- }
- var mapObj = graphDiv._fullLayout[id]._subplot.map;
- mapObj.fitBounds(args.bounds, args.options);
- }
-
- });
-
- // Attach attributes (e.g., "key", "z") to plotly event data
- function eventDataWithKey(eventData) {
- if (eventData === undefined || !eventData.hasOwnProperty("points")) {
- return null;
- }
- return eventData.points.map(function(pt) {
- var obj = {
- curveNumber: pt.curveNumber,
- pointNumber: pt.pointNumber,
- x: pt.x,
- y: pt.y
- };
-
- // If 'z' is reported with the event data, then use it!
- if (pt.hasOwnProperty("z")) {
- obj.z = pt.z;
- }
-
- if (pt.hasOwnProperty("customdata")) {
- obj.customdata = pt.customdata;
- }
-
- /*
- TL;DR: (I think) we have to select the graph div (again) to attach keys...
-
- Why? Remember that crosstalk will dynamically add/delete traces
- (see traceManager.prototype.updateSelection() below)
- For this reason, we can't simply grab keys from x.data (like we did previously)
- Moreover, we can't use _fullData, since that doesn't include
- unofficial attributes. It's true that click/hover events fire with
- pt.data, but drag events don't...
- */
- var gd = document.getElementById(el.id);
- var trace = gd.data[pt.curveNumber];
-
- if (!trace._isSimpleKey) {
- var attrsToAttach = ["key"];
- } else {
- // simple keys fire the whole key
- obj.key = trace.key;
- var attrsToAttach = [];
- }
-
- for (var i = 0; i < attrsToAttach.length; i++) {
- var attr = trace[attrsToAttach[i]];
- if (Array.isArray(attr)) {
- if (typeof pt.pointNumber === "number") {
- obj[attrsToAttach[i]] = attr[pt.pointNumber];
- } else if (Array.isArray(pt.pointNumber)) {
- obj[attrsToAttach[i]] = attr[pt.pointNumber[0]][pt.pointNumber[1]];
- } else if (Array.isArray(pt.pointNumbers)) {
- obj[attrsToAttach[i]] = pt.pointNumbers.map(function(idx) { return attr[idx]; });
- }
- }
- }
- return obj;
- });
- }
-
-
- var legendEventData = function(d) {
- // if legendgroup is not relevant just return the trace
- var trace = d.data[d.curveNumber];
- if (!trace.legendgroup) return trace;
-
- // if legendgroup was specified, return all traces that match the group
- var legendgrps = d.data.map(function(trace){ return trace.legendgroup; });
- var traces = [];
- for (i = 0; i < legendgrps.length; i++) {
- if (legendgrps[i] == trace.legendgroup) {
- traces.push(d.data[i]);
- }
- }
-
- return traces;
- };
-
-
- // send user input event data to shiny
- if (HTMLWidgets.shinyMode && Shiny.setInputValue) {
-
- // Some events clear other input values
- // TODO: always register these?
- var eventClearMap = {
- plotly_deselect: ["plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing", "plotly_click"],
- plotly_unhover: ["plotly_hover"],
- plotly_doubleclick: ["plotly_click"]
- };
-
- Object.keys(eventClearMap).map(function(evt) {
- graphDiv.on(evt, function() {
- var inputsToClear = eventClearMap[evt];
- inputsToClear.map(function(input) {
- Shiny.setInputValue(input + "-" + x.source, null, {priority: "event"});
- });
- });
- });
-
- var eventDataFunctionMap = {
- plotly_click: eventDataWithKey,
- plotly_sunburstclick: eventDataWithKey,
- plotly_hover: eventDataWithKey,
- plotly_unhover: eventDataWithKey,
- // If 'plotly_selected' has already been fired, and you click
- // on the plot afterwards, this event fires `undefined`?!?
- // That might be considered a plotly.js bug, but it doesn't make
- // sense for this input change to occur if `d` is falsy because,
- // even in the empty selection case, `d` is truthy (an object),
- // and the 'plotly_deselect' event will reset this input
- plotly_selected: function(d) { if (d) { return eventDataWithKey(d); } },
- plotly_selecting: function(d) { if (d) { return eventDataWithKey(d); } },
- plotly_brushed: function(d) {
- if (d) { return d.range ? d.range : d.lassoPoints; }
- },
- plotly_brushing: function(d) {
- if (d) { return d.range ? d.range : d.lassoPoints; }
- },
- plotly_legendclick: legendEventData,
- plotly_legenddoubleclick: legendEventData,
- plotly_clickannotation: function(d) { return d.fullAnnotation }
- };
-
- var registerShinyValue = function(event) {
- var eventDataPreProcessor = eventDataFunctionMap[event] || function(d) { return d ? d : el.id };
- // some events are unique to the R package
- var plotlyJSevent = (event == "plotly_brushed") ? "plotly_selected" : (event == "plotly_brushing") ? "plotly_selecting" : event;
- // register the event
- graphDiv.on(plotlyJSevent, function(d) {
- Shiny.setInputValue(
- event + "-" + x.source,
- JSON.stringify(eventDataPreProcessor(d)),
- {priority: "event"}
- );
- });
- }
-
- var shinyEvents = x.shinyEvents || [];
- shinyEvents.map(registerShinyValue);
- }
-
- // Given an array of {curveNumber: x, pointNumber: y} objects,
- // return a hash of {
- // set1: {value: [key1, key2, ...], _isSimpleKey: false},
- // set2: {value: [key3, key4, ...], _isSimpleKey: false}
- // }
- function pointsToKeys(points) {
- var keysBySet = {};
- for (var i = 0; i < points.length; i++) {
-
- var trace = graphDiv.data[points[i].curveNumber];
- if (!trace.key || !trace.set) {
- continue;
- }
-
- // set defaults for this keySet
- // note that we don't track the nested property (yet) since we always
- // emit the union -- http://cpsievert.github.io/talks/20161212b/#21
- keysBySet[trace.set] = keysBySet[trace.set] || {
- value: [],
- _isSimpleKey: trace._isSimpleKey
- };
-
- // Use pointNumber by default, but aggregated traces should emit pointNumbers
- var ptNum = points[i].pointNumber;
- var hasPtNum = typeof ptNum === "number";
- var ptNum = hasPtNum ? ptNum : points[i].pointNumbers;
-
- // selecting a point of a "simple" trace means: select the
- // entire key attached to this trace, which is useful for,
- // say clicking on a fitted line to select corresponding observations
- var key = trace._isSimpleKey ? trace.key : Array.isArray(ptNum) ? ptNum.map(function(idx) { return trace.key[idx]; }) : trace.key[ptNum];
- // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript
- var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key;
-
- // TODO: better to only add new values?
- keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat);
- }
-
- return keysBySet;
- }
-
-
- x.highlight.color = x.highlight.color || [];
- // make sure highlight color is an array
- if (!Array.isArray(x.highlight.color)) {
- x.highlight.color = [x.highlight.color];
- }
-
- var traceManager = new TraceManager(graphDiv, x.highlight);
-
- // Gather all *unique* sets.
- var allSets = [];
- for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) {
- var newSet = x.data[curveIdx].set;
- if (newSet) {
- if (allSets.indexOf(newSet) === -1) {
- allSets.push(newSet);
- }
- }
- }
-
- // register event listeners for all sets
- for (var i = 0; i < allSets.length; i++) {
-
- var set = allSets[i];
- var selection = new crosstalk.SelectionHandle(set);
- var filter = new crosstalk.FilterHandle(set);
-
- var filterChange = function(e) {
- removeBrush(el);
- traceManager.updateFilter(set, e.value);
- };
- filter.on("change", filterChange);
-
-
- var selectionChange = function(e) {
-
- // Workaround for 'plotly_selected' now firing previously selected
- // points (in addition to new ones) when holding shift key. In our case,
- // we just want the new keys
- if (x.highlight.on === "plotly_selected" && x.highlight.persistentShift) {
- // https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript
- Array.prototype.diff = function(a) {
- return this.filter(function(i) {return a.indexOf(i) < 0;});
- };
- e.value = e.value.diff(e.oldValue);
- }
-
- // array of "event objects" tracking the selection history
- // this is used to avoid adding redundant selections
- var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || [];
-
- // Construct an event object "defining" the current event.
- var event = {
- receiverID: traceManager.gd.id,
- plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get()
- };
- event[set] = e.value;
- // TODO: is there a smarter way to check object equality?
- if (selectionHistory.length > 0) {
- var ev = JSON.stringify(event);
- for (var i = 0; i < selectionHistory.length; i++) {
- var sel = JSON.stringify(selectionHistory[i]);
- if (sel == ev) {
- return;
- }
- }
- }
-
- // accumulate history for persistent selection
- if (!x.highlight.persistent) {
- selectionHistory = [event];
- } else {
- selectionHistory.push(event);
- }
- crosstalk.var("plotlySelectionHistory").set(selectionHistory);
-
- // do the actual updating of traces, frames, and the selectize widget
- traceManager.updateSelection(set, e.value);
- // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items
- if (x.selectize) {
- if (!x.highlight.persistent || e.value === null) {
- selectize.clear(true);
- }
- selectize.addItems(e.value, true);
- selectize.close();
- }
- }
- selection.on("change", selectionChange);
-
- // Set a crosstalk variable selection value, triggering an update
- var turnOn = function(e) {
- if (e) {
- var selectedKeys = pointsToKeys(e.points);
- // Keys are group names, values are array of selected keys from group.
- for (var set in selectedKeys) {
- if (selectedKeys.hasOwnProperty(set)) {
- selection.set(selectedKeys[set].value, {sender: el});
- }
- }
- }
- };
- if (x.highlight.debounce > 0) {
- turnOn = debounce(turnOn, x.highlight.debounce);
- }
- graphDiv.on(x.highlight.on, turnOn);
-
- graphDiv.on(x.highlight.off, function turnOff(e) {
- // remove any visual clues
- removeBrush(el);
- // remove any selection history
- crosstalk.var("plotlySelectionHistory").set(null);
- // trigger the actual removal of selection traces
- selection.set(null, {sender: el});
- });
-
- // register a callback for selectize so that there is bi-directional
- // communication between the widget and direct manipulation events
- if (x.selectize) {
- var selectizeID = Object.keys(x.selectize)[i];
- var items = x.selectize[selectizeID].items;
- var first = [{value: "", label: "(All)"}];
- var opts = {
- options: first.concat(items),
- searchField: "label",
- valueField: "value",
- labelField: "label",
- maxItems: 50
- };
- var select = $("#" + selectizeID).find("select")[0];
- var selectize = $(select).selectize(opts)[0].selectize;
- // NOTE: this callback is triggered when *directly* altering
- // dropdown items
- selectize.on("change", function() {
- var currentItems = traceManager.groupSelections[set] || [];
- if (!x.highlight.persistent) {
- removeBrush(el);
- for (var i = 0; i < currentItems.length; i++) {
- selectize.removeItem(currentItems[i], true);
- }
- }
- var newItems = selectize.items.filter(function(idx) {
- return currentItems.indexOf(idx) < 0;
- });
- if (newItems.length > 0) {
- traceManager.updateSelection(set, newItems);
- } else {
- // Item has been removed...
- // TODO: this logic won't work for dynamically changing palette
- traceManager.updateSelection(set, null);
- traceManager.updateSelection(set, selectize.items);
- }
- });
- }
- } // end of selectionChange
-
- } // end of renderValue
-}); // end of widget definition
-
-/**
- * @param graphDiv The Plotly graph div
- * @param highlight An object with options for updating selection(s)
- */
-function TraceManager(graphDiv, highlight) {
- // The Plotly graph div
- this.gd = graphDiv;
-
- // Preserve the original data.
- // TODO: try using Lib.extendFlat() as done in
- // https://github.com/plotly/plotly.js/pull/1136
- this.origData = JSON.parse(JSON.stringify(graphDiv.data));
-
- // avoid doing this over and over
- this.origOpacity = [];
- for (var i = 0; i < this.origData.length; i++) {
- this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1);
- }
-
- // key: group name, value: null or array of keys representing the
- // most recently received selection for that group.
- this.groupSelections = {};
-
- // selection parameters (e.g., transient versus persistent selection)
- this.highlight = highlight;
-}
-
-TraceManager.prototype.close = function() {
- // TODO: Unhook all event handlers
-};
-
-TraceManager.prototype.updateFilter = function(group, keys) {
-
- if (typeof(keys) === "undefined" || keys === null) {
-
- this.gd.data = JSON.parse(JSON.stringify(this.origData));
-
- } else {
-
- var traces = [];
- for (var i = 0; i < this.origData.length; i++) {
- var trace = this.origData[i];
- if (!trace.key || trace.set !== group) {
- continue;
- }
- var matchFunc = getMatchFunc(trace);
- var matches = matchFunc(trace.key, keys);
-
- if (matches.length > 0) {
- if (!trace._isSimpleKey) {
- // subsetArrayAttrs doesn't mutate trace (it makes a modified clone)
- trace = subsetArrayAttrs(trace, matches);
- }
- traces.push(trace);
- }
- }
- }
-
- this.gd.data = traces;
- Plotly.redraw(this.gd);
-
- // NOTE: we purposely do _not_ restore selection(s), since on filter,
- // axis likely will update, changing the pixel -> data mapping, leading
- // to a likely mismatch in the brush outline and highlighted marks
-
-};
-
-TraceManager.prototype.updateSelection = function(group, keys) {
-
- if (keys !== null && !Array.isArray(keys)) {
- throw new Error("Invalid keys argument; null or array expected");
- }
-
- // if selection has been cleared, or if this is transient
- // selection, delete the "selection traces"
- var nNewTraces = this.gd.data.length - this.origData.length;
- if (keys === null || !this.highlight.persistent && nNewTraces > 0) {
- var tracesToRemove = [];
- for (var i = 0; i < this.gd.data.length; i++) {
- if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i);
- }
- Plotly.deleteTraces(this.gd, tracesToRemove);
- this.groupSelections[group] = keys;
- } else {
- // add to the groupSelection, rather than overwriting it
- // TODO: can this be removed?
- this.groupSelections[group] = this.groupSelections[group] || [];
- for (var i = 0; i < keys.length; i++) {
- var k = keys[i];
- if (this.groupSelections[group].indexOf(k) < 0) {
- this.groupSelections[group].push(k);
- }
- }
- }
-
- if (keys === null) {
-
- Plotly.restyle(this.gd, {"opacity": this.origOpacity});
-
- } else if (keys.length >= 1) {
-
- // placeholder for new "selection traces"
- var traces = [];
- // this variable is set in R/highlight.R
- var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() ||
- this.highlight.color[0];
-
- for (var i = 0; i < this.origData.length; i++) {
- // TODO: try using Lib.extendFlat() as done in
- // https://github.com/plotly/plotly.js/pull/1136
- var trace = JSON.parse(JSON.stringify(this.gd.data[i]));
- if (!trace.key || trace.set !== group) {
- continue;
- }
- // Get sorted array of matching indices in trace.key
- var matchFunc = getMatchFunc(trace);
- var matches = matchFunc(trace.key, keys);
-
- if (matches.length > 0) {
- // If this is a "simple" key, that means select the entire trace
- if (!trace._isSimpleKey) {
- trace = subsetArrayAttrs(trace, matches);
- }
- // reach into the full trace object so we can properly reflect the
- // selection attributes in every view
- var d = this.gd._fullData[i];
-
- /*
- / Recursively inherit selection attributes from various sources,
- / in order of preference:
- / (1) official plotly.js selected attribute
- / (2) highlight(selected = attrs_selected(...))
- */
- // TODO: it would be neat to have a dropdown to dynamically specify these!
- $.extend(true, trace, this.highlight.selected);
-
- // if it is defined, override color with the "dynamic brush color""
- if (d.marker) {
- trace.marker = trace.marker || {};
- trace.marker.color = selectionColour || trace.marker.color || d.marker.color;
- }
- if (d.line) {
- trace.line = trace.line || {};
- trace.line.color = selectionColour || trace.line.color || d.line.color;
- }
- if (d.textfont) {
- trace.textfont = trace.textfont || {};
- trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color;
- }
- if (d.fillcolor) {
- // TODO: should selectionColour inherit alpha from the existing fillcolor?
- trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor;
- }
- // attach a sensible name/legendgroup
- trace.name = trace.name || keys.join("
");
- trace.legendgroup = trace.legendgroup || keys.join("
");
-
- // keep track of mapping between this new trace and the trace it targets
- // (necessary for updating frames to reflect the selection traces)
- trace._originalIndex = i;
- trace._newIndex = this.gd._fullData.length + traces.length;
- trace._isCrosstalkTrace = true;
- traces.push(trace);
- }
- }
-
- if (traces.length > 0) {
-
- Plotly.addTraces(this.gd, traces).then(function(gd) {
- // incrementally add selection traces to frames
- // (this is heavily inspired by Plotly.Plots.modifyFrames()
- // in src/plots/plots.js)
- var _hash = gd._transitionData._frameHash;
- var _frames = gd._transitionData._frames || [];
-
- for (var i = 0; i < _frames.length; i++) {
-
- // add to _frames[i].traces *if* this frame references selected trace(s)
- var newIndices = [];
- for (var j = 0; j < traces.length; j++) {
- var tr = traces[j];
- if (_frames[i].traces.indexOf(tr._originalIndex) > -1) {
- newIndices.push(tr._newIndex);
- _frames[i].traces.push(tr._newIndex);
- }
- }
-
- // nothing to do...
- if (newIndices.length === 0) {
- continue;
- }
-
- var ctr = 0;
- var nFrameTraces = _frames[i].data.length;
-
- for (var j = 0; j < nFrameTraces; j++) {
- var frameTrace = _frames[i].data[j];
- if (!frameTrace.key || frameTrace.set !== group) {
- continue;
- }
-
- var matchFunc = getMatchFunc(frameTrace);
- var matches = matchFunc(frameTrace.key, keys);
-
- if (matches.length > 0) {
- if (!trace._isSimpleKey) {
- frameTrace = subsetArrayAttrs(frameTrace, matches);
- }
- var d = gd._fullData[newIndices[ctr]];
- if (d.marker) {
- frameTrace.marker = d.marker;
- }
- if (d.line) {
- frameTrace.line = d.line;
- }
- if (d.textfont) {
- frameTrace.textfont = d.textfont;
- }
- ctr = ctr + 1;
- _frames[i].data.push(frameTrace);
- }
- }
-
- // update gd._transitionData._frameHash
- _hash[_frames[i].name] = _frames[i];
- }
-
- });
-
- // dim traces that have a set matching the set of selection sets
- var tracesToDim = [],
- opacities = [],
- sets = Object.keys(this.groupSelections),
- n = this.origData.length;
-
- for (var i = 0; i < n; i++) {
- var opacity = this.origOpacity[i] || 1;
- // have we already dimmed this trace? Or is this even worth doing?
- if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) {
- continue;
- }
- // is this set an element of the set of selection sets?
- var matches = findMatches(sets, [this.gd.data[i].set]);
- if (matches.length) {
- tracesToDim.push(i);
- opacities.push(opacity * this.highlight.opacityDim);
- }
- }
-
- if (tracesToDim.length > 0) {
- Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim);
- // turn off the selected/unselected API
- Plotly.restyle(this.gd, {"selectedpoints": null});
- }
-
- }
-
- }
-};
-
-/*
-Note: in all of these match functions, we assume needleSet (i.e. the selected keys)
-is a 1D (or flat) array. The real difference is the meaning of haystack.
-findMatches() does the usual thing you'd expect for
-linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff
-haystack is a subset of the needleSet. findNestedMatches() returns
-*/
-
-function getMatchFunc(trace) {
- return (trace._isNestedKey) ? findNestedMatches :
- (trace._isSimpleKey) ? findSimpleMatches : findMatches;
-}
-
-// find matches for "flat" keys
-function findMatches(haystack, needleSet) {
- var matches = [];
- haystack.forEach(function(obj, i) {
- if (obj === null || needleSet.indexOf(obj) >= 0) {
- matches.push(i);
- }
- });
- return matches;
-}
-
-// find matches for "simple" keys
-function findSimpleMatches(haystack, needleSet) {
- var match = haystack.every(function(val) {
- return val === null || needleSet.indexOf(val) >= 0;
- });
- // yes, this doesn't make much sense other than conforming
- // to the output type of the other match functions
- return (match) ? [0] : []
-}
-
-// find matches for a "nested" haystack (2D arrays)
-function findNestedMatches(haystack, needleSet) {
- var matches = [];
- for (var i = 0; i < haystack.length; i++) {
- var hay = haystack[i];
- var match = hay.every(function(val) {
- return val === null || needleSet.indexOf(val) >= 0;
- });
- if (match) {
- matches.push(i);
- }
- }
- return matches;
-}
-
-function isPlainObject(obj) {
- return (
- Object.prototype.toString.call(obj) === '[object Object]' &&
- Object.getPrototypeOf(obj) === Object.prototype
- );
-}
-
-function subsetArrayAttrs(obj, indices) {
- var newObj = {};
- Object.keys(obj).forEach(function(k) {
- var val = obj[k];
-
- if (k.charAt(0) === "_") {
- newObj[k] = val;
- } else if (k === "transforms" && Array.isArray(val)) {
- newObj[k] = val.map(function(transform) {
- return subsetArrayAttrs(transform, indices);
- });
- } else if (k === "colorscale" && Array.isArray(val)) {
- newObj[k] = val;
- } else if (isPlainObject(val)) {
- newObj[k] = subsetArrayAttrs(val, indices);
- } else if (Array.isArray(val)) {
- newObj[k] = subsetArray(val, indices);
- } else {
- newObj[k] = val;
- }
- });
- return newObj;
-}
-
-function subsetArray(arr, indices) {
- var result = [];
- for (var i = 0; i < indices.length; i++) {
- result.push(arr[indices[i]]);
- }
- return result;
-}
-
-// Convenience function for removing plotly's brush
-function removeBrush(el) {
- var outlines = el.querySelectorAll(".select-outline");
- for (var i = 0; i < outlines.length; i++) {
- outlines[i].remove();
- }
-}
-
-
-// https://davidwalsh.name/javascript-debounce-function
-
-// Returns a function, that, as long as it continues to be invoked, will not
-// be triggered. The function will be called after it stops being called for
-// N milliseconds. If `immediate` is passed, trigger the function on the
-// leading edge, instead of the trailing.
-function debounce(func, wait, immediate) {
- var timeout;
- return function() {
- var context = this, args = arguments;
- var later = function() {
- timeout = null;
- if (!immediate) func.apply(context, args);
- };
- var callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) func.apply(context, args);
- };
-};
+!function i(r,o,n){function a(e,t){if(!o[e]){if(!r[e]){var l="function"==typeof require&&require;if(!t&&l)return l(e,!0);if(s)return s(e,!0);throw(l=new Error("Cannot find module '"+e+"'")).code="MODULE_NOT_FOUND",l}l=o[e]={exports:{}},r[e][0].call(l.exports,function(t){return a(r[e][1][t]||t)},l,l.exports,i,r,o,n)}return o[e].exports}for(var s="function"==typeof require&&require,t=0;t"),d.legendgroup=d.legendgroup||p.join("
"),d._originalIndex=l,d._newIndex=this.gd._fullData.length+g.length,d._isCrosstalkTrace=!0,g.push(d))}if(0 0) {\n var ev = JSON.stringify(event);\n for (var i = 0; i < selectionHistory.length; i++) {\n var sel = JSON.stringify(selectionHistory[i]);\n if (sel == ev) {\n return;\n }\n }\n }\n \n // accumulate history for persistent selection\n if (!x.highlight.persistent) {\n selectionHistory = [event];\n } else {\n selectionHistory.push(event);\n }\n crosstalk.var(\"plotlySelectionHistory\").set(selectionHistory);\n \n // do the actual updating of traces, frames, and the selectize widget\n traceManager.updateSelection(set, e.value);\n // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items\n if (x.selectize) {\n if (!x.highlight.persistent || e.value === null) {\n selectize.clear(true);\n }\n selectize.addItems(e.value, true);\n selectize.close();\n }\n }\n selection.on(\"change\", selectionChange);\n \n // Set a crosstalk variable selection value, triggering an update\n var turnOn = function(e) {\n if (e) {\n var selectedKeys = pointsToKeys(e.points);\n // Keys are group names, values are array of selected keys from group.\n for (var set in selectedKeys) {\n if (selectedKeys.hasOwnProperty(set)) {\n selection.set(selectedKeys[set].value, {sender: el});\n }\n }\n }\n };\n if (x.highlight.debounce > 0) {\n turnOn = debounce(turnOn, x.highlight.debounce);\n }\n graphDiv.on(x.highlight.on, turnOn);\n \n graphDiv.on(x.highlight.off, function turnOff(e) {\n // remove any visual clues\n removeBrush(el);\n // remove any selection history\n crosstalk.var(\"plotlySelectionHistory\").set(null);\n // trigger the actual removal of selection traces\n selection.set(null, {sender: el});\n });\n \n // register a callback for selectize so that there is bi-directional\n // communication between the widget and direct manipulation events\n if (x.selectize) {\n var selectizeID = Object.keys(x.selectize)[i];\n var items = x.selectize[selectizeID].items;\n var first = [{value: \"\", label: \"(All)\"}];\n var opts = {\n options: first.concat(items),\n searchField: \"label\",\n valueField: \"value\",\n labelField: \"label\",\n maxItems: 50\n };\n var select = $(\"#\" + selectizeID).find(\"select\")[0];\n var selectize = $(select).selectize(opts)[0].selectize;\n // NOTE: this callback is triggered when *directly* altering \n // dropdown items\n selectize.on(\"change\", function() {\n var currentItems = traceManager.groupSelections[set] || [];\n if (!x.highlight.persistent) {\n removeBrush(el);\n for (var i = 0; i < currentItems.length; i++) {\n selectize.removeItem(currentItems[i], true);\n }\n }\n var newItems = selectize.items.filter(function(idx) { \n return currentItems.indexOf(idx) < 0;\n });\n if (newItems.length > 0) {\n traceManager.updateSelection(set, newItems);\n } else {\n // Item has been removed...\n // TODO: this logic won't work for dynamically changing palette \n traceManager.updateSelection(set, null);\n traceManager.updateSelection(set, selectize.items);\n }\n });\n }\n } // end of selectionChange\n } // end of renderValue\n}\n\n/**\n * @param graphDiv The Plotly graph div\n * @param highlight An object with options for updating selection(s)\n */\nfunction TraceManager(graphDiv, highlight) {\n // The Plotly graph div\n this.gd = graphDiv;\n\n // Preserve the original data.\n // TODO: try using Lib.extendFlat() as done in \n // https://github.com/plotly/plotly.js/pull/1136 \n this.origData = JSON.parse(JSON.stringify(graphDiv.data));\n \n // avoid doing this over and over\n this.origOpacity = [];\n for (var i = 0; i < this.origData.length; i++) {\n this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1);\n }\n\n // key: group name, value: null or array of keys representing the\n // most recently received selection for that group.\n this.groupSelections = {};\n \n // selection parameters (e.g., transient versus persistent selection)\n this.highlight = highlight;\n}\n\nTraceManager.prototype.close = function() {\n // TODO: Unhook all event handlers\n};\n\nTraceManager.prototype.updateFilter = function(group, keys) {\n\n if (typeof(keys) === \"undefined\" || keys === null) {\n \n this.gd.data = JSON.parse(JSON.stringify(this.origData));\n \n } else {\n \n var traces = [];\n for (var i = 0; i < this.origData.length; i++) {\n var trace = this.origData[i];\n if (!trace.key || trace.set !== group) {\n continue;\n }\n var matchFunc = getMatchFunc(trace);\n var matches = matchFunc(trace.key, keys);\n \n if (matches.length > 0) {\n if (!trace._isSimpleKey) {\n // subsetArrayAttrs doesn't mutate trace (it makes a modified clone)\n trace = subsetArrayAttrs(trace, matches);\n }\n traces.push(trace);\n }\n }\n }\n \n this.gd.data = traces;\n Plotly.redraw(this.gd);\n \n // NOTE: we purposely do _not_ restore selection(s), since on filter,\n // axis likely will update, changing the pixel -> data mapping, leading \n // to a likely mismatch in the brush outline and highlighted marks\n \n};\n\nTraceManager.prototype.updateSelection = function(group, keys) {\n \n if (keys !== null && !Array.isArray(keys)) {\n throw new Error(\"Invalid keys argument; null or array expected\");\n }\n \n // if selection has been cleared, or if this is transient\n // selection, delete the \"selection traces\"\n var nNewTraces = this.gd.data.length - this.origData.length;\n if (keys === null || !this.highlight.persistent && nNewTraces > 0) {\n var tracesToRemove = [];\n for (var i = 0; i < this.gd.data.length; i++) {\n if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i);\n }\n Plotly.deleteTraces(this.gd, tracesToRemove);\n this.groupSelections[group] = keys;\n } else {\n // add to the groupSelection, rather than overwriting it\n // TODO: can this be removed?\n this.groupSelections[group] = this.groupSelections[group] || [];\n for (var i = 0; i < keys.length; i++) {\n var k = keys[i];\n if (this.groupSelections[group].indexOf(k) < 0) {\n this.groupSelections[group].push(k);\n }\n }\n }\n \n if (keys === null) {\n \n Plotly.restyle(this.gd, {\"opacity\": this.origOpacity});\n \n } else if (keys.length >= 1) {\n \n // placeholder for new \"selection traces\"\n var traces = [];\n // this variable is set in R/highlight.R\n var selectionColour = crosstalk.group(group).var(\"plotlySelectionColour\").get() || \n this.highlight.color[0];\n\n for (var i = 0; i < this.origData.length; i++) {\n // TODO: try using Lib.extendFlat() as done in \n // https://github.com/plotly/plotly.js/pull/1136 \n var trace = JSON.parse(JSON.stringify(this.gd.data[i]));\n if (!trace.key || trace.set !== group) {\n continue;\n }\n // Get sorted array of matching indices in trace.key\n var matchFunc = getMatchFunc(trace);\n var matches = matchFunc(trace.key, keys);\n \n if (matches.length > 0) {\n // If this is a \"simple\" key, that means select the entire trace\n if (!trace._isSimpleKey) {\n trace = subsetArrayAttrs(trace, matches);\n }\n // reach into the full trace object so we can properly reflect the \n // selection attributes in every view\n var d = this.gd._fullData[i];\n \n /* \n / Recursively inherit selection attributes from various sources, \n / in order of preference:\n / (1) official plotly.js selected attribute\n / (2) highlight(selected = attrs_selected(...))\n */\n // TODO: it would be neat to have a dropdown to dynamically specify these!\n $.extend(true, trace, this.highlight.selected);\n \n // if it is defined, override color with the \"dynamic brush color\"\"\n if (d.marker) {\n trace.marker = trace.marker || {};\n trace.marker.color = selectionColour || trace.marker.color || d.marker.color;\n }\n if (d.line) {\n trace.line = trace.line || {};\n trace.line.color = selectionColour || trace.line.color || d.line.color;\n }\n if (d.textfont) {\n trace.textfont = trace.textfont || {};\n trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color;\n }\n if (d.fillcolor) {\n // TODO: should selectionColour inherit alpha from the existing fillcolor?\n trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor;\n }\n // attach a sensible name/legendgroup\n trace.name = trace.name || keys.join(\"
\");\n trace.legendgroup = trace.legendgroup || keys.join(\"
\");\n \n // keep track of mapping between this new trace and the trace it targets\n // (necessary for updating frames to reflect the selection traces)\n trace._originalIndex = i;\n trace._newIndex = this.gd._fullData.length + traces.length;\n trace._isCrosstalkTrace = true;\n traces.push(trace);\n }\n }\n \n if (traces.length > 0) {\n \n Plotly.addTraces(this.gd, traces).then(function(gd) {\n // incrementally add selection traces to frames\n // (this is heavily inspired by Plotly.Plots.modifyFrames() \n // in src/plots/plots.js)\n var _hash = gd._transitionData._frameHash;\n var _frames = gd._transitionData._frames || [];\n \n for (var i = 0; i < _frames.length; i++) {\n \n // add to _frames[i].traces *if* this frame references selected trace(s)\n var newIndices = [];\n for (var j = 0; j < traces.length; j++) {\n var tr = traces[j];\n if (_frames[i].traces.indexOf(tr._originalIndex) > -1) {\n newIndices.push(tr._newIndex);\n _frames[i].traces.push(tr._newIndex);\n }\n }\n \n // nothing to do...\n if (newIndices.length === 0) {\n continue;\n }\n \n var ctr = 0;\n var nFrameTraces = _frames[i].data.length;\n \n for (var j = 0; j < nFrameTraces; j++) {\n var frameTrace = _frames[i].data[j];\n if (!frameTrace.key || frameTrace.set !== group) {\n continue;\n }\n \n var matchFunc = getMatchFunc(frameTrace);\n var matches = matchFunc(frameTrace.key, keys);\n \n if (matches.length > 0) {\n if (!trace._isSimpleKey) {\n frameTrace = subsetArrayAttrs(frameTrace, matches);\n }\n var d = gd._fullData[newIndices[ctr]];\n if (d.marker) {\n frameTrace.marker = d.marker;\n }\n if (d.line) {\n frameTrace.line = d.line;\n }\n if (d.textfont) {\n frameTrace.textfont = d.textfont;\n }\n ctr = ctr + 1;\n _frames[i].data.push(frameTrace);\n }\n }\n \n // update gd._transitionData._frameHash\n _hash[_frames[i].name] = _frames[i];\n }\n \n });\n \n // dim traces that have a set matching the set of selection sets\n var tracesToDim = [],\n opacities = [],\n sets = Object.keys(this.groupSelections),\n n = this.origData.length;\n \n for (var i = 0; i < n; i++) {\n var opacity = this.origOpacity[i] || 1;\n // have we already dimmed this trace? Or is this even worth doing?\n if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) {\n continue;\n }\n // is this set an element of the set of selection sets?\n var matches = findMatches(sets, [this.gd.data[i].set]);\n if (matches.length) {\n tracesToDim.push(i);\n opacities.push(opacity * this.highlight.opacityDim);\n }\n }\n \n if (tracesToDim.length > 0) {\n Plotly.restyle(this.gd, {\"opacity\": opacities}, tracesToDim);\n // turn off the selected/unselected API\n Plotly.restyle(this.gd, {\"selectedpoints\": null});\n }\n \n }\n \n }\n};\n\n/* \nNote: in all of these match functions, we assume needleSet (i.e. the selected keys)\nis a 1D (or flat) array. The real difference is the meaning of haystack.\nfindMatches() does the usual thing you'd expect for \nlinked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff \nhaystack is a subset of the needleSet. findNestedMatches() returns \n*/\n\nfunction getMatchFunc(trace) {\n return (trace._isNestedKey) ? findNestedMatches : \n (trace._isSimpleKey) ? findSimpleMatches : findMatches;\n}\n\n// find matches for \"flat\" keys\nfunction findMatches(haystack, needleSet) {\n var matches = [];\n haystack.forEach(function(obj, i) {\n if (obj === null || needleSet.indexOf(obj) >= 0) {\n matches.push(i);\n }\n });\n return matches;\n}\n\n// find matches for \"simple\" keys\nfunction findSimpleMatches(haystack, needleSet) {\n var match = haystack.every(function(val) {\n return val === null || needleSet.indexOf(val) >= 0;\n });\n // yes, this doesn't make much sense other than conforming \n // to the output type of the other match functions\n return (match) ? [0] : []\n}\n\n// find matches for a \"nested\" haystack (2D arrays)\nfunction findNestedMatches(haystack, needleSet) {\n var matches = [];\n for (var i = 0; i < haystack.length; i++) {\n var hay = haystack[i];\n var match = hay.every(function(val) { \n return val === null || needleSet.indexOf(val) >= 0; \n });\n if (match) {\n matches.push(i);\n }\n }\n return matches;\n}\n\nfunction isPlainObject(obj) {\n return (\n Object.prototype.toString.call(obj) === '[object Object]' &&\n Object.getPrototypeOf(obj) === Object.prototype\n );\n}\n\nfunction subsetArrayAttrs(obj, indices) {\n var newObj = {};\n Object.keys(obj).forEach(function(k) {\n var val = obj[k];\n\n if (k.charAt(0) === \"_\") {\n newObj[k] = val;\n } else if (k === \"transforms\" && Array.isArray(val)) {\n newObj[k] = val.map(function(transform) {\n return subsetArrayAttrs(transform, indices);\n });\n } else if (k === \"colorscale\" && Array.isArray(val)) {\n newObj[k] = val;\n } else if (isPlainObject(val)) {\n newObj[k] = subsetArrayAttrs(val, indices);\n } else if (Array.isArray(val)) {\n newObj[k] = subsetArray(val, indices);\n } else {\n newObj[k] = val;\n }\n });\n return newObj;\n}\n\nfunction subsetArray(arr, indices) {\n var result = [];\n for (var i = 0; i < indices.length; i++) {\n result.push(arr[indices[i]]);\n }\n return result;\n}\n\n// Convenience function for removing plotly's brush \nfunction removeBrush(el) {\n var outlines = el.querySelectorAll(\".select-outline\");\n for (var i = 0; i < outlines.length; i++) {\n outlines[i].remove();\n }\n}\n\n\n// https://davidwalsh.name/javascript-debounce-function\n\n// Returns a function, that, as long as it continues to be invoked, will not\n// be triggered. The function will be called after it stops being called for\n// N milliseconds. If `immediate` is passed, trigger the function on the\n// leading edge, instead of the trailing.\nfunction debounce(func, wait, immediate) {\n\tvar timeout;\n\treturn function() {\n\t\tvar context = this, args = arguments;\n\t\tvar later = function() {\n\t\t\ttimeout = null;\n\t\t\tif (!immediate) func.apply(context, args);\n\t\t};\n\t\tvar callNow = immediate && !timeout;\n\t\tclearTimeout(timeout);\n\t\ttimeout = setTimeout(later, wait);\n\t\tif (callNow) func.apply(context, args);\n\t};\n};\n\nmodule.exports = widgetDefinition"]}
\ No newline at end of file
diff --git a/inst/htmlwidgets/plotly.yaml b/inst/htmlwidgets/plotly.yaml
index e69de29bb2..2e5f75e3bd 100644
--- a/inst/htmlwidgets/plotly.yaml
+++ b/inst/htmlwidgets/plotly.yaml
@@ -0,0 +1,7 @@
+dependencies:
+
+ - name: Maps
+ version: 1.0.0
+ src: htmlwidgets
+ script:
+ - plotly.js.map
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..a1fd92b0fe
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "plotly",
+ "private": true,
+ "engines": {
+ "node": ">=12.9.0"
+ },
+ "scripts": {
+ "preinstall": "([ \"$CI\" != true ] && npx npm-force-resolutions) || true"
+ },
+ "devDependencies": {
+ "gulp": "^4.0.2",
+ "husky": "^0.13.3",
+ "npm-force-resolutions": "0.0.10",
+ "rhtmlBuildUtils": "github:Displayr/rhtmlBuildUtils#7.2.3"
+ },
+ "resolutions": {
+ "glob-parent": "5.1.2",
+ "ini": "1.3.8",
+ "minimist": "1.2.5",
+ "y18n": "4.0.3",
+ "yargs-parser": "18.1.3"
+ }
+}
\ No newline at end of file
diff --git a/theSrc/scripts/plotly.js b/theSrc/scripts/plotly.js
new file mode 100644
index 0000000000..7eebf5d32e
--- /dev/null
+++ b/theSrc/scripts/plotly.js
@@ -0,0 +1,3 @@
+import widgetDefinition from './widgetdefinition'
+
+HTMLWidgets.widget(widgetDefinition)
diff --git a/theSrc/scripts/vis-992.test.js b/theSrc/scripts/vis-992.test.js
new file mode 100644
index 0000000000..add799c728
--- /dev/null
+++ b/theSrc/scripts/vis-992.test.js
@@ -0,0 +1,26 @@
+// Unit test for VIS-992
+// Check that resize() works even when element is not attached to the DOM.
+// We create a mock of the Plotly object that has the relayout function that throws an
+// error if an ID is supplied for the element and it is not attached to the DOM.
+
+const widgetDefinition = require('./widgetdefinition.js')
+
+global.Plotly = {
+ relayout: el => {
+ if (typeof el === 'string') {
+ elementInDocument = document.getElementById(el);
+ if (elementInDocument === null) {
+ throw new Error('No DOM element with id \'' + el + '\' exists on the page.');
+ }
+ }
+ }
+}
+
+test('test that', () => {
+ const div = document.createElement('div')
+ div.id = 'ID'
+ expect(document.getElementById(div.id)).toBe(null)
+
+ const instance = {autosize: true, width: 0, height: 0}
+ widgetDefinition.resize(div, 0, 0, instance) // this used to error before VIS-992
+})
diff --git a/theSrc/scripts/widgetdefinition.js b/theSrc/scripts/widgetdefinition.js
new file mode 100644
index 0000000000..8f25d2fd78
--- /dev/null
+++ b/theSrc/scripts/widgetdefinition.js
@@ -0,0 +1,935 @@
+const widgetDefinition = {
+ name: "plotly",
+ type: "output",
+
+ initialize: function(el, width, height) {
+ return {};
+ },
+
+ resize: function(el, width, height, instance) {
+ if (instance.autosize) {
+ var width = instance.width || width;
+ var height = instance.height || height;
+ Plotly.relayout(el, {width: width, height: height});
+ }
+ },
+
+ renderValue: function(el, x, instance) {
+
+ // Plotly.relayout() mutates the plot input object, so make sure to
+ // keep a reference to the user-supplied width/height *before*
+ // we call Plotly.plot();
+ var lay = x.layout || {};
+ instance.width = lay.width;
+ instance.height = lay.height;
+ instance.autosize = lay.autosize || true;
+
+ /*
+ / 'inform the world' about highlighting options this is so other
+ / crosstalk libraries have a chance to respond to special settings
+ / such as persistent selection.
+ / AFAIK, leaflet is the only library with such intergration
+ / https://github.com/rstudio/leaflet/pull/346/files#diff-ad0c2d51ce5fdf8c90c7395b102f4265R154
+ */
+ var ctConfig = crosstalk.var('plotlyCrosstalkOpts').set(x.highlight);
+
+ if (typeof(window) !== "undefined") {
+ // make sure plots don't get created outside the network (for on-prem)
+ window.PLOTLYENV = window.PLOTLYENV || {};
+ window.PLOTLYENV.BASE_URL = x.base_url;
+
+ // Enable persistent selection when shift key is down
+ // https://stackoverflow.com/questions/1828613/check-if-a-key-is-down
+ var persistOnShift = function(e) {
+ if (!e) window.event;
+ if (e.shiftKey) {
+ x.highlight.persistent = true;
+ x.highlight.persistentShift = true;
+ } else {
+ x.highlight.persistent = false;
+ x.highlight.persistentShift = false;
+ }
+ };
+
+ // Only relevant if we haven't forced persistent mode at command line
+ if (!x.highlight.persistent) {
+ window.onmousemove = persistOnShift;
+ }
+ }
+
+ var graphDiv = document.getElementById(el.id);
+
+ // TODO: move the control panel injection strategy inside here...
+ HTMLWidgets.addPostRenderHandler(function() {
+
+ // lower the z-index of the modebar to prevent it from highjacking hover
+ // (TODO: do this via CSS?)
+ // https://github.com/ropensci/plotly/issues/956
+ // https://www.w3schools.com/jsref/prop_style_zindex.asp
+ var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar");
+ for (var i = 0; i < modebars.length; i++) {
+ modebars[i].style.zIndex = 1;
+ }
+ });
+
+ // inject a "control panel" holding selectize/dynamic color widget(s)
+ if ((x.selectize || x.highlight.dynamic) && !instance.plotly) {
+ var flex = document.createElement("div");
+ flex.class = "plotly-crosstalk-control-panel";
+ flex.style = "display: flex; flex-wrap: wrap";
+
+ // inject the colourpicker HTML container into the flexbox
+ if (x.highlight.dynamic) {
+ var pickerDiv = document.createElement("div");
+
+ var pickerInput = document.createElement("input");
+ pickerInput.id = el.id + "-colourpicker";
+ pickerInput.placeholder = "asdasd";
+
+ var pickerLabel = document.createElement("label");
+ pickerLabel.for = pickerInput.id;
+ pickerLabel.innerHTML = "Brush color ";
+
+ pickerDiv.appendChild(pickerLabel);
+ pickerDiv.appendChild(pickerInput);
+ flex.appendChild(pickerDiv);
+ }
+
+ // inject selectize HTML containers (one for every crosstalk group)
+ if (x.selectize) {
+ var ids = Object.keys(x.selectize);
+
+ for (var i = 0; i < ids.length; i++) {
+ var container = document.createElement("div");
+ container.id = ids[i];
+ container.style = "width: 80%; height: 10%";
+ container.class = "form-group crosstalk-input-plotly-highlight";
+
+ var label = document.createElement("label");
+ label.for = ids[i];
+ label.innerHTML = x.selectize[ids[i]].group;
+ label.class = "control-label";
+
+ var selectDiv = document.createElement("div");
+ var select = document.createElement("select");
+ select.multiple = true;
+
+ selectDiv.appendChild(select);
+ container.appendChild(label);
+ container.appendChild(selectDiv);
+ flex.appendChild(container);
+ }
+ }
+
+ // finally, insert the flexbox inside the htmlwidget container,
+ // but before the plotly graph div
+ graphDiv.parentElement.insertBefore(flex, graphDiv);
+
+ if (x.highlight.dynamic) {
+ var picker = $("#" + pickerInput.id);
+ var colors = x.highlight.color || [];
+ // TODO: let users specify options?
+ var opts = {
+ value: colors[0],
+ showColour: "both",
+ palette: "limited",
+ allowedCols: colors.join(" "),
+ width: "20%",
+ height: "10%"
+ };
+ picker.colourpicker({changeDelay: 0});
+ picker.colourpicker("settings", opts);
+ picker.colourpicker("value", opts.value);
+ // inform crosstalk about a change in the current selection colour
+ var grps = x.highlight.ctGroups || [];
+ for (var i = 0; i < grps.length; i++) {
+ crosstalk.group(grps[i]).var('plotlySelectionColour')
+ .set(picker.colourpicker('value'));
+ }
+ picker.on("change", function() {
+ for (var i = 0; i < grps.length; i++) {
+ crosstalk.group(grps[i]).var('plotlySelectionColour')
+ .set(picker.colourpicker('value'));
+ }
+ });
+ }
+ }
+
+ // if no plot exists yet, create one with a particular configuration
+ if (!instance.plotly) {
+
+ var plot = Plotly.plot(graphDiv, x);
+ instance.plotly = true;
+
+ } else {
+
+ // this is essentially equivalent to Plotly.newPlot(), but avoids creating
+ // a new webgl context
+ // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532
+
+ // TODO: restore crosstalk selections?
+ Plotly.purge(graphDiv);
+ // TODO: why is this necessary to get crosstalk working?
+ graphDiv.data = undefined;
+ graphDiv.layout = undefined;
+ var plot = Plotly.plot(graphDiv, x);
+ }
+
+ // Trigger plotly.js calls defined via `plotlyProxy()`
+ plot.then(function() {
+ if (HTMLWidgets.shinyMode) {
+ Shiny.addCustomMessageHandler("plotly-calls", function(msg) {
+ var gd = document.getElementById(msg.id);
+ if (!gd) {
+ throw new Error("Couldn't find plotly graph with id: " + msg.id);
+ }
+ // This isn't an official plotly.js method, but it's the only current way to
+ // change just the configuration of a plot
+ // https://community.plot.ly/t/update-config-function/9057
+ if (msg.method == "reconfig") {
+ Plotly.react(gd, gd.data, gd.layout, msg.args);
+ return;
+ }
+ if (!Plotly[msg.method]) {
+ throw new Error("Unknown method " + msg.method);
+ }
+ var args = [gd].concat(msg.args);
+ Plotly[msg.method].apply(null, args);
+ });
+ }
+
+ // plotly's mapbox API doesn't currently support setting bounding boxes
+ // https://www.mapbox.com/mapbox-gl-js/example/fitbounds/
+ // so we do this manually...
+ // TODO: make sure this triggers on a redraw and relayout as well as on initial draw
+ var mapboxIDs = graphDiv._fullLayout._subplots.mapbox || [];
+ for (var i = 0; i < mapboxIDs.length; i++) {
+ var id = mapboxIDs[i];
+ var mapOpts = x.layout[id] || {};
+ var args = mapOpts._fitBounds || {};
+ if (!args) {
+ continue;
+ }
+ var mapObj = graphDiv._fullLayout[id]._subplot.map;
+ mapObj.fitBounds(args.bounds, args.options);
+ }
+
+ });
+
+ // Attach attributes (e.g., "key", "z") to plotly event data
+ function eventDataWithKey(eventData) {
+ if (eventData === undefined || !eventData.hasOwnProperty("points")) {
+ return null;
+ }
+ return eventData.points.map(function(pt) {
+ var obj = {
+ curveNumber: pt.curveNumber,
+ pointNumber: pt.pointNumber,
+ x: pt.x,
+ y: pt.y
+ };
+
+ // If 'z' is reported with the event data, then use it!
+ if (pt.hasOwnProperty("z")) {
+ obj.z = pt.z;
+ }
+
+ if (pt.hasOwnProperty("customdata")) {
+ obj.customdata = pt.customdata;
+ }
+
+ /*
+ TL;DR: (I think) we have to select the graph div (again) to attach keys...
+
+ Why? Remember that crosstalk will dynamically add/delete traces
+ (see traceManager.prototype.updateSelection() below)
+ For this reason, we can't simply grab keys from x.data (like we did previously)
+ Moreover, we can't use _fullData, since that doesn't include
+ unofficial attributes. It's true that click/hover events fire with
+ pt.data, but drag events don't...
+ */
+ var gd = document.getElementById(el.id);
+ var trace = gd.data[pt.curveNumber];
+
+ if (!trace._isSimpleKey) {
+ var attrsToAttach = ["key"];
+ } else {
+ // simple keys fire the whole key
+ obj.key = trace.key;
+ var attrsToAttach = [];
+ }
+
+ for (var i = 0; i < attrsToAttach.length; i++) {
+ var attr = trace[attrsToAttach[i]];
+ if (Array.isArray(attr)) {
+ if (typeof pt.pointNumber === "number") {
+ obj[attrsToAttach[i]] = attr[pt.pointNumber];
+ } else if (Array.isArray(pt.pointNumber)) {
+ obj[attrsToAttach[i]] = attr[pt.pointNumber[0]][pt.pointNumber[1]];
+ } else if (Array.isArray(pt.pointNumbers)) {
+ obj[attrsToAttach[i]] = pt.pointNumbers.map(function(idx) { return attr[idx]; });
+ }
+ }
+ }
+ return obj;
+ });
+ }
+
+
+ var legendEventData = function(d) {
+ // if legendgroup is not relevant just return the trace
+ var trace = d.data[d.curveNumber];
+ if (!trace.legendgroup) return trace;
+
+ // if legendgroup was specified, return all traces that match the group
+ var legendgrps = d.data.map(function(trace){ return trace.legendgroup; });
+ var traces = [];
+ for (i = 0; i < legendgrps.length; i++) {
+ if (legendgrps[i] == trace.legendgroup) {
+ traces.push(d.data[i]);
+ }
+ }
+
+ return traces;
+ };
+
+
+ // send user input event data to shiny
+ if (HTMLWidgets.shinyMode && Shiny.setInputValue) {
+
+ // Some events clear other input values
+ // TODO: always register these?
+ var eventClearMap = {
+ plotly_deselect: ["plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing", "plotly_click"],
+ plotly_unhover: ["plotly_hover"],
+ plotly_doubleclick: ["plotly_click"]
+ };
+
+ Object.keys(eventClearMap).map(function(evt) {
+ graphDiv.on(evt, function() {
+ var inputsToClear = eventClearMap[evt];
+ inputsToClear.map(function(input) {
+ Shiny.setInputValue(input + "-" + x.source, null, {priority: "event"});
+ });
+ });
+ });
+
+ var eventDataFunctionMap = {
+ plotly_click: eventDataWithKey,
+ plotly_sunburstclick: eventDataWithKey,
+ plotly_hover: eventDataWithKey,
+ plotly_unhover: eventDataWithKey,
+ // If 'plotly_selected' has already been fired, and you click
+ // on the plot afterwards, this event fires `undefined`?!?
+ // That might be considered a plotly.js bug, but it doesn't make
+ // sense for this input change to occur if `d` is falsy because,
+ // even in the empty selection case, `d` is truthy (an object),
+ // and the 'plotly_deselect' event will reset this input
+ plotly_selected: function(d) { if (d) { return eventDataWithKey(d); } },
+ plotly_selecting: function(d) { if (d) { return eventDataWithKey(d); } },
+ plotly_brushed: function(d) {
+ if (d) { return d.range ? d.range : d.lassoPoints; }
+ },
+ plotly_brushing: function(d) {
+ if (d) { return d.range ? d.range : d.lassoPoints; }
+ },
+ plotly_legendclick: legendEventData,
+ plotly_legenddoubleclick: legendEventData,
+ plotly_clickannotation: function(d) { return d.fullAnnotation }
+ };
+
+ var registerShinyValue = function(event) {
+ var eventDataPreProcessor = eventDataFunctionMap[event] || function(d) { return d ? d : el.id };
+ // some events are unique to the R package
+ var plotlyJSevent = (event == "plotly_brushed") ? "plotly_selected" : (event == "plotly_brushing") ? "plotly_selecting" : event;
+ // register the event
+ graphDiv.on(plotlyJSevent, function(d) {
+ Shiny.setInputValue(
+ event + "-" + x.source,
+ JSON.stringify(eventDataPreProcessor(d)),
+ {priority: "event"}
+ );
+ });
+ }
+
+ var shinyEvents = x.shinyEvents || [];
+ shinyEvents.map(registerShinyValue);
+ }
+
+ // Given an array of {curveNumber: x, pointNumber: y} objects,
+ // return a hash of {
+ // set1: {value: [key1, key2, ...], _isSimpleKey: false},
+ // set2: {value: [key3, key4, ...], _isSimpleKey: false}
+ // }
+ function pointsToKeys(points) {
+ var keysBySet = {};
+ for (var i = 0; i < points.length; i++) {
+
+ var trace = graphDiv.data[points[i].curveNumber];
+ if (!trace.key || !trace.set) {
+ continue;
+ }
+
+ // set defaults for this keySet
+ // note that we don't track the nested property (yet) since we always
+ // emit the union -- http://cpsievert.github.io/talks/20161212b/#21
+ keysBySet[trace.set] = keysBySet[trace.set] || {
+ value: [],
+ _isSimpleKey: trace._isSimpleKey
+ };
+
+ // Use pointNumber by default, but aggregated traces should emit pointNumbers
+ var ptNum = points[i].pointNumber;
+ var hasPtNum = typeof ptNum === "number";
+ var ptNum = hasPtNum ? ptNum : points[i].pointNumbers;
+
+ // selecting a point of a "simple" trace means: select the
+ // entire key attached to this trace, which is useful for,
+ // say clicking on a fitted line to select corresponding observations
+ var key = trace._isSimpleKey ? trace.key : Array.isArray(ptNum) ? ptNum.map(function(idx) { return trace.key[idx]; }) : trace.key[ptNum];
+ // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript
+ var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key;
+
+ // TODO: better to only add new values?
+ keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat);
+ }
+
+ return keysBySet;
+ }
+
+
+ x.highlight.color = x.highlight.color || [];
+ // make sure highlight color is an array
+ if (!Array.isArray(x.highlight.color)) {
+ x.highlight.color = [x.highlight.color];
+ }
+
+ var traceManager = new TraceManager(graphDiv, x.highlight);
+
+ // Gather all *unique* sets.
+ var allSets = [];
+ for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) {
+ var newSet = x.data[curveIdx].set;
+ if (newSet) {
+ if (allSets.indexOf(newSet) === -1) {
+ allSets.push(newSet);
+ }
+ }
+ }
+
+ // register event listeners for all sets
+ for (var i = 0; i < allSets.length; i++) {
+
+ var set = allSets[i];
+ var selection = new crosstalk.SelectionHandle(set);
+ var filter = new crosstalk.FilterHandle(set);
+
+ var filterChange = function(e) {
+ removeBrush(el);
+ traceManager.updateFilter(set, e.value);
+ };
+ filter.on("change", filterChange);
+
+
+ var selectionChange = function(e) {
+
+ // Workaround for 'plotly_selected' now firing previously selected
+ // points (in addition to new ones) when holding shift key. In our case,
+ // we just want the new keys
+ if (x.highlight.on === "plotly_selected" && x.highlight.persistentShift) {
+ // https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript
+ Array.prototype.diff = function(a) {
+ return this.filter(function(i) {return a.indexOf(i) < 0;});
+ };
+ e.value = e.value.diff(e.oldValue);
+ }
+
+ // array of "event objects" tracking the selection history
+ // this is used to avoid adding redundant selections
+ var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || [];
+
+ // Construct an event object "defining" the current event.
+ var event = {
+ receiverID: traceManager.gd.id,
+ plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get()
+ };
+ event[set] = e.value;
+ // TODO: is there a smarter way to check object equality?
+ if (selectionHistory.length > 0) {
+ var ev = JSON.stringify(event);
+ for (var i = 0; i < selectionHistory.length; i++) {
+ var sel = JSON.stringify(selectionHistory[i]);
+ if (sel == ev) {
+ return;
+ }
+ }
+ }
+
+ // accumulate history for persistent selection
+ if (!x.highlight.persistent) {
+ selectionHistory = [event];
+ } else {
+ selectionHistory.push(event);
+ }
+ crosstalk.var("plotlySelectionHistory").set(selectionHistory);
+
+ // do the actual updating of traces, frames, and the selectize widget
+ traceManager.updateSelection(set, e.value);
+ // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items
+ if (x.selectize) {
+ if (!x.highlight.persistent || e.value === null) {
+ selectize.clear(true);
+ }
+ selectize.addItems(e.value, true);
+ selectize.close();
+ }
+ }
+ selection.on("change", selectionChange);
+
+ // Set a crosstalk variable selection value, triggering an update
+ var turnOn = function(e) {
+ if (e) {
+ var selectedKeys = pointsToKeys(e.points);
+ // Keys are group names, values are array of selected keys from group.
+ for (var set in selectedKeys) {
+ if (selectedKeys.hasOwnProperty(set)) {
+ selection.set(selectedKeys[set].value, {sender: el});
+ }
+ }
+ }
+ };
+ if (x.highlight.debounce > 0) {
+ turnOn = debounce(turnOn, x.highlight.debounce);
+ }
+ graphDiv.on(x.highlight.on, turnOn);
+
+ graphDiv.on(x.highlight.off, function turnOff(e) {
+ // remove any visual clues
+ removeBrush(el);
+ // remove any selection history
+ crosstalk.var("plotlySelectionHistory").set(null);
+ // trigger the actual removal of selection traces
+ selection.set(null, {sender: el});
+ });
+
+ // register a callback for selectize so that there is bi-directional
+ // communication between the widget and direct manipulation events
+ if (x.selectize) {
+ var selectizeID = Object.keys(x.selectize)[i];
+ var items = x.selectize[selectizeID].items;
+ var first = [{value: "", label: "(All)"}];
+ var opts = {
+ options: first.concat(items),
+ searchField: "label",
+ valueField: "value",
+ labelField: "label",
+ maxItems: 50
+ };
+ var select = $("#" + selectizeID).find("select")[0];
+ var selectize = $(select).selectize(opts)[0].selectize;
+ // NOTE: this callback is triggered when *directly* altering
+ // dropdown items
+ selectize.on("change", function() {
+ var currentItems = traceManager.groupSelections[set] || [];
+ if (!x.highlight.persistent) {
+ removeBrush(el);
+ for (var i = 0; i < currentItems.length; i++) {
+ selectize.removeItem(currentItems[i], true);
+ }
+ }
+ var newItems = selectize.items.filter(function(idx) {
+ return currentItems.indexOf(idx) < 0;
+ });
+ if (newItems.length > 0) {
+ traceManager.updateSelection(set, newItems);
+ } else {
+ // Item has been removed...
+ // TODO: this logic won't work for dynamically changing palette
+ traceManager.updateSelection(set, null);
+ traceManager.updateSelection(set, selectize.items);
+ }
+ });
+ }
+ } // end of selectionChange
+ } // end of renderValue
+}
+
+/**
+ * @param graphDiv The Plotly graph div
+ * @param highlight An object with options for updating selection(s)
+ */
+function TraceManager(graphDiv, highlight) {
+ // The Plotly graph div
+ this.gd = graphDiv;
+
+ // Preserve the original data.
+ // TODO: try using Lib.extendFlat() as done in
+ // https://github.com/plotly/plotly.js/pull/1136
+ this.origData = JSON.parse(JSON.stringify(graphDiv.data));
+
+ // avoid doing this over and over
+ this.origOpacity = [];
+ for (var i = 0; i < this.origData.length; i++) {
+ this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1);
+ }
+
+ // key: group name, value: null or array of keys representing the
+ // most recently received selection for that group.
+ this.groupSelections = {};
+
+ // selection parameters (e.g., transient versus persistent selection)
+ this.highlight = highlight;
+}
+
+TraceManager.prototype.close = function() {
+ // TODO: Unhook all event handlers
+};
+
+TraceManager.prototype.updateFilter = function(group, keys) {
+
+ if (typeof(keys) === "undefined" || keys === null) {
+
+ this.gd.data = JSON.parse(JSON.stringify(this.origData));
+
+ } else {
+
+ var traces = [];
+ for (var i = 0; i < this.origData.length; i++) {
+ var trace = this.origData[i];
+ if (!trace.key || trace.set !== group) {
+ continue;
+ }
+ var matchFunc = getMatchFunc(trace);
+ var matches = matchFunc(trace.key, keys);
+
+ if (matches.length > 0) {
+ if (!trace._isSimpleKey) {
+ // subsetArrayAttrs doesn't mutate trace (it makes a modified clone)
+ trace = subsetArrayAttrs(trace, matches);
+ }
+ traces.push(trace);
+ }
+ }
+ }
+
+ this.gd.data = traces;
+ Plotly.redraw(this.gd);
+
+ // NOTE: we purposely do _not_ restore selection(s), since on filter,
+ // axis likely will update, changing the pixel -> data mapping, leading
+ // to a likely mismatch in the brush outline and highlighted marks
+
+};
+
+TraceManager.prototype.updateSelection = function(group, keys) {
+
+ if (keys !== null && !Array.isArray(keys)) {
+ throw new Error("Invalid keys argument; null or array expected");
+ }
+
+ // if selection has been cleared, or if this is transient
+ // selection, delete the "selection traces"
+ var nNewTraces = this.gd.data.length - this.origData.length;
+ if (keys === null || !this.highlight.persistent && nNewTraces > 0) {
+ var tracesToRemove = [];
+ for (var i = 0; i < this.gd.data.length; i++) {
+ if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i);
+ }
+ Plotly.deleteTraces(this.gd, tracesToRemove);
+ this.groupSelections[group] = keys;
+ } else {
+ // add to the groupSelection, rather than overwriting it
+ // TODO: can this be removed?
+ this.groupSelections[group] = this.groupSelections[group] || [];
+ for (var i = 0; i < keys.length; i++) {
+ var k = keys[i];
+ if (this.groupSelections[group].indexOf(k) < 0) {
+ this.groupSelections[group].push(k);
+ }
+ }
+ }
+
+ if (keys === null) {
+
+ Plotly.restyle(this.gd, {"opacity": this.origOpacity});
+
+ } else if (keys.length >= 1) {
+
+ // placeholder for new "selection traces"
+ var traces = [];
+ // this variable is set in R/highlight.R
+ var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() ||
+ this.highlight.color[0];
+
+ for (var i = 0; i < this.origData.length; i++) {
+ // TODO: try using Lib.extendFlat() as done in
+ // https://github.com/plotly/plotly.js/pull/1136
+ var trace = JSON.parse(JSON.stringify(this.gd.data[i]));
+ if (!trace.key || trace.set !== group) {
+ continue;
+ }
+ // Get sorted array of matching indices in trace.key
+ var matchFunc = getMatchFunc(trace);
+ var matches = matchFunc(trace.key, keys);
+
+ if (matches.length > 0) {
+ // If this is a "simple" key, that means select the entire trace
+ if (!trace._isSimpleKey) {
+ trace = subsetArrayAttrs(trace, matches);
+ }
+ // reach into the full trace object so we can properly reflect the
+ // selection attributes in every view
+ var d = this.gd._fullData[i];
+
+ /*
+ / Recursively inherit selection attributes from various sources,
+ / in order of preference:
+ / (1) official plotly.js selected attribute
+ / (2) highlight(selected = attrs_selected(...))
+ */
+ // TODO: it would be neat to have a dropdown to dynamically specify these!
+ $.extend(true, trace, this.highlight.selected);
+
+ // if it is defined, override color with the "dynamic brush color""
+ if (d.marker) {
+ trace.marker = trace.marker || {};
+ trace.marker.color = selectionColour || trace.marker.color || d.marker.color;
+ }
+ if (d.line) {
+ trace.line = trace.line || {};
+ trace.line.color = selectionColour || trace.line.color || d.line.color;
+ }
+ if (d.textfont) {
+ trace.textfont = trace.textfont || {};
+ trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color;
+ }
+ if (d.fillcolor) {
+ // TODO: should selectionColour inherit alpha from the existing fillcolor?
+ trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor;
+ }
+ // attach a sensible name/legendgroup
+ trace.name = trace.name || keys.join("
");
+ trace.legendgroup = trace.legendgroup || keys.join("
");
+
+ // keep track of mapping between this new trace and the trace it targets
+ // (necessary for updating frames to reflect the selection traces)
+ trace._originalIndex = i;
+ trace._newIndex = this.gd._fullData.length + traces.length;
+ trace._isCrosstalkTrace = true;
+ traces.push(trace);
+ }
+ }
+
+ if (traces.length > 0) {
+
+ Plotly.addTraces(this.gd, traces).then(function(gd) {
+ // incrementally add selection traces to frames
+ // (this is heavily inspired by Plotly.Plots.modifyFrames()
+ // in src/plots/plots.js)
+ var _hash = gd._transitionData._frameHash;
+ var _frames = gd._transitionData._frames || [];
+
+ for (var i = 0; i < _frames.length; i++) {
+
+ // add to _frames[i].traces *if* this frame references selected trace(s)
+ var newIndices = [];
+ for (var j = 0; j < traces.length; j++) {
+ var tr = traces[j];
+ if (_frames[i].traces.indexOf(tr._originalIndex) > -1) {
+ newIndices.push(tr._newIndex);
+ _frames[i].traces.push(tr._newIndex);
+ }
+ }
+
+ // nothing to do...
+ if (newIndices.length === 0) {
+ continue;
+ }
+
+ var ctr = 0;
+ var nFrameTraces = _frames[i].data.length;
+
+ for (var j = 0; j < nFrameTraces; j++) {
+ var frameTrace = _frames[i].data[j];
+ if (!frameTrace.key || frameTrace.set !== group) {
+ continue;
+ }
+
+ var matchFunc = getMatchFunc(frameTrace);
+ var matches = matchFunc(frameTrace.key, keys);
+
+ if (matches.length > 0) {
+ if (!trace._isSimpleKey) {
+ frameTrace = subsetArrayAttrs(frameTrace, matches);
+ }
+ var d = gd._fullData[newIndices[ctr]];
+ if (d.marker) {
+ frameTrace.marker = d.marker;
+ }
+ if (d.line) {
+ frameTrace.line = d.line;
+ }
+ if (d.textfont) {
+ frameTrace.textfont = d.textfont;
+ }
+ ctr = ctr + 1;
+ _frames[i].data.push(frameTrace);
+ }
+ }
+
+ // update gd._transitionData._frameHash
+ _hash[_frames[i].name] = _frames[i];
+ }
+
+ });
+
+ // dim traces that have a set matching the set of selection sets
+ var tracesToDim = [],
+ opacities = [],
+ sets = Object.keys(this.groupSelections),
+ n = this.origData.length;
+
+ for (var i = 0; i < n; i++) {
+ var opacity = this.origOpacity[i] || 1;
+ // have we already dimmed this trace? Or is this even worth doing?
+ if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) {
+ continue;
+ }
+ // is this set an element of the set of selection sets?
+ var matches = findMatches(sets, [this.gd.data[i].set]);
+ if (matches.length) {
+ tracesToDim.push(i);
+ opacities.push(opacity * this.highlight.opacityDim);
+ }
+ }
+
+ if (tracesToDim.length > 0) {
+ Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim);
+ // turn off the selected/unselected API
+ Plotly.restyle(this.gd, {"selectedpoints": null});
+ }
+
+ }
+
+ }
+};
+
+/*
+Note: in all of these match functions, we assume needleSet (i.e. the selected keys)
+is a 1D (or flat) array. The real difference is the meaning of haystack.
+findMatches() does the usual thing you'd expect for
+linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff
+haystack is a subset of the needleSet. findNestedMatches() returns
+*/
+
+function getMatchFunc(trace) {
+ return (trace._isNestedKey) ? findNestedMatches :
+ (trace._isSimpleKey) ? findSimpleMatches : findMatches;
+}
+
+// find matches for "flat" keys
+function findMatches(haystack, needleSet) {
+ var matches = [];
+ haystack.forEach(function(obj, i) {
+ if (obj === null || needleSet.indexOf(obj) >= 0) {
+ matches.push(i);
+ }
+ });
+ return matches;
+}
+
+// find matches for "simple" keys
+function findSimpleMatches(haystack, needleSet) {
+ var match = haystack.every(function(val) {
+ return val === null || needleSet.indexOf(val) >= 0;
+ });
+ // yes, this doesn't make much sense other than conforming
+ // to the output type of the other match functions
+ return (match) ? [0] : []
+}
+
+// find matches for a "nested" haystack (2D arrays)
+function findNestedMatches(haystack, needleSet) {
+ var matches = [];
+ for (var i = 0; i < haystack.length; i++) {
+ var hay = haystack[i];
+ var match = hay.every(function(val) {
+ return val === null || needleSet.indexOf(val) >= 0;
+ });
+ if (match) {
+ matches.push(i);
+ }
+ }
+ return matches;
+}
+
+function isPlainObject(obj) {
+ return (
+ Object.prototype.toString.call(obj) === '[object Object]' &&
+ Object.getPrototypeOf(obj) === Object.prototype
+ );
+}
+
+function subsetArrayAttrs(obj, indices) {
+ var newObj = {};
+ Object.keys(obj).forEach(function(k) {
+ var val = obj[k];
+
+ if (k.charAt(0) === "_") {
+ newObj[k] = val;
+ } else if (k === "transforms" && Array.isArray(val)) {
+ newObj[k] = val.map(function(transform) {
+ return subsetArrayAttrs(transform, indices);
+ });
+ } else if (k === "colorscale" && Array.isArray(val)) {
+ newObj[k] = val;
+ } else if (isPlainObject(val)) {
+ newObj[k] = subsetArrayAttrs(val, indices);
+ } else if (Array.isArray(val)) {
+ newObj[k] = subsetArray(val, indices);
+ } else {
+ newObj[k] = val;
+ }
+ });
+ return newObj;
+}
+
+function subsetArray(arr, indices) {
+ var result = [];
+ for (var i = 0; i < indices.length; i++) {
+ result.push(arr[indices[i]]);
+ }
+ return result;
+}
+
+// Convenience function for removing plotly's brush
+function removeBrush(el) {
+ var outlines = el.querySelectorAll(".select-outline");
+ for (var i = 0; i < outlines.length; i++) {
+ outlines[i].remove();
+ }
+}
+
+
+// https://davidwalsh.name/javascript-debounce-function
+
+// Returns a function, that, as long as it continues to be invoked, will not
+// be triggered. The function will be called after it stops being called for
+// N milliseconds. If `immediate` is passed, trigger the function on the
+// leading edge, instead of the trailing.
+function debounce(func, wait, immediate) {
+ var timeout;
+ return function() {
+ var context = this, args = arguments;
+ var later = function() {
+ timeout = null;
+ if (!immediate) func.apply(context, args);
+ };
+ var callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) func.apply(context, args);
+ };
+};
+
+module.exports = widgetDefinition
\ No newline at end of file