diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index 22caa8847d0..fd005f52824 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -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 = { + ids: 'id', + locations: 'location', + labels: 'label', + values: 'value', + 'marker.colors': 'color' +}; + +function getPointKey(astr) { + return pointKeyMap[astr] || astr; +} + +function getPointData(val, pointNumber) { + if(Array.isArray(pointNumber)) { + if(Array.isArray(val) && Array.isArray(val[pointNumber[0]])) { + return val[pointNumber[0]][pointNumber[1]]; + } + } else { + return val[pointNumber]; + } +} diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 6f32c7b3f78..78a2ebf36d5 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -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, diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index 5adbd886ca4..cab596b95be 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -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: { diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 7fd82028790..bb425b1a606 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -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('
'); diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 7984ed7ce73..4cd2f99eb3e 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -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) { + // 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'); diff --git a/src/traces/pie/event_data.js b/src/traces/pie/event_data.js new file mode 100644 index 00000000000..d987f47e8b3 --- /dev/null +++ b/src/traces/pie/event_data.js @@ -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; +}; diff --git a/src/traces/pie/helpers.js b/src/traces/pie/helpers.js index ac19f6f6c1e..360a695786e 100644 --- a/src/traces/pie/helpers.js +++ b/src/traces/pie/helpers.js @@ -25,3 +25,16 @@ exports.formatPieValue = function formatPieValue(v, separators) { } return Lib.numSeparate(vRounded, separators); }; + +exports.getFirstFilled = function getFirstFilled(array, indices) { + 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; +}; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 026dad443c7..b083188c283 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -16,6 +16,7 @@ var Drawing = require('../../components/drawing'); var svgTextUtils = require('../../lib/svg_text_utils'); var helpers = require('./helpers'); +var eventData = require('./event_data'); module.exports = function plot(gd, cdpie) { var fullLayout = gd._fullLayout; @@ -34,31 +35,13 @@ module.exports = function plot(gd, cdpie) { pieGroups.order(); pieGroups.each(function(cd) { - var pieGroup = d3.select(this), - cd0 = cd[0], - trace = cd0.trace, - tiltRads = 0, // trace.tilt * Math.PI / 180, - depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2, - tiltAxis = trace.tiltaxis || 0, - tiltAxisRads = tiltAxis * Math.PI / 180, - depthVector = [ - depthLength * Math.sin(tiltAxisRads), - depthLength * Math.cos(tiltAxisRads) - ], - rSmall = cd0.r * Math.cos(tiltRads); - - var pieParts = pieGroup.selectAll('g.part') - .data(trace.tilt ? ['top', 'sides'] : ['top']); - - pieParts.enter().append('g').attr('class', function(d) { - return d + ' part'; - }); - pieParts.exit().remove(); - pieParts.order(); + var pieGroup = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; setCoords(cd); - pieGroup.selectAll('.top').each(function() { + pieGroup.each(function() { var slices = d3.select(this).selectAll('g.slice').data(cd); slices.enter().append('g') @@ -66,10 +49,10 @@ module.exports = function plot(gd, cdpie) { slices.exit().remove(); var quadrants = [ - [[], []], // y<0: x<0, x>=0 - [[], []] // y>=0: x<0, x>=0 - ], - hasOutsideText = false; + [[], []], // y<0: x<0, x>=0 + [[], []] // y>=0: x<0, x>=0 + ]; + var hasOutsideText = false; slices.each(function(pt) { if(pt.hidden) { @@ -83,90 +66,116 @@ module.exports = function plot(gd, cdpie) { quadrants[pt.pxmid[1] < 0 ? 0 : 1][pt.pxmid[0] < 0 ? 0 : 1].push(pt); - var cx = cd0.cx + depthVector[0], - cy = cd0.cy + depthVector[1], - sliceTop = d3.select(this), - slicePath = sliceTop.selectAll('path.surface').data([pt]), - hasHoverData = false; - - function handleMouseOver(evt) { - evt.originalEvent = d3.event; - + var cx = cd0.cx; + var cy = cd0.cy; + var sliceTop = d3.select(this); + var slicePath = sliceTop.selectAll('path.surface').data([pt]); + + // hover state vars + // have we drawn a hover label, so it should be cleared later + var hasHoverLabel = false; + // have we emitted a hover event, so later an unhover event should be emitted + // note that click events do not depend on this - you can still get them + // with hovermode: false or if you were earlier dragging, then clicked + // in the same slice that you moused up in + var hasHoverEvent = false; + + function handleMouseOver() { // in case fullLayout or fullData has changed without a replot var fullLayout2 = gd._fullLayout; var trace2 = gd._fullData[trace.index]; - var hoverinfo = Fx.castHoverinfo(trace2, fullLayout2, pt.i); + + if(gd._dragging || fullLayout2.hovermode === false) return; + + var hoverinfo = trace2.hoverinfo; + if(Array.isArray(hoverinfo)) { + // super hacky: we need to pull out the *first* hoverinfo from + // pt.pts, then put it back into an array in a dummy trace + // and call castHoverinfo on that. + // TODO: do we want to have Fx.castHoverinfo somehow handle this? + // it already takes an array for index, for 2D, so this seems tricky. + hoverinfo = Fx.castHoverinfo({ + hoverinfo: [helpers.castOption(hoverinfo, pt.pts)], + _module: trace._module + }, fullLayout2, 0); + } if(hoverinfo === 'all') hoverinfo = 'label+text+value+percent+name'; // in case we dragged over the pie from another subplot, // or if hover is turned off - if(gd._dragging || fullLayout2.hovermode === false || - hoverinfo === 'none' || hoverinfo === 'skip' || !hoverinfo) { - Fx.hover(gd, evt, 'pie'); - return; - } - - var rInscribed = getInscribedRadiusFraction(pt, cd0), - hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed), - hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed), - separators = fullLayout.separators, - thisText = []; - - if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label); - if(hoverinfo.indexOf('text') !== -1) { - if(trace2.hovertext) { - thisText.push( - Array.isArray(trace2.hovertext) ? - trace2.hovertext[pt.i] : - trace2.hovertext - ); - } else if(trace2.text && trace2.text[pt.i]) { - thisText.push(trace2.text[pt.i]); + if(hoverinfo !== 'none' && hoverinfo !== 'skip' && hoverinfo) { + var rInscribed = getInscribedRadiusFraction(pt, cd0); + var hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed); + var hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed); + var separators = fullLayout.separators; + var thisText = []; + + if(hoverinfo.indexOf('label') !== -1) thisText.push(pt.label); + if(hoverinfo.indexOf('text') !== -1) { + var texti = helpers.castOption(trace2.hovertext || trace2.text, pt.pts); + if(texti) thisText.push(texti); } - } - if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v, separators)); - if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); - - Fx.loneHover({ - x0: hoverCenterX - rInscribed * cd0.r, - x1: hoverCenterX + rInscribed * cd0.r, - y: hoverCenterY, - text: thisText.join('
'), - name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined, - idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right', - color: Fx.castHoverOption(trace, pt.i, 'bgcolor') || pt.color, - borderColor: Fx.castHoverOption(trace, pt.i, 'bordercolor'), - fontFamily: Fx.castHoverOption(trace, pt.i, 'font.family'), - fontSize: Fx.castHoverOption(trace, pt.i, 'font.size'), - fontColor: Fx.castHoverOption(trace, pt.i, 'font.color') - }, { - container: fullLayout2._hoverlayer.node(), - outerContainer: fullLayout2._paper.node(), - gd: gd - }); + if(hoverinfo.indexOf('value') !== -1) thisText.push(helpers.formatPieValue(pt.v, separators)); + if(hoverinfo.indexOf('percent') !== -1) thisText.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); + + var hoverLabel = trace.hoverlabel; + var hoverFont = hoverLabel.font; + + Fx.loneHover({ + x0: hoverCenterX - rInscribed * cd0.r, + x1: hoverCenterX + rInscribed * cd0.r, + y: hoverCenterY, + text: thisText.join('
'), + name: hoverinfo.indexOf('name') !== -1 ? trace2.name : undefined, + idealAlign: pt.pxmid[0] < 0 ? 'left' : 'right', + color: helpers.castOption(hoverLabel.bgcolor, pt.pts) || pt.color, + borderColor: helpers.castOption(hoverLabel.bordercolor, pt.pts), + fontFamily: helpers.castOption(hoverFont.family, pt.pts), + fontSize: helpers.castOption(hoverFont.size, pt.pts), + fontColor: helpers.castOption(hoverFont.color, pt.pts) + }, { + container: fullLayout2._hoverlayer.node(), + outerContainer: fullLayout2._paper.node(), + gd: gd + }); - Fx.hover(gd, evt, 'pie'); + hasHoverLabel = true; + } - hasHoverData = true; + gd.emit('plotly_hover', { + points: [eventData(pt, trace2)], + event: d3.event + }); + hasHoverEvent = true; } function handleMouseOut(evt) { - evt.originalEvent = d3.event; - gd.emit('plotly_unhover', { - event: d3.event, - points: [evt] - }); + var fullLayout2 = gd._fullLayout; + var trace2 = gd._fullData[trace.index]; - if(hasHoverData) { - Fx.loneUnhover(fullLayout._hoverlayer.node()); - hasHoverData = false; + if(hasHoverEvent) { + evt.originalEvent = d3.event; + gd.emit('plotly_unhover', { + points: [eventData(pt, trace2)], + event: d3.event + }); + hasHoverEvent = false; + } + + if(hasHoverLabel) { + Fx.loneUnhover(fullLayout2._hoverlayer.node()); + hasHoverLabel = false; } } function handleClick() { - gd._hoverdata = [pt]; - gd._hoverdata.trace = cd0.trace; + var fullLayout2 = gd._fullLayout; + var trace2 = gd._fullData[trace.index]; + + if(gd._dragging || fullLayout2.hovermode === false) return; + + gd._hoverdata = [eventData(pt, trace2)]; Fx.click(gd, d3.event); } @@ -182,7 +191,7 @@ module.exports = function plot(gd, cdpie) { .on('click', handleClick); if(trace.pull) { - var pull = +(Array.isArray(trace.pull) ? trace.pull[pt.i] : trace.pull) || 0; + var pull = +helpers.castOption(trace.pull, pt.pts) || 0; if(pull > 0) { cx += pull * pt.pxmid[0]; cy += pull * pt.pxmid[1]; @@ -193,7 +202,7 @@ module.exports = function plot(gd, cdpie) { pt.cyFinal = cy; function arc(start, finish, cw, scale) { - return 'a' + (scale * cd0.r) + ',' + (scale * rSmall) + ' ' + tiltAxis + ' ' + + return 'a' + (scale * cd0.r) + ',' + (scale * cd0.r) + ' 0 ' + pt.largeArc + (cw ? ' 1 ' : ' 0 ') + (scale * (finish[0] - start[0])) + ',' + (scale * (finish[1] - start[1])); } @@ -233,9 +242,8 @@ module.exports = function plot(gd, cdpie) { } // add text - var textPosition = Array.isArray(trace.textposition) ? - trace.textposition[pt.i] : trace.textposition, - sliceTextGroup = sliceTop.selectAll('g.slicetext') + var textPosition = helpers.castOption(trace.textposition, pt.pts); + var sliceTextGroup = sliceTop.selectAll('g.slicetext') .data(pt.text && (textPosition !== 'none') ? [0] : []); sliceTextGroup.enter().append('g') @@ -262,9 +270,8 @@ module.exports = function plot(gd, cdpie) { .call(svgTextUtils.convertToTspans, gd); // position the text relative to the slice - // TODO: so far this only accounts for flat - var textBB = Drawing.bBox(sliceText.node()), - transform; + var textBB = Drawing.bBox(sliceText.node()); + var transform; if(textPosition === 'outside') { transform = transformOutsideText(textBB, pt); @@ -280,8 +287,8 @@ module.exports = function plot(gd, cdpie) { } } - var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0), - translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0); + var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0); + var translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0); // save some stuff to use later ensure no labels overlap if(transform.outside) { @@ -309,20 +316,21 @@ module.exports = function plot(gd, cdpie) { slices.each(function(pt) { if(pt.labelExtraX || pt.labelExtraY) { // first move the text to its new location - var sliceTop = d3.select(this), - sliceText = sliceTop.select('g.slicetext text'); + var sliceTop = d3.select(this); + var sliceText = sliceTop.select('g.slicetext text'); sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' + sliceText.attr('transform')); // then add a line to the new location - var lineStartX = pt.cxFinal + pt.pxmid[0], - lineStartY = pt.cyFinal + pt.pxmid[1], - textLinePath = 'M' + lineStartX + ',' + lineStartY, - finalX = (pt.yLabelMax - pt.yLabelMin) * (pt.pxmid[0] < 0 ? -1 : 1) / 4; + var lineStartX = pt.cxFinal + pt.pxmid[0]; + var lineStartY = pt.cyFinal + pt.pxmid[1]; + var textLinePath = 'M' + lineStartX + ',' + lineStartY; + var finalX = (pt.yLabelMax - pt.yLabelMin) * (pt.pxmid[0] < 0 ? -1 : 1) / 4; + if(pt.labelExtraX) { - var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0], - yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]); + var yFromX = pt.labelExtraX * pt.pxmid[1] / pt.pxmid[0]; + var yNet = pt.yLabelMid + pt.labelExtraY - (pt.cyFinal + pt.pxmid[1]); if(Math.abs(yFromX) > Math.abs(yNet)) { textLinePath += @@ -368,11 +376,11 @@ module.exports = function plot(gd, cdpie) { function transformInsideText(textBB, pt, cd0) { - var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height), - textAspect = textBB.width / textBB.height, - halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5), - ring = 1 - cd0.trace.hole, - rInscribed = getInscribedRadiusFraction(pt, cd0), + var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height); + var textAspect = textBB.width / textBB.height; + var halfAngle = Math.PI * Math.min(pt.v / cd0.vTotal, 0.5); + var ring = 1 - cd0.trace.hole; + var rInscribed = getInscribedRadiusFraction(pt, cd0), // max size text can be inserted inside without rotating it // this inscribes the text rectangle in a circle, which is then inscribed @@ -389,34 +397,34 @@ function transformInsideText(textBB, pt, cd0) { if(transform.scale >= 1) return transform; // max size if text is rotated radially - var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)), - maxHalfHeightRotRadial = cd0.r * Math.min( - 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr), - ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect) - ), - radialTransform = { - scale: maxHalfHeightRotRadial * 2 / textBB.height, - rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) - - maxHalfHeightRotRadial * textAspect / cd0.r, - rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90 - }, + var Qr = textAspect + 1 / (2 * Math.tan(halfAngle)); + var maxHalfHeightRotRadial = cd0.r * Math.min( + 1 / (Math.sqrt(Qr * Qr + 0.5) + Qr), + ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect) + ); + var radialTransform = { + scale: maxHalfHeightRotRadial * 2 / textBB.height, + rCenter: Math.cos(maxHalfHeightRotRadial / cd0.r) - + maxHalfHeightRotRadial * textAspect / cd0.r, + rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90 + }; // max size if text is rotated tangentially - aspectInv = 1 / textAspect, - Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)), - maxHalfWidthTangential = cd0.r * Math.min( - 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt), - ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv) - ), - tangentialTransform = { - scale: maxHalfWidthTangential * 2 / textBB.width, - rCenter: Math.cos(maxHalfWidthTangential / cd0.r) - - maxHalfWidthTangential / textAspect / cd0.r, - rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90 - }, - // if we need a rotated transform, pick the biggest one - // even if both are bigger than 1 - rotatedTransform = tangentialTransform.scale > radialTransform.scale ? + var aspectInv = 1 / textAspect; + var Qt = aspectInv + 1 / (2 * Math.tan(halfAngle)); + var maxHalfWidthTangential = cd0.r * Math.min( + 1 / (Math.sqrt(Qt * Qt + 0.5) + Qt), + ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv) + ); + var tangentialTransform = { + scale: maxHalfWidthTangential * 2 / textBB.width, + rCenter: Math.cos(maxHalfWidthTangential / cd0.r) - + maxHalfWidthTangential / textAspect / cd0.r, + rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90 + }; + // if we need a rotated transform, pick the biggest one + // even if both are bigger than 1 + var rotatedTransform = tangentialTransform.scale > radialTransform.scale ? tangentialTransform : radialTransform; if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform; @@ -431,10 +439,10 @@ function getInscribedRadiusFraction(pt, cd0) { } function transformOutsideText(textBB, pt) { - var x = pt.pxmid[0], - y = pt.pxmid[1], - dx = textBB.width / 2, - dy = textBB.height / 2; + var x = pt.pxmid[0]; + var y = pt.pxmid[1]; + var dx = textBB.width / 2; + var dy = textBB.height / 2; if(x < 0) dx *= -1; if(y < 0) dy *= -1; @@ -450,19 +458,9 @@ function transformOutsideText(textBB, pt) { } function scootLabels(quadrants, trace) { - var xHalf, - yHalf, - equatorFirst, - farthestX, - farthestY, - xDiffSign, - yDiffSign, - thisQuad, - oppositeQuad, - wholeSide, - i, - thisQuadOutside, - firstOppositeOutsidePt; + var xHalf, yHalf, equatorFirst, farthestX, farthestY, + xDiffSign, yDiffSign, thisQuad, oppositeQuad, + wholeSide, i, thisQuadOutside, firstOppositeOutsidePt; function topFirst(a, b) { return a.pxmid[1] - b.pxmid[1]; } function bottomFirst(a, b) { return b.pxmid[1] - a.pxmid[1]; } @@ -470,17 +468,14 @@ function scootLabels(quadrants, trace) { function scootOneLabel(thisPt, prevPt) { if(!prevPt) prevPt = {}; - var prevOuterY = prevPt.labelExtraY + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin), - thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax, - thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin, - thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]), - newExtraY = prevOuterY - thisInnerY, - xBuffer, - i, - otherPt, - otherOuterY, - otherOuterX, - newExtraX; + var prevOuterY = prevPt.labelExtraY + (yHalf ? prevPt.yLabelMax : prevPt.yLabelMin); + var thisInnerY = yHalf ? thisPt.yLabelMin : thisPt.yLabelMax; + var thisOuterY = yHalf ? thisPt.yLabelMax : thisPt.yLabelMin; + var thisSliceOuterY = thisPt.cyFinal + farthestY(thisPt.px0[1], thisPt.px1[1]); + var newExtraY = prevOuterY - thisInnerY; + + var xBuffer, i, otherPt, otherOuterY, otherOuterX, newExtraX; + // make sure this label doesn't overlap other labels // this *only* has us move these labels vertically if(newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY; @@ -492,7 +487,12 @@ function scootLabels(quadrants, trace) { otherPt = wholeSide[i]; // overlap can only happen if the other point is pulled more than this one - if(otherPt === thisPt || ((trace.pull[thisPt.i] || 0) >= trace.pull[otherPt.i] || 0)) continue; + if(otherPt === thisPt || ( + (helpers.castOption(trace.pull, thisPt.pts) || 0) >= + (helpers.castOption(trace.pull, otherPt.pts) || 0)) + ) { + continue; + } if((thisPt.pxmid[1] - otherPt.pxmid[1]) * yDiffSign > 0) { // closer to the equator - by construction all of these happen first @@ -564,17 +564,10 @@ function scootLabels(quadrants, trace) { } function scalePies(cdpie, plotSize) { - var pieBoxWidth, - pieBoxHeight, - i, - j, - cd0, - trace, - tiltAxisRads, - maxPull, - scaleGroups = [], - scaleGroup, - minPxPerValUnit; + var scaleGroups = []; + + var pieBoxWidth, pieBoxHeight, i, j, cd0, trace, + maxPull, scaleGroup, minPxPerValUnit; // first figure out the center and maximum radius for each pie for(i = 0; i < cdpie.length; i++) { @@ -582,7 +575,6 @@ function scalePies(cdpie, plotSize) { trace = cd0.trace; pieBoxWidth = plotSize.w * (trace.domain.x[1] - trace.domain.x[0]); pieBoxHeight = plotSize.h * (trace.domain.y[1] - trace.domain.y[0]); - tiltAxisRads = trace.tiltaxis * Math.PI / 180; maxPull = trace.pull; if(Array.isArray(maxPull)) { @@ -592,10 +584,7 @@ function scalePies(cdpie, plotSize) { } } - cd0.r = Math.min( - pieBoxWidth / maxExtent(trace.tilt, Math.sin(tiltAxisRads), trace.depth), - pieBoxHeight / maxExtent(trace.tilt, Math.cos(tiltAxisRads), trace.depth) - ) / (2 + 2 * maxPull); + cd0.r = Math.min(pieBoxWidth, pieBoxHeight) / (2 + 2 * maxPull); cd0.cx = plotSize.l + plotSize.w * (trace.domain.x[1] + trace.domain.x[0]) / 2; cd0.cy = plotSize.t + plotSize.h * (2 - trace.domain.y[1] - trace.domain.y[0]) / 2; @@ -629,22 +618,14 @@ function scalePies(cdpie, plotSize) { } function setCoords(cd) { - var cd0 = cd[0], - trace = cd0.trace, - tilt = trace.tilt, - tiltAxisRads, - tiltAxisSin, - tiltAxisCos, - tiltRads, - crossTilt, - inPlane, - currentAngle = trace.rotation * Math.PI / 180, - angleFactor = 2 * Math.PI / cd0.vTotal, - firstPt = 'px0', - lastPt = 'px1', - i, - cdi, - currentCoords; + var cd0 = cd[0]; + var trace = cd0.trace; + var currentAngle = trace.rotation * Math.PI / 180; + var angleFactor = 2 * Math.PI / cd0.vTotal; + var firstPt = 'px0'; + var lastPt = 'px1'; + + var i, cdi, currentCoords; if(trace.direction === 'counterclockwise') { for(i = 0; i < cd.length; i++) { @@ -658,26 +639,8 @@ function setCoords(cd) { lastPt = 'px0'; } - if(tilt) { - tiltRads = tilt * Math.PI / 180; - tiltAxisRads = trace.tiltaxis * Math.PI / 180; - crossTilt = Math.sin(tiltAxisRads) * Math.cos(tiltAxisRads); - inPlane = 1 - Math.cos(tiltRads); - tiltAxisSin = Math.sin(tiltAxisRads); - tiltAxisCos = Math.cos(tiltAxisRads); - } - function getCoords(angle) { - var xFlat = cd0.r * Math.sin(angle), - yFlat = -cd0.r * Math.cos(angle); - - if(!tilt) return [xFlat, yFlat]; - - return [ - xFlat * (1 - inPlane * tiltAxisSin * tiltAxisSin) + yFlat * crossTilt * inPlane, - xFlat * crossTilt * inPlane + yFlat * (1 - inPlane * tiltAxisCos * tiltAxisCos), - Math.sin(tiltRads) * (yFlat * tiltAxisCos - xFlat * tiltAxisSin) - ]; + return [cd0.r * Math.sin(angle), -cd0.r * Math.cos(angle)]; } currentCoords = getCoords(currentAngle); @@ -700,11 +663,3 @@ function setCoords(cd) { cdi.largeArc = (cdi.v > cd0.vTotal / 2) ? 1 : 0; } } - -function maxExtent(tilt, tiltAxisFraction, depth) { - if(!tilt) return 1; - var sinTilt = Math.sin(tilt * Math.PI / 180); - return Math.max(0.01, // don't let it go crazy if you tilt the pie totally on its side - depth * sinTilt * Math.abs(tiltAxisFraction) + - 2 * Math.sqrt(1 - sinTilt * sinTilt * tiltAxisFraction * tiltAxisFraction)); -} diff --git a/src/traces/pie/style.js b/src/traces/pie/style.js index fb02933eb00..35c11ab0d7a 100644 --- a/src/traces/pie/style.js +++ b/src/traces/pie/style.js @@ -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); }); }); diff --git a/src/traces/pie/style_one.js b/src/traces/pie/style_one.js index 0b7f3e7cd60..9ed05500fcf 100644 --- a/src/traces/pie/style_one.js +++ b/src/traces/pie/style_one.js @@ -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); }; diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index cf53c53857d..59d978fa4a4 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -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) { diff --git a/test/image/baselines/pie_aggregated.png b/test/image/baselines/pie_aggregated.png new file mode 100644 index 00000000000..576ae7dd8c6 Binary files /dev/null and b/test/image/baselines/pie_aggregated.png differ diff --git a/test/image/mocks/pie_aggregated.json b/test/image/mocks/pie_aggregated.json new file mode 100644 index 00000000000..9740acecae2 --- /dev/null +++ b/test/image/mocks/pie_aggregated.json @@ -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 + } +} diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 4a9f675cf72..1f4e109a18f 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -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}] + }); }); afterEach(destroyGraphDiv); + function checkEventData(data) { + 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? + 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'); - - 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,11 +505,19 @@ 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; + 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(); }); }); @@ -534,7 +525,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_unhover', function(data) { futureData = data; @@ -542,13 +534,15 @@ describe('Test event property of interactions on a pie plot:', function() { }); 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(); }); }); });