Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
7ee2e85
add attributes for text on shapes
emilykl Nov 17, 2022
932fa65
draw text, but not in the right place
emilykl Nov 17, 2022
1d0e471
draw text in correct place, when x0 and y0 are defined for shape
emilykl Jan 9, 2023
e46e1bc
update shape label api
emilykl Jan 9, 2023
60823dc
implement xanchor
emilykl Jan 10, 2023
2f8c810
add image test
emilykl Jan 10, 2023
e405293
fix xanchor bug
emilykl Jan 10, 2023
e31de9e
partial handling of re-render
emilykl Jan 10, 2023
0bcfff3
handle numeric angles
emilykl Jan 10, 2023
9972dc9
handle auto angle for lines, put label drawing code into func
emilykl Jan 10, 2023
6d5709a
nicer colors on image test
emilykl Jan 11, 2023
072edf6
fix weird behavior on pan
emilykl Jan 11, 2023
8bee979
disable mathjax rendering for shape text
emilykl Jan 11, 2023
8528561
clip shape text outside plot area
emilykl Jan 11, 2023
b4a4576
fix textangle bug for lines -- angle should always be correct now
emilykl Jan 23, 2023
97ee8c4
correctly handle position prop in most cases
emilykl Jan 23, 2023
674abd8
revert shapes demo to original
emilykl Jan 23, 2023
3743de4
handle label font
emilykl Jan 23, 2023
c3cc5ed
redraw text during shape move/resize
emilykl Jan 26, 2023
45a0564
handle yanchor for text
emilykl Jan 27, 2023
b8085c2
rename mock
emilykl Jan 27, 2023
8c0476e
handle multiline text and support text on arbitrary paths (sort of)
emilykl Jan 27, 2023
95dd9b6
fix jasmine tests... maybe
emilykl Jan 27, 2023
58e39cb
update plot-schema.json
emilykl Jan 30, 2023
488318f
add data-index attr to shape group
emilykl Jan 30, 2023
c8d20f2
update jasmine test
emilykl Jan 30, 2023
dad6ea5
add mocks for text on shapes
emilykl Jan 30, 2023
3134869
fix mocks
emilykl Jan 30, 2023
f17e218
add image baselines
emilykl Jan 30, 2023
01ef185
fix text positioning for lines
emilykl Feb 16, 2023
d8c4790
Apply suggestions from code review
emilykl Jan 31, 2023
5f39a62
rename position to textposition, fix some defaults
emilykl Feb 20, 2023
1ab9e24
fix docstrings for xanchor and yanchor
emilykl Feb 20, 2023
b9ea251
updated plot-schema.json
emilykl Feb 20, 2023
1f366a8
updated image baselines
emilykl Feb 20, 2023
f1087f3
updated plot-schema.json
emilykl Feb 21, 2023
f1570cf
fix Jasmine tests in shapes_test.js
emilykl Feb 21, 2023
baec66d
revert dist/plot-schema.json
emilykl Feb 21, 2023
d83e0cd
update textangle docstring and fix edge case
emilykl Feb 21, 2023
a91d12f
remove label dflt
emilykl Feb 21, 2023
4948d67
Update src/components/shapes/draw.js
emilykl Feb 23, 2023
a5e19d3
Update src/components/shapes/draw.js
emilykl Feb 23, 2023
e55953e
add new mock
emilykl Feb 24, 2023
44d5f08
fix text position for paths
emilykl Feb 28, 2023
5424ac5
fix mock
emilykl Feb 28, 2023
8769cff
add label attributes to newshape (broken)
emilykl Feb 28, 2023
f192a16
adjust edit types for newshape.label
archmoj Feb 28, 2023
7ca5cf8
fix syntax in zz-text_on_shapes_reversed_axes mock
archmoj Feb 28, 2023
822fe0d
add baseline
archmoj Feb 28, 2023
d50da33
fix circular dependency
archmoj Mar 1, 2023
8cec651
set newshape label editType
archmoj Mar 1, 2023
36771a8
change default xanchor and yanchor behavior
emilykl Mar 1, 2023
3f9727a
update mocks
emilykl Mar 1, 2023
9a4a121
Merge branch 'add-text-to-shapes' of https://github.com/plotly/plotly…
emilykl Mar 1, 2023
e493873
validate new mock
archmoj Mar 1, 2023
a17a315
update yanchor values and defaults in draw_newshape
emilykl Mar 1, 2023
d9bef04
Merge branch 'add-text-to-shapes' of https://github.com/plotly/plotly…
emilykl Mar 1, 2023
f54f102
input newshape label attributes
archmoj Mar 1, 2023
f642a52
draw labels on new shapes
archmoj Mar 1, 2023
dfb608e
provide missing editTypes
archmoj Mar 1, 2023
c20370a
merge shapes/defaults.js
emilykl Mar 1, 2023
feb2f38
standardize default yanchor behavior between shape and draw_newshape
emilykl Mar 1, 2023
11b3421
better docstrings for xanchor and yanchor
emilykl Mar 1, 2023
ea9493e
update image baselines
emilykl Mar 1, 2023
52463fa
update plot-schema
emilykl Mar 1, 2023
394b48f
improve label.textposition docstring
emilykl Mar 2, 2023
1325b7c
only coerce label properties if label text is given
emilykl Mar 2, 2023
63bb5d9
fix line xanchor behavior when x0 == x1
emilykl Mar 2, 2023
61e3df7
docstring update
emilykl Mar 2, 2023
40933d0
update plot-schema
emilykl Mar 2, 2023
8d35f6e
set default angle to auto for all shapes
emilykl Mar 2, 2023
d5d1927
set min padding to 0
emilykl Mar 2, 2023
c34d6de
update plot-schema
emilykl Mar 2, 2023
ffea962
docstring: specify padding is in px
emilykl Mar 6, 2023
ebe85c4
calculate textangle sin and cos only once
emilykl Mar 6, 2023
d113bcf
docstring updates
emilykl Mar 6, 2023
c703c11
simplify dfltYanchor implementation
emilykl Mar 6, 2023
468363e
change temp render logic
emilykl Mar 9, 2023
cdf2d37
update shape label while editing an editable shape
archmoj Mar 8, 2023
9eb7c90
test label behavior for draw_newshape
emilykl Mar 9, 2023
3d2c792
update docstring formatting
emilykl Mar 9, 2023
394555c
update plot-schema
emilykl Mar 9, 2023
eaf270d
add draft log
emilykl Mar 9, 2023
095561f
update draft log
emilykl Mar 9, 2023
b0b8ab1
Merge branch 'master' into add-text-to-shapes
emilykl Mar 10, 2023
d927b6a
run linter
emilykl Mar 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
correctly handle position prop in most cases
  • Loading branch information
