|
-
- HTMLWidgets.widget({
- name: "plotly",
- type: "output",
-
- initialize: function(el, width, height) {
- return {};
- },
-
- resize: function(el, width, height, instance) {
- if (instance.autosize) {
- var width = instance.width || width;
- var height = instance.height || height;
- Plotly.relayout(el.id, {width: width, height: height});
- }
- },
-
- renderValue: function(el, x, instance) {
-
- // Plotly.relayout() mutates the plot input object, so make sure to
- // keep a reference to the user-supplied width/height *before*
- // we call Plotly.plot();
- var lay = x.layout || {};
- instance.width = lay.width;
- instance.height = lay.height;
- instance.autosize = lay.autosize || true;
-
- /*
- / 'inform the world' about highlighting options this is so other
- / crosstalk libraries have a chance to respond to special settings
- / such as persistent selection.
- / AFAIK, leaflet is the only library with such intergration
- / https://github.com/rstudio/leaflet/pull/346/files#diff-ad0c2d51ce5fdf8c90c7395b102f4265R154
- */
- var ctConfig = crosstalk.var('plotlyCrosstalkOpts').set(x.highlight);
-
- if (typeof(window) !== "undefined") {
- // make sure plots don't get created outside the network (for on-prem)
- window.PLOTLYENV = window.PLOTLYENV || {};
- window.PLOTLYENV.BASE_URL = x.base_url;
-
- // Enable persistent selection when shift key is down
- // https://stackoverflow.com/questions/1828613/check-if-a-key-is-down
- var persistOnShift = function(e) {
- if (!e) window.event;
- if (e.shiftKey) {
- x.highlight.persistent = true;
- x.highlight.persistentShift = true;
- } else {
- x.highlight.persistent = false;
- x.highlight.persistentShift = false;
- }
- };
-
- // Only relevant if we haven't forced persistent mode at command line
- if (!x.highlight.persistent) {
- window.onmousemove = persistOnShift;
- }
- }
-
- var graphDiv = document.getElementById(el.id);
-
- // TODO: move the control panel injection strategy inside here...
- HTMLWidgets.addPostRenderHandler(function() {
-
- // lower the z-index of the modebar to prevent it from highjacking hover
- // (TODO: do this via CSS?)
- // https://github.com/ropensci/plotly/issues/956
- // https://www.w3schools.com/jsref/prop_style_zindex.asp
- var modebars = document.querySelectorAll(".js-plotly-plot .plotly .modebar");
- for (var i = 0; i < modebars.length; i++) {
- modebars[i].style.zIndex = 1;
- }
- });
-
- // inject a "control panel" holding selectize/dynamic color widget(s)
- if (x.selectize || x.highlight.dynamic && !instance.plotly) {
- var flex = document.createElement("div");
- flex.class = "plotly-crosstalk-control-panel";
- flex.style = "display: flex; flex-wrap: wrap";
-
- // inject the colourpicker HTML container into the flexbox
- if (x.highlight.dynamic) {
- var pickerDiv = document.createElement("div");
-
- var pickerInput = document.createElement("input");
- pickerInput.id = el.id + "-colourpicker";
- pickerInput.placeholder = "asdasd";
-
- var pickerLabel = document.createElement("label");
- pickerLabel.for = pickerInput.id;
- pickerLabel.innerHTML = "Brush color ";
-
- pickerDiv.appendChild(pickerLabel);
- pickerDiv.appendChild(pickerInput);
- flex.appendChild(pickerDiv);
- }
-
- // inject selectize HTML containers (one for every crosstalk group)
- if (x.selectize) {
- var ids = Object.keys(x.selectize);
-
- for (var i = 0; i < ids.length; i++) {
- var container = document.createElement("div");
- container.id = ids[i];
- container.style = "width: 80%; height: 10%";
- container.class = "form-group crosstalk-input-plotly-highlight";
-
- var label = document.createElement("label");
- label.for = ids[i];
- label.innerHTML = x.selectize[ids[i]].group;
- label.class = "control-label";
-
- var selectDiv = document.createElement("div");
- var select = document.createElement("select");
- select.multiple = true;
-
- selectDiv.appendChild(select);
- container.appendChild(label);
- container.appendChild(selectDiv);
- flex.appendChild(container);
- }
- }
-
- // finally, insert the flexbox inside the htmlwidget container,
- // but before the plotly graph div
- graphDiv.parentElement.insertBefore(flex, graphDiv);
-
- if (x.highlight.dynamic) {
- var picker = $("#" + pickerInput.id);
- var colors = x.highlight.color || [];
- // TODO: let users specify options?
- var opts = {
- value: colors[0],
- showColour: "both",
- palette: "limited",
- allowedCols: colors.join(" "),
- width: "20%",
- height: "10%"
- };
- picker.colourpicker({changeDelay: 0});
- picker.colourpicker("settings", opts);
- picker.colourpicker("value", opts.value);
- // inform crosstalk about a change in the current selection colour
- var grps = x.highlight.ctGroups || [];
- for (var i = 0; i < grps.length; i++) {
- crosstalk.group(grps[i]).var('plotlySelectionColour')
- .set(picker.colourpicker('value'));
- }
- picker.on("change", function() {
- for (var i = 0; i < grps.length; i++) {
- crosstalk.group(grps[i]).var('plotlySelectionColour')
- .set(picker.colourpicker('value'));
- }
- });
- }
- }
-
- // if no plot exists yet, create one with a particular configuration
- if (!instance.plotly) {
-
- var plot = Plotly.plot(graphDiv, x);
- instance.plotly = true;
-
- } else {
-
- // this is essentially equivalent to Plotly.newPlot(), but avoids creating
- // a new webgl context
- // https://github.com/plotly/plotly.js/blob/2b24f9def901831e61282076cf3f835598d56f0e/src/plot_api/plot_api.js#L531-L532
-
- // TODO: restore crosstalk selections?
- Plotly.purge(graphDiv);
- // TODO: why is this necessary to get crosstalk working?
- graphDiv.data = undefined;
- graphDiv.layout = undefined;
- var plot = Plotly.plot(graphDiv, x);
- }
-
- // Trigger plotly.js calls defined via `plotlyProxy()`
- plot.then(function() {
- if (HTMLWidgets.shinyMode) {
- Shiny.addCustomMessageHandler("plotly-calls", function(msg) {
- var gd = document.getElementById(msg.id);
- if (!gd) {
- throw new Error("Couldn't find plotly graph with id: " + msg.id);
- }
- // This isn't an official plotly.js method, but it's the only current way to
- // change just the configuration of a plot
- // https://community.plot.ly/t/update-config-function/9057
- if (msg.method == "reconfig") {
- Plotly.react(gd, gd.data, gd.layout, msg.args);
- return;
- }
- if (!Plotly[msg.method]) {
- throw new Error("Unknown method " + msg.method);
- }
- var args = [gd].concat(msg.args);
- Plotly[msg.method].apply(null, args);
- });
- }
-
- // plotly's mapbox API doesn't currently support setting bounding boxes
- // https://www.mapbox.com/mapbox-gl-js/example/fitbounds/
- // so we do this manually...
- // TODO: make sure this triggers on a redraw and relayout as well as on initial draw
- var mapboxIDs = graphDiv._fullLayout._subplots.mapbox || [];
- for (var i = 0; i < mapboxIDs.length; i++) {
- var id = mapboxIDs[i];
- var mapOpts = x.layout[id] || {};
- var args = mapOpts._fitBounds || {};
- if (!args) {
- continue;
- }
- var mapObj = graphDiv._fullLayout[id]._subplot.map;
- mapObj.fitBounds(args.bounds, args.options);
- }
-
- });
-
- // Attach attributes (e.g., "key", "z") to plotly event data
- function eventDataWithKey(eventData) {
- if (eventData === undefined || !eventData.hasOwnProperty("points")) {
- return null;
- }
- return eventData.points.map(function(pt) {
- var obj = {
- curveNumber: pt.curveNumber,
- pointNumber: pt.pointNumber,
- x: pt.x,
- y: pt.y
- };
-
- // If 'z' is reported with the event data, then use it!
- if (pt.hasOwnProperty("z")) {
- obj.z = pt.z;
- }
-
- if (pt.hasOwnProperty("customdata")) {
- obj.customdata = pt.customdata;
- }
-
- /*
- TL;DR: (I think) we have to select the graph div (again) to attach keys...
-
- Why? Remember that crosstalk will dynamically add/delete traces
- (see traceManager.prototype.updateSelection() below)
- For this reason, we can't simply grab keys from x.data (like we did previously)
- Moreover, we can't use _fullData, since that doesn't include
- unofficial attributes. It's true that click/hover events fire with
- pt.data, but drag events don't...
- */
- var gd = document.getElementById(el.id);
- var trace = gd.data[pt.curveNumber];
-
- if (!trace._isSimpleKey) {
- var attrsToAttach = ["key"];
- } else {
- // simple keys fire the whole key
- obj.key = trace.key;
- var attrsToAttach = [];
- }
-
- for (var i = 0; i < attrsToAttach.length; i++) {
- var attr = trace[attrsToAttach[i]];
- if (Array.isArray(attr)) {
- if (typeof pt.pointNumber === "number") {
- obj[attrsToAttach[i]] = attr[pt.pointNumber];
- } else if (Array.isArray(pt.pointNumber)) {
- obj[attrsToAttach[i]] = attr[pt.pointNumber[0]][pt.pointNumber[1]];
- } else if (Array.isArray(pt.pointNumbers)) {
- obj[attrsToAttach[i]] = pt.pointNumbers.map(function(idx) { return attr[idx]; });
- }
- }
- }
- return obj;
- });
- }
-
-
- var legendEventData = function(d) {
- // if legendgroup is not relevant just return the trace
- var trace = d.data[d.curveNumber];
- if (!trace.legendgroup) return trace;
-
- // if legendgroup was specified, return all traces that match the group
- var legendgrps = d.data.map(function(trace){ return trace.legendgroup; });
- var traces = [];
- for (i = 0; i < legendgrps.length; i++) {
- if (legendgrps[i] == trace.legendgroup) {
- traces.push(d.data[i]);
- }
- }
-
- return traces;
- };
-
-
- // send user input event data to shiny
- if (HTMLWidgets.shinyMode && Shiny.setInputValue) {
-
- // Some events clear other input values
- // TODO: always register these?
- var eventClearMap = {
- plotly_deselect: ["plotly_selected", "plotly_selecting", "plotly_brushed", "plotly_brushing", "plotly_click"],
- plotly_unhover: ["plotly_hover"],
- plotly_doubleclick: ["plotly_click"]
- };
-
- Object.keys(eventClearMap).map(function(evt) {
- graphDiv.on(evt, function() {
- var inputsToClear = eventClearMap[evt];
- inputsToClear.map(function(input) {
- Shiny.setInputValue(input + "-" + x.source, null, {priority: "event"});
- });
- });
- });
-
- var eventDataFunctionMap = {
- plotly_click: eventDataWithKey,
- plotly_sunburstclick: eventDataWithKey,
- plotly_hover: eventDataWithKey,
- plotly_unhover: eventDataWithKey,
- // If 'plotly_selected' has already been fired, and you click
- // on the plot afterwards, this event fires `undefined`?!?
- // That might be considered a plotly.js bug, but it doesn't make
- // sense for this input change to occur if `d` is falsy because,
- // even in the empty selection case, `d` is truthy (an object),
- // and the 'plotly_deselect' event will reset this input
- plotly_selected: function(d) { if (d) { return eventDataWithKey(d); } },
- plotly_selecting: function(d) { if (d) { return eventDataWithKey(d); } },
- plotly_brushed: function(d) {
- if (d) { return d.range ? d.range : d.lassoPoints; }
- },
- plotly_brushing: function(d) {
- if (d) { return d.range ? d.range : d.lassoPoints; }
- },
- plotly_legendclick: legendEventData,
- plotly_legenddoubleclick: legendEventData,
- plotly_clickannotation: function(d) { return d.fullAnnotation }
- };
-
- var registerShinyValue = function(event) {
- var eventDataPreProcessor = eventDataFunctionMap[event] || function(d) { return d ? d : el.id };
- // some events are unique to the R package
- var plotlyJSevent = (event == "plotly_brushed") ? "plotly_selected" : (event == "plotly_brushing") ? "plotly_selecting" : event;
- // register the event
- graphDiv.on(plotlyJSevent, function(d) {
- Shiny.setInputValue(
- event + "-" + x.source,
- JSON.stringify(eventDataPreProcessor(d)),
- {priority: "event"}
- );
- });
- }
-
- var shinyEvents = x.shinyEvents || [];
- shinyEvents.map(registerShinyValue);
- }
-
- // Given an array of {curveNumber: x, pointNumber: y} objects,
- // return a hash of {
- // set1: {value: [key1, key2, ...], _isSimpleKey: false},
- // set2: {value: [key3, key4, ...], _isSimpleKey: false}
- // }
- function pointsToKeys(points) {
- var keysBySet = {};
- for (var i = 0; i < points.length; i++) {
-
- var trace = graphDiv.data[points[i].curveNumber];
- if (!trace.key || !trace.set) {
- continue;
- }
-
- // set defaults for this keySet
- // note that we don't track the nested property (yet) since we always
- // emit the union -- http://cpsievert.github.io/talks/20161212b/#21
- keysBySet[trace.set] = keysBySet[trace.set] || {
- value: [],
- _isSimpleKey: trace._isSimpleKey
- };
-
- // Use pointNumber by default, but aggregated traces should emit pointNumbers
- var ptNum = points[i].pointNumber;
- var hasPtNum = typeof ptNum === "number";
- var ptNum = hasPtNum ? ptNum : points[i].pointNumbers;
-
- // selecting a point of a "simple" trace means: select the
- // entire key attached to this trace, which is useful for,
- // say clicking on a fitted line to select corresponding observations
- var key = trace._isSimpleKey ? trace.key : Array.isArray(ptNum) ? ptNum.map(function(idx) { return trace.key[idx]; }) : trace.key[ptNum];
- // http://stackoverflow.com/questions/10865025/merge-flatten-an-array-of-arrays-in-javascript
- var keyFlat = trace._isNestedKey ? [].concat.apply([], key) : key;
-
- // TODO: better to only add new values?
- keysBySet[trace.set].value = keysBySet[trace.set].value.concat(keyFlat);
- }
-
- return keysBySet;
- }
-
-
- x.highlight.color = x.highlight.color || [];
- // make sure highlight color is an array
- if (!Array.isArray(x.highlight.color)) {
- x.highlight.color = [x.highlight.color];
- }
-
- var traceManager = new TraceManager(graphDiv, x.highlight);
-
- // Gather all *unique* sets.
- var allSets = [];
- for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) {
- var newSet = x.data[curveIdx].set;
- if (newSet) {
- if (allSets.indexOf(newSet) === -1) {
- allSets.push(newSet);
- }
- }
- }
-
- // register event listeners for all sets
- for (var i = 0; i < allSets.length; i++) {
-
- var set = allSets[i];
- var selection = new crosstalk.SelectionHandle(set);
- var filter = new crosstalk.FilterHandle(set);
-
- var filterChange = function(e) {
- removeBrush(el);
- traceManager.updateFilter(set, e.value);
- };
- filter.on("change", filterChange);
-
-
- var selectionChange = function(e) {
-
- // Workaround for 'plotly_selected' now firing previously selected
- // points (in addition to new ones) when holding shift key. In our case,
- // we just want the new keys
- if (x.highlight.on === "plotly_selected" && x.highlight.persistentShift) {
- // https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript
- Array.prototype.diff = function(a) {
- return this.filter(function(i) {return a.indexOf(i) < 0;});
- };
- e.value = e.value.diff(e.oldValue);
- }
-
- // array of "event objects" tracking the selection history
- // this is used to avoid adding redundant selections
- var selectionHistory = crosstalk.var("plotlySelectionHistory").get() || [];
-
- // Construct an event object "defining" the current event.
- var event = {
- receiverID: traceManager.gd.id,
- plotlySelectionColour: crosstalk.group(set).var("plotlySelectionColour").get()
- };
- event[set] = e.value;
- // TODO: is there a smarter way to check object equality?
- if (selectionHistory.length > 0) {
- var ev = JSON.stringify(event);
- for (var i = 0; i < selectionHistory.length; i++) {
- var sel = JSON.stringify(selectionHistory[i]);
- if (sel == ev) {
- return;
- }
- }
- }
-
- // accumulate history for persistent selection
- if (!x.highlight.persistent) {
- selectionHistory = [event];
- } else {
- selectionHistory.push(event);
- }
- crosstalk.var("plotlySelectionHistory").set(selectionHistory);
-
- // do the actual updating of traces, frames, and the selectize widget
- traceManager.updateSelection(set, e.value);
- // https://github.com/selectize/selectize.js/blob/master/docs/api.md#methods_items
- if (x.selectize) {
- if (!x.highlight.persistent || e.value === null) {
- selectize.clear(true);
- }
- selectize.addItems(e.value, true);
- selectize.close();
- }
- }
- selection.on("change", selectionChange);
-
- // Set a crosstalk variable selection value, triggering an update
- var turnOn = function(e) {
- if (e) {
- var selectedKeys = pointsToKeys(e.points);
- // Keys are group names, values are array of selected keys from group.
- for (var set in selectedKeys) {
- if (selectedKeys.hasOwnProperty(set)) {
- selection.set(selectedKeys[set].value, {sender: el});
- }
- }
- }
- };
- if (x.highlight.debounce > 0) {
- turnOn = debounce(turnOn, x.highlight.debounce);
- }
- graphDiv.on(x.highlight.on, turnOn);
-
- graphDiv.on(x.highlight.off, function turnOff(e) {
- // remove any visual clues
- removeBrush(el);
- // remove any selection history
- crosstalk.var("plotlySelectionHistory").set(null);
- // trigger the actual removal of selection traces
- selection.set(null, {sender: el});
- });
-
- // register a callback for selectize so that there is bi-directional
- // communication between the widget and direct manipulation events
- if (x.selectize) {
- var selectizeID = Object.keys(x.selectize)[i];
- var items = x.selectize[selectizeID].items;
- var first = [{value: "", label: "(All)"}];
- var opts = {
- options: first.concat(items),
- searchField: "label",
- valueField: "value",
- labelField: "label",
- maxItems: 50
- };
- var select = $("#" + selectizeID).find("select")[0];
- var selectize = $(select).selectize(opts)[0].selectize;
- // NOTE: this callback is triggered when *directly* altering
- // dropdown items
- selectize.on("change", function() {
- var currentItems = traceManager.groupSelections[set] || [];
- if (!x.highlight.persistent) {
- removeBrush(el);
- for (var i = 0; i < currentItems.length; i++) {
- selectize.removeItem(currentItems[i], true);
- }
- }
- var newItems = selectize.items.filter(function(idx) {
- return currentItems.indexOf(idx) < 0;
- });
- if (newItems.length > 0) {
- traceManager.updateSelection(set, newItems);
- } else {
- // Item has been removed...
- // TODO: this logic won't work for dynamically changing palette
- traceManager.updateSelection(set, null);
- traceManager.updateSelection(set, selectize.items);
- }
- });
- }
- } // end of selectionChange
-
- } // end of renderValue
- }); // end of widget definition
-
- /**
- * @param graphDiv The Plotly graph div
- * @param highlight An object with options for updating selection(s)
- */
- function TraceManager(graphDiv, highlight) {
- // The Plotly graph div
- this.gd = graphDiv;
-
- // Preserve the original data.
- // TODO: try using Lib.extendFlat() as done in
- // https://github.com/plotly/plotly.js/pull/1136
- this.origData = JSON.parse(JSON.stringify(graphDiv.data));
-
- // avoid doing this over and over
- this.origOpacity = [];
- for (var i = 0; i < this.origData.length; i++) {
- this.origOpacity[i] = this.origData[i].opacity === 0 ? 0 : (this.origData[i].opacity || 1);
- }
-
- // key: group name, value: null or array of keys representing the
- // most recently received selection for that group.
- this.groupSelections = {};
-
- // selection parameters (e.g., transient versus persistent selection)
- this.highlight = highlight;
- }
-
- TraceManager.prototype.close = function() {
- // TODO: Unhook all event handlers
- };
-
- TraceManager.prototype.updateFilter = function(group, keys) {
-
- if (typeof(keys) === "undefined" || keys === null) {
-
- this.gd.data = JSON.parse(JSON.stringify(this.origData));
-
- } else {
-
- var traces = [];
- for (var i = 0; i < this.origData.length; i++) {
- var trace = this.origData[i];
- if (!trace.key || trace.set !== group) {
- continue;
- }
- var matchFunc = getMatchFunc(trace);
- var matches = matchFunc(trace.key, keys);
-
- if (matches.length > 0) {
- if (!trace._isSimpleKey) {
- // subsetArrayAttrs doesn't mutate trace (it makes a modified clone)
- trace = subsetArrayAttrs(trace, matches);
- }
- traces.push(trace);
- }
- }
- }
-
- this.gd.data = traces;
- Plotly.redraw(this.gd);
-
- // NOTE: we purposely do _not_ restore selection(s), since on filter,
- // axis likely will update, changing the pixel -> data mapping, leading
- // to a likely mismatch in the brush outline and highlighted marks
-
- };
-
- TraceManager.prototype.updateSelection = function(group, keys) {
-
- if (keys !== null && !Array.isArray(keys)) {
- throw new Error("Invalid keys argument; null or array expected");
- }
-
- // if selection has been cleared, or if this is transient
- // selection, delete the "selection traces"
- var nNewTraces = this.gd.data.length - this.origData.length;
- if (keys === null || !this.highlight.persistent && nNewTraces > 0) {
- var tracesToRemove = [];
- for (var i = 0; i < this.gd.data.length; i++) {
- if (this.gd.data[i]._isCrosstalkTrace) tracesToRemove.push(i);
- }
- Plotly.deleteTraces(this.gd, tracesToRemove);
- this.groupSelections[group] = keys;
- } else {
- // add to the groupSelection, rather than overwriting it
- // TODO: can this be removed?
- this.groupSelections[group] = this.groupSelections[group] || [];
- for (var i = 0; i < keys.length; i++) {
- var k = keys[i];
- if (this.groupSelections[group].indexOf(k) < 0) {
- this.groupSelections[group].push(k);
- }
- }
- }
-
- if (keys === null) {
-
- Plotly.restyle(this.gd, {"opacity": this.origOpacity});
-
- } else if (keys.length >= 1) {
-
- // placeholder for new "selection traces"
- var traces = [];
- // this variable is set in R/highlight.R
- var selectionColour = crosstalk.group(group).var("plotlySelectionColour").get() ||
- this.highlight.color[0];
-
- for (var i = 0; i < this.origData.length; i++) {
- // TODO: try using Lib.extendFlat() as done in
- // https://github.com/plotly/plotly.js/pull/1136
- var trace = JSON.parse(JSON.stringify(this.gd.data[i]));
- if (!trace.key || trace.set !== group) {
- continue;
- }
- // Get sorted array of matching indices in trace.key
- var matchFunc = getMatchFunc(trace);
- var matches = matchFunc(trace.key, keys);
-
- if (matches.length > 0) {
- // If this is a "simple" key, that means select the entire trace
- if (!trace._isSimpleKey) {
- trace = subsetArrayAttrs(trace, matches);
- }
- // reach into the full trace object so we can properly reflect the
- // selection attributes in every view
- var d = this.gd._fullData[i];
-
- /*
- / Recursively inherit selection attributes from various sources,
- / in order of preference:
- / (1) official plotly.js selected attribute
- / (2) highlight(selected = attrs_selected(...))
- */
- // TODO: it would be neat to have a dropdown to dynamically specify these!
- $.extend(true, trace, this.highlight.selected);
-
- // if it is defined, override color with the "dynamic brush color""
- if (d.marker) {
- trace.marker = trace.marker || {};
- trace.marker.color = selectionColour || trace.marker.color || d.marker.color;
- }
- if (d.line) {
- trace.line = trace.line || {};
- trace.line.color = selectionColour || trace.line.color || d.line.color;
- }
- if (d.textfont) {
- trace.textfont = trace.textfont || {};
- trace.textfont.color = selectionColour || trace.textfont.color || d.textfont.color;
- }
- if (d.fillcolor) {
- // TODO: should selectionColour inherit alpha from the existing fillcolor?
- trace.fillcolor = selectionColour || trace.fillcolor || d.fillcolor;
- }
- // attach a sensible name/legendgroup
- trace.name = trace.name || keys.join("<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;
- trace._isCrosstalkTrace = true;
- traces.push(trace);
- }
- }
-
- if (traces.length > 0) {
-
- Plotly.addTraces(this.gd, traces).then(function(gd) {
- // incrementally add selection traces to frames
- // (this is heavily inspired by Plotly.Plots.modifyFrames()
- // in src/plots/plots.js)
- var _hash = gd._transitionData._frameHash;
- var _frames = gd._transitionData._frames || [];
-
- for (var i = 0; i < _frames.length; i++) {
-
- // add to _frames[i].traces *if* this frame references selected trace(s)
- var newIndices = [];
- for (var j = 0; j < traces.length; j++) {
- var tr = traces[j];
- if (_frames[i].traces.indexOf(tr._originalIndex) > -1) {
- newIndices.push(tr._newIndex);
- _frames[i].traces.push(tr._newIndex);
- }
- }
-
- // nothing to do...
- if (newIndices.length === 0) {
- continue;
- }
-
- var ctr = 0;
- var nFrameTraces = _frames[i].data.length;
-
- for (var j = 0; j < nFrameTraces; j++) {
- var frameTrace = _frames[i].data[j];
- if (!frameTrace.key || frameTrace.set !== group) {
- continue;
- }
-
- var matchFunc = getMatchFunc(frameTrace);
- var matches = matchFunc(frameTrace.key, keys);
-
- if (matches.length > 0) {
- if (!trace._isSimpleKey) {
- frameTrace = subsetArrayAttrs(frameTrace, matches);
- }
- var d = gd._fullData[newIndices[ctr]];
- if (d.marker) {
- frameTrace.marker = d.marker;
- }
- if (d.line) {
- frameTrace.line = d.line;
- }
- if (d.textfont) {
- frameTrace.textfont = d.textfont;
- }
- ctr = ctr + 1;
- _frames[i].data.push(frameTrace);
- }
- }
-
- // update gd._transitionData._frameHash
- _hash[_frames[i].name] = _frames[i];
- }
-
- });
-
- // dim traces that have a set matching the set of selection sets
- var tracesToDim = [],
- opacities = [],
- sets = Object.keys(this.groupSelections),
- n = this.origData.length;
-
- for (var i = 0; i < n; i++) {
- var opacity = this.origOpacity[i] || 1;
- // have we already dimmed this trace? Or is this even worth doing?
- if (opacity !== this.gd._fullData[i].opacity || this.highlight.opacityDim === 1) {
- continue;
- }
- // is this set an element of the set of selection sets?
- var matches = findMatches(sets, [this.gd.data[i].set]);
- if (matches.length) {
- tracesToDim.push(i);
- opacities.push(opacity * this.highlight.opacityDim);
- }
- }
-
- if (tracesToDim.length > 0) {
- Plotly.restyle(this.gd, {"opacity": opacities}, tracesToDim);
- // turn off the selected/unselected API
- Plotly.restyle(this.gd, {"selectedpoints": null});
- }
-
- }
-
- }
- };
-
- /*
- Note: in all of these match functions, we assume needleSet (i.e. the selected keys)
- is a 1D (or flat) array. The real difference is the meaning of haystack.
- findMatches() does the usual thing you'd expect for
- linked brushing on a scatterplot matrix. findSimpleMatches() returns a match iff
- haystack is a subset of the needleSet. findNestedMatches() returns
- */
-
- function getMatchFunc(trace) {
- return (trace._isNestedKey) ? findNestedMatches :
- (trace._isSimpleKey) ? findSimpleMatches : findMatches;
- }
-
- // find matches for "flat" keys
- function findMatches(haystack, needleSet) {
- var matches = [];
- haystack.forEach(function(obj, i) {
- if (obj === null || needleSet.indexOf(obj) >= 0) {
- matches.push(i);
- }
- });
- return matches;
- }
-
- // find matches for "simple" keys
- function findSimpleMatches(haystack, needleSet) {
- var match = haystack.every(function(val) {
- return val === null || needleSet.indexOf(val) >= 0;
- });
- // yes, this doesn't make much sense other than conforming
- // to the output type of the other match functions
- return (match) ? [0] : []
- }
-
- // find matches for a "nested" haystack (2D arrays)
- function findNestedMatches(haystack, needleSet) {
- var matches = [];
- for (var i = 0; i < haystack.length; i++) {
- var hay = haystack[i];
- var match = hay.every(function(val) {
- return val === null || needleSet.indexOf(val) >= 0;
- });
- if (match) {
- matches.push(i);
- }
- }
- return matches;
- }
-
- function isPlainObject(obj) {
- return (
- Object.prototype.toString.call(obj) === '[object Object]' &&
- Object.getPrototypeOf(obj) === Object.prototype
- );
- }
-
- function subsetArrayAttrs(obj, indices) {
- var newObj = {};
- Object.keys(obj).forEach(function(k) {
- var val = obj[k];
-
- if (k.charAt(0) === "_") {
- newObj[k] = val;
- } else if (k === "transforms" && Array.isArray(val)) {
- newObj[k] = val.map(function(transform) {
- return subsetArrayAttrs(transform, indices);
- });
- } else if (k === "colorscale" && Array.isArray(val)) {
- newObj[k] = val;
- } else if (isPlainObject(val)) {
- newObj[k] = subsetArrayAttrs(val, indices);
- } else if (Array.isArray(val)) {
- newObj[k] = subsetArray(val, indices);
- } else {
- newObj[k] = val;
- }
- });
- return newObj;
- }
-
- function subsetArray(arr, indices) {
- var result = [];
- for (var i = 0; i < indices.length; i++) {
- result.push(arr[indices[i]]);
- }
- return result;
- }
-
- // Convenience function for removing plotly's brush
- function removeBrush(el) {
- var outlines = el.querySelectorAll(".select-outline");
- for (var i = 0; i < outlines.length; i++) {
- outlines[i].remove();
- }
- }
-
-
- // https://davidwalsh.name/javascript-debounce-function
-
- // Returns a function, that, as long as it continues to be invoked, will not
- // be triggered. The function will be called after it stops being called for
- // N milliseconds. If `immediate` is passed, trigger the function on the
- // leading edge, instead of the trailing.
- function debounce(func, wait, immediate) {
- var timeout;
- return function() {
- var context = this, args = arguments;
- var later = function() {
- timeout = null;
- if (!immediate) func.apply(context, args);
- };
- var callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) func.apply(context, args);
- };
- };
|