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

No innerHTML and pseudo-html cleanup #1792

Merged
merged 37 commits into from
Jun 19, 2017
Merged

No innerHTML and pseudo-html cleanup #1792

merged 37 commits into from
Jun 19, 2017

Conversation

alexcjohnson
Copy link
Collaborator

@alexcjohnson alexcjohnson commented Jun 16, 2017

Started out as a bugfix but kind of turned into a spring cleaning...

In an effort to fix bounding box calculations in IE (one part of #1782) - which were broken by trying to use the innerHTML property of an SVG element - I wound up going through every user of Drawing.bBox and trying to make sure it was a <text> or .*-math-group node that had previously had svgTextUtils.convertToTspans called on it. Because if so, I could use the data-unformatted attribute, along with whether or not the node was converted to MathJax, in place of the innerHTML in the cache key, which is also much smaller than the innerHTML, reducing my memory concerns about this cache.

But then while doing THAT it became clear that all the monkey business using tspan.line elements to count lines and to properly align multi-line text was a disaster, and not done properly in a lot of places, so I centralized all that in two new utilities:

  • svgTextUtils.positionText - to be used instead of Drawing.setPosition, handles multiline alignment, and also gets called within convertToTspans so if you use transform instead of the x and y attributes to position the text, you never even need to use this at all.
  • svgTextUtils.lineCount - nuff said. No more hacky and/or broken selectors for counting lines of text.

So after that, almost nobody outside of svgTextUtils even needs to know about <tspan> or the .line class.

Along the way I found a few places we were not supporting or had bugs supporting pseudo-HTML but easily could, and added it: rangeselectors, updatemenus, sliders, carpet axes (titles and tick labels). And in cases that currently break with MathJax I explicitly cancel it (with the data-notex attribute). (MathJax test mocks coming in a future PR)

And while I was working in test_syntax.js I took care of #1773 as well.

There are still a few Drawing.bBox users that call it on elements larger than a <text> or MathJax group node, that at this point in this branch we will NOT cache at all. For 🐎 I need to either convert them to their base nodes or find an alternate way to give them a robust cache key:

  • colorbar/draw:473 & 490
  • titles/index:146 & 171

not a huge deal, but can improve caching
so we don't depend on callers getting this right
because now text nodes (that have been through convertToTspans) are
cacheable but larger nodes are generally not.
fallback avoids errors with mathjax in the legend, but it still doesn't
always get the sizing right because we're not handling async correctly.
that way nobody needs to know about 'tspan.line' outside of svg_text_utils
hidden elements still contribute to bounding boxes. mostly we are
careful to calculate the bbox of the visible one but not in all cases.
@etpinard etpinard added status: reviewable bug something broken labels Jun 16, 2017
Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

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

Looking great so far 👍

As of the latest commit, a few image tests are failing as well as one updatemenu jasmine test and there's one linting mistake.

Thanks for making svg_text_utils.js robust and reusable. Not having to think about tspan.line should speed up development for new components.


function showText() {
if(!parent.empty()) {
svgClass = _context.attr('class') + '-math';
parent.select('svg.' + svgClass).remove();
}
_context.text('')
.style({
visibility: 'inherit',
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Why did this line get 🔪ed ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I didn't notice the original issue #984 that led to this change... assumed it was a remnant of a much older time. My goal was to remove inline styles whenever possible, rather than setting an explicit reset value, but anyway I moved from visibility:hidden to display:none because only the latter takes the item out of the bounding box, and there are still a couple of places (most notably titles/index:146 & 171 that I'm still working on) we get the bounding box of the combined text+MathJax element. I'll see if I can come up with a test of #984 to include with this PR.


// width is given by max width of all buttons
var tWidth = text.node() && Drawing.bBox(text.node()).width,
wEff = Math.max(tWidth + constants.textPadX, constants.minWidth);

// height is determined by item text
var tHeight = menuOpts.font.size * constants.fontSizeToHeight,
tLines = tspans[0].length || 1,
tLines = tspans.size() || 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

Oops. My bad for putting these in.

textLines = textSpans[0].length || 1;
textSpans = text.selectAll('tspan.line'),
textLines = textSpans.size() || 1,
textNode = text.node();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there always only one <.legendtext> node per g? If so, we could replace the

var text = g.selectAll('.legendtext')

line with

var text = g.select('.legendtext')

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure, that's a bit cleaner -> 27b4be3

}
// standardize its position (and newline tspans if any)
d3.select(testNode)
.attr('transform', '')
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Wouldn't .attr('transform', null) be d3-esque?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, that's better -> 82ba7ee

@@ -149,8 +149,6 @@ function getFillColor(selectorLayout, d) {
function drawButtonText(button, selectorLayout, d, gd) {
function textLayout(s) {
svgTextUtils.convertToTspans(s, gd);

// TODO do we need anything else here?
Copy link
Contributor

Choose a reason for hiding this comment

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

... I guess not. 😆

tHeight = opts.font.size * 1.3,
tLines = svgTextUtils.lineCount(text);
var tWidth = text.node() && Drawing.bBox(text.node()).width;
var tHeight = opts.font.size * 1.3;
Copy link
Contributor

Choose a reason for hiding this comment

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

that 1.3 factor should be put in a constant file.

Maybe in

https://github.com/plotly/plotly.js/blob/master/src/constants/alignment.js

?

Copy link
Contributor

Choose a reason for hiding this comment

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

or maybe inside a new svg_text_utils.js method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call. It's used in a few different ways, so I went for alignment.js -> c5fa44b

// but in a static plot it's useless and it can confuse batik
// we've tried to standardize on display:none but make sure we still
// catch visibility:hidden if it ever arises
if(txt.style('visibility') === 'hidden' || txt.style('display') === 'none') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Referencing #990

@@ -214,6 +221,7 @@ function drawAxisTitle(layer, trace, t, xy, dxy, axis, xa, ya, offset, labelClas
var el = d3.select(this);

el.text(axis.title || '')
.call(svgTextUtils.convertToTspans, gd)
Copy link
Contributor

Choose a reason for hiding this comment

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

Amazing, Pinging @rreusser

logs.push(file + ' : contains .classList (IE failure)');
}
else if(lastPart === 'innerHTML') {
logs.push(file + ' : contains .innerHTML (IE failure in SVG)');
Copy link
Contributor

Choose a reason for hiding this comment

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

There might be exceptions to this rule in the future, if we end up using innerHTML on non-SVG DOM elements. Oh well, we could always filter the cases out then. We might we want to add a comment here signalling this check is too strict.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good thought. Comment added -> a68f861

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

d3.select(gd).style('visibility', 'inherit');

Plotly.plot(gd, subplotMock.data, subplotMock.layout).then(function() {

d3.select(gd).selectAll('text').each(function() {
expect(d3.select(this).style('visibility')).toEqual('visible');
expect(d3.select(this).style('display')).toEqual('block');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice touch. Thanks!

otherwise each tspan is relative to the previous, so the 3rd line `dy=2.6em`
goes 2 lines below the 2nd. Alternatively I guess we could put `dy=1.3em` on
all of them except the first but this way seems more explicit and robust
and stopped autosizing the mock, which makes it look different in the
test dashboard vs the image server
and this is fine because we're calculating them in a totally
different way in order to allow for multiline labels
multiline labels are calculating their widths correctly now
because the newline tspans are lining up rather than stair stepping
@etpinard
Copy link
Contributor

💃 after that linting mistake is fixed.

For 🐎 I need to either convert them to their base nodes or find an alternate way to give them a robust cache key:

I suppose this will wait for a future PR?

@alexcjohnson
Copy link
Collaborator Author

For 🐎 I need to either convert them to their base nodes or find an alternate way to give them a robust cache key:

I suppose this will wait for a future PR?

I'm going to take a stab at making the ones in titles cacheable for this PR, as I think that's actually a big part of the speedup in #1772 that we're losing at the moment. The ones in colorbar though I'll probably leave for later.

@alexcjohnson alexcjohnson changed the title [WIP] No innerHTML and pseudo-html cleanup No innerHTML and pseudo-html cleanup Jun 19, 2017
@@ -214,7 +214,7 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
.classed('user-select-none', true)
.attr('text-anchor', 'end')
.call(Drawing.font, menuOpts.font)
.text('▼');
.text(constants.arrowSymbol[menuOpts.direction]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice 🎉

@@ -77,6 +77,9 @@ function assertSrcContents() {
else if(lastPart === 'innerHTML') {
logs.push(file + ' : contains .innerHTML (IE failure in SVG)');
}
else if(lastPart === 'parentElement') {
logs.push(file + ' : contains .parentElement (IE failure)');
Copy link
Contributor

Choose a reason for hiding this comment

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

}
},
// multiple of fontSize to get the vertical offset between lines
LINE_SPACING: 1.3
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉 🎉 🎉 🎉 🎉

@@ -131,7 +131,7 @@ function createCamera(scene) {

if(Math.abs(dx * dydx) > Math.abs(dy)) {
result.boxEnd[1] = result.boxStart[1] +
Math.abs(dx) * dydx * (Math.sign(dy) || 1);
Math.abs(dx) * dydx * (dy >= 0 ? 1 : -1);
Copy link
Contributor

Choose a reason for hiding this comment

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

[Non-blocking comment] It might be worth making a Lib.sign method at some point.


// then merge in a few more with other component types
mock.layout.updatemenus = require('@mocks/updatemenus.json').layout.updatemenus;
mock.layout.sliders = require('@mocks/sliders.json').layout.sliders;
Copy link
Contributor

Choose a reason for hiding this comment

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

Great idea here putting all traces and components in one data/layout. Maybe we should do the same in the svg/pdf/eps image export tests

@etpinard
Copy link
Contributor

Amazing PR. 💃

Text placement was always a tricky part of adding new layout components. This PR will make that so much easier. 🎉

@alexcjohnson
Copy link
Collaborator Author

Text placement was always a tricky part of adding new layout components. This PR will make that so much easier. 🎉

I hope so! One piece that's still pretty tricky is mathjax (because you have to support async rendering, and any bbox calls need to look for the mathjax group), and I'll try to address that later. In the meantime, we should make a practice of including data-notex (as I did with existing components in this PR) anytime we call convertToTspans if it hasn't been explicitly tested with tex. That way not only will it work consistently, but we'll have an easy tag to look for when someone wants to upgrade a component with tex compatibility.

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

Successfully merging this pull request may close these issues.

2 participants