From 2b404c6293fd80826fab604eb04495ee4ba88299 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 8 Nov 2016 16:51:02 +0000 Subject: [PATCH 01/15] bar: add text-related attributes * Added attributes textposition, textfont, insidetextfont and outsidetextfont. * All these attributes accept arrays. --- src/plot_api/helpers.js | 2 +- src/traces/bar/attributes.js | 28 +++++++ src/traces/bar/defaults.js | 140 ++++++++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 4d432cea9d6..d813dfc70f4 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -298,7 +298,7 @@ exports.cleanData = function(data, existingData) { trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); } - if(!Registry.traceIs(trace, 'pie')) { + if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) { if(Array.isArray(trace.textposition)) { trace.textposition = trace.textposition.map(cleanTextPosition); } diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 26436e87656..c3c64cf2ad8 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -12,6 +12,7 @@ var scatterAttrs = require('../scatter/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); var errorBarAttrs = require('../../components/errorbars/attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); +var fontAttrs = require('../../plots/font_attributes'); var extendFlat = require('../../lib/extend').extendFlat; @@ -40,8 +41,35 @@ module.exports = { y: scatterAttrs.y, y0: scatterAttrs.y0, dy: scatterAttrs.dy, + text: scatterAttrs.text, + textposition: { + valType: 'enumerated', + role: 'info', + values: ['inside', 'outside', 'auto', 'none'], + dflt: 'none', + arrayOk: true, + description: [ + 'Specifies the location of the `textinfo`.' + ].join(' ') + }, + + textfont: extendFlat({}, fontAttrs, { + arrayOk: true, + description: 'Sets the font used for `textinfo`.' + }), + + insidetextfont: extendFlat({}, fontAttrs, { + arrayOk: true, + description: 'Sets the font used for `textinfo` lying inside the bar.' + }), + + outsidetextfont: extendFlat({}, fontAttrs, { + arrayOk: true, + description: 'Sets the font used for `textinfo` lying outside the bar.' + }), + orientation: { valType: 'enumerated', role: 'info', diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 6e094021140..95f9fd56f86 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -9,6 +9,9 @@ 'use strict'; +var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); + var Lib = require('../../lib'); var Color = require('../../components/color'); @@ -23,6 +26,125 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + function coerceEnumerated(attributeDefinition, value, defaultValue) { + if(attributeDefinition.coerceNumber) value = +value; + + if(attributeDefinition.values.indexOf(value) !== -1) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; + } + + function coerceString(attributeDefinition, value, defaultValue) { + if(typeof value === 'string') { + if(value || !attributeDefinition.noBlank) return value; + } + else if(typeof value === 'number') { + if(!attributeDefinition.strict) return String(value); + } + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; + } + + function coerceNumber(attributeDefinition, value, defaultValue) { + if(isNumeric(value)) { + value = +value; + + var min = attributeDefinition.min, + max = attributeDefinition.max, + isOutOfBounds = (min !== undefined && value < min) || + (max !== undefined && value > max); + + if(!isOutOfBounds) return value; + } + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; + } + + function coerceColor(attributeDefinition, value, defaultValue) { + if(tinycolor(value).isValid()) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; + } + + function coerceFont(attributeDefinition, value, defaultValue) { + value = value || {}; + defaultValue = defaultValue || {}; + + return { + family: coerceString( + attributeDefinition.family, value.family, defaultValue.family), + size: coerceNumber( + attributeDefinition.size, value.size, defaultValue.size), + color: coerceColor( + attributeDefinition.color, value.color, defaultValue.color) + }; + } + + function coerceArray(attribute, coerceFunction, defaultValue, defaultValue2) { + var attributeDefinition = attributes[attribute], + arrayOk = attributeDefinition.arrayOk, + inValue = traceIn[attribute], + inValueIsArray = Array.isArray(inValue), + defaultValueIsArray = Array.isArray(defaultValue), + outValue, + i; + + // Case: inValue and defaultValue not treated as arrays + if(!arrayOk || (!inValueIsArray && !defaultValueIsArray)) { + outValue = coerceFunction( + attributeDefinition, inValue, defaultValue); + traceOut[attribute] = outValue; + return outValue; + } + + // Coerce into an array + outValue = []; + + // Case: defaultValue is an array and inValue isn't + if(!inValueIsArray) { + for(i = 0; i < defaultValue.length; i++) { + outValue.push( + coerceFunction( + attributeDefinition, inValue, defaultValue[i])); + } + } + + // Case: inValue is an array and defaultValue isn't + else if(!defaultValueIsArray) { + for(i = 0; i < inValue.length; i++) { + outValue.push( + coerceFunction( + attributeDefinition, inValue[i], defaultValue)); + } + } + + // Case: inValue and defaultValue are both arrays + else { + for(i = 0; i < defaultValue.length; i++) { + outValue.push( + coerceFunction( + attributeDefinition, inValue[i], defaultValue[i])); + } + for(; i < inValue.length; i++) { + outValue.push( + coerceFunction( + attributeDefinition, inValue[i], defaultValue2)); + } + } + + traceOut[attribute] = outValue; + + return outValue; + } + var len = handleXYDefaults(traceIn, traceOut, coerce); if(!len) { traceOut.visible = false; @@ -33,7 +155,23 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('base'); coerce('offset'); coerce('width'); - coerce('text'); + + coerceArray('text', coerceString); + + var textPosition = coerceArray('textposition', coerceEnumerated); + + var hasBoth = Array.isArray(textPosition) || textPosition === 'auto', + hasInside = hasBoth || textPosition === 'inside', + hasOutside = hasBoth || textPosition === 'outside'; + if(hasInside || hasOutside) { + var textFont = coerceArray('textfont', coerceFont, layout.font); + if(hasInside) { + coerceArray('insidetextfont', coerceFont, textFont, layout.font); + } + if(hasOutside) { + coerceArray('outsidetextfont', coerceFont, textFont, layout.font); + } + } handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); From f3a45af488d144a8ad517e829a7bc3f10f72cce7 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 8 Nov 2016 16:53:32 +0000 Subject: [PATCH 02/15] bar: draw bar texts --- src/traces/bar/plot.js | 275 +++++++++++++++++++++++++++++++- src/traces/bar/set_positions.js | 25 ++- 2 files changed, 291 insertions(+), 9 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 4e0cb53ac29..0f4cb7f0558 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -13,7 +13,10 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); +var svgTextUtils = require('../../lib/svg_text_utils'); + var Color = require('../../components/color'); +var Drawing = require('../../components/drawing'); var ErrorBars = require('../../components/errorbars'); var arraysToCalcdata = require('./arrays_to_calcdata'); @@ -42,9 +45,9 @@ module.exports = function plot(gd, plotinfo, cdbar) { arraysToCalcdata(d); - d3.select(this).selectAll('path') + d3.select(this).selectAll('g.point') .data(Lib.identity) - .enter().append('path') + .enter().append('g').classed('point', true) .each(function(di, i) { // now display the bar // clipped xf/yf (2nd arg true): non-positive @@ -75,15 +78,18 @@ module.exports = function plot(gd, plotinfo, cdbar) { d3.select(this).remove(); return; } + var lw = (di.mlw + 1 || trace.marker.line.width + 1 || (di.trace ? di.trace.marker.line.width : 0) + 1) - 1, offset = d3.round((lw / 2) % 1, 2); + function roundWithLine(v) { // if there are explicit gaps, don't round, // it can make the gaps look crappy return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ? d3.round(Math.round(v) - offset, 2) : v; } + function expandToVisible(v, vc) { // if it's not in danger of disappearing entirely, // round more precisely @@ -93,6 +99,7 @@ module.exports = function plot(gd, plotinfo, cdbar) { // its neighbor (v > vc ? Math.ceil(v) : Math.floor(v)); } + if(!gd._context.staticPlot) { // if bars are not fully opaque or they have a line // around them, round to integer pixels, mainly for @@ -108,8 +115,14 @@ module.exports = function plot(gd, plotinfo, cdbar) { y0 = fixpx(y0, y1); y1 = fixpx(y1, y0); } - d3.select(this).attr('d', + + // append bar path and text + var bar = d3.select(this); + + bar.append('path').attr('d', 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z'); + + appendBarText(gd, bar, d, i, x0, x1, y0, y1); }); }); @@ -117,3 +130,259 @@ module.exports = function plot(gd, plotinfo, cdbar) { bartraces.call(ErrorBars.plot, plotinfo); }; + +function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { + var trace = calcTrace[0].trace; + + // get bar text + var traceText = trace.text; + if(!traceText) return; + + var text; + if(Array.isArray(traceText)) { + if(i >= traceText.length) return; + text = traceText[i]; + } + else { + text = traceText; + } + + // get text position + var traceTextPosition = trace.textposition, + textPosition; + if(Array.isArray(traceTextPosition)) { + if(i >= traceTextPosition.length) return; + textPosition = traceTextPosition[i]; + } + else { + textPosition = traceTextPosition; + } + + if(textPosition === 'none') return; + + var barWidth = Math.abs(x1 - x0), + barHeight = Math.abs(y1 - y0), + barIsTooSmall = (barWidth < 8 || barHeight < 8), + + barmode = gd._fullLayout.barmode, + inStackMode = (barmode === 'stack'), + inRelativeMode = (barmode === 'relative'), + inStackOrRelativeMode = inStackMode || inRelativeMode, + + calcBar = calcTrace[i], + isOutmostBar = !inStackOrRelativeMode || calcBar._outmost; + + if(textPosition === 'auto') { + textPosition = (barIsTooSmall && isOutmostBar) ? 'outside' : 'inside'; + } + + if(textPosition === 'outside') { + if(!isOutmostBar) textPosition = 'inside'; + } + + if(textPosition === 'inside') { + if(barIsTooSmall) return; + } + + + // get text font + var textFont; + + if(textPosition === 'outside') { + var traceOutsideTextFont = trace.outsidetextfont; + if(Array.isArray(traceOutsideTextFont)) { + if(i >= traceOutsideTextFont.length) return; + textFont = traceOutsideTextFont[i]; + } + else { + textFont = traceOutsideTextFont; + } + } + else { + var traceInsideTextFont = trace.insidetextfont; + if(Array.isArray(traceInsideTextFont)) { + if(i >= traceInsideTextFont.length) return; + textFont = traceInsideTextFont[i]; + } + else { + textFont = traceInsideTextFont; + } + } + + if(!textFont) { + var traceTextFont = trace.textfont; + if(Array.isArray(traceTextFont)) { + if(i >= traceTextFont.length) return; + textFont = traceTextFont[i]; + } + else { + textFont = traceTextFont; + } + } + + if(!textFont) { + textFont = gd._fullLayout.font; + } + + // append bar text + var textSelection = bar.append('text') + // prohibit tex interpretation until we can handle + // tex and regular text together + .attr('data-notex', 1) + .text(text) + .attr({ + 'class': 'bartext', + transform: '', + 'data-bb': '', + 'text-anchor': 'middle', + x: 0, + y: 0 + }) + .call(Drawing.font, textFont); + + textSelection.call(svgTextUtils.convertToTspans); + textSelection.selectAll('tspan.line').attr({x: 0, y: 0}); + + // position bar text + var textBB = Drawing.bBox(textSelection.node()), + textWidth = textBB.width, + textHeight = textBB.height; + if(!textWidth || !textHeight) { + textSelection.remove(); + return; + } + + // compute translate transform + var transform; + if(textPosition === 'outside') { + transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, + trace.orientation); + } + else { + transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB); + } + + textSelection.attr('transform', transform); +} + +function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) { + // compute text and target positions + var barWidth = Math.abs(x1 - x0), + barHeight = Math.abs(y1 - y0), + textWidth = textBB.width, + textHeight = textBB.height, + barX = (x0 + x1) / 2, + barY = (y0 + y1) / 2, + textX = (textBB.left + textBB.right) / 2, + textY = (textBB.top + textBB.bottom) / 2; + + // apply target padding + var targetWidth = 0.95 * barWidth, + targetHeight = 0.95 * barHeight; + + return getTransform( + textX, textY, textWidth, textHeight, + barX, barY, targetWidth, targetHeight); +} + +function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { + + // compute text and target positions + var textWidth = textBB.width, + textHeight = textBB.height, + textX = (textBB.left + textBB.right) / 2, + textY = (textBB.top + textBB.bottom) / 2; + + var targetWidth, targetHeight, + targetX, targetY; + if(orientation === 'h') { + if(x1 < x0) { + // bar end is on the left hand side + targetWidth = 2 + textWidth; // padding included + targetHeight = Math.abs(y1 - y0); + targetX = x1 - targetWidth / 2; + targetY = (y0 + y1) / 2; + } + else { + targetWidth = 2 + textWidth; // padding included + targetHeight = Math.abs(y1 - y0); + targetX = x1 + targetWidth / 2; + targetY = (y0 + y1) / 2; + } + } + else { + if(y1 > y0) { + // bar end is on the bottom + targetWidth = Math.abs(x1 - x0); + targetHeight = 2 + textHeight; // padding included + targetX = (x0 + x1) / 2; + targetY = y1 + targetHeight / 2; + } + else { + targetWidth = Math.abs(x1 - x0); + targetHeight = 2 + textHeight; // padding included + targetX = (x0 + x1) / 2; + targetY = y1 - targetHeight / 2; + } + } + + return getTransform( + textX, textY, textWidth, textHeight, + targetX, targetY, targetWidth, targetHeight); +} + +/** + * Compute SVG transform to move a text box into a target box + * + * @param {number} textX X pixel coord of the text box center + * @param {number} textY Y pixel coord of the text box center + * @param {number} textWidth text box width + * @param {number} textHeight text box height + * @param {number} targetX X pixel coord of the target box center + * @param {number} targetY Y pixel coord of the target box center + * @param {number} targetWidth target box width + * @param {number} targetHeight target box height + * + * @returns {string} SVG transform + */ +function getTransform( + textX, textY, textWidth, textHeight, + targetX, targetY, targetWidth, targetHeight) { + + // compute translate transform + var translateX = targetX - textX, + translateY = targetY - textY, + translate = 'translate(' + translateX + ' ' + translateY + ')'; + + // if bar text doesn't fit, compute rotate and scale transforms + var doesntFit = (textWidth > targetWidth || textHeight > targetHeight), + rotate, scale, scaleX, scaleY; + + if(doesntFit) { + var textIsHorizontal = (textWidth > textHeight), + targetIsHorizontal = (targetWidth > targetHeight); + if(textIsHorizontal !== targetIsHorizontal) { + rotate = 'rotate(-90 ' + textX + ' ' + textY + ')'; + scaleX = targetWidth / textHeight; + scaleY = targetHeight / textWidth; + } + else { + scaleX = targetWidth / textWidth; + scaleY = targetHeight / textHeight; + } + + if(scaleX > 1) scaleX = 1; + if(scaleY > 1) scaleY = 1; + + if(scaleX !== 1 || scaleY !== 1) { + scale = 'scale(' + scaleX + ' ' + scaleY + ')'; + } + } + + // compute transform + var transform = translate; + if(scale) transform += ' ' + scale; + if(rotate) transform += ' ' + rotate; + + return transform; +} diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index c34adab1b02..7ad3b655c1e 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -179,6 +179,24 @@ function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) { // set bar bases and sizes, and update size axis stackBars(gd, sa, sieve); + + // flag the outmost bar (for text display purposes) + for(var i = 0; i < calcTraces.length; i++) { + var calcTrace = calcTraces[i]; + + for(var j = 0; j < calcTrace.length; j++) { + var bar = calcTrace[j]; + + if(!isNumeric(bar.s)) continue; + + var isOutmostBar = ((bar.b + bar.s) === sieve.get(bar.p, bar.s)); + if(isOutmostBar) bar._outmost = true; + } + } + + // Note that marking the outmost bars has to be done + // before `normalizeBars` changes `bar.b` and `bar.s`. + if(barnorm) normalizeBars(gd, sa, sieve); } @@ -506,12 +524,7 @@ function stackBars(gd, sa, sieve) { } // if barnorm is set, let normalizeBars update the axis range - if(barnorm) { - normalizeBars(gd, sa, sieve); - } - else { - Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); - } + if(!barnorm) Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true}); } From 2e583de3d6afab5394396eb59c7144680151c265 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 9 Nov 2016 15:15:56 +0000 Subject: [PATCH 03/15] bar: fix indentation of .enter() --- src/traces/bar/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 0f4cb7f0558..98b85fd13ae 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -47,7 +47,7 @@ module.exports = function plot(gd, plotinfo, cdbar) { d3.select(this).selectAll('g.point') .data(Lib.identity) - .enter().append('g').classed('point', true) + .enter().append('g').classed('point', true) .each(function(di, i) { // now display the bar // clipped xf/yf (2nd arg true): non-positive From 7df4e090702f81ccd79be15dc47ba7ca7c5759b8 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 11 Nov 2016 13:05:39 +0000 Subject: [PATCH 04/15] bar: increase text padding to 3px --- src/traces/bar/plot.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 98b85fd13ae..b5d661b0320 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -276,9 +276,9 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) { textX = (textBB.left + textBB.right) / 2, textY = (textBB.top + textBB.bottom) / 2; - // apply target padding - var targetWidth = 0.95 * barWidth, - targetHeight = 0.95 * barHeight; + // apply 3px target padding + var targetWidth = barWidth - 6, + targetHeight = barHeight - 6; return getTransform( textX, textY, textWidth, textHeight, @@ -298,14 +298,14 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { if(orientation === 'h') { if(x1 < x0) { // bar end is on the left hand side - targetWidth = 2 + textWidth; // padding included - targetHeight = Math.abs(y1 - y0); + targetWidth = textWidth + 6; // 3px padding included + targetHeight = Math.abs(y1 - y0) - 6; targetX = x1 - targetWidth / 2; targetY = (y0 + y1) / 2; } else { - targetWidth = 2 + textWidth; // padding included - targetHeight = Math.abs(y1 - y0); + targetWidth = textWidth + 6; // padding included + targetHeight = Math.abs(y1 - y0) - 6; targetX = x1 + targetWidth / 2; targetY = (y0 + y1) / 2; } From 3fd0d18cfab54a524e48cb7ec0d2f69b1b529dce Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 11 Nov 2016 16:08:28 +0000 Subject: [PATCH 05/15] bar: change criteria for `textposition: 'auto'` * With the new criteria, bar texts are drawn inside bars except in those cases that would require scaling down. --- src/traces/bar/plot.js | 169 +++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 75 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index b5d661b0320..d6ca0710799 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -21,6 +21,8 @@ var ErrorBars = require('../../components/errorbars'); var arraysToCalcdata = require('./arrays_to_calcdata'); +// padding in pixels around text +var TEXTPAD = 3; module.exports = function plot(gd, plotinfo, cdbar) { var xa = plotinfo.xaxis, @@ -147,6 +149,8 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { text = traceText; } + if(!text) return; + // get text position var traceTextPosition = trace.textposition, textPosition; @@ -160,21 +164,63 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { if(textPosition === 'none') return; - var barWidth = Math.abs(x1 - x0), - barHeight = Math.abs(y1 - y0), - barIsTooSmall = (barWidth < 8 || barHeight < 8), + // get text font + var traceTextFont = trace.textfont, + textFont = (Array.isArray(traceTextFont)) ? + traceTextFont[i] : traceTextFont; + textFont = textFont || gd._fullLayout.font; + + // get outside text font + var traceOutsideTextFont = trace.outsidetextfont, + outsideTextFont = (Array.isArray(traceOutsideTextFont)) ? + traceOutsideTextFont[i] : traceOutsideTextFont; + outsideTextFont = outsideTextFont || textFont; + + // get inside text font + var traceInsideTextFont = trace.insidetextfont, + insideTextFont = (Array.isArray(traceInsideTextFont)) ? + traceInsideTextFont[i] : traceInsideTextFont; + insideTextFont = insideTextFont || textFont; + + // append text node + function appendTextNode(bar, text, textFont) { + var textSelection = bar.append('text') + // prohibit tex interpretation until we can handle + // tex and regular text together + .attr('data-notex', 1) + .text(text) + .attr({ + 'class': 'bartext', + transform: '', + 'data-bb': '', + 'text-anchor': 'middle', + x: 0, + y: 0 + }) + .call(Drawing.font, textFont); + + textSelection.call(svgTextUtils.convertToTspans); + textSelection.selectAll('tspan.line').attr({x: 0, y: 0}); + + return textSelection; + } - barmode = gd._fullLayout.barmode, + var barmode = gd._fullLayout.barmode, inStackMode = (barmode === 'stack'), inRelativeMode = (barmode === 'relative'), inStackOrRelativeMode = inStackMode || inRelativeMode, calcBar = calcTrace[i], - isOutmostBar = !inStackOrRelativeMode || calcBar._outmost; + isOutmostBar = !inStackOrRelativeMode || calcBar._outmost, - if(textPosition === 'auto') { - textPosition = (barIsTooSmall && isOutmostBar) ? 'outside' : 'inside'; - } + barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, // padding excluded + barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, // padding excluded + barIsTooSmall = (barWidth <= 0 || barHeight <= 0), + + textSelection, + textBB, + textWidth, + textHeight; if(textPosition === 'outside') { if(!isOutmostBar) textPosition = 'inside'; @@ -184,72 +230,46 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { if(barIsTooSmall) return; } - - // get text font - var textFont; - - if(textPosition === 'outside') { - var traceOutsideTextFont = trace.outsidetextfont; - if(Array.isArray(traceOutsideTextFont)) { - if(i >= traceOutsideTextFont.length) return; - textFont = traceOutsideTextFont[i]; - } - else { - textFont = traceOutsideTextFont; - } - } - else { - var traceInsideTextFont = trace.insidetextfont; - if(Array.isArray(traceInsideTextFont)) { - if(i >= traceInsideTextFont.length) return; - textFont = traceInsideTextFont[i]; - } - else { - textFont = traceInsideTextFont; - } - } - - if(!textFont) { - var traceTextFont = trace.textfont; - if(Array.isArray(traceTextFont)) { - if(i >= traceTextFont.length) return; - textFont = traceTextFont[i]; - } - else { - textFont = traceTextFont; + if(textPosition === 'auto') { + if(isOutmostBar) { + // draw text using insideTextFont and check if it fits inside bar + textSelection = appendTextNode(bar, text, insideTextFont); + + textBB = Drawing.bBox(textSelection.node()), + textWidth = textBB.width, + textHeight = textBB.height; + + var textHasSize = (textWidth > 0 && textHeight > 0), + fitsInside = + (textWidth <= barWidth && textHeight <= barHeight), + fitsInsideIfRotated = + (textWidth <= barHeight && textHeight <= barWidth); + if(textHasSize && (fitsInside || fitsInsideIfRotated)) { + textPosition = 'inside'; + } + else { + textPosition = 'outside'; + textSelection.remove(); + textSelection = null; + } } + else if(!barIsTooSmall) textPosition = 'inside'; + else return; } - if(!textFont) { - textFont = gd._fullLayout.font; - } + if(!textSelection) { + textSelection = appendTextNode(bar, text, + (textPosition === 'outside') ? + outsideTextFont : insideTextFont); - // append bar text - var textSelection = bar.append('text') - // prohibit tex interpretation until we can handle - // tex and regular text together - .attr('data-notex', 1) - .text(text) - .attr({ - 'class': 'bartext', - transform: '', - 'data-bb': '', - 'text-anchor': 'middle', - x: 0, - y: 0 - }) - .call(Drawing.font, textFont); - - textSelection.call(svgTextUtils.convertToTspans); - textSelection.selectAll('tspan.line').attr({x: 0, y: 0}); - - // position bar text - var textBB = Drawing.bBox(textSelection.node()), + textBB = Drawing.bBox(textSelection.node()), textWidth = textBB.width, textHeight = textBB.height; - if(!textWidth || !textHeight) { - textSelection.remove(); - return; + + if(textWidth <= 0 || textHeight <= 0) { + textSelection.remove(); + return; + } } // compute translate transform @@ -277,8 +297,8 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) { textY = (textBB.top + textBB.bottom) / 2; // apply 3px target padding - var targetWidth = barWidth - 6, - targetHeight = barHeight - 6; + var targetWidth = barWidth - 2 * TEXTPAD, + targetHeight = barHeight - 2 * TEXTPAD; return getTransform( textX, textY, textWidth, textHeight, @@ -286,7 +306,6 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) { } function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { - // compute text and target positions var textWidth = textBB.width, textHeight = textBB.height, @@ -298,14 +317,14 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { if(orientation === 'h') { if(x1 < x0) { // bar end is on the left hand side - targetWidth = textWidth + 6; // 3px padding included - targetHeight = Math.abs(y1 - y0) - 6; + targetWidth = textWidth + 2 * TEXTPAD; // padding included + targetHeight = Math.abs(y1 - y0) - 2 * TEXTPAD; targetX = x1 - targetWidth / 2; targetY = (y0 + y1) / 2; } else { - targetWidth = textWidth + 6; // padding included - targetHeight = Math.abs(y1 - y0) - 6; + targetWidth = textWidth + 2 * TEXTPAD; // padding included + targetHeight = Math.abs(y1 - y0) - 2 * TEXTPAD; targetX = x1 + targetWidth / 2; targetY = (y0 + y1) / 2; } From 13f0f6af58a8d82d5cdacfe6d2e594da5ab515cf Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Sat, 12 Nov 2016 01:34:08 +0000 Subject: [PATCH 06/15] bar: update algorithm to position bar texts * Inside texts are rotated only if the rotation helps shrink the text as little as possible. * Outside texts are rotated only if the rotation doesn't leave the text perpendicular to the bar. --- src/traces/bar/plot.js | 233 ++++++++++++++++++++++++++--------------- 1 file changed, 147 insertions(+), 86 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index d6ca0710799..05ee5a7c666 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -272,136 +272,197 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { } } - // compute translate transform + // set text transform var transform; if(textPosition === 'outside') { transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, trace.orientation); } else { - transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB); + transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, + trace.orientation); } textSelection.attr('transform', transform); } -function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) { +function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { // compute text and target positions - var barWidth = Math.abs(x1 - x0), - barHeight = Math.abs(y1 - y0), - textWidth = textBB.width, + var textWidth = textBB.width, textHeight = textBB.height, - barX = (x0 + x1) / 2, - barY = (y0 + y1) / 2, textX = (textBB.left + textBB.right) / 2, - textY = (textBB.top + textBB.bottom) / 2; - - // apply 3px target padding - var targetWidth = barWidth - 2 * TEXTPAD, - targetHeight = barHeight - 2 * TEXTPAD; - - return getTransform( - textX, textY, textWidth, textHeight, - barX, barY, targetWidth, targetHeight); -} + textY = (textBB.top + textBB.bottom) / 2, + barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, + barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, + targetWidth, + targetHeight, + targetX, + targetY; + + // compute rotation and scale + var needsRotating, + scale; + + if(textWidth <= barWidth && textHeight <= barHeight) { + // no scale or rotation is required + needsRotating = false; + scale = 1; + } + else if(textWidth <= barHeight && textHeight <= barWidth) { + // only rotation is required + needsRotating = true; + scale = 1; + } + else if((textWidth < textHeight) === (barWidth < barHeight)) { + // only scale is required + needsRotating = false; + scale = Math.min(barWidth / textWidth, barHeight / textHeight); + } + else { + // both scale and rotation are required + needsRotating = true; + scale = Math.min(barHeight / textWidth, barWidth / textHeight); + } -function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { // compute text and target positions - var textWidth = textBB.width, - textHeight = textBB.height, - textX = (textBB.left + textBB.right) / 2, - textY = (textBB.top + textBB.bottom) / 2; + if(needsRotating) { + targetWidth = scale * textHeight; + targetHeight = scale * textWidth; + } + else { + targetWidth = scale * textWidth; + targetHeight = scale * textHeight; + } - var targetWidth, targetHeight, - targetX, targetY; if(orientation === 'h') { if(x1 < x0) { // bar end is on the left hand side - targetWidth = textWidth + 2 * TEXTPAD; // padding included - targetHeight = Math.abs(y1 - y0) - 2 * TEXTPAD; - targetX = x1 - targetWidth / 2; + targetX = x1 + TEXTPAD + targetWidth / 2; targetY = (y0 + y1) / 2; } else { - targetWidth = textWidth + 2 * TEXTPAD; // padding included - targetHeight = Math.abs(y1 - y0) - 2 * TEXTPAD; - targetX = x1 + targetWidth / 2; + targetX = x1 - TEXTPAD - targetWidth / 2; targetY = (y0 + y1) / 2; } } else { if(y1 > y0) { // bar end is on the bottom - targetWidth = Math.abs(x1 - x0); - targetHeight = 2 + textHeight; // padding included targetX = (x0 + x1) / 2; - targetY = y1 + targetHeight / 2; + targetY = y1 - TEXTPAD - targetHeight / 2; } else { - targetWidth = Math.abs(x1 - x0); - targetHeight = 2 + textHeight; // padding included targetX = (x0 + x1) / 2; - targetY = y1 - targetHeight / 2; + targetY = y1 + TEXTPAD + targetHeight / 2; } } - return getTransform( - textX, textY, textWidth, textHeight, - targetX, targetY, targetWidth, targetHeight); + return getTransform(textX, textY, targetX, targetY, scale, needsRotating); } -/** - * Compute SVG transform to move a text box into a target box - * - * @param {number} textX X pixel coord of the text box center - * @param {number} textY Y pixel coord of the text box center - * @param {number} textWidth text box width - * @param {number} textHeight text box height - * @param {number} targetX X pixel coord of the target box center - * @param {number} targetY Y pixel coord of the target box center - * @param {number} targetWidth target box width - * @param {number} targetHeight target box height - * - * @returns {string} SVG transform - */ -function getTransform( - textX, textY, textWidth, textHeight, - targetX, targetY, targetWidth, targetHeight) { - - // compute translate transform - var translateX = targetX - textX, - translateY = targetY - textY, - translate = 'translate(' + translateX + ' ' + translateY + ')'; - - // if bar text doesn't fit, compute rotate and scale transforms - var doesntFit = (textWidth > targetWidth || textHeight > targetHeight), - rotate, scale, scaleX, scaleY; - - if(doesntFit) { - var textIsHorizontal = (textWidth > textHeight), - targetIsHorizontal = (targetWidth > targetHeight); - if(textIsHorizontal !== targetIsHorizontal) { - rotate = 'rotate(-90 ' + textX + ' ' + textY + ')'; - scaleX = targetWidth / textHeight; - scaleY = targetHeight / textWidth; +function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { + // In order to handle both orientations with the same algorithm, + // *textWidth* is defined as the text length in the direction of *barWidth*. + var barWidth, + textWidth, + textHeight; + if(orientation === 'h') { + barWidth = Math.abs(y1 - y0) - 2 * TEXTPAD; + textWidth = textBB.height; + textHeight = textBB.width; + } + else { + barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD; + textWidth = textBB.width; + textHeight = textBB.height; + } + + // compute rotation and scale + var needsRotating, + scale; + if(textWidth <= barWidth) { + // no scale or rotation + needsRotating = false; + scale = 1; + } + else if(textHeight <= textWidth) { + // only scale + // (don't rotate to prevent having text perpendicular to the bar) + needsRotating = false; + scale = barWidth / textWidth; + } + else if(textHeight <= barWidth) { + // only rotation + needsRotating = true; + scale = 1; + } + else { + // both scale and rotation + // (rotation prevents having text perpendicular to the bar) + needsRotating = true; + scale = barWidth / textHeight; + } + + // compute text and target positions + var textX = (textBB.left + textBB.right) / 2, + textY = (textBB.top + textBB.bottom) / 2, + targetWidth, + targetHeight, + targetX, + targetY; + if(needsRotating) { + targetWidth = scale * textBB.height; + targetHeight = scale * textBB.width; + } + else { + targetWidth = scale * textBB.width; + targetHeight = scale * textBB.height; + } + + if(orientation === 'h') { + if(x1 < x0) { + // bar end is on the left hand side + targetX = x1 - TEXTPAD - targetWidth / 2; + targetY = (y0 + y1) / 2; } else { - scaleX = targetWidth / textWidth; - scaleY = targetHeight / textHeight; + targetX = x1 + TEXTPAD + targetWidth / 2; + targetY = (y0 + y1) / 2; } + } + else { + if(y1 > y0) { + // bar end is on the bottom + targetX = (x0 + x1) / 2; + targetY = y1 + TEXTPAD + targetHeight / 2; + } + else { + targetX = (x0 + x1) / 2; + targetY = y1 - TEXTPAD - targetHeight / 2; + } + } - if(scaleX > 1) scaleX = 1; - if(scaleY > 1) scaleY = 1; + return getTransform(textX, textY, targetX, targetY, scale, needsRotating); +} - if(scaleX !== 1 || scaleY !== 1) { - scale = 'scale(' + scaleX + ' ' + scaleY + ')'; - } +function getTransform(textX, textY, targetX, targetY, scale, needsRotating) { + var transformScale, + transformRotate, + transformTranslate; + + if(scale < 1) transformScale = 'scale(' + scale + ') '; + else { + scale = 1; + transformScale = ''; } - // compute transform - var transform = translate; - if(scale) transform += ' ' + scale; - if(rotate) transform += ' ' + rotate; + transformRotate = (needsRotating) ? + 'rotate(-90 ' + textX + ' ' + textY + ') ' : ''; + + // Note that scaling also affects the center of the text box + var translateX = (targetX - scale * textX), + translateY = (targetY - scale * textY); + transformTranslate = 'translate(' + translateX + ' ' + translateY + ')'; - return transform; + return transformTranslate + transformScale + transformRotate; } From efd6abdbcaa6b0b7c32deb34477688979645d7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Sat, 12 Nov 2016 02:12:48 +0000 Subject: [PATCH 07/15] bar: don't apply text padding if bar too small * This change ensures inside texts can be drawn, even inside bars smaller than the text padding. --- src/traces/bar/plot.js | 48 +++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 05ee5a7c666..def08cbb9cc 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -215,7 +215,6 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, // padding excluded barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, // padding excluded - barIsTooSmall = (barWidth <= 0 || barHeight <= 0), textSelection, textBB, @@ -226,10 +225,6 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { if(!isOutmostBar) textPosition = 'inside'; } - if(textPosition === 'inside') { - if(barIsTooSmall) return; - } - if(textPosition === 'auto') { if(isOutmostBar) { // draw text using insideTextFont and check if it fits inside bar @@ -253,8 +248,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { textSelection = null; } } - else if(!barIsTooSmall) textPosition = 'inside'; - else return; + else textPosition = 'inside'; } if(!textSelection) { @@ -292,13 +286,22 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { textHeight = textBB.height, textX = (textBB.left + textBB.right) / 2, textY = (textBB.top + textBB.bottom) / 2, - barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD, - barHeight = Math.abs(y1 - y0) - 2 * TEXTPAD, + barWidth = Math.abs(x1 - x0), + barHeight = Math.abs(y1 - y0), targetWidth, targetHeight, targetX, targetY; + // apply text padding + var textpad; + if(barWidth > 2 * TEXTPAD && barHeight > 2 * TEXTPAD) { + textpad = TEXTPAD; + barWidth -= 2 * textpad; + barHeight -= 2 * textpad; + } + else textpad = 0; + // compute rotation and scale var needsRotating, scale; @@ -337,11 +340,11 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { if(orientation === 'h') { if(x1 < x0) { // bar end is on the left hand side - targetX = x1 + TEXTPAD + targetWidth / 2; + targetX = x1 + textpad + targetWidth / 2; targetY = (y0 + y1) / 2; } else { - targetX = x1 - TEXTPAD - targetWidth / 2; + targetX = x1 - textpad - targetWidth / 2; targetY = (y0 + y1) / 2; } } @@ -349,11 +352,11 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { if(y1 > y0) { // bar end is on the bottom targetX = (x0 + x1) / 2; - targetY = y1 - TEXTPAD - targetHeight / 2; + targetY = y1 - textpad - targetHeight / 2; } else { targetX = (x0 + x1) / 2; - targetY = y1 + TEXTPAD + targetHeight / 2; + targetY = y1 + textpad + targetHeight / 2; } } @@ -367,16 +370,23 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { textWidth, textHeight; if(orientation === 'h') { - barWidth = Math.abs(y1 - y0) - 2 * TEXTPAD; + barWidth = Math.abs(y1 - y0); textWidth = textBB.height; textHeight = textBB.width; } else { - barWidth = Math.abs(x1 - x0) - 2 * TEXTPAD; + barWidth = Math.abs(x1 - x0); textWidth = textBB.width; textHeight = textBB.height; } + // apply text padding + var textpad; + if(barWidth > 2 * TEXTPAD) { + textpad = TEXTPAD; + barWidth -= 2 * textpad; + } + // compute rotation and scale var needsRotating, scale; @@ -422,11 +432,11 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { if(orientation === 'h') { if(x1 < x0) { // bar end is on the left hand side - targetX = x1 - TEXTPAD - targetWidth / 2; + targetX = x1 - textpad - targetWidth / 2; targetY = (y0 + y1) / 2; } else { - targetX = x1 + TEXTPAD + targetWidth / 2; + targetX = x1 + textpad + targetWidth / 2; targetY = (y0 + y1) / 2; } } @@ -434,11 +444,11 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { if(y1 > y0) { // bar end is on the bottom targetX = (x0 + x1) / 2; - targetY = y1 + TEXTPAD + targetHeight / 2; + targetY = y1 + textpad + targetHeight / 2; } else { targetX = (x0 + x1) / 2; - targetY = y1 - TEXTPAD - targetHeight / 2; + targetY = y1 - textpad - targetHeight / 2; } } From de8451887ae25ff348518a4c9772fae17fc5d9b3 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Mon, 14 Nov 2016 18:00:27 +0000 Subject: [PATCH 08/15] bar: update text rotation algorithm * If rotation is needed: - inside texts are rotated counter-clockwise. - outside texts are rotated clockwise or counter-clockwise, so that the text is laid along the bar end. --- src/traces/bar/plot.js | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index def08cbb9cc..e2e2d9199cf 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -303,32 +303,34 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { else textpad = 0; // compute rotation and scale - var needsRotating, + var rotate, scale; if(textWidth <= barWidth && textHeight <= barHeight) { // no scale or rotation is required - needsRotating = false; + rotate = false; scale = 1; } else if(textWidth <= barHeight && textHeight <= barWidth) { // only rotation is required - needsRotating = true; + rotate = true; scale = 1; } else if((textWidth < textHeight) === (barWidth < barHeight)) { // only scale is required - needsRotating = false; + rotate = false; scale = Math.min(barWidth / textWidth, barHeight / textHeight); } else { // both scale and rotation are required - needsRotating = true; + rotate = true; scale = Math.min(barHeight / textWidth, barWidth / textHeight); } + if(rotate) rotate = -90; // rotate counter-clockwise + // compute text and target positions - if(needsRotating) { + if(rotate) { targetWidth = scale * textHeight; targetHeight = scale * textWidth; } @@ -360,7 +362,7 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { } } - return getTransform(textX, textY, targetX, targetY, scale, needsRotating); + return getTransform(textX, textY, targetX, targetY, scale, rotate); } function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { @@ -388,28 +390,28 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { } // compute rotation and scale - var needsRotating, + var rotate, scale; if(textWidth <= barWidth) { // no scale or rotation - needsRotating = false; + rotate = false; scale = 1; } else if(textHeight <= textWidth) { // only scale // (don't rotate to prevent having text perpendicular to the bar) - needsRotating = false; + rotate = false; scale = barWidth / textWidth; } else if(textHeight <= barWidth) { // only rotation - needsRotating = true; + rotate = true; scale = 1; } else { // both scale and rotation // (rotation prevents having text perpendicular to the bar) - needsRotating = true; + rotate = true; scale = barWidth / textHeight; } @@ -420,7 +422,7 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { targetHeight, targetX, targetY; - if(needsRotating) { + if(rotate) { targetWidth = scale * textBB.height; targetHeight = scale * textBB.width; } @@ -434,10 +436,12 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { // bar end is on the left hand side targetX = x1 - textpad - targetWidth / 2; targetY = (y0 + y1) / 2; + if(rotate) rotate = -90; // rotate counter-clockwise } else { targetX = x1 + textpad + targetWidth / 2; targetY = (y0 + y1) / 2; + if(rotate) rotate = 90; // rotate clockwise } } else { @@ -445,17 +449,19 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { // bar end is on the bottom targetX = (x0 + x1) / 2; targetY = y1 + textpad + targetHeight / 2; + if(rotate) rotate = -90; // rotate counter-clockwise } else { targetX = (x0 + x1) / 2; targetY = y1 - textpad - targetHeight / 2; + if(rotate) rotate = 90; // rotate clockwise } } - return getTransform(textX, textY, targetX, targetY, scale, needsRotating); + return getTransform(textX, textY, targetX, targetY, scale, rotate); } -function getTransform(textX, textY, targetX, targetY, scale, needsRotating) { +function getTransform(textX, textY, targetX, targetY, scale, rotate) { var transformScale, transformRotate, transformTranslate; @@ -466,8 +472,8 @@ function getTransform(textX, textY, targetX, targetY, scale, needsRotating) { transformScale = ''; } - transformRotate = (needsRotating) ? - 'rotate(-90 ' + textX + ' ' + textY + ') ' : ''; + transformRotate = (rotate) ? + 'rotate(' + rotate + ' ' + textX + ' ' + textY + ') ' : ''; // Note that scaling also affects the center of the text box var translateX = (targetX - scale * textX), From 4a45032686ee4228c9bfbfb12840bac644710212 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 15 Nov 2016 16:23:05 +0000 Subject: [PATCH 09/15] bar: do not coerce elements of an array attribute --- src/traces/bar/defaults.js | 136 ++----------------------------------- 1 file changed, 6 insertions(+), 130 deletions(-) diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 95f9fd56f86..e907b40ad06 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -9,9 +9,6 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); - var Lib = require('../../lib'); var Color = require('../../components/color'); @@ -26,124 +23,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - function coerceEnumerated(attributeDefinition, value, defaultValue) { - if(attributeDefinition.coerceNumber) value = +value; - - if(attributeDefinition.values.indexOf(value) !== -1) return value; - - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; - } - - function coerceString(attributeDefinition, value, defaultValue) { - if(typeof value === 'string') { - if(value || !attributeDefinition.noBlank) return value; - } - else if(typeof value === 'number') { - if(!attributeDefinition.strict) return String(value); - } - - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; - } - - function coerceNumber(attributeDefinition, value, defaultValue) { - if(isNumeric(value)) { - value = +value; - - var min = attributeDefinition.min, - max = attributeDefinition.max, - isOutOfBounds = (min !== undefined && value < min) || - (max !== undefined && value > max); - - if(!isOutOfBounds) return value; - } - - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; - } - - function coerceColor(attributeDefinition, value, defaultValue) { - if(tinycolor(value).isValid()) return value; - - return (defaultValue !== undefined) ? - defaultValue : - attributeDefinition.dflt; - } - - function coerceFont(attributeDefinition, value, defaultValue) { - value = value || {}; - defaultValue = defaultValue || {}; - - return { - family: coerceString( - attributeDefinition.family, value.family, defaultValue.family), - size: coerceNumber( - attributeDefinition.size, value.size, defaultValue.size), - color: coerceColor( - attributeDefinition.color, value.color, defaultValue.color) - }; - } - - function coerceArray(attribute, coerceFunction, defaultValue, defaultValue2) { - var attributeDefinition = attributes[attribute], - arrayOk = attributeDefinition.arrayOk, - inValue = traceIn[attribute], - inValueIsArray = Array.isArray(inValue), - defaultValueIsArray = Array.isArray(defaultValue), - outValue, - i; - - // Case: inValue and defaultValue not treated as arrays - if(!arrayOk || (!inValueIsArray && !defaultValueIsArray)) { - outValue = coerceFunction( - attributeDefinition, inValue, defaultValue); - traceOut[attribute] = outValue; - return outValue; - } - - // Coerce into an array - outValue = []; - - // Case: defaultValue is an array and inValue isn't - if(!inValueIsArray) { - for(i = 0; i < defaultValue.length; i++) { - outValue.push( - coerceFunction( - attributeDefinition, inValue, defaultValue[i])); - } - } - - // Case: inValue is an array and defaultValue isn't - else if(!defaultValueIsArray) { - for(i = 0; i < inValue.length; i++) { - outValue.push( - coerceFunction( - attributeDefinition, inValue[i], defaultValue)); - } - } - - // Case: inValue and defaultValue are both arrays - else { - for(i = 0; i < defaultValue.length; i++) { - outValue.push( - coerceFunction( - attributeDefinition, inValue[i], defaultValue[i])); - } - for(; i < inValue.length; i++) { - outValue.push( - coerceFunction( - attributeDefinition, inValue[i], defaultValue2)); - } - } - - traceOut[attribute] = outValue; - - return outValue; - } + var coerceFont = Lib.coerceFont; var len = handleXYDefaults(traceIn, traceOut, coerce); if(!len) { @@ -156,21 +36,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('offset'); coerce('width'); - coerceArray('text', coerceString); + coerce('text'); - var textPosition = coerceArray('textposition', coerceEnumerated); + var textPosition = coerce('textposition'); var hasBoth = Array.isArray(textPosition) || textPosition === 'auto', hasInside = hasBoth || textPosition === 'inside', hasOutside = hasBoth || textPosition === 'outside'; if(hasInside || hasOutside) { - var textFont = coerceArray('textfont', coerceFont, layout.font); - if(hasInside) { - coerceArray('insidetextfont', coerceFont, textFont, layout.font); - } - if(hasOutside) { - coerceArray('outsidetextfont', coerceFont, textFont, layout.font); - } + var textFont = coerceFont(coerce, 'textfont', layout.font); + if(hasInside) coerceFont(coerce, 'insidetextfont', textFont); + if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont); } handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); From b06a80a383cf599259b5f20b0f6a310cb4e7cf41 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 15 Nov 2016 16:24:40 +0000 Subject: [PATCH 10/15] bar: accept font family, size and color arrays * Before this commit, font attributes accepted: [{family: family0, size: size0, color: color0}, {family: family1, size: size1, color: color1}, ...] * With this commit, font attributes accept: {family: [family0, family1, ...], size: [size0, size1, ...], color: [color0, color1, ...]} --- src/traces/bar/attributes.js | 15 ++-- src/traces/bar/plot.js | 170 ++++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 56 deletions(-) diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index c3c64cf2ad8..f1b50a9847f 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -15,6 +15,12 @@ var colorbarAttrs = require('../../components/colorbar/attributes'); var fontAttrs = require('../../plots/font_attributes'); var extendFlat = require('../../lib/extend').extendFlat; +var extendDeep = require('../../lib/extend').extendDeep; + +var textFontAttrs = extendDeep({}, fontAttrs); +textFontAttrs.family.arrayOk = true; +textFontAttrs.size.arrayOk = true; +textFontAttrs.color.arrayOk = true; var scatterMarkerAttrs = scatterAttrs.marker; var scatterMarkerLineAttrs = scatterMarkerAttrs.line; @@ -55,18 +61,15 @@ module.exports = { ].join(' ') }, - textfont: extendFlat({}, fontAttrs, { - arrayOk: true, + textfont: extendFlat({}, textFontAttrs, { description: 'Sets the font used for `textinfo`.' }), - insidetextfont: extendFlat({}, fontAttrs, { - arrayOk: true, + insidetextfont: extendFlat({}, textFontAttrs, { description: 'Sets the font used for `textinfo` lying inside the bar.' }), - outsidetextfont: extendFlat({}, fontAttrs, { - arrayOk: true, + outsidetextfont: extendFlat({}, textFontAttrs, { description: 'Sets the font used for `textinfo` lying outside the bar.' }), diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index e2e2d9199cf..e15c36bbf5c 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -11,6 +11,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); @@ -21,6 +22,13 @@ var ErrorBars = require('../../components/errorbars'); var arraysToCalcdata = require('./arrays_to_calcdata'); +var attributes = require('./attributes'), + attributeText = attributes.text, + attributeTextPosition = attributes.textposition, + attributeTextFont = attributes.textfont, + attributeInsideTextFont = attributes.insidetextfont, + attributeOutsideTextFont = attributes.outsidetextfont; + // padding in pixels around text var TEXTPAD = 3; @@ -134,55 +142,6 @@ module.exports = function plot(gd, plotinfo, cdbar) { }; function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { - var trace = calcTrace[0].trace; - - // get bar text - var traceText = trace.text; - if(!traceText) return; - - var text; - if(Array.isArray(traceText)) { - if(i >= traceText.length) return; - text = traceText[i]; - } - else { - text = traceText; - } - - if(!text) return; - - // get text position - var traceTextPosition = trace.textposition, - textPosition; - if(Array.isArray(traceTextPosition)) { - if(i >= traceTextPosition.length) return; - textPosition = traceTextPosition[i]; - } - else { - textPosition = traceTextPosition; - } - - if(textPosition === 'none') return; - - // get text font - var traceTextFont = trace.textfont, - textFont = (Array.isArray(traceTextFont)) ? - traceTextFont[i] : traceTextFont; - textFont = textFont || gd._fullLayout.font; - - // get outside text font - var traceOutsideTextFont = trace.outsidetextfont, - outsideTextFont = (Array.isArray(traceOutsideTextFont)) ? - traceOutsideTextFont[i] : traceOutsideTextFont; - outsideTextFont = outsideTextFont || textFont; - - // get inside text font - var traceInsideTextFont = trace.insidetextfont, - insideTextFont = (Array.isArray(traceInsideTextFont)) ? - traceInsideTextFont[i] : traceInsideTextFont; - insideTextFont = insideTextFont || textFont; - - // append text node function appendTextNode(bar, text, textFont) { var textSelection = bar.append('text') // prohibit tex interpretation until we can handle @@ -205,6 +164,20 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { return textSelection; } + // get trace attributes + var trace = calcTrace[0].trace; + + var text = getText(trace, i); + if(!text) return; + + var textPosition = getTextPosition(trace, i); + if(textPosition === 'none') return; + + var textFont = getTextFont(trace, i, gd._fullLayout.font), + insideTextFont = getInsideTextFont(trace, i, textFont), + outsideTextFont = getOutsideTextFont(trace, i, textFont); + + // compute text position var barmode = gd._fullLayout.barmode, inStackMode = (barmode === 'stack'), inRelativeMode = (barmode === 'relative'), @@ -266,7 +239,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { } } - // set text transform + // compute text transform var transform; if(textPosition === 'outside') { transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, @@ -482,3 +455,100 @@ function getTransform(textX, textY, targetX, targetY, scale, rotate) { return transformTranslate + transformScale + transformRotate; } + +function getText(trace, index) { + var value = getValue(trace.text, index); + return coerceString(attributeText, value); +} + +function getTextPosition(trace, index) { + var value = getValue(trace.textposition, index); + return coerceEnumerated(attributeTextPosition, value); +} + +function getTextFont(trace, index, defaultValue) { + return getFontValue( + attributeTextFont, trace.textfont, index, defaultValue); +} + +function getInsideTextFont(trace, index, defaultValue) { + return getFontValue( + attributeInsideTextFont, trace.insidetextfont, index, defaultValue); +} + +function getOutsideTextFont(trace, index, defaultValue) { + return getFontValue( + attributeOutsideTextFont, trace.outsidetextfont, index, defaultValue); +} + +function getFontValue(attributeDefinition, attributeValue, index, defaultValue) { + attributeValue = attributeValue || {}; + + var familyValue = getValue(attributeValue.family, index), + sizeValue = getValue(attributeValue.size, index), + colorValue = getValue(attributeValue.color, index); + + return { + family: coerceString( + attributeDefinition.family, familyValue, defaultValue.family), + size: coerceNumber( + attributeDefinition.size, sizeValue, defaultValue.size), + color: coerceColor( + attributeDefinition.color, colorValue, defaultValue.color) + }; +} + +function getValue(arrayOrScalar, index) { + var value; + if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; + else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; + return value; +} + +function coerceString(attributeDefinition, value, defaultValue) { + if(typeof value === 'string') { + if(value || !attributeDefinition.noBlank) return value; + } + else if(typeof value === 'number') { + if(!attributeDefinition.strict) return String(value); + } + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +} + +function coerceEnumerated(attributeDefinition, value, defaultValue) { + if(attributeDefinition.coerceNumber) value = +value; + + if(attributeDefinition.values.indexOf(value) !== -1) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +} + +function coerceNumber(attributeDefinition, value, defaultValue) { + if(isNumeric(value)) { + value = +value; + + var min = attributeDefinition.min, + max = attributeDefinition.max, + isOutOfBounds = (min !== undefined && value < min) || + (max !== undefined && value > max); + + if(!isOutOfBounds) return value; + } + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +} + +function coerceColor(attributeDefinition, value, defaultValue) { + if(tinycolor(value).isValid()) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +} From f7806c85cbf4de6c5938b04616bb064d216afdd8 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 16 Nov 2016 16:42:10 +0000 Subject: [PATCH 11/15] bar: bar texts only rotate clockwise --- src/traces/bar/plot.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index e15c36bbf5c..01e4369a850 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -300,7 +300,7 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { scale = Math.min(barHeight / textWidth, barWidth / textHeight); } - if(rotate) rotate = -90; // rotate counter-clockwise + if(rotate) rotate = 90; // rotate clockwise // compute text and target positions if(rotate) { @@ -388,6 +388,8 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { scale = barWidth / textHeight; } + if(rotate) rotate = 90; // rotate clockwise + // compute text and target positions var textX = (textBB.left + textBB.right) / 2, textY = (textBB.top + textBB.bottom) / 2, @@ -409,12 +411,10 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { // bar end is on the left hand side targetX = x1 - textpad - targetWidth / 2; targetY = (y0 + y1) / 2; - if(rotate) rotate = -90; // rotate counter-clockwise } else { targetX = x1 + textpad + targetWidth / 2; targetY = (y0 + y1) / 2; - if(rotate) rotate = 90; // rotate clockwise } } else { @@ -422,12 +422,10 @@ function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { // bar end is on the bottom targetX = (x0 + x1) / 2; targetY = y1 + textpad + targetHeight / 2; - if(rotate) rotate = -90; // rotate counter-clockwise } else { targetX = (x0 + x1) / 2; targetY = y1 - textpad - targetHeight / 2; - if(rotate) rotate = 90; // rotate clockwise } } From a1ef6942a06fcfffee4663cbca750219bc9ab1eb Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 16 Nov 2016 20:56:44 +0000 Subject: [PATCH 12/15] bar: simplify outside text positioning * Don't rotate outside labels. --- src/traces/bar/plot.js | 52 ++++++++---------------------------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 01e4369a850..750acd661fd 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -339,56 +339,22 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { } function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) { - // In order to handle both orientations with the same algorithm, - // *textWidth* is defined as the text length in the direction of *barWidth*. - var barWidth, - textWidth, - textHeight; - if(orientation === 'h') { - barWidth = Math.abs(y1 - y0); - textWidth = textBB.height; - textHeight = textBB.width; - } - else { - barWidth = Math.abs(x1 - x0); - textWidth = textBB.width; - textHeight = textBB.height; - } + var barWidth = (orientation === 'h') ? + Math.abs(y1 - y0) : + Math.abs(x1 - x0), + textpad; - // apply text padding - var textpad; + // apply text padding if possible if(barWidth > 2 * TEXTPAD) { textpad = TEXTPAD; barWidth -= 2 * textpad; } // compute rotation and scale - var rotate, - scale; - if(textWidth <= barWidth) { - // no scale or rotation - rotate = false; - scale = 1; - } - else if(textHeight <= textWidth) { - // only scale - // (don't rotate to prevent having text perpendicular to the bar) - rotate = false; - scale = barWidth / textWidth; - } - else if(textHeight <= barWidth) { - // only rotation - rotate = true; - scale = 1; - } - else { - // both scale and rotation - // (rotation prevents having text perpendicular to the bar) - rotate = true; - scale = barWidth / textHeight; - } - - if(rotate) rotate = 90; // rotate clockwise + var rotate = false, + scale = (orientation === 'h') ? + Math.min(1, barWidth / textBB.height) : + Math.min(1, barWidth / textBB.width); // compute text and target positions var textX = (textBB.left + textBB.right) / 2, From 5c8e7b461ae97f9a521d6c27fde311271fa63704 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 16 Nov 2016 22:16:39 +0000 Subject: [PATCH 13/15] bar: update criteria for 'textposition: 'auto'` * 'auto' will draw a label inside a bar if larger than drawn outside. --- src/traces/bar/plot.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 750acd661fd..db68a618f50 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -165,7 +165,8 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { } // get trace attributes - var trace = calcTrace[0].trace; + var trace = calcTrace[0].trace, + orientation = trace.orientation; var text = getText(trace, i); if(!text) return; @@ -211,8 +212,12 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { fitsInside = (textWidth <= barWidth && textHeight <= barHeight), fitsInsideIfRotated = - (textWidth <= barHeight && textHeight <= barWidth); - if(textHasSize && (fitsInside || fitsInsideIfRotated)) { + (textWidth <= barHeight && textHeight <= barWidth), + fitsInsideIfShrunk = (orientation === 'h') ? + (barWidth >= textWidth * (barHeight / textHeight)) : + (barHeight >= textHeight * (barWidth / textWidth)); + if(textHasSize && + (fitsInside || fitsInsideIfRotated || fitsInsideIfShrunk)) { textPosition = 'inside'; } else { @@ -243,11 +248,11 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { var transform; if(textPosition === 'outside') { transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, - trace.orientation); + orientation); } else { transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, - trace.orientation); + orientation); } textSelection.attr('transform', transform); @@ -268,7 +273,7 @@ function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) { // apply text padding var textpad; - if(barWidth > 2 * TEXTPAD && barHeight > 2 * TEXTPAD) { + if(barWidth > (2 * TEXTPAD) && barHeight > (2 * TEXTPAD)) { textpad = TEXTPAD; barWidth -= 2 * textpad; barHeight -= 2 * textpad; From b41f3cd17a069e589398f379e3150fe2b67af21c Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 16 Nov 2016 22:39:34 +0000 Subject: [PATCH 14/15] bar: describe `textposition` options --- src/traces/bar/attributes.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index f1b50a9847f..bba0d30611f 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -57,20 +57,26 @@ module.exports = { dflt: 'none', arrayOk: true, description: [ - 'Specifies the location of the `textinfo`.' + 'Specifies the location of the `text`.', + '*inside* positions `text` inside, next to the bar end', + '(rotated and scaled if needed).', + '*outside* positions `text` outside, next to the bar end', + '(scaled if needed).', + '*auto* positions `text` inside or outside', + 'so that `text` size is maximized.' ].join(' ') }, textfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `textinfo`.' + description: 'Sets the font used for `text`.' }), insidetextfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `textinfo` lying inside the bar.' + description: 'Sets the font used for `text` lying inside the bar.' }), outsidetextfont: extendFlat({}, textFontAttrs, { - description: 'Sets the font used for `textinfo` lying outside the bar.' + description: 'Sets the font used for `text` lying outside the bar.' }), orientation: { From b7bfc97f777904f32ae985b65d176c23e09411aa Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 8 Nov 2016 16:54:20 +0000 Subject: [PATCH 15/15] test: bar texts * bar_test.js: Test `textposition`. * bar_test.js: Test that `textposition` can also be restyled. * bar_test.js: Test whether outside bars are correctly identified when `barnorm: 'fraction'`. * bar_attrs_relative: Updated to test the bar text functionality. * bar_attrs_group_norm: Updated to test the bar text functionality. * bar_attrs_relative: Reduced font size of text "Three" to ensure the baseline image also fits the text inside the bar. * bar_attrs_group_norm: Exercise functionality to rotate bar texts. * bar_attrs_group_norm: Use "50%" as bar texts to ensure the bounding box width is larger than its height (unfortunately, this wasn't case when the baseline images were generated using "0.5", because the height of the bounding box includes some white space). --- test/image/baselines/bar_attrs_group_norm.png | Bin 9554 -> 13794 bytes test/image/baselines/bar_attrs_relative.png | Bin 11045 -> 15131 bytes test/image/mocks/bar_attrs_group_norm.json | 5 + test/image/mocks/bar_attrs_relative.json | 12 +- test/jasmine/tests/bar_test.js | 401 +++++++++++++++++- 5 files changed, 409 insertions(+), 9 deletions(-) diff --git a/test/image/baselines/bar_attrs_group_norm.png b/test/image/baselines/bar_attrs_group_norm.png index 53a17a1353ab299bf63cfbf4d28a8e60a1eef7f8..330d6607ce7eff85ad321ef3e2e66a13a9fa35f2 100644 GIT binary patch literal 13794 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 literal 9554 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 diff --git a/test/image/baselines/bar_attrs_relative.png b/test/image/baselines/bar_attrs_relative.png index fe44f4c1eb7d9ed7ab986b24843738f38104a83a..a2dee28d1ee3beb555cae6b64c82751dfc04814e 100644 GIT binary patch literal 15131 zcmeHuWmME(_b-kN!YD8_lH<^V3P|VB-QA6(N~gdm!q8ok(nv@NNQlDFh=PKE0@6r> zB8~Tqzvo};-n-U)ci%i~&Agd8XMgwEXZQYmqqH@Zh+%XvJUl#N6=iu{JiJSZxGy0A z_$91UXc!Lzpd4k)d0a-!G3xBWs1!yE5OAdU4b~e$o+;Ua!oB|fs*Iw!1e2!`GDQ9*0G40*_ws*B=HE*2Z}a$n@dVJOgN-kC zi@kBXr{^t8$!ZV#82!r2dyqKi1E0d@OyoU&bTDqT*F$6VeKu%E!NtYJ{sTdLJkB8r z0Out8_QlJWeHy|u;c47PNH#Ww%ZAi!z!RbSIZ){Eeh*7aCZTLk>5{9vug+zQ*q`^{>cv24!`mo)y3++GMv?Nwm-z@yJ;K@ zL}T3Ol!TFHz(HB_q;j2@GyvnPBP1J-(cNqCuH9ExbjH0_+4A~mt1hy0D6~Oe|djiVPm!o z{&3f}_{fXNFb#<`My65WXf=oeqIVV?26eQ{K=rhFq^ST$$yXz{RFSYaE zf1jNO3D|zU%4_`=&pddqC!J$=cfR{*W+x<)BhOzpAo`b9=t;^x<&PGR#o}L2|Bn<5 zAOSlahqbmnFjjVUBo`Oa;fx>i#n1ddh#NO#j^E72Gm58n(pa5Xw)^v8T}oo^*WHZHGpL}Tpvd7jFAqSh*;$_GI_Zc{5Dt{Iy zoQt-aXkmi6l1_{vn@)9vdvT(D>K1&jWGCR~%Bw8xy@6sbz0$Xfdz_xeuUrgL>Nh5; znn{&{yEqp_`vt7ytyxVWpu>iWSyOHo9`_c43~w#5C_9znPzo!VkpJgwbuiz^&~Tx3qh=9?4UlI08MK5u1OIWIpHT8@~FtczCeb zD2g=Ox!|91^{UQq3P;5oS|FopBN2f(&6DZ*Z< zxS)#K)6DCaX;k6H&r{RWZ6~Tsjux5!VDcn`Ztg`# zMy=?24->E^`*g^&PK=gbTIk-91k@S z81`1yLTE`Fr=CU|oil_i|KnuCJT)A=F0Penu=Y z(8_6Y&qr|V=gJ^efA5dCziB>em0R7u95GY)CRm(H1>M3qniPUm)zb3Q5&G;07Am5{ z^RD_naOO6Qy3_G1dB66_WTgCx47$JA*|SPnnoI}5u7#lJYV*}JZGqHzshRC*+}hjX z*}Ktm%dCl$JSjjM!SDQxVd#~#A(gp^5LF_BG4@qJgE9g~vo!X|fAM%VKf$%;HNnfy zi`LI3@@VBA=li^0E{qMV=O^`1p4MbvR^B7gnAEA)CGOp+Wb~R*eKlP0D;Jom^<>x3 zs+)KoZZ{F4wHVi*jt!`)>9KT(M}LB1u$R^5g~O`SqWP(vL6Tg&29)&*wTJtx8#Ttj zY+4sVay-go-l9E4JUo)n`}%j^3uGiiHcXj>XJNX1iU(fV)L4RI*exPsj{ExBi^JB| z)-+f^)9se?((cwHJs+RCA`^Z6)YkoXcrK@ZbS+GXN*ZH}UabhkRmWXS{HAwxVpZ(> zXt>DVJK^G%;(0c*o$^A|<+|A;c#Exe>H1_fmAZ-wpMGau{nNifgUOG0k203b7kSv) z1=TIv|4itO%GOhiFAu#Fv3lO4DBEb-e6b)(Xvwb>_^k;N$h>ZCV&*PLX{>EuPU^Va zrClbgPkl@kkp4*Gl>>8nN1K+fv3h6Kol=YGFWTmNy=|%8vgpZwd0y2g)wNPI8z>P|~OJnYm;qfvWMJeRRn9nhL*@|t&| zvdJ~(ELuWM3VZ*N*8N#-(VM(>-qORgGgg7-7ME^OVrnj47n3tD5+{{6NYimdi>e@w z1?xB~;uQZBM^7lB&`y7dbP=!y-y87ixh~c1VXdsj{g^_OY>wl|g;NeEdtOfB#i&Rw zM>XlyPf2D;VcD(B&rx)Q521w?&#FH=(Lj=9qh&bZ#9nXLsW8SisT0ShTl1@ITq4T} zrKpioIkZ#edv}@=RI#-i4Q`!tlPO{+e>3b*%dv5~Teg$5drXxR4wLV`W@wZO(dmmS z!GmG9P2Q>*@AD%9p!M5MLLuYJ{*xD5H`R0puc_XCz;oLC#DcHY?nW!tpxLyOi3!8s z>@Ce8^oFzcH(+hX4u-DsgK51U#rMlbKJF4{T|1L14)MYZu&XSKYk$ID5@U zhIE2y{jml|J)96CN%4ZW(u%3x_H2Dp@D7^^B6RxG6|d7(+jn2ic?GJkDTFYeuo@Iv z*+MO)zFl|b^EuKv;i5b(nm@ohPvPF}Lf=>ZIc}>L!k#DQm$C21OkGk`GyCKnmqUw4 zgYl1p-+C4P7bxK~-qF8(CTEdklobZO>@6khQix(YnYF~9f43mn;n6Lsv^zLIa^$c|L7S+L4$W6L z!cR83o{)6C>?8Mca;=@6=Q*2y_P0dW^1TBcbg%C&OfZ!hFd{{8O74bc;)XRL`0TC>D| zEcKY5zUSbld>1pMPYij@S!8UYBAN1q(a1*%Z4j{f{mrG@Kw=yDM zMYerE9dHm4epTi1O zb|{&k{o(rfV|OYbcbGP#rJ@WtUmV!Dl|h#4zep$e$t%V(Urrg?ONsrIQH$OCD16Q< z@|sfm{mrhsr)@0U6A7tnVrNx)m-5bkGbr5tuypWNFgIbb`iuwv`@8V_N{Qc>*?1mN zEVY@EhWtWU&is6_&U;mf2itP7sQ*ve+;+EG2f<8LvZ!sqP_r@J+mR5l*P@PqUeFFt ziNa&9?D!5#!rRIeCx5?&5XF>NLqLokx#ix|ce^JZlOJ-zRajKS7ku<^Ic8yD&Vhjv zb1hoYHXN?3=?r_rBYGa5AhP~g=cs^}(d%Tn&`Q@~&Cr51FmTfMg9i49*5}&^^sY(Re zOie3k)7?P`Ln17Z*>qm+oX^I@{L$WGrbIw}>T6lF@;fI4rdQ4Qg_$QBb@Q@L=AQ$+ z*bPqxmJhAM1%a{euj(RNM{_NbAkI;v{gQAFH|MXsg^^5wge*$&*pdv(n+`SF2wh$} zLULKe!pcUk9b?*e8NB67%wk7pElopwv4gOmKRjs&m*N&p22V*w^X>{i{a|f=^2CBR z^INYHQto*v>Y6%s=Y=j}X zZyd?(ASu*y%gop^N5EMoO{>+=$}+0x%scSP=Ej*Il*5{i873#FdH*ULoRxujesFeh7n|^9{D)FFd-uN9~pUpg`Cg4H->}ov8ek za~jcT9C-QBX_-}Hm8lu@XyG8jt%1o1;c0~Mz3&|K{p}?QkTv3p;*YOfjedB6S}I=O z?kyZbb|{H$PV`MJ=6t~a1&U@aO|@~!e=<1aYK|6QvHnGFNRMLVhR5k?xO>3lYfzit z#ti!x&kyGwcz$4EXaAdOO#gt_GyI2|Acf*iT12Sso`%HSf@Z0**d`=;=Sw5+7t$9@ z1RB8PVY)an#jHKgL0D;{0FGuM=H)8uXKVXNI(Rc>*_lwFZ@}`n!l- zisBU?mxfrDesbjgXUk{zEGjc<1(bA*o=XzSZ#376v3oC-sheg=QDR!>QUgl5g4K0` zo0fXL;%`C%R^{1MT|3}b5mbkAobXi`YtWzU{vAof*B>)=j|2xlofb*mq+=v~6Vo-q z0^enGdZtH-VK?V-BfA8{B0std0shbhSEw$%+(&nuJ)^}4f9lC2k7NFHb=+@OJ|o>k z@(?Aq-0}3x(g<(2KyQ{kXOOA1N_k8(IDWnOcDd$SaYg8VCm4v2TqleQ4!mh_92?YU z+*vOi{)J+NptW2Xtvpo4gl44)i93p3j25tc5kscu1-oeR#_Cglcbcn&$BzBx6Nky0 zid|Pmf~bO2w|FjFg?a(eY4KY{{8Fh3pA0Qz|1RUxL32nttdQIs7gtOtCWBs6RdP-H zMJSJr;+7_`au*%^6e#EReW8*|#@(FmG1JzAOL~a0u>ll0kv4+dW*tT)i?AidB)2-5 z&m9;qYIwgaV_NnZJ(8Z{kQG(})FOG%-Xa%VgJY+KLPFIl{Um)-iaM*z&fE0R!t2+$ zjZtBcIy}#Qc3DFCP3145O{P;xHS=6m`(k$%+7DU^?Jhyba>AZ4EpFu;-eK4{@G6PS zQj(I_`F&h|*o$0TQ{o&tVmhmVFIrk>+@ph3m^YW8K73WBFx$@T2SUct-o@&yw zy)xz@!ys*hi%JEzQLXLM*2ni$74A7AAD#JY_)N>$fDDGG@+dWGt?(!~s>SE_tg?}h zHsS?u1y`{W!edz}Dc~0ie(+wo>gMCg7b%$E88>&H=B)9VYBXpJY9pQ{J*|4UiGC__ zkPuo-<7>ai9vjCyyY(JHi=i4HU8_zr;@X6&O|uv#`=1#@0O|CbfZe0--kR&KGz8z zdj|&<^h@ygr8@6K@a$ZOGva$H@SG)$k<1~qDeM|ui8thm{~-XH8MPjZisqmtsI08~ z_L-n7WzZetS`;i8l$?rX)qnRBvB9~4>#TRUntsPBJ~|q9cJigO_d)2XnY@zgDY>j! zr#?tAMR|mUX+V?i-3xaf{JG2;nC zGh!%p_!NibJ<`+RP&Kdj1IPDSj0)Xsv8hUu6~ zQ<7(idO6zNN}^;@+Eh{_I~}F?TV6O~CCjhxSx7qw?haqMQv0%?UZKS%jhWU3Nqenp zaWuqMvTbyWNZNTc@{LJU%Lc~`@xfMPf)jK0(fWOz)vG$IYT?aIkb^h!%Y2)g?adF{ zSC_+jKV$_5SPj)(r_;v_BKtlBKhzHNPl{{{8SB>mmYQ0c-}i-n_&(bBb;;ILY&?VL zNZHVkK5U(?^Wdr;cl_@sHJR-@Whp zeb3kYow#9b=99KX6%Tc|5+8;)o_G9J{;wcQkU7)pm-}`|E9T9~u9<@=E@pLqJI$$q z4u5BCX&Db1S~gwbM6`Ui;go9{kF^o|S$R~WHB`!toeUb+tVOPU%l=r6rp67it6jhf3AM$1ARqa&n0CqekF8f_kp^88$J?C z7G9S2Ncz`|d>LvE!HaRiwQf`O)CujkELtfqUm;1oTf_wy zW%!D*%|O}DfnDmk?AK~i@SuehIgKuq7=hGkXuTSuJ!+PpUh^b1sv@O^5n6`Y)btIa zR^|-@4phcv-e^#qVnNX>B!RCyCduoGR`JAbG=}3PFz_D~MlwC*Y^EeyKm@iSd@aStul28;Q0|rtjF1lrmct6@_ zNQDr8KtU3`qv&i>hhs()t(=V|A(2N<<}$#F`7S#{tq`U4s4o#}?j@tQ(Yx$oP}{?b zp6K6PaBj{>7obOL7&WPx-!OK7wIkgOtk3gR`wSy=NLopsVJw zFP6ND9kDf3gbxb!5V{=oa!+&+z8s~wF9Gj#-ZO1X3b}Vs0WJ@IOx6C2nfzgrYs!F` zR9_yr*4u=dQ%fa}GMjSc4Leo#O>$**gXjBu*swd#2U^07e@T=M(-3K7F1f?Rp zV16B(vH~i)=Kt{O)(8jVQTNBE5?t|1pI&vWc(mzH0mZPdRuWHkzT-z(oN>uTY z<K#*3?&sD(lJ`prJv_aVq` ztO|7HxCN7E44IXGFc3A@r7P_BO+OmKmw5|@MW9kDzxoix;H)yGtcj&d4?*w=g{H-K z^7Ijhf=Hl|GDL+LNSTS)1zfKA63Vwr->9kB2*^hbJq#e)5V$2Nx}xD-gK9jR_-2|8 zh>A*b&^*z&xkF{oI33-69$J0;2y)9^4{?H{Mu|zEduBGIiy*jruM|bDO4>9lo^U0q z=k$B!t-evIP63g}O_jK{$@=xof_quzaVpLbkqTDQ<%$l0)mSYXXT@F^HJBP*lNHiK zSa=FYY%xN~>5?oCQT2zNQD?JuWcZ2ymBB5Pbb$oUGun}R-x%GXE!lqhkwC&6ohhL?$8ZS`k8T2o1<;|j{5xT9+PSHNAh`6*d{7)RW|vYqQy>^j9sD=z`n2`+!)H5!>}J~o);`X+oilL4m4+9E#BIt@_Q~^l#{ACw z={yihWy0ixgM*gkbTeM}X@Q*3i(})?5J{=i&ASr6KFYkD`NtjlN;Y^9!k6pNQut{5PA^uU_{si?K0`S74Htie(U<@lHuvuj0;6?}I9I&6 z?z4s8%+nvA%Ivi;r9>PwuIF4Bwp`vRsFd*?DYRNszmR)$AoEM7YL#N z6IJ-VO2YZ+q1`|-n~28(eyiV3jrqvn$VihpBG+;|{A}-d1TCK*t*Gxqc{UaDq~o3s`9q-ESd-&I94YXpf&BCCAjXI5$$I5# zHFrfIfZ`#rpPV#q9K~Tu<+1o;!=TkY|L?bcB3n7>I9@J2B74GR??VHyDLH98|5~-M z{fXJ_nvedqDng_1P%Q|V98N$tfz}@L;%v8fofYnZlSD}>a5-QYg+PHP5rbcdYk4?T z(X`>}z=!p(vlgI1z`c|XRRCKP!57aY_wUqM;B9>YCom#@@I_Rngb{#?B`~I@ECHS+ zWc95$0ZaKyOC6ojmb97^(-XDXh6ASaG?Ns3bp#5Xi8qc?hGFr*NQ0V&hDB0xrrz;& zYyCArdR>(;hz`+Up)Dmwp};{v4m1k-Y45Y6&?4MHIK?5W7pBC-B#(a_4@5ZMBVX`Z zLs0S1oN6;r>z&wV=*Mz;ss9LWIJf5 zr+%dpxRga@oD-h%_8D0^H~en-U>w)|Opa}dbE@TJ=Cp!VqgVSoI)XaNb2;(#HV)qi zhw(gd@}U4-6n2y#H!IYZ+Qe%yn*PTRvoRgM7RdOF=GCW>Qb6WX_Z`OzQNu`AS#uZ` zznnet*XO{*3rn$^1O^2wIdmyzF=rs~GfhFS>qcfJhsRR9)60a4`l}A9m z9@LttTQ_r+m4o97w_%mjCBFr1l*mr|Pr5(t|C9{x7J6a=NU^CEPVOHkH07b(hToT> z%c{39=n4KZO__<$6}OP``-WIb44TMJcHH+4Tu@z2Rh2S8{h0ufT*Ty|)pAlGDR%J? z5pa)x;Z2<0m`;XyS@5>`*{7-}5WbkI*B)GOuVa@uO?3DT!~dDx8@|yUQhI?Zd{GDw z5}`KecoF7=@bE+0#`hK+joDV5jgTm!Gb1KGzoo=f_zPYsL&+j4tE#X7p22tjn@B8! z^B>}6Wh&*Ll$cvqpDuDEQjDH8yD~!ykIm;XFiH%|*vCmC7^}S0tp6f02u#;TM@KbO z|CQ1=N~CPbAaurcl#f}VK0kaona%Ih*}<>^jim&oDD|F7Q4!GX$a+MDG(pV5?(okJ zYYSPv_@VnGIf<=2p<0+&2x?1bs&`orQMxeB0A>NP!*5_o$iyb!EedFGWZusdT#^-@xy9v^DYzLA0+@*5fy4^1eAX{4(DQ~ zOHj7)eTd~xl>ROfgKU+ppT$yhjUrkF-drJfk=<30r8)O!X{-cZi=4iK8!oQd=H#VqDiAihhZdO2t-&@KArC+-~sKks-uSY@Zl6{Yw zI1_hc8T1}Qk}_bp2{Si~4#IGZWR4n2o|j~&3YO}E-m0Cm11k)xIz9ij5Y=_pQyp}T zEpKwd19+-q<dz_4rzpWaNYv=4Pe10^e7)2_p93 z6(dCQPZ_i_!L*`C9hdP+uX?JbcJzAQc&Q-h=|&hDS>v#hK{q|XK5^XMJh~q{W!-uk zG7fX9FnLl`#>8{rW<)rMSkEJfbO**ie?|xi8M+KFWK4jAM>mdJ?*;5(M9H26K9B@{ ziwpiUNxT#a!}3)S&XI8tvIgHdNPE~d=}%!1wB2rCS=^BkQ8*Nn_$z?iMO+s#&DGkB zyTosz^Q^Aaw>e_tAftH}HxdSA(su8iCbi!JO^Eo9#fn6+>c$|5+u}i;$=6^%vM)Q6+YxVOcuPFLD8#-b{e{W()MFF_t(42AAUsJ zO7oIxkC&+i&<({t(Xz>sF$h(M$xDk5xr%|uPZZ?x3c;|(zONyCy&vLU;UGZo!85B{ zECZIQ%tOx)ZoP1yZX_}?_fS~OzXOa3AXpv(PoQXIHR!ArFt5>gDwwk`ydM>Mioaj10sC47z*&O-7T91YFxzAa!H1u4rXg7c}lnEYTUYJW5RUGXIzseHl@4 zYb%}WMAf^k=DC{w=b?Wt;M_3;=vW3(@|PXIpY8wYJYJ!9n`t#jLMbW|7*?A;y&)HY z1K2z^X>=lU9Lb4#dX>g@?c;M1&m}^~_s>dp{ZF@=)5ev))L59EoyXjHL%2sWkn1-) zWqkXa>QinwjmC+iMG3HOaU2T|(@@yK#huVUZT0(3@P4xg9Zvy(=R25tbghjR(L<30 zhPt|iRs8Y+b)KRCb25Jrh#L)o*@5xv@Po~U54O{d&Ke?w=wI72^fRqKu6u`t?Z%J3 z`Fyd6M#;Z^^XPPa@TL)>wbsNGSS+RAWG`o8Ehb0rtur@VW(5Q^1p@=Bo7~)d9t-jk zfId7ULF&($*K+X%fZB0ZMgjh5zc#tc+1T7**LYTpDVrxx7x}>~;jc{=X*{a0ua6KE zq+0AtF#5i_va-PZ=O|M5V#aUI{p{4TBao|-)FX5W%z~mN=mveZznOR~aKhjB^Snf% zjpK7NYV8p;UzU4Ho`IyE9$-w8$#lK0alqt%BBsl~hu7&K>g$~?8DLn6ra+_@v@rQu zi_o15?x4-dI!cVHqiM7b!qh4E_Ru-1@va^5Ymg~YNeDlfjm;JJLxO?D69QrvB;4bC z=0#CsBPACXm!aT2XK_thvwwxjl0~60aGUv_uz#rkpwkUFvj)w;asRR zz#e%l6YPF}_avK8KIf=PEW8S3t9#oy0nSeglTBn(A+H0kaMl714qm3e9>)8z4k`!lm3)N;g#%L*A(D z`|r3oEtl;u%nGUp6(dhKu#Angf-i@n*D(UMUsOa!a}9YA%j1jfvwIXs7ZUm_bDpM! z>;G~JIYiKcZQbo##0vziM}!)_?zd{h3?AQp^(sO~7ZE|mA0`8YBok(6=!X;1o){1k zA(qDyc^hiWZqaK9N+}(=fg28go&Hmt)6Yz^*z$PQuM^}wk*E*`Nm3YlEP|7npgJzT z@Nu_IFM`OEHKp?eQum~FL?{<%!PG(Rcn)y?tZvVExPgU6Ol!+gVr~Ra82aL#N+u4% z4#s3^oGAmS*l;|E&5WOYntp_*RE^X`RA z3P{CNo7M^ZJfyRpv3`@K$;!$;cdC-zd{a5yuBGnh&U+Rh$UehBdmzXX(sgxI{Cl4I zGQj1^zWYni`k-;h<1nFN+o-A$yYYY$Bfc8{DM$k?A}L2APL}m_h=^lKnJN1^gb2@H zoDKR%nv3iJPwdNtt*i&%*gAHOR@|QyODkRJqK7a{O8-crRZotXhIbJK+?7M5>F6A3 z>5|{ReMiask>sqZ#yg#S(&juK0re~hU+l+ZJW32(=CZ&-I7L!UQe$jaU6#T`YY;-o zHrV*n^UEv7?OzLDFVRX;f`cpke1cm7y)jzgL+_OV6%q2S?xO|U?x+QvO{hV% z5$V?Hp9rDI$f9wboJ8RUsOQb;n6id?-jaf_LsV6swNR51dXmt}^^>Lune$e|^gctj zsqV?(09@Iu?N)SkVE)K3RVoduP9R|Wj_43w@|-^Jgzu|-~t-e8=yt|jXI!2$c& zy(1hBT7}^s>IW$?4&`yeOF&wCI*m+dySp(}0iePJl^n1WOSgY;hrsPpXIdtSDAa4f}f!)Jcj^}qqb9sY2AW5eTzHS+E?7(AEUtBl2Io05?QQd6&u|Q@kVD{lA4v_zrO1WJ0;jcBF;e2T; z6Au9Z8yc&s+UW+m)@2c{nnA0I6y?DexGSpOI+S^HF$u)LK$YHeY`I8kE}qmeDdb(kIa zdo#+U!SMz+_fYH#Pn-#;@G>j|iOibs8%%Fazv9M*#$rMh$8_}dizSRb0oK~?bGc5E zfIz$g6pXl`tZgwEKOL!)H5M>@Tg(gEj-o}UrloP+akBicu_7Wb*bJ21GyTNPcQmVz z_llGf6y4l-utVh_LD4g;P4WilM0i?S+8`_!5>-6>g8nM#oTpTm_-;<3L56jjdG{Vk zsd^SUAHJ}|e!(3*mj zX}j}Jh{VOwqH#yytNr_wP;#=0aREZy6e8%VQauWXrKXwyr8mn06DE`E`1p7*T8Gg7 zXm@__X!C+0zUn&9?YoviTdzRB5`$EN$y1y7_26oem!KjPc^93sQLK`ZN}*fKxWJOL z;MnSKjXL|%`enoC06qtx`9NKi0&*+>fT;45b2+GwalSlW9*rqWTX~;dWF7X>C~`jAQpZw z?iH^5Yoznw7W~jS5)z_8er1hpUM!z$_4#%s@l8$@*Rq&Wyf?fCzi))lY^qzpz_inOSXv z4$gcYN(0A!F1kdBdxh>THCT3Qa*7jYFD?0?#dN{oiH{>-eHARD-gHaFy#@143rH%I zL==J}Q2P|jo38o_sp7cV2Cu-hNJlbU!V#F11+CnFALrk<^Z#4fb~{b}+|}aPFBbtX Pz2d1TXv){fT0j0@OBsAq literal 11045 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 diff --git a/test/image/mocks/bar_attrs_group_norm.json b/test/image/mocks/bar_attrs_group_norm.json index 0f6c54b2886..0c4e252975f 100644 --- a/test/image/mocks/bar_attrs_group_norm.json +++ b/test/image/mocks/bar_attrs_group_norm.json @@ -1,10 +1,15 @@ { "data":[ { + "text":"50%", + "textposition":"inside", + "outsidetextfont": {"size":32}, "base":[100,40,25,10], "x":[-50,10,25,40], "type":"bar" }, { + "text":"50%", + "textposition":"outside", "base":[0,60,75,90], "x":[50,-10,-25,-40], "type":"bar" diff --git a/test/image/mocks/bar_attrs_relative.json b/test/image/mocks/bar_attrs_relative.json index a7e15e0ff96..962fdfbc141 100644 --- a/test/image/mocks/bar_attrs_relative.json +++ b/test/image/mocks/bar_attrs_relative.json @@ -2,27 +2,37 @@ "data":[ { "width":[1,0.8,0.6,0.4], + "text":[1,2,3333333333,4], + "textposition":"outside", "y":[1,2,3,4], "x":[1,2,3,4], "type":"bar" }, { "width":[0.4,0.6,0.8,1], + "text":["Three",2,"inside text",0], + "textposition":"auto", + "textfont":{"size":[10]}, "y":[3,2,1,0], "x":[1,2,3,4], "type":"bar" }, { "width":1, + "text":[-1,-3,-2,-4], + "textposition":"inside", "y":[-1,-3,-2,-4], "x":[1,2,3,4], "type":"bar" }, { - "y":[0,-1,-3,-2], + "text":[0,"outside text",-3,-2], + "textposition":"auto", + "y":[0,-0.25,-3,-2], "x":[1,2,3,4], "type":"bar" } ], "layout":{ "xaxis": {"showgrid":true}, + "yaxis": {"range":[-6,6]}, "height":400, "width":400, "barmode":"relative", diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index a67ee339cf7..75f8bc01926 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1,17 +1,17 @@ var Plotly = require('@lib/index'); -var Plots = require('@src/plots/plots'); + +var Bar = require('@src/traces/bar'); var Lib = require('@src/lib'); +var Plots = require('@src/plots/plots'); var PlotlyInternal = require('@src/plotly'); var Axes = PlotlyInternal.Axes; -var Bar = require('@src/traces/bar'); - var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); -describe('bar supplyDefaults', function() { +describe('Bar.supplyDefaults', function() { 'use strict'; var traceIn, @@ -84,6 +84,38 @@ describe('bar supplyDefaults', function() { supplyDefaults(traceIn, traceOut, defaultColor); expect(traceOut.width).toBeUndefined(); }); + + it('should coerce textposition to none', function() { + traceIn = { + y: [1, 2, 3] + }; + supplyDefaults(traceIn, traceOut, defaultColor); + expect(traceOut.textposition).toBe('none'); + expect(traceOut.texfont).toBeUndefined(); + expect(traceOut.insidetexfont).toBeUndefined(); + expect(traceOut.outsidetexfont).toBeUndefined(); + }); + + it('should default textfont to layout.font', function() { + traceIn = { + textposition: 'inside', + y: [1, 2, 3] + }; + + var layout = { + font: {family: 'arial', color: '#AAA', size: 13} + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + + expect(traceOut.textposition).toBe('inside'); + expect(traceOut.textfont).toEqual(layout.font); + expect(traceOut.textfont).not.toBe(layout.font); + expect(traceOut.insidetextfont).toEqual(layout.font); + expect(traceOut.insidetextfont).not.toBe(layout.font); + expect(traceOut.insidetextfont).not.toBe(traceOut.textfont); + expect(traceOut.outsidetexfont).toBeUndefined(); + }); }); describe('heatmap calc / setPositions', function() { @@ -633,6 +665,201 @@ describe('A bar plot', function() { afterEach(destroyGraphDiv); + function getAllTraceNodes(node) { + return node.querySelectorAll('g.points'); + } + + function getAllBarNodes(node) { + return node.querySelectorAll('g.point'); + } + + function assertTextIsInsidePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.left).not.toBeGreaterThan(textBB.left); + expect(textBB.right).not.toBeGreaterThan(pathBB.right); + expect(pathBB.top).not.toBeGreaterThan(textBB.top); + expect(textBB.bottom).not.toBeGreaterThan(pathBB.bottom); + } + + function assertTextIsAbovePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(textBB.bottom).not.toBeGreaterThan(pathBB.top); + } + + function assertTextIsBelowPath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.bottom).not.toBeGreaterThan(textBB.top); + } + + function assertTextIsAfterPath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(pathBB.right).not.toBeGreaterThan(textBB.left); + } + + var colorMap = { + 'rgb(0, 0, 0)': 'black', + 'rgb(255, 0, 0)': 'red', + 'rgb(0, 128, 0)': 'green', + 'rgb(0, 0, 255)': 'blue' + }; + function assertTextFont(textNode, textFont, index) { + expect(textNode.style.fontFamily).toBe(textFont.family[index]); + expect(textNode.style.fontSize).toBe(textFont.size[index] + 'px'); + + var color = textNode.style.fill; + if(!colorMap[color]) colorMap[color] = color; + expect(colorMap[color]).toBe(textFont.color[index]); + } + + function assertTextIsBeforePath(textNode, pathNode) { + var textBB = textNode.getBoundingClientRect(), + pathBB = pathNode.getBoundingClientRect(); + + expect(textBB.right).not.toBeGreaterThan(pathBB.left); + } + + it('should show bar texts (inside case)', function(done) { + var gd = createGraphDiv(), + data = [{ + y: [10, 20, 30], + type: 'bar', + text: ['1', 'Very very very very very long bar text'], + textposition: 'inside', + }], + layout = { + }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for(var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector('path'), + textNode = barNode.querySelector('text'); + if(textNode) { + foundTextNodes = true; + assertTextIsInsidePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); + }); + }); + + it('should show bar texts (outside case)', function(done) { + var gd = createGraphDiv(), + data = [{ + y: [10, -20, 30], + type: 'bar', + text: ['1', 'Very very very very very long bar text'], + textposition: 'outside', + }], + layout = { + barmode: 'relative' + }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for(var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector('path'), + textNode = barNode.querySelector('text'); + if(textNode) { + foundTextNodes = true; + if(data[0].y[i] > 0) assertTextIsAbovePath(textNode, pathNode); + else assertTextIsBelowPath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); + }); + }); + + it('should show bar texts (horizontal case)', function(done) { + var gd = createGraphDiv(), + data = [{ + x: [10, -20, 30], + type: 'bar', + text: ['Very very very very very long bar text', -20], + textposition: 'outside', + }], + layout = { + }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for(var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector('path'), + textNode = barNode.querySelector('text'); + if(textNode) { + foundTextNodes = true; + if(data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); + else assertTextIsBeforePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); + }); + }); + + it('should show bar texts (barnorm case)', function(done) { + var gd = createGraphDiv(), + data = [{ + x: [100, -100, 100], + type: 'bar', + text: [100, -100, 100], + textposition: 'outside', + }], + layout = { + barmode: 'relative', + barnorm: 'percent' + }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + foundTextNodes; + + for(var i = 0; i < barNodes.length; i++) { + var barNode = barNodes[i], + pathNode = barNode.querySelector('path'), + textNode = barNode.querySelector('text'); + if(textNode) { + foundTextNodes = true; + if(data[0].x[i] > 0) assertTextIsAfterPath(textNode, pathNode); + else assertTextIsBeforePath(textNode, pathNode); + } + } + + expect(foundTextNodes).toBe(true); + + done(); + }); + }); + it('should be able to restyle', function(done) { var gd = createGraphDiv(), mock = Lib.extendDeep({}, require('@mocks/bar_attrs_relative')); @@ -644,13 +871,13 @@ describe('A bar plot', function() { [1, 2, 3, 4], [1, 2, 3, 4]]); assertPointField(cd, 'y', [ [1, 2, 3, 4], [4, 4, 4, 4], - [-1, -3, -2, -4], [4, -4, -5, -6]]); + [-1, -3, -2, -4], [4, -3.25, -5, -6]]); assertPointField(cd, 'b', [ [0, 0, 0, 0], [1, 2, 3, 4], [0, 0, 0, 0], [4, -3, -2, -4]]); assertPointField(cd, 's', [ [1, 2, 3, 4], [3, 2, 1, 0], - [-1, -3, -2, -4], [0, -1, -3, -2]]); + [-1, -3, -2, -4], [0, -0.25, -3, -2]]); assertPointField(cd, 'p', [ [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]); @@ -672,13 +899,13 @@ describe('A bar plot', function() { [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]); assertPointField(cd, 'y', [ [1, 2, 3, 4], [4, 4, 4, 4], - [-1, -3, -2, -4], [4, -4, -5, -6]]); + [-1, -3, -2, -4], [4, -3.25, -5, -6]]); assertPointField(cd, 'b', [ [0, 0, 0, 0], [1, 2, 3, 4], [0, 0, 0, 0], [4, -3, -2, -4]]); assertPointField(cd, 's', [ [1, 2, 3, 4], [3, 2, 1, 0], - [-1, -3, -2, -4], [0, -1, -3, -2]]); + [-1, -3, -2, -4], [0, -0.25, -3, -2]]); assertPointField(cd, 'p', [ [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]); @@ -692,6 +919,164 @@ describe('A bar plot', function() { expect(cd[3][0].t.poffset).toBe(0); assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); + var traceNodes = getAllTraceNodes(gd), + trace0Bar3 = getAllBarNodes(traceNodes[0])[3], + path03 = trace0Bar3.querySelector('path'), + text03 = trace0Bar3.querySelector('text'), + trace1Bar2 = getAllBarNodes(traceNodes[1])[2], + path12 = trace1Bar2.querySelector('path'), + text12 = trace1Bar2.querySelector('text'), + trace2Bar0 = getAllBarNodes(traceNodes[2])[0], + path20 = trace2Bar0.querySelector('path'), + text20 = trace2Bar0.querySelector('text'), + trace3Bar0 = getAllBarNodes(traceNodes[3])[0], + path30 = trace3Bar0.querySelector('path'), + text30 = trace3Bar0.querySelector('text'); + + expect(text03.textContent).toBe('4'); + expect(text12.textContent).toBe('inside text'); + expect(text20.textContent).toBe('-1'); + expect(text30.textContent).toBe('outside text'); + + assertTextIsAbovePath(text03, path03); // outside + assertTextIsInsidePath(text12, path12); // inside + assertTextIsInsidePath(text20, path20); // inside + assertTextIsBelowPath(text30, path30); // outside + + return Plotly.restyle(gd, 'textposition', 'inside'); + }).then(function() { + var cd = gd.calcdata; + assertPointField(cd, 'x', [ + [1.5, 2.4, 3.3, 4.2], [1.2, 2.3, 3.4, 4.5], + [1.5, 2.5, 3.5, 4.5], [1.4, 2.4, 3.4, 4.4]]); + assertPointField(cd, 'y', [ + [1, 2, 3, 4], [4, 4, 4, 4], + [-1, -3, -2, -4], [4, -3.25, -5, -6]]); + assertPointField(cd, 'b', [ + [0, 0, 0, 0], [1, 2, 3, 4], + [0, 0, 0, 0], [4, -3, -2, -4]]); + assertPointField(cd, 's', [ + [1, 2, 3, 4], [3, 2, 1, 0], + [-1, -3, -2, -4], [0, -0.25, -3, -2]]); + assertPointField(cd, 'p', [ + [1, 2, 3, 4], [1, 2, 3, 4], + [1, 2, 3, 4], [1, 2, 3, 4]]); + assertArrayField(cd[0][0], 't.barwidth', [1, 0.8, 0.6, 0.4]); + assertArrayField(cd[1][0], 't.barwidth', [0.4, 0.6, 0.8, 1]); + expect(cd[2][0].t.barwidth).toBe(1); + expect(cd[3][0].t.barwidth).toBe(0.8); + expect(cd[0][0].t.poffset).toBe(0); + expect(cd[1][0].t.poffset).toBe(0); + expect(cd[2][0].t.poffset).toBe(0); + expect(cd[3][0].t.poffset).toBe(0); + assertTraceField(cd, 't.bargroupwidth', [0.8, 0.8, 0.8, 0.8]); + + var traceNodes = getAllTraceNodes(gd), + trace0Bar3 = getAllBarNodes(traceNodes[0])[3], + path03 = trace0Bar3.querySelector('path'), + text03 = trace0Bar3.querySelector('text'), + trace1Bar2 = getAllBarNodes(traceNodes[1])[2], + path12 = trace1Bar2.querySelector('path'), + text12 = trace1Bar2.querySelector('text'), + trace2Bar0 = getAllBarNodes(traceNodes[2])[0], + path20 = trace2Bar0.querySelector('path'), + text20 = trace2Bar0.querySelector('text'), + trace3Bar0 = getAllBarNodes(traceNodes[3])[0], + path30 = trace3Bar0.querySelector('path'), + text30 = trace3Bar0.querySelector('text'); + + expect(text03.textContent).toBe('4'); + expect(text12.textContent).toBe('inside text'); + expect(text20.textContent).toBe('-1'); + expect(text30.textContent).toBe('outside text'); + + assertTextIsInsidePath(text03, path03); // inside + assertTextIsInsidePath(text12, path12); // inside + assertTextIsInsidePath(text20, path20); // inside + assertTextIsInsidePath(text30, path30); // inside + + done(); + }); + }); + + it('should coerce text-related attributes', function(done) { + var gd = createGraphDiv(), + data = [{ + y: [10, 20, 30, 40], + type: 'bar', + text: ['T1P1', 'T1P2', 13, 14], + textposition: ['inside', 'outside', 'auto', 'BADVALUE'], + textfont: { + family: ['"comic sans"'], + color: ['red', 'green'], + }, + insidetextfont: { + size: [8, 12, 16], + color: ['black'], + }, + outsidetextfont: { + size: [null, 24, 32] + } + }], + layout = { + font: {family: 'arial', color: 'blue', size: 13} + }; + + var expected = { + y: [10, 20, 30, 40], + type: 'bar', + text: ['T1P1', 'T1P2', '13', '14'], + textposition: ['inside', 'outside', 'none'], + textfont: { + family: ['"comic sans"', 'arial'], + color: ['red', 'green'], + size: [13, 13] + }, + insidetextfont: { + family: ['"comic sans"', 'arial', 'arial'], + color: ['black', 'green', 'blue'], + size: [8, 12, 16] + }, + outsidetextfont: { + family: ['"comic sans"', 'arial', 'arial'], + color: ['red', 'green', 'blue'], + size: [13, 24, 32] + } + }; + + Plotly.plot(gd, data, layout).then(function() { + var traceNodes = getAllTraceNodes(gd), + barNodes = getAllBarNodes(traceNodes[0]), + pathNodes = [ + barNodes[0].querySelector('path'), + barNodes[1].querySelector('path'), + barNodes[2].querySelector('path'), + barNodes[3].querySelector('path') + ], + textNodes = [ + barNodes[0].querySelector('text'), + barNodes[1].querySelector('text'), + barNodes[2].querySelector('text'), + barNodes[3].querySelector('text') + ], + i; + + // assert bar texts + for(i = 0; i < 3; i++) { + expect(textNodes[i].textContent).toBe(expected.text[i]); + } + + // assert bar positions + assertTextIsInsidePath(textNodes[0], pathNodes[0]); // inside + assertTextIsAbovePath(textNodes[1], pathNodes[1]); // outside + assertTextIsInsidePath(textNodes[2], pathNodes[2]); // auto -> inside + expect(textNodes[3]).toBe(null); // BADVALUE -> none + + // assert fonts + assertTextFont(textNodes[0], expected.insidetextfont, 0); + assertTextFont(textNodes[1], expected.outsidetextfont, 1); + assertTextFont(textNodes[2], expected.insidetextfont, 2); + done(); }); });