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