Skip to content

Commit 3561cab

Browse files
authored
Merge pull request #4071 from plotly/texttemplate
texttemplate with date formatting
2 parents 19c0683 + c7dd845 commit 3561cab

File tree

90 files changed

+1391
-145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+1391
-145
lines changed

Diff for: src/components/drawing/index.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var subTypes = require('../../traces/scatter/subtypes');
2828
var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func');
2929

3030
var drawing = module.exports = {};
31+
var appendArrayPointValue = require('../fx/helpers').appendArrayPointValue;
3132

3233
// -----------------------------------------------------
3334
// styling functions for plot elements
@@ -679,7 +680,7 @@ function extracTextFontSize(d, trace) {
679680
}
680681

681682
// draw text at points
682-
drawing.textPointStyle = function(s, trace, gd) {
683+
drawing.textPointStyle = function(s, trace, gd, inLegend) {
683684
if(!s.size()) return;
684685

685686
var selectedTextColorFn;
@@ -689,15 +690,24 @@ drawing.textPointStyle = function(s, trace, gd) {
689690
selectedTextColorFn = fns.selectedTextColorFn;
690691
}
691692

693+
var template = trace.texttemplate;
694+
// If styling text in legends, do not use texttemplate
695+
if(inLegend) template = false;
692696
s.each(function(d) {
693697
var p = d3.select(this);
694-
var text = Lib.extractOption(d, trace, 'tx', 'text');
698+
var text = Lib.extractOption(d, trace, template ? 'txt' : 'tx', template ? 'texttemplate' : 'text');
695699

696700
if(!text && text !== 0) {
697701
p.remove();
698702
return;
699703
}
700704

705+
if(template) {
706+
var pt = {};
707+
appendArrayPointValue(pt, trace, d.i);
708+
text = Lib.texttemplateString(text, {}, gd._fullLayout._d3locale, pt, d, trace._meta || {});
709+
}
710+
701711
var pos = d.tp || trace.textposition;
702712
var fontSize = extracTextFontSize(d, trace);
703713
var fontColor = selectedTextColorFn ?

Diff for: src/components/legend/style.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ module.exports = function style(s, gd) {
271271
.append('g').classed('pointtext', true)
272272
.append('text').attr('transform', 'translate(20,0)');
273273
txt.exit().remove();
274-
txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd);
274+
txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd, true);
275275
}
276276

277277
function styleWaterfalls(d) {

Diff for: src/lib/index.js

+43-18
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ lib.numSeparate = function(value, separators, separatethousands) {
964964
return x1 + x2;
965965
};
966966

967-
lib.TEMPLATE_STRING_REGEX = /%{([^\s%{}:]*)(:[^}]*)?}/g;
967+
lib.TEMPLATE_STRING_REGEX = /%{([^\s%{}:]*)([:|\|][^}]*)?}/g;
968968
var SIMPLE_PROPERTY_REGEX = /^\w*$/;
969969

970970
/**
@@ -993,9 +993,25 @@ lib.templateString = function(string, obj) {
993993
});
994994
};
995995

996-
var TEMPLATE_STRING_FORMAT_SEPARATOR = /^:/;
997-
var numberOfHoverTemplateWarnings = 0;
998-
var maximumNumberOfHoverTemplateWarnings = 10;
996+
var hovertemplateWarnings = {
997+
max: 10,
998+
count: 0,
999+
name: 'hovertemplate'
1000+
};
1001+
lib.hovertemplateString = function() {
1002+
return templateFormatString.apply(hovertemplateWarnings, arguments);
1003+
};
1004+
1005+
var texttemplateWarnings = {
1006+
max: 10,
1007+
count: 0,
1008+
name: 'texttemplate'
1009+
};
1010+
lib.texttemplateString = function() {
1011+
return templateFormatString.apply(texttemplateWarnings, arguments);
1012+
};
1013+
1014+
var TEMPLATE_STRING_FORMAT_SEPARATOR = /^[:|\|]/;
9991015
/**
10001016
* Substitute values from an object into a string and optionally formats them using d3-format,
10011017
* or fallback to associated labels.
@@ -1005,15 +1021,17 @@ var maximumNumberOfHoverTemplateWarnings = 10;
10051021
* Lib.hovertemplateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
10061022
* Lib.hovertemplateString('price: %{y:$.2f}', {y: 1}) --> 'price: $1.00'
10071023
*
1008-
* @param {obj} d3 locale
10091024
* @param {string} input string containing %{...:...} template strings
10101025
* @param {obj} data object containing fallback text when no formatting is specified, ex.: {yLabel: 'formattedYValue'}
1026+
* @param {obj} d3 locale
10111027
* @param {obj} data objects containing substitution values
10121028
*
10131029
* @return {string} templated string
10141030
*/
1015-
lib.hovertemplateString = function(string, labels, d3locale) {
1031+
function templateFormatString(string, labels, d3locale) {
1032+
var opts = this;
10161033
var args = arguments;
1034+
if(!labels) labels = {};
10171035
// Not all that useful, but cache nestedProperty instantiation
10181036
// just in case it speeds things up *slightly*:
10191037
var getterCache = {};
@@ -1022,6 +1040,7 @@ lib.hovertemplateString = function(string, labels, d3locale) {
10221040
var obj, value, i;
10231041
for(i = 3; i < args.length; i++) {
10241042
obj = args[i];
1043+
if(!obj) continue;
10251044
if(obj.hasOwnProperty(key)) {
10261045
value = obj[key];
10271046
break;
@@ -1034,32 +1053,38 @@ lib.hovertemplateString = function(string, labels, d3locale) {
10341053
if(value !== undefined) break;
10351054
}
10361055

1037-
if(value === undefined) {
1038-
if(numberOfHoverTemplateWarnings < maximumNumberOfHoverTemplateWarnings) {
1039-
lib.warn('Variable \'' + key + '\' in hovertemplate could not be found!');
1056+
if(value === undefined && opts) {
1057+
if(opts.count < opts.max) {
1058+
lib.warn('Variable \'' + key + '\' in ' + opts.name + ' could not be found!');
10401059
value = match;
10411060
}
10421061

1043-
if(numberOfHoverTemplateWarnings === maximumNumberOfHoverTemplateWarnings) {
1044-
lib.warn('Too many hovertemplate warnings - additional warnings will be suppressed');
1062+
if(opts.count === opts.max) {
1063+
lib.warn('Too many ' + opts.name + ' warnings - additional warnings will be suppressed');
10451064
}
1046-
numberOfHoverTemplateWarnings++;
1065+
opts.count++;
1066+
1067+
return match;
10471068
}
10481069

10491070
if(format) {
10501071
var fmt;
1051-
if(d3locale) {
1052-
fmt = d3locale.numberFormat;
1053-
} else {
1054-
fmt = d3.format;
1072+
if(format[0] === ':') {
1073+
fmt = d3locale ? d3locale.numberFormat : d3.format;
1074+
value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
1075+
}
1076+
1077+
if(format[0] === '|') {
1078+
fmt = d3locale ? d3locale.timeFormat.utc : d3.time.format.utc;
1079+
var ms = lib.dateTime2ms(value);
1080+
value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt);
10551081
}
1056-
value = fmt(format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''))(value);
10571082
} else {
10581083
if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label'];
10591084
}
10601085
return value;
10611086
});
1062-
};
1087+
}
10631088

10641089
/*
10651090
* alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc

Diff for: src/components/fx/hovertemplate_attributes.js renamed to src/plots/template_attributes.js

+47-8
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@
88

99
'use strict';
1010

11-
var FORMAT_LINK = require('../../constants/docs').FORMAT_LINK;
11+
var FORMAT_LINK = require('../constants/docs').FORMAT_LINK;
12+
var DATE_FORMAT_LINK = require('../constants/docs').DATE_FORMAT_LINK;
1213

13-
module.exports = function(opts, extra) {
14-
opts = opts || {};
15-
extra = extra || {};
14+
var templateFormatStringDescription = [
15+
'Variables are inserted using %{variable}, for example "y: %{y}".',
16+
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".',
17+
FORMAT_LINK,
18+
'for details on the formatting syntax.',
19+
'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".',
20+
DATE_FORMAT_LINK,
21+
'for details on the date formatting syntax.'
22+
].join(' ');
1623

24+
function describeVariables(extra) {
1725
var descPart = extra.description ? ' ' + extra.description : '';
1826
var keys = extra.keys || [];
1927
if(keys.length > 0) {
@@ -28,6 +36,14 @@ module.exports = function(opts, extra) {
2836
descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
2937
}
3038
}
39+
return descPart;
40+
}
41+
42+
exports.hovertemplateAttrs = function(opts, extra) {
43+
opts = opts || {};
44+
extra = extra || {};
45+
46+
var descPart = describeVariables(extra);
3147

3248
var hovertemplate = {
3349
valType: 'string',
@@ -37,10 +53,7 @@ module.exports = function(opts, extra) {
3753
description: [
3854
'Template string used for rendering the information that appear on hover box.',
3955
'Note that this will override `hoverinfo`.',
40-
'Variables are inserted using %{variable}, for example "y: %{y}".',
41-
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".',
42-
FORMAT_LINK,
43-
'for details on the formatting syntax.',
56+
templateFormatStringDescription,
4457
'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plot.ly/javascript/plotlyjs-events/#event-data.',
4558
'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
4659
descPart,
@@ -55,3 +68,29 @@ module.exports = function(opts, extra) {
5568

5669
return hovertemplate;
5770
};
71+
72+
exports.texttemplateAttrs = function(opts, extra) {
73+
opts = opts || {};
74+
extra = extra || {};
75+
76+
var descPart = describeVariables(extra);
77+
78+
var texttemplate = {
79+
valType: 'string',
80+
role: 'info',
81+
dflt: '',
82+
editType: opts.editType || 'calc',
83+
description: [
84+
'Template string used for rendering the information text that appear on points.',
85+
'Note that this will override `textinfo`.',
86+
templateFormatStringDescription,
87+
'Every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.',
88+
descPart
89+
].join(' ')
90+
};
91+
92+
if(opts.arrayOk !== false) {
93+
texttemplate.arrayOk = true;
94+
}
95+
return texttemplate;
96+
};

Diff for: src/traces/bar/attributes.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
'use strict';
1010

1111
var scatterAttrs = require('../scatter/attributes');
12-
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
12+
var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs;
13+
var texttemplateAttrs = require('../../plots/template_attributes').texttemplateAttrs;
1314
var colorScaleAttrs = require('../../components/colorscale/attributes');
1415
var fontAttrs = require('../../plots/font_attributes');
1516
var constants = require('./constants.js');
@@ -59,6 +60,9 @@ module.exports = {
5960
dy: scatterAttrs.dy,
6061

6162
text: scatterAttrs.text,
63+
texttemplate: texttemplateAttrs({editType: 'plot'}, {
64+
keys: constants.eventDataKeys
65+
}),
6266
hovertext: scatterAttrs.hovertext,
6367
hovertemplate: hovertemplateAttrs({}, {
6468
keys: constants.eventDataKeys

Diff for: src/traces/bar/defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ function handleText(traceIn, traceOut, layout, coerce, textposition, opts) {
155155
if(moduleHasConstrain) coerce('constraintext');
156156
if(moduleHasCliponaxis) coerce('cliponaxis');
157157
if(moduleHasTextangle) coerce('textangle');
158+
159+
coerce('texttemplate');
158160
}
159161

160162
if(hasInside) {

Diff for: src/traces/bar/plot.js

+69-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var attributes = require('./attributes');
2626
var attributeText = attributes.text;
2727
var attributeTextPosition = attributes.textposition;
2828

29+
var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue;
30+
2931
// padding in pixels around text
3032
var TEXTPAD = 3;
3133

@@ -226,7 +228,7 @@ function appendBarText(gd, plotinfo, bar, calcTrace, i, x0, x1, y0, y1, opts) {
226228
var trace = calcTrace[0].trace;
227229
var isHorizontal = (trace.orientation === 'h');
228230

229-
var text = getText(calcTrace, i, xa, ya);
231+
var text = getText(fullLayout, calcTrace, i, xa, ya);
230232
textPosition = getTextPosition(trace, i);
231233

232234
// compute text position
@@ -537,14 +539,17 @@ function getTransform(opts) {
537539
return transformTranslate + transformScale + transformRotate;
538540
}
539541

540-
function getText(calcTrace, index, xa, ya) {
542+
function getText(fullLayout, calcTrace, index, xa, ya) {
541543
var trace = calcTrace[0].trace;
544+
var texttemplate = trace.texttemplate;
542545

543546
var value;
544-
if(!trace.textinfo) {
545-
value = helpers.getValue(trace.text, index);
546-
} else {
547+
if(texttemplate) {
548+
value = calcTexttemplate(fullLayout, calcTrace, index, xa, ya);
549+
} else if(trace.textinfo) {
547550
value = calcTextinfo(calcTrace, index, xa, ya);
551+
} else {
552+
value = helpers.getValue(trace.text, index);
548553
}
549554

550555
return helpers.coerceString(attributeText, value);
@@ -555,6 +560,65 @@ function getTextPosition(trace, index) {
555560
return helpers.coerceEnumerated(attributeTextPosition, value);
556561
}
557562

563+
function calcTexttemplate(fullLayout, calcTrace, index, xa, ya) {
564+
var trace = calcTrace[0].trace;
565+
var texttemplate = Lib.castOption(trace, index, 'texttemplate');
566+
if(!texttemplate) return '';
567+
var isHorizontal = (trace.orientation === 'h');
568+
var isWaterfall = (trace.type === 'waterfall');
569+
var isFunnel = (trace.type === 'funnel');
570+
571+
function formatLabel(u) {
572+
var pAxis = isHorizontal ? ya : xa;
573+
return tickText(pAxis, u, true).text;
574+
}
575+
576+
function formatNumber(v) {
577+
var sAxis = isHorizontal ? xa : ya;
578+
return tickText(sAxis, +v, true).text;
579+
}
580+
581+
var cdi = calcTrace[index];
582+
var obj = {};
583+
584+
obj.label = cdi.p;
585+
obj.labelLabel = formatLabel(cdi.p);
586+
587+
var tx = Lib.castOption(trace, cdi.i, 'text');
588+
if(tx === 0 || tx) obj.text = tx;
589+
590+
obj.value = cdi.s;
591+
obj.valueLabel = formatNumber(cdi.s);
592+
593+
var pt = {};
594+
appendArrayPointValue(pt, trace, cdi.i);
595+
596+
if(isWaterfall) {
597+
obj.delta = +cdi.rawS || cdi.s;
598+
obj.deltaLabel = formatNumber(obj.delta);
599+
obj.final = cdi.v;
600+
obj.finalLabel = formatNumber(obj.final);
601+
obj.initial = obj.final - obj.delta;
602+
obj.initialLabel = formatNumber(obj.initial);
603+
}
604+
605+
if(isFunnel) {
606+
obj.value = cdi.s;
607+
obj.valueLabel = formatNumber(obj.value);
608+
609+
obj.percentInitial = cdi.begR;
610+
obj.percentInitialLabel = Lib.formatPercent(cdi.begR);
611+
obj.percentPrevious = cdi.difR;
612+
obj.percentPreviousLabel = Lib.formatPercent(cdi.difR);
613+
obj.percentTotal = cdi.sumR;
614+
obj.percenTotalLabel = Lib.formatPercent(cdi.sumR);
615+
}
616+
617+
var customdata = Lib.castOption(trace, cdi.i, 'customdata');
618+
if(customdata) obj.customdata = customdata;
619+
return Lib.texttemplateString(texttemplate, obj, fullLayout._d3locale, pt, obj, trace._meta || {});
620+
}
621+
558622
function calcTextinfo(calcTrace, index, xa, ya) {
559623
var trace = calcTrace[0].trace;
560624
var isHorizontal = (trace.orientation === 'h');

Diff for: src/traces/barpolar/attributes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
'use strict';
1010

11-
var hovertemplateAttrs = require('../../components/fx/hovertemplate_attributes');
11+
var hovertemplateAttrs = require('../../plots/template_attributes').hovertemplateAttrs;
1212
var extendFlat = require('../../lib/extend').extendFlat;
1313
var scatterPolarAttrs = require('../scatterpolar/attributes');
1414
var barAttrs = require('../bar/attributes');

0 commit comments

Comments
 (0)