diff --git a/src/traces/bar/layout_attributes.js b/src/traces/bar/layout_attributes.js index 31f5f00b593..e2cca32a822 100644 --- a/src/traces/bar/layout_attributes.js +++ b/src/traces/bar/layout_attributes.js @@ -12,13 +12,15 @@ module.exports = { barmode: { valType: 'enumerated', - values: ['stack', 'group', 'overlay'], + values: ['stack', 'group', 'overlay', 'relative'], dflt: 'group', role: 'info', description: [ 'Determines how bars at the same location coordinate', 'are displayed on the graph.', 'With *stack*, the bars are stacked on top of one another', + 'With *relative*, the bars are stacked on top of one another,', + 'with negative values below the axis, positive values above', 'With *group*, the bars are plotted next to one another', 'centered around the shared location.', 'With *overlay*, the bars are plotted over one another,', diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index 4e486113300..a7d8027afb1 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -116,10 +116,11 @@ module.exports = function setPositions(gd, plotinfo) { else barposition(bl); var stack = (fullLayout.barmode === 'stack'), + relative = (fullLayout.barmode ==='relative'), norm = fullLayout.barnorm; // bar size range and stacking calculation - if(stack || norm) { + if(stack || relative || norm) { // for stacked bars, we need to evaluate every step in every // stack, because negative bars mean the extremes could be // anywhere @@ -142,13 +143,15 @@ module.exports = function setPositions(gd, plotinfo) { ti = gd.calcdata[bl[i]]; for(j = 0; j < ti.length; j++) { sv = Math.round(ti[j].p / sumround); + // store the negative sum value for p at the same key, with sign flipped + if(relative && ti[j].s < 0) sv = -sv; var previousSum = sums[sv] || 0; - if(stack) ti[j].b = previousSum; + if(stack || relative) ti[j].b = previousSum; barEnd = ti[j].b + ti[j].s; sums[sv] = previousSum + ti[j].s; // store the bar top in each calcdata item - if(stack) { + if(stack || relative) { ti[j][sLetter] = barEnd; if(!norm && isNumeric(sa.c2l(barEnd))) { sMax = Math.max(sMax,barEnd); @@ -161,13 +164,18 @@ module.exports = function setPositions(gd, plotinfo) { if(norm) { padded = false; var top = norm==='fraction' ? 1 : 100, + relAndNegative = false, tiny = top/1e9; // in case of rounding error in sum sMin = 0; sMax = stack ? top : 0; for(i = 0; i < bl.length; i++) { // trace index ti = gd.calcdata[bl[i]]; for(j = 0; j < ti.length; j++) { - scale = top / sums[Math.round(ti[j].p/sumround)]; + relAndNegative = relative && ti[j].s < 0; + sv = Math.round(ti[j].p / sumround); + if(relAndNegative) sv = -sv; // locate negative sum amount for this p val + scale = top / sums[sv]; + if(relAndNegative) scale *= -1; // preserve sign if negative ti[j].b *= scale; ti[j].s *= scale; barEnd = ti[j].b + ti[j].s; diff --git a/test/image/baselines/bar_stackrelative_negative.png b/test/image/baselines/bar_stackrelative_negative.png new file mode 100644 index 00000000000..68090397c57 Binary files /dev/null and b/test/image/baselines/bar_stackrelative_negative.png differ diff --git a/test/image/baselines/bar_stackrelativeto100_negative.png b/test/image/baselines/bar_stackrelativeto100_negative.png new file mode 100644 index 00000000000..7dd1d60bce9 Binary files /dev/null and b/test/image/baselines/bar_stackrelativeto100_negative.png differ diff --git a/test/image/mocks/bar_stackrelative_negative.json b/test/image/mocks/bar_stackrelative_negative.json new file mode 100644 index 00000000000..2849dda2747 --- /dev/null +++ b/test/image/mocks/bar_stackrelative_negative.json @@ -0,0 +1,33 @@ +{ + "data":[ + { + "name":"Col1", + "y":["-1","2","3","4","5"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col2", + "y":["2","3","4","-3","2"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col3", + "y":["5","4","3","-2","1"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col4", + "y":["-3","0","1","0","-3"], + "x":["1","2","3","4","5"], + "type":"bar" + } + ], + "layout":{ + "height":400, + "width":400, + "barmode":"relative" + } +} diff --git a/test/image/mocks/bar_stackrelativeto100_negative.json b/test/image/mocks/bar_stackrelativeto100_negative.json new file mode 100644 index 00000000000..16b6b18ca64 --- /dev/null +++ b/test/image/mocks/bar_stackrelativeto100_negative.json @@ -0,0 +1,34 @@ +{ + "data":[ + { + "name":"Col1", + "y":["-1","2","3","4","5"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col2", + "y":["2","3","4","-3","2"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col3", + "y":["5","4","3","-2","1"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col4", + "y":["-3","0","1","0","-3"], + "x":["1","2","3","4","5"], + "type":"bar" + } + ], + "layout":{ + "height":400, + "width":400, + "barmode":"relative", + "barnorm":"percent" + } +} diff --git a/test/image/mocks/bar_stackto100_negative.json b/test/image/mocks/bar_stackto100_negative.json index 2fce003f1f2..e6e81b934ba 100644 --- a/test/image/mocks/bar_stackto100_negative.json +++ b/test/image/mocks/bar_stackto100_negative.json @@ -1,9 +1,29 @@ { "data":[ - {"name":"Col1","y":["1","2","3","4","5"],"x":["1","2","3","4","5"],"type":"bar","uid":"aeb9ea"}, - {"name":"Col2","y":["2","3","4","3","2"],"x":["1","2","3","4","5"],"type":"bar","uid":"2f201d"}, - {"name":"Col3","y":["5","4","3","2","1"],"x":["1","2","3","4","5"],"type":"bar","uid":"aef0bf"}, - {"name":"Col4","y":["-1","0","1","0","-1"],"x":["1","2","3","4","5"],"type":"bar","uid":"330b4d"} + { + "name":"Col1", + "y":["1","2","3","4","5"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col2", + "y":["2","3","4","3","2"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col3", + "y":["5","4","3","2","1"], + "x":["1","2","3","4","5"], + "type":"bar" + }, + { + "name":"Col4", + "y":["-1","0","1","0","-1"], + "x":["1","2","3","4","5"], + "type":"bar" + } ], "layout":{ "height":400,