25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

936 satır
33KB

  1. HTMLWidgets.widget({
  2. name: "plotly",
  3. type: "output",
  4. initialize: function(el, width, height) {
  5. return {};
  6. },
  7. resize: function(el, width, height, instance) {
  8. if (instance.autosize) {
  9. var width = instance.width || width;
  10. var height = instance.height || height;
  11. Plotly.relayout(el.id, {width: width, height: height});
  12. }
  13. },
  14. renderValue: function(el, x, instance) {
  15. // Plotly.relayout() mutates the plot input object, so make sure to
  16. // keep a reference to the user-supplied width/height *before*
  17. // we call Plotly.plot();
  18. var lay = x.layout || {};
  19. instance.width = lay.width;
  20. instance.height = lay.height;
  21. instance.autosize = lay.autosize || true;
  22. /*
  23. / 'inform the world' about highlighting options this is so other
  24. / crosstalk libraries have a chance to respond to special settings
  25. / such as persistent selection.
  26. / AFAIK, leaflet is the only library with such intergration
  27. / https://github.com/rstudio/leaflet/pull/346/files#diff-ad0c2d51ce5fdf8c90c7395b102f4265R154
  28. */
  29. var ctConfig = crosstalk.var('plotlyCrosstalkOpts').set(x.highlight);
  30. if (typeof(window) !== "undefined") {
  31. // make sure plots don't get created outside the network (for on-prem)
  32. window.PLOTLYENV = window.PLOTLYENV || {};
  33. window.PLOTLYENV.BASE_URL = x.base_url;
  34. // Enable persistent selection when shift key is down
  35. // https://stackoverflow.com/questions/1828613/check-if-a-key-is-down
  36. var persistOnShift = function(e) {
  37. if (!e) window.event;
  38. if (e.shiftKey) {
  39. x.highlight.persistent = true;
  40. x.highlight.persistentShift = true;
  41. } else {
  42. x.highlight.persistent = false;
  43. x.highlight.persistentShift = false;
  44. }
  45. };
  46. // Only relevant if we haven't forced persistent mode at command line
  47. if (!x.highlight.persistent) {
  48. window.onmousemove = persistOnShift;
  49. }
  50. }
  51. var graphDiv = document.getElementById(el.id);
  52. // TODO: move the control panel injection strategy inside here...
  53. HTMLWidgets.addPostRenderHandler(function() {
  54. // lower the z-index of the modebar to prevent it from highjacking hover
  55. // (TODO: do this via CSS?)
  56. // https://github.com/ropensci/plotly/issues/956
  57. // https://www.w3schools.com/jsref/prop_style_zindex.asp
  58. var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar");
  59. for (var i = 0; i < modebars.length; i++) {
  60. modebars[i].style.zIndex = 1;
  61. }
  62. });
  63. // inject a "control panel" holding selectize/dynamic color widget(s)
  64. if (x.selectize || x.highlight.dynamic && !instance.plotly) {
  65. var flex = document.createElement("div");
  66. flex.class = "plotly-crosstalk-control-panel";
  67. flex.style = "display: flex; flex-wrap: wrap";
  68. // inject the colourpicker HTML container into the flexbox
  69. if (x.highlight.dynamic) {
  70. var pickerDiv = document.createElement("div");
  71. var pickerInput = document.createElement("input");
  72. pickerInput.id = el.id + "-colourpicker";
  73. pickerInput.placeholder = "asdasd";
  74. var pickerLabel = document.createElement("label");
  75. pickerLabel.for = pickerInput.id;
  76. pickerLabel.innerHTML = "Brush color&nbsp;&nbsp;";
  77. pickerDiv.appendChild(pickerLabel);
  78. pickerDiv.appendChild(pickerInput);
  79. flex.appendChild(pickerDiv);
  80. }
  81. // inject selectize HTML containers (one for every crosstalk group)
  82. if (x.selectize) {
  83. var ids = Object.keys(x.selectize);
  84. for (var i = 0; i < ids.length; i++) {
  85. var container = document.createElement("div");
  86. container.id = ids[i];
  87. container.style = "width: 80%; height: 10%";
  88. container.class = "form-group crosstalk-input-plotly-highlight";
  89. var label = document.createElement("label");
  90. label.for = ids[i];
  91. label.innerHTML = x.selectize[ids[i]].group;
  92. label.class = "control-label";
  93. var selectDiv = document.createElement("div");
  94. var select = document.createElement("select");
  95. select.multiple = true;
  96. selectDiv.appendChild(select);
  97. container.appendChild(label);
  98. container.appendChild(selectDiv);
  99. flex.appendChild(container);
  100. }
  101. }
  102. // finally, insert the flexbox inside the htmlwidget container,
  103. // but before the plotly graph div
  104. graphDiv.parentElement.insertBefore(flex, graphDiv);
  105. if (x.highlight.dynamic) {
  106. var picker = $("#" + pickerInput.id);
  107. var colors = x.highlight.color || [];
  108. // TODO: let users specify options?
  109. var opts = {
  110. value: colors[0],
  111. showColour: "both",
  112. palette: "limited",
  113. allowedCols: colors.join(" "),
  114. width: "20%",
  115. height: "10%"
  116. };
  117. picker.colourpicker({changeDelay: 0});
  118. picker.colourpicker("settings", opts);
  119. picker.colourpicker("value", opts.value);
  120. // inform crosstalk about a change in the current selection colour
  121. var grps = x.highlight.ctGroups || [];
  122. for (var i = 0; i < grps.length; i++) {
  123. crosstalk.group(grps[i]).var('plotlySelectionColour')
  124. .set(picker.colourpicker('value'));
  125. }
  126. picker.on("change", function() {
  127. for (var i = 0; i < grps.length; i++) {
  128. crosstalk.group(grps[i]).var('plotlySelectionColour')
  129. .set(picker.colourpicker('value'));
  130. }
  131. });
  132. }
  133. }
  134. // if no plot exists yet, create one with a particular configuration
  135. if (!instance.plotly) {
  136. var plot = Plotly.plot(graphDiv, x);
  137. instance.plotly = true;
  138. } else {
  139. // this is essentially equivalent to Plotly.newPlot(), but avoids creating
  140. // a new webgl context
  141. // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532
  142. // TODO: restore crosstalk selections?
  143. Plotly.purge(graphDiv);
  144. // TODO: why is this necessary to get crosstalk working?
  145. graphDiv.data = undefined;
  146. graphDiv.layout = undefined;
  147. var plot = Plotly.plot(graphDiv, x);
  148. }
  149. // Trigger plotly.js calls defined via `plotlyProxy()`
  150. plot.then(function() {
  151. if (HTMLWidgets.shinyMode) {
  152. Shiny.addCustomMessageHandler("plotly-calls", function(msg) {
  153. var gd = document.getElementById(msg.id);
  154. if (!gd) {
  155. throw new Error("Couldn't find plotly graph with id: " + msg.id);
  156. }
  157. // This isn't an official plotly.js method, but it's the only current way to
  158. // change just the configuration of a plot
  159. // https://community.plot.ly/t/update-config-function/9057
  160. if (msg.method == "reconfig") {
  161. Plotly.react(gd, gd.data, gd.layout, msg.args);
  162. return;
  163. }
  164. if (!Plotly[msg.method]) {
  165. throw new Error("Unknown method " + msg.method);
  166. }
  167. var args = [gd].concat(msg.args);
  168. Plotly[msg.method].apply(null, args);
  169. });
  170. }
  171. // plotly's mapbox API doesn't currently support setting bounding boxes
  172. // https://www.mapbox.com/mapbox-gl-js/example/fitbounds/
  173. // so we do this manually...
  174. // TODO: make sure this triggers on a redraw and relayout as well as on initial draw
  175. var mapboxIDs = graphDiv._fullLayout._subplots.mapbox || [];
  176. for (var i = 0; i < mapboxIDs.length; i++) {
  177. var id = mapboxIDs[i];
  178. var mapOpts = x.layout[id] || {};
  179. var args = mapOpts._fitBounds || {};
  180. if (!args) {
  181. continue;
  182. }
  183. var mapObj = graphDiv._fullLayout[id]._subplot.map;
  184. mapObj.fitBounds(args.bounds, args.options);
  185. }
  186. });
  187. // Attach attributes (e.g., "key", "z") to plotly event data
  188. function eventDataWithKey(eventData) {
  189. if (eventData === undefined || !eventData.hasOwnProperty("points")) {
  190. return null;
  191. }
  192. return eventData.points.map(function(pt) {
  193. var obj = {
  194. curveNumber: pt.curveNumber,
  195. pointNumber: pt.pointNumber,
  196. x: pt.x,
  197. y: pt.y
  198. };
  199. // If 'z' is reported with the event data, then use it!
  200. if (pt.hasOwnProperty("z")) {
  201. obj.z = pt.z;
  202. }
  203. if (pt.hasOwnProperty("customdata")) {
  204. obj.customdata = pt.customdata;
  205. }
  206. /*
  207. TL;DR: (I think) we have to select the graph div (again) to attach keys...
  208. Why? Remember that crosstalk will dynamically add/delete traces
  209. (see traceManager.prototype.updateSelection() below)
  210. For this reason, we can't simply grab keys from x.data (like we did previously)
  211. Moreover, we can't use _fullData, since that doesn't include
  212. unofficial attributes. It's true that click/hover events fire with
  213. pt.data, but drag events don't...
  214. */
  215. var gd = document.getElementById(el.id);
  216. var trace = gd.data[pt.curveNumber];
  217. if (!trace._isSimpleKey) {
  218. var attrsToAttach = ["key"];
  219. } else {
  220. // simple keys fire the whole key
  221. obj.key = trace.key;
  222. var attrsToAttach = [];
  223. }
  224. for (var i = 0; i < attrsToAttach.length; i++) {
  225. var attr = trace[attrsToAttach[i]];
  226. if (Array.isArray(attr)) {
  227. if (typeof pt.pointNumber === "number") {
  228. obj[attrsToAttach[i]] = attr[pt.pointNumber];
  229. } else if (Array.isArray(pt.pointNumber)) {
  230. obj[attrsToAttach[i]] = attr[pt.pointNumber[0]][pt.pointNumber[1]];
  231. } else if (Array.isArray(pt.pointNumbers)) {
  232. obj[attrsToAttach[i]] = pt.pointNumbers.map(function(idx) { return attr[idx]; });
  233. }
  234. }
  235. }
  236. return obj;
  237. });
  238. }
  239. var legendEventData = function(d) {
  240. // if legendgroup is not relevant just return the trace
  241. var trace = d.data[d.curveNumber];
  242. if (!trace.legendgroup) return trace;
  243. // if legendgroup was specified, return all traces that match the group
  244. var legendgrps = d.data.map(function(trace){ return trace.legendgroup; });
  245. var traces = [];
  246. for (i = 0; i < legendgrps.length; i++) {
  247. if (legendgrps[i] == trace.legendgroup) {
  248. traces.push(d.data[i]);
  249. }
  250. }
  251. return traces;
  252. };
  253. // send user input event data to shiny
  254. if (HTMLWidgets.shinyMode && Shiny.setInputValue) {
  255. // Some events clear other input values
  256. // TODO: always register these?
  257. var eventClearMap = {
  258. plotly_deselect: ["plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing", "plotly_click"],
  259. plotly_unhover: ["plotly_hover"],
  260. plotly_doubleclick: ["plotly_click"]
  261. };
  262. Object.keys(eventClearMap).map(function(evt) {
  263. graphDiv.on(evt, function() {
  264. var inputsToClear = eventClearMap[evt];
  265. inputsToClear.map(function(input) {
  266. Shiny.setInputValue(input + "-" + x.source, null, {priority: "event"});
  267. });
  268. });
  269. });
  270. var eventDataFunctionMap = {
  271. plotly_click: eventDataWithKey,
  272. plotly_sunburstclick: eventDataWithKey,
  273. plotly_hover: eventDataWithKey,
  274. plotly_unhover: eventDataWithKey,
  275. // If 'plotly_selected' has already been fired, and you click
  276. // on the plot afterwards, this event fires `undefined`?!?
  277. // That might be considered a plotly.js bug, but it doesn't make
  278. // sense for this input change to occur if `d` is falsy because,
  279. // even in the empty selection case, `d` is truthy (an object),
  280. // and the 'plotly_deselect' event will reset this input
  281. plotly_selected: function(d) { if (d) { return eventDataWithKey(d); } },
  282. plotly_selecting: function(d) { if (d) { return eventDataWithKey(d); } },
  283. plotly_brushed: function(d) {
  284. if (d) { return d.range ? d.range : d.lassoPoints; }
  285. },
  286. plotly_brushing: function(d) {
  287. if (d) { return d.range ? d.range : d.lassoPoints; }
  288. },
  289. plotly_legendclick: legendEventData,
  290. plotly_legenddoubleclick: legendEventData,
  291. plotly_clickannotation: function(d) { return d.fullAnnotation }
  292. };
  293. var registerShinyValue = function(event) {
  294. var eventDataPreProcessor = eventDataFunctionMap[event] || function(d) { return d ? d : el.id };
  295. // some events are unique to the R package
  296. var plotlyJSevent = (event == "plotly_brushed") ? "plotly_selected" : (event == "plotly_brushing") ? "plotly_selecting" : event;
  297. // register the event
  298. graphDiv.on(plotlyJSevent, function(d) {
  299. Shiny.setInputValue(
  300. event + "-" + x.source,
  301. JSON.stringify(eventDataPreProcessor(d)),
  302. {priority: "event"}
  303. );
  304. });
  305. }
  306. var shinyEvents = x.shinyEvents || [];
  307. shinyEvents.map(registerShinyValue);
  308. }
  309. // Given an array of {curveNumber: x, pointNumber: y} objects,
  310. // return a hash of {
  311. // set1: {value: [key1, key2, ...], _isSimpleKey: false},
  312. // set2: {value: [key3, key4, ...], _isSimpleKey: false}
  313. // }
  314. function pointsToKeys(points) {
  315. var keysBySet = {};
  316. for (var i = 0; i < points.length; i++) {
  317. var trace = graphDiv.data[points[i].curveNumber];
  318. if (!trace.key || !trace.set) {
  319. continue;
  320. }
  321. // set defaults for this keySet
  322. // note that we don't track the nested property (yet) since we always
  323. // emit the union -- http://cpsievert.github.io/talks/20161212b/#21
  324. keysBySet[trace.set] = keysBySet[trace.set] || {
  325. value: [],
  326. _isSimpleKey: trace._isSimpleKey
  327. };
  328. // Use pointNumber by default, but aggregated traces should emit pointNumbers
  329. var ptNum = points[i].pointNumber;
  330. var hasPtNum = typeof ptNum === "number";
  331. var ptNum = hasPtNum ? ptNum : points[i].pointNumbers;
  332. // selecting a point of a "simple" trace means: select the
  333. // entire key attached to this trace, which is useful for,
  334. // say clicking on a fitted line to select corresponding observations
  335. var key = trace._isSimpleKey ? trace.key : Array.isArray(ptNum) ? ptNum.map(function(idx) { return trace.key[idx]; }) : trace.key[ptNum];
  336. // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript
  337. var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key;
  338. // TODO: better to only add new values?
  339. keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat);
  340. }
  341. return keysBySet;
  342. }
  343. x.highlight.color = x.highlight.color || [];
  344. // make sure highlight color is an array
  345. if (!Array.isArray(x.highlight.color)) {
  346. x.highlight.color = [x.highlight.color];
  347. }
  348. var traceManager = new TraceManager(graphDiv, x.highlight);
  349. // Gather all *unique* sets.
  350. var allSets = [];
  351. for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) {
  352. var newSet = x.data[curveIdx].set;
  353. if (newSet) {
  354. if (allSets.indexOf(newSet) === -1) {
  355. allSets.push(newSet);
  356. }
  357. }
  358. }
  359. // register event listeners for all sets
  360. for (var i = 0; i < allSets.length; i++) {
  361. var set = allSets[i];
  362. var selection = new crosstalk.SelectionHandle(set);
  363. var filter = new crosstalk.FilterHandle(set);
  364. var filterChange = function(e) {
  365. removeBrush(el);
  366. traceManager.updateFilter(set, e.value);
  367. };
  368. filter.on("change", filterChange);
  369. var selectionChange = function(e) {
  370. // Workaround for 'plotly_selected' now firing previously selected
  371. // points (in addition to new ones) when holding shift key. In our case,
  372. // we just want the new keys
  373. if (x.highlight.on === "plotly_selected" && x.highlight.persistentShift) {
  374. // https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript
  375. Array.prototype.diff = function(a) {
  376. return this.filter(function(i) {return a.indexOf(i) < 0;});
  377. };
  378. e.value = e.value.diff(e.oldValue);
  379. }
  380. // array of "event objects" tracking the selection history
  381. // this is used to avoid adding redundant selections
  382. var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || [];
  383. // Construct an event object "defining" the current event.
  384. var event = {
  385. receiverID: traceManager.gd.id,
  386. plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get()
  387. };
  388. event[set] = e.value;
  389. // TODO: is there a smarter way to check object equality?
  390. if (selectionHistory.length > 0) {
  391. var ev = JSON.stringify(event);
  392. for (var i = 0; i < selectionHistory.length; i++) {
  393. var sel = JSON.stringify(selectionHistory[i]);
  394. if (sel == ev) {
  395. return;
  396. }
  397. }
  398. }
  399. // accumulate history for persistent selection
  400. if (!x.highlight.persistent) {
  401. selectionHistory = [event];
  402. } else {
  403. selectionHistory.push(event);
  404. }
  405. crosstalk.var("plotlySelectionHistory").set(selectionHistory);
  406. // do the actual updating of traces, frames, and the selectize widget
  407. traceManager.updateSelection(set, e.value);
  408. // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items
  409. if (x.selectize) {
  410. if (!x.highlight.persistent || e.value === null) {
  411. selectize.clear(true);
  412. }
  413. selectize.addItems(e.value, true);
  414. selectize.close();
  415. }
  416. }
  417. selection.on("change", selectionChange);
  418. // Set a crosstalk variable selection value, triggering an update
  419. var turnOn = function(e) {
  420. if (e) {
  421. var selectedKeys = pointsToKeys(e.points);
  422. // Keys are group names, values are array of selected keys from group.
  423. for (var set in selectedKeys) {
  424. if (selectedKeys.hasOwnProperty(set)) {
  425. selection.set(selectedKeys[set].value, {sender: el});
  426. }
  427. }
  428. }
  429. };
  430. if (x.highlight.debounce > 0) {
  431. turnOn = debounce(turnOn, x.highlight.debounce);
  432. }
  433. graphDiv.on(x.highlight.on, turnOn);
  434. graphDiv.on(x.highlight.off, function turnOff(e) {
  435. // remove any visual clues
  436. removeBrush(el);
  437. // remove any selection history
  438. crosstalk.var("plotlySelectionHistory").set(null);
  439. // trigger the actual removal of selection traces
  440. selection.set(null, {sender: el});
  441. });
  442. // register a callback for selectize so that there is bi-directional
  443. // communication between the widget and direct manipulation events
  444. if (x.selectize) {
  445. var selectizeID = Object.keys(x.selectize)[i];
  446. var items = x.selectize[selectizeID].items;
  447. var first = [{value: "", label: "(All)"}];
  448. var opts = {
  449. options: first.concat(items),
  450. searchField: "label",
  451. valueField: "value",
  452. labelField: "label",
  453. maxItems: 50
  454. };
  455. var select = $("#" + selectizeID).find("select")[0];
  456. var selectize = $(select).selectize(opts)[0].selectize;
  457. // NOTE: this callback is triggered when *directly* altering
  458. // dropdown items
  459. selectize.on("change", function() {
  460. var currentItems = traceManager.groupSelections[set] || [];
  461. if (!x.highlight.persistent) {
  462. removeBrush(el);
  463. for (var i = 0; i < currentItems.length; i++) {
  464. selectize.removeItem(currentItems[i], true);
  465. }
  466. }
  467. var newItems = selectize.items.filter(function(idx) {
  468. return currentItems.indexOf(idx) < 0;
  469. });
  470. if (newItems.length > 0) {
  471. traceManager.updateSelection(set, newItems);
  472. } else {
  473. // Item has been removed...
  474. // TODO: this logic won't work for dynamically changing palette
  475. traceManager.updateSelection(set, null);
  476. traceManager.updateSelection(set, selectize.items);
  477. }
  478. });
  479. }
  480. } // end of selectionChange
  481. } // end of renderValue
  482. }); // end of widget definition
  483. /**
  484. * @param graphDiv The Plotly graph div
  485. * @param highlight An object with options for updating selection(s)
  486. */
  487. function TraceManager(graphDiv, highlight) {
  488. // The Plotly graph div
  489. this.gd = graphDiv;
  490. // Preserve the original data.
  491. // TODO: try using Lib.extendFlat() as done in
  492. // https://github.com/plotly/plotly.js/pull/1136
  493. this.origData = JSON.parse(JSON.stringify(graphDiv.data));
  494. // avoid doing this over and over
  495. this.origOpacity = [];
  496. for (var i = 0; i < this.origData.length; i++) {
  497. this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1);
  498. }
  499. // key: group name, value: null or array of keys representing the
  500. // most recently received selection for that group.
  501. this.groupSelections = {};
  502. // selection parameters (e.g., transient versus persistent selection)
  503. this.highlight = highlight;
  504. }
  505. TraceManager.prototype.close = function() {
  506. // TODO: Unhook all event handlers
  507. };
  508. TraceManager.prototype.updateFilter = function(group, keys) {
  509. if (typeof(keys) === "undefined" || keys === null) {
  510. this.gd.data = JSON.parse(JSON.stringify(this.origData));
  511. } else {
  512. var traces = [];
  513. for (var i = 0; i < this.origData.length; i++) {
  514. var trace = this.origData[i];
  515. if (!trace.key || trace.set !== group) {
  516. continue;
  517. }
  518. var matchFunc = getMatchFunc(trace);
  519. var matches = matchFunc(trace.key, keys);
  520. if (matches.length > 0) {
  521. if (!trace._isSimpleKey) {
  522. // subsetArrayAttrs doesn't mutate trace (it makes a modified clone)
  523. trace = subsetArrayAttrs(trace, matches);
  524. }
  525. traces.push(trace);
  526. }
  527. }
  528. }
  529. this.gd.data = traces;
  530. Plotly.redraw(this.gd);
  531. // NOTE: we purposely do _not_ restore selection(s), since on filter,
  532. // axis likely will update, changing the pixel -> data mapping, leading
  533. // to a likely mismatch in the brush outline and highlighted marks
  534. };
  535. TraceManager.prototype.updateSelection = function(group, keys) {
  536. if (keys !== null && !Array.isArray(keys)) {
  537. throw new Error("Invalid keys argument; null or array expected");
  538. }
  539. // if selection has been cleared, or if this is transient
  540. // selection, delete the "selection traces"
  541. var nNewTraces = this.gd.data.length - this.origData.length;
  542. if (keys === null || !this.highlight.persistent && nNewTraces > 0) {
  543. var tracesToRemove = [];
  544. for (var i = 0; i < this.gd.data.length; i++) {
  545. if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i);
  546. }
  547. Plotly.deleteTraces(this.gd, tracesToRemove);
  548. this.groupSelections[group] = keys;
  549. } else {
  550. // add to the groupSelection, rather than overwriting it
  551. // TODO: can this be removed?
  552. this.groupSelections[group] = this.groupSelections[group] || [];
  553. for (var i = 0; i < keys.length; i++) {
  554. var k = keys[i];
  555. if (this.groupSelections[group].indexOf(k) < 0) {
  556. this.groupSelections[group].push(k);
  557. }
  558. }
  559. }
  560. if (keys === null) {
  561. Plotly.restyle(this.gd, {"opacity": this.origOpacity});
  562. } else if (keys.length >= 1) {
  563. // placeholder for new "selection traces"
  564. var traces = [];
  565. // this variable is set in R/highlight.R
  566. var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() ||
  567. this.highlight.color[0];
  568. for (var i = 0; i < this.origData.length; i++) {
  569. // TODO: try using Lib.extendFlat() as done in
  570. // https://github.com/plotly/plotly.js/pull/1136
  571. var trace = JSON.parse(JSON.stringify(this.gd.data[i]));
  572. if (!trace.key || trace.set !== group) {
  573. continue;
  574. }
  575. // Get sorted array of matching indices in trace.key
  576. var matchFunc = getMatchFunc(trace);
  577. var matches = matchFunc(trace.key, keys);
  578. if (matches.length > 0) {
  579. // If this is a "simple" key, that means select the entire trace
  580. if (!trace._isSimpleKey) {
  581. trace = subsetArrayAttrs(trace, matches);
  582. }
  583. // reach into the full trace object so we can properly reflect the
  584. // selection attributes in every view
  585. var d = this.gd._fullData[i];
  586. /*
  587. / Recursively inherit selection attributes from various sources,
  588. / in order of preference:
  589. / (1) official plotly.js selected attribute
  590. / (2) highlight(selected = attrs_selected(...))
  591. */
  592. // TODO: it would be neat to have a dropdown to dynamically specify these!
  593. $.extend(true, trace, this.highlight.selected);
  594. // if it is defined, override color with the "dynamic brush color""
  595. if (d.marker) {
  596. trace.marker = trace.marker || {};
  597. trace.marker.color = selectionColour || trace.marker.color || d.marker.color;
  598. }
  599. if (d.line) {
  600. trace.line = trace.line || {};
  601. trace.line.color = selectionColour || trace.line.color || d.line.color;
  602. }
  603. if (d.textfont) {
  604. trace.textfont = trace.textfont || {};
  605. trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color;
  606. }
  607. if (d.fillcolor) {
  608. // TODO: should selectionColour inherit alpha from the existing fillcolor?
  609. trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor;
  610. }
  611. // attach a sensible name/legendgroup
  612. trace.name = trace.name || keys.join("<br />");
  613. trace.legendgroup = trace.legendgroup || keys.join("<br />");
  614. // keep track of mapping between this new trace and the trace it targets
  615. // (necessary for updating frames to reflect the selection traces)
  616. trace._originalIndex = i;
  617. trace._newIndex = this.gd._fullData.length + traces.length;
  618. trace._isCrosstalkTrace = true;
  619. traces.push(trace);
  620. }
  621. }
  622. if (traces.length > 0) {
  623. Plotly.addTraces(this.gd, traces).then(function(gd) {
  624. // incrementally add selection traces to frames
  625. // (this is heavily inspired by Plotly.Plots.modifyFrames()
  626. // in src/plots/plots.js)
  627. var _hash = gd._transitionData._frameHash;
  628. var _frames = gd._transitionData._frames || [];
  629. for (var i = 0; i < _frames.length; i++) {
  630. // add to _frames[i].traces *if* this frame references selected trace(s)
  631. var newIndices = [];
  632. for (var j = 0; j < traces.length; j++) {
  633. var tr = traces[j];
  634. if (_frames[i].traces.indexOf(tr._originalIndex) > -1) {
  635. newIndices.push(tr._newIndex);
  636. _frames[i].traces.push(tr._newIndex);
  637. }
  638. }
  639. // nothing to do...
  640. if (newIndices.length === 0) {
  641. continue;
  642. }
  643. var ctr = 0;
  644. var nFrameTraces = _frames[i].data.length;
  645. for (var j = 0; j < nFrameTraces; j++) {
  646. var frameTrace = _frames[i].data[j];
  647. if (!frameTrace.key || frameTrace.set !== group) {
  648. continue;
  649. }
  650. var matchFunc = getMatchFunc(frameTrace);
  651. var matches = matchFunc(frameTrace.key, keys);
  652. if (matches.length > 0) {
  653. if (!trace._isSimpleKey) {
  654. frameTrace = subsetArrayAttrs(frameTrace, matches);
  655. }
  656. var d = gd._fullData[newIndices[ctr]];
  657. if (d.marker) {
  658. frameTrace.marker = d.marker;
  659. }
  660. if (d.line) {
  661. frameTrace.line = d.line;
  662. }
  663. if (d.textfont) {
  664. frameTrace.textfont = d.textfont;
  665. }
  666. ctr = ctr + 1;
  667. _frames[i].data.push(frameTrace);
  668. }
  669. }
  670. // update gd._transitionData._frameHash
  671. _hash[_frames[i].name] = _frames[i];
  672. }
  673. });
  674. // dim traces that have a set matching the set of selection sets
  675. var tracesToDim = [],
  676. opacities = [],
  677. sets = Object.keys(this.groupSelections),
  678. n = this.origData.length;
  679. for (var i = 0; i < n; i++) {
  680. var opacity = this.origOpacity[i] || 1;
  681. // have we already dimmed this trace? Or is this even worth doing?
  682. if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) {
  683. continue;
  684. }
  685. // is this set an element of the set of selection sets?
  686. var matches = findMatches(sets, [this.gd.data[i].set]);
  687. if (matches.length) {
  688. tracesToDim.push(i);
  689. opacities.push(opacity * this.highlight.opacityDim);
  690. }
  691. }
  692. if (tracesToDim.length > 0) {
  693. Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim);
  694. // turn off the selected/unselected API
  695. Plotly.restyle(this.gd, {"selectedpoints": null});
  696. }
  697. }
  698. }
  699. };
  700. /*
  701. Note: in all of these match functions, we assume needleSet (i.e. the selected keys)
  702. is a 1D (or flat) array. The real difference is the meaning of haystack.
  703. findMatches() does the usual thing you'd expect for
  704. linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff
  705. haystack is a subset of the needleSet. findNestedMatches() returns
  706. */
  707. function getMatchFunc(trace) {
  708. return (trace._isNestedKey) ? findNestedMatches :
  709. (trace._isSimpleKey) ? findSimpleMatches : findMatches;
  710. }
  711. // find matches for "flat" keys
  712. function findMatches(haystack, needleSet) {
  713. var matches = [];
  714. haystack.forEach(function(obj, i) {
  715. if (obj === null || needleSet.indexOf(obj) >= 0) {
  716. matches.push(i);
  717. }
  718. });
  719. return matches;
  720. }
  721. // find matches for "simple" keys
  722. function findSimpleMatches(haystack, needleSet) {
  723. var match = haystack.every(function(val) {
  724. return val === null || needleSet.indexOf(val) >= 0;
  725. });
  726. // yes, this doesn't make much sense other than conforming
  727. // to the output type of the other match functions
  728. return (match) ? [0] : []
  729. }
  730. // find matches for a "nested" haystack (2D arrays)
  731. function findNestedMatches(haystack, needleSet) {
  732. var matches = [];
  733. for (var i = 0; i < haystack.length; i++) {
  734. var hay = haystack[i];
  735. var match = hay.every(function(val) {
  736. return val === null || needleSet.indexOf(val) >= 0;
  737. });
  738. if (match) {
  739. matches.push(i);
  740. }
  741. }
  742. return matches;
  743. }
  744. function isPlainObject(obj) {
  745. return (
  746. Object.prototype.toString.call(obj) === '[object Object]' &&
  747. Object.getPrototypeOf(obj) === Object.prototype
  748. );
  749. }
  750. function subsetArrayAttrs(obj, indices) {
  751. var newObj = {};
  752. Object.keys(obj).forEach(function(k) {
  753. var val = obj[k];
  754. if (k.charAt(0) === "_") {
  755. newObj[k] = val;
  756. } else if (k === "transforms" && Array.isArray(val)) {
  757. newObj[k] = val.map(function(transform) {
  758. return subsetArrayAttrs(transform, indices);
  759. });
  760. } else if (k === "colorscale" && Array.isArray(val)) {
  761. newObj[k] = val;
  762. } else if (isPlainObject(val)) {
  763. newObj[k] = subsetArrayAttrs(val, indices);
  764. } else if (Array.isArray(val)) {
  765. newObj[k] = subsetArray(val, indices);
  766. } else {
  767. newObj[k] = val;
  768. }
  769. });
  770. return newObj;
  771. }
  772. function subsetArray(arr, indices) {
  773. var result = [];
  774. for (var i = 0; i < indices.length; i++) {
  775. result.push(arr[indices[i]]);
  776. }
  777. return result;
  778. }
  779. // Convenience function for removing plotly's brush
  780. function removeBrush(el) {
  781. var outlines = el.querySelectorAll(".select-outline");
  782. for (var i = 0; i < outlines.length; i++) {
  783. outlines[i].remove();
  784. }
  785. }
  786. // https://davidwalsh.name/javascript-debounce-function
  787. // Returns a function, that, as long as it continues to be invoked, will not
  788. // be triggered. The function will be called after it stops being called for
  789. // N milliseconds. If `immediate` is passed, trigger the function on the
  790. // leading edge, instead of the trailing.
  791. function debounce(func, wait, immediate) {
  792. var timeout;
  793. return function() {
  794. var context = this, args = arguments;
  795. var later = function() {
  796. timeout = null;
  797. if (!immediate) func.apply(context, args);
  798. };
  799. var callNow = immediate && !timeout;
  800. clearTimeout(timeout);
  801. timeout = setTimeout(later, wait);
  802. if (callNow) func.apply(context, args);
  803. };
  804. };