From 679ed9cf051eda44d53fa3aa3b6a031cd34ddab5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 23 Oct 2017 22:36:47 -0400 Subject: [PATCH 1/8] remove obsolete comments, esp. about 3d pies --- src/traces/pie/defaults.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 7984ed7ce73..973f39cd600 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -34,14 +34,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 +59,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'); From ce8946c013278ad2c0cf56eddc1dcf57c3524836 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 23 Oct 2017 22:37:11 -0400 Subject: [PATCH 2/8] first cut at aggregated pies --- src/traces/pie/attributes.js | 13 ++- src/traces/pie/calc.js | 101 ++++++++++++++---------- src/traces/pie/defaults.js | 11 +-- src/traces/pie/helpers.js | 13 +++ src/traces/pie/plot.js | 50 +++++++----- src/traces/pie/style_one.js | 13 ++- test/image/baselines/pie_aggregated.png | Bin 0 -> 22330 bytes test/image/mocks/pie_aggregated.json | 39 +++++++++ test/jasmine/tests/pie_test.js | 6 +- 9 files changed, 170 insertions(+), 76 deletions(-) create mode 100644 test/image/baselines/pie_aggregated.png create mode 100644 test/image/mocks/pie_aggregated.json 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..5f5f08f425e 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -16,18 +16,18 @@ var helpers = require('./helpers'); module.exports = function calc(gd, trace) { var vals = trace.values, + hasVals = Array.isArray(vals) && vals.length, labels = trace.labels, + colors = trace.marker.colors, cd = [], fullLayout = gd._fullLayout, colorMap = fullLayout._piecolormap, allThisTraceLabels = {}, - needDefaults = false, vTotal = 0, hiddenLabels = fullLayout.hiddenlabels || [], i, v, label, - color, hidden, pt; @@ -38,46 +38,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 +102,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++; } @@ -113,7 +131,10 @@ module.exports = function calc(gd, trace) { 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 973f39cd600..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'); } 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..5efe98eae24 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -95,7 +95,19 @@ module.exports = function plot(gd, cdpie) { // 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); + + 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'; @@ -115,19 +127,15 @@ module.exports = function plot(gd, cdpie) { 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]); - } + 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)); + var hoverLabel = trace.hoverlabel; + var hoverFont = hoverLabel.font; + Fx.loneHover({ x0: hoverCenterX - rInscribed * cd0.r, x1: hoverCenterX + rInscribed * cd0.r, @@ -135,11 +143,11 @@ module.exports = function plot(gd, cdpie) { 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') + 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(), @@ -182,7 +190,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]; @@ -233,8 +241,7 @@ module.exports = function plot(gd, cdpie) { } // add text - var textPosition = Array.isArray(trace.textposition) ? - trace.textposition[pt.i] : trace.textposition, + var textPosition = helpers.castOption(trace.textposition, pt.pts), sliceTextGroup = sliceTop.selectAll('g.slicetext') .data(pt.text && (textPosition !== 'none') ? [0] : []); @@ -492,7 +499,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 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/test/image/baselines/pie_aggregated.png b/test/image/baselines/pie_aggregated.png new file mode 100644 index 0000000000000000000000000000000000000000..576ae7dd8c63665562376591428ad57f16955c34 GIT binary patch literal 22330 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 LnCidMb-?}?5CGZ4 literal 0 HcmV?d00001 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..1ff55e23ecd 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -178,7 +178,7 @@ describe('pie hovering', function() { expect(unhoverData.points.length).toEqual(1); var fields = [ - 'v', 'label', 'color', 'i', 'hidden', + 'v', 'label', 'color', 'i', 'pts', 'hidden', 'text', 'px1', 'pxmid', 'midangle', 'px0', 'largeArc', 'pointNumber', 'curveNumber', @@ -405,12 +405,12 @@ describe('Test event property of interactions on a pie plot:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual(jasmine.arrayContaining([ - 'v', 'label', 'color', 'i', 'hidden', 'vTotal', 'text', 't', + 'v', 'label', 'color', 'i', 'pts', '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(Object.keys(pt).length).toBe(22); expect(typeof pt.color).toEqual(typeof '#1f77b4', 'points[0].color'); expect(pt.cx).toEqual(200, 'points[0].cx'); From d2756f1a9378e90277a45bf4a43146f804206269 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 23 Oct 2017 23:33:30 -0400 Subject: [PATCH 3/8] lint pie --- src/traces/pie/calc.js | 40 ++++--- src/traces/pie/plot.js | 231 ++++++++++++++++++---------------------- src/traces/pie/style.js | 6 +- 3 files changed, 125 insertions(+), 152 deletions(-) diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 5f5f08f425e..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, - hasVals = Array.isArray(vals) && vals.length, - labels = trace.labels, - colors = trace.marker.colors, - cd = [], - fullLayout = gd._fullLayout, - colorMap = fullLayout._piecolormap, - allThisTraceLabels = {}, - vTotal = 0, - hiddenLabels = fullLayout.hiddenlabels || [], - i, - v, - label, - 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); @@ -121,12 +118,13 @@ 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]; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 5efe98eae24..d23ffb530fd 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -34,18 +34,18 @@ 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 pieGroup = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; + var tiltRads = 0; // trace.tilt * Math.PI / 180, + var depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2; + var tiltAxis = trace.tiltaxis || 0; + var tiltAxisRads = tiltAxis * Math.PI / 180; + var depthVector = [ + depthLength * Math.sin(tiltAxisRads), + depthLength * Math.cos(tiltAxisRads) + ]; + var rSmall = cd0.r * Math.cos(tiltRads); var pieParts = pieGroup.selectAll('g.part') .data(trace.tilt ? ['top', 'sides'] : ['top']); @@ -66,10 +66,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,11 +83,11 @@ 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; + var cx = cd0.cx + depthVector[0]; + var cy = cd0.cy + depthVector[1]; + var sliceTop = d3.select(this); + var slicePath = sliceTop.selectAll('path.surface').data([pt]); + var hasHoverData = false; function handleMouseOver(evt) { evt.originalEvent = d3.event; @@ -119,11 +119,11 @@ module.exports = function plot(gd, cdpie) { return; } - var rInscribed = getInscribedRadiusFraction(pt, cd0), - hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed), - hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed), - separators = fullLayout.separators, - thisText = []; + 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) { @@ -241,8 +241,8 @@ module.exports = function plot(gd, cdpie) { } // add text - var textPosition = helpers.castOption(trace.textposition, pt.pts), - 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') @@ -270,8 +270,8 @@ module.exports = function plot(gd, cdpie) { // 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); @@ -287,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) { @@ -316,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 += @@ -375,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 @@ -396,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; @@ -438,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; @@ -457,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]; } @@ -477,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; @@ -576,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, tiltAxisRads, + maxPull, scaleGroup, minPxPerValUnit; // first figure out the center and maximum radius for each pie for(i = 0; i < cdpie.length; i++) { @@ -641,22 +622,16 @@ 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 tilt = trace.tilt; + var currentAngle = trace.rotation * Math.PI / 180; + var angleFactor = 2 * Math.PI / cd0.vTotal; + var firstPt = 'px0'; + var lastPt = 'px1'; + + var tiltAxisRads, tiltAxisSin, tiltAxisCos, tiltRads, crossTilt, + inPlane, i, cdi, currentCoords; if(trace.direction === 'counterclockwise') { for(i = 0; i < cd.length; i++) { @@ -680,8 +655,8 @@ function setCoords(cd) { } function getCoords(angle) { - var xFlat = cd0.r * Math.sin(angle), - yFlat = -cd0.r * Math.cos(angle); + var xFlat = cd0.r * Math.sin(angle); + var yFlat = -cd0.r * Math.cos(angle); if(!tilt) return [xFlat, yFlat]; diff --git a/src/traces/pie/style.js b/src/traces/pie/style.js index fb02933eb00..a87c9f49cc4 100644 --- a/src/traces/pie/style.js +++ b/src/traces/pie/style.js @@ -14,9 +14,9 @@ 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}); From ebc4d8b2efc48e190b489e5cfa2dfa582a251dd7 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 23 Oct 2017 23:51:23 -0400 Subject: [PATCH 4/8] :hocho: my zeroed-out 3D pie code apparently I got a lot farther than just defining attributes (deleted in #1152) --- src/traces/pie/plot.js | 67 +++++------------------------------------ src/traces/pie/style.js | 2 +- 2 files changed, 9 insertions(+), 60 deletions(-) diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index d23ffb530fd..6e4974e2e5a 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -37,28 +37,10 @@ module.exports = function plot(gd, cdpie) { var pieGroup = d3.select(this); var cd0 = cd[0]; var trace = cd0.trace; - var tiltRads = 0; // trace.tilt * Math.PI / 180, - var depthLength = (trace.depth||0) * cd0.r * Math.sin(tiltRads) / 2; - var tiltAxis = trace.tiltaxis || 0; - var tiltAxisRads = tiltAxis * Math.PI / 180; - var depthVector = [ - depthLength * Math.sin(tiltAxisRads), - depthLength * Math.cos(tiltAxisRads) - ]; - var 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(); setCoords(cd); - pieGroup.selectAll('.top').each(function() { + pieGroup.each(function() { var slices = d3.select(this).selectAll('g.slice').data(cd); slices.enter().append('g') @@ -83,8 +65,8 @@ 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]; - var cy = cd0.cy + depthVector[1]; + var cx = cd0.cx; + var cy = cd0.cy; var sliceTop = d3.select(this); var slicePath = sliceTop.selectAll('path.surface').data([pt]); var hasHoverData = false; @@ -201,7 +183,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])); } @@ -269,7 +251,6 @@ 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()); var transform; @@ -566,7 +547,7 @@ function scootLabels(quadrants, trace) { function scalePies(cdpie, plotSize) { var scaleGroups = []; - var pieBoxWidth, pieBoxHeight, i, j, cd0, trace, tiltAxisRads, + var pieBoxWidth, pieBoxHeight, i, j, cd0, trace, maxPull, scaleGroup, minPxPerValUnit; // first figure out the center and maximum radius for each pie @@ -575,7 +556,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)) { @@ -585,10 +565,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; @@ -624,14 +601,12 @@ function scalePies(cdpie, plotSize) { function setCoords(cd) { var cd0 = cd[0]; var trace = cd0.trace; - var tilt = trace.tilt; var currentAngle = trace.rotation * Math.PI / 180; var angleFactor = 2 * Math.PI / cd0.vTotal; var firstPt = 'px0'; var lastPt = 'px1'; - var tiltAxisRads, tiltAxisSin, tiltAxisCos, tiltRads, crossTilt, - inPlane, i, cdi, currentCoords; + var i, cdi, currentCoords; if(trace.direction === 'counterclockwise') { for(i = 0; i < cd.length; i++) { @@ -645,26 +620,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); - var 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); @@ -687,11 +644,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 a87c9f49cc4..35c11ab0d7a 100644 --- a/src/traces/pie/style.js +++ b/src/traces/pie/style.js @@ -20,7 +20,7 @@ module.exports = function style(gd) { 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); }); }); From 5d0f3e2bddeec1ad1fc8f6c25bc4720cb155fbfd Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 24 Oct 2017 23:53:50 -0400 Subject: [PATCH 5/8] standardize pie events, and add internal aggregation info --- src/components/fx/helpers.js | 56 ++++++++-- src/components/fx/hover.js | 2 +- src/traces/pie/event_data.js | 41 +++++++ src/traces/pie/plot.js | 127 ++++++++++++---------- test/jasmine/tests/pie_test.js | 189 ++++++++++++++------------------- 5 files changed, 239 insertions(+), 176 deletions(-) create mode 100644 src/traces/pie/event_data.js diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index 22caa8847d0..0c7c33d2e01 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -100,22 +100,58 @@ 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; + } + } +}; + +exports.appendArrayPointValues = 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..da67de47512 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -155,7 +155,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent) { - if((subplot === 'pie' || subplot === 'sankey') && !noHoverEvent) { + if(subplot === 'sankey' && !noHoverEvent) { gd.emit('plotly_hover', { event: evt.originalEvent, points: [evt] diff --git a/src/traces/pie/event_data.js b/src/traces/pie/event_data.js new file mode 100644 index 00000000000..60653b99190 --- /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 appendArrayPointValues = require('../../components/fx/helpers').appendArrayPointValues; + + +// 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. + appendArrayPointValues(out, trace, pt.pts); + + return out; +}; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 6e4974e2e5a..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; @@ -69,15 +70,23 @@ module.exports = function plot(gd, cdpie) { var cy = cd0.cy; var sliceTop = d3.select(this); var slicePath = sliceTop.selectAll('path.surface').data([pt]); - var hasHoverData = false; - function handleMouseOver(evt) { - evt.originalEvent = d3.event; + // 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]; + 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 @@ -95,68 +104,78 @@ module.exports = function plot(gd, cdpie) { // 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); - var hoverCenterX = cx + pt.pxmid[0] * (1 - rInscribed); - var hoverCenterY = cy + pt.pxmid[1] * (1 - rInscribed); - var separators = fullLayout.separators; - var thisText = []; + 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)); + + 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 + }); - 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); + hasHoverLabel = true; } - 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'); - 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(hasHoverEvent) { + evt.originalEvent = d3.event; + gd.emit('plotly_unhover', { + points: [eventData(pt, trace2)], + event: d3.event + }); + hasHoverEvent = false; + } - if(hasHoverData) { - Fx.loneUnhover(fullLayout._hoverlayer.node()); - hasHoverData = 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); } diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 1ff55e23ecd..ee66c1da147 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -141,24 +141,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 +160,17 @@ describe('pie hovering', function() { expect(unhoverData.points.length).toEqual(1); var fields = [ - 'v', 'label', 'color', 'i', 'pts', '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 +335,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 +356,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 +407,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', 'pts', 'hidden', 'vTotal', 'text', 't', - 'trace', 'r', 'cx', 'cy', 'px1', 'pxmid', 'midangle', 'px0', - 'largeArc', 'cxFinal', 'cyFinal', - 'pointNumber', 'curveNumber' - ])); - expect(Object.keys(pt).length).toBe(22); - - 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); + }); + + 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)] + }); - var evt = futureData.event; - expect(evt.clientX).toEqual(pointPos[0], 'event.clientX'); - expect(evt.clientY).toEqual(pointPos[1], 'event.clientY'); + 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 +454,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 +467,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 +478,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 +498,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 +507,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(); }); }); }); From e08e37ae58296d0abcedb6f02f21f9153bb6676b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 25 Oct 2017 12:05:52 -0400 Subject: [PATCH 6/8] simplify sankey hover events --- src/components/fx/hover.js | 8 -------- src/traces/sankey/plot.js | 14 ++++++++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index da67de47512..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 === '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/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) { From 4ba02497d920b1ff77cb0c878aff8209120104e1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 25 Oct 2017 14:23:02 -0400 Subject: [PATCH 7/8] rename and document appendArrayMultiPointValues --- src/components/fx/helpers.js | 15 +++++++++++++-- src/traces/pie/event_data.js | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index 0c7c33d2e01..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; @@ -111,7 +112,17 @@ exports.appendArrayPointValue = function(pointData, trace, pointNumber) { } }; -exports.appendArrayPointValues = function(pointData, trace, pointNumbers) { +/** + * 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) { diff --git a/src/traces/pie/event_data.js b/src/traces/pie/event_data.js index 60653b99190..d987f47e8b3 100644 --- a/src/traces/pie/event_data.js +++ b/src/traces/pie/event_data.js @@ -9,7 +9,7 @@ 'use strict'; -var appendArrayPointValues = require('../../components/fx/helpers').appendArrayPointValues; +var appendArrayMultiPointValues = require('../../components/fx/helpers').appendArrayMultiPointValues; // Note: like other eventData routines, this creates the data for hover/unhover/click events @@ -35,7 +35,7 @@ module.exports = function eventData(pt, trace) { // 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. - appendArrayPointValues(out, trace, pt.pts); + appendArrayMultiPointValues(out, trace, pt.pts); return out; }; From ee1f8c4f776fbc8dff26c4f630a6669d5e0b3df9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 25 Oct 2017 17:24:17 -0400 Subject: [PATCH 8/8] jasmine test of pie agg with or without values --- test/jasmine/tests/pie_test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index ee66c1da147..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() {