-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Render shape label while drawing #6608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+358
−308
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
'use strict'; | ||
|
||
var Lib = require('../../lib'); | ||
var Axes = require('../../plots/cartesian/axes'); | ||
var svgTextUtils = require('../../lib/svg_text_utils'); | ||
|
||
var Drawing = require('../drawing'); | ||
|
||
var readPaths = require('./draw_newshape/helpers').readPaths; | ||
var helpers = require('./helpers'); | ||
var getPathString = helpers.getPathString; | ||
var shapeLabelTexttemplateVars = require('./label_texttemplate'); | ||
|
||
var FROM_TL = require('../../constants/alignment').FROM_TL; | ||
|
||
|
||
module.exports = function drawLabel(gd, index, options, shapeGroup) { | ||
// Remove existing label | ||
shapeGroup.selectAll('.shape-label').remove(); | ||
|
||
// 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 font = options.label.font; | ||
|
||
var labelTextAttrs = { | ||
'data-notex': 1 | ||
}; | ||
|
||
var labelGroup = shapeGroup.append('g') | ||
.attr(labelGroupAttrs) | ||
.classed('shape-label', true); | ||
var labelText = labelGroup.append('text') | ||
.attr(labelTextAttrs) | ||
.classed('shape-label-text', true) | ||
.text(text); | ||
|
||
// Get x and y bounds of shape | ||
var shapex0, shapex1, shapey0, shapey1; | ||
if(options.path) { | ||
// If shape is defined as a path, get the | ||
// min and max bounds across all polygons in path | ||
var d = getPathString(gd, options); | ||
var polygons = readPaths(d, gd); | ||
shapex0 = Infinity; | ||
shapey0 = Infinity; | ||
shapex1 = -Infinity; | ||
shapey1 = -Infinity; | ||
for(var i = 0; i < polygons.length; i++) { | ||
for(var j = 0; j < polygons[i].length; j++) { | ||
var p = polygons[i][j]; | ||
for(var k = 1; k < p.length; k += 2) { | ||
var _x = p[k]; | ||
var _y = p[k + 1]; | ||
|
||
shapex0 = Math.min(shapex0, _x); | ||
shapex1 = Math.max(shapex1, _x); | ||
shapey0 = Math.min(shapey0, _y); | ||
shapey1 = Math.max(shapey1, _y); | ||
} | ||
} | ||
} | ||
} else { | ||
// Otherwise, we use the x and y bounds defined in the shape options | ||
// and convert them to pixel coordinates | ||
// Setup conversion functions | ||
var xa = Axes.getFromId(gd, options.xref); | ||
var xRefType = Axes.getRefType(options.xref); | ||
var ya = Axes.getFromId(gd, options.yref); | ||
var yRefType = Axes.getRefType(options.yref); | ||
var x2p = helpers.getDataToPixel(gd, xa, false, xRefType); | ||
var y2p = helpers.getDataToPixel(gd, ya, true, yRefType); | ||
shapex0 = x2p(options.x0); | ||
shapex1 = x2p(options.x1); | ||
shapey0 = y2p(options.y0); | ||
shapey1 = y2p(options.y1); | ||
} | ||
|
||
// Handle `auto` angle | ||
var textangle = options.label.textangle; | ||
if(textangle === 'auto') { | ||
if(options.type === 'line') { | ||
// Auto angle for line is same angle as line | ||
textangle = calcTextAngle(shapex0, shapey0, shapex1, shapey1); | ||
} else { | ||
// Auto angle for all other shapes is 0 | ||
textangle = 0; | ||
} | ||
} | ||
|
||
// Do an initial render so we can get the text bounding box height | ||
labelText.call(function(s) { | ||
s.call(Drawing.font, font).attr({}); | ||
svgTextUtils.convertToTspans(s, gd); | ||
return s; | ||
}); | ||
var textBB = Drawing.bBox(labelText.node()); | ||
|
||
// Calculate correct (x,y) for text | ||
// We also determine true xanchor since xanchor depends on position when set to 'auto' | ||
var textPos = calcTextPosition(shapex0, shapey0, shapex1, shapey1, options, textangle, textBB); | ||
var textx = textPos.textx; | ||
var texty = textPos.texty; | ||
var xanchor = textPos.xanchor; | ||
|
||
// Update (x,y) position, xanchor, and angle | ||
labelText.attr({ | ||
'text-anchor': { | ||
left: 'start', | ||
center: 'middle', | ||
right: 'end' | ||
}[xanchor], | ||
y: texty, | ||
x: textx, | ||
transform: 'rotate(' + textangle + ',' + textx + ',' + texty + ')' | ||
}).call(svgTextUtils.positionText, textx, texty); | ||
}; | ||
|
||
function calcTextAngle(shapex0, shapey0, shapex1, shapey1) { | ||
var dy, dx; | ||
dx = Math.abs(shapex1 - shapex0); | ||
if(shapex1 >= shapex0) { | ||
dy = shapey0 - shapey1; | ||
} else { | ||
dy = shapey1 - shapey0; | ||
} | ||
return -180 / Math.PI * Math.atan2(dy, dx); | ||
} | ||
|
||
function calcTextPosition(shapex0, shapey0, shapex1, shapey1, shapeOptions, actualTextAngle, textBB) { | ||
var textPosition = shapeOptions.label.textposition; | ||
var textAngle = shapeOptions.label.textangle; | ||
var textPadding = shapeOptions.label.padding; | ||
var shapeType = shapeOptions.type; | ||
var textAngleRad = Math.PI / 180 * actualTextAngle; | ||
var sinA = Math.sin(textAngleRad); | ||
var cosA = Math.cos(textAngleRad); | ||
var xanchor = shapeOptions.label.xanchor; | ||
var yanchor = shapeOptions.label.yanchor; | ||
|
||
var textx, texty, paddingX, paddingY; | ||
|
||
// Text position functions differently for lines vs. other shapes | ||
if(shapeType === 'line') { | ||
// Set base position for start vs. center vs. end of line (default is 'center') | ||
if(textPosition === 'start') { | ||
textx = shapex0; | ||
texty = shapey0; | ||
} else if(textPosition === 'end') { | ||
textx = shapex1; | ||
texty = shapey1; | ||
} else { // Default: center | ||
textx = (shapex0 + shapex1) / 2; | ||
texty = (shapey0 + shapey1) / 2; | ||
} | ||
|
||
// Set xanchor if xanchor is 'auto' | ||
if(xanchor === 'auto') { | ||
if(textPosition === 'start') { | ||
if(textAngle === 'auto') { | ||
if(shapex1 > shapex0) xanchor = 'left'; | ||
else if(shapex1 < shapex0) xanchor = 'right'; | ||
else xanchor = 'center'; | ||
} else { | ||
if(shapex1 > shapex0) xanchor = 'right'; | ||
else if(shapex1 < shapex0) xanchor = 'left'; | ||
else xanchor = 'center'; | ||
} | ||
} else if(textPosition === 'end') { | ||
if(textAngle === 'auto') { | ||
if(shapex1 > shapex0) xanchor = 'right'; | ||
else if(shapex1 < shapex0) xanchor = 'left'; | ||
else xanchor = 'center'; | ||
} else { | ||
if(shapex1 > shapex0) xanchor = 'left'; | ||
else if(shapex1 < shapex0) xanchor = 'right'; | ||
else xanchor = 'center'; | ||
} | ||
} else { | ||
xanchor = 'center'; | ||
} | ||
} | ||
|
||
// Special case for padding when angle is 'auto' for lines | ||
// Padding should be treated as an orthogonal offset in this case | ||
// Otherwise, padding is just a simple x and y offset | ||
var paddingConstantsX = { left: 1, center: 0, right: -1 }; | ||
var paddingConstantsY = { bottom: -1, middle: 0, top: 1 }; | ||
if(textAngle === 'auto') { | ||
// Set direction to apply padding (based on `yanchor` only) | ||
var paddingDirection = paddingConstantsY[yanchor]; | ||
paddingX = -textPadding * sinA * paddingDirection; | ||
paddingY = textPadding * cosA * paddingDirection; | ||
} else { | ||
// Set direction to apply padding (based on `xanchor` and `yanchor`) | ||
var paddingDirectionX = paddingConstantsX[xanchor]; | ||
var paddingDirectionY = paddingConstantsY[yanchor]; | ||
paddingX = textPadding * paddingDirectionX; | ||
paddingY = textPadding * paddingDirectionY; | ||
} | ||
textx = textx + paddingX; | ||
texty = texty + paddingY; | ||
} else { | ||
// Text position for shapes that are not lines | ||
// calc horizontal position | ||
// Horizontal needs a little extra padding to look balanced | ||
paddingX = textPadding + 3; | ||
if(textPosition.indexOf('right') !== -1) { | ||
textx = Math.max(shapex0, shapex1) - paddingX; | ||
if(xanchor === 'auto') xanchor = 'right'; | ||
} else if(textPosition.indexOf('left') !== -1) { | ||
textx = Math.min(shapex0, shapex1) + paddingX; | ||
if(xanchor === 'auto') xanchor = 'left'; | ||
} else { // Default: center | ||
textx = (shapex0 + shapex1) / 2; | ||
if(xanchor === 'auto') xanchor = 'center'; | ||
} | ||
|
||
// calc vertical position | ||
if(textPosition.indexOf('top') !== -1) { | ||
texty = Math.min(shapey0, shapey1); | ||
} else if(textPosition.indexOf('bottom') !== -1) { | ||
texty = Math.max(shapey0, shapey1); | ||
} else { | ||
texty = (shapey0 + shapey1) / 2; | ||
} | ||
// Apply padding | ||
paddingY = textPadding; | ||
if(yanchor === 'bottom') { | ||
texty = texty - paddingY; | ||
} else if(yanchor === 'top') { | ||
texty = texty + paddingY; | ||
} | ||
} | ||
|
||
// Shift vertical (& horizontal) position according to `yanchor` | ||
var shiftFraction = FROM_TL[yanchor]; | ||
// Adjust so that text is anchored at top of first line rather than at baseline of first line | ||
var baselineAdjust = shapeOptions.label.font.size; | ||
var textHeight = textBB.height; | ||
var xshift = (textHeight * shiftFraction - baselineAdjust) * sinA; | ||
var yshift = -(textHeight * shiftFraction - baselineAdjust) * cosA; | ||
|
||
return { textx: textx + xshift, texty: texty + yshift, xanchor: xanchor }; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just the
drawLabels()
function which used to be inshapes/draw.js
being moved into its own file (in order to call it elsewhere without circular imports). No code changes.