Created
January 16, 2018 03:18
-
-
Save jeremyyeo/6a18ca8d6177a399c15d41b2ff7f83f4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| HTMLWidgets.widget({ | |
| name: "plotly", | |
| type: "output", | |
| initialize: function(el, width, height) { | |
| // when upgrading plotly.js, | |
| // uncomment this console.log(), then do `load_all(); plot_ly()` | |
| // open in chrome, right-click on console output: "save-as" -> "schema.json" | |
| // Schema <- jsonlite::fromJSON("~/Downloads/schema.json") | |
| // devtools::use_data(Schema, overwrite = T, internal = T) | |
| // console.log(JSON.stringify(Plotly.PlotSchema.get())); | |
| return {}; | |
| }, | |
| resize: function(el, width, height, instance) { | |
| if (instance.autosize) { | |
| var width = instance.width || width; | |
| var height = instance.height || height; | |
| Plotly.relayout(el.id, {width: width, height: height}); | |
| } | |
| }, | |
| renderValue: function(el, x, instance) { | |
| if (typeof(window) !== "undefined") { | |
| // make sure plots don't get created outside the network (for on-prem) | |
| window.PLOTLYENV = window.PLOTLYENV || {}; | |
| window.PLOTLYENV.BASE_URL = x.base_url; | |
| } | |
| var graphDiv = document.getElementById(el.id); | |
| // TODO: move the control panel injection strategy inside here... | |
| HTMLWidgets.addPostRenderHandler(function() { | |
| // lower the z-index of the modebar to prevent it from highjacking hover | |
| // (TODO: do this via CSS?) | |
| // https://github.com/ropensci/plotly/issues/956 | |
| // https://www.w3schools.com/jsref/prop_style_zindex.asp | |
| var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar"); | |
| for (var i = 0; i < modebars.length; i++) { | |
| modebars[i].style.zIndex = 1; | |
| } | |
| }); | |
| // inject a "control panel" holding selectize/dynamic color widget(s) | |
| if (x.selectize || x.highlight.dynamic && !instance.plotly) { | |
| var flex = document.createElement("div"); | |
| flex.class = "plotly-crosstalk-control-panel"; | |
| flex.style = "display: flex; flex-wrap: wrap"; | |
| // inject the colourpicker HTML container into the flexbox | |
| if (x.highlight.dynamic) { | |
| var pickerDiv = document.createElement("div"); | |
| var pickerInput = document.createElement("input"); | |
| pickerInput.id = el.id + "-colourpicker"; | |
| pickerInput.placeholder = "asdasd"; | |
| var pickerLabel = document.createElement("label"); | |
| pickerLabel.for = pickerInput.id; | |
| pickerLabel.innerHTML = "Brush color "; | |
| pickerDiv.appendChild(pickerLabel); | |
| pickerDiv.appendChild(pickerInput); | |
| flex.appendChild(pickerDiv); | |
| } | |
| // inject selectize HTML containers (one for every crosstalk group) | |
| if (x.selectize) { | |
| var ids = Object.keys(x.selectize); | |
| for (var i = 0; i < ids.length; i++) { | |
| var container = document.createElement("div"); | |
| container.id = ids[i]; | |
| container.style = "width: 80%; height: 10%"; | |
| container.class = "form-group crosstalk-input-plotly-highlight"; | |
| var label = document.createElement("label"); | |
| label.for = ids[i]; | |
| label.innerHTML = x.selectize[ids[i]].group; | |
| label.class = "control-label"; | |
| var selectDiv = document.createElement("div"); | |
| var select = document.createElement("select"); | |
| select.multiple = true; | |
| selectDiv.appendChild(select); | |
| container.appendChild(label); | |
| container.appendChild(selectDiv); | |
| flex.appendChild(container); | |
| } | |
| } | |
| // finally, insert the flexbox inside the htmlwidget container, | |
| // but before the plotly graph div | |
| graphDiv.parentElement.insertBefore(flex, graphDiv); | |
| if (x.highlight.dynamic) { | |
| var picker = $("#" + pickerInput.id); | |
| var colors = x.highlight.color || []; | |
| // TODO: let users specify options? | |
| var opts = { | |
| value: colors[0], | |
| showColour: "both", | |
| palette: "limited", | |
| allowedCols: colors.join(" "), | |
| width: "20%", | |
| height: "10%" | |
| }; | |
| picker.colourpicker({changeDelay: 0}); | |
| picker.colourpicker("settings", opts); | |
| picker.colourpicker("value", opts.value); | |
| // inform crosstalk about a change in the current selection colour | |
| var grps = x.highlight.ctGroups || []; | |
| for (var i = 0; i < grps.length; i++) { | |
| crosstalk.group(grps[i]).var('plotlySelectionColour') | |
| .set(picker.colourpicker('value')); | |
| } | |
| picker.on("change", function() { | |
| for (var i = 0; i < grps.length; i++) { | |
| crosstalk.group(grps[i]).var('plotlySelectionColour') | |
| .set(picker.colourpicker('value')); | |
| } | |
| }); | |
| } | |
| } | |
| // remove "sendDataToCloud", unless user has specified they want it | |
| x.config = x.config || {}; | |
| if (!x.config.cloud) { | |
| x.config.modeBarButtonsToRemove = x.config.modeBarButtonsToRemove || []; | |
| x.config.modeBarButtonsToRemove.push("sendDataToCloud"); | |
| } | |
| // if no plot exists yet, create one with a particular configuration | |
| if (!instance.plotly) { | |
| var plot = Plotly.plot(graphDiv, x); | |
| instance.plotly = true; | |
| instance.autosize = x.layout.autosize || true; | |
| instance.width = x.layout.width; | |
| instance.height = x.layout.height; | |
| } else { | |
| // this is essentially equivalent to Plotly.newPlot(), but avoids creating | |
| // a new webgl context | |
| // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532 | |
| // TODO: restore crosstalk selections? | |
| Plotly.purge(graphDiv); | |
| // TODO: why is this necessary to get crosstalk working? | |
| graphDiv.data = undefined; | |
| graphDiv.layout = undefined; | |
| var plot = Plotly.plot(graphDiv, x); | |
| } | |
| // Trigger plotly.js calls defined via `plotlyProxy()` | |
| plot.then(function() { | |
| if (HTMLWidgets.shinyMode) { | |
| Shiny.addCustomMessageHandler("plotly-calls", function(msg) { | |
| var gd = document.getElementById(msg.id); | |
| if (!gd) { | |
| throw new Error("Couldn't find plotly graph with id: " + msg.id); | |
| } | |
| if (!Plotly[msg.method]) { | |
| throw new Error("Unknown method " + msg.method); | |
| } | |
| var args = [gd].concat(msg.args); | |
| Plotly[msg.method].apply(null, args); | |
| }); | |
| } | |
| }); | |
| // Attach attributes (e.g., "key", "z") to plotly event data | |
| function eventDataWithKey(eventData) { | |
| if (eventData === undefined || !eventData.hasOwnProperty("points")) { | |
| return null; | |
| } | |
| return eventData.points.map(function(pt) { | |
| var obj = { | |
| curveNumber: pt.curveNumber, | |
| pointNumber: pt.pointNumber, | |
| x: pt.x, | |
| y: pt.y | |
| }; | |
| /* | |
| TL;DR: (I think) we have to select the graph div (again) to attach keys... | |
| Why? Remember that crosstalk will dynamically add/delete traces | |
| (see traceManager.prototype.updateSelection() below) | |
| For this reason, we can't simply grab keys from x.data (like we did previously) | |
| Moreover, we can't use _fullData, since that doesn't include | |
| unofficial attributes. It's true that click/hover events fire with | |
| pt.data, but drag events don't... | |
| */ | |
| var gd = document.getElementById(el.id); | |
| var trace = gd.data[pt.curveNumber]; | |
| // Add other attributes here, if desired | |
| if (!trace._isSimpleKey) { | |
| var attrsToAttach = ["key", "z"]; | |
| } else { | |
| // simple keys fire the whole key | |
| obj.key = trace.key; | |
| var attrsToAttach = ["z"]; | |
| } | |
| for (var i = 0; i < attrsToAttach.length; i++) { | |
| var attr = trace[attrsToAttach[i]]; | |
| if (Array.isArray(attr)) { | |
| // pointNumber can be an array (e.g., heatmaps) | |
| // TODO: can pointNumber be 3D? | |
| obj[attrsToAttach[i]] = typeof pt.pointNumber === "number" ? | |
| attr[pt.pointNumber] : attr[pt.pointNumber[0]][pt.pointNumber[1]]; | |
| } | |
| } | |
| return obj; | |
| }); | |
| } | |
| // send user input event data to shiny | |
| if (HTMLWidgets.shinyMode) { | |
| // https://plot.ly/javascript/zoom-events/ | |
| graphDiv.on('plotly_relayout', function(d) { | |
| Shiny.onInputChange( | |
| ".clientValue-plotly_relayout-" + x.source, | |
| JSON.stringify(d) | |
| ); | |
| }); | |
| graphDiv.on('plotly_hover', function(d) { | |
| Shiny.onInputChange( | |
| ".clientValue-plotly_hover-" + x.source, | |
| JSON.stringify(eventDataWithKey(d)) | |
| ); | |
| }); | |
| graphDiv.on('plotly_click', function(d) { | |
| Shiny.onInputChange( | |
| ".clientValue-plotly_click-" + x.source, | |
| JSON.stringify(eventDataWithKey(d)) | |
| ); | |
| }); | |
| graphDiv.on('plotly_selected', function(d) { | |
| Shiny.onInputChange( | |
| ".clientValue-plotly_selected-" + x.source, | |
| JSON.stringify(eventDataWithKey(d)) | |
| ); | |
| }); | |
| graphDiv.on('plotly_unhover', function(eventData) { | |
| Shiny.onInputChange(".clientValue-plotly_hover-" + x.source, null); | |
| }); | |
| graphDiv.on('plotly_doubleclick', function(eventData) { | |
| Shiny.onInputChange(".clientValue-plotly_click-" + x.source, null); | |
| }); | |
| // 'plotly_deselect' is code for doubleclick when in select mode | |
| graphDiv.on('plotly_deselect', function(eventData) { | |
| Shiny.onInputChange(".clientValue-plotly_selected-" + x.source, null); | |
| Shiny.onInputChange(".clientValue-plotly_click-" + x.source, null); | |
| }); | |
| } | |
| // Given an array of {curveNumber: x, pointNumber: y} objects, | |
| // return a hash of { | |
| // set1: {value: [key1, key2, ...], _isSimpleKey: false}, | |
| // set2: {value: [key3, key4, ...], _isSimpleKey: false} | |
| // } | |
| function pointsToKeys(points) { | |
| var keysBySet = {}; | |
| for (var i = 0; i < points.length; i++) { | |
| var trace = graphDiv.data[points[i].curveNumber]; | |
| if (!trace.key || !trace.set) { | |
| continue; | |
| } | |
| // set defaults for this keySet | |
| // note that we don't track the nested property (yet) since we always | |
| // emit the union -- http://cpsievert.github.io/talks/20161212b/#21 | |
| keysBySet[trace.set] = keysBySet[trace.set] || { | |
| value: [], | |
| _isSimpleKey: trace._isSimpleKey | |
| }; | |
| // selecting a point of a "simple" trace means: select the | |
| // entire key attached to this trace, which is useful for, | |
| // say clicking on a fitted line to select corresponding observations | |
| var key = trace._isSimpleKey ? trace.key : trace.key[points[i].pointNumber]; | |
| // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript | |
| var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key; | |
| // TODO: better to only add new values? | |
| keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat); | |
| } | |
| return keysBySet; | |
| } | |
| x.highlight.color = x.highlight.color || []; | |
| // make sure highlight color is an array | |
| if (!Array.isArray(x.highlight.color)) { | |
| x.highlight.color = [x.highlight.color]; | |
| } | |
| var traceManager = new TraceManager(graphDiv, x.highlight); | |
| // Gather all *unique* sets. | |
| var allSets = []; | |
| for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) { | |
| var newSet = x.data[curveIdx].set; | |
| if (newSet) { | |
| if (allSets.indexOf(newSet) === -1) { | |
| allSets.push(newSet); | |
| } | |
| } | |
| } | |
| // register event listeners for all sets | |
| for (var i = 0; i < allSets.length; i++) { | |
| var set = allSets[i]; | |
| var selection = new crosstalk.SelectionHandle(set); | |
| var filter = new crosstalk.FilterHandle(set); | |
| var filterChange = function(e) { | |
| removeBrush(el); | |
| traceManager.updateFilter(set, e.value); | |
| }; | |
| filter.on("change", filterChange); | |
| var selectionChange = function(e) { | |
| // array of "event objects" tracking the selection history | |
| // this is used to avoid adding redundant selections | |
| var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || []; | |
| // Construct an event object "defining" the current event. | |
| var event = { | |
| receiverID: traceManager.gd.id, | |
| plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get() | |
| }; | |
| event[set] = e.value; | |
| // TODO: is there a smarter way to check object equality? | |
| if (selectionHistory.length > 0) { | |
| var ev = JSON.stringify(event); | |
| for (var i = 0; i < selectionHistory.length; i++) { | |
| var sel = JSON.stringify(selectionHistory[i]); | |
| if (sel == ev) { | |
| return; | |
| } | |
| } | |
| } | |
| // accumulate history for persistent selection | |
| if (!x.highlight.persistent) { | |
| selectionHistory = [event]; | |
| } else { | |
| selectionHistory.push(event); | |
| } | |
| crosstalk.var("plotlySelectionHistory").set(selectionHistory); | |
| // do the actual updating of traces, frames, and the selectize widget | |
| traceManager.updateSelection(set, e.value); | |
| // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items | |
| if (x.selectize) { | |
| if (!x.highlight.persistent || e.value === null) { | |
| selectize.clear(true); | |
| } | |
| selectize.addItems(e.value, true); | |
| selectize.close(); | |
| } | |
| } | |
| selection.on("change", selectionChange); | |
| // Set a crosstalk variable selection value, triggering an update | |
| graphDiv.on(x.highlight.on, function turnOn(e) { | |
| if (e) { | |
| var selectedKeys = pointsToKeys(e.points); | |
| // Keys are group names, values are array of selected keys from group. | |
| for (var set in selectedKeys) { | |
| if (selectedKeys.hasOwnProperty(set)) { | |
| selection.set(selectedKeys[set].value, {sender: el}); | |
| } | |
| } | |
| } | |
| }); | |
| graphDiv.on(x.highlight.off, function turnOff(e) { | |
| // remove any visual clues | |
| removeBrush(el); | |
| // remove any selection history | |
| crosstalk.var("plotlySelectionHistory").set(null); | |
| // trigger the actual removal of selection traces | |
| selection.set(null, {sender: el}); | |
| }); | |
| // register a callback for selectize so that there is bi-directional | |
| // communication between the widget and direct manipulation events | |
| if (x.selectize) { | |
| var selectizeID = Object.keys(x.selectize)[i]; | |
| var items = x.selectize[selectizeID].items; | |
| var first = [{value: "", label: "(All)"}]; | |
| var opts = { | |
| options: first.concat(items), | |
| searchField: "label", | |
| valueField: "value", | |
| labelField: "label", | |
| maxItems: 50 | |
| }; | |
| var select = $("#" + selectizeID).find("select")[0]; | |
| var selectize = $(select).selectize(opts)[0].selectize; | |
| // NOTE: this callback is triggered when *directly* altering | |
| // dropdown items | |
| selectize.on("change", function() { | |
| var currentItems = traceManager.groupSelections[set] || []; | |
| if (!x.highlight.persistent) { | |
| removeBrush(el); | |
| for (var i = 0; i < currentItems.length; i++) { | |
| selectize.removeItem(currentItems[i], true); | |
| } | |
| } | |
| var newItems = selectize.items.filter(function(idx) { | |
| return currentItems.indexOf(idx) < 0; | |
| }); | |
| if (newItems.length > 0) { | |
| traceManager.updateSelection(set, newItems); | |
| } else { | |
| // Item has been removed... | |
| // TODO: this logic won't work for dynamically changing palette | |
| traceManager.updateSelection(set, null); | |
| traceManager.updateSelection(set, selectize.items); | |
| } | |
| }); | |
| } | |
| } | |
| } // end of renderValue | |
| }); // end of widget definition | |
| /** | |
| * @param graphDiv The Plotly graph div | |
| * @param highlight An object with options for updating selection(s) | |
| */ | |
| function TraceManager(graphDiv, highlight) { | |
| // The Plotly graph div | |
| this.gd = graphDiv; | |
| // Preserve the original data. | |
| // TODO: try using Lib.extendFlat() as done in | |
| // https://github.com/plotly/plotly.js/pull/1136 | |
| this.origData = JSON.parse(JSON.stringify(graphDiv.data)); | |
| // avoid doing this over and over | |
| this.origOpacity = []; | |
| for (var i = 0; i < this.origData.length; i++) { | |
| this.origOpacity[i] = this.origData[i].opacity || 1; | |
| } | |
| // key: group name, value: null or array of keys representing the | |
| // most recently received selection for that group. | |
| this.groupSelections = {}; | |
| // selection parameters (e.g., transient versus persistent selection) | |
| this.highlight = highlight; | |
| } | |
| TraceManager.prototype.close = function() { | |
| // TODO: Unhook all event handlers | |
| }; | |
| TraceManager.prototype.updateFilter = function(group, keys) { | |
| if (typeof(keys) === "undefined" || keys === null) { | |
| this.gd.data = JSON.parse(JSON.stringify(this.origData)); | |
| } else { | |
| var traces = []; | |
| for (var i = 0; i < this.origData.length; i++) { | |
| var trace = this.origData[i]; | |
| if (!trace.key || trace.set !== group) { | |
| continue; | |
| } | |
| var matchFunc = getMatchFunc(trace); | |
| var matches = matchFunc(trace.key, keys); | |
| if (matches.length > 0) { | |
| if (!trace._isSimpleKey) { | |
| // subsetArrayAttrs doesn't mutate trace (it makes a modified clone) | |
| trace = subsetArrayAttrs(trace, matches); | |
| } | |
| traces.push(trace); | |
| } | |
| } | |
| } | |
| this.gd.data = traces; | |
| Plotly.redraw(this.gd); | |
| // NOTE: we purposely do _not_ restore selection(s), since on filter, | |
| // axis likely will update, changing the pixel -> data mapping, leading | |
| // to a likely mismatch in the brush outline and highlighted marks | |
| }; | |
| TraceManager.prototype.updateSelection = function(group, keys) { | |
| if (keys !== null && !Array.isArray(keys)) { | |
| throw new Error("Invalid keys argument; null or array expected"); | |
| } | |
| // if selection has been cleared, or if this is transient | |
| // selection, delete the "selection traces" | |
| var nNewTraces = this.gd.data.length - this.origData.length; | |
| if (keys === null || !this.highlight.persistent && nNewTraces > 0) { | |
| var tracesToRemove = []; | |
| for (var i = this.origData.length; i < this.gd.data.length; i++) { | |
| tracesToRemove.push(i); | |
| } | |
| Plotly.deleteTraces(this.gd, tracesToRemove); | |
| this.groupSelections[group] = keys; | |
| } else { | |
| // add to the groupSelection, rather than overwriting it | |
| // TODO: can this be removed? | |
| this.groupSelections[group] = this.groupSelections[group] || []; | |
| for (var i = 0; i < keys.length; i++) { | |
| var k = keys[i]; | |
| if (this.groupSelections[group].indexOf(k) < 0) { | |
| this.groupSelections[group].push(k); | |
| } | |
| } | |
| } | |
| if (keys === null) { | |
| Plotly.restyle(this.gd, {"opacity": this.origOpacity}); | |
| } else if (keys.length >= 1) { | |
| // placeholder for new "selection traces" | |
| var traces = []; | |
| // this variable is set in R/highlight.R | |
| var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() || | |
| this.highlight.color[0]; | |
| // selection brush attributes | |
| var selectAttrs = Object.keys(this.highlight.selected); | |
| for (var i = 0; i < this.origData.length; i++) { | |
| // TODO: try using Lib.extendFlat() as done in | |
| // https://github.com/plotly/plotly.js/pull/1136 | |
| var trace = JSON.parse(JSON.stringify(this.gd.data[i])); | |
| if (!trace.key || trace.set !== group) { | |
| continue; | |
| } | |
| // Get sorted array of matching indices in trace.key | |
| var matchFunc = getMatchFunc(trace); | |
| var matches = matchFunc(trace.key, keys); | |
| if (matches.length > 0) { | |
| // If this is a "simple" key, that means select the entire trace | |
| if (!trace._isSimpleKey) { | |
| trace = subsetArrayAttrs(trace, matches); | |
| } | |
| // Apply selection brush attributes (supplied from R) | |
| // TODO: it would be neat to have a dropdown to dynamically specify these | |
| for (var j = 0; j < selectAttrs.length; j++) { | |
| var attr = selectAttrs[j]; | |
| trace[attr] = this.highlight.selected[attr]; | |
| } | |
| // if it is defined, override color with the "dynamic brush color"" | |
| // TODO: DRY this up | |
| var d = this.gd._fullData[i]; | |
| if (d.marker) { | |
| trace.marker = trace.marker || {}; | |
| trace.marker.color = selectionColour || trace.marker.color || d.marker.color; | |
| // adopt any user-defined styling for the selection | |
| var selected = this.highlight.selected.marker || {}; | |
| var attrs = Object.keys(selected); | |
| for (var j = 0; j < attrs.length; j++) { | |
| trace.marker[attrs[j]] = selected[attrs[j]]; | |
| } | |
| } | |
| if (d.line) { | |
| trace.line = trace.line || {}; | |
| trace.line.color = selectionColour || trace.line.color || d.line.color; | |
| // adopt any user-defined styling for the selection | |
| var selected = this.highlight.selected.line || {}; | |
| var attrs = Object.keys(selected); | |
| for (var j = 0; j < attrs.length; j++) { | |
| trace.line[attrs[j]] = selected[attrs[j]]; | |
| } | |
| } | |
| if (d.textfont) { | |
| trace.textfont = trace.textfont || {}; | |
| trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color; | |
| // adopt any user-defined styling for the selection | |
| var selected = this.highlight.selected.textfont || {}; | |
| var attrs = Object.keys(selected); | |
| for (var j = 0; j < attrs.length; j++) { | |
| trace.textfont[attrs[j]] = selected[attrs[j]]; | |
| } | |
| } | |
| // attach a sensible name/legendgroup | |
| trace.name = trace.name || keys.join("<br />"); | |
| trace.legendgroup = trace.legendgroup || keys.join("<br />"); | |
| // keep track of mapping between this new trace and the trace it targets | |
| // (necessary for updating frames to reflect the selection traces) | |
| trace._originalIndex = i; | |
| trace._newIndex = this.gd._fullData.length + traces.length; | |
| traces.push(trace); | |
| } | |
| } | |
| if (traces.length > 0) { | |
| Plotly.addTraces(this.gd, traces).then(function(gd) { | |
| // incrementally add selection traces to frames | |
| // (this is heavily inspired by Plotly.Plots.modifyFrames() | |
| // in src/plots/plots.js) | |
| var _hash = gd._transitionData._frameHash; | |
| var _frames = gd._transitionData._frames || []; | |
| for (var i = 0; i < _frames.length; i++) { | |
| // add to _frames[i].traces *if* this frame references selected trace(s) | |
| var newIndices = []; | |
| for (var j = 0; j < traces.length; j++) { | |
| var tr = traces[j]; | |
| if (_frames[i].traces.indexOf(tr._originalIndex) > -1) { | |
| newIndices.push(tr._newIndex); | |
| _frames[i].traces.push(tr._newIndex); | |
| } | |
| } | |
| // nothing to do... | |
| if (newIndices.length === 0) { | |
| continue; | |
| } | |
| var ctr = 0; | |
| var nFrameTraces = _frames[i].data.length; | |
| for (var j = 0; j < nFrameTraces; j++) { | |
| var frameTrace = _frames[i].data[j]; | |
| if (!frameTrace.key || frameTrace.set !== group) { | |
| continue; | |
| } | |
| var matchFunc = getMatchFunc(frameTrace); | |
| var matches = matchFunc(frameTrace.key, keys); | |
| if (matches.length > 0) { | |
| if (!trace._isSimpleKey) { | |
| frameTrace = subsetArrayAttrs(frameTrace, matches); | |
| } | |
| var d = gd._fullData[newIndices[ctr]]; | |
| if (d.marker) { | |
| frameTrace.marker = d.marker; | |
| } | |
| if (d.line) { | |
| frameTrace.line = d.line; | |
| } | |
| if (d.textfont) { | |
| frameTrace.textfont = d.textfont; | |
| } | |
| ctr = ctr + 1; | |
| _frames[i].data.push(frameTrace); | |
| } | |
| } | |
| // update gd._transitionData._frameHash | |
| _hash[_frames[i].name] = _frames[i]; | |
| } | |
| }); | |
| // dim traces that have a set matching the set of selection sets | |
| var tracesToDim = [], | |
| opacities = [], | |
| sets = Object.keys(this.groupSelections), | |
| n = this.origData.length; | |
| for (var i = 0; i < n; i++) { | |
| var opacity = this.origOpacity[i] || 1; | |
| // have we already dimmed this trace? Or is this even worth doing? | |
| if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) { | |
| continue; | |
| } | |
| // is this set an element of the set of selection sets? | |
| var matches = findMatches(sets, [this.gd.data[i].set]); | |
| if (matches.length) { | |
| tracesToDim.push(i); | |
| opacities.push(opacity * this.highlight.opacityDim); | |
| } | |
| } | |
| if (tracesToDim.length > 0) { | |
| Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim); | |
| } | |
| } | |
| } | |
| }; | |
| /* | |
| Note: in all of these match functions, we assume needleSet (i.e. the selected keys) | |
| is a 1D (or flat) array. The real difference is the meaning of haystack. | |
| findMatches() does the usual thing you'd expect for | |
| linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff | |
| haystack is a subset of the needleSet. findNestedMatches() returns | |
| */ | |
| function getMatchFunc(trace) { | |
| return (trace._isNestedKey) ? findNestedMatches : | |
| (trace._isSimpleKey) ? findSimpleMatches : findMatches; | |
| } | |
| // find matches for "flat" keys | |
| function findMatches(haystack, needleSet) { | |
| var matches = []; | |
| haystack.forEach(function(obj, i) { | |
| if (obj === null || needleSet.indexOf(obj) >= 0) { | |
| matches.push(i); | |
| } | |
| }); | |
| return matches; | |
| } | |
| // find matches for "simple" keys | |
| function findSimpleMatches(haystack, needleSet) { | |
| var match = haystack.every(function(val) { | |
| return val === null || needleSet.indexOf(val) >= 0; | |
| }); | |
| // yes, this doesn't make much sense other than conforming | |
| // to the output type of the other match functions | |
| return (match) ? [0] : [] | |
| } | |
| // find matches for a "nested" haystack (2D arrays) | |
| function findNestedMatches(haystack, needleSet) { | |
| var matches = []; | |
| for (var i = 0; i < haystack.length; i++) { | |
| var hay = haystack[i]; | |
| var match = hay.every(function(val) { | |
| return val === null || needleSet.indexOf(val) >= 0; | |
| }); | |
| if (match) { | |
| matches.push(i); | |
| } | |
| } | |
| return matches; | |
| } | |
| function isPlainObject(obj) { | |
| return ( | |
| Object.prototype.toString.call(obj) === '[object Object]' && | |
| Object.getPrototypeOf(obj) === Object.prototype | |
| ); | |
| } | |
| function subsetArrayAttrs(obj, indices) { | |
| var newObj = {}; | |
| Object.keys(obj).forEach(function(k) { | |
| var val = obj[k]; | |
| if (k.charAt(0) === "_") { | |
| newObj[k] = val; | |
| } else if (k === "transforms" && Array.isArray(val)) { | |
| newObj[k] = val.map(function(transform) { | |
| return subsetArrayAttrs(transform, indices); | |
| }); | |
| } else if (k === "colorscale" && Array.isArray(val)) { | |
| newObj[k] = val; | |
| } else if (isPlainObject(val)) { | |
| newObj[k] = subsetArrayAttrs(val, indices); | |
| } else if (Array.isArray(val)) { | |
| newObj[k] = subsetArray(val, indices); | |
| } else { | |
| newObj[k] = val; | |
| } | |
| }); | |
| return newObj; | |
| } | |
| function subsetArray(arr, indices) { | |
| var result = []; | |
| for (var i = 0; i < indices.length; i++) { | |
| result.push(arr[indices[i]]); | |
| } | |
| return result; | |
| } | |
| // Convenience function for removing plotly's brush | |
| function removeBrush(el) { | |
| var outlines = el.querySelectorAll(".select-outline"); | |
| for (var i = 0; i < outlines.length; i++) { | |
| outlines[i].remove(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment