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 options = x.selectize[selectizeID]; 525 var first = [{value: "", label: "(All)"}]; 526 var opts = $.extend({ 527 options: first.concat(options.items), 528 searchField: "label", 529 valueField: "value", 530 labelField: "label", 531 maxItems: 50 532 }, 533 options 534 ); 535 var select = $("#" + selectizeID).find("select")[0]; 536 var selectize = $(select).selectize(opts)[0].selectize; 537 // NOTE: this callback is triggered when *directly* altering 538 // dropdown items 539 selectize.on("change", function() { 540 var currentItems = traceManager.groupSelections[set] || []; 541 if (!x.highlight.persistent) { 542 removeBrush(el); 543 for (var i = 0; i < currentItems.length; i++) { 544 selectize.removeItem(currentItems[i], true); 545 } 546 } 547 var newItems = selectize.items.filter(function(idx) { 548 return currentItems.indexOf(idx) < 0; 549 }); 550 if (newItems.length > 0) { 551 traceManager.updateSelection(set, newItems); 552 } else { 553 // Item has been removed... 554 // TODO: this logic won't work for dynamically changing palette 555 traceManager.updateSelection(set, null); 556 traceManager.updateSelection(set, selectize.items); 557 } 558 }); 559 } 560 } // end of selectionChange 561 562 } // end of renderValue 563}); // end of widget definition 564 565/** 566 * @param graphDiv The Plotly graph div 567 * @param highlight An object with options for updating selection(s) 568 */ 569function TraceManager(graphDiv, highlight) { 570 // The Plotly graph div 571 this.gd = graphDiv; 572 573 // Preserve the original data. 574 // TODO: try using Lib.extendFlat() as done in 575 // https://github.com/plotly/plotly.js/pull/1136 576 this.origData = JSON.parse(JSON.stringify(graphDiv.data)); 577 578 // avoid doing this over and over 579 this.origOpacity = []; 580 for (var i = 0; i < this.origData.length; i++) { 581 this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1); 582 } 583 584 // key: group name, value: null or array of keys representing the 585 // most recently received selection for that group. 586 this.groupSelections = {}; 587 588 // selection parameters (e.g., transient versus persistent selection) 589 this.highlight = highlight; 590} 591 592TraceManager.prototype.close = function() { 593 // TODO: Unhook all event handlers 594}; 595 596TraceManager.prototype.updateFilter = function(group, keys) { 597 598 if (typeof(keys) === "undefined" || keys === null) { 599 600 this.gd.data = JSON.parse(JSON.stringify(this.origData)); 601 602 } else { 603
604 var traces = []; 605 for (var i = 0; i < this.origData.length; i++) { 606 var trace = this.origData[i]; 607 if (!trace.key || trace.set !== group) { 608 continue; 609 } 610 var matchFunc = getMatchFunc(trace); 611 var matches = matchFunc(trace.key, keys); 612 613 if (matches.length > 0) { 614 if (!trace._isSimpleKey) { 615 // subsetArrayAttrs doesn't mutate trace (it makes a modified clone) 616 trace = subsetArrayAttrs(trace, matches); 617 } 618 traces.push(trace); 619 } 620 } 621 this.gd.data = traces; 622 } 623 624 Plotly.redraw(this.gd); 625 626 // NOTE: we purposely do _not_ restore selection(s), since on filter, 627 // axis likely will update, changing the pixel -> data mapping, leading 628 // to a likely mismatch in the brush outline and highlighted marks 629 630}; 631 632TraceManager.prototype.updateSelection = function(group, keys) { 633 634 if (keys !== null && !Array.isArray(keys)) { 635 throw new Error("Invalid keys argument; null or array expected"); 636 } 637 638 // if selection has been cleared, or if this is transient 639 // selection, delete the "selection traces" 640 var nNewTraces = this.gd.data.length - this.origData.length; 641 if (keys === null || !this.highlight.persistent && nNewTraces > 0) { 642 var tracesToRemove = []; 643 for (var i = 0; i < this.gd.data.length; i++) { 644 if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i); 645 } 646 Plotly.deleteTraces(this.gd, tracesToRemove); 647 this.groupSelections[group] = keys; 648 } else { 649 // add to the groupSelection, rather than overwriting it 650 // TODO: can this be removed? 651 this.groupSelections[group] = this.groupSelections[group] || []; 652 for (var i = 0; i < keys.length; i++) { 653 var k = keys[i]; 654 if (this.groupSelections[group].indexOf(k) < 0) { 655 this.groupSelections[group].push(k); 656 } 657 } 658 } 659 660 if (keys === null) { 661 662 Plotly.restyle(this.gd, {"opacity": this.origOpacity}); 663 664 } else if (keys.length >= 1) { 665 666 // placeholder for new "selection traces" 667 var traces = []; 668 // this variable is set in R/highlight.R 669 var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() || 670 this.highlight.color[0]; 671 672 for (var i = 0; i < this.origData.length; i++) { 673 // TODO: try using Lib.extendFlat() as done in 674 // https://github.com/plotly/plotly.js/pull/1136 675 var trace = JSON.parse(JSON.stringify(this.gd.data[i])); 676 if (!trace.key || trace.set !== group) { 677 continue; 678 } 679 // Get sorted array of matching indices in trace.key 680 var matchFunc = getMatchFunc(trace); 681 var matches = matchFunc(trace.key, keys); 682 683 if (matches.length > 0) { 684 // If this is a "simple" key, that means select the entire trace 685 if (!trace._isSimpleKey) { 686 trace = subsetArrayAttrs(trace, matches); 687 } 688 // reach into the full trace object so we can properly reflect the 689 // selection attributes in every view 690 var d = this.gd._fullData[i]; 691 692 /* 693 / Recursively inherit selection attributes from various sources, 694 / in order of preference: 695 / (1) official plotly.js selected attribute 696 / (2) highlight(selected = attrs_selected(...)) 697 */ 698 // TODO: it would be neat to have a dropdown to dynamically specify these! 699 $.extend(true, trace, this.highlight.selected); 700 701 // if it is defined, override color with the "dynamic brush color"" 702 if (d.marker) { 703 trace.marker = trace.marker || {}; 704 trace.marker.color = selectionColour || trace.marker.color || d.marker.color; 705 } 706 if (d.line) { 707 trace.line = trace.line || {}; 708 trace.line.color = selectionColour || trace.line.color || d.line.color; 709 } 710 if (d.textfont) { 711 trace.textfont = trace.textfont || {}; 712 trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color; 713 } 714 if (d.fillcolor) { 715 // TODO: should selectionColour inherit alpha from the existing fillcolor? 716 trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor; 717 } 718 // attach a sensible name/legendgroup 719 trace.name = trace.name || keys.join("<br />"); 720 trace.legendgroup = trace.legendgroup || keys.join("<br />"); 721 722 // keep track of mapping between this new trace and the trace it targets 723 // (necessary for updating frames to reflect the selection traces) 724 trace._originalIndex = i; 725 trace._newIndex = this.gd._fullData.length + traces.length; 726 trace._isCrosstalkTrace = true; 727 traces.push(trace); 728 } 729 } 730 731 if (traces.length > 0) { 732 733 Plotly.addTraces(this.gd, traces).then(function(gd) { 734 // incrementally add selection traces to frames 735 // (this is heavily inspired by Plotly.Plots.modifyFrames() 736 // in src/plots/plots.js) 737 var _hash = gd._transitionData._frameHash; 738 var _frames = gd._transitionData._frames || []; 739 740 for (var i = 0; i < _frames.length; i++) { 741 742 // add to _frames[i].traces *if* this frame references selected trace(s) 743 var newIndices = []; 744 for (var j = 0; j < traces.length; j++) { 745 var tr = traces[j]; 746 if (_frames[i].traces.indexOf(tr._originalIndex) > -1) { 747 newIndices.push(tr._newIndex); 748 _frames[i].traces.push(tr._newIndex); 749 } 750 } 751 752 // nothing to do... 753 if (newIndices.length === 0) { 754 continue; 755 } 756 757 var ctr = 0; 758 var nFrameTraces = _frames[i].data.length; 759 760 for (var j = 0; j < nFrameTraces; j++) { 761 var frameTrace = _frames[i].data[j]; 762 if (!frameTrace.key || frameTrace.set !== group) { 763 continue; 764 } 765 766 var matchFunc = getMatchFunc(frameTrace); 767 var matches = matchFunc(frameTrace.key, keys); 768 769 if (matches.length > 0) { 770 if (!trace._isSimpleKey) { 771 frameTrace = subsetArrayAttrs(frameTrace, matches); 772 } 773 var d = gd._fullData[newIndices[ctr]]; 774 if (d.marker) { 775 frameTrace.marker = d.marker; 776 } 777 if (d.line) { 778 frameTrace.line = d.line; 779 } 780 if (d.textfont) { 781 frameTrace.textfont = d.textfont; 782 } 783 ctr = ctr + 1; 784 _frames[i].data.push(frameTrace); 785 } 786 } 787 788 // update gd._transitionData._frameHash 789 _hash[_frames[i].name] = _frames[i]; 790 } 791 792 }); 793 794 // dim traces that have a set matching the set of selection sets 795 var tracesToDim = [], 796 opacities = [], 797 sets = Object.keys(this.groupSelections), 798 n = this.origData.length; 799 800 for (var i = 0; i < n; i++) { 801 var opacity = this.origOpacity[i] || 1; 802 // have we already dimmed this trace? Or is this even worth doing? 803 if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) { 804 continue; 805 } 806 // is this set an element of the set of selection sets? 807 var matches = findMatches(sets, [this.gd.data[i].set]); 808 if (matches.length) { 809 tracesToDim.push(i); 810 opacities.push(opacity * this.highlight.opacityDim); 811 } 812 } 813 814 if (tracesToDim.length > 0) { 815 Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim); 816 // turn off the selected/unselected API 817 Plotly.restyle(this.gd, {"selectedpoints": null}); 818 } 819 820 } 821 822 } 823}; 824 825/* 826Note: in all of these match functions, we assume needleSet (i.e. the selected keys) 827is a 1D (or flat) array. The real difference is the meaning of haystack.
828findMatches() does the usual thing you'd expect for 829linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff 830haystack is a subset of the needleSet. findNestedMatches() returns 831*/ 832 833function getMatchFunc(trace) { 834 return (trace._isNestedKey) ? findNestedMatches : 835 (trace._isSimpleKey) ? findSimpleMatches : findMatches; 836} 837 838// find matches for "flat" keys 839function findMatches(haystack, needleSet) { 840 var matches = []; 841 haystack.forEach(function(obj, i) { 842 if (obj === null || needleSet.indexOf(obj) >= 0) { 843 matches.push(i); 844 } 845 }); 846 return matches; 847} 848 849// find matches for "simple" keys 850function findSimpleMatches(haystack, needleSet) { 851 var match = haystack.every(function(val) { 852 return val === null || needleSet.indexOf(val) >= 0; 853 }); 854 // yes, this doesn't make much sense other than conforming 855 // to the output type of the other match functions 856 return (match) ? [0] : [] 857} 858 859// find matches for a "nested" haystack (2D arrays) 860function findNestedMatches(haystack, needleSet) { 861 var matches = []; 862 for (var i = 0; i < haystack.length; i++) { 863 var hay = haystack[i]; 864 var match = hay.every(function(val) { 865 return val === null || needleSet.indexOf(val) >= 0; 866 }); 867 if (match) { 868 matches.push(i); 869 } 870 } 871 return matches; 872} 873 874function isPlainObject(obj) { 875 return ( 876 Object.prototype.toString.call(obj) === '[object Object]' && 877 Object.getPrototypeOf(obj) === Object.prototype 878 ); 879} 880 881function subsetArrayAttrs(obj, indices) { 882 var newObj = {}; 883 Object.keys(obj).forEach(function(k) { 884 var val = obj[k]; 885 886 if (k.charAt(0) === "_") { 887 newObj[k] = val; 888 } else if (k === "transforms" && Array.isArray(val)) { 889 newObj[k] = val.map(function(transform) { 890 return subsetArrayAttrs(transform, indices); 891 }); 892 } else if (k === "colorscale" && Array.isArray(val)) { 893 newObj[k] = val; 894 } else if (isPlainObject(val)) { 895 newObj[k] = subsetArrayAttrs(val, indices); 896 } else if (Array.isArray(val)) { 897 newObj[k] = subsetArray(val, indices); 898 } else { 899 newObj[k] = val; 900 } 901 }); 902 return newObj; 903} 904 905function subsetArray(arr, indices) { 906 var result = []; 907 for (var i = 0; i < indices.length; i++) { 908 result.push(arr[indices[i]]); 909 } 910 return result; 911} 912 913// Convenience function for removing plotly's brush 914function removeBrush(el) { 915 var outlines = el.querySelectorAll(".select-outline"); 916 for (var i = 0; i < outlines.length; i++) { 917 outlines[i].remove(); 918 } 919} 920 921 922// https://davidwalsh.name/javascript-debounce-function 923 924// Returns a function, that, as long as it continues to be invoked, will not 925// be triggered. The function will be called after it stops being called for 926// N milliseconds. If `immediate` is passed, trigger the function on the 927// leading edge, instead of the trailing. 928function debounce(func, wait, immediate) { 929 var timeout; 930 return function() { 931 var context = this, args = arguments; 932 var later = function() { 933 timeout = null; 934 if (!immediate) func.apply(context, args); 935 }; 936 var callNow = immediate && !timeout; 937 clearTimeout(timeout); 938 timeout = setTimeout(later, wait); 939 if (callNow) func.apply(context, args); 940 }; 941};
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.