Skip to content

Commit b21e3db

Browse files
authored
Merge pull request #6527 from plotly/shape-label-templates
Add `texttemplate` attribute to `shape.label`
2 parents 21d09a6 + 0163281 commit b21e3db

11 files changed

+585
-49
lines changed

src/components/shapes/attributes.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ var dash = require('../drawing/attributes').dash;
77
var extendFlat = require('../../lib/extend').extendFlat;
88
var templatedArray = require('../../plot_api/plot_template').templatedArray;
99
var axisPlaceableObjs = require('../../constants/axis_placeable_objects');
10+
var shapeTexttemplateAttrs = require('../../plots/template_attributes').shapeTexttemplateAttrs;
11+
var shapeLabelTexttemplateVars = require('./label_texttemplate');
1012

1113
module.exports = templatedArray('shape', {
1214
visible: {
@@ -232,6 +234,7 @@ module.exports = templatedArray('shape', {
232234
editType: 'arraydraw',
233235
description: 'Sets the text to display with shape.'
234236
},
237+
texttemplate: shapeTexttemplateAttrs({}, {keys: Object.keys(shapeLabelTexttemplateVars)}),
235238
font: fontAttrs({
236239
editType: 'calc+arraydraw',
237240
colorEditType: 'arraydraw',

src/components/shapes/defaults.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {
128128

129129
// Label options
130130
var isLine = shapeType === 'line';
131-
var labelText = coerce('label.text');
132-
if(labelText) {
131+
var labelTextTemplate, labelText;
132+
if(noPath) { labelTextTemplate = coerce('label.texttemplate'); }
133+
if(!labelTextTemplate) { labelText = coerce('label.text'); }
134+
if(labelText || labelTextTemplate) {
133135
coerce('label.textangle');
134136
var labelTextPosition = coerce('label.textposition', isLine ? 'middle' : 'middle center');
135137
coerce('label.xanchor');

src/components/shapes/draw.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
2323
var constants = require('./constants');
2424
var helpers = require('./helpers');
2525
var getPathString = helpers.getPathString;
26+
var shapeLabelTexttemplateVars = require('./label_texttemplate');
2627
var FROM_TL = require('../../constants/alignment').FROM_TL;
2728

2829

@@ -38,7 +39,8 @@ var FROM_TL = require('../../constants/alignment').FROM_TL;
3839
module.exports = {
3940
draw: draw,
4041
drawOne: drawOne,
41-
eraseActiveShape: eraseActiveShape
42+
eraseActiveShape: eraseActiveShape,
43+
drawLabel: drawLabel,
4244
};
4345

4446
function draw(gd) {
@@ -168,7 +170,7 @@ function drawOne(gd, index) {
168170
plotinfo: plotinfo,
169171
gd: gd,
170172
editHelpers: editHelpers,
171-
hasText: options.label.text,
173+
hasText: options.label.text || options.label.texttemplate,
172174
isActiveShape: true // i.e. to enable controllers
173175
};
174176

@@ -605,13 +607,32 @@ function drawLabel(gd, index, options, shapeGroup) {
605607
// Remove existing label
606608
shapeGroup.selectAll('.shape-label').remove();
607609

608-
// If no label, return
609-
if(!options.label.text) return;
610+
// If no label text or texttemplate, return
611+
if(!(options.label.text || options.label.texttemplate)) return;
612+
613+
// Text template overrides text
614+
var text;
615+
if(options.label.texttemplate) {
616+
var templateValues = {};
617+
if(options.type !== 'path') {
618+
var _xa = Axes.getFromId(gd, options.xref);
619+
var _ya = Axes.getFromId(gd, options.yref);
620+
for(var key in shapeLabelTexttemplateVars) {
621+
var val = shapeLabelTexttemplateVars[key](options, _xa, _ya);
622+
if(val !== undefined) templateValues[key] = val;
623+
}
624+
}
625+
text = Lib.texttemplateStringForShapes(options.label.texttemplate,
626+
{},
627+
gd._fullLayout._d3locale,
628+
templateValues);
629+
} else {
630+
text = options.label.text;
631+
}
610632

611633
var labelGroupAttrs = {
612634
'data-index': index,
613635
};
614-
var text = options.label.text;
615636
var font = options.label.font;
616637

617638
var labelTextAttrs = {

src/components/shapes/draw_newshape/attributes.js

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
var fontAttrs = require('../../../plots/font_attributes');
44
var dash = require('../../drawing/attributes').dash;
55
var extendFlat = require('../../../lib/extend').extendFlat;
6+
var shapeTexttemplateAttrs = require('../../../plots/template_attributes').shapeTexttemplateAttrs;
7+
var shapeLabelTexttemplateVars = require('../label_texttemplate');
8+
69

710
module.exports = {
811
newshape: {
@@ -86,6 +89,7 @@ module.exports = {
8689
editType: 'none',
8790
description: 'Sets the text to display with the new shape.'
8891
},
92+
texttemplate: shapeTexttemplateAttrs({newshape: true, editType: 'none'}, {keys: Object.keys(shapeLabelTexttemplateVars)}),
8993
font: fontAttrs({
9094
editType: 'none',
9195
description: 'Sets the new shape label text font.'

src/components/shapes/draw_newshape/defaults.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ module.exports = function supplyDrawNewShapeDefaults(layoutIn, layoutOut, coerce
2828

2929
var isLine = layoutIn.dragmode === 'drawline';
3030
var labelText = coerce('newshape.label.text');
31-
if(labelText) {
31+
var labelTextTemplate = coerce('newshape.label.texttemplate');
32+
if(labelText || labelTextTemplate) {
3233
coerce('newshape.label.textangle');
3334
var labelTextPosition = coerce('newshape.label.textposition', isLine ? 'middle' : 'middle center');
3435
coerce('newshape.label.xanchor');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
// Wrapper functions to handle paper-referenced shapes, which have no axis
4+
5+
function d2l(v, axis) {
6+
return axis ? axis.d2l(v) : v;
7+
}
8+
9+
function l2d(v, axis) {
10+
return axis ? axis.l2d(v) : v;
11+
}
12+
13+
14+
function x0Fn(shape) { return shape.x0; }
15+
function x1Fn(shape) { return shape.x1; }
16+
function y0Fn(shape) { return shape.y0; }
17+
function y1Fn(shape) { return shape.y1; }
18+
19+
function dxFn(shape, xa) {
20+
return d2l(shape.x1, xa) - d2l(shape.x0, xa);
21+
}
22+
23+
function dyFn(shape, xa, ya) {
24+
return d2l(shape.y1, ya) - d2l(shape.y0, ya);
25+
}
26+
27+
function widthFn(shape, xa) {
28+
return Math.abs(dxFn(shape, xa));
29+
}
30+
31+
function heightFn(shape, xa, ya) {
32+
return Math.abs(dyFn(shape, xa, ya));
33+
}
34+
35+
function lengthFn(shape, xa, ya) {
36+
return (shape.type !== 'line') ? undefined :
37+
Math.sqrt(
38+
Math.pow(dxFn(shape, xa), 2) +
39+
Math.pow(dyFn(shape, xa, ya), 2)
40+
);
41+
}
42+
43+
function xcenterFn(shape, xa) {
44+
return l2d((d2l(shape.x1, xa) + d2l(shape.x0, xa)) / 2, xa);
45+
}
46+
47+
function ycenterFn(shape, xa, ya) {
48+
return l2d((d2l(shape.y1, ya) + d2l(shape.y0, ya)) / 2, ya);
49+
}
50+
51+
function slopeFn(shape, xa, ya) {
52+
return (shape.type !== 'line') ? undefined : (
53+
dyFn(shape, xa, ya) / dxFn(shape, xa)
54+
);
55+
}
56+
57+
module.exports = {
58+
x0: x0Fn,
59+
x1: x1Fn,
60+
y0: y0Fn,
61+
y1: y1Fn,
62+
slope: slopeFn,
63+
dx: dxFn,
64+
dy: dyFn,
65+
width: widthFn,
66+
height: heightFn,
67+
length: lengthFn,
68+
xcenter: xcenterFn,
69+
ycenter: ycenterFn,
70+
};

src/lib/index.js

+37
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,26 @@ lib.texttemplateString = function() {
10741074
return templateFormatString.apply(texttemplateWarnings, arguments);
10751075
};
10761076

1077+
// Regex for parsing multiplication and division operations applied to a template key
1078+
// Used for shape.label.texttemplate
1079+
// Matches a key name (non-whitespace characters), followed by a * or / character, followed by a number
1080+
// For example, the following strings are matched: `x0*2`, `slope/1.60934`, `y1*2.54`
1081+
var MULT_DIV_REGEX = /^(\S+)([\*\/])(-?\d+(\.\d+)?)$/;
1082+
function multDivParser(inputStr) {
1083+
var match = inputStr.match(MULT_DIV_REGEX);
1084+
if(match) return { key: match[1], op: match[2], number: Number(match[3]) };
1085+
return { key: inputStr, op: null, number: null };
1086+
}
1087+
var texttemplateWarningsForShapes = {
1088+
max: 10,
1089+
count: 0,
1090+
name: 'texttemplate',
1091+
parseMultDiv: true,
1092+
};
1093+
lib.texttemplateStringForShapes = function() {
1094+
return templateFormatString.apply(texttemplateWarningsForShapes, arguments);
1095+
};
1096+
10771097
var TEMPLATE_STRING_FORMAT_SEPARATOR = /^[:|\|]/;
10781098
/**
10791099
* Substitute values from an object into a string and optionally formats them using d3-format,
@@ -1122,6 +1142,17 @@ function templateFormatString(string, labels, d3locale) {
11221142
if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1);
11231143
if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1);
11241144

1145+
// Shape labels support * and / operators in template string
1146+
// Parse these if the parseMultDiv param is set to true
1147+
var parsedOp = null;
1148+
var parsedNumber = null;
1149+
if(opts.parseMultDiv) {
1150+
var _match = multDivParser(key);
1151+
key = _match.key;
1152+
parsedOp = _match.op;
1153+
parsedNumber = _match.number;
1154+
}
1155+
11251156
var value;
11261157
if(hasOther) {
11271158
value = labels[key];
@@ -1145,6 +1176,12 @@ function templateFormatString(string, labels, d3locale) {
11451176
}
11461177
}
11471178

1179+
// Apply mult/div operation (if applicable)
1180+
if(value !== undefined) {
1181+
if(parsedOp === '*') value *= parsedNumber;
1182+
if(parsedOp === '/') value /= parsedNumber;
1183+
}
1184+
11481185
if(value === undefined && opts) {
11491186
if(opts.count < opts.max) {
11501187
lib.warn('Variable \'' + key + '\' in ' + opts.name + ' could not be found!');

src/plots/template_attributes.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ function templateFormatStringDescription(opts) {
2323
].join(' ');
2424
}
2525

26+
function shapeTemplateFormatStringDescription() {
27+
return [
28+
'Variables are inserted using %{variable},',
29+
'for example "x0: %{x0}".',
30+
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{x0:$.2f}". See',
31+
FORMAT_LINK,
32+
'for details on the formatting syntax.',
33+
'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{x0|%m %b %Y}". See',
34+
DATE_FORMAT_LINK,
35+
'for details on the date formatting syntax.',
36+
'A single multiplication or division operation may be applied to numeric variables, and combined with',
37+
'd3 number formatting, for example "Length in cm: %{x0*2.54}", "%{slope*60:.1f} meters per second."',
38+
'For log axes, variable values are given in log units.',
39+
'For date axes, x/y coordinate variables and center variables use datetimes, while all other variable values use values in ms.',
40+
].join(' ');
41+
}
42+
2643
function describeVariables(extra) {
2744
var descPart = extra.description ? ' ' + extra.description : '';
2845
var keys = extra.keys || [];
@@ -33,9 +50,9 @@ function describeVariables(extra) {
3350
}
3451
descPart = descPart + 'Finally, the template string has access to ';
3552
if(keys.length === 1) {
36-
descPart = 'variable ' + quotedKeys[0];
53+
descPart = descPart + 'variable ' + quotedKeys[0];
3754
} else {
38-
descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
55+
descPart = descPart + 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
3956
}
4057
}
4158
return descPart;
@@ -94,3 +111,26 @@ exports.texttemplateAttrs = function(opts, extra) {
94111
}
95112
return texttemplate;
96113
};
114+
115+
116+
exports.shapeTexttemplateAttrs = function(opts, extra) {
117+
opts = opts || {};
118+
extra = extra || {};
119+
120+
var newStr = opts.newshape ? 'new ' : '';
121+
122+
var descPart = describeVariables(extra);
123+
124+
var texttemplate = {
125+
valType: 'string',
126+
dflt: '',
127+
editType: opts.editType || 'arraydraw',
128+
description: [
129+
'Template string used for rendering the ' + newStr + 'shape\'s label.',
130+
'Note that this will override `text`.',
131+
shapeTemplateFormatStringDescription(),
132+
descPart,
133+
].join(' ')
134+
};
135+
return texttemplate;
136+
};
Loading

0 commit comments

Comments
 (0)