Skip to content

Commit

Permalink
Merge pull request #6527 from plotly/shape-label-templates
Browse files Browse the repository at this point in the history
Add `texttemplate` attribute to `shape.label`
  • Loading branch information
emilykl authored Apr 17, 2023
2 parents 21d09a6 + 0163281 commit b21e3db
Show file tree
Hide file tree
Showing 11 changed files with 585 additions and 49 deletions.
3 changes: 3 additions & 0 deletions src/components/shapes/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ var dash = require('../drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
var templatedArray = require('../../plot_api/plot_template').templatedArray;
var axisPlaceableObjs = require('../../constants/axis_placeable_objects');
var shapeTexttemplateAttrs = require('../../plots/template_attributes').shapeTexttemplateAttrs;
var shapeLabelTexttemplateVars = require('./label_texttemplate');

module.exports = templatedArray('shape', {
visible: {
Expand Down Expand Up @@ -232,6 +234,7 @@ module.exports = templatedArray('shape', {
editType: 'arraydraw',
description: 'Sets the text to display with shape.'
},
texttemplate: shapeTexttemplateAttrs({}, {keys: Object.keys(shapeLabelTexttemplateVars)}),
font: fontAttrs({
editType: 'calc+arraydraw',
colorEditType: 'arraydraw',
Expand Down
6 changes: 4 additions & 2 deletions src/components/shapes/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {

// Label options
var isLine = shapeType === 'line';
var labelText = coerce('label.text');
if(labelText) {
var labelTextTemplate, labelText;
if(noPath) { labelTextTemplate = coerce('label.texttemplate'); }
if(!labelTextTemplate) { labelText = coerce('label.text'); }
if(labelText || labelTextTemplate) {
coerce('label.textangle');
var labelTextPosition = coerce('label.textposition', isLine ? 'middle' : 'middle center');
coerce('label.xanchor');
Expand Down
31 changes: 26 additions & 5 deletions src/components/shapes/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var constants = require('./constants');
var helpers = require('./helpers');
var getPathString = helpers.getPathString;
var shapeLabelTexttemplateVars = require('./label_texttemplate');
var FROM_TL = require('../../constants/alignment').FROM_TL;


Expand All @@ -38,7 +39,8 @@ var FROM_TL = require('../../constants/alignment').FROM_TL;
module.exports = {
draw: draw,
drawOne: drawOne,
eraseActiveShape: eraseActiveShape
eraseActiveShape: eraseActiveShape,
drawLabel: drawLabel,
};

function draw(gd) {
Expand Down Expand Up @@ -168,7 +170,7 @@ function drawOne(gd, index) {
plotinfo: plotinfo,
gd: gd,
editHelpers: editHelpers,
hasText: options.label.text,
hasText: options.label.text || options.label.texttemplate,
isActiveShape: true // i.e. to enable controllers
};

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

// If no label, return
if(!options.label.text) return;
// If no label text or texttemplate, return
if(!(options.label.text || options.label.texttemplate)) return;

// Text template overrides text
var text;
if(options.label.texttemplate) {
var templateValues = {};
if(options.type !== 'path') {
var _xa = Axes.getFromId(gd, options.xref);
var _ya = Axes.getFromId(gd, options.yref);
for(var key in shapeLabelTexttemplateVars) {
var val = shapeLabelTexttemplateVars[key](options, _xa, _ya);
if(val !== undefined) templateValues[key] = val;
}
}
text = Lib.texttemplateStringForShapes(options.label.texttemplate,
{},
gd._fullLayout._d3locale,
templateValues);
} else {
text = options.label.text;
}

var labelGroupAttrs = {
'data-index': index,
};
var text = options.label.text;
var font = options.label.font;

var labelTextAttrs = {
Expand Down
4 changes: 4 additions & 0 deletions src/components/shapes/draw_newshape/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
var fontAttrs = require('../../../plots/font_attributes');
var dash = require('../../drawing/attributes').dash;
var extendFlat = require('../../../lib/extend').extendFlat;
var shapeTexttemplateAttrs = require('../../../plots/template_attributes').shapeTexttemplateAttrs;
var shapeLabelTexttemplateVars = require('../label_texttemplate');


module.exports = {
newshape: {
Expand Down Expand Up @@ -86,6 +89,7 @@ module.exports = {
editType: 'none',
description: 'Sets the text to display with the new shape.'
},
texttemplate: shapeTexttemplateAttrs({newshape: true, editType: 'none'}, {keys: Object.keys(shapeLabelTexttemplateVars)}),
font: fontAttrs({
editType: 'none',
description: 'Sets the new shape label text font.'
Expand Down
3 changes: 2 additions & 1 deletion src/components/shapes/draw_newshape/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ module.exports = function supplyDrawNewShapeDefaults(layoutIn, layoutOut, coerce

var isLine = layoutIn.dragmode === 'drawline';
var labelText = coerce('newshape.label.text');
if(labelText) {
var labelTextTemplate = coerce('newshape.label.texttemplate');
if(labelText || labelTextTemplate) {
coerce('newshape.label.textangle');
var labelTextPosition = coerce('newshape.label.textposition', isLine ? 'middle' : 'middle center');
coerce('newshape.label.xanchor');
Expand Down
70 changes: 70 additions & 0 deletions src/components/shapes/label_texttemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';

// Wrapper functions to handle paper-referenced shapes, which have no axis

function d2l(v, axis) {
return axis ? axis.d2l(v) : v;
}

function l2d(v, axis) {
return axis ? axis.l2d(v) : v;
}


function x0Fn(shape) { return shape.x0; }
function x1Fn(shape) { return shape.x1; }
function y0Fn(shape) { return shape.y0; }
function y1Fn(shape) { return shape.y1; }

function dxFn(shape, xa) {
return d2l(shape.x1, xa) - d2l(shape.x0, xa);
}

function dyFn(shape, xa, ya) {
return d2l(shape.y1, ya) - d2l(shape.y0, ya);
}

function widthFn(shape, xa) {
return Math.abs(dxFn(shape, xa));
}

function heightFn(shape, xa, ya) {
return Math.abs(dyFn(shape, xa, ya));
}

function lengthFn(shape, xa, ya) {
return (shape.type !== 'line') ? undefined :
Math.sqrt(
Math.pow(dxFn(shape, xa), 2) +
Math.pow(dyFn(shape, xa, ya), 2)
);
}

function xcenterFn(shape, xa) {
return l2d((d2l(shape.x1, xa) + d2l(shape.x0, xa)) / 2, xa);
}

function ycenterFn(shape, xa, ya) {
return l2d((d2l(shape.y1, ya) + d2l(shape.y0, ya)) / 2, ya);
}

function slopeFn(shape, xa, ya) {
return (shape.type !== 'line') ? undefined : (
dyFn(shape, xa, ya) / dxFn(shape, xa)
);
}

module.exports = {
x0: x0Fn,
x1: x1Fn,
y0: y0Fn,
y1: y1Fn,
slope: slopeFn,
dx: dxFn,
dy: dyFn,
width: widthFn,
height: heightFn,
length: lengthFn,
xcenter: xcenterFn,
ycenter: ycenterFn,
};
37 changes: 37 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,26 @@ lib.texttemplateString = function() {
return templateFormatString.apply(texttemplateWarnings, arguments);
};

// Regex for parsing multiplication and division operations applied to a template key
// Used for shape.label.texttemplate
// Matches a key name (non-whitespace characters), followed by a * or / character, followed by a number
// For example, the following strings are matched: `x0*2`, `slope/1.60934`, `y1*2.54`
var MULT_DIV_REGEX = /^(\S+)([\*\/])(-?\d+(\.\d+)?)$/;
function multDivParser(inputStr) {
var match = inputStr.match(MULT_DIV_REGEX);
if(match) return { key: match[1], op: match[2], number: Number(match[3]) };
return { key: inputStr, op: null, number: null };
}
var texttemplateWarningsForShapes = {
max: 10,
count: 0,
name: 'texttemplate',
parseMultDiv: true,
};
lib.texttemplateStringForShapes = function() {
return templateFormatString.apply(texttemplateWarningsForShapes, arguments);
};

var TEMPLATE_STRING_FORMAT_SEPARATOR = /^[:|\|]/;
/**
* Substitute values from an object into a string and optionally formats them using d3-format,
Expand Down Expand Up @@ -1122,6 +1142,17 @@ function templateFormatString(string, labels, d3locale) {
if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1);
if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1);

// Shape labels support * and / operators in template string
// Parse these if the parseMultDiv param is set to true
var parsedOp = null;
var parsedNumber = null;
if(opts.parseMultDiv) {
var _match = multDivParser(key);
key = _match.key;
parsedOp = _match.op;
parsedNumber = _match.number;
}

var value;
if(hasOther) {
value = labels[key];
Expand All @@ -1145,6 +1176,12 @@ function templateFormatString(string, labels, d3locale) {
}
}

// Apply mult/div operation (if applicable)
if(value !== undefined) {
if(parsedOp === '*') value *= parsedNumber;
if(parsedOp === '/') value /= parsedNumber;
}

if(value === undefined && opts) {
if(opts.count < opts.max) {
lib.warn('Variable \'' + key + '\' in ' + opts.name + ' could not be found!');
Expand Down
44 changes: 42 additions & 2 deletions src/plots/template_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ function templateFormatStringDescription(opts) {
].join(' ');
}

function shapeTemplateFormatStringDescription() {
return [
'Variables are inserted using %{variable},',
'for example "x0: %{x0}".',
'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{x0:$.2f}". See',
FORMAT_LINK,
'for details on the formatting syntax.',
'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{x0|%m %b %Y}". See',
DATE_FORMAT_LINK,
'for details on the date formatting syntax.',
'A single multiplication or division operation may be applied to numeric variables, and combined with',
'd3 number formatting, for example "Length in cm: %{x0*2.54}", "%{slope*60:.1f} meters per second."',
'For log axes, variable values are given in log units.',
'For date axes, x/y coordinate variables and center variables use datetimes, while all other variable values use values in ms.',
].join(' ');
}

function describeVariables(extra) {
var descPart = extra.description ? ' ' + extra.description : '';
var keys = extra.keys || [];
Expand All @@ -33,9 +50,9 @@ function describeVariables(extra) {
}
descPart = descPart + 'Finally, the template string has access to ';
if(keys.length === 1) {
descPart = 'variable ' + quotedKeys[0];
descPart = descPart + 'variable ' + quotedKeys[0];
} else {
descPart = 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
descPart = descPart + 'variables ' + quotedKeys.slice(0, -1).join(', ') + ' and ' + quotedKeys.slice(-1) + '.';
}
}
return descPart;
Expand Down Expand Up @@ -94,3 +111,26 @@ exports.texttemplateAttrs = function(opts, extra) {
}
return texttemplate;
};


exports.shapeTexttemplateAttrs = function(opts, extra) {
opts = opts || {};
extra = extra || {};

var newStr = opts.newshape ? 'new ' : '';

var descPart = describeVariables(extra);

var texttemplate = {
valType: 'string',
dflt: '',
editType: opts.editType || 'arraydraw',
description: [
'Template string used for rendering the ' + newStr + 'shape\'s label.',
'Note that this will override `text`.',
shapeTemplateFormatStringDescription(),
descPart,
].join(' ')
};
return texttemplate;
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b21e3db

Please sign in to comment.