Skip to content
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

Add texttemplate attribute to shape.label #6527

Merged
merged 29 commits into from
Apr 17, 2023
Merged

Conversation

emilykl
Copy link
Contributor

@emilykl emilykl commented Mar 15, 2023

Resolves #6511

Add texttemplate attribute to shape.label and newshape.label.

texttemplate supports insertion of shape variables using "%{variable}" syntax, with d3 number and date formatting. A single multiplication or division operation is also supported, to allow for unit conversions. Shape label displays correct values for variables used in texttemplate, and update values live as shape is moved or resized.

Supported variables:

  • Raw variables (from shape definition):
    • x0
    • x1
    • y0
    • y1
      Calculated variables:
    • xcenter (calculated as (x0+x1)/2)
    • ycenter (calculated as (y0+y1)/2)
    • dx (calculated as x1-x0)
    • dy (calculated as y1-y0)
    • width (calculated as abs(x1-x0))
    • height (calculated as abs(y1-y0))
    • length (calculated as sqrt(dx^2+dy^2)) -- for lines only
    • slope (calculated as (y1-y0)/(x1-x0))

Behavior for log, date, and category axes:

  • Raw variables use raw data values from shape definition (datetimes for date axes and strings for category axes)
  • Calculated variables use linearized values (log values for log axes, milliseconds for date axes, integers for category axes)
    • xcenter and ycenter are converted back to data values after calculation

This PR does not cover rendering of the shape text during mousedown of the initial draw -- due to additional complexity, that will be covered in a separate issue, captured by #6540.

API

  • shape.label.texttemplate: Template string used for rendering the new shape's label. Note that this will override text. 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 https://github.com/d3/d3-format/tree/v1.4.5#d3-format for details on the formatting syntax.

    Dates are formatted using d3-time-format's syntax "%{variable|d3-time-format}", for example "Day: %{x0|%A}". See: https://github.com/d3/d3-time-format/tree/v2.2.3#locale_format

    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.

    Finally, the template string has access to variables x0, x1, y0, y1, slope, dx, dy, width, height, length, xcenter and ycenter.

Partnership

Development of this feature is sponsored by Volkswagen's Center of Excellence for Battery Systems.

@emilykl
Copy link
Contributor Author

emilykl commented Mar 15, 2023

Currently working for "easy" cases. Edge cases I haven't tackled yet:

  • Paths (no x0/x1/y0/y1)
  • Shapes where the coordinates are datetime strings
  • Divide by zero errors
  • "slope" for circles and rectangles -- should we allow it?

@emilykl
Copy link
Contributor Author

emilykl commented Mar 15, 2023

Here is a codepen demonstrating the current behavior: https://codepen.io/emily_plotly/pen/eYLrKNV

@alexcjohnson
Copy link
Collaborator

Nice! If it's easy there are a few more that occur to me might be useful. We can leave them for later too though:

  • dx===x1-x0
  • dy===y1-y0
  • length===sqrt(dx^2+dy^2)
  • xcenter===(x0+x1)/2 or maybe just x
  • ycenter===(y0+y1)/2 or y
  • Paths (no x0/x1/y0/y1)

Let's not support paths for now.

  • Shapes where the coordinates are datetime strings

Seems like we should handle absolute and relative differently here: for {x|y}{0|1} we should use the date directly, but support d3 datetime format specifiers. For slope (and, later, if we add dx, dy, or length) it should use milliseconds.

Makes me think about log axes though - seems like if you've drawn a line on a log axis the only meaningful slope is also in log units. The use cases I've seen for this:

  • log/linear plots: the slope of a line is the decay constant (or time constant if the linear axis is time)
  • log/log plots: the slope is the power law relationship between x and y - linear has slope 1, quadratic has slope 2, etc.

So ax.d2l is your friend here - will convert to "linearized numeric" format which is milliseconds for dates and log base 10 for log axes. So users who want natural log will need to get comfortable with Math.LN10 but that should be fine.

  • Divide by zero errors

JS doesn't have those 😉

> 1/0
Infinity
> 0/0
NaN

If the result is we just show those strings with no further formatting, that's fine IMO

  • "slope" for circles and rectangles -- should we allow it?

I guess it's the aspect ratio for those shapes. Odd use case but probably easy to just allow it.

@emilykl
Copy link
Contributor Author

emilykl commented Mar 15, 2023

Thanks @alexcjohnson !

  • dx===x1-x0
  • dy===y1-y0
  • length===sqrt(dx^2+dy^2)
  • xcenter===(x0+x1)/2 or maybe just x
  • ycenter===(y0+y1)/2 or y

These should all be pretty easy. For the first two, would width and height be better names than dx and dy?

Let's not support paths for now.

Fine with me. How should we handle the case where a user sets a texttemplate for a path? Not display the text at all? Display the template string unaltered? Display undefined for the variables?

@alexcjohnson
Copy link
Collaborator

For the first two, would width and height be better names than dx and dy?

yes 😁

How should we handle the case where a user sets a texttemplate for a path?

Do whatever we do today if you make a texttemplate or hovertemplate in another context that has invalid fields.

@archmoj
Copy link
Contributor

archmoj commented Mar 16, 2023

Nice! If it's easy there are a few more that occur to me might be useful. We can leave them for later too though:

  • dx===x1-x0
  • dy===y1-y0
  • length===sqrt(dx^2+dy^2)
  • xcenter===(x0+x1)/2 or maybe just x
  • ycenter===(y0+y1)/2 or y

Instead of length why not using centroid or cen?
That would also have a meaning for paths.

On another note, perhaps atan2((x1-x0)/(y1-y0)) converted to degrees would also be very useful.
It doesn't have problem with Infinity as well as big and small numbers.

Also concerning dx and dy shouldn't the absolute be printed?
Or perhaps we could add xlen and ylen.

@alexcjohnson
Copy link
Collaborator

Ah interesting - width and height would be only positive but dx and dy would be signed. So perhaps there’s room for all of those.

angle is interesting, sure. I’ll note that both angle and length are meaningful only on a space with a well-defined metric, ie x and y have matching units, and they’ll look funny if you don’t enforce scaleratio = 1. There would also be cases where they might seem correct but they aren’t, like if you plot latitude and longitude on cartesian axes rather than a map. Not a reason to not do them, just a note of caution we might include in our docs.

Let’s stay away from paths please. It’s going to be a rabbit hole dealing with straight lines, arcs, and two orders of Bézier curves, plus open and closed paths, and nobody has asked for it. If and when we do that we can add centroid or whatever, but no need to tailor the fields that apply to simple shapes to try and match paths, it’ll just make the normal cases more confusing.

@archmoj archmoj added the feature something new label Mar 16, 2023
@emilykl
Copy link
Contributor Author

emilykl commented Mar 16, 2023

Sure, I can see an argument for having both dy/dx (signed) and width/height (unsigned).

Is there ever an instance where the x and y axes are swapped, i.e. y is horizontal and x is vertical? That would be weird but just double-checking whether width and height variable names make sense in all cases.

I can't think of a use-case for angle that isn't affected by the issues @alexcjohnson mentioned so I'd argue against it for now.

@JulianWgs
Copy link

Very cool discussion going on!

@alexcjohnson

Seems like we should handle absolute and relative differently here: for {x|y}{0|1} we should use the date directly, but support d3 datetime format specifiers. For slope (and, later, if we add dx, dy, or length) it should use milliseconds.

Why would you opt for milliseconds? I know that in many programming language including JavaScript milliseconds is the default timedelta unit. However in Python it is the much human friendly second 😄 Would it add too much complexity to have different units for plotly.js and plotly.py or just use seconds all together? Also since it is only for displaying we dont encounter floating point arithmetic issues.

