Skip to content

Pie aggregation and event cleanup #2117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 26, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions src/components/fx/helpers.js
Original file line number Diff line number Diff line change
@@ -89,7 +89,8 @@ exports.quadrature = function quadrature(dx, dy) {
*
* @param {object} pointData : point data object (gets mutated here)
* @param {object} trace : full trace object
* @param {number} pointNumber : point number
* @param {number|Array(number)} pointNumber : point number. May be a length-2 array
* [row, col] to dig into 2D arrays
*/
exports.appendArrayPointValue = function(pointData, trace, pointNumber) {
var arrayAttrs = trace._arrayAttrs;
@@ -100,22 +101,68 @@ exports.appendArrayPointValue = function(pointData, trace, pointNumber) {

for(var i = 0; i < arrayAttrs.length; i++) {
var astr = arrayAttrs[i];
var key;
var key = getPointKey(astr);

if(astr === 'ids') key = 'id';
else if(astr === 'locations') key = 'location';
else key = astr;
if(pointData[key] === undefined) {
var val = Lib.nestedProperty(trace, astr).get();
var pointVal = getPointData(val, pointNumber);

if(pointVal !== undefined) pointData[key] = pointVal;
}
}
};

/**
* Appends values inside array attributes corresponding to given point number array
* For use when pointData references a plot entity that arose (or potentially arose)
* from multiple points in the input data
*
* @param {object} pointData : point data object (gets mutated here)
* @param {object} trace : full trace object
* @param {Array(number)|Array(Array(number))} pointNumbers : Array of point numbers.
* Each entry in the array may itself be a length-2 array [row, col] to dig into 2D arrays
*/
exports.appendArrayMultiPointValues = function(pointData, trace, pointNumbers) {
var arrayAttrs = trace._arrayAttrs;

if(!arrayAttrs) {
return;
}

for(var i = 0; i < arrayAttrs.length; i++) {
var astr = arrayAttrs[i];
var key = getPointKey(astr);

if(pointData[key] === undefined) {
var val = Lib.nestedProperty(trace, astr).get();
var keyVal = new Array(pointNumbers.length);

if(Array.isArray(pointNumber)) {
if(Array.isArray(val) && Array.isArray(val[pointNumber[0]])) {
pointData[key] = val[pointNumber[0]][pointNumber[1]];
}
} else {
pointData[key] = val[pointNumber];
for(var j = 0; j < pointNumbers.length; j++) {
keyVal[j] = getPointData(val, pointNumbers[j]);
}
pointData[key] = keyVal;
}
}
};

var pointKeyMap = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ at some point we might want to move this to the attribute declarations.

ids: 'id',
locations: 'location',
labels: 'label',
values: 'value',
'marker.colors': 'color'
};

function getPointKey(astr) {
return pointKeyMap[astr] || astr;
}

function getPointData(val, pointNumber) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This thing is very similar to Lib.castOption. I wonder if we could combine them? Note that getPointData should not fallback to trace-wide properties.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the multipoint case it's nice to keep the nestedProperty call out of this function, as that would slow it down dramatically. Between that and the fact that, as you mention, this one does not return trace-wide properties (it would return undefined but in fact it never gets there as it's iterating over arrayAttrs) these functions seem fairly distinct - though perhaps Lib.castOption could call this one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it might be nice to move getPointData to Lib at some point, but let's keep this as is for now.

if(Array.isArray(pointNumber)) {
if(Array.isArray(val) && Array.isArray(val[pointNumber[0]])) {
return val[pointNumber[0]][pointNumber[1]];
}
} else {
return val[pointNumber];
}
}
8 changes: 0 additions & 8 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
@@ -155,14 +155,6 @@ exports.loneHover = function loneHover(hoverItem, opts) {

// The actual implementation is here:
function _hover(gd, evt, subplot, noHoverEvent) {
if((subplot === 'pie' || subplot === 'sankey') && !noHoverEvent) {
gd.emit('plotly_hover', {
event: evt.originalEvent,
points: [evt]
});
return;
}

if(!subplot) subplot = 'xy';

// if the user passed in an array of subplots,
13 changes: 11 additions & 2 deletions src/traces/pie/attributes.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,13 @@ module.exports = {
labels: {
valType: 'data_array',
editType: 'calc',
description: 'Sets the sector labels.'
description: [
'Sets the sector labels.',
'If `labels` entries are duplicated, we sum associated `values`',
'or simply count occurrences if `values` is not provided.',
'For other array attributes (including color) we use the first',
'non-empty entry among all occurrences of the label.'
].join(' ')
},
// equivalent of x0 and dx, if label is missing
label0: {
@@ -50,7 +56,10 @@ module.exports = {
values: {
valType: 'data_array',
editType: 'calc',
description: 'Sets the values of the sectors of this pie chart.'
description: [
'Sets the values of the sectors of this pie chart.',
'If omitted, we count occurrences of each label.'
].join(' ')
},

marker: {
137 changes: 78 additions & 59 deletions src/traces/pie/calc.js
Original file line number Diff line number Diff line change
@@ -15,21 +15,18 @@ var Color = require('../../components/color');
var helpers = require('./helpers');

module.exports = function calc(gd, trace) {
var vals = trace.values,
labels = trace.labels,
cd = [],
fullLayout = gd._fullLayout,
colorMap = fullLayout._piecolormap,
allThisTraceLabels = {},
needDefaults = false,
vTotal = 0,
hiddenLabels = fullLayout.hiddenlabels || [],
i,
v,
label,
color,
hidden,
pt;
var vals = trace.values;
var hasVals = Array.isArray(vals) && vals.length;
var labels = trace.labels;
var colors = trace.marker.colors;
var cd = [];
var fullLayout = gd._fullLayout;
var colorMap = fullLayout._piecolormap;
var allThisTraceLabels = {};
var vTotal = 0;
var hiddenLabels = fullLayout.hiddenlabels || [];

var i, v, label, hidden, pt;

if(trace.dlabel) {
labels = new Array(vals.length);
@@ -38,46 +35,60 @@ module.exports = function calc(gd, trace) {
}
}

for(i = 0; i < vals.length; i++) {
v = vals[i];
if(!isNumeric(v)) continue;
v = +v;
if(v < 0) continue;
function pullColor(color, label) {
if(!color) return false;

color = tinycolor(color);
if(!color.isValid()) return false;

color = Color.addOpacity(color, color.getAlpha());
if(!colorMap[label]) colorMap[label] = color;

return color;
}

var seriesLen = (hasVals ? vals : labels).length;

for(i = 0; i < seriesLen; i++) {
if(hasVals) {
v = vals[i];
if(!isNumeric(v)) continue;
v = +v;
if(v < 0) continue;
}
else v = 1;

label = labels[i];
if(label === undefined || label === '') label = i;
label = String(label);
// only take the first occurrence of any given label.
// TODO: perhaps (optionally?) sum values for a repeated label?
if(allThisTraceLabels[label] === undefined) allThisTraceLabels[label] = true;
else continue;

color = tinycolor(trace.marker.colors[i]);
if(color.isValid()) {
color = Color.addOpacity(color, color.getAlpha());
if(!colorMap[label]) {
colorMap[label] = color;
}
}
// have we seen this label and assigned a color to it in a previous trace?
else if(colorMap[label]) color = colorMap[label];
// color needs a default - mark it false, come back after sorting
else {
color = false;
needDefaults = true;
}

hidden = hiddenLabels.indexOf(label) !== -1;
var thisLabelIndex = allThisTraceLabels[label];
if(thisLabelIndex === undefined) {
allThisTraceLabels[label] = cd.length;

if(!hidden) vTotal += v;
hidden = hiddenLabels.indexOf(label) !== -1;

cd.push({
v: v,
label: label,
color: color,
i: i,
hidden: hidden
});
if(!hidden) vTotal += v;

cd.push({
v: v,
label: label,
color: pullColor(colors[i]),
i: i,
pts: [i],
hidden: hidden
});
}
else {
pt = cd[thisLabelIndex];
pt.v += v;
pt.pts.push(i);
if(!pt.hidden) vTotal += v;

if(pt.color === false && colors[i]) {
pt.color = pullColor(colors[i], label);
}
}
}

if(trace.sort) cd.sort(function(a, b) { return b.v - a.v; });
@@ -88,10 +99,14 @@ module.exports = function calc(gd, trace) {
* in the order slices will be displayed
*/

if(needDefaults) {
for(i = 0; i < cd.length; i++) {
pt = cd[i];
if(pt.color === false) {
for(i = 0; i < cd.length; i++) {
pt = cd[i];
if(pt.color === false) {
// have we seen this label and assigned a color to it in a previous trace?
if(colorMap[pt.label]) {
pt.color = colorMap[pt.label];
}
else {
colorMap[pt.label] = pt.color = nextDefaultColor(fullLayout._piedefaultcolorcount);
fullLayout._piedefaultcolorcount++;
}
@@ -103,17 +118,21 @@ module.exports = function calc(gd, trace) {

// now insert text
if(trace.textinfo && trace.textinfo !== 'none') {
var hasLabel = trace.textinfo.indexOf('label') !== -1,
hasText = trace.textinfo.indexOf('text') !== -1,
hasValue = trace.textinfo.indexOf('value') !== -1,
hasPercent = trace.textinfo.indexOf('percent') !== -1,
separators = fullLayout.separators,
thisText;
var hasLabel = trace.textinfo.indexOf('label') !== -1;
var hasText = trace.textinfo.indexOf('text') !== -1;
var hasValue = trace.textinfo.indexOf('value') !== -1;
var hasPercent = trace.textinfo.indexOf('percent') !== -1;
var separators = fullLayout.separators;

var thisText;

for(i = 0; i < cd.length; i++) {
pt = cd[i];
thisText = hasLabel ? [pt.label] : [];
if(hasText && trace.text[pt.i]) thisText.push(trace.text[pt.i]);
if(hasText) {
var texti = helpers.getFirstFilled(trace.text, pt.pts);
if(texti) thisText.push(texti);
}
if(hasValue) thisText.push(helpers.formatPieValue(pt.v, separators));
if(hasPercent) thisText.push(helpers.formatPiePercent(pt.v / vTotal, separators));
pt.text = thisText.join('<br>');
27 changes: 8 additions & 19 deletions src/traces/pie/defaults.js
Original file line number Diff line number Diff line change
@@ -19,13 +19,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
var coerceFont = Lib.coerceFont;

var vals = coerce('values');
if(!Array.isArray(vals) || !vals.length) {
traceOut.visible = false;
return;
}

var labels = coerce('labels');
if(!Array.isArray(labels)) {
if(!Array.isArray(vals) || !vals.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: Would you mind locking this down in a jasmine test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure -> ee1f8c4
It was done in the image test I added, but nice to have it in jasmine too.

// must have at least one of vals or labels
traceOut.visible = false;
return;
}

coerce('label0');
coerce('dlabel');
}
@@ -34,14 +35,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
if(lineWidth) coerce('marker.line.color');

var colors = coerce('marker.colors');
if(!Array.isArray(colors)) traceOut.marker.colors = []; // later this will get padded with default colors
if(!Array.isArray(colors)) traceOut.marker.colors = [];

coerce('scalegroup');
// TODO: tilt, depth, and hole all need to be coerced to the same values within a scaleegroup
// (ideally actually, depth would get set the same *after* scaling, ie the same absolute depth)
// and if colors aren't specified we should match these up - potentially even if separate pies
// are NOT in the same sharegroup

// TODO: hole needs to be coerced to the same value within a scaleegroup

var textData = coerce('text');
var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent');
@@ -63,14 +60,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('domain.x');
coerce('domain.y');

// 3D attributes commented out until I finish them in a later PR
// var tilt = coerce('tilt');
// if(tilt) {
// coerce('tiltaxis');
// coerce('depth');
// coerce('shading');
// }

coerce('hole');

coerce('sort');
41 changes: 41 additions & 0 deletions src/traces/pie/event_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

var appendArrayMultiPointValues = require('../../components/fx/helpers').appendArrayMultiPointValues;


// Note: like other eventData routines, this creates the data for hover/unhover/click events
// but it has a different API and goes through a totally different pathway.
// So to ensure it doesn't get misused, it's not attached to the Pie module.
module.exports = function eventData(pt, trace) {
var out = {
curveNumber: trace.index,
pointNumbers: pt.pts,
data: trace._input,
fullData: trace,
label: pt.label,
color: pt.color,
value: pt.v,

// pt.v (and pt.i below) for backward compatibility
v: pt.v
};

// Only include pointNumber if it's unambiguous
if(pt.pts.length === 1) out.pointNumber = out.i = pt.pts[0];

// Add extra data arrays to the output
// notice that this is the multi-point version ('s' on the end!)
// so added data will be arrays matching the pointNumbers array.
appendArrayMultiPointValues(out, trace, pt.pts);

return out;
};
13 changes: 13 additions & 0 deletions src/traces/pie/helpers.js
Original file line number Diff line number Diff line change
@@ -25,3 +25,16 @@ exports.formatPieValue = function formatPieValue(v, separators) {
}
return Lib.numSeparate(vRounded, separators);
};

exports.getFirstFilled = function getFirstFilled(array, indices) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For arrayOk attributes, as well as slice color, I chose to take the first non-empty item corresponding to each label. I suppose in principle we could refine that further to the first valid item for each label?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picking the first non-empty item is fine 👍

if(!Array.isArray(array)) return;
for(var i = 0; i < indices.length; i++) {
var v = array[indices[i]];
if(v || v === 0) return v;
}
};

exports.castOption = function castOption(item, indices) {
if(Array.isArray(item)) return exports.getFirstFilled(item, indices);
else if(item) return item;
};
411 changes: 183 additions & 228 deletions src/traces/pie/plot.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/traces/pie/style.js
Original file line number Diff line number Diff line change
@@ -14,13 +14,13 @@ var styleOne = require('./style_one');

module.exports = function style(gd) {
gd._fullLayout._pielayer.selectAll('.trace').each(function(cd) {
var cd0 = cd[0],
trace = cd0.trace,
traceSelection = d3.select(this);
var cd0 = cd[0];
var trace = cd0.trace;
var traceSelection = d3.select(this);

traceSelection.style({opacity: trace.opacity});

traceSelection.selectAll('.top path.surface').each(function(pt) {
traceSelection.selectAll('path.surface').each(function(pt) {
d3.select(this).call(styleOne, pt, trace);
});
});
13 changes: 6 additions & 7 deletions src/traces/pie/style_one.js
Original file line number Diff line number Diff line change
@@ -9,15 +9,14 @@
'use strict';

var Color = require('../../components/color');
var castOption = require('./helpers').castOption;

module.exports = function styleOne(s, pt, trace) {
var lineColor = trace.marker.line.color;
if(Array.isArray(lineColor)) lineColor = lineColor[pt.i] || Color.defaultLine;

var lineWidth = trace.marker.line.width || 0;
if(Array.isArray(lineWidth)) lineWidth = lineWidth[pt.i] || 0;
var line = trace.marker.line;
var lineColor = castOption(line.color, pt.pts) || Color.defaultLine;
var lineWidth = castOption(line.width, pt.pts) || 0;

s.style({'stroke-width': lineWidth})
.call(Color.fill, pt.color)
.call(Color.stroke, lineColor);
.call(Color.fill, pt.color)
.call(Color.stroke, lineColor);
};
14 changes: 8 additions & 6 deletions src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
@@ -128,10 +128,11 @@ module.exports = function plot(gd, calcData) {
};

var linkHover = function(element, d, sankey) {
var evt = d.link;
evt.originalEvent = d3.event;
d3.select(element).call(linkHoveredStyle.bind(0, d, sankey, true));
Fx.hover(gd, evt, 'sankey');
gd.emit('plotly_hover', {
event: d3.event,
points: [d.link]
});
};

var linkHoverFollow = function(element, d) {
@@ -185,10 +186,11 @@ module.exports = function plot(gd, calcData) {
};

var nodeHover = function(element, d, sankey) {
var evt = d.node;
evt.originalEvent = d3.event;
d3.select(element).call(nodeHoveredStyle, d, sankey);
Fx.hover(gd, evt, 'sankey');
gd.emit('plotly_hover', {
event: d3.event,
points: [d.node]
});
};

var nodeHoverFollow = function(element, d) {
Binary file added test/image/baselines/pie_aggregated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions test/image/mocks/pie_aggregated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"data": [
{
"labels": [
"Alice",
"Bob",
"Charlie",
"Charlie",
"Charlie",
"Alice"
],
"type": "pie",
"domain": {"x": [0, 0.4]}
},
{
"labels": [
"Alice",
"Alice",
"Allison",
"Alys",
"Elise",
"Allison",
"Alys",
"Alys"
],
"values": [
10, 20, 30, 40, 50, 60, 70, 80
],
"marker": {"colors": ["", "", "#ccc"]},
"type": "pie",
"domain": {"x": [0.5, 1]},
"textinfo": "label+value+percent"
}
],
"layout": {
"height": 300,
"width": 500
}
}
216 changes: 105 additions & 111 deletions test/jasmine/tests/pie_test.js
Original file line number Diff line number Diff line change
@@ -65,6 +65,33 @@ describe('Pie traces:', function() {
.catch(failTest)
.then(done);
});

it('can sum values or count labels', function(done) {
Plotly.newPlot(gd, [{
labels: ['a', 'b', 'c', 'a', 'b', 'a'],
values: [1, 2, 3, 4, 5, 6],
type: 'pie',
domain: {x: [0, 0.45]}
}, {
labels: ['d', 'e', 'f', 'd', 'e', 'd'],
type: 'pie',
domain: {x: [0.55, 1]}
}])
.then(function() {
var expected = [
[['a', 11], ['b', 7], ['c', 3]],
[['d', 3], ['e', 2], ['f', 1]]
];
for(var i = 0; i < 2; i++) {
for(var j = 0; j < 3; j++) {
expect(gd.calcdata[i][j].label).toBe(expected[i][j][0], i + ',' + j);
expect(gd.calcdata[i][j].v).toBe(expected[i][j][1], i + ',' + j);
}
}
})
.catch(failTest)
.then(done);
});
});

describe('pie hovering', function() {
@@ -141,24 +168,6 @@ describe('pie hovering', function() {

it('should contain the correct fields', function() {

/*
* expected = [{
* v: 4,
* label: '3',
* color: '#ff7f0e',
* i: 3,
* hidden: false,
* text: '26.7%',
* px1: [0,-60],
* pxmid: [-44.588689528643656,-40.14783638153149],
* midangle: -0.8377580409572781,
* px0: [-59.67131372209641,6.2717077960592],
* largeArc: 0,
* cxFinal: 200,
* cyFinal: 160,
* originalEvent: MouseEvent
* }];
*/
var hoverData,
unhoverData;

@@ -178,19 +187,17 @@ describe('pie hovering', function() {
expect(unhoverData.points.length).toEqual(1);

var fields = [
'v', 'label', 'color', 'i', 'hidden',
'text', 'px1', 'pxmid', 'midangle',
'px0', 'largeArc',
'pointNumber', 'curveNumber',
'cxFinal', 'cyFinal',
'originalEvent'
'curveNumber', 'pointNumber', 'pointNumbers',
'data', 'fullData',
'label', 'color', 'value',
'i', 'v'
];

expect(Object.keys(hoverData.points[0])).toEqual(fields);
expect(hoverData.points[0].i).toEqual(3);
expect(Object.keys(hoverData.points[0]).sort()).toEqual(fields.sort());
expect(hoverData.points[0].pointNumber).toEqual(3);

expect(Object.keys(unhoverData.points[0])).toEqual(fields);
expect(unhoverData.points[0].i).toEqual(3);
expect(Object.keys(unhoverData.points[0]).sort()).toEqual(fields.sort());
expect(unhoverData.points[0].pointNumber).toEqual(3);
});

it('should fire hover event when moving from one slice to another', function(done) {
@@ -355,7 +362,7 @@ describe('pie hovering', function() {
});


describe('Test event property of interactions on a pie plot:', function() {
describe('Test event data of interactions on a pie plot:', function() {
var mock = require('@mocks/pie_simple.json');

var mockCopy, gd;
@@ -376,10 +383,37 @@ describe('Test event property of interactions on a pie plot:', function() {
beforeEach(function() {
gd = createGraphDiv();
mockCopy = Lib.extendDeep({}, mock);
Lib.extendFlat(mockCopy.data[0], {
ids: ['marge', 'homer', 'bart', 'lisa', 'maggie'],
customdata: [{1: 2}, {3: 4}, {5: 6}, {7: 8}, {9: 10}]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I was wondering if customdata would get passed properly 🎉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which by the way ✅ #2048

});
});

afterEach(destroyGraphDiv);

function checkEventData(data) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A long-overdue test abstraction. Thanks!

var point = data.points[0];

expect(point.curveNumber).toBe(0);
expect(point.pointNumber).toBe(4);
expect(point.pointNumbers).toEqual([4]);
expect(point.data).toBe(gd.data[0]);
expect(point.fullData).toBe(gd._fullData[0]);
expect(point.label).toBe('4');
expect(point.value).toBe(5);
expect(point.color).toBe('#1f77b4');
expect(point.id).toEqual(['maggie']);
expect(point.customdata).toEqual([{9: 10}]);

// for backward compat - i/v to be removed at some point?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: yeah, we should remove that in v2 - would you mind adding it to #420 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented #420 (comment)

expect(point.i).toBe(point.pointNumber);
expect(point.v).toBe(point.value);

var evt = data.event;
expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
}

describe('click events', function() {
var futureData;

@@ -400,43 +434,24 @@ describe('Test event property of interactions on a pie plot:', function() {
click(pointPos[0], pointPos[1]);
expect(futureData.points.length).toEqual(1);

var trace = futureData.points.trace;
expect(typeof trace).toEqual(typeof {}, 'points.trace');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably didn't mean to attach trace as a property of the whole array 🙈

And yet we had a test to verify that that's exactly what we did 🤔


var pt = futureData.points[0];
expect(Object.keys(pt)).toEqual(jasmine.arrayContaining([
'v', 'label', 'color', 'i', 'hidden', 'vTotal', 'text', 't',
'trace', 'r', 'cx', 'cy', 'px1', 'pxmid', 'midangle', 'px0',
'largeArc', 'cxFinal', 'cyFinal',
'pointNumber', 'curveNumber'
]));
expect(Object.keys(pt).length).toBe(21);

expect(typeof pt.color).toEqual(typeof '#1f77b4', 'points[0].color');
expect(pt.cx).toEqual(200, 'points[0].cx');
expect(pt.cxFinal).toEqual(200, 'points[0].cxFinal');
expect(pt.cy).toEqual(160, 'points[0].cy');
expect(pt.cyFinal).toEqual(160, 'points[0].cyFinal');
expect(pt.hidden).toEqual(false, 'points[0].hidden');
expect(pt.i).toEqual(4, 'points[0].i');
expect(pt.pointNumber).toEqual(4, 'points[0].pointNumber');
expect(pt.label).toEqual('4', 'points[0].label');
expect(pt.largeArc).toEqual(0, 'points[0].largeArc');
expect(pt.midangle).toEqual(1.0471975511965976, 'points[0].midangle');
expect(pt.px0).toEqual([0, -60], 'points[0].px0');
expect(pt.px1).toEqual([51.96152422706632, 29.999999999999986], 'points[0].px1');
expect(pt.pxmid).toEqual([51.96152422706631, -30.000000000000007], 'points[0].pxmid');
expect(pt.r).toEqual(60, 'points[0].r');
expect(typeof pt.t).toEqual(typeof {}, 'points[0].t');
expect(pt.text).toEqual('33.3%', 'points[0].text');
expect(typeof pt.trace).toEqual(typeof {}, 'points[0].trace');
expect(pt.v).toEqual(5, 'points[0].v');
expect(pt.vTotal).toEqual(15, 'points[0].vTotal');
expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
checkEventData(futureData);
});

var evt = futureData.event;
expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
it('should not contain pointNumber if aggregating', function() {
var values = gd.data[0].values;
var labels = [];
for(var i = 0; i < values.length; i++) labels.push(i);
Plotly.restyle(gd, {
labels: [labels.concat(labels)],
values: [values.concat(values)]
});

click(pointPos[0], pointPos[1]);
expect(futureData.points.length).toEqual(1);

expect(futureData.points[0].pointNumber).toBeUndefined();
expect(futureData.points[0].i).toBeUndefined();
expect(futureData.points[0].pointNumbers).toEqual([4, 9]);
});
});

@@ -466,42 +481,9 @@ describe('Test event property of interactions on a pie plot:', function() {
click(pointPos[0], pointPos[1], clickOpts);
expect(futureData.points.length).toEqual(1);

var trace = futureData.points.trace;
expect(typeof trace).toEqual(typeof {}, 'points.trace');

var pt = futureData.points[0];
expect(Object.keys(pt)).toEqual(jasmine.arrayContaining([
'v', 'label', 'color', 'i', 'hidden', 'vTotal', 'text', 't',
'trace', 'r', 'cx', 'cy', 'px1', 'pxmid', 'midangle', 'px0',
'largeArc', 'cxFinal', 'cyFinal',
'pointNumber', 'curveNumber'
]));

expect(typeof pt.color).toEqual(typeof '#1f77b4', 'points[0].color');
expect(pt.cx).toEqual(200, 'points[0].cx');
expect(pt.cxFinal).toEqual(200, 'points[0].cxFinal');
expect(pt.cy).toEqual(160, 'points[0].cy');
expect(pt.cyFinal).toEqual(160, 'points[0].cyFinal');
expect(pt.hidden).toEqual(false, 'points[0].hidden');
expect(pt.i).toEqual(4, 'points[0].i');
expect(pt.pointNumber).toEqual(4, 'points[0].pointNumber');
expect(pt.label).toEqual('4', 'points[0].label');
expect(pt.largeArc).toEqual(0, 'points[0].largeArc');
expect(pt.midangle).toEqual(1.0471975511965976, 'points[0].midangle');
expect(pt.px0).toEqual([0, -60], 'points[0].px0');
expect(pt.px1).toEqual([51.96152422706632, 29.999999999999986], 'points[0].px1');
expect(pt.pxmid).toEqual([51.96152422706631, -30.000000000000007], 'points[0].pxmid');
expect(pt.r).toEqual(60, 'points[0].r');
expect(typeof pt.t).toEqual(typeof {}, 'points[0].t');
expect(pt.text).toEqual('33.3%', 'points[0].text');
expect(typeof pt.trace).toEqual(typeof {}, 'points[0].trace');
expect(pt.v).toEqual(5, 'points[0].v');
expect(pt.vTotal).toEqual(15, 'points[0].vTotal');
expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
checkEventData(futureData);

var evt = futureData.event;
expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
Object.getOwnPropertyNames(clickOpts).forEach(function(opt) {
expect(evt[opt]).toEqual(clickOpts[opt], 'event.' + opt);
});
@@ -512,7 +494,8 @@ describe('Test event property of interactions on a pie plot:', function() {
var futureData;

beforeEach(function(done) {
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
futureData = undefined;
Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(done);

gd.on('plotly_hover', function(data) {
futureData = data;
@@ -522,33 +505,44 @@ describe('Test event property of interactions on a pie plot:', function() {
it('should contain the correct fields', function() {
mouseEvent('mouseover', pointPos[0], pointPos[1]);

var point0 = futureData.points[0],
evt = futureData.event;
expect(point0.originalEvent).toEqual(evt, 'points');
expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
checkEventData(futureData);
});

it('should not emit a hover if you\'re dragging', function() {
gd._dragging = true;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurred to me when fixing this bit of logic, it would perhaps be better to check event.buttons instead? Seems both cleaner and more powerful, for example it would prevent hover if you drag over a plot from somewhere else on the page. So in principle perhaps all our hover handlers should contain this check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that will need a bunch more testing and ideally rolling out to all subplots at once, so I'll defer it -> #2120

mouseEvent('mouseover', pointPos[0], pointPos[1]);
expect(futureData).toBeUndefined();
});

it('should not emit a hover if hover is disabled', function() {
Plotly.relayout(gd, 'hovermode', false);
mouseEvent('mouseover', pointPos[0], pointPos[1]);
expect(futureData).toBeUndefined();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also different from before, but consistent with other subplot types, I believe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is how it should be.

For the record, to get hover event data w/o visible hover labels, one should set hoverinfo: 'none' in all the traces of interest.

});
});

describe('unhover events', function() {
var futureData;

beforeEach(function(done) {
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
futureData = undefined;
Plotly.newPlot(gd, mockCopy.data, mockCopy.layout).then(done);

gd.on('plotly_unhover', function(data) {
futureData = data;
});
});

it('should contain the correct fields', function() {
mouseEvent('mouseover', pointPos[0], pointPos[1]);
mouseEvent('mouseout', pointPos[0], pointPos[1]);

var point0 = futureData.points[0],
evt = futureData.event;
expect(point0.originalEvent).toEqual(evt, 'points');
expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
checkEventData(futureData);
});

it('should not emit an unhover if you didn\'t first hover', function() {
mouseEvent('mouseout', pointPos[0], pointPos[1]);
expect(futureData).toBeUndefined();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of a weird one... but lets say you dragged over the pie from another subplot, so there was no hover event - you shouldn't get an unhover event even if you mouse up and then mouse out of that slice.

});
});
});