emilykl committed Feb 20, 2023
commit 97ee8c413d8c615bbaaaa74af434353e709dd164
6 changes: 4 additions & 2 deletions src/components/shapes/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,11 @@ module.exports = templatedArray('shape', {
values: [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we need to have auto dflt and value in respect to src/components/shapes/defaults.js logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@archmoj What would be the use case for an auto value for textposition?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@archmoj What would be the use case for an auto value for textposition?

Hmm.. that's a good idea. But as far as I recall we don't have such an option for textposition in other places. So perhaps we could keep this part as is.

'top left', 'top center', 'top right',
'middle left', 'middle center', 'middle right',
'bottom left', 'bottom center', 'bottom right'
'bottom left', 'bottom center', 'bottom right',
'top start', 'top end',
'middle start', 'middle end',
'bottom start', 'bottom end',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these start|end really necessary?
If so, let's describe them in the description please.

Copy link
Contributor Author

@emilykl emilykl Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely open to suggestions here, but my thought was that corner-based positions like top right, top left etc. don't really make sense for lines because imagine e.g. a downward sloping line -- top right of the bounding box would place the label at a far-off point in space nowhere close to the line.

Even if the line started out sloping upward and so top right made sense, it could be dragged by the user into a downward-sloping position.

I agree that mixing start|end positions with top|middle|bottom is a little convoluted though. One thought I had was that maybe lines should only support 4 specific label positions: top, bottom, start and end (or potentially, top, bottom, left and right). Not sure if that would be more or less confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @alexcjohnson has thoughts here as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking about these attributes is that position refers to a single point on the shape, and [xanchor, yanchor] (plus padding) refers to a single point on the text element, and you position the text so that those two points are in the same place.

So for lines, the only position values that make sense are start|middle|end. We can set smart defaults for xanchor based on those ({start: 'left', middle: 'center', end: 'right'} so that by default for position: start|end the start or end of the text aligns with the start or end of the line, and for position: middle the text is centered on the line. Then yanchor: bottom puts the text on top of the line (ie the bottom of the text is on the line), yanchor: top puts the text under the line, and yanchor: 'middle' puts the text on top of the line. You can get other cool effects then like text preceding the line (position: start, xanchor: right, yanchor: middle) or following the line (position: end, xanchor: left, yanchor: middle)

For other shapes the first nine positions you have are good, and in those cases we can set smart defaults for both xanchor and yanchor to match, ie [xanchorDefault, yanchorDefault] = position.split(' ') so (for rects anyway) by default the text is drawn just inside whatever corner or side you specify with position. Default for all these shapes should be middle center though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this makes sense @alexcjohnson , thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson I've implemented these changes to the position property for lines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson Oh, I realized I skimmed over this part of your comment before:

For other shapes the first nine positions you have are good, and in those cases we can set smart defaults for both xanchor and yanchor to match, ie [xanchorDefault, yanchorDefault] = position.split(' ') so (for rects anyway) by default the text is drawn just inside whatever corner or side you specify with position.

I've implemented the exact opposite -- by default the text is drawn outside the corner, if xanchor and yanchor are not specified. That feels more intuitive to me, and I think it generally looks better, so wanted to verify that that's okay. If it needs to be the other way for consistency with other parts of the API, I can change it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it depends what we consider the default or most common use case for labels on shapes. I feel like if I was using a shape to label a particular region of a graph - an important x or y range, typically - I'd likely want the label within that region, but off in a corner so it avoids most of the data in that region.

I guess that really only applies to rectangles, for circles or paths being outside the region would be better as otherwise the label would often overlap the edge of the shape, and there are other use cases even for rects where an outside label would make more sense. Perhaps though even then it would make sense for xanchor to be within the bounds and just yanchor to be outside? So for example textposition="top left" would default to xanchor="left", yanchor="bottom"?

Also: can we handle this logic at the defaults stage instead of the draw stage? If we do that, the choices we made show up in fullLayout which can help some users understand and fix unwanted behavior. And at that point I don't see a use for "auto" in these attributes (which in turn means they can probably stop inheriting from annotations)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I guess xanchor on lines with textposition='start'|'end' still needs 'auto' because it depends whether the start is toward the left or right, and we can't know that unless we also know the axis range it's on. That doesn't apply to yanchor though, I still don't see a reason to let that be 'auto'.

But a little more logic around this perhaps, just for lines: If textangle='auto' I would expect the text to be along the line by default, rather than extending off the end. Whereas for any other textangle I'd expect the text to extend off the end, since if it tried to go along the line it would often overlap the line.

Copy link
Contributor Author

@emilykl emilykl Mar 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson I've updated xanchor and yanchor defaults so they reflect the following behavior:

  • yanchor:
    • Default for lines is bottom
    • Default for all other shapes is such that the label is positioned inside the shape bounding box (e.g. if textposition is top right, yanchor is top.
    • auto is not a supported value for yanchor
  • xanchor default is always auto. This has the following behavior at render:
    • For lines:
      • If textangle is auto (parallel to line) xanchor is set so that text is inside the line bounding box
      • If textangle is anything else, xanchor is set in the opposite direction, so that text is outside the line bounding box
    • For other shapes: xanchor is set such that the label is positioned inside the shape bounding box (e.g. if textposition is top right, xanchor is right

This mostly works pretty nicely. The only cases that are not ideal are circles and paths -- since the text is positioned inside the bounding box, it overlaps the circle/path in an odd way for any position besides middle center.

We could potentially position the text outside the bounding box by default for circles and paths... but not sure whether that's worth the additional complexity.

],
dflt: 'top left',
editType: 'arraydraw',
description: 'Sets the position of the label relative to she shape.'
},
Expand Down
3 changes: 2 additions & 1 deletion src/components/shapes/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,6 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) {
coerce('label.xanchor');
coerce('label.yanchor');
coerce('label.textangle', shapeType === 'line' ? 'auto' : 0);

coerce('label.position', '');
coerce('label.padding');
}
66 changes: 63 additions & 3 deletions src/components/shapes/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,10 @@ function drawLabel(gd, options, labelGroupAttrs, shapeGroup) {
var shapey0 = y2p(options.y0);
var shapey1 = y2p(options.y1);

// TODO: Calculate correct (x,y) based on 'position' param
var textx = shapex0;
var texty = shapey0;
// Calculate correct (x,y) for text
var textPos = calcTextPosition(shapex0, shapey0, shapex1, shapey1, options);
var textx = textPos.textx;
var texty = textPos.texty;

var textangle = options.label.textangle;

Expand Down Expand Up @@ -667,6 +668,65 @@ function calcTextAngle(shapex0, shapey0, shapex1, shapey1) {
return -180 / Math.PI * Math.atan2(dy, dx);
}

function calcTextPosition(shapex0, shapey0, shapex1, shapey1, shapeOptions) {
var textPosition = shapeOptions.label.position;
var textPadding = shapeOptions.label.padding;
var shapeType = shapeOptions.type;
var textAngle = shapeOptions.label.textangle;

var textx, texty;

// Text position functions differently for lines vs. other shapes
if(shapeType === 'line') {
// Handle 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 paddingX, paddingY;
if(textAngle === 'auto') {
var textAngleRad = Math.PI / 180 * calcTextAngle(shapex0, shapey0, shapex1, shapey1);
paddingX = textPadding * Math.sin(textAngleRad);
paddingY = -textPadding * Math.cos(textAngleRad);
} else {
paddingX = textPadding;
paddingY = textPadding;
}

// Handle directional offset for top vs. bottom vs. center of line (default is 'top')
var paddingMultiplier = textPosition.indexOf('middle') !== -1 ? 0 : textPosition.indexOf('bottom') !== -1 ? -1 : 1;

if(textPosition.indexOf('start') !== -1) {
textx = shapex0 + paddingX * paddingMultiplier;
texty = shapey0 + paddingY * paddingMultiplier;
} else if(textPosition.indexOf('end') !== -1) {
textx = shapex1 + paddingX * paddingMultiplier;
texty = shapey1 + paddingY * paddingMultiplier;
} else { // Default: center
textx = (shapex0 + shapex1) / 2 + paddingX * paddingMultiplier;
texty = (shapey0 + shapey1) / 2 + paddingY * paddingMultiplier;
}
} else { // Text position for shapes that are not lines
// calc horizontal position
if(textPosition.indexOf('top') !== -1) {
textx = Math.max(shapex0, shapex1) + textPadding;
} else if(textPosition.indexOf('bottom') !== -1) {
textx = Math.min(shapex0, shapex1) - textPadding;
} else { // Default: center
textx = (shapex0 + shapex1) / 2;
}

// calc vertical position
if(textPosition.indexOf('top') !== -1) {
texty = Math.min(shapey0, shapey1) - textPadding;
} else if(textPosition.indexOf('bottom') !== -1) {
texty = Math.max(shapey0, shapey1) + textPadding;
} else { // Default: middle
texty = (shapey0 + shapey1) / 2;
}
}

return { textx: textx, texty: texty };
}

function movePath(pathIn, moveX, moveY) {
return pathIn.replace(constants.segmentRE, function(segment) {
var paramNumber = 0;
Expand Down
7 changes: 5 additions & 2 deletions test/image/mocks/x-text_on_shapes.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
"margin": {"l":20,"r":20,"pad":0},
"showlegend":false,
"shapes":[
{"label":{"text":"anchored right, <br>angle 45", "xanchor":"right", "textangle":45}, "layer":"below","xref":"paper","yref":"paper","x0":0.5,"x1":0.6,"y0":0.4,"y1":0.6, "editable": true},
{"label":{"text":"anchored right, <br>angle 45", "xanchor":"right", "textangle":45, "position": "top right", "padding": 20}, "layer":"below","xref":"paper","yref":"paper","x0":0.5,"x1":0.6,"y0":0.4,"y1":0.6, "editable": true},
{"label":{"text":"anchored left"}, "xref":"paper","yref":"paper","type":"circle","x0":0.3,"x1":0.35,"y0":0.2,"y1":0.4, "editable": true},
{"label":{"text":"anchored right", "xanchor":"right"}, "xref":"paper","yref":"paper","type":"line","x0":0.8,"x1":0.9,"y0":0.8,"y1":0.9, "editable": true},
{"label":{"text":"anchored right <br> two lines", "xanchor":"right", "padding": 10, "position": "top center"}, "xref":"paper","yref":"paper","type":"line","x0":0.8,"x1":0.9,"y0":0.8,"y1":0.9, "editable": true},
{"label":{"text":"anchored center, angle -30", "xanchor":"center", "textangle":-30}, "layer":"below","x0":2,"x1":3,"y0":7,"y1":9.5,"opacity":0.5,"fillcolor":"#05e","line":{"width":3,"color":"#025","dash":"dashdot"}, "editable": true},
{"label":{"text":"anchored left"}, "yref":"paper","type":"line","x0":1.1,"x1":2.4,"y0":0.1,"y1":0.4,"line":{"color":"#039","dash":"dot","width":2}, "editable": true},
{"label":{"text":"anchored center", "xanchor":"center"}, "xref":"paper","x0":0.8,"x1":0.9,"y0":1,"y1":3,"fillcolor":"#ccc", "editable": true}
]
},
"config": {
"editable": false
}
}