@emilykl Could you please update the pull request description, whenever we finalized the set of supported variables. Also it might be worthwhile to split the pull request into the common and not so common cases, but I dont have a strong opinion about this 😄 This may include log axis support, since although the use case you describe seams really elegant I dont know how often this is actually is needed.

I would prefer xcenter, ycenter, because many programs dont anchor their shapes in the center, so just x and y might be confusing.

@emilykl
Copy link
Contributor Author

emilykl commented Mar 20, 2023

@JulianWgs Thank you, great to have your feedback.

For the final list of variables, @archmoj are you okay with the following? If there's any dispute about any of these I'd argue for leaving them out of this PR rather than prolonging the discussion, since it's trivial to add more variables later.

  • x0
  • x1
  • y0
  • y1
  • xcenter (calculated as (x0+x1)/2)
  • ycenter (calculated as (y0+y1)/2)
  • dx (calculated as x1-x0)
  • dy (calculated as y1-y0)
  • width (calculated as abs(x1-x0))
  • height (calculated as abs(y1-y0))
  • length (calculated as sqrt(dx^2+dy^2))
  • slope (calculated as (y1-y0)/(x1-x0))

Regarding time units, I believe Plotly.js uses milliseconds internally for datetime representations, and using a different unit only for shape labels has the potential to create some confusion -- I will verify internally though whether there's precedent for this question.

@archmoj @alexcjohnson Would it be reasonable to exclude slope calculations for log axes for the moment? Unless you think the correct slope is obvious in all cases and it's a very simple implementation.

@archmoj
Copy link
Contributor

archmoj commented Mar 21, 2023

Let's leave log axes for now.
IMHO width & height could confusing as they may point to pixel coordinate.
I'd also avoid length as it could be mixed with the length of arrays in JavaScript. So could we use len instead?
Also for center we could use cen or mid.
So here is my suggestion:

  • x0
  • x1
  • y0
  • y1
  • xmid (calculated as (x0+x1)/2)
  • ymid (calculated as (y0+y1)/2)
  • dx (calculated as x1-x0)
  • dy (calculated as y1-y0)
  • xlen (calculated as abs(x1-x0))
  • ylen (calculated as abs(y1-y0))
  • len (calculated as sqrt(dx^2+dy^2))
  • slope (calculated as (y1-y0)/(x1-x0))

Finally instead of slope could we use tan?
If so, then we could similarly add atan for the angle of inclination.

@alexcjohnson
Copy link
Collaborator

Let’s keep slope, and if we add angle let’s just call it angle, even though if you stretch the axes (or invert them!) the angle on-screen will be different (the same can be said of slope, though typically a slope is given with units so this is clearer).

Personally I like width, height, and length, I think if I were not reading the docs but just guessing those are among the first I would guess whereas len would take a while and I might never guess xlen and ylen. But I feel super strongly about that, with docs all of those names work.

To me, the behavior on log axes is clear (one unit of difference on a log scale is one power of ten in the data, again the same as our internal representation), but since nobody is requesting it right now we can leave it for later.

Re: milliseconds - we have a bunch of places in plotlyjs where time differences are already specified in milliseconds, so I don’t think it’s a good idea to change just this one. At some point we could try to add a units field somewhere, then people could specify seconds, days, or whatever. But absent that I think we need to leave it as milliseconds.

@JulianWgs
Copy link

I would always prefer English words over mathematical or software expressions (but for example dx and dy are fine), but this might be due to my Python background.

len(array) in Python gives you the length of an array, so there will always be confusion depending on background, so I prefer length. Although distance might be a good fit as well.

Slope is a must for our use case.

@emilykl Everything else on your list is fine for me as well. I haven't checked the code yet. Have you started the implementation? If we need further discussion on naming I would propose not to wait for the discussion to end, but start implementing as soon as possible :)

@emilykl
Copy link
Contributor Author

emilykl commented Mar 21, 2023

@JulianWgs They are almost all implemented -- I should be able to finish the remaining ones today ;)

@archmoj
Copy link
Contributor

archmoj commented Apr 3, 2023

@emilykl the PR is looking good now.
In your tests, would you possibly add/modify a subplot to use category axes as well?
Also it would be great to have a paper referenced shape in the mock.

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

// Text template overrides text
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let defaults do the overriding - ie don't coerce text if there's a texttemplate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

and even better - don't try to coerce texttemplate if type==='path' - then you don't need the if(options.type !== 'path') below :)

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 see the comment below about new shapes - you may be right that the if below is still needed, but it'll still be better if we only coerce what's needed.

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 Changed the coercion in defaults.js but I believe we still need the if(texttemplate) statement (see updated code) -- correct me if wrong!

@emilykl emilykl force-pushed the shape-label-templates branch from f949a5d to 66a12de Compare April 13, 2023 21:50
"editable": true,
"path": "M3.71,0.75L2.26,3.99L3.71,3.99Z",
"layer": "above"
},
Copy link
Contributor

@archmoj archmoj Apr 14, 2023

Choose a reason for hiding this comment

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

@emilykl
Copy link
Contributor Author

emilykl commented Apr 14, 2023

@alexcjohnson @archmoj I think this PR is finally ready! I've addressed all above conversations -- let me know if there's anything additional remaining.

I added a section in the PR description to explain the behavior for log, date, and category axes, and I also added a category axis and a paper-referenced shape to the shape texttemplates mock.

@archmoj
Copy link
Contributor

archmoj commented Apr 17, 2023

All is looking ⭐ good ⭐ to me.
@alexcjohnson would you like to have a final look?

@archmoj archmoj requested a review from alexcjohnson April 17, 2023 12:46
Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

💃 Fantastic work @emilykl!

@emilykl
Copy link
Contributor Author

emilykl commented Apr 17, 2023

🎉 🎉 Thanks @archmoj and @alexcjohnson! 🎉 🎉

@emilykl emilykl merged commit b21e3db into master Apr 17, 2023
@alexcjohnson alexcjohnson deleted the shape-label-templates branch April 17, 2023 19:44
@mosaikme
Copy link

mosaikme commented Nov 8, 2023

Hey thanks for this Future, i try to have something that can calculate percentage distance, so simply draw a rectangle, than ( (y0 / y1) -1 ) *100 , can we intigrate something like this, or enable to do math direct in the texttemplate. Or did somebody know of a other way for it?

@alexcjohnson
Copy link
Collaborator

@mosaikme arbitrary math was not implemented but was discussed in the original issue #6511 (comment) - feel free to open a new issue to request this, or even better a PR to make it happen 😉

@mosaikme
Copy link

mosaikme commented Dec 24, 2023

ok , sad. I will try to add this. But where to start? Would it be enough to edit or add percent calculation here https://github.com/plotly/plotly.js/blob/master/src/components/shapes/label_texttemplate.js as a new function , and than add it the module.export?

x0
x1
y0
y1
xmid (calculated as (x0+x1)/2)
ymid (calculated as (y0+y1)/2)
dx (calculated as x1-x0)
dy (calculated as y1-y0)
xlen (calculated as abs(x1-x0))
ylen (calculated as abs(y1-y0))
len (calculated as sqrt(dx^2+dy^2))
slope (calculated as (y1-y0)/(x1-x0))

ADD THIS: ?? 
xlenPCT (calculated as (x1 / x0)  ) ||  so then we could do rest of the math in the texttamplete , like "x0: %{(xlenPCT -1) * 100}"
ylenPCT (calculated as (y1 / y0)  ) ||  so then we could do rest of the math in the texttamplete , like "x0: %{(ylenPCT -1) * 100}"

Or even better would be allowing for example : "xPCT: %{ ( (x0 / x1) -1 ) * 100}"   this would give us so much freedom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support text templates for shape labels
5 participants