Skip to content

Commit 7ec2ec3

Browse files
committed
Allow a plot to have traces with differing crosstalk sets (groups)
1 parent 537ecb1 commit 7ec2ec3

File tree

3 files changed

+122
-75
lines changed

3 files changed

+122
-75
lines changed

R/plotly.R

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,15 @@ plot_ly <- function(data = data.frame(), ..., type = "scatter",
8787
if (!missing(symbol)) argz$symbol <- substitute(symbol)
8888
if (!missing(symbols)) argz$symbols <- substitute(symbols)
8989
if (!missing(size)) argz$size <- substitute(size)
90+
if (!missing(key)) argz$key <- substitute(key)
91+
if (!missing(set)) argz$set <- substitute(set)
9092
# trace information
9193
tr <- list(
9294
type = type,
9395
args = argz,
9496
env = list2env(data), # environment in which to evaluate arguments
9597
enclos = parent.frame(), # if objects aren't found in env, look here
96-
inherit = inherit,
97-
key = key
98+
inherit = inherit
9899
)
99100
# plotly objects should always have a _list_ of trace(s)
100101
p <- list(
@@ -103,7 +104,6 @@ plot_ly <- function(data = data.frame(), ..., type = "scatter",
103104
url = NULL,
104105
width = width,
105106
height = height,
106-
set = set,
107107
source = source
108108
)
109109

@@ -134,7 +134,7 @@ plot_ly <- function(data = data.frame(), ..., type = "scatter",
134134
#' @author Carson Sievert
135135
#' @export
136136
add_trace <- function(p = last_plot(), ...,
137-
group, color, colors, symbol, symbols, size,
137+
group, color, colors, symbol, symbols, size, key, set,
138138
data = NULL, evaluate = FALSE) {
139139
# "native" plotly arguments
140140
argz <- substitute(list(...))
@@ -145,6 +145,8 @@ add_trace <- function(p = last_plot(), ...,
145145
if (!missing(symbol)) argz$symbol <- substitute(symbol)
146146
if (!missing(symbols)) argz$symbols <- substitute(symbols)
147147
if (!missing(size)) argz$size <- substitute(size)
148+
if (!missing(key)) argz$key <- substitute(key)
149+
if (!missing(set)) argz$set <- substitute(set)
148150
data <- data %||% if (is.data.frame(p)) p else list()
149151
tr <- list(
150152
args = argz,

inst/examples/shiny-crosstalk/app.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ server <- function(input, output, session) {
2626
output$p1 <- renderPlotly({
2727
plot_ly(mtcars, x = wt, y = mpg, color = cyl, mode = "markers",
2828
key = mtcars$rowname, set = "A", height = "100%") %>%
29+
add_trace(x = wt, y = hp, mode = "markers", key = mtcars$rowname, set = "B") %>%
2930
layout(dragmode = "select")
3031
})
3132

3233
output$p2 <- renderPlotly({
3334
plot_ly(mtcars, x = wt, y = disp, color = cyl, mode = "markers",
3435
key = mtcars$rowname, set = "A", height = "100%") %>%
36+
add_trace(x = wt, y = hp, mode = "markers", key = mtcars$rowname, set = "B") %>%
3537
layout(dragmode = "select")
3638
})
3739

inst/htmlwidgets/plotly.js

Lines changed: 114 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -93,31 +93,43 @@ HTMLWidgets.widget({
9393
// The pointsToKeys and keysToPoints functions let you convert
9494
// between the two schemes.
9595

96-
// To allow translation from keys to points in O(1) time, we
96+
// Combine the name of a set and key into a single string, suitable for
97+
// using as a keyCache key.
98+
function joinSetAndKey(set, key) {
99+
return set + "\n" + key;
100+
}
101+
102+
// To allow translation from sets+keys to points in O(1) time, we
97103
// make a cache that lets us map keys to objects with
98104
// {curveNumber, pointNumber} properties.
99105
var keyCache = {};
100106
for (var curve = 0; curve < x.data.length; curve++) {
101-
if (!x.data[curve].key) {
107+
var curveObj = x.data[curve];
108+
if (!curveObj.key || !curveObj.set) {
102109
continue;
103110
}
104-
for (var pointIdx = 0; pointIdx < x.data[curve].key.length; pointIdx++) {
105-
keyCache[x.data[curve].key[pointIdx]] = {curveNumber: curve, pointNumber: pointIdx};
111+
for (var pointIdx = 0; pointIdx < curveObj.key.length; pointIdx++) {
112+
keyCache[joinSetAndKey(curveObj.set, curveObj.key[pointIdx])] =
113+
{curveNumber: curve, pointNumber: pointIdx};
106114
}
107115
}
108116

109117
// Given an array of {curveNumber: x, pointNumber: y} objects,
110-
// return an array of key strings.
111-
// TODO: Throw proper error if any point is invalid or doesn't
112-
// have a key?
118+
// return a hash of {[set1]: [key1, key2, ...], [set2]: [key3, key4, ...]}
113119
function pointsToKeys(points) {
114-
var keys = [];
120+
var keysBySet = {};
115121
for (var i = 0; i < points.length; i++) {
122+
var curveObj = graphDiv.data[points[i].curveNumber];
123+
if (!curveObj.key || !curveObj.set) {
124+
// If this curve isn't mapped to a set, ignore this point.
125+
continue;
126+
}
116127
// Look up the keys
117-
var key = graphDiv.data[points[i].curveNumber].key[points[i].pointNumber];
118-
keys.push(key);
128+
var key = curveObj.key[points[i].pointNumber];
129+
keysBySet[curveObj.set] = keysBySet[curveObj.set] || [];
130+
keysBySet[curveObj.set].push(key);
119131
}
120-
return keys;
132+
return keysBySet;
121133
}
122134

123135
// Given an array of strings, return an object that hierarchically
@@ -136,10 +148,10 @@ HTMLWidgets.widget({
136148
// "0": [1, 2],
137149
// "2": [1]
138150
// }
139-
function keysToPoints(keys) {
151+
function keysToPoints(set, keys) {
140152
var curves = {};
141153
for (var i = 0; i < keys.length; i++) {
142-
var pt = keyCache[keys[i]];
154+
var pt = keyCache[joinSetAndKey(set, keys[i])];
143155
if (!pt) {
144156
throw new Error("Unknown key " + keys[i]);
145157
}
@@ -148,84 +160,115 @@ HTMLWidgets.widget({
148160
}
149161
return curves;
150162
}
163+
164+
// Gather all sets.
165+
var crosstalkGroups = {};
166+
var allSets = [];
167+
for (var curveIdx = 0; curveIdx < x.data.length; curveIdx++) {
168+
if (x.data[curveIdx].set) {
169+
if (!crosstalkGroups[x.data[curveIdx].set]) {
170+
allSets.push(x.data[curveIdx].set);
171+
crosstalkGroups[x.data[curveIdx].set] = [];
172+
}
173+
crosstalkGroups[x.data[curveIdx].set].push(curveIdx);
174+
}
175+
}
151176

152-
// Grab the specified crosstalk group.
153-
if (x.set) {
154-
var grp = crosstalk.group(x.set);
155-
177+
if (allSets.length > 0) {
156178
// When plotly selection changes, update crosstalk
157179
graphDiv.on("plotly_selected", function plotly_selecting(e) {
158180
if (e) {
159181
var selectedKeys = pointsToKeys(e.points);
160-
grp.var("selection").set(selectedKeys, {sender: el});
182+
// Keys are group names, values are array of selected keys from group.
183+
for (var set in selectedKeys) {
184+
if (selectedKeys.hasOwnProperty(set))
185+
crosstalk.group(set).var("selection").set(selectedKeys[set], {sender: el});
186+
}
187+
// Any groups that weren't represented in the selection, should be
188+
// treated as if zero points were selected.
189+
for (var i = 0; i < allSets.length; i++) {
190+
if (!selectedKeys[allSets[i]]) {
191+
crosstalk.group(allSets[i]).var("selection").set([], {sender: el});
192+
}
193+
}
161194
}
162195
});
163196
// When plotly selection is cleared, update crosstalk
164197
graphDiv.on("plotly_deselect", function plotly_deselect(e) {
165-
grp.var("selection").set(null);
166-
});
167-
168-
// When crosstalk selection changes, update plotly style
169-
grp.var("selection").on("change", function crosstalk_sel_change(e) {
170-
// e.value is either null, or an array of newly selected values
171-
172-
if (e.sender !== el) {
173-
// If we're not the originator of this selection, and we have an
174-
// active selection outline box, we need to remove it. Otherwise
175-
// it could appear like there are two active brushes in one plot
176-
// group.
177-
var outlines = el.querySelectorAll(".select-outline");
178-
for (var i = 0; i < outlines.length; i++) {
179-
outlines[i].remove();
180-
}
198+
for (var i = 0; i < allSets.length; i++) {
199+
crosstalk.group(allSets[i]).var("selection").set(null, {sender: el});
181200
}
182-
183-
// Restyle each relevant trace
184-
var selectedPoints = keysToPoints(e.value || []);
185-
186-
var opacityTraces = [];
187-
var traceIndices = [];
188-
189-
for (var i = 0; i < x.data.length; i++) {
190-
if (!x.data[i].key) {
191-
// Not a brushable trace apparently. Don't restyle.
192-
continue;
193-
}
194-
195-
// Make an opacity array, one element for each data point
196-
// in this trace.
197-
var opacity = new Array(x.data[i].x.length);
198-
199-
if (typeof(e.value) === "undefined" || e.value === null) {
200-
// The Crosstalk selection has been cleared. Full opacity
201-
for (var k = 0; k < opacity.length; k++) {
202-
opacity[k] = 1;
201+
});
202+
203+
for (var i = 0; i < allSets.length; i++) {
204+
(function() {
205+
var set = allSets[i];
206+
var grp = crosstalk.group(set);
207+
208+
// When crosstalk selection changes, update plotly style
209+
grp.var("selection").on("change", function crosstalk_sel_change(e) {
210+
// e.value is either null, or an array of newly selected values
211+
212+
if (e.sender !== el) {
213+
// If we're not the originator of this selection, and we have an
214+
// active selection outline box, we need to remove it. Otherwise
215+
// it could appear like there are two active brushes in one plot
216+
// group.
217+
var outlines = el.querySelectorAll(".select-outline");
218+
for (var i = 0; i < outlines.length; i++) {
219+
outlines[i].remove();
220+
}
203221
}
204-
} else {
205-
// Array of pointNumber numbers that should be highlighted
206-
var theseSelectedPoints = selectedPoints[i] || [];
207222

208-
for (var j = 0; j < opacity.length; j++) {
209-
if (theseSelectedPoints.indexOf(j) >= 0) {
210-
opacity[j] = 1;
223+
// Restyle each relevant trace
224+
var selectedPoints = keysToPoints(set, e.value || []);
225+
226+
var opacityTraces = [];
227+
var relevantTraces = crosstalkGroups[set];
228+
229+
for (var i = 0; i < relevantTraces.length; i++) {
230+
var trace = x.data[relevantTraces[i]];
231+
232+
// Make an opacity array, one element for each data point
233+
// in this trace.
234+
var opacity = new Array(trace.x.length);
235+
236+
if (typeof(e.value) === "undefined" || e.value === null) {
237+
// The Crosstalk selection has been cleared. Full opacity
238+
for (var k = 0; k < opacity.length; k++) {
239+
opacity[k] = 1;
240+
}
211241
} else {
212-
opacity[j] = 0.2;
242+
// Array of pointNumber numbers that should be highlighted
243+
var theseSelectedPoints = selectedPoints[relevantTraces[i]] || [];
244+
245+
for (var j = 0; j < opacity.length; j++) {
246+
if (theseSelectedPoints.indexOf(j) >= 0) {
247+
opacity[j] = 1;
248+
} else {
249+
opacity[j] = 0.2;
250+
}
251+
}
213252
}
253+
254+
opacityTraces.push(opacity);
214255
}
215-
}
216-
217-
opacityTraces.push(opacity);
218-
traceIndices.push(i);
219-
}
220-
// Restyle the current trace
221-
Plotly.restyle(graphDiv, {"marker.opacity": opacityTraces}, traceIndices);
222-
});
256+
console.log(graphDiv.id, relevantTraces, opacityTraces)
257+
// Restyle the current trace
258+
Plotly.restyle(graphDiv, {"marker.opacity": opacityTraces}, relevantTraces);
259+
});
260+
261+
// Remove event listeners in the future
262+
instance.onNextRender.push(function() {
263+
grp.removeListener("selection", crosstalk_sel_change);
264+
});
265+
})();
266+
}
223267

224268
// Remove event listeners in the future
225269
instance.onNextRender.push(function() {
226270
graphDiv.removeListener("plotly_selecting", plotly_selecting);
227271
graphDiv.removeListener("plotly_deselect", plotly_deselect);
228-
grp.removeListener("selection", crosstalk_sel_change);
229272
});
230273
}
231274

0 commit comments

Comments
 (0)