Skip to content

Commit

Permalink
Merge pull request #4071 from plotly/texttemplate
Browse files Browse the repository at this point in the history
texttemplate with date formatting
  • Loading branch information
antoinerg authored Aug 28, 2019
2 parents 19c0683 + c7dd845 commit 3561cab
Show file tree
Hide file tree
Showing 90 changed files with 1,391 additions and 145 deletions.
14 changes: 12 additions & 2 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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 ?
Expand Down
2 changes: 1 addition & 1 deletion src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
61 changes: 43 additions & 18 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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*$/;

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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 = {};
Expand All @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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;
};
6 changes: 5 additions & 1 deletion src/traces/bar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/traces/bar/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
74 changes: 69 additions & 5 deletions src/traces/bar/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/traces/barpolar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading

0 comments on commit 3561cab

Please sign in to comment.