Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b21e3db

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

11 files changed

+585
-49
lines changed

‎src/components/shapes/attributes.js

Copy file name to clipboardExpand all lines: src/components/shapes/attributes.js
+3Lines changed: 3 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/components/shapes/defaults.js
+4-2Lines changed: 4 additions & 2 deletions
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

Copy file name to clipboardExpand all lines: src/components/shapes/draw.js
+26-5Lines changed: 26 additions & 5 deletions
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

Copy file name to clipboardExpand all lines: src/components/shapes/draw_newshape/attributes.js
+4Lines changed: 4 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/components/shapes/draw_newshape/defaults.js
+2-1Lines changed: 2 additions & 1 deletion
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');
+70Lines changed: 70 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/lib/index.js
+37Lines changed: 37 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/plots/template_attributes.js
+42-2Lines changed: 42 additions & 2 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.