diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 02a2846737d..11dbe6d375e 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -46,8 +46,21 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(hasInside || hasOutside) { var textFont = coerceFont(coerce, 'textfont', layout.font); - if(hasInside) coerceFont(coerce, 'insidetextfont', textFont); + + // Note that coercing `insidetextfont` is always needed – + // even if `textposition` is `outside` for each trace – since + // an outside label can become an inside one, for example because + // of a bar being stacked on top of it. + var insideTextFontDefault = Lib.extendFlat({}, textFont); + var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color; + var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet; + if(isColorInheritedFromLayoutFont) { + delete insideTextFontDefault.color; + } + coerceFont(coerce, 'insidetextfont', insideTextFontDefault); + if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont); + coerce('constraintext'); coerce('selected.textfont.color'); coerce('unselected.textfont.color'); diff --git a/src/traces/bar/helpers.js b/src/traces/bar/helpers.js new file mode 100644 index 00000000000..b86a1b619ee --- /dev/null +++ b/src/traces/bar/helpers.js @@ -0,0 +1,67 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); + +exports.coerceString = function(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; +}; + +exports.coerceNumber = function(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; +}; + +exports.coerceColor = function(attributeDefinition, value, defaultValue) { + if(tinycolor(value).isValid()) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +}; + +exports.coerceEnumerated = function(attributeDefinition, value, defaultValue) { + if(attributeDefinition.coerceNumber) value = +value; + + if(attributeDefinition.values.indexOf(value) !== -1) return value; + + return (defaultValue !== undefined) ? + defaultValue : + attributeDefinition.dflt; +}; + +exports.getValue = function(arrayOrScalar, index) { + var value; + if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; + else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; + return value; +}; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index ead3f4eb754..4a793a8a201 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -11,7 +11,6 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); -var tinycolor = require('tinycolor2'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); @@ -22,10 +21,9 @@ var Registry = require('../../registry'); var attributes = require('./attributes'), attributeText = attributes.text, - attributeTextPosition = attributes.textposition, - attributeTextFont = attributes.textfont, - attributeInsideTextFont = attributes.insidetextfont, - attributeOutsideTextFont = attributes.outsidetextfont; + attributeTextPosition = attributes.textposition; +var helpers = require('./helpers'); +var style = require('./style'); // padding in pixels around text var TEXTPAD = 3; @@ -177,9 +175,10 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { return; } - var textFont = getTextFont(trace, i, gd._fullLayout.font), - insideTextFont = getInsideTextFont(trace, i, textFont), - outsideTextFont = getOutsideTextFont(trace, i, textFont); + var layoutFont = gd._fullLayout.font; + var barColor = style.getBarColor(calcTrace[i], trace); + var insideTextFont = style.getInsideTextFont(trace, i, layoutFont, barColor); + var outsideTextFont = style.getOutsideTextFont(trace, i, layoutFont); // compute text position var barmode = gd._fullLayout.barmode, @@ -429,98 +428,11 @@ function getTransform(textX, textY, targetX, targetY, scale, rotate) { } function getText(trace, index) { - var value = getValue(trace.text, index); - return coerceString(attributeText, value); + var value = helpers.getValue(trace.text, index); + return helpers.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; + var value = helpers.getValue(trace.textposition, index); + return helpers.coerceEnumerated(attributeTextPosition, value); } diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js index af30dfc6c72..df729416dc1 100644 --- a/src/traces/bar/style.js +++ b/src/traces/bar/style.js @@ -10,9 +10,17 @@ 'use strict'; var d3 = require('d3'); +var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); +var Lib = require('../../lib'); var Registry = require('../../registry'); +var attributes = require('./attributes'), + attributeTextFont = attributes.textfont, + attributeInsideTextFont = attributes.insidetextfont, + attributeOutsideTextFont = attributes.outsidetextfont; +var helpers = require('./helpers'); + function style(gd, cd) { var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.bars'); var barcount = s.size(); @@ -50,21 +58,8 @@ function stylePoints(sel, trace, gd) { txs.each(function(d) { var tx = d3.select(this); - var textFont; - - if(tx.classed('bartext-inside')) { - textFont = trace.insidetextfont; - } else if(tx.classed('bartext-outside')) { - textFont = trace.outsidetextfont; - } - if(!textFont) textFont = trace.textfont; - - function cast(k) { - var cont = textFont[k]; - return Array.isArray(cont) ? cont[d.i] : cont; - } - - Drawing.font(tx, cast('family'), cast('size'), cast('color')); + var font = determineFont(tx, d, trace, gd); + Drawing.font(tx, font); }); } @@ -73,14 +68,105 @@ function styleOnSelect(gd, cd) { var trace = cd[0].trace; if(trace.selectedpoints) { - Drawing.selectedPointStyle(s.selectAll('path'), trace); - Drawing.selectedTextStyle(s.selectAll('text'), trace); + stylePointsInSelectionMode(s, trace, gd); } else { stylePoints(s, trace, gd); } } +function stylePointsInSelectionMode(s, trace, gd) { + Drawing.selectedPointStyle(s.selectAll('path'), trace); + styleTextInSelectionMode(s.selectAll('text'), trace, gd); +} + +function styleTextInSelectionMode(txs, trace, gd) { + txs.each(function(d) { + var tx = d3.select(this); + var font; + + if(d.selected) { + font = Lib.extendFlat({}, determineFont(tx, d, trace, gd)); + + var selectedFontColor = trace.selected.textfont && trace.selected.textfont.color; + if(selectedFontColor) { + font.color = selectedFontColor; + } + + Drawing.font(tx, font); + } else { + Drawing.selectedTextStyle(tx, trace); + } + }); +} + +function determineFont(tx, d, trace, gd) { + var layoutFont = gd._fullLayout.font; + var textFont = trace.textfont; + + if(tx.classed('bartext-inside')) { + var barColor = getBarColor(d, trace); + textFont = getInsideTextFont(trace, d.i, layoutFont, barColor); + } else if(tx.classed('bartext-outside')) { + textFont = getOutsideTextFont(trace, d.i, layoutFont); + } + + return textFont; +} + +function getTextFont(trace, index, defaultValue) { + return getFontValue( + attributeTextFont, trace.textfont, index, defaultValue); +} + +function getInsideTextFont(trace, index, layoutFont, barColor) { + var defaultFont = getTextFont(trace, index, layoutFont); + + var wouldFallBackToLayoutFont = + (trace._input.textfont === undefined || trace._input.textfont.color === undefined) || + (Array.isArray(trace.textfont.color) && trace.textfont.color[index] === undefined); + if(wouldFallBackToLayoutFont) { + defaultFont = { + color: Color.contrast(barColor), + family: defaultFont.family, + size: defaultFont.size + }; + } + + return getFontValue( + attributeInsideTextFont, trace.insidetextfont, index, defaultFont); +} + +function getOutsideTextFont(trace, index, layoutFont) { + var defaultFont = getTextFont(trace, index, layoutFont); + return getFontValue( + attributeOutsideTextFont, trace.outsidetextfont, index, defaultFont); +} + +function getFontValue(attributeDefinition, attributeValue, index, defaultValue) { + attributeValue = attributeValue || {}; + + var familyValue = helpers.getValue(attributeValue.family, index), + sizeValue = helpers.getValue(attributeValue.size, index), + colorValue = helpers.getValue(attributeValue.color, index); + + return { + family: helpers.coerceString( + attributeDefinition.family, familyValue, defaultValue.family), + size: helpers.coerceNumber( + attributeDefinition.size, sizeValue, defaultValue.size), + color: helpers.coerceColor( + attributeDefinition.color, colorValue, defaultValue.color) + }; +} + +function getBarColor(cd, trace) { + return cd.mc || trace.marker.color; +} + module.exports = { style: style, - styleOnSelect: styleOnSelect + styleOnSelect: styleOnSelect, + getInsideTextFont: getInsideTextFont, + getOutsideTextFont: getOutsideTextFont, + getBarColor: getBarColor }; diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index d81ba172554..58583322004 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -17,6 +17,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var textFontAttrs = fontAttrs({ editType: 'calc', + arrayOk: true, colorEditType: 'style', description: 'Sets the font used for `textinfo`.' }); @@ -106,7 +107,7 @@ module.exports = { editType: 'calc', description: [ 'Sets text elements associated with each sector.', - 'If trace `textinfo` contains a *text* flag, these elements will seen', + 'If trace `textinfo` contains a *text* flag, these elements will be seen', 'on the chart.', 'If trace `hoverinfo` contains a *text* flag and *hovertext* is not set,', 'these elements will be seen in the hover labels.' @@ -169,7 +170,6 @@ module.exports = { 'Specifies the location of the `textinfo`.' ].join(' ') }, - // TODO make those arrayOk? textfont: extendFlat({}, textFontAttrs, { description: 'Sets the font used for `textinfo`.' }), diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 2a03a68075a..108644c44f9 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -60,7 +60,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(hasInside || hasOutside) { var dfltFont = coerceFont(coerce, 'textfont', layout.font); - if(hasInside) coerceFont(coerce, 'insidetextfont', dfltFont); + if(hasInside) { + var insideTextFontDefault = Lib.extendFlat({}, dfltFont); + var isTraceTextfontColorSet = traceIn.textfont && traceIn.textfont.color; + var isColorInheritedFromLayoutFont = !isTraceTextfontColorSet; + if(isColorInheritedFromLayoutFont) { + delete insideTextFontDefault.color; + } + coerceFont(coerce, 'insidetextfont', insideTextFontDefault); + } if(hasOutside) coerceFont(coerce, 'outsidetextfont', dfltFont); } } diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 27c3518a705..db45d9be2ea 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -264,7 +264,8 @@ module.exports = function plot(gd, cdpie) { 'text-anchor': 'middle' }) .call(Drawing.font, textPosition === 'outside' ? - trace.outsidetextfont : trace.insidetextfont) + determineOutsideTextFont(trace, pt, gd._fullLayout.font) : + determineInsideTextFont(trace, pt, gd._fullLayout.font)) .call(svgTextUtils.convertToTspans, gd); // position the text relative to the slice @@ -409,6 +410,52 @@ module.exports = function plot(gd, cdpie) { }, 0); }; +function determineOutsideTextFont(trace, pt, layoutFont) { + var color = helpers.castOption(trace.outsidetextfont.color, pt.pts) || + helpers.castOption(trace.textfont.color, pt.pts) || + layoutFont.color; + + var family = helpers.castOption(trace.outsidetextfont.family, pt.pts) || + helpers.castOption(trace.textfont.family, pt.pts) || + layoutFont.family; + + var size = helpers.castOption(trace.outsidetextfont.size, pt.pts) || + helpers.castOption(trace.textfont.size, pt.pts) || + layoutFont.size; + + return { + color: color, + family: family, + size: size + }; +} + +function determineInsideTextFont(trace, pt, layoutFont) { + var customColor = helpers.castOption(trace.insidetextfont.color, pt.pts); + if(!customColor && trace._input.textfont) { + + // Why not simply using trace.textfont? Because if not set, it + // defaults to layout.font which has a default color. But if + // textfont.color and insidetextfont.color don't supply a value, + // a contrasting color shall be used. + customColor = helpers.castOption(trace._input.textfont.color, pt.pts); + } + + var family = helpers.castOption(trace.insidetextfont.family, pt.pts) || + helpers.castOption(trace.textfont.family, pt.pts) || + layoutFont.family; + + var size = helpers.castOption(trace.insidetextfont.size, pt.pts) || + helpers.castOption(trace.textfont.size, pt.pts) || + layoutFont.size; + + return { + color: customColor || Color.contrast(pt.color), + family: family, + size: size + }; +} + function prerenderTitles(cdpie, gd) { var cd0, trace; // Determine the width and height of the title for each pie. @@ -418,10 +465,10 @@ function prerenderTitles(cdpie, gd) { if(trace.title) { var dummyTitle = Drawing.tester.append('text') - .attr('data-notex', 1) - .text(trace.title) - .call(Drawing.font, trace.titlefont) - .call(svgTextUtils.convertToTspans, gd); + .attr('data-notex', 1) + .text(trace.title) + .call(Drawing.font, trace.titlefont) + .call(svgTextUtils.convertToTspans, gd); var bBox = Drawing.bBox(dummyTitle.node(), true); cd0.titleBox = { width: bBox.width, diff --git a/test/image/baselines/bar_attrs_group_norm.png b/test/image/baselines/bar_attrs_group_norm.png index ceb86e17b82..7868b195ebf 100644 Binary files a/test/image/baselines/bar_attrs_group_norm.png and b/test/image/baselines/bar_attrs_group_norm.png differ diff --git a/test/image/baselines/bar_attrs_relative.png b/test/image/baselines/bar_attrs_relative.png index a190cfc7e23..15348a2001e 100644 Binary files a/test/image/baselines/bar_attrs_relative.png and b/test/image/baselines/bar_attrs_relative.png differ diff --git a/test/image/baselines/grid_subplot_types.png b/test/image/baselines/grid_subplot_types.png index 24d144923ce..bcf32c5a049 100644 Binary files a/test/image/baselines/grid_subplot_types.png and b/test/image/baselines/grid_subplot_types.png differ diff --git a/test/image/baselines/layout-colorway.png b/test/image/baselines/layout-colorway.png index 87f397d1041..ca88c794ebb 100644 Binary files a/test/image/baselines/layout-colorway.png and b/test/image/baselines/layout-colorway.png differ diff --git a/test/image/baselines/mathjax.png b/test/image/baselines/mathjax.png index ae40a4fcab4..1c85c4d8dd2 100644 Binary files a/test/image/baselines/mathjax.png and b/test/image/baselines/mathjax.png differ diff --git a/test/image/baselines/pie_aggregated.png b/test/image/baselines/pie_aggregated.png index 576ae7dd8c6..c8e29a1e90e 100644 Binary files a/test/image/baselines/pie_aggregated.png and b/test/image/baselines/pie_aggregated.png differ diff --git a/test/image/baselines/pie_fonts.png b/test/image/baselines/pie_fonts.png index 604622b3ad0..03560a12730 100644 Binary files a/test/image/baselines/pie_fonts.png and b/test/image/baselines/pie_fonts.png differ diff --git a/test/image/baselines/pie_label0_dlabel.png b/test/image/baselines/pie_label0_dlabel.png index c894b985d75..41bd57b5c82 100644 Binary files a/test/image/baselines/pie_label0_dlabel.png and b/test/image/baselines/pie_label0_dlabel.png differ diff --git a/test/image/baselines/pie_labels_colors_text.png b/test/image/baselines/pie_labels_colors_text.png index 15862a61eb9..f338d0a468b 100644 Binary files a/test/image/baselines/pie_labels_colors_text.png and b/test/image/baselines/pie_labels_colors_text.png differ diff --git a/test/image/baselines/pie_scale_textpos_hideslices.png b/test/image/baselines/pie_scale_textpos_hideslices.png index 72e8241f847..be3812b567b 100644 Binary files a/test/image/baselines/pie_scale_textpos_hideslices.png and b/test/image/baselines/pie_scale_textpos_hideslices.png differ diff --git a/test/image/baselines/pie_simple.png b/test/image/baselines/pie_simple.png index 516515244a7..272f4d74d11 100644 Binary files a/test/image/baselines/pie_simple.png and b/test/image/baselines/pie_simple.png differ diff --git a/test/image/baselines/pie_sort_direction.png b/test/image/baselines/pie_sort_direction.png index 49b6212bc9f..833c7ec566e 100644 Binary files a/test/image/baselines/pie_sort_direction.png and b/test/image/baselines/pie_sort_direction.png differ diff --git a/test/image/baselines/pie_style.png b/test/image/baselines/pie_style.png index 14a7aca811d..a870ed1c8cd 100644 Binary files a/test/image/baselines/pie_style.png and b/test/image/baselines/pie_style.png differ diff --git a/test/image/baselines/pie_style_arrays.png b/test/image/baselines/pie_style_arrays.png index 17f06ef2218..2bca68676f3 100644 Binary files a/test/image/baselines/pie_style_arrays.png and b/test/image/baselines/pie_style_arrays.png differ diff --git a/test/image/baselines/pie_title_middle_center.png b/test/image/baselines/pie_title_middle_center.png index 6ea5b0179c3..6018a07e748 100644 Binary files a/test/image/baselines/pie_title_middle_center.png and b/test/image/baselines/pie_title_middle_center.png differ diff --git a/test/image/baselines/pie_title_middle_center_multiline.png b/test/image/baselines/pie_title_middle_center_multiline.png index 2e04b1330d0..2b79c7de962 100644 Binary files a/test/image/baselines/pie_title_middle_center_multiline.png and b/test/image/baselines/pie_title_middle_center_multiline.png differ diff --git a/test/image/baselines/pie_title_pull.png b/test/image/baselines/pie_title_pull.png index 83945eeb9e9..ae6044d147a 100644 Binary files a/test/image/baselines/pie_title_pull.png and b/test/image/baselines/pie_title_pull.png differ diff --git a/test/image/baselines/plot_types.png b/test/image/baselines/plot_types.png index 79a35b7af79..57456e30b6f 100644 Binary files a/test/image/baselines/plot_types.png and b/test/image/baselines/plot_types.png differ diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 609231fffda..f73d10982fe 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -7,11 +7,15 @@ var Drawing = require('@src/components/drawing'); var Axes = require('@src/plots/cartesian/axes'); +var click = require('../assets/click'); +var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); +var color = require('../../../src/components/color'); +var rgb = color.rgb; var customAssertions = require('../assets/custom_assertions'); var assertClip = customAssertions.assertClip; @@ -19,6 +23,8 @@ var assertNodeDisplay = customAssertions.assertNodeDisplay; var d3 = require('d3'); +var BAR_TEXT_SELECTOR = '.bars .bartext'; + describe('Bar.supplyDefaults', function() { 'use strict'; @@ -122,28 +128,58 @@ describe('Bar.supplyDefaults', function() { expect(traceOut.constraintext).toBeUndefined(); }); - it('should default textfont to layout.font', function() { + it('should default textfont to layout.font except for insidetextfont.color', function() { traceIn = { textposition: 'inside', y: [1, 2, 3] }; - var layout = { font: {family: 'arial', color: '#AAA', size: 13} }; + var layoutFontMinusColor = {family: 'arial', 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).toEqual(layoutFontMinusColor); expect(traceOut.insidetextfont).not.toBe(layout.font); expect(traceOut.insidetextfont).not.toBe(traceOut.textfont); expect(traceOut.outsidetexfont).toBeUndefined(); expect(traceOut.constraintext).toBe('both'); }); + it('should not default insidetextfont.color to layout.font.color', function() { + traceIn = { + textposition: 'inside', + y: [1, 2, 3] + }; + var layout = { + font: {family: 'arial', color: '#AAA', size: 13} + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + + expect(traceOut.insidetextfont.family).toBe('arial'); + expect(traceOut.insidetextfont.color).toBeUndefined(); + expect(traceOut.insidetextfont.size).toBe(13); + }); + + it('should default insidetextfont.color to textfont.color', function() { + traceIn = { + textposition: 'inside', + y: [1, 2, 3], + textfont: {family: 'arial', color: '#09F', size: 20} + }; + + supplyDefaults(traceIn, traceOut, defaultColor, {}); + + expect(traceOut.insidetextfont.family).toBe('arial'); + expect(traceOut.insidetextfont.color).toBe('#09F'); + expect(traceOut.insidetextfont.size).toBe(20); + }); + it('should inherit layout.calendar', function() { traceIn = { x: [1, 2, 3], @@ -802,6 +838,9 @@ describe('Bar.crossTraceCalc (formerly known as setPositions)', function() { describe('A bar plot', function() { 'use strict'; + var DARK = '#444'; + var LIGHT = '#fff'; + var gd; beforeEach(function() { @@ -849,19 +888,13 @@ describe('A bar plot', function() { 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'); + function assertTextFont(textNode, expectedFontProps, index) { + expect(textNode.style.fontFamily).toBe(expectedFontProps.family[index]); + expect(textNode.style.fontSize).toBe(expectedFontProps.size[index] + 'px'); - var color = textNode.style.fill; - if(!colorMap[color]) colorMap[color] = color; - expect(colorMap[color]).toBe(textFont.color[index]); + var actualColorRGB = textNode.style.fill; + var expectedColorRGB = rgb(expectedFontProps.color[index]); + expect(actualColorRGB).toBe(expectedColorRGB); } function assertTextIsBeforePath(textNode, pathNode) { @@ -871,6 +904,43 @@ describe('A bar plot', function() { expect(textBB.right).not.toBeGreaterThan(pathBB.left); } + function assertTextFontColors(expFontColors, label) { + return function() { + var selection = d3.selectAll(BAR_TEXT_SELECTOR); + expect(selection.size()).toBe(expFontColors.length); + + selection.each(function(d, i) { + var expFontColor = expFontColors[i]; + var isArray = Array.isArray(expFontColor); + + expect(this.style.fill).toBe(isArray ? rgb(expFontColor[0]) : rgb(expFontColor), + (label || '') + ', fill for element ' + i); + expect(this.style.fillOpacity).toBe(isArray ? expFontColor[1] : '1', + (label || '') + ', fillOpacity for element ' + i); + }); + }; + } + + function assertTextFontFamilies(expFontFamilies) { + return function() { + var selection = d3.selectAll(BAR_TEXT_SELECTOR); + expect(selection.size()).toBe(expFontFamilies.length); + selection.each(function(d, i) { + expect(this.style.fontFamily).toBe(expFontFamilies[i]); + }); + }; + } + + function assertTextFontSizes(expFontSizes) { + return function() { + var selection = d3.selectAll(BAR_TEXT_SELECTOR); + expect(selection.size()).toBe(expFontSizes.length); + selection.each(function(d, i) { + expect(this.style.fontSize).toBe(expFontSizes[i] + 'px'); + }); + }; + } + it('should show bar texts (inside case)', function(done) { var data = [{ y: [10, 20, 30], @@ -999,6 +1069,226 @@ describe('A bar plot', function() { .then(done); }); + var insideTextTestsTrace = { + x: ['giraffes', 'orangutans', 'monkeys', 'elefants', 'spiders', 'snakes'], + y: [20, 14, 23, 10, 59, 15], + text: [20, 14, 23, 10, 59, 15], + type: 'bar', + textposition: 'auto', + marker: { + color: ['#ee1', '#eee', '#333', '#9467bd', '#dda', '#922'], + } + }; + + it('should use inside text colors contrasting to bar colors by default', function(done) { + var noMarkerTrace = Lib.extendFlat({}, insideTextTestsTrace); + delete noMarkerTrace.marker; + + Plotly.plot(gd, [insideTextTestsTrace, noMarkerTrace]) + .then(function() { + var trace1Colors = [DARK, DARK, LIGHT, LIGHT, DARK, LIGHT]; + var trace2Colors = Lib.repeat(DARK, 6); + var allExpectedColors = trace1Colors.concat(trace2Colors); + assertTextFontColors(allExpectedColors)(); + }) + .catch(failTest) + .then(done); + }); + + it('should take bar fill opacities into account when calculating contrasting inside text colors', function(done) { + var trace = { + x: [5, 10], + y: [5, 15], + text: ['Giraffes', 'Zebras'], + type: 'bar', + textposition: 'inside', + marker: { + color: ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.8)'] + } + }; + + Plotly.plot(gd, [trace]) + .then(assertTextFontColors([DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use defined textfont.color for inside text instead of the contrasting default', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, { textfont: { color: '#09f' } }); + + Plotly.plot(gd, [data]) + .then(assertTextFontColors(Lib.repeat('#09f', 6))) + .catch(failTest) + .then(done); + }); + + it('should use matching color from textfont.color array for inside text, contrasting otherwise', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, { textfont: { color: ['#09f', 'green'] } }); + + Plotly.plot(gd, [data]) + .then(assertTextFontColors(['#09f', 'green', LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use defined insidetextfont.color for inside text instead of the contrasting default', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, { insidetextfont: { color: '#09f' } }); + + Plotly.plot(gd, [data]) + .then(assertTextFontColors(Lib.repeat('#09f', 6))) + .catch(failTest) + .then(done); + }); + + it('should use matching color from insidetextfont.color array instead of the contrasting default', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, { insidetextfont: { color: ['yellow', 'green'] } }); + + Plotly.plot(gd, [data]) + .then(assertTextFontColors(['yellow', 'green', LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use a contrasting text color by default for outside labels being pushed inside ' + + 'because of another bar stacked above', function(done) { + var trace1 = { + x: [5], + y: [5], + text: ['Giraffes'], + type: 'bar', + textposition: 'outside' + }; + var trace2 = Lib.extendFlat({}, trace1); + var layout = {barmode: 'stack'}; + + Plotly.plot(gd, [trace1, trace2], layout) + .then(assertTextFontColors([LIGHT, DARK])) + .catch(failTest) + .then(done); + }); + + it('should style outside labels pushed inside by bars stacked above as inside labels', function(done) { + var trace1 = { + x: [5], + y: [5], + text: ['Giraffes'], + type: 'bar', + textposition: 'outside', + insidetextfont: {color: 'blue', family: 'serif', size: 24} + }; + var trace2 = Lib.extendFlat({}, trace1); + var layout = {barmode: 'stack', font: {family: 'Arial'}}; + + Plotly.plot(gd, [trace1, trace2], layout) + .then(assertTextFontColors(['blue', DARK])) + .then(assertTextFontFamilies(['serif', 'Arial'])) + .then(assertTextFontSizes([24, 12])) + .catch(failTest) + .then(done); + }); + + it('should fall back to textfont array values if insidetextfont array values don\'t ' + + 'cover all bars', function(done) { + var trace = Lib.extendFlat({}, insideTextTestsTrace, { + textfont: { + color: ['blue', 'blue', 'blue'], + family: ['Arial', 'serif'], + size: [8, 24] + }, + insidetextfont: { + color: ['yellow', 'green'], + family: ['Arial'], + size: [16] + } + }); + var layout = {font: {family: 'Roboto', size: 12}}; + + Plotly.plot(gd, [trace], layout) + .then(assertTextFontColors(['yellow', 'green', 'blue', LIGHT, DARK, LIGHT])) + .then(assertTextFontFamilies(['Arial', 'serif', 'Roboto', 'Roboto', 'Roboto', 'Roboto'])) + .then(assertTextFontSizes([16, 24, 12, 12, 12, 12])) + .catch(failTest) + .then(done); + }); + + it('should retain text styles throughout selecting and deselecting data points', function(done) { + var trace1 = { + x: ['giraffes', 'orangutans', 'monkeys'], + y: [12, 18, 29], + text: [12, 18, 29], + type: 'bar', + textposition: 'inside', + textfont: { + color: ['red', 'orange'], + family: ['Arial', 'serif'], + size: [8, 24] + }, + insidetextfont: { + color: ['blue'], + family: ['Arial'], + size: [16] + } + }; + var trace2 = Lib.extendDeep({}, trace1, {textposition: 'outside'}); + var layout = { + barmode: 'group', + font: { + family: 'Roboto', + size: 12 + }, + clickmode: 'event+select' + }; + + Plotly.plot(gd, [trace1, trace2], layout) + .then(function() { + assertNonSelectionModeStyle('before selection'); + }) + .then(function() { + return select1stBar2ndTrace(); + }) + .then(function() { + assertSelectionModeStyle('in selection mode'); + }) + .then(function() { + return deselect1stBar2ndTrace(); + }) + .then(function() { + assertNonSelectionModeStyle('after selection'); + }) + .catch(failTest) + .then(done); + + function assertSelectionModeStyle(label) { + var unselColor = ['black', '0.2']; + assertTextFontColors([unselColor, unselColor, unselColor, 'red', unselColor, unselColor], label)(); + assertTextFontFamilies(['Arial', 'serif', 'Roboto', 'Arial', 'serif', 'Roboto'])(); + assertTextFontSizes([16, 24, 12, 8, 24, 12])(); + } + + function assertNonSelectionModeStyle(label) { + assertTextFontColors(['blue', 'orange', LIGHT, 'red', 'orange', DARK], label)(); + assertTextFontFamilies(['Arial', 'serif', 'Roboto', 'Arial', 'serif', 'Roboto'])(); + assertTextFontSizes([16, 24, 12, 8, 24, 12])(); + } + + function select1stBar2ndTrace() { + return new Promise(function(resolve) { + click(176, 354); + resolve(); + }); + } + + function deselect1stBar2ndTrace() { + return new Promise(function(resolve) { + var delayAvoidingDblClick = DBLCLICKDELAY * 1.01; + setTimeout(function() { + click(176, 354); + resolve(); + }, delayAvoidingDblClick); + }); + } + }); + it('should be able to restyle', function(done) { var mock = Lib.extendDeep({}, require('@mocks/bar_attrs_relative')); @@ -1170,6 +1460,9 @@ describe('A bar plot', function() { font: {family: 'arial', color: 'blue', size: 13} }; + // Note: insidetextfont.color does NOT inherit from textfont.color + // since insidetextfont.color should be contrasting to bar's fill by default. + var contrastingLightColorVal = color.contrast('black'); var expected = { y: [10, 20, 30, 40], type: 'bar', @@ -1182,7 +1475,7 @@ describe('A bar plot', function() { }, insidetextfont: { family: ['"comic sans"', 'arial', 'arial'], - color: ['black', 'green', 'blue'], + color: ['black', 'green', contrastingLightColorVal], size: [8, 12, 16] }, outsidetextfont: { diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 24c85e494e1..c0e51a07eba 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -9,19 +9,21 @@ var click = require('../assets/click'); var getClientPosition = require('../assets/get_client_position'); var mouseEvent = require('../assets/mouse_event'); var supplyAllDefaults = require('../assets/supply_defaults'); +var rgb = require('../../../src/components/color').rgb; var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; var SLICES_SELECTOR = '.slice path'; +var SLICES_TEXT_SELECTOR = '.pielayer text.slicetext'; var LEGEND_ENTRIES_SELECTOR = '.legendpoints path'; describe('Pie defaults', function() { - function _supply(trace) { + function _supply(trace, layout) { var gd = { data: [trace], - layout: {} + layout: layout || {} }; supplyAllDefaults(gd); @@ -59,11 +61,24 @@ describe('Pie defaults', function() { out = _supply({type: 'pie', labels: ['A', 'B'], values: []}); expect(out.visible).toBe(false); }); + + it('does not apply layout.font.color to insidetextfont.color (it\'ll be contrasting instead)', function() { + var out = _supply({type: 'pie', values: [1, 2]}, {font: {color: 'blue'}}); + expect(out.insidetextfont.color).toBe(undefined); + }); + + it('does apply textfont.color to insidetextfont.color if not set', function() { + var out = _supply({type: 'pie', values: [1, 2], textfont: {color: 'blue'}}, {font: {color: 'red'}}); + expect(out.insidetextfont.color).toBe('blue'); + }); }); -describe('Pie traces:', function() { +describe('Pie traces', function() { 'use strict'; + var DARK = '#444'; + var LIGHT = '#fff'; + var gd; beforeEach(function() { gd = createGraphDiv(); }); @@ -149,7 +164,31 @@ describe('Pie traces:', function() { }; } - it('propagates explicit colors to the same labels in earlier OR later traces', function(done) { + function _checkFontColors(expFontColors) { + return function() { + d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) { + expect(this.style.fill).toBe(rgb(expFontColors[i]), 'fill color of ' + i); + }); + }; + } + + function _checkFontFamilies(expFontFamilies) { + return function() { + d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) { + expect(this.style.fontFamily).toBe(expFontFamilies[i], 'fontFamily of ' + i); + }); + }; + } + + function _checkFontSizes(expFontSizes) { + return function() { + d3.selectAll(SLICES_TEXT_SELECTOR).each(function(d, i) { + expect(this.style.fontSize).toBe(expFontSizes[i] + 'px', 'fontSize of ' + i); + }); + }; + } + + it('propagate explicit colors to the same labels in earlier OR later traces', function(done) { var data1 = [ {type: 'pie', values: [3, 2], marker: {colors: ['red', 'black']}, domain: {x: [0.5, 1]}}, {type: 'pie', values: [2, 5], domain: {x: [0, 0.5]}} @@ -432,7 +471,7 @@ describe('Pie traces:', function() { .then(done); }); - it('supports separate stroke width values per slice', function(done) { + it('support separate stroke width values per slice', function(done) { var data = [ { values: [20, 26, 55], @@ -465,6 +504,193 @@ describe('Pie traces:', function() { .catch(failTest) .then(done); }); + + [ + {fontAttr: 'textfont', textposition: 'outside'}, + {fontAttr: 'textfont', textposition: 'inside'}, + {fontAttr: 'outsidetextfont', textposition: 'outside'}, + {fontAttr: 'insidetextfont', textposition: 'inside'} + ].forEach(function(spec) { + var desc = 'allow to specify ' + spec.fontAttr + + ' properties per individual slice (textposition ' + spec.textposition + ')'; + it(desc, function(done) { + var data = { + values: [3, 2, 1], + type: 'pie', + textposition: spec.textposition + }; + data[spec.fontAttr] = { + color: ['red', 'green', 'blue'], + family: ['Arial', 'Gravitas', 'Roboto'], + size: [12, 20, 16] + }; + + Plotly.plot(gd, [data]) + .then(_checkFontColors(['red', 'green', 'blue'])) + .then(_checkFontFamilies(['Arial', 'Gravitas', 'Roboto'])) + .then(_checkFontSizes([12, 20, 16])) + .catch(failTest) + .then(done); + }); + }); + + var insideTextTestsTrace = { + values: [6, 5, 4, 3, 2, 1], + type: 'pie', + marker: { + colors: ['#ee1', '#eee', '#333', '#9467bd', '#dda', '#922'], + } + }; + + it('should use inside text colors contrasting to explicitly set slice colors by default', function(done) { + Plotly.plot(gd, [insideTextTestsTrace]) + .then(_checkFontColors([DARK, DARK, LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use inside text colors contrasting to standard slice colors by default', function(done) { + var noMarkerTrace = Lib.extendFlat({}, insideTextTestsTrace); + delete noMarkerTrace.marker; + + Plotly.plot(gd, [noMarkerTrace]) + .then(_checkFontColors([LIGHT, DARK, LIGHT, LIGHT, LIGHT, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use textfont.color for inside text instead of the contrasting default', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: 'red'}}); + Plotly.plot(gd, [data]) + .then(_checkFontColors(Lib.repeat('red', 6))) + .catch(failTest) + .then(done); + }); + + it('should use matching color from textfont.color array for inside text, contrasting otherwise', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: ['red', 'blue']}}); + Plotly.plot(gd, [data]) + .then(_checkFontColors(['red', 'blue', LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should not use layout.font.color for inside text, but a contrasting color instead', function(done) { + Plotly.plot(gd, [insideTextTestsTrace], {font: {color: 'green'}}) + .then(_checkFontColors([DARK, DARK, LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + it('should use matching color from insidetextfont.color array instead of the contrasting default', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, {textfont: {color: ['orange', 'purple']}}); + Plotly.plot(gd, [data]) + .then(_checkFontColors(['orange', 'purple', LIGHT, LIGHT, DARK, LIGHT])) + .catch(failTest) + .then(done); + }); + + [ + {fontAttr: 'outsidetextfont', textposition: 'outside'}, + {fontAttr: 'insidetextfont', textposition: 'inside'} + ].forEach(function(spec) { + it('should fall back to textfont scalar values if ' + spec.fontAttr + ' value ' + + 'arrays don\'t cover all slices', function(done) { + var data = Lib.extendFlat({}, insideTextTestsTrace, { + textposition: spec.textposition, + textfont: {color: 'orange', family: 'Gravitas', size: 12} + }); + data[spec.fontAttr] = {color: ['blue', 'yellow'], family: ['Arial', 'Arial'], size: [24, 34]}; + + Plotly.plot(gd, [data]) + .then(_checkFontColors(['blue', 'yellow', 'orange', 'orange', 'orange', 'orange'])) + .then(_checkFontFamilies(['Arial', 'Arial', 'Gravitas', 'Gravitas', 'Gravitas', 'Gravitas'])) + .then(_checkFontSizes([24, 34, 12, 12, 12, 12])) + .catch(failTest) + .then(done); + }); + }); + + it('should fall back to textfont array values and layout.font scalar (except color)' + + ' values for inside text', function(done) { + var layout = {font: {color: 'orange', family: 'serif', size: 16}}; + var data = Lib.extendFlat({}, insideTextTestsTrace, { + textfont: { + color: ['blue', 'blue'], family: ['Arial', 'Arial'], size: [18, 18] + }, + insidetextfont: { + color: ['purple'], family: ['Roboto'], size: [24] + } + }); + + Plotly.plot(gd, [data], layout) + .then(_checkFontColors(['purple', 'blue', LIGHT, LIGHT, DARK, LIGHT])) + .then(_checkFontFamilies(['Roboto', 'Arial', 'serif', 'serif', 'serif', 'serif'])) + .then(_checkFontSizes([24, 18, 16, 16, 16, 16])) + .catch(failTest) + .then(done); + }); + + it('should fall back to textfont array values and layout.font scalar' + + ' values for outside text', function(done) { + var layout = {font: {color: 'orange', family: 'serif', size: 16}}; + var data = Lib.extendFlat({}, insideTextTestsTrace, { + textposition: 'outside', + textfont: { + color: ['blue', 'blue'], family: ['Arial', 'Arial'], size: [18, 18] + }, + outsidetextfont: { + color: ['purple'], family: ['Roboto'], size: [24] + } + }); + + Plotly.plot(gd, [data], layout) + .then(_checkFontColors(['purple', 'blue', 'orange', 'orange', 'orange', 'orange'])) + .then(_checkFontFamilies(['Roboto', 'Arial', 'serif', 'serif', 'serif', 'serif'])) + .then(_checkFontSizes([24, 18, 16, 16, 16, 16])) + .catch(failTest) + .then(done); + }); + + [ + {fontAttr: 'textfont'}, + {fontAttr: 'insidetextfont'} + ].forEach(function(spec) { + it('should fall back to layout.font scalar values for inside text (except color) if ' + spec.fontAttr + ' value ' + + 'arrays don\'t cover all slices', function(done) { + var layout = {font: {color: 'orange', family: 'serif', size: 16}}; + var data = Lib.extendFlat({}, insideTextTestsTrace); + data.textposition = 'inside'; + data[spec.fontAttr] = {color: ['blue', 'yellow'], family: ['Arial', 'Arial'], size: [24, 34]}; + + Plotly.plot(gd, [data], layout) + .then(_checkFontColors(['blue', 'yellow', LIGHT, LIGHT, DARK, LIGHT])) + .then(_checkFontFamilies(['Arial', 'Arial', 'serif', 'serif', 'serif', 'serif'])) + .then(_checkFontSizes([24, 34, 16, 16, 16, 16])) + .catch(failTest) + .then(done); + }); + }); + + [ + {fontAttr: 'textfont'}, + {fontAttr: 'outsidetextfont'} + ].forEach(function(spec) { + it('should fall back to layout.font scalar values for outside text if ' + spec.fontAttr + ' value ' + + 'arrays don\'t cover all slices', function(done) { + var layout = {font: {color: 'orange', family: 'serif', size: 16}}; + var data = Lib.extendFlat({}, insideTextTestsTrace); + data.textposition = 'outside'; + data[spec.fontAttr] = {color: ['blue', 'yellow'], family: ['Arial', 'Arial'], size: [24, 34]}; + + Plotly.plot(gd, [data], layout) + .then(_checkFontColors(['blue', 'yellow', 'orange', 'orange', 'orange', 'orange'])) + .then(_checkFontFamilies(['Arial', 'Arial', 'serif', 'serif', 'serif', 'serif'])) + .then(_checkFontSizes([24, 34, 16, 16, 16, 16])) + .catch(failTest) + .then(done); + }); + }); }); describe('pie hovering', function() {