diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 32744bcbaea..7343d3bb58b 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -28,6 +28,7 @@ var subTypes = require('../../traces/scatter/subtypes'); var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); var drawing = module.exports = {}; +var appendArrayPointValue = require('../fx/helpers').appendArrayPointValue; // ----------------------------------------------------- // styling functions for plot elements @@ -679,7 +680,7 @@ function extracTextFontSize(d, trace) { } // draw text at points -drawing.textPointStyle = function(s, trace, gd) { +drawing.textPointStyle = function(s, trace, gd, inLegend) { if(!s.size()) return; var selectedTextColorFn; @@ -689,15 +690,24 @@ drawing.textPointStyle = function(s, trace, gd) { selectedTextColorFn = fns.selectedTextColorFn; } + var template = trace.texttemplate; + // If styling text in legends, do not use texttemplate + if(inLegend) template = false; s.each(function(d) { var p = d3.select(this); - var text = Lib.extractOption(d, trace, 'tx', 'text'); + var text = Lib.extractOption(d, trace, template ? 'txt' : 'tx', template ? 'texttemplate' : 'text'); if(!text && text !== 0) { p.remove(); return; } + if(template) { + var pt = {}; + appendArrayPointValue(pt, trace, d.i); + text = Lib.texttemplateString(text, {}, gd._fullLayout._d3locale, pt, d, trace._meta || {}); + } + var pos = d.tp || trace.textposition; var fontSize = extracTextFontSize(d, trace); var fontColor = selectedTextColorFn ? diff --git a/src/components/legend/style.js b/src/components/legend/style.js index b93a1232771..85c222e4671 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -271,7 +271,7 @@ module.exports = function style(s, gd) { .append('g').classed('pointtext', true) .append('text').attr('transform', 'translate(20,0)'); txt.exit().remove(); - txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd); + txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd, true); } function styleWaterfalls(d) { diff --git a/src/lib/index.js b/src/lib/index.js index 726cc5e0a30..0073533aab1 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -964,7 +964,7 @@ lib.numSeparate = function(value, separators, separatethousands) { return x1 + x2; }; -lib.TEMPLATE_STRING_REGEX = /%{([^\s%{}:]*)(:[^}]*)?}/g; +lib.TEMPLATE_STRING_REGEX = /%{([^\s%{}:]*)([:|\|][^}]*)?}/g; var SIMPLE_PROPERTY_REGEX = /^\w*$/; /** @@ -993,9 +993,25 @@ lib.templateString = function(string, obj) { }); }; -var TEMPLATE_STRING_FORMAT_SEPARATOR = /^:/; -var numberOfHoverTemplateWarnings = 0; -var maximumNumberOfHoverTemplateWarnings = 10; +var hovertemplateWarnings = { + max: 10, + count: 0, + name: 'hovertemplate' +}; +lib.hovertemplateString = function() { + return templateFormatString.apply(hovertemplateWarnings, arguments); +}; + +var texttemplateWarnings = { + max: 10, + count: 0, + name: 'texttemplate' +}; +lib.texttemplateString = function() { + return templateFormatString.apply(texttemplateWarnings, arguments); +}; + +var TEMPLATE_STRING_FORMAT_SEPARATOR = /^[:|\|]/; /** * Substitute values from an object into a string and optionally formats them using d3-format, * or fallback to associated labels. @@ -1005,15 +1021,17 @@ var maximumNumberOfHoverTemplateWarnings = 10; * Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf' * Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00' * - * @param {obj} d3 locale * @param {string} input string containing %{...:...} template strings * @param {obj} data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'} + * @param {obj} d3 locale * @param {obj} data objects containing substitution values * * @return {string} templated string */ -lib.hovertemplateString = function(string, labels, d3locale) { +function templateFormatString(string, labels, d3locale) { + var opts = this; var args = arguments; + if(!labels) labels = {}; // Not all that useful, but cache nestedProperty instantiation // just in case it speeds things up *slightly*: var getterCache = {}; @@ -1022,6 +1040,7 @@ lib.hovertemplateString = function(string, labels, d3locale) { var obj, value, i; for(i = 3; i < args.length; i++) { obj = args[i]; + if(!obj) continue; if(obj.hasOwnProperty(key)) { value = obj[key]; break; @@ -1034,32 +1053,38 @@ lib.hovertemplateString = function(string, labels, d3locale) { if(value !== undefined) break; } - if(value === undefined) { - if(numberOfHoverTemplateWarnings < maximumNumberOfHoverTemplateWarnings) { - lib.warn('Variable \'' + key + '\' in hovertemplate could not be found!'); + if(value === undefined && opts) { + if(opts.count < opts.max) { + lib.warn('Variable \'' + key + '\' in ' + opts.name + ' could not be found!'); value = match; } - if(numberOfHoverTemplateWarnings === maximumNumberOfHoverTemplateWarnings) { - lib.warn('Too many hovertemplate warnings - additional warnings will be suppressed'); + if(opts.count === opts.max) { + lib.warn('Too many ' + opts.name + ' warnings - additional warnings will be suppressed'); } - numberOfHoverTemplateWarnings++; + opts.count++; + + return match; } if(format) { var fmt; - if(d3locale) { - fmt = d3locale.numberFormat; - } else { - fmt = d3.format; + if(format[0] === ':') { + fmt = d3locale ? d3locale.numberFormat : d3.format; + value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value); + } + + if(format[0] === '|') { + fmt = d3locale ? d3locale.timeFormat.utc : d3.time.format.utc; + var ms = lib.dateTime2ms(value); + value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt); } - value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value); } else { if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label']; } return value; }); -}; +} /* * alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc diff --git a/src/components/fx/hovertemplate_attributes.js b/src/plots/template_attributes.js similarity index 53% rename from src/components/fx/hovertemplate_attributes.js rename to src/plots/template_attributes.js index 23e53961500..fd5a5bdb59c 100644 --- a/src/components/fx/hovertemplate_attributes.js +++ b/src/plots/template_attributes.js @@ -8,12 +8,20 @@ 'use strict'; -var FORMAT_LINK = require('../../constants/docs').FORMAT_LINK; +var FORMAT_LINK = require('../constants/docs').FORMAT_LINK; +var DATE_FORMAT_LINK = require('../constants/docs').DATE_FORMAT_LINK; -module.exports = function(opts, extra) { - opts = opts || {}; - extra = extra || {}; +var templateFormatStringDescription = [ + 'Variables are inserted using %{variable}, for example "y: %{y}".', + 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', + FORMAT_LINK, + 'for details on the formatting syntax.', + 'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".', + DATE_FORMAT_LINK, + 'for details on the date formatting syntax.' +].join(' '); +function describeVariables(extra) { var descPart = extra.description ? ' ' + extra.description : ''; var keys = extra.keys || []; if(keys.length > 0) { @@ -28,6 +36,14 @@ module.exports = function(opts, extra) { descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.'; } } + return descPart; +} + +exports.hovertemplateAttrs = function(opts, extra) { + opts = opts || {}; + extra = extra || {}; + + var descPart = describeVariables(extra); var hovertemplate = { valType: 'string', @@ -37,10 +53,7 @@ module.exports = function(opts, extra) { description: [ 'Template string used for rendering the information that appear on hover box.', 'Note that this will override `hoverinfo`.', - 'Variables are inserted using %{variable}, for example "y: %{y}".', - 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', - FORMAT_LINK, - 'for details on the formatting syntax.', + templateFormatStringDescription, 'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plot.ly/javascript/plotlyjs-events/#event-data.', 'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', descPart, @@ -55,3 +68,29 @@ module.exports = function(opts, extra) { return hovertemplate; }; + +exports.texttemplateAttrs = function(opts, extra) { + opts = opts || {}; + extra = extra || {}; + + var descPart = describeVariables(extra); + + var texttemplate = { + valType: 'string', + role: 'info', + dflt: '', + editType: opts.editType || 'calc', + description: [ + 'Template string used for rendering the information text that appear on points.', + 'Note that this will override `textinfo`.', + templateFormatStringDescription, + 'Every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', + descPart + ].join(' ') + }; + + if(opts.arrayOk !== false) { + texttemplate.arrayOk = true; + } + return texttemplate; +}; diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 5c391df606f..c800d8bc30a 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -9,7 +9,8 @@ 'use strict'; var scatterAttrs = require('../scatter/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var colorScaleAttrs = require('../../components/colorscale/attributes'); var fontAttrs = require('../../plots/font_attributes'); var constants = require('./constants.js'); @@ -59,6 +60,9 @@ module.exports = { dy: scatterAttrs.dy, text: scatterAttrs.text, + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: constants.eventDataKeys + }), hovertext: scatterAttrs.hovertext, hovertemplate: hovertemplateAttrs({}, { keys: constants.eventDataKeys diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index e6df70f4562..5627ffa3db7 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -155,6 +155,8 @@ function handleText(traceIn, traceOut, layout, coerce, textposition, opts) { if(moduleHasConstrain) coerce('constraintext'); if(moduleHasCliponaxis) coerce('cliponaxis'); if(moduleHasTextangle) coerce('textangle'); + + coerce('texttemplate'); } if(hasInside) { diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index d730165dac7..11d8ad24f68 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -26,6 +26,8 @@ var attributes = require('./attributes'); var attributeText = attributes.text; var attributeTextPosition = attributes.textposition; +var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue; + // padding in pixels around text var TEXTPAD = 3; @@ -226,7 +228,7 @@ function appendBarText(gd, plotinfo, bar, calcTrace, i, x0, x1, y0, y1, opts) { var trace = calcTrace[0].trace; var isHorizontal = (trace.orientation === 'h'); - var text = getText(calcTrace, i, xa, ya); + var text = getText(fullLayout, calcTrace, i, xa, ya); textPosition = getTextPosition(trace, i); // compute text position @@ -537,14 +539,17 @@ function getTransform(opts) { return transformTranslate + transformScale + transformRotate; } -function getText(calcTrace, index, xa, ya) { +function getText(fullLayout, calcTrace, index, xa, ya) { var trace = calcTrace[0].trace; + var texttemplate = trace.texttemplate; var value; - if(!trace.textinfo) { - value = helpers.getValue(trace.text, index); - } else { + if(texttemplate) { + value = calcTexttemplate(fullLayout, calcTrace, index, xa, ya); + } else if(trace.textinfo) { value = calcTextinfo(calcTrace, index, xa, ya); + } else { + value = helpers.getValue(trace.text, index); } return helpers.coerceString(attributeText, value); @@ -555,6 +560,65 @@ function getTextPosition(trace, index) { return helpers.coerceEnumerated(attributeTextPosition, value); } +function calcTexttemplate(fullLayout, calcTrace, index, xa, ya) { + var trace = calcTrace[0].trace; + var texttemplate = Lib.castOption(trace, index, 'texttemplate'); + if(!texttemplate) return ''; + var isHorizontal = (trace.orientation === 'h'); + var isWaterfall = (trace.type === 'waterfall'); + var isFunnel = (trace.type === 'funnel'); + + function formatLabel(u) { + var pAxis = isHorizontal ? ya : xa; + return tickText(pAxis, u, true).text; + } + + function formatNumber(v) { + var sAxis = isHorizontal ? xa : ya; + return tickText(sAxis, +v, true).text; + } + + var cdi = calcTrace[index]; + var obj = {}; + + obj.label = cdi.p; + obj.labelLabel = formatLabel(cdi.p); + + var tx = Lib.castOption(trace, cdi.i, 'text'); + if(tx === 0 || tx) obj.text = tx; + + obj.value = cdi.s; + obj.valueLabel = formatNumber(cdi.s); + + var pt = {}; + appendArrayPointValue(pt, trace, cdi.i); + + if(isWaterfall) { + obj.delta = +cdi.rawS || cdi.s; + obj.deltaLabel = formatNumber(obj.delta); + obj.final = cdi.v; + obj.finalLabel = formatNumber(obj.final); + obj.initial = obj.final - obj.delta; + obj.initialLabel = formatNumber(obj.initial); + } + + if(isFunnel) { + obj.value = cdi.s; + obj.valueLabel = formatNumber(obj.value); + + obj.percentInitial = cdi.begR; + obj.percentInitialLabel = Lib.formatPercent(cdi.begR); + obj.percentPrevious = cdi.difR; + obj.percentPreviousLabel = Lib.formatPercent(cdi.difR); + obj.percentTotal = cdi.sumR; + obj.percenTotalLabel = Lib.formatPercent(cdi.sumR); + } + + var customdata = Lib.castOption(trace, cdi.i, 'customdata'); + if(customdata) obj.customdata = customdata; + return Lib.texttemplateString(texttemplate, obj, fullLayout._d3locale, pt, obj, trace._meta || {}); +} + function calcTextinfo(calcTrace, index, xa, ya) { var trace = calcTrace[0].trace; var isHorizontal = (trace.orientation === 'h'); diff --git a/src/traces/barpolar/attributes.js b/src/traces/barpolar/attributes.js index e6540f1a6c6..46de5c8f4bc 100644 --- a/src/traces/barpolar/attributes.js +++ b/src/traces/barpolar/attributes.js @@ -8,7 +8,7 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; var scatterPolarAttrs = require('../scatterpolar/attributes'); var barAttrs = require('../bar/attributes'); diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 66476cb1f26..a8144de00d0 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -11,7 +11,7 @@ var scatterAttrs = require('../scatter/attributes'); var barAttrs = require('../bar/attributes'); var colorAttrs = require('../../components/color/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; var scatterMarkerAttrs = scatterAttrs.marker; diff --git a/src/traces/choropleth/attributes.js b/src/traces/choropleth/attributes.js index 0e5145f18e5..16a3712413a 100644 --- a/src/traces/choropleth/attributes.js +++ b/src/traces/choropleth/attributes.js @@ -8,7 +8,7 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var scatterGeoAttrs = require('../scattergeo/attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); var plotAttrs = require('../../plots/attributes'); diff --git a/src/traces/choroplethmapbox/attributes.js b/src/traces/choroplethmapbox/attributes.js index 4b36f3d44ee..31c932e1273 100644 --- a/src/traces/choroplethmapbox/attributes.js +++ b/src/traces/choroplethmapbox/attributes.js @@ -10,7 +10,7 @@ var choroplethAttrs = require('../choropleth/attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; diff --git a/src/traces/cone/attributes.js b/src/traces/cone/attributes.js index a47c8dc8ee5..5e263c86515 100644 --- a/src/traces/cone/attributes.js +++ b/src/traces/cone/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var mesh3dAttrs = require('../mesh3d/attributes'); var baseAttrs = require('../../plots/attributes'); diff --git a/src/traces/densitymapbox/attributes.js b/src/traces/densitymapbox/attributes.js index 4dcc168f8eb..c37046b1f76 100644 --- a/src/traces/densitymapbox/attributes.js +++ b/src/traces/densitymapbox/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var plotAttrs = require('../../plots/attributes'); var scatterMapboxAttrs = require('../scattermapbox/attributes'); diff --git a/src/traces/funnel/attributes.js b/src/traces/funnel/attributes.js index 5e5f5f2ed27..c21d05865a7 100644 --- a/src/traces/funnel/attributes.js +++ b/src/traces/funnel/attributes.js @@ -11,7 +11,8 @@ var barAttrs = require('../bar/attributes'); var lineAttrs = require('../scatter/attributes').line; var plotAttrs = require('../../plots/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var constants = require('./constants'); var extendFlat = require('../../lib/extend').extendFlat; var Color = require('../../components/color'); @@ -46,6 +47,10 @@ module.exports = { 'are computed separately (per trace).' ].join(' ') }, + // TODO: incorporate `label` and `value` in the eventData + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: constants.eventDataKeys.concat(['label', 'value']) + }), text: barAttrs.text, textposition: extendFlat({}, barAttrs.textposition, {dflt: 'auto'}), diff --git a/src/traces/funnel/defaults.js b/src/traces/funnel/defaults.js index f4ae0a1fd71..870f09cf9a8 100644 --- a/src/traces/funnel/defaults.js +++ b/src/traces/funnel/defaults.js @@ -46,7 +46,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { moduleHasInsideanchor: true }); - if(traceOut.textposition !== 'none') { + if(traceOut.textposition !== 'none' && !traceOut.texttemplate) { coerce('textinfo', Array.isArray(text) ? 'text+value' : 'value'); } diff --git a/src/traces/funnelarea/attributes.js b/src/traces/funnelarea/attributes.js index 7c995d007fb..d0bc5dbed7b 100644 --- a/src/traces/funnelarea/attributes.js +++ b/src/traces/funnelarea/attributes.js @@ -11,7 +11,8 @@ var pieAttrs = require('../pie/attributes'); var plotAttrs = require('../../plots/attributes'); var domainAttrs = require('../../plots/domain').attributes; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; @@ -53,12 +54,16 @@ module.exports = { flags: ['label', 'text', 'value', 'percent'] }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['label', 'color', 'value', 'text', 'percent'] + }), + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['label', 'text', 'value', 'percent', 'name'] }), hovertemplate: hovertemplateAttrs({}, { - keys: ['label', 'color', 'value', 'percent', 'text'] + keys: ['label', 'color', 'value', 'text', 'percent'] }), textposition: extendFlat({}, pieAttrs.textposition, { diff --git a/src/traces/funnelarea/defaults.js b/src/traces/funnelarea/defaults.js index ae20aba2d6f..a138b8d51ea 100644 --- a/src/traces/funnelarea/defaults.js +++ b/src/traces/funnelarea/defaults.js @@ -46,11 +46,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('scalegroup'); var textData = coerce('text'); - var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); + var textTemplate = coerce('texttemplate'); + var textInfo; + if(!textTemplate) textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); + coerce('hovertext'); coerce('hovertemplate'); - if(textInfo && textInfo !== 'none') { + if(textTemplate || (textInfo && textInfo !== 'none')) { var textposition = coerce('textposition'); handleText(traceIn, traceOut, layout, coerce, textposition, { moduleHasSelected: false, diff --git a/src/traces/funnelarea/plot.js b/src/traces/funnelarea/plot.js index 3103238e5d6..9e1091e2d66 100644 --- a/src/traces/funnelarea/plot.js +++ b/src/traces/funnelarea/plot.js @@ -79,6 +79,7 @@ module.exports = function plot(gd, cdModule) { slicePath.attr('d', shape); // add text + piePlot.formatSliceLabel(gd, pt, cd0); var textPosition = pieHelpers.castOption(trace.textposition, pt.pts); var sliceTextGroup = sliceTop.selectAll('g.slicetext') .data(pt.text && (textPosition !== 'none') ? [0] : []); diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js index 1fc7aa07eb6..6be9ef1fc5a 100644 --- a/src/traces/heatmap/attributes.js +++ b/src/traces/heatmap/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var scatterAttrs = require('../scatter/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var colorScaleAttrs = require('../../components/colorscale/attributes'); var FORMAT_LINK = require('../../constants/docs').FORMAT_LINK; diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 4869e9b02fc..7f9a4d8a2a8 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var barAttrs = require('../bar/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var makeBinAttrs = require('./bin_attributes'); var constants = require('./constants'); var extendFlat = require('../../lib/extend').extendFlat; diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js index d4a6c6de48a..2fa6ffbd04f 100644 --- a/src/traces/histogram2d/attributes.js +++ b/src/traces/histogram2d/attributes.js @@ -11,7 +11,7 @@ var histogramAttrs = require('../histogram/attributes'); var makeBinAttrs = require('../histogram/bin_attributes'); var heatmapAttrs = require('../heatmap/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var colorScaleAttrs = require('../../components/colorscale/attributes'); var extendFlat = require('../../lib/extend').extendFlat; diff --git a/src/traces/isosurface/attributes.js b/src/traces/isosurface/attributes.js index 37cf47f316e..c294ca45f01 100644 --- a/src/traces/isosurface/attributes.js +++ b/src/traces/isosurface/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var meshAttrs = require('../mesh3d/attributes'); var baseAttrs = require('../../plots/attributes'); diff --git a/src/traces/mesh3d/attributes.js b/src/traces/mesh3d/attributes.js index 4d538925f7d..db7c1963568 100644 --- a/src/traces/mesh3d/attributes.js +++ b/src/traces/mesh3d/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var surfaceAttrs = require('../surface/attributes'); var baseAttrs = require('../../plots/attributes'); diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index a2188a08636..8289c4a6642 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -12,7 +12,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var plotAttrs = require('../../plots/attributes'); var fontAttrs = require('../../plots/font_attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var domainAttrs = require('../../plots/domain').attributes; var line = extendFlat( diff --git a/src/traces/pie/attributes.js b/src/traces/pie/attributes.js index 475ae885fb0..04518282f6a 100644 --- a/src/traces/pie/attributes.js +++ b/src/traces/pie/attributes.js @@ -12,7 +12,8 @@ var plotAttrs = require('../../plots/attributes'); var domainAttrs = require('../../plots/domain').attributes; var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; @@ -105,7 +106,7 @@ module.exports = { text: { valType: 'data_array', - editType: 'calc', + editType: 'plot', description: [ 'Sets text elements associated with each sector.', 'If trace `textinfo` contains a *text* flag, these elements will be seen', @@ -163,6 +164,9 @@ module.exports = { hovertemplate: hovertemplateAttrs({}, { keys: ['label', 'color', 'value', 'percent', 'text'] }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['label', 'color', 'value', 'percent', 'text'] + }), textposition: { valType: 'enumerated', role: 'info', diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 461094aac19..89b361d29fc 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -13,8 +13,6 @@ var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var tinycolor = require('tinycolor2'); var Color = require('../../components/color'); -var helpers = require('./helpers'); -var isValidTextValue = require('../../lib').isValidTextValue; var extendedColorWayList = {}; @@ -93,32 +91,6 @@ function calc(gd, trace) { // include the sum of all values in the first point if(cd[0]) cd[0].vTotal = vTotal; - // now insert text - var textinfo = trace.textinfo; - if(textinfo && textinfo !== 'none') { - var parts = textinfo.split('+'); - var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; - var hasLabel = hasFlag('label'); - var hasText = hasFlag('text'); - var hasValue = hasFlag('value'); - var hasPercent = hasFlag('percent'); - - var separators = fullLayout.separators; - var text; - - for(i = 0; i < cd.length; i++) { - pt = cd[i]; - text = hasLabel ? [pt.label] : []; - if(hasText) { - var tx = helpers.getFirstFilled(trace.text, pt.pts); - if(isValidTextValue(tx)) text.push(tx); - } - if(hasValue) text.push(helpers.formatPieValue(pt.v, separators)); - if(hasPercent) text.push(helpers.formatPiePercent(pt.v / vTotal, separators)); - pt.text = text.join('
'); - } - } - return cd; } diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 3e94c24483d..c25b46b7671 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -47,11 +47,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // TODO: hole needs to be coerced to the same value within a scaleegroup var textData = coerce('text'); - var textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); + var textTemplate = coerce('texttemplate'); + var textInfo; + if(!textTemplate) textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); + coerce('hovertext'); coerce('hovertemplate'); - if(textInfo && textInfo !== 'none') { + if(textTemplate || (textInfo && textInfo !== 'none')) { var textposition = coerce('textposition'); handleText(traceIn, traceOut, layout, coerce, textposition, { moduleHasSelected: false, diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index bcc67a99d51..8f528c27aad 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -18,6 +18,7 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var helpers = require('./helpers'); var eventData = require('./event_data'); +var isValidTextValue = require('../../lib').isValidTextValue; function plot(gd, cdModule) { var fullLayout = gd._fullLayout; @@ -125,6 +126,7 @@ function plot(gd, cdModule) { } // add text + formatSliceLabel(gd, pt, cd0); var textPosition = helpers.castOption(trace.textposition, pt.pts); var sliceTextGroup = sliceTop.selectAll('g.slicetext') .data(pt.text && (textPosition !== 'none') ? [0] : []); @@ -941,8 +943,63 @@ function setCoords(cd) { } } +function formatSliceLabel(gd, pt, cd0) { + var fullLayout = gd._fullLayout; + var trace = cd0.trace; + // look for textemplate + var texttemplate = trace.texttemplate; + + // now insert text + var textinfo = trace.textinfo; + if(!texttemplate && textinfo && textinfo !== 'none') { + var parts = textinfo.split('+'); + var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; + var hasLabel = hasFlag('label'); + var hasText = hasFlag('text'); + var hasValue = hasFlag('value'); + var hasPercent = hasFlag('percent'); + + var separators = fullLayout.separators; + var text; + + text = hasLabel ? [pt.label] : []; + if(hasText) { + var tx = helpers.getFirstFilled(trace.text, pt.pts); + if(isValidTextValue(tx)) text.push(tx); + } + if(hasValue) text.push(helpers.formatPieValue(pt.v, separators)); + if(hasPercent) text.push(helpers.formatPiePercent(pt.v / cd0.vTotal, separators)); + pt.text = text.join('
'); + } + + function makeTemplateVariables(pt) { + return { + label: pt.label, + value: pt.v, + valueLabel: helpers.formatPieValue(pt.v, fullLayout.separators), + percent: pt.v / cd0.vTotal, + percentLabel: helpers.formatPiePercent(pt.v / cd0.vTotal, fullLayout.separators), + color: pt.color, + text: pt.text, + customdata: Lib.castOption(trace, pt.i, 'customdata') + }; + } + + if(texttemplate) { + var txt = Lib.castOption(trace, pt.i, 'texttemplate'); + if(!txt) { + pt.text = ''; + } else { + var obj = makeTemplateVariables(pt); + var ptTx = helpers.getFirstFilled(trace.text, pt.pts); + if(isValidTextValue(ptTx)) obj.text = ptTx; + pt.text = Lib.texttemplateString(txt, obj, gd._fullLayout._d3locale, obj, trace._meta || {}); + } + } +} module.exports = { plot: plot, + formatSliceLabel: formatSliceLabel, transformInsideText: transformInsideText, determineInsideTextFont: determineInsideTextFont, positionTitleOutside: positionTitleOutside, diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js index 7fe25588f35..a3501323cf9 100644 --- a/src/traces/sankey/attributes.js +++ b/src/traces/sankey/attributes.js @@ -13,7 +13,7 @@ var plotAttrs = require('../../plots/attributes'); var colorAttrs = require('../../components/color/attributes'); var fxAttrs = require('../../components/fx/attributes'); var domainAttrs = require('../../plots/domain').attributes; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var colorAttributes = require('../../components/colorscale/attributes'); var templatedArray = require('../../plot_api/plot_template').templatedArray; diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index 8842602402a..69086bc3e51 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -18,6 +18,7 @@ module.exports = function arraysToCalcdata(cd, trace) { for(var i = 0; i < cd.length; i++) cd[i].i = i; Lib.mergeArray(trace.text, cd, 'tx'); + Lib.mergeArray(trace.texttemplate, cd, 'txt'); Lib.mergeArray(trace.hovertext, cd, 'htx'); Lib.mergeArray(trace.customdata, cd, 'data'); Lib.mergeArray(trace.textposition, cd, 'tp'); diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index e46a5f75513..209c4f9ec3e 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -8,7 +8,8 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var colorScaleAttrs = require('../../components/colorscale/attributes'); var fontAttrs = require('../../plots/font_attributes'); var dash = require('../../components/drawing/attributes').dash; @@ -166,6 +167,10 @@ module.exports = { 'these elements will be seen in the hover labels.' ].join(' ') }, + + texttemplate: texttemplateAttrs({}, { + + }), hovertext: { valType: 'string', role: 'info', diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 3ff337d0e57..0efdc43345a 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -52,6 +52,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js index d02559bf30c..e985957375e 100644 --- a/src/traces/scatter3d/attributes.js +++ b/src/traces/scatter3d/attributes.js @@ -10,7 +10,8 @@ var scatterAttrs = require('../scatter/attributes'); var colorAttributes = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var baseAttrs = require('../../plots/attributes'); var DASHES = require('../../constants/gl3d_dashes'); @@ -84,6 +85,9 @@ var attrs = module.exports = overrideAll({ 'If trace `hoverinfo` contains a *text* flag and *hovertext* is not set,', 'these elements will be seen in the hover labels.' ].join(' ') + }), + texttemplate: texttemplateAttrs({}, { + }), hovertext: extendFlat({}, scatterAttrs.hovertext, { description: [ diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index c903b3f8c93..79d48bed66f 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -22,6 +22,8 @@ var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); var DASH_PATTERNS = require('../../constants/gl3d_dashes'); var MARKER_SYMBOLS = require('../../constants/gl3d_markers'); +var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue; + var calculateError = require('./calc_errors'); function LineWithMarkers(scene, uid) { @@ -239,6 +241,22 @@ function convertPlotlyOptions(scene, data) { for(i = 0; i < len; i++) text[i] = data.text; } + // check texttemplate + var texttemplate = data.texttemplate; + if(texttemplate) { + var isArray = Array.isArray(texttemplate); + var N = isArray ? Math.min(texttemplate.length, len) : len; + var txt = isArray ? function(i) {return texttemplate[i];} : function() { return texttemplate;}; + var d3locale = scene.fullLayout._d3locale; + text = new Array(N); + for(i = 0; i < N; i++) { + var pt = {}; + pt.text = text[i]; + appendArrayPointValue(pt, data, i); + text[i] = Lib.texttemplateString(txt(i), pt, d3locale, pt, data._meta || {}); + } + } + // Build object parameters params = { position: points, diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 14de88e49ad..3ad572306a3 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -45,6 +45,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true}); } diff --git a/src/traces/scatter3d/index.js b/src/traces/scatter3d/index.js index 98fecd71957..58ea27a3e00 100644 --- a/src/traces/scatter3d/index.js +++ b/src/traces/scatter3d/index.js @@ -29,7 +29,7 @@ module.exports = { moduleType: 'trace', name: 'scatter3d', basePlotModule: require('../../plots/gl3d'), - categories: ['gl3d', 'symbols', 'showLegend'], + categories: ['gl3d', 'symbols', 'showLegend', 'scatter-like'], meta: { hrName: 'scatter_3d', description: [ diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 3c2fed05336..88dd85fb404 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -10,7 +10,8 @@ var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var colorScaleAttrs = require('../../components/colorscale/attributes'); var extendFlat = require('../../lib/extend').extendFlat; @@ -52,6 +53,9 @@ module.exports = { 'these elements will be seen in the hover labels.' ].join(' ') }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['a', 'b', 'text'] + }), hovertext: extendFlat({}, scatterAttrs.hovertext, { description: [ 'Sets hover text elements associated with each (a,b) point.', diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index cea51be903d..e32d6aafec6 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -44,6 +44,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut._length = len; coerce('text'); + coerce('texttemplate'); coerce('hovertext'); var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 772d6f8c786..c327921cd58 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -8,7 +8,8 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); var colorAttributes = require('../../components/colorscale/attributes'); @@ -64,6 +65,9 @@ module.exports = overrideAll({ 'these elements will be seen in the hover labels.' ].join(' ') }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['lat', 'lon', 'location', 'text'] + }), hovertext: extendFlat({}, scatterAttrs.hovertext, { description: [ 'Sets hover text elements associated with each (lon,lat) pair', diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js index d2b51fbfeb2..f4c8c8fad4f 100644 --- a/src/traces/scattergeo/defaults.js +++ b/src/traces/scattergeo/defaults.js @@ -46,6 +46,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 4aa74ec4202..c56697489b2 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -98,3 +98,4 @@ var attrs = module.exports = overrideAll({ attrs.x.editType = attrs.y.editType = attrs.x0.editType = attrs.y0.editType = 'calc+clearAxisTypes'; attrs.hovertemplate = scatterAttrs.hovertemplate; +attrs.texttemplate = scatterAttrs.texttemplate; diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 27916fca170..92536d4ddce 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -28,6 +28,8 @@ var TEXTOFFSETSIGN = { start: 1, left: 1, end: -1, right: -1, middle: 0, center: 0, bottom: 1, top: -1 }; +var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue; + function convertStyle(gd, trace) { var i; @@ -47,7 +49,7 @@ function convertStyle(gd, trace) { if(trace.visible !== true) return opts; if(subTypes.hasText(trace)) { - opts.text = convertTextStyle(trace); + opts.text = convertTextStyle(trace, gd); opts.textSel = convertTextSelection(trace, trace.selected); opts.textUnsel = convertTextSelection(trace, trace.unselected); } @@ -100,7 +102,7 @@ function convertStyle(gd, trace) { return opts; } -function convertTextStyle(trace) { +function convertTextStyle(trace, gd) { var count = trace._length; var textfontIn = trace.textfont; var textpositionIn = trace.textposition; @@ -111,13 +113,33 @@ function convertTextStyle(trace) { var optsOut = {}; var i; - optsOut.text = trace.text; - if(Array.isArray(optsOut.text) && optsOut.text.length < count) { - optsOut.text = trace.text.slice(); + var texttemplate = trace.texttemplate; + if(texttemplate) { + optsOut.text = []; + var isArray = Array.isArray(texttemplate); + var N = isArray ? Math.min(texttemplate.length, count) : count; + var txt = isArray ? function(i) {return texttemplate[i];} : function() { return texttemplate;}; + var d3locale = gd._fullLayout._d3locale; + for(i = 0; i < N; i++) { + var pt = {}; + appendArrayPointValue(pt, trace, i); + optsOut.text.push(Lib.texttemplateString(txt(i), pt, d3locale, pt, trace._meta || {})); + } + } else { + if(Array.isArray(trace.text) && trace.text.length < count) { + // if text array is shorter, we'll need to append to it, so let's slice to prevent mutating + optsOut.text = trace.text.slice(); + } else { + optsOut.text = trace.text; + } + } + // pad text array with empty strings + if(Array.isArray(optsOut.text)) { for(i = optsOut.text.length; i < count; i++) { optsOut.text[i] = ''; } } + optsOut.opacity = trace.opacity; optsOut.font = {}; optsOut.align = []; diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index 82a05c4e52f..5987ca12d7b 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -52,6 +52,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 7fb71c44e11..c9f96a02630 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -8,7 +8,8 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var scatterGeoAttrs = require('../scattergeo/attributes'); var scatterAttrs = require('../scatter/attributes'); var mapboxAttrs = require('../../plots/mapbox/layout_attributes'); @@ -49,6 +50,9 @@ module.exports = overrideAll({ 'these elements will be seen in the hover labels.' ].join(' ') }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['lat', 'lon', 'text'] + }), hovertext: extendFlat({}, scatterAttrs.hovertext, { description: [ 'Sets hover text elements associated with each (lon,lat) pair', diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index ace20bea379..9aa9c13b79c 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -20,7 +20,7 @@ var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); var subTypes = require('../scatter/subtypes'); var convertTextOpts = require('../../plots/mapbox/convert_text_opts'); -module.exports = function convert(calcTrace) { +module.exports = function convert(gd, calcTrace) { var trace = calcTrace[0].trace; var isVisible = (trace.visible === true && trace._length !== 0); @@ -87,7 +87,7 @@ module.exports = function convert(calcTrace) { } if(hasSymbols || hasText) { - symbol.geojson = makeSymbolGeoJSON(calcTrace); + symbol.geojson = makeSymbolGeoJSON(calcTrace, gd); Lib.extendFlat(symbol.layout, { visibility: 'visible', @@ -229,7 +229,7 @@ function makeCircleOpts(calcTrace) { }; } -function makeSymbolGeoJSON(calcTrace) { +function makeSymbolGeoJSON(calcTrace, gd) { var trace = calcTrace[0].trace; var marker = trace.marker || {}; @@ -251,6 +251,16 @@ function makeSymbolGeoJSON(calcTrace) { if(isBADNUM(calcPt.lonlat)) continue; + var txt = trace.texttemplate; + if(txt) { + var txti = Array.isArray(txt) ? (txt[i] || '') : txt; + calcPt.text = calcPt.tx; + calcPt.lon = calcPt.lonlat[0]; + calcPt.lat = calcPt.lonlat[1]; + calcPt.customdata = calcPt.data; + calcPt.txt = Lib.texttemplateString(txti, {}, gd._fullLayout._d3locale, calcPt, trace._meta || {}); + } + features.push({ type: 'Feature', geometry: { @@ -259,7 +269,7 @@ function makeSymbolGeoJSON(calcTrace) { }, properties: { symbol: fillSymbol(calcPt.mx), - text: fillText(calcPt.tx) + text: txt ? calcPt.txt : fillText(calcPt.tx) } }); } diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index 4708202f393..b0f088f9f14 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -29,6 +29,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('text'); + coerce('texttemplate'); coerce('hovertext'); coerce('hovertemplate'); coerce('mode'); diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js index 279b1bad81e..ebe19400d3a 100644 --- a/src/traces/scattermapbox/index.js +++ b/src/traces/scattermapbox/index.js @@ -28,7 +28,7 @@ module.exports = { moduleType: 'trace', name: 'scattermapbox', basePlotModule: require('../../plots/mapbox'), - categories: ['mapbox', 'gl', 'symbols', 'showLegend', 'scatterlike'], + categories: ['mapbox', 'gl', 'symbols', 'showLegend', 'scatter-like'], meta: { hrName: 'scatter_mapbox', description: [ diff --git a/src/traces/scattermapbox/plot.js b/src/traces/scattermapbox/plot.js index e683980a722..55c2e910dcb 100644 --- a/src/traces/scattermapbox/plot.js +++ b/src/traces/scattermapbox/plot.js @@ -67,7 +67,7 @@ proto.addLayer = function(k, opts, below) { proto.update = function update(calcTrace) { var subplot = this.subplot; var map = subplot.map; - var optsAll = convert(calcTrace); + var optsAll = convert(subplot.gd, calcTrace); var below = subplot.belowLookup['trace-' + this.uid]; var i, k, opts; @@ -113,7 +113,7 @@ proto.dispose = function dispose() { module.exports = function createScatterMapbox(subplot, calcTrace) { var trace = calcTrace[0].trace; var scatterMapbox = new ScatterMapbox(subplot, trace.uid); - var optsAll = convert(calcTrace); + var optsAll = convert(subplot.gd, calcTrace); var below = scatterMapbox.below = subplot.belowLookup['trace-' + trace.uid]; for(var i = 0; i < ORDER.length; i++) { diff --git a/src/traces/scatterpolar/attributes.js b/src/traces/scatterpolar/attributes.js index d1a2b86fce5..1e5b2742dc6 100644 --- a/src/traces/scatterpolar/attributes.js +++ b/src/traces/scatterpolar/attributes.js @@ -8,7 +8,8 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var extendFlat = require('../../lib/extend').extendFlat; var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); @@ -85,6 +86,9 @@ module.exports = { }, text: scatterAttrs.text, + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['r', 'theta', 'text'] + }), hovertext: scatterAttrs.hovertext, line: { diff --git a/src/traces/scatterpolar/defaults.js b/src/traces/scatterpolar/defaults.js index 81f7178549a..cc1c626b5a1 100644 --- a/src/traces/scatterpolar/defaults.js +++ b/src/traces/scatterpolar/defaults.js @@ -48,6 +48,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scatterpolargl/attributes.js b/src/traces/scatterpolargl/attributes.js index 14ebb4568da..bef0834efa1 100644 --- a/src/traces/scatterpolargl/attributes.js +++ b/src/traces/scatterpolargl/attributes.js @@ -10,6 +10,7 @@ var scatterPolarAttrs = require('../scatterpolar/attributes'); var scatterGlAttrs = require('../scattergl/attributes'); +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; module.exports = { mode: scatterPolarAttrs.mode, @@ -22,6 +23,9 @@ module.exports = { thetaunit: scatterPolarAttrs.thetaunit, text: scatterPolarAttrs.text, + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['r', 'theta', 'text'] + }), hovertext: scatterPolarAttrs.hovertext, hovertemplate: scatterPolarAttrs.hovertemplate, diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js index 3f534d7a207..5164d8f10e5 100644 --- a/src/traces/scatterpolargl/defaults.js +++ b/src/traces/scatterpolargl/defaults.js @@ -47,6 +47,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 9b86099c84b..990b9dbc9cf 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -8,7 +8,8 @@ 'use strict'; -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var scatterAttrs = require('../scatter/attributes'); var plotAttrs = require('../../plots/attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); @@ -80,6 +81,9 @@ module.exports = { 'these elements will be seen in the hover labels.' ].join(' ') }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['a', 'b', 'c', 'text'] + }), hovertext: extendFlat({}, scatterAttrs.hovertext, { description: [ 'Sets hover text elements associated with each (a,b,c) point.', diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index 920df2a0317..23d3bbda305 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -75,6 +75,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { + coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index e75878fdf25..037079f95cb 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -10,7 +10,7 @@ var scatterAttrs = require('../scatter/attributes'); var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var scatterGlAttrs = require('../scattergl/attributes'); var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; var templatedArray = require('../../plot_api/plot_template').templatedArray; diff --git a/src/traces/streamtube/attributes.js b/src/traces/streamtube/attributes.js index 47b4f2f103b..9cf58c2b3fb 100644 --- a/src/traces/streamtube/attributes.js +++ b/src/traces/streamtube/attributes.js @@ -9,7 +9,7 @@ 'use strict'; var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var mesh3dAttrs = require('../mesh3d/attributes'); var baseAttrs = require('../../plots/attributes'); diff --git a/src/traces/sunburst/attributes.js b/src/traces/sunburst/attributes.js index 88bddf3ef9e..f36294917aa 100644 --- a/src/traces/sunburst/attributes.js +++ b/src/traces/sunburst/attributes.js @@ -9,7 +9,8 @@ 'use strict'; var plotAttrs = require('../../plots/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var domainAttrs = require('../../plots/domain').attributes; var pieAttrs = require('../pie/attributes'); @@ -129,6 +130,9 @@ module.exports = { editType: 'plot', flags: ['label', 'text', 'value'] }), + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: ['label', 'text', 'value', 'color'] + }), textfont: pieAttrs.textfont, hovertext: pieAttrs.hovertext, diff --git a/src/traces/sunburst/defaults.js b/src/traces/sunburst/defaults.js index 5a8a1d3bc2f..c34e27b019e 100644 --- a/src/traces/sunburst/defaults.js +++ b/src/traces/sunburst/defaults.js @@ -40,7 +40,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('leaf.opacity'); var text = coerce('text'); - coerce('textinfo', Array.isArray(text) ? 'text+label' : 'label'); + coerce('texttemplate'); + if(!traceOut.texttemplate) coerce('textinfo', Array.isArray(text) ? 'text+label' : 'label'); coerce('hovertext'); coerce('hovertemplate'); diff --git a/src/traces/sunburst/plot.js b/src/traces/sunburst/plot.js index 0a0eed4b4ba..c7bd78f7e92 100644 --- a/src/traces/sunburst/plot.js +++ b/src/traces/sunburst/plot.js @@ -676,30 +676,51 @@ function attachFxHandlers(sliceTop, gd, cd) { } function formatSliceLabel(pt, trace, fullLayout) { + var texttemplate = trace.texttemplate; var textinfo = trace.textinfo; - if(!textinfo || textinfo === 'none') { + if(!texttemplate && (!textinfo || textinfo === 'none')) { return ''; } var cdi = pt.data.data; var separators = fullLayout.separators; - var parts = textinfo.split('+'); - var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; - var thisText = []; + if(!texttemplate) { + var parts = textinfo.split('+'); + var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; + var thisText = []; - if(hasFlag('label') && cdi.label) thisText.push(cdi.label); + if(hasFlag('label') && cdi.label) { + thisText.push(cdi.label); + } - if(cdi.hasOwnProperty('v') && hasFlag('value')) { - thisText.push(formatPieValue(cdi.v, separators)); - } + if(cdi.hasOwnProperty('v') && hasFlag('value')) { + thisText.push(formatPieValue(cdi.v, separators)); + } - if(hasFlag('text')) { - var tx = Lib.castOption(trace, cdi.i, 'text'); - if(Lib.isValidTextValue(tx)) thisText.push(tx); + if(hasFlag('text')) { + var tx = Lib.castOption(trace, cdi.i, 'text'); + if(Lib.isValidTextValue(tx)) thisText.push(tx); + } + + return thisText.join('
'); } - return thisText.join('
'); + var txt = Lib.castOption(trace, cdi.i, 'texttemplate'); + if(!txt) return ''; + var obj = {}; + if(cdi.label) obj.label = cdi.label; + if(cdi.hasOwnProperty('v')) { + obj.value = cdi.v; + obj.valueLabel = formatPieValue(cdi.v, separators); + } + if(cdi.hasOwnProperty('color')) { + obj.color = cdi.color; + } + var ptTx = Lib.castOption(trace, cdi.i, 'text'); + if(Lib.isValidTextValue(ptTx)) obj.text = ptTx; + obj.customdata = Lib.castOption(trace, cdi.i, 'customdata'); + return Lib.texttemplateString(txt, obj, fullLayout._d3locale, obj, trace._meta || {}); } function getInscribedRadiusFraction(pt) { diff --git a/src/traces/surface/attributes.js b/src/traces/surface/attributes.js index 7e0fbfe0a6f..6d9ae82c2f1 100644 --- a/src/traces/surface/attributes.js +++ b/src/traces/surface/attributes.js @@ -10,7 +10,7 @@ var Color = require('../../components/color'); var colorScaleAttrs = require('../../components/colorscale/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; var baseAttrs = require('../../plots/attributes'); var extendFlat = require('../../lib/extend').extendFlat; diff --git a/src/traces/waterfall/attributes.js b/src/traces/waterfall/attributes.js index 08ddfb6bdd2..94f0745cd02 100644 --- a/src/traces/waterfall/attributes.js +++ b/src/traces/waterfall/attributes.js @@ -11,7 +11,8 @@ var barAttrs = require('../bar/attributes'); var lineAttrs = require('../scatter/attributes').line; var plotAttrs = require('../../plots/attributes'); -var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes'); +var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs; +var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs; var constants = require('./constants'); var extendFlat = require('../../lib/extend').extendFlat; var Color = require('../../components/color'); @@ -98,7 +99,10 @@ module.exports = { 'are computed separately (per trace).' ].join(' ') }, - + // TODO: incorporate `label` and `value` in the eventData + texttemplate: texttemplateAttrs({editType: 'plot'}, { + keys: constants.eventDataKeys.concat(['label']) + }), text: barAttrs.text, textposition: barAttrs.textposition, insidetextanchor: barAttrs.insidetextanchor, diff --git a/src/traces/waterfall/defaults.js b/src/traces/waterfall/defaults.js index cbff5eb6427..7e79d568591 100644 --- a/src/traces/waterfall/defaults.js +++ b/src/traces/waterfall/defaults.js @@ -62,7 +62,8 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { if(traceOut.textposition !== 'none') { - coerce('textinfo'); + coerce('texttemplate'); + if(!traceOut.texttemplate) coerce('textinfo'); } handleDirection(coerce, 'increasing', INCREASING_COLOR); diff --git a/test/image/baselines/gl2d_texttemplate.png b/test/image/baselines/gl2d_texttemplate.png new file mode 100644 index 00000000000..778520f3608 Binary files /dev/null and b/test/image/baselines/gl2d_texttemplate.png differ diff --git a/test/image/baselines/gl3d_scatter3d-texttemplate.png b/test/image/baselines/gl3d_scatter3d-texttemplate.png new file mode 100644 index 00000000000..46897a1e65d Binary files /dev/null and b/test/image/baselines/gl3d_scatter3d-texttemplate.png differ diff --git a/test/image/baselines/mapbox_texttemplate.png b/test/image/baselines/mapbox_texttemplate.png new file mode 100644 index 00000000000..96878d67d19 Binary files /dev/null and b/test/image/baselines/mapbox_texttemplate.png differ diff --git a/test/image/baselines/texttemplate.png b/test/image/baselines/texttemplate.png new file mode 100644 index 00000000000..a1302aca93c Binary files /dev/null and b/test/image/baselines/texttemplate.png differ diff --git a/test/image/baselines/texttemplate_scatter.png b/test/image/baselines/texttemplate_scatter.png new file mode 100644 index 00000000000..3d858675867 Binary files /dev/null and b/test/image/baselines/texttemplate_scatter.png differ diff --git a/test/image/baselines/waterfall_funnel_texttemplate_date.png b/test/image/baselines/waterfall_funnel_texttemplate_date.png new file mode 100644 index 00000000000..5a890f72b42 Binary files /dev/null and b/test/image/baselines/waterfall_funnel_texttemplate_date.png differ diff --git a/test/image/mocks/gl2d_texttemplate.json b/test/image/mocks/gl2d_texttemplate.json new file mode 100644 index 00000000000..e90aace0a8a --- /dev/null +++ b/test/image/mocks/gl2d_texttemplate.json @@ -0,0 +1,61 @@ +{ + "data": [{ + "type": "scattergl", + "mode": "markers+text", + "x": [0, 1, 2, 3], + "y": [0, 1, 4, 9], + + "text": ["a", "b", "c", "d"], + "texttemplate": "%{text}: (%{x}, %{y})", + "textposition": "top center", + "xaxis": "x2", + "yaxis": "y2" + }, { + "type": "scattergl", + "mode": "markers+text", + "x": [3, 2, 1, 0], + "y": [0, 1, 4, 9], + "texttemplate": ["1", "2", "3"], + "textposition": "top center", + "xaxis": "x2", + "yaxis": "y2" + },{ + "type": "scatterpolargl", + "mode": "markers+text", + "text": ["A", "B", "C", "D"], + "texttemplate": "%{text}: (%{r:0.2f},%{theta:0.2f})", + "textposition": "top center", + "r": [1, 0.5, 1, 1.5], + "theta": [0, 90, 180, 270], + "showgrid": false, + "subplot": "polar" + } + ], + "layout": { + "showlegend": false, + "width": 600, + "height": 300, + "margin": { + "t": 50, + "b": 50, + "l": 50, + "r": 50 + }, + "polar": { + "radialaxis": { + "showline": false, + "linewidth": 0, + "tickwidth": 2, + "gridcolor": "white", + "gridwidth": 0 + } + }, + "grid": { + "rows": 1, + "columns": 2, + "pattern": "independent", + "xgap": 5, + "ygap": 5 + } + } +} diff --git a/test/image/mocks/gl3d_scatter3d-texttemplate.json b/test/image/mocks/gl3d_scatter3d-texttemplate.json new file mode 100644 index 00000000000..e74e981d3d0 --- /dev/null +++ b/test/image/mocks/gl3d_scatter3d-texttemplate.json @@ -0,0 +1,25 @@ +{ + "data": [{ + "type": "scatter3d", + "x": [0, 1, 2], + "y": [0, 2, 1], + "z": [-5, -2, 4], + "text": ["A", "B", "C"], + "texttemplate": "%{text}: (%{x}, %{y}, %{z})", + "mode": "markers+text" + }, { + "type": "scatter3d", + "x": [2, 1, 0], + "y": [0, 4, 1], + "z": [-5, -2, 4], + "texttemplate": ["1", "2 with %{meta.text}"], + "meta": {"text": "meta"}, + "mode": "markers+text" + }], + "layout": { + "showlegend": true, + "width": 400, + "height": 400, + "margin": {"t": 20, "l": 0, "r": 0, "b": 0} + } +} diff --git a/test/image/mocks/mapbox_texttemplate.json b/test/image/mocks/mapbox_texttemplate.json new file mode 100644 index 00000000000..68aa29cdcf9 --- /dev/null +++ b/test/image/mocks/mapbox_texttemplate.json @@ -0,0 +1,38 @@ +{ + "data": [{ + "type": "scattermapbox", + "mode": "markers+text", + "lon": [ + -73.57, + -79.24, + -123.06 + ], + "lat": [ + 45.5, + 43.4, + 49.13 + ], + "text": [ + "Montreal", + "Toronto", + "Vancouver" + ], + "texttemplate": "%{text} (%{lon}, %{lat}): %{customdata:.2s}", + "textposition": "top center", + "customdata": [1780000, 2930000, 675218] + }], + + "layout": { + "width": 500, + "height": 500, + "title": {"text": "Mapbox with texttemplate"}, + "margin": {"t": 30, "b": 0, "l": 0, "r": 0}, + "mapbox": { + "center": { + "lon": -90, + "lat": 45 + }, + "zoom": 1.5 + } + } +} diff --git a/test/image/mocks/texttemplate.json b/test/image/mocks/texttemplate.json new file mode 100644 index 00000000000..b7ce21629d0 --- /dev/null +++ b/test/image/mocks/texttemplate.json @@ -0,0 +1,196 @@ +{ + "data": [ + { + "type": "pie", + "values": [ + 1, + 5, + 3, + 2 + ], + "labels": [ + "A", + "B", + "C", + "D" + ], + "text": [ + "textA", + "textB", + "textC", + "textD" + ], + "texttemplate": "%{label}: %{value} (%{percent})", + "textposition": "inside", + "hovertemplate": "%{text}", + "domain": { + "row": 0, + "column": 0 + } + }, + { + "type": "funnelarea", + "values": [ + 1, + 5, + 3, + 2 + ], + "labels": [ + "A", + "B", + "C", + "D" + ], + "text": [ + "textA", + "textB", + "textC", + "textD" + ], + "texttemplate": "%{label}-%{color}-%{value}-%{percent}-%{text}", + "textposition": "inside", + "hovertemplate": "%{text}", + "domain": { + "row": 0, + "column": 1 + } + }, + { + "type": "sunburst", + "labels": [ + "Eve", + "Cain", + "Seth", + "Enos", + "Esther" + ], + "values": [ + 11, + 12, + 13, + 14, + 15 + ], + "text": [ + "txt1", + "txt2", + "txt3", + "txt4", + "txt5" + ], + "parents": [ + "", + "Eve", + "Eve", + "Seth", + "Seth" + ], + "texttemplate": "%{label} %{value} %{text}", + "domain": { + "row": 0, + "column": 2 + } + }, + { + "type": "scatter", + "mode": "markers+lines+text", + "y": [ + 1, + 5, + 3, + 2 + ], + "texttemplate": "%{x}, %{y}", + "textposition": "top center", + "xaxis": "x5", + "yaxis": "y5" + }, + { + "type": "bar", + "y": [ + 1, + 5, + 3, + 2 + ], + "text": [ + "A", + "B", + "C", + "D" + ], + "texttemplate": "%{text}
%{value}", + "textposition": "inside", + "hovertemplate": "%{text}", + "xaxis": "x6", + "yaxis": "y6" + }, + { + "type": "waterfall", + "y": [ + 1, + 5, + 3, + 2 + ], + "text": [ + "A", + "B", + "C", + "D" + ], + "texttemplate": "%{text}-%{initial}-%{final}-%{delta}", + "textposition": "inside", + "hovertemplate": "%{text}", + "xaxis": "x7", + "yaxis": "y7" + }, + { + "type": "funnel", + "orientation": "h", + "y": [ + "A", + "B", + "C" + ], + "x": [ + 3, + 2, + 1 + ], + "textinfo": "value+percent initial+percent previous+percent total", + "texttemplate": "%{value}-%{percentInitial}-%{percentPrevious}-%{percentTotal:0.3f}", + "xaxis": "x8", + "yaxis": "y8" + } + ], + "layout": { + "width": 1000, + "height": 500, + "margin": {"t": 5, "b": 25, "l": 15, "r": 0}, + "grid": { + "rows": 2, + "columns": 4, + "pattern": "independent", + "xgap": 5, + "ygap": 5 + }, + "xaxis": { + "type": "category", + "range": [ + -0.5, + 3.5 + ], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [ + -2.1944444444444446, + 11.694444444444445 + ], + "autorange": true + } + } +} diff --git a/test/image/mocks/texttemplate_scatter.json b/test/image/mocks/texttemplate_scatter.json new file mode 100644 index 00000000000..3fff2e746f4 --- /dev/null +++ b/test/image/mocks/texttemplate_scatter.json @@ -0,0 +1,140 @@ +{ + "data": [{ + "type": "scattergeo", + "mode": "markers+text", + "lon": [ + -73.57, + -79.24, + -123.06 + ], + "lat": [ + 45.5, + 43.4, + 49.13 + ], + "text": [ + "Montreal", + "Toronto", + "Vancouver" + ], + "texttemplate": "%{text}: (%{lon:.0f}, %{lat:0.f}): %{customdata:.2s}", + "textposition": "top center", + "customdata": [1780000, 2930000, 675218], + "geo": "geo" + }, { + "type": "carpet", + "carpet": "carpet1", + "a": [0.1, 0.2, 0.3], + "b": [1, 2, 3], + "y": [ + [1, 2.2, 3], + [1.5, 2.7, 3.5], + [1.7, 2.9, 3.7] + ], + "cheaterslope": 1, + + "xaxis": "x2", + "yaxis": "y2" + }, + { + "type": "scattercarpet", + "carpet": "carpet1", + "name": "b = 1.5", + "mode": "markers+text", + "a": [0.1, 0.15, 0.25, 0.3], + "b": [1.5, 1.5, 1.5, 1.5], + + "text": ["a", "b", "c", "d"], + "texttemplate": "%{text}: (%{a}, %{b})", + "textposition": "top center", + "xaxis": "x2", + "yaxis": "y2" + }, { + "type": "scatterpolar", + "mode": "markers+text", + "text": ["A", "B", "C", "D"], + "texttemplate": "%{text}: (%{r:0.2f},%{theta:0.2f})", + "textposition": "top center", + "r": [1, 0.5, 1, 1.5], + "theta": [0, 90, 180, 270], + "showgrid": false + }, { + "type": "scatterternary", + "a": [ + 3, + 2, + 5 + ], + "b": [ + 2, + 5, + 2 + ], + "c": [ + 5, + 2, + 2 + ], + "mode": "markers+text", + "text": ["A", "B", "C"], + "texttemplate": "%{text}
(%{a:.2f}, %{b:.2f}, %{c:.2f})", + "textposition": "bottom center" + } + ], + "layout": { + "showlegend": false, + "width": 800, + "height": 800, + "margin": { + "t": 50, + "b": 50, + "l": 50, + "r": 50 + }, + "geo": { + "scope": "north america", + "domain": { + "row": 1, + "column": 0 + }, + "lonaxis": { + "range": [ + -130, + -55 + ] + }, + "lataxis": { + "range": [ + 40, + 70 + ] + }, + "center": { + "lat": 57 + } + }, + "polar": { + "radialaxis": { + "showline": false, + "linewidth": 0, + "tickwidth": 2, + "gridcolor": "white", + "gridwidth": 0 + } + }, + "ternary": { + "sum": 10, + "domain": { + "row": 1, + "column": 1 + } + }, + "grid": { + "rows": 2, + "columns": 2, + "pattern": "independent", + "xgap": 5, + "ygap": 5 + } + } +} diff --git a/test/image/mocks/waterfall_funnel_texttemplate_date.json b/test/image/mocks/waterfall_funnel_texttemplate_date.json new file mode 100644 index 00000000000..b8d4be3e574 --- /dev/null +++ b/test/image/mocks/waterfall_funnel_texttemplate_date.json @@ -0,0 +1,73 @@ +{ + "data": [{ + "type": "waterfall", + "x": [ + "2010-01-01", + "2010-07-01", + "2011-01-01", + "2012-01-01" + ], + "y": [1, -2, 3, 0], + "textinfo": "label", + "textposition": "inside" + }, { + "type": "waterfall", + "x": [ + "2010-01-01", + "2010-07-01", + "2011-01-01", + "2012-01-01" + ], + "y": [1, -2, 3, 0], + "texttemplate": "%{label}", + "textposition": "inside", + "xaxis": "x1", + "yaxis": "y1" + }, { + "type": "funnel", + "orientation": "v", + "x": [ + "2010-01-01", + "2010-07-01", + "2011-01-01", + "2012-01-01" + ], + "y": [10, 7, 3, 0], + "textinfo": "label", + "textposition": "inside", + "xaxis": "x2", + "yaxis": "y2" + }, { + "type": "funnel", + "orientation": "v", + "x": [ + "2010-01-01", + "2010-07-01", + "2011-01-01", + "2012-01-01" + ], + "y": [10, 7, 3, 0], + "texttemplate": "%{label}", + "textposition": "inside", + "xaxis": "x2", + "yaxis": "y2" + }], + "layout": { + "title": { + "text": "textinfo: 'label' versus texttemplate: '%{label}'" + }, + "xaxis": { + "type": "date" + }, + "margin": {"t": 40, "b": 20, "l": 20, "r": 20}, + "width": 800, + "height": 400, + "grid": { + "rows": 1, + "columns": 2, + "pattern": "independent", + "xgap": 5, + "ygap": 5 + } + } +} diff --git a/test/jasmine/assets/check_texttemplate.js b/test/jasmine/assets/check_texttemplate.js new file mode 100644 index 00000000000..4960865dbd7 --- /dev/null +++ b/test/jasmine/assets/check_texttemplate.js @@ -0,0 +1,123 @@ +var Plotly = require('@lib/index'); +var Registry = require('@src/registry'); + +var Lib = require('@src/lib'); +var failTest = require('../assets/fail_test'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var supplyAllDefaults = require('../assets/supply_defaults'); + +'use strict'; + +module.exports = function checkTextTemplate(mock, selector, tests) { + var isGL = Registry.traceIs(mock[0].type, 'gl'); + var isPolar = Registry.traceIs(mock[0].type, 'polar'); + var isScatterLike = Registry.traceIs(mock[0].type, 'scatter-like'); + var isBarLike = Registry.traceIs(mock[0].type, 'bar-like'); + + it('should not coerce textinfo when texttemplate is defined', function() { + var gd = {}; + gd.data = Lib.extendDeep([], mock); + gd.data[0].textinfo = 'text'; + gd.data[0].texttemplate = 'texttemplate'; + supplyAllDefaults(gd); + expect(gd._fullData[0].textinfo).toBe(undefined); + }); + + if(isScatterLike) { + it('should not coerce texttemplate when mode has no `text` flag', function() { + var gd = {}; + gd.data = Lib.extendDeep([], mock); + gd.data[0].mode = 'markers'; + gd.data[0].texttemplate = 'texttemplate'; + supplyAllDefaults(gd); + expect(gd._fullData[0].texttemplate).toBe(undefined); + }); + } + + if(isBarLike) { + it('should not coerce texttemplate when textposition is `none`', function() { + var gd = {}; + gd.data = Lib.extendDeep([], mock); + gd.data[0].textposition = 'none'; + gd.data[0].texttemplate = 'texttemplate'; + supplyAllDefaults(gd); + expect(gd._fullData[0].texttemplate).toBe(undefined); + }); + } + + var N = tests[0][1].length; + var i; + + // Generate customdata + var customdata = []; + for(i = 0; i < N; i++) { + customdata.push(Lib.randstr({})); + } + mock[0].customdata = customdata; + tests.push(['%{customdata}', customdata]); + + // Generate meta + mock[0].meta = {'colname': 'A'}; + var metaSolution = []; + for(i = 0; i < N; i++) { + metaSolution.push(mock[0].meta.colname); + } + tests.push(['%{meta.colname}', metaSolution]); + + if(isGL) { + tests.forEach(function(test) { + it('@gl should support texttemplate', function(done) { + var gd = createGraphDiv(); + var mockCopy = Lib.extendDeep([], mock); + mockCopy[0].texttemplate = test[0]; + Plotly.newPlot(gd, mockCopy) + .then(function() { + var glText; + if(isPolar) { + glText = gd._fullLayout.polar._subplot._scene.glText; + } else { + glText = gd._fullLayout._plots.xy._scene.glText; + } + expect(glText.length).toEqual(1); + expect(glText[0].textOffsets.length).toEqual(test[1].length); + for(var i = 0; i < glText[0].textOffsets.length - 1; i++) { + var from = glText[0].textOffsets[i]; + var to = glText[0].textOffsets[i + 1]; + + var text = glText[0].text.slice(from, to); + expect(text).toEqual(test[1][i]); + } + }) + .catch(failTest) + .finally(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }) + .then(done); + }); + }); + } else { + tests.forEach(function(test) { + it('should support texttemplate', function(done) { + var gd = createGraphDiv(); + var mockCopy = Lib.extendDeep([], mock); + mockCopy[0].texttemplate = test[0]; + Plotly.newPlot(gd, mockCopy) + .then(function() { + var pts = Plotly.d3.selectAll(selector); + expect(pts.size()).toBe(test[1].length); + pts.each(function() { + expect(test[1]).toContain(Plotly.d3.select(this).text()); + }); + }) + .catch(failTest) + .finally(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }) + .then(done); + }); + }); + } +}; diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index c4f191194aa..32d45cffb7f 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -25,6 +25,7 @@ var customAssertions = require('../assets/custom_assertions'); var assertClip = customAssertions.assertClip; var assertNodeDisplay = customAssertions.assertNodeDisplay; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var Fx = require('@src/components/fx'); var d3 = require('d3'); @@ -1110,6 +1111,28 @@ describe('A bar plot', function() { }; } + checkTextTemplate([{ + type: 'bar', + y: [1, 5, 3, 2], + text: ['A', 'B', 'C', 'D'], + textposition: 'inside', + hovertemplate: '%{text}' + }], 'text.bartext', [ + ['%{text} - %{value}', ['A - 1', 'B - 5', 'C - 3', 'D - 2']], + [['%{y}', '%{value}', '%{text}'], ['1', '5', 'C']] + ]); + + checkTextTemplate([{ + type: 'bar', + textposition: 'outside', + x: ['2019-01-01', '2019-02-01'], + y: [1, 2], + hovertemplate: '%{x}', + texttemplate: '%{x}' + }], 'text.bartext', [ + ['%{x}', ['2019-01-01', '2019-02-01']] + ]); + it('should show bar texts (inside case)', function(done) { var data = [{ y: [10, 20, 30], diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index 3fada7b2e71..a71ffc3471a 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -13,6 +13,7 @@ var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var assertHoverLabelContent = require('../assets/custom_assertions').assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var supplyAllDefaults = require('../assets/supply_defaults'); @@ -714,6 +715,24 @@ describe('scattercarpet hover labels', function() { }); }); +describe('scattercarpet texttemplates', function() { + checkTextTemplate([{ + 'type': 'scattercarpet', + 'carpet': 'carpet1', + 'mode': 'markers+text', + 'a': [0.1, 0.15, 0.25], + 'b': [1.5, 1.5, 1.5], + 'text': ['A', 'B', 'C'] + }, { + 'type': 'carpet', + 'carpet': 'carpet1', + 'a': [0.1, 0.2, 0.3], + 'b': [1, 2, 3], + 'y': [[1, 2.2, 3], [1.5, 2.7, 3.5], [1.7, 2.9, 3.7]] + }], 'g.textpoint', [ + ['%{text}: %{a:0.1f}, %{b:0.1f}', ['A: 0.1, 1.5', 'B: 0.1, 1.5', 'C: 0.3, 1.5']] + ]); +}); describe('contourcarpet plotting & editing', function() { var gd; diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js index 6804069adaf..9a26e14fcb6 100644 --- a/test/jasmine/tests/funnel_test.js +++ b/test/jasmine/tests/funnel_test.js @@ -16,6 +16,7 @@ var rgb = color.rgb; var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var Fx = require('@src/components/fx'); var d3 = require('d3'); @@ -1234,6 +1235,17 @@ describe('A funnel plot', function() { .catch(failTest) .then(done); }); + + checkTextTemplate([{ + type: 'funnel', + orientation: 'v', + x: ['A', 'B', 'C'], + y: [3, 2, 1], + textinfo: 'value+percent initial+percent previous+percent total', + }], 'text.bartext', [ + ['txt: %{value}', ['txt: 3', 'txt: 2', 'txt: 1']], + ['%{value}-%{percentInitial}-%{percentPrevious}-%{percentTotal:0.3f}', ['3-100%-100%-0.500', '2-67%-67%-0.333', '1-33%-50%-0.167']] + ]); }); describe('funnel hover', function() { diff --git a/test/jasmine/tests/funnelarea_test.js b/test/jasmine/tests/funnelarea_test.js index 81b6e50f492..67e2fa6a015 100644 --- a/test/jasmine/tests/funnelarea_test.js +++ b/test/jasmine/tests/funnelarea_test.js @@ -14,6 +14,7 @@ var rgb = require('../../../src/components/color').rgb; var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var SLICES_SELECTOR = '.slice path'; var SLICES_TEXT_SELECTOR = '.funnelarealayer text.slicetext'; @@ -637,6 +638,18 @@ describe('Funnelarea traces', function() { .catch(failTest) .then(done); }); + + checkTextTemplate([{ + type: 'funnelarea', + values: [1, 5, 3, 2], + labels: ['A', 'B', 'C', 'D'], + text: ['textA', 'textB', 'textC', 'textD'], + textposition: 'inside', + hovertemplate: '%{text}' + }], 'g.slicetext', [ + ['%{label}-%{color}-%{value}-%{percent}-%{text}', ['A-#1f77b4-1-9.09%-textA', 'B-#ff7f0e-5-45.5%-textB', 'C-#2ca02c-3-27.3%-textC', 'D-#d62728-2-18.2%-textD']], + [['%{label} - %{value}', '%{text}', '%{value}', '%{percent}'], ['A - 1', 'textB', '3', '18.2%']], + ]); }); describe('funnelarea hovering', function() { diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 38dc37927a0..cbcdfb2ec18 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -2225,12 +2225,17 @@ describe('Test lib.js:', function() { expect(Lib.hovertemplateString('y: %{y}', {}, locale, {y: 0}, {y: 1})).toEqual('y: 0'); }); - it('formats value using d3 mini-language', function() { + it('formats numbers using d3-format mini-language when `:`', function() { expect(Lib.hovertemplateString('a: %{a:.0%}', {}, locale, {a: 0.123})).toEqual('a: 12%'); expect(Lib.hovertemplateString('a: %{a:0.2%}', {}, locale, {a: 0.123})).toEqual('a: 12.30%'); expect(Lib.hovertemplateString('b: %{b:2.2f}', {}, locale, {b: 43})).toEqual('b: 43.00'); }); + it('formats date using d3-time-format mini-language `|`', function() { + expect(Lib.hovertemplateString('a: %{a|%A}', {}, locale, {a: '2019-05-22'})).toEqual('a: Wednesday'); + expect(Lib.hovertemplateString('%{x|%b %-d, %Y}', {}, locale, {x: '2019-01-01'})).toEqual('Jan 1, 2019'); + }); + it('looks for default label if no format is provided', function() { expect(Lib.hovertemplateString('y: %{y}', {yLabel: '0.1'}, locale, {y: 0.123})).toEqual('y: 0.1'); }); @@ -2245,6 +2250,34 @@ describe('Test lib.js:', function() { } expect(Lib.warn.calls.count()).toBe(10); }); + + it('does not error out when arguments are undefined', function() { + expect(function() { + Lib.hovertemplateString('y: %{y}', undefined, locale, undefined); + }).not.toThrow(); + }); + }); + + describe('texttemplateString', function() { + var locale = false; + it('evaluates attributes', function() { + expect(Lib.texttemplateString('foo %{bar}', {}, locale, {bar: 'baz'})).toEqual('foo baz'); + }); + + it('looks for default label if no format is provided', function() { + expect(Lib.texttemplateString('y: %{y}', {yLabel: '0.1'}, locale, {y: 0.123})).toEqual('y: 0.1'); + }); + + it('warns user up to 10 times if a variable cannot be found', function() { + spyOn(Lib, 'warn').and.callThrough(); + Lib.texttemplateString('%{idontexist}', {}); + expect(Lib.warn.calls.count()).toBe(1); + + for(var i = 0; i < 15; i++) { + Lib.texttemplateString('%{idontexist}', {}); + } + expect(Lib.warn.calls.count()).toBe(11); + }); }); describe('relativeAttr()', function() { diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 4b6f84c8ebc..d4afa9ed524 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -14,6 +14,7 @@ var rgb = require('../../../src/components/color').rgb; var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var SLICES_SELECTOR = '.slice path'; var SLICES_TEXT_SELECTOR = '.pielayer text.slicetext'; @@ -862,6 +863,19 @@ describe('Pie traces', function() { .catch(failTest) .then(done); }); + + checkTextTemplate([{ + type: 'pie', + values: [1, 5, 3, 2], + labels: ['A', 'B', 'C', 'D'], + text: ['textA', 'textB', 'textC', 'textD'], + textposition: 'inside', + hovertemplate: '%{text}' + }], 'g.slicetext', [ + ['%{label} - %{value}', ['A - 1', 'B - 5', 'C - 3', 'D - 2']], + [['%{label} - %{value}', '%{text}', '%{value}', '%{percent}'], ['A - 1', 'textB', '3', '18.2%']], + ['%{text}-%{color}', ['textA-#d62728', 'textB-#1f77b4', 'textC-#ff7f0e', 'textD-#2ca02c']] + ]); }); describe('pie hovering', function() { diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index d7639333903..8c599352fd8 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -17,6 +17,7 @@ var assertClip = customAssertions.assertClip; var assertNodeDisplay = customAssertions.assertNodeDisplay; var assertMultiNodeOrder = customAssertions.assertMultiNodeOrder; var checkEventData = require('../assets/check_event_data'); +var checkTextTemplate = require('../assets/check_texttemplate'); var constants = require('@src/traces/scatter/constants'); var supplyAllDefaults = require('../assets/supply_defaults'); @@ -1238,6 +1239,16 @@ describe('end-to-end scatter tests', function() { .catch(failTest) .then(done); }); + + checkTextTemplate([{ + type: 'scatter', + mode: 'markers+lines+text', + y: [1, 5, 3, 2], + textposition: 'top' + }], '.textpoint', [ + ['%{y}', ['1', '5', '3', '2']], + [['%{y}', '%{x}-%{y}'], ['1', '1-5', '', '']] + ]); }); describe('stacked area', function() { diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index 718fa6401c4..12976f642cd 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -12,6 +12,7 @@ var mouseEvent = require('../assets/mouse_event'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var failTest = require('../assets/fail_test'); var supplyAllDefaults = require('../assets/supply_defaults'); @@ -436,3 +437,24 @@ describe('scattergeo drawing', function() { .then(done); }); }); + +describe('Test scattergeo texttemplate:', function() { + checkTextTemplate([{ + 'type': 'scattergeo', + 'mode': 'markers+text', + 'lon': [-73.57, -79.24, -123.06], + 'lat': [45.5, 43.4, 49.13], + 'text': ['Montreal', 'Toronto', 'Vancouver'] + }], '.scattergeo text', [ + ['%{text}: %{lon}, %{lat}', ['Montreal: -73.57, 45.5', 'Toronto: -79.24, 43.4', 'Vancouver: -123.06, 49.13']] + ]); + + checkTextTemplate([{ + 'type': 'scattergeo', + 'mode': 'markers+text', + 'locations': ['Canada'], + 'locationmode': 'country names' + }], '.scattergeo text', [ + ['%{location}', ['Canada']] + ]); +}); diff --git a/test/jasmine/tests/scattergl_test.js b/test/jasmine/tests/scattergl_test.js index fed8cb7eb0f..e08a967e028 100644 --- a/test/jasmine/tests/scattergl_test.js +++ b/test/jasmine/tests/scattergl_test.js @@ -8,6 +8,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var delay = require('../assets/delay'); var readPixel = require('../assets/read_pixel'); +var checkTextTemplate = require('../assets/check_texttemplate'); describe('end-to-end scattergl tests', function() { var gd; @@ -37,6 +38,17 @@ describe('end-to-end scattergl tests', function() { }).catch(failTest).then(done); }); + checkTextTemplate([{ + type: 'scattergl', + mode: 'text+lines', + x: [1, 2, 3, 4], + y: [2, 3, 4, 5], + text: ['A', 'B', 'C', 'D'], + }], '@gl', [ + ['%{text}: %{x}, %{y}', ['A: 1, 2', 'B: 2, 3', 'C: 3, 4', 'D: 4, 5']], + [['%{x}', '%{x}', '%{text}', '%{y}'], ['1', '2', 'C', '5']] + ]); + it('@gl should update a plot with text labels', function(done) { Plotly.react(gd, [{ type: 'scattergl', @@ -105,23 +117,25 @@ describe('end-to-end scattergl tests', function() { }).catch(failTest).then(done); }); - it('@gl should handle a plot with less text labels than data points', function(done) { - expect(function() { - var mock = { - 'type': 'scattergl', - 'mode': 'markers+text', - 'x': [3, 2, 1, 0], - 'y': [0, 1, 4, 9], - 'textposition': 'top center' - }; - mock.text = ['1', '2', '3']; - Plotly.plot(gd, [mock]) - .then(function() { - expect(mock.text.length).toBe(3); - }) - .catch(failTest); - }).not.toThrow(); - done(); + ['text', 'texttemplate'].forEach(function(attr) { + it('@gl should handle a plot with less ' + attr + ' labels than data points', function(done) { + expect(function() { + var mock = { + 'type': 'scattergl', + 'mode': 'markers+text', + 'x': [3, 2, 1, 0], + 'y': [0, 1, 4, 9], + 'textposition': 'top center' + }; + mock[attr] = ['1', '2', '3']; + Plotly.plot(gd, [mock]) + .then(function() { + expect(mock[attr].length).toBe(3); + }) + .catch(failTest); + }).not.toThrow(); + done(); + }); }); it('@gl should be able to toggle visibility', function(done) { diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 701306dc6bb..51839f51af7 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -139,7 +139,7 @@ describe('scattermapbox convert', function() { Plots.doCalcdata(gd, fullTrace); var calcTrace = gd.calcdata[0]; - return convert(calcTrace); + return convert({_fullLayout: {_d3locale: false}}, calcTrace); } function assertVisibility(opts, expectations) { @@ -478,6 +478,44 @@ describe('scattermapbox convert', function() { expect(actualText).toEqual(['A', 'B', 'C', 'F', undefined]); }); + it('should generate correct output for texttemplate without text', function() { + var opts = _convert(Lib.extendFlat({}, base, { + mode: 'lines+text', + connectgaps: true, + textposition: 'outside', + texttemplate: ['A', 'B', 'C', 'D', 'E', 'F'] + })); + + var actualText = opts.symbol.geojson.features.map(function(f) { + return f.properties.text; + }); + + expect(actualText).toEqual(['A', 'B', 'C', 'F', '']); + }); + + it('should generate correct output for texttemplate', function() { + var mock = { + 'type': 'scattermapbox', + 'mode': 'markers+text', + 'lon': [-73.57, -79.24, -123.06], + 'lat': [45.5, 43.4, 49.13], + 'text': ['Montreal', 'Toronto', 'Vancouver'], + 'texttemplate': '%{text} (%{lon}, %{lat}): %{customdata:.2s}', + 'textposition': 'top center', + 'customdata': [1780000, 2930000, 675218] + }; + var opts = _convert(mock); + var actualText = opts.symbol.geojson.features.map(function(f) { + return f.properties.text; + }); + + expect(actualText).toEqual([ + 'Montreal (-73.57, 45.5): 1.8M', + 'Toronto (-79.24, 43.4): 2.9M', + 'Vancouver (-123.06, 49.13): 680k' + ]); + }); + it('should generate correct output for lines traces with trailing gaps', function() { var opts = _convert(Lib.extendFlat({}, base, { mode: 'lines', diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js index 69d26588719..494a893db5f 100644 --- a/test/jasmine/tests/scatterpolar_test.js +++ b/test/jasmine/tests/scatterpolar_test.js @@ -9,6 +9,7 @@ var mouseEvent = require('../assets/mouse_event'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); describe('Test scatterpolar trace defaults:', function() { var traceOut; @@ -198,3 +199,17 @@ describe('Test scatterpolar hover:', function() { }); }); }); + +describe('Test scatterpolar texttemplate:', function() { + checkTextTemplate([{ + 'type': 'scatterpolar', + 'mode': 'markers+text', + 'text': ['A', 'B', 'C'], + 'textposition': 'top center', + 'r': [1, 0.5, 1], + 'theta': [0, 90, 180], + }], 'g.textpoint', [ + ['%{text}: (%{r:0.2f}, %{theta:0.1f})', ['A: (1.00, 0.0)', 'B: (0.50, 90.0)', 'C: (1.00, 180.0)']], + [['', 'b%{theta:0.2f}', '%{theta:0.2f}'], ['', 'b90.00', '180.00']] + ]); +}); diff --git a/test/jasmine/tests/scatterpolargl_test.js b/test/jasmine/tests/scatterpolargl_test.js index 5e6d2f2866e..6afd16c50dd 100644 --- a/test/jasmine/tests/scatterpolargl_test.js +++ b/test/jasmine/tests/scatterpolargl_test.js @@ -11,6 +11,7 @@ var readPixel = require('../assets/read_pixel'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); describe('Test scatterpolargl hover:', function() { var gd; @@ -377,3 +378,17 @@ describe('Test scatterpolargl autorange:', function() { }); }); }); + +describe('Test scatterpolargl texttemplate:', function() { + checkTextTemplate([{ + 'type': 'scatterpolargl', + 'mode': 'markers+text', + 'text': ['A', 'B', 'C'], + 'textposition': 'top center', + 'r': [1, 0.5, 1], + 'theta': [0, 90, 180], + }], 'g.textpoint', [ + ['%{text}: (%{r:0.2f}, %{theta:0.1f})', ['A: (1.00, 0.0)', 'B: (0.50, 90.0)', 'C: (1.00, 180.0)']], + [['', 'b%{theta:0.2f}', '%{theta:0.2f}'], ['', 'b90.00', '180.00']] + ]); +}); diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index 0cabd822e5d..83299d8027b 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -11,6 +11,7 @@ var supplyAllDefaults = require('../assets/supply_defaults'); var mouseEvent = require('../assets/mouse_event'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var assertClip = customAssertions.assertClip; var assertNodeDisplay = customAssertions.assertNodeDisplay; @@ -554,3 +555,16 @@ describe('Test scatterternary *cliponaxis*', function() { .then(done); }); }); + +describe('Test scatterternary texttemplate:', function() { + checkTextTemplate([{ + 'type': 'scatterternary', + 'a': [3, 2, 5], + 'b': [2, 5, 2], + 'c': [5, 2, 2 ], + 'mode': 'markers+text', + 'text': ['A', 'B', 'C'] + }], 'g.textpoint', [ + ['%{text} (%{a:.1f}, %{b:.1f}, %{c:.1f})', ['A (3.0, 2.0, 5.0)', 'B (2.0, 5.0, 2.0)', 'C (5.0, 2.0, 2.0)']] + ]); +}); diff --git a/test/jasmine/tests/sunburst_test.js b/test/jasmine/tests/sunburst_test.js index a6ce493d67f..cce6b6034ff 100644 --- a/test/jasmine/tests/sunburst_test.js +++ b/test/jasmine/tests/sunburst_test.js @@ -14,6 +14,7 @@ var failTest = require('../assets/fail_test'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); function _mouseEvent(type, gd, v) { return function() { @@ -1237,3 +1238,17 @@ describe('Test sunburst interactions edge cases', function() { .then(done); }); }); + +describe('Test sunburst texttemplate:', function() { + checkTextTemplate([{ + type: 'sunburst', + labels: ['Eve', 'Cain', 'Seth', 'Enos', 'Esther'], + values: [11, 12, 13, 14, 15], + text: ['1', '2', '3', '4', '5'], + parents: ['', 'Eve', 'Eve', 'Seth', 'Seth' ] + }], 'g.slicetext', [ + ['txt: %{label}', ['txt: Eve', 'txt: Cain', 'txt: Seth', 'txt: Enos', 'txt: Esther']], + [['txt: %{label}', '%{text}', 'value: %{value}'], ['txt: Eve', '2', 'value: 13', '', '']], + ['%{color}', ['rgba(0,0,0,0)', '#1f77b4', '#ff7f0e', '#1f77b4', '#1f77b4']] + ]); +}); diff --git a/test/jasmine/tests/waterfall_test.js b/test/jasmine/tests/waterfall_test.js index 20845b600f2..f9a35962863 100644 --- a/test/jasmine/tests/waterfall_test.js +++ b/test/jasmine/tests/waterfall_test.js @@ -16,6 +16,7 @@ var rgb = color.rgb; var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; +var checkTextTemplate = require('../assets/check_texttemplate'); var Fx = require('@src/components/fx'); var d3 = require('d3'); @@ -1208,6 +1209,18 @@ describe('A waterfall plot', function() { .catch(failTest) .then(done); }); + + checkTextTemplate([{ + type: 'waterfall', + y: [1, 5, 3, 2], + text: ['A', 'B', 'C', 'D'], + textposition: 'inside', + hovertemplate: '%{text}' + }], 'text.bartext', [ + ['%{value}', ['1', '6', '9', '11']], + ['%{initial} %{delta} %{final}', ['0 1 1', '1 5 6', '6 3 9', '9 2 11']], + [['%{y}', '%{value}', '%{text}', '%{label}'], ['1', '6', 'C', '3']] + ]); }); describe('waterfall visibility toggling:', function